2025年6月18日水曜日

RoboBrain2.0をAPEXのアプリケーションから呼び出し画像の説明を求める

北京智源人工智能研究院(The Beijing Academy of Artificial Intelligence - BAAI)がリリースしている画像や動画をソースとして推論を行うLLM - RoboBrain2.0を、APEXアプリケーションから呼び出してみました。

RoboBrain2.0については、以下のサイトで紹介されています。

RoboBrain2.0: See Better. Think Harder. Do Smarter.

Hugging FaceにはLM Studioで実行できるGGUF形式のモデルがありますが、与えた画像とLLMが返す座標が一致しないため、RoboBrain2.0のGitHubリポジトリに含まれるSimpleInferenceクラス(inference.pyに含まれる)を呼び出すFastAPIのサーバーを実行し、それをAPEXアプリケーションから呼び出しています。

作成したAPEXアプリケーションは以下のように動作します。


プロンプトには「ミャクミャク」と入力していますが、Taskとしてgroundingを選択しているためLLMに以下のプロンプトが渡されます。
Please provide the bounding box coordinate of the region this sentence describes: ミャクミャク.
Enable Thinkingにチェックを入れているので推論の過程も返されます。以下はthinkingとして返された英語を、日本語に翻訳した内容です。

視覚的な情報から、このアート作品は、大きな青い胴体を持ち、一般的に「ミスター・ポテトヘッド」として知られるキャラクターに似ていることで明確に識別できます。青い体と赤い風船が組み合わさっており、視覚的に非常に印象的で記憶に残るデザインとなっています。さらに、足元、もしくは階段のように見える部分も青く縁取られており、彫刻全体のデザインをより際立たせています。

このタスクでは、画像内でこのアート作品を特定し、その位置を見つけ出す必要があります。作品は黒と赤のプラットフォーム上に目立って配置されており、地面とのコントラストがはっきりしているため、その存在を明確に認識できます。彫刻自体はかなり高い台座の上に置かれており、独特で目を引くデザインによって、見る者の視線を上方へと導きます。

鮮やかな色使い、精緻なデザイン、台座上の配置といった視覚的手がかりを踏まえると、このアート作品は大きな青い「ミスター・ポテトヘッド」風のフィギュアを中心に構成されていることは明らかです。この分析は、指定された座標情報とも一致しており、この彫刻がシーンの中心的存在であることを裏付けています。したがって、視覚的観察と空間的推論に基づいた結論として、このアート作品は「大きな青い『ミスター・ポテトヘッド』風の彫刻」であると断定できます。

「ミャクミャク」は認識していませんが、「大きな青い『ミスター・ポテトヘッド』風の彫刻」を囲む矩形の座標が返されています。

実際には、temperatureとして0.7を指定しているせいか、または、FastAPIでSimpleInferenceを呼び出しているせいか、レスポンスが安定しません。レスポンスに含まれる推論や座標が途中で切れたり、推論中に例外が発生したりします。そのため実用性はまったくありませんが、「こんなレスポンスを返すLLMがある」ということを知ることはできます。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-robobrain2-apex-app.zip

macOSのpodman上に作成しているAPEX環境にインポートして実行しています。

以下よりRoboBrain2.0を呼び出す作業を紹介します。

最初にRoboBrain2.0による推論を呼び出すFastAPIのサーバーを実行します。uvを使います。

プロジェクトのディレクトリとしてRoboBrainを作成し、作成後に移動します。

uv init -p 3.10 RoboBrain
cd RoboBrain

% uv init -p 3.10 RoboBrain

Initialized project `robobrain` at `/Users/ynakakoshi/Documents/RoboBrain`

% cd RoboBrain

RoboBrain % 


RoboBrain2.0のGitHubリポジトリよりinference.pyを取得し、このディレクトリに配置します。


inference.pyの93行目に以下の記述があります。

inputs = inputs.to("cuda")

本記事はmacOSで作業をしているためNVIDIA GPUの指定であるcudaを、以下のようにApple Siliconの指定であるmpsに変更しています。

inputs = inputs.to("mps")

cudaの代わりにcpuを指定することにより、NVIDIA GPUおよびApple Silicon以外でも動作するかもしれませんが、確認はできていません(Apple SiliconのMacでcpuを指定すると、mpsを使ってください、というエラーが発生するため)。

次に以下のmain.pyをダウンロードします。SimpleInferenceクラスによる推論を呼び出すFastAPIのサーバーです。全てClaude Sonnet 4に書いてもらいました。

import os
import base64
import tempfile
from typing import Optional, Union, List
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import uvicorn
# inference.pyからSimpleInferenceクラスをインポート
from inference import SimpleInference
# グローバルにモデルインスタンスを保持
model_instance = None
@asynccontextmanager
async def lifespan(app: FastAPI):
# スタートアップ
global model_instance
print("Initializing model...")
try:
model_instance = SimpleInference("BAAI/RoboBrain2.0-7B")
print("Model initialized successfully")
except Exception as e:
print(f"Failed to initialize model: {str(e)}")
raise e
yield
# シャットダウン(必要に応じて)
# モデルのクリーンアップなどをここに記述
# FastAPIアプリケーションの初期化
app = FastAPI(
title="SimpleInference API",
version="1.0.0",
lifespan=lifespan
)
class InferenceRequest(BaseModel):
text: str
image: Union[str, List[str]] = Field(..., description="base64エンコードされた画像文字列")
task: str = "general"
enable_thinking: bool = True
do_sample: bool = True
temperature: float = 0.7
class InferenceResponse(BaseModel):
thinking: str
answer: str
def base64_to_temp_file(base64_string: str, suffix: str = ".jpg") -> str:
"""
base64文字列を一時ファイルに変換
Args:
base64_string: base64エンコードされた画像文字列
suffix: ファイル拡張子
Returns:
一時ファイルのパス
"""
try:
# base64文字列にdata:image/jpeg;base64,のようなプレフィックスがある場合は削除
if "," in base64_string:
base64_string = base64_string.split(",")[1]
# base64文字列の長さをチェック(デバッグ用)
print(f"Processing base64 string of length: {len(base64_string)}")
# base64をデコード
try:
image_data = base64.b64decode(base64_string)
print(f"Decoded image data size: {len(image_data)} bytes")
except Exception as decode_error:
raise HTTPException(
status_code=400,
detail=f"Invalid base64 encoding: {str(decode_error)}"
)
# 一時ファイルを作成
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
temp_file.write(image_data)
temp_file.close()
return temp_file.name
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to process image data: {str(e)}")
@app.get("/")
async def root():
"""
ヘルスチェック用エンドポイント
"""
return {"message": "SimpleInference API is running", "status": "healthy"}
@app.post("/inference", response_model=InferenceResponse)
async def inference_endpoint(request: InferenceRequest):
"""
推論実行エンドポイント
Args:
request: 推論リクエスト
Returns:
推論結果
"""
if model_instance is None:
raise HTTPException(status_code=503, detail="Model not initialized")
temp_files = []
try:
# リクエストサイズのログ出力
if isinstance(request.image, str):
print(f"Received single image, base64 length: {len(request.image)}")
image_paths = [base64_to_temp_file(request.image)]
else:
total_length = sum(len(img) for img in request.image)
print(f"Received {len(request.image)} images, total base64 length: {total_length}")
image_paths = [base64_to_temp_file(img) for img in request.image]
temp_files.extend(image_paths)
# 推論実行
result = model_instance.inference(
text=request.text,
image=image_paths,
task=request.task,
plot=False, # 常にFalseに設定
enable_thinking=request.enable_thinking,
do_sample=request.do_sample,
temperature=request.temperature
)
return InferenceResponse(
thinking=result["thinking"],
answer=result["answer"]
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Inference failed: {str(e)}")
finally:
# 一時ファイルをクリーンアップ
for temp_file in temp_files:
try:
if os.path.exists(temp_file):
os.unlink(temp_file)
except Exception as e:
print(f"Failed to delete temp file {temp_file}: {str(e)}")
if __name__ == "__main__":
import uvicorn
# サーバー起動(大きなリクエストに対応する設定)
uvicorn.run(
app,
host="0.0.0.0",
port=8000,
reload=False, # プロダクション環境ではFalseに設定
# 大きなリクエストボディを許可する設定
limit_max_requests=1000,
limit_concurrency=100,
# HTTPリクエストのサイズ制限を大幅に増加
h11_max_incomplete_event_size=100 * 1024 * 1024, # 100MB
)
view raw main.py hosted with ❤ by GitHub

pyproject.tomlを以下に置き換えます。

[project]
name = "robobrain"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"accelerate==1.7.0",
"annotated-types==0.7.0",
"anyio==4.9.0",
"av==14.4.0",
"certifi==2025.6.15",
"charset-normalizer==3.4.2",
"click==8.2.1",
"exceptiongroup==1.3.0",
"fastapi==0.115.12",
"filelock==3.18.0",
"fsspec==2025.5.1",
"h11==0.16.0",
"hf-xet==1.1.4",
"huggingface-hub==0.33.0",
"idna==3.10",
"jinja2==3.1.6",
"markupsafe==3.0.2",
"mpmath==1.3.0",
"networkx==3.4.2",
"numpy==2.2.6",
"opencv-python==4.11.0.86",
"packaging==25.0",
"pillow==11.2.1",
"psutil==7.0.0",
"pydantic==2.11.7",
"pydantic-core==2.33.2",
"pyyaml==6.0.2",
"qwen-vl-utils==0.0.11",
"regex==2024.11.6",
"requests==2.32.4",
"safetensors==0.5.3",
"setuptools==80.9.0",
"sniffio==1.3.1",
"starlette==0.46.2",
"sympy==1.14.0",
"tokenizers==0.21.1",
"torch==2.7.1",
"torchvision==0.22.1",
"tqdm==4.67.1",
"transformers==4.52.4",
"typing-extensions==4.14.0",
"typing-inspection==0.4.1",
"urllib3==2.4.0",
"uvicorn==0.34.3",
]
view raw pyproject.toml hosted with ❤ by GitHub
依存関係をインストールします。

uv sync

RoboBrain % uv sync

Using CPython 3.10.15 interpreter at: /opt/homebrew/opt/python@3.10/bin/python3.10

Creating virtual environment at: .venv

Resolved 61 packages in 162ms

Installed 44 packages in 372ms

 + accelerate==1.7.0

 + annotated-types==0.7.0

 + anyio==4.9.0

 + av==14.4.0

 + certifi==2025.6.15

 + charset-normalizer==3.4.2

 + click==8.2.1

 + exceptiongroup==1.3.0

 + fastapi==0.115.12

 + filelock==3.18.0

 + fsspec==2025.5.1

 + h11==0.16.0

 + hf-xet==1.1.4

 + huggingface-hub==0.33.0

 + idna==3.10

 + jinja2==3.1.6

 + markupsafe==3.0.2

 + mpmath==1.3.0

 + networkx==3.4.2

 + numpy==2.2.6

 + opencv-python==4.11.0.86

 + packaging==25.0

 + pillow==11.2.1

 + psutil==7.0.0

 + pydantic==2.11.7

 + pydantic-core==2.33.2

 + pyyaml==6.0.2

 + qwen-vl-utils==0.0.11

 + regex==2024.11.6

 + requests==2.32.4

 + safetensors==0.5.3

 + setuptools==80.9.0

 + sniffio==1.3.1

 + starlette==0.46.2

 + sympy==1.14.0

 + tokenizers==0.21.1

 + torch==2.7.1

 + torchvision==0.22.1

 + tqdm==4.67.1

 + transformers==4.52.4

 + typing-extensions==4.14.0

 + typing-inspection==0.4.1

 + urllib3==2.4.0

 + uvicorn==0.34.3

RoboBrain % 


FastAPIサーバーを実行します。Hugging Faceよりモデルをダウンロードするため、初回実行時は起動までに時間がかかります。

uv run main.py

RoboBrain % uv run main.py

INFO:     Started server process [29096]

INFO:     Waiting for application startup.

Initializing model...

Loading Checkpoint ...

Loading checkpoint shards: 100%|█████████████████████| 4/4 [00:07<00:00,  1.96s/it]

Using a slow image processor as `use_fast` is unset and a slow processor was saved with this model. `use_fast=True` will be the default behavior in v4.52, even if the model was saved with a slow processor. This will result in minor differences in outputs. You'll still be able to use a slow processor with `use_fast=False`.

You have video processor config saved in `preprocessor.json` file which is deprecated. Video processor configs should be saved in their own `video_preprocessor.json` file. You can rename the file or load and save the processor back which renames it automatically. Loading from `preprocessor.json` will be removed in v5.0.

Model initialized successfully

INFO:     Application startup complete.

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)



Uvicornがhttp://0.0.0.0:8000で実行されているとメッセージが表示されれば、RoboBrain2.0を呼び出せる状態です。

作成したAPEXアプリケーションについて説明します。

Image Fileをクリックし、推論の対象とする画像ファイルを選択します。画像ファイルを選択した後にボタンSet Imageをクリックします。画像ファイルが大きい場合(縦横で1000pxを超えるようなサイズ)は、推論を実行する際にエラーが発生するようです。そのため、画像が大きい場合は、あらかじめリサイズしておきます。


ボタンSet Imageをクリックすると、以下のPL/SQLコードが実行されます。APEX_APPLICATION_TEMP_FILESにアップロードされた画像ファイル(BLOB)をAPEXコレクションIMAGEに保存しています。

declare
l_blob blob;
l_mime_type varchar2(4000);
begin
select blob_content, mime_type into l_blob, l_mime_type
from apex_application_temp_files where name = :P1_IMAGE_FILE;
/*
* アップロードしたイメージはつねにN001=1で取得できる。
* これがページ・アイテムP1_IMAGEのソースとなる。
*/
apex_collection.create_or_truncate_collection('IMAGE');
apex_collection.add_member(
p_collection_name => 'IMAGE'
,p_n001 => 1
,p_c001 => l_mime_type
,p_blob001 => l_blob
);
end;
view raw set-image.sql hosted with ❤ by GitHub

APEXコレクションに保存された画像は、ページの左半分に表示されます。


Promptに文字列の入力、Enable Thinkingのチェック、Taskの選択を行い、ボタンAskをクリックします。

ボタンAskのクリックでREST APIを経由して、SimpleInferenceクラスのファンクションinferenceが呼び出されます。inferenceの定義は以下になっています。

def inference(self, text:str, image: Union[list,str], task="general", plot=False, enable_thinking=True, do_sample=True, temperature=0.7):

Taskで選択した値が引数taskに渡されます。plotにTrueを与えるとSimpleInferenceクラスではパッケージcv2を使って画像に描画を行います。APEXではWeb上での描画に置き換えるため、plotは常にFalseにします。

ボタンAskをクリックすると、以下のPL/SQLコードが実行されます。

declare
/* Local LM Studio */
C_ENDPOINT constant varchar2(4000) := 'http://host.containers.internal:8000/inference';
/*
* /inferenceは以下のリクエストを受け付ける。
{
"text": "What is shown in this image?",
"image": image_base64,
"task": "general",
"plot": False,
"enable_thinking": True,
"do_sample": True,
"temperature": 0.7
}
*/
/* 送信するメッセージ */
l_request json_object_t;
l_request_clob clob;
l_mime_type apex_application_temp_files.mime_type%type;
l_blob_content apex_application_temp_files.blob_content%type;
l_text clob;
l_image clob;
/* 受信するメッセージ */
l_response clob;
l_response_json json_object_t;
e_call_api_failed exception;
l_choices json_array_t;
l_choice json_object_t;
l_response_message clob;
begin
/* 写真の取り出し */
select blob001, c001 into l_blob_content, l_mime_type
from apex_collections where collection_name = 'IMAGE' and n001 = 1;
/* リクエストの準備 */
l_request := json_object_t();
l_text := :P1_PROMPT;
l_image := 'data:' || l_mime_type || ';base64,'
|| apex_web_service.blob2clobbase64(l_blob_content, 'N', 'N');
l_request.put('text', l_text);
l_request.put('image', l_image);
l_request.put('task', :P1_TASK);
l_request.put('plot', false);
l_request.put('enable_thinking', (:P1_ENABLE_THINKING = 'Y'));
l_request.put('do_sample', true);
l_request.put('temperature', 0.7);
l_request_clob := l_request.to_clob();
/*
* FastAPIサーバーの呼び出し。
*/
apex_web_service.clear_request_headers();
apex_web_service.set_request_headers('Content-Type', 'application/json');
l_response := apex_web_service.make_rest_request(
p_url => C_ENDPOINT
,p_http_method => 'POST'
,p_body => l_request_clob
);
/* process response */
if not apex_web_service.g_status_code between 200 and 300 then
raise e_call_api_failed;
end if;
:P1_RESPONSE := l_response;
end;
view raw ask-fastapi.sql hosted with ❤ by GitHub

RoboBrain2.0のレスポンスはResponseに表示されます。レスポンスはJSONドキュメントで、属性thinkingに推論、属性answerに座標が返されます。


Taskとしてpointingを選択して推論しているため、answerには点の位置となるx,y座標が配列として返されます。

ボタンRenderをクリックすると、左の画像の上にanswerで返された点を描画します。


「オカピのツノ」がプロンプトですが、尖っている耳とお尻に点が表示されています。


意図した結果ではないかもしれませんが、thinkingに推論の過程が説明されているため、なぜそうなったのかを確認できます。

ボタンRenderをクリックすると、以下のJavaScriptコードを実行します。

/*
* 応答のJSONからanswer属性として返されている座標を取り出す。
*/
const response = JSON.parse(apex.item("P1_RESPONSE").getValue());
let positions = response.answer;
if (! positions ) {
console.log('No position found.');
return;
};
apex.debug.info(positions);
/*
* 画像が表示されているIMG要素を取り出す。
*/
const img = document.getElementById("P1_IMAGE");
/*
* 選択されているタスクを取り出す。
*/
const task = apex.item("P1_TASK").getValue();
render(positions, img, task);
view raw render.js hosted with ❤ by GitHub

ファンクションrenderは静的アプリケーション・ファイルとして作成したapp.jsに記述しています。Taskpointingであれば点、trajectoryであれば折れ線、affordanceまたはgroundingであれば矩形を表示します。それぞれの図形は、画像にDIV要素を重ね合わせて表示しています。

function render(positions, img, task) {
/*
* IMG要素の親要素に当たるDIV要素を取り出す。
*
* これは選択しているアイテム・テンプレートより <div class="t-Form-itemWrapper"></div>になることを
* 想定している。
* APEXが生成するHTMLの内部構造を直接操作しているため、推奨される方法ではない。
*/
const parentDiv = img.parentElement;
parentDiv.style.position = 'relative';
/*
* IMG要素の表示されている画像の元々のサイズを取り出す。
*/
const naturalWidth = img.naturalWidth;
const naturalHeight = img.naturalHeight;
apex.debug.info(`actual image size: naturalWidth: ${naturalWidth}, naturalHeight: ${naturalHeight}`);
/*
* taskに応じた描画を行う。
*
* pointingは点を描画。
* trajectoryは折れ線。
* grounding, affordanceは矩形。
*/
if ( task === "pointing" ) {
/*
* (x, y)の配列を取り出す。
*/
const jsonStr = positions.replace(/\(/g, '[').replace(/\)/g, ']');
const points = JSON.parse(jsonStr);
// 点の描画を繰り返す。
for (let i = 0; i < points.length; i++ ) {
const [x, y] = points[i];
const percentX = 100 * ( x / naturalWidth );
const percentY = 100 * ( y / naturalHeight );
const pointElement = document.createElement("div");
pointElement.className = "point";
apex.debug.info(`point plotted on x: ${percentX}%, y: ${percentY}%`);
Object.assign(pointElement.style, {
left: percentX + '%',
top: percentY + '%'
});
parentDiv.appendChild(pointElement);
}
}
if ( task === "trajectory" ) {
/*
* (x, y)の配列を取り出す。
*/
const jsonStr = positions.replace(/\(/g, '[').replace(/\)/g, ']');
const points = JSON.parse(jsonStr);
// 連続する点の間に線を描画する。
for (let i = 0; i < points.length - 1; i++) {
const [x0, y0] = points[i];
const [x1, y1] = points[i + 1];
// 画像のサイズに対する百分率で座標を計算
const percentX0 = 100 * ( x0 / naturalWidth );
const percentY0 = 100 * ( y0 / naturalHeight );
const deltaX = (x1 - x0);
const deltaY = (y1 - y0);
// 線の長さを計算
const lengthPx = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const length = 100 * (lengthPx / naturalWidth);
// 線の角度を計算(ラジアンから度に変換)
const angle = Math.atan2( deltaY, deltaX ) * 180 / Math.PI;
// DIV要素で線を作成
const lineElement = document.createElement("div");
lineElement.className = "line-div";
apex.debug.info(`line plotted on left: ${percentX0}%, top: ${percentY0}%, width: ${length}%, transform: rotate${angle}deg`);
Object.assign(lineElement.style, {
left: percentX0 + '%',
top: percentY0 + '%',
width: length + '%',
transform: `rotate(${angle}deg)`, // 角度を適用
});
parentDiv.appendChild(lineElement);
}
}
if ( task === "affordance" || task === "grounding" ) {
/*
* [x0, y0, x1, y1]の形式の矩形を取り出す。
*/
const coords = JSON.parse(positions);
apex.debug.info(`bounding box in px left: ${coords[0]}px, top: ${coords[1]}px, right: ${coords[2]}px, bottom: ${coords[3]}px`);
const percentLeft = 100 * (coords[0] / naturalWidth);
const percentTop = 100 * (coords[1] / naturalHeight);
const percentWidth = 100 * ((coords[2] - coords[0]) / naturalWidth);
const percentHeight = 100 * ((coords[3] - coords[1]) / naturalHeight);
// 描画する矩形を定義する。
const boxElement = document.createElement("div");
boxElement.className = "bounding-box";
apex.debug.info(`bounding box plotted on left: ${percentLeft}%, top: ${percentTop}%, width: ${percentWidth}%, height: ${percentHeight}%` );
Object.assign(boxElement.style, {
left: percentLeft + '%',
top: percentTop + '%',
width: percentWidth + '%',
height: percentHeight + '%',
});
parentDiv.appendChild(boxElement);
}
}
view raw app.js hosted with ❤ by GitHub

APEXアプリケーションの実装についての説明は以上です。

RoboBrain2.0のGitHubリポジトリには、RoboBrain2.0の紹介に使用しているJPEGファイルが含まれています。


trajectory.jpgをダウンロードして、RoboBrain2.0の紹介と同じプロンプトを与えて呼び出してみます。


5. Usage for Trajectory Prediction (Embodied)にあたる処理で、プロンプトはreach for the banana on the plate、Enable ThinkingはTrue、Taskはtrajectoryです。


temperatureが0.7なので同じレスポンスにはなりませんが、近い結果になります。

適切な座標値がとれませんが、LM Studioのローカル・サーバーにロードしたRoboBrain2.0を呼び出すページも作成しています。


動作確認のためにロードしたモデルは、bartowski/BAAI_RoboBrain2.0-7B-GGUFの量子化タイプがQ6_K_Lのものです。


ボタンAskをクリックしたときに実行するコードは以下です。レスポンスをJSONで返すようにプロンプトを変更しています。

declare
/* Local LM Studio */
C_ENDPOINT constant varchar2(4000) := 'http://host.containers.internal:8080/v1/chat/completions';
/* LLM model name */
C_MODEL constant varchar2(200) := 'baai_robobrain2.0-7b@q6_k_l';
/* 写真に関する変数 */
l_image json_object_t;
l_image_url json_object_t;
l_mime_type apex_application_temp_files.mime_type%type;
l_blob_content apex_application_temp_files.blob_content%type;
/* 送信するメッセージ */
l_prompt json_object_t;
l_text clob;
l_content json_array_t;
l_message json_object_t;
l_messages json_array_t;
l_request json_object_t;
l_request_clob clob;
/* 受信するメッセージ */
l_response clob;
l_response_json json_object_t;
e_call_api_failed exception;
l_choices json_array_t;
l_choice json_object_t;
l_response_message clob;
/* PROMPT */
PROMPT_POINTING constant varchar2(4000) := q'~
%s.
Your answer should be formatted as a list of tuples, i.e. [(x1, y1), (x2, y2), ...],
where each tuple contains the x and y coordinates of a point satisfying the conditions above.
The coordinates should indicate the normalized pixel locations of the points in the image.
The response should be in JSON format as follows:
```json
{
"thinking": "the reasoning behind your answer.",
"answer": "a list of tuples, i.e. [(x1, y1), (x2, y2), ...], where each tuple contains the x and y coordinates of a point."
}
```
~';
PROMPT_AFFORDANCE constant varchar2(4000) := q'~
You are a robot using the joint control. The task is "%s".
Please predict a possible affordance area of the end effector.
The response should be in JSON format as follows:
```json
{
"thinking": "the reasoning behind your answer.",
"answer": "i.e. [x0, y0, x1, y1], where x0 is left, y0 is top, x1 is bottom, y1 is right coordinates of a bounding box."
}
```
~';
PROMPT_TRAJECTORY constant varchar2(4000) := q'~
You are a robot using the joint control. The task is "%s".
Please predict up to 10 key trajectory points to complete the task.
The response should be in JSON format as follows:
```json
{
"thinking": "the reasoning behind your answer.",
"answer": "a list of tuples, i.e. [(x1, y1), (x2, y2), ...], where each tuple contains the x and y coordinates of a point."
}
```
~';
PROMPT_GROUNDING constant varchar2(4000) := q'~
Please provide the bounding box coordinate of the region this sentence describes: %s.
The response should be in JSON format as follows:
```json
{
"thinking": "the reasoning behind your answer.",
"answer": "i.e. [x0, y0, x1, y1], where x0 is left, y0 is top, x1 is bottom, y1 is right coordinates of a bounding box."
}
```
~';
begin
/* 写真の取り出し */
select blob001, c001 into l_blob_content, l_mime_type
from apex_collections where collection_name = 'IMAGE' and n001 = 1;
/* イメージの準備 - base64で送信する */
l_image_url := json_object_t();
/*
* LM Studioではimage_urlオブジェクトに直接base64の文字列を入れるのは不可で、
* urlオブジェクトにbase64の文字列を入れて、image_urlに与える必要がある。
*/
apex_debug.info('mime_type = %s', l_mime_type);
l_image_url.put('url', 'data:' || l_mime_type || ';base64,'
|| apex_web_service.blob2clobbase64(l_blob_content, 'N', 'N')
);
l_image := json_object_t();
l_image.put('type', 'image_url');
l_image.put('image_url', l_image_url);
/* プロンプトの準備 */
l_prompt := json_object_t();
l_prompt.put('type', 'text');
/* タスクの選択に従って、プロントプトを拡張する。 */
if :P2_TASK = 'pointing' then
l_text := apex_string.format(PROMPT_POINTING, :P2_PROMPT);
elsif :P2_TASK = 'affordance' then
l_text := apex_string.format(PROMPT_AFFORDANCE, :P2_PROMPT);
elsif :P2_TASK = 'trajectory' then
l_text := apex_string.format(PROMPT_TRAJECTORY, :P2_PROMPT);
elsif :P2_TASK = 'grounding' then
l_text := apex_string.format(PROMPT_GROUNDING, :P2_PROMPT);
else
apex_debug.info('General Task.');
l_text := :P2_PROMPT;
end if;
/*
* Reasoningを有効にする。
*/
if :P2_ENABLE_THINKING = 'Y' then
l_text := '# Reasoning ' || CHR(10) || l_text;
end if;
apex_debug.info('Prompt: %s', l_text);
l_prompt.put('text', l_text);
/* contentの作成 */
l_content := json_array_t();
l_content.append(l_prompt);
l_content.append(l_image);
/* messageの作成 - systemプロンプトは無し, userの単発のみ */
l_message := json_object_t();
l_message.put('role', 'user');
l_message.put('content', l_content);
/* messagesの作成 */
l_messages := json_array_t();
l_messages.append(l_message);
/* requestの作成 */
l_request := json_object_t();
l_request.put('model', C_MODEL);
l_request.put('messages', l_messages);
-- l_request.put('max_tokens', p_max_tokens);
/* call OpenAI chat completions api */
l_request_clob := l_request.to_clob();
apex_web_service.clear_request_headers();
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_ENDPOINT
,p_http_method => 'POST'
,p_body => l_request_clob
);
apex_debug.info('Response: %s', l_response);
/* process response */
if not apex_web_service.g_status_code between 200 and 300 then
raise e_call_api_failed;
end if;
l_response_json := json_object_t(l_response);
l_choices := l_response_json.get_array('choices');
l_response_message := '';
for i in 1..l_choices.get_size()
loop
l_choice := treat(l_choices.get(i-1) as json_object_t);
l_message := l_choice.get_object('message');
l_response_message := l_response_message || l_message.get_clob('content');
end loop;
:P2_RESPONSE := l_response_message;
end;
view raw ask.sql hosted with ❤ by GitHub
LM StudioではFastAPIサーバーのように推論中に例外が発生することはありませんが、answerで返される座標が画像にうまく合いません。


推論を読むとプロンプトは認識されているので、LM Studioが行なっている画像の前処理の影響だろうと予想されます。

視覚入力には、2人の人物が互いに近くに座っている様子が映し出されています。1人は赤い帽子をかぶっており、バナナを食べているもう1人の人物と何らかの活動に関わっているように見えます。赤い帽子はその鮮やかな色のために目立ち、衣服の一部として容易に識別できます。このタスクでは、その人物が身につけている特定のアイテムを識別することが求められています。シーンを観察すると、私は赤い帽子をかぶった人物に注目し、視野内ではっきりと帽子をかぶっていることを確認します。この分析により、赤い帽子が識別対象の物体であると結論づけられます。

LM Studioで動かすことができればよいのですが、解決方法を見つけることができなかったので、現状は残念です。

今回の記事は以上になります。