はじめに:AI VTuber「牡丹プロジェクト」とは
本記事は、AI VTuber三姉妹(Kasho、牡丹、ユリ)の記憶製造機システムの技術解説シリーズ第2弾です。
プロジェクト概要
「牡丹プロジェクト」は、過去の記憶を持つAI VTuberを実現するプロジェクトです。三姉妹それぞれが固有の記憶・個性・価値観を持ち、配信中に画像を理解して反応するVLM機能を実装しました。
三姉妹の構成
- Kasho(長女): 論理的・分析的、慎重でリスク重視、保護者的な姉
- 牡丹(次女): ギャル系、感情的・直感的、明るく率直、行動力抜群
- ユリ(三女): 統合的・洞察的、調整役、共感力が高い
GitHubリポジトリ
本プロジェクトのコードは以下で公開しています:
- リポジトリ: https://github.com/koshikawa-masato/AI-Vtuber-Project
- 主要機能: 記憶生成システム、三姉妹決議システム、LangSmith統合、VLM対応
Phase 2: VLM統合の重要性
テキスト生成だけでなく、画像を理解して説明できるAIを実装したい。そんなニーズに応えるのがVLM (Vision Language Model) です。
本記事では、GPT-4oとGeminiを使ってVLM機能を実装し、LangSmithでトレーシングする方法を紹介します。
🎯 この記事で分かること
- VLM (Vision Language Model) とは何か
- GPT-4o Visionの統合方法
- Gemini Visionの統合方法(エラー対処含む)
- LangSmithでVLM呼び出しをトレーシング
- 実際のベンチマーク結果と性能比較
📦 対象モデル
- OpenAI GPT-4o: マルチモーダル対応の最新モデル
- Google Gemini 2.5 Flash: 高速マルチモーダルモデル
VLM (Vision Language Model) とは
VLM (Vision Language Model) は、テキストと画像を同時に理解できるAIモデルです。
従来のLLM vs VLM
| 機能 | 従来のLLM | VLM |
|---|---|---|
| 入力 | テキストのみ | テキスト + 画像 |
| 出力 | テキスト | テキスト |
| 用途 | 対話、文章生成 | 画像説明、OCR、視覚的質問応答 |
VLMのユースケース
- 画像キャプション生成: 画像を見て説明文を生成
- 視覚的質問応答(VQA): 画像について質問に答える
- OCR(文字認識): 画像内の文字を読み取る
- 物体検出の説明: 画像内の物体を自然言語で説明
- アクセシビリティ: 視覚障害者向けの画像説明
実装:VLM統合
1. 環境構築
パッケージインストール
pip install openai google-generativeai langsmith Pillow requests
環境変数設定
.envファイル:
# OpenAI API
OPENAI_API_KEY=sk-proj-...your_key_here
# Google Gemini API
GOOGLE_API_KEY=AIza...your_key_here
# LangSmith (オプション)
LANGSMITH_API_KEY=lsv2_pt_...
LANGSMITH_TRACING=true
LANGSMITH_PROJECT=botan-vlm-benchmark-v1
2. GPT-4o Vision実装
基本的な実装
GPT-4oはcontentを配列形式で受け取り、テキストと画像を含められます。
import openai
from typing import Optional, Dict, Any
def gpt4o_vision_generate(
prompt: str,
image_url: str,
api_key: str,
max_tokens: int = 300
) -> Dict[str, Any]:
"""
GPT-4o Visionで画像を理解して応答生成
Args:
prompt: テキストプロンプト
image_url: 画像URL(httpsまたはbase64)
api_key: OpenAI APIキー
max_tokens: 最大トークン数
Returns:
応答辞書(response, tokens, latency_ms)
"""
import time
client = openai.Client(api_key=api_key)
# content配列でテキスト+画像を指定
content = [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": image_url}}
]
start_time = time.time()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": content}],
max_tokens=max_tokens
)
latency_ms = (time.time() - start_time) * 1000
return {
"response": response.choices[0].message.content,
"tokens": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens
},
"latency_ms": latency_ms
}
使用例
import os
result = gpt4o_vision_generate(
prompt="What do you see in this image? Describe it in detail.",
image_url="https://example.com/image.jpg",
api_key=os.getenv("OPENAI_API_KEY")
)
print(f"Response: {result['response']}")
print(f"Tokens: {result['tokens']['total_tokens']}")
print(f"Latency: {result['latency_ms']:.2f}ms")
ポイント解説
-
content配列:
[{"type": "text", ...}, {"type": "image_url", ...}]の形式 -
画像URL形式:
- HTTPSのURL(
https://example.com/image.jpg) - Base64エンコード(
data:image/jpeg;base64,...)
- HTTPSのURL(
-
モデル:
gpt-4o(またはgpt-4o-mini)
3. Gemini Vision実装
基本的な実装
GeminiはPIL.Imageオブジェクトを直接受け取れます。
import google.generativeai as genai
from PIL import Image
import requests
from io import BytesIO
from typing import Optional, Dict, Any
def gemini_vision_generate(
prompt: str,
image_url: str,
api_key: str,
model: str = "gemini-2.5-flash",
max_tokens: int = 300
) -> Dict[str, Any]:
"""
Gemini Visionで画像を理解して応答生成
Args:
prompt: テキストプロンプト
image_url: 画像URL(httpsまたはローカルパス)
api_key: Google API キー
model: Geminiモデル名
max_tokens: 最大トークン数
Returns:
応答辞書(response, tokens, latency_ms)
"""
import time
genai.configure(api_key=api_key)
# 画像をダウンロード
if image_url.startswith('http'):
response_img = requests.get(image_url, stream=True)
response_img.raise_for_status()
img = Image.open(response_img.raw)
else:
# ローカルファイル
img = Image.open(image_url)
# コンテンツ = [テキスト, 画像]
content = [prompt, img]
# 安全フィルター設定(オプション)
safety_settings = [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
]
model_instance = genai.GenerativeModel(model, safety_settings=safety_settings)
start_time = time.time()
response = model_instance.generate_content(
content,
generation_config=genai.types.GenerationConfig(max_output_tokens=max_tokens)
)
latency_ms = (time.time() - start_time) * 1000
# エラーチェック
if not response.candidates:
return {
"response": "",
"tokens": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
"latency_ms": latency_ms,
"error": "Response blocked: No candidates returned"
}
return {
"response": response.text,
"tokens": {
"prompt_tokens": response.usage_metadata.prompt_token_count,
"completion_tokens": response.usage_metadata.candidates_token_count,
"total_tokens": response.usage_metadata.total_token_count
},
"latency_ms": latency_ms
}
使用例
import os
result = gemini_vision_generate(
prompt="この画像に何が写っていますか?詳しく説明してください。",
image_url="https://example.com/image.jpg",
api_key=os.getenv("GOOGLE_API_KEY")
)
print(f"Response: {result['response']}")
print(f"Tokens: {result['tokens']['total_tokens']}")
print(f"Latency: {result['latency_ms']:.2f}ms")
ポイント解説
-
PIL.Image: GeminiはPillowの
Imageオブジェクトを受け取る -
content配列:
[テキスト, PIL.Image]の順 - stream=True: 大きな画像の場合、メモリ効率化のため使用
- safety_settings: 必要に応じて安全フィルターを調整
4. LangSmith統合
既存のLangSmithトレーシングモジュールにVLM機能を追加します。
TracedLLMクラスの拡張
from langsmith import traceable
from typing import Optional, Dict, Any
import os
class TracedLLM:
def __init__(
self,
provider: str = "openai",
model: str = "gpt-4o",
project_name: str = "botan-project"
):
self.provider = provider
self.model = model
self.project_name = project_name
self.langsmith_enabled = os.getenv("LANGSMITH_TRACING", "false").lower() == "true"
def generate(
self,
prompt: str,
temperature: float = 0.7,
max_tokens: int = 1024,
metadata: Optional[Dict[str, Any]] = None,
image_url: Optional[str] = None # ← VLM用パラメータ
) -> Dict[str, Any]:
"""
Generate text with LLM (with automatic tracing)
Args:
prompt: Input prompt
temperature: Sampling temperature
max_tokens: Maximum tokens to generate
metadata: Additional metadata for tracing
image_url: Optional image URL for Vision models
Returns:
Response dict with 'response', 'tokens', 'latency_ms'
"""
trace_name = metadata.get("model_name", self.model) if metadata else self.model
def do_generate(input_prompt: str) -> str:
if self.provider == "openai":
full_result = self._openai_generate(input_prompt, temperature, max_tokens, image_url)
elif self.provider == "gemini":
full_result = self._gemini_generate(input_prompt, temperature, max_tokens, image_url)
else:
raise ValueError(f"Unknown provider: {self.provider}")
# エラーがあれば例外を投げる
if "error" in full_result:
raise RuntimeError(full_result["error"])
do_generate.full_result = full_result
return full_result.get("response", "")
# トレーシングを適用
if self.langsmith_enabled:
traced_func = traceable(
run_type="llm",
name=trace_name,
project_name=self.project_name
)(do_generate)
try:
response_text = traced_func(prompt)
result = do_generate.full_result
except RuntimeError:
result = do_generate.full_result
else:
response_text = do_generate(prompt)
result = do_generate.full_result
if metadata:
result["metadata"] = metadata
result["timestamp"] = datetime.now().isoformat()
return result
使用例(LangSmith有効)
import os
# LangSmith環境変数設定
os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_..."
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "my-vlm-project"
# VLM実行(自動的にトレース)
llm = TracedLLM(provider="openai", model="gpt-4o")
result = llm.generate(
prompt="What do you see in this image?",
image_url="https://example.com/image.jpg",
max_tokens=300,
metadata={"model_name": "gpt4o_vision", "has_image": True}
)
print(result["response"])
LangSmithダッシュボードで以下が確認できます:
-
Name:
gpt4o_vision - Input: プロンプト
- Output: 生成されたテキスト
-
Metadata:
{"has_image": True, ...}
ベンチマーク結果
実行環境
- 日付: 2025年11月5日
- OS: WSL2 Linux
- CPU: AMD Ryzen 9 9950X
- テスト画像: https://picsum.photos/800/600(ランダム画像)
測定結果
GPT-4o Vision
✅ Success
Response: "The image shows a wooden surface, likely a deck or boardwalk,
with weathered, grayish planks laid horizontally..."
Latency: 6,185ms (6.2秒)
Tokens: 890 (prompt: 約100, completion: 約790)
特徴:
- ✅ 安定した動作
- ✅ 詳細な画像説明
- ⚠️ レイテンシがやや長い(6秒)
Gemini 2.5 Flash Vision
❌ Failed
Error: Generation failed: MAX_TOKENS (no content returned)
Latency: 約2秒
Tokens: 0
問題:
-
finish_reason=2(MAX_TOKENS) でレスポンスが空 - これは既知の問題で、Gemini APIの一時的な不具合の可能性
- 同じコードでGemini 1.5 Flashは動作する場合がある
性能比較まとめ
| モデル | レイテンシ | トークン数 | 状態 |
|---|---|---|---|
| GPT-4o Vision | 6.2秒 | 890 | ✅ 成功 |
| Gemini 2.5 Flash Vision | 2秒 | 0 | ❌ エラー |
結論:
- GPT-4o: 安定性が高く、詳細な説明が可能
- Gemini: レイテンシは速いが、現在エラーが発生中
トラブルシューティング
Q1. OpenAI APIで画像が表示されない
原因
画像URLが正しくないか、アクセス権限がありません。
解決策
-
HTTPS URLを使用:
http://ではなくhttps:// - 公開アクセス可能な画像: 認証が不要な画像を使用
- Base64エンコード: 画像をBase64でエンコードして埋め込む
import base64
def encode_image_to_base64(image_path: str) -> str:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
# Base64 URLを作成
base64_image = encode_image_to_base64("./image.jpg")
image_url = f"data:image/jpeg;base64,{base64_image}"
Q2. Geminiで"cannot identify image file"エラー
原因
BytesIOから直接Image.open()すると失敗する場合があります。
解決策
stream=Trueを使用してレスポンスを直接読み込みます。
import requests
from PIL import Image
# ❌ 失敗する例
response = requests.get(image_url)
img = Image.open(BytesIO(response.content)) # エラー
# ✅ 成功する例
response = requests.get(image_url, stream=True)
response.raise_for_status()
img = Image.open(response.raw) # OK
Q3. Geminiで"finish_reason=MAX_TOKENS"エラー
原因
Gemini APIの既知の問題で、レスポンスが空なのにMAX_TOKENSを返す場合があります。
解決策
-
別のモデルを試す:
gemini-1.5-flashやgemini-pro - max_tokensを増やす: デフォルトより大きな値を設定
- プロンプトを簡潔に: 長すぎるプロンプトを短くする
# max_tokensを増やす
result = gemini_vision_generate(
prompt="画像を説明してください。",
image_url=image_url,
api_key=api_key,
max_tokens=1024 # デフォルトより大きく
)
注意: これはGemini APIの一時的な問題の可能性があり、時間が経つと解決する場合があります。
Q4. LangSmithにトレースが表示されない
原因
環境変数が正しく設定されていません。
解決策
# 環境変数を確認
echo $LANGSMITH_API_KEY
echo $LANGSMITH_TRACING
echo $LANGSMITH_PROJECT
# 設定されていない場合
export LANGSMITH_API_KEY=lsv2_pt_...
export LANGSMITH_TRACING=true
export LANGSMITH_PROJECT=my-project-name
または、Pythonコード内で設定:
import os
os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_..."
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "my-project-name"
まとめ
Phase 2 の成果
| 項目 | 内容 |
|---|---|
| 実装内容 | VLM統合(GPT-4o Vision/Gemini Vision) |
| 機能 | 画像理解、OCR、視覚的質問応答 |
| トレーシング | LangSmith完全統合 |
| ベンチマーク | GPT-4o: 6.2秒、Gemini: エラー |
Phase 1 (LangSmith統合) との関係
| 項目 | Phase 1 | Phase 2 |
|---|---|---|
| 入力 | テキストのみ | テキスト + 画像 |
| 出力 | テキスト | テキスト |
| トレーシング | LangSmith基盤 | Phase 1の基盤を活用 |
| 用途 | 応答生成 | 画像理解、配信画面認識 |
ベンチマーク結果のまとめ
| モデル | レイテンシ | トークン数 | 状態 |
|---|---|---|---|
| GPT-4o Vision | 6.2秒 | 890 | ✅ 成功 |
| Gemini 2.5 Flash Vision | 2秒 | 0 | ❌ エラー |
得られた知見
- GPT-4oの安定性: 画像理解タスクで高い信頼性
- Geminiの速度: レイテンシは速いが、現在エラーあり
- LangSmithの有用性: VLM呼び出しも可視化できる
Phase 1-5の完成状況
| Phase | 内容 | 記事 | 状態 |
|---|---|---|---|
| Phase 1 | LangSmithマルチプロバイダートレーシング | 記事 | ✅ |
| Phase 2 | VLM (Vision Language Model) 統合 | 本記事 | ✅ |
| Phase 3 | LLM as a Judge実装 | 記事 | ✅ |
| Phase 4 | 三姉妹討論システム実装(起承転結) | 記事 | ✅ |
| Phase 5 | センシティブ判定システム実装 | 記事 | ✅ |
次のステップ
- Phase 3: LLM as a Judge(品質評価システム)
- Phase 4: 三姉妹討論システム(起承転結)
- Phase 5: センシティブ判定システム
応用例
1. AI VTuberの視覚機能
# 配信画面のスクリーンショットを理解
result = llm.generate(
prompt="この配信画面で何が起きていますか?視聴者に説明してください。",
image_url="screenshot.jpg",
metadata={"character": "botan", "task": "stream_narration"}
)
print(f"牡丹: {result['response']}")
2. チャット画像の自動説明
# ユーザーが送った画像を説明
result = llm.generate(
prompt="この画像について簡潔に説明してください。",
image_url=user_uploaded_image_url,
max_tokens=100
)
print(f"AI: {result['response']}")
3. OCR(文字認識)
# 画像内のテキストを抽出
result = llm.generate(
prompt="この画像に書かれている文字をすべて抽出してください。",
image_url="document.jpg",
max_tokens=500
)
print(f"抽出されたテキスト:\n{result['response']}")
参考資料
- OpenAI Vision Guide
- Google Gemini API Documentation
- LangSmith Documentation
- Pillow (PIL) Documentation
関連記事
おわりに
VLM (Vision Language Model) を実装することで、テキストだけでなく画像も理解できるAIシステムを構築できました。
特に以下の点が重要でした:
- 🔍 GPT-4oの安定性で本番環境でも使用可能
- ⚡ Geminiの速度は魅力的だがエラー対処が必要
- 📊 LangSmithトレーシングでVLM呼び出しも可視化
今後は、OCR、物体検出、視覚的質問応答など、さらに高度なVLMタスクにも挑戦していきます。
質問・コメントお待ちしています!