📷
スマホのカメラに映る景色を音声で案内する Visual Sonar の試作
これは非公式Infocom Advent Calendar 2023の22日目の記事です。
Visual Sonar とは
Visual Sonarとは、スマホのカメラに映る映像を解析し、音声で教えてくれるWebアプリです。
OpenAIのGPT-4で画像を扱えるようになったので、それを使って作ってみました。
構成要素
Visual Sonarは次の要素で構成されています。
- mediaDevices.getUserMedia()を使って、カメラの映像取得
- Cnavasを使って、映像から1コマ画像を取得
- OpenAIのgpt-4-vision-previewを使い、画像の内容を解析
- OpenAIのTTS(text-to-speech)を使い、テキストを音声に
- Audio要素で再生
動作している様子
iPhoneで動いている様子を、画面録画しました。最後の方で音が出ます(テキストを読み上げています)
使い方
GitHub Pagesで試すことができます
URL
操作方法
- vsonar.htmlをブラウザで表示
- [api key]に、OpenAIのAPIキーを指定
- または、vsonar.html?key=xxxxxx とURLのクエリーパラメータに指定してもOK
- [Start]ボタンをクリック
- カメラの許可を求められらた、許可する
- カメラの映像が表示される
- [Explain in Voice]ボタンをクリック
- 映像から画面を切り抜き
- OpenAIの GPT-4 Vで画面を解析
- TTSで音声に変換、それを再生して画像の説明をする
- [Stop]ボタンをクリックすると、カメラの映像が停止
各部の実装(抜粋)
説明のために一部抜粋して簡略化しています。全体のソースはGitHubにあります
カメラ映像の取得
背面カメラを使用するため、videoのオプションとして、次のように指定します
- video: { facingMode: "environment" }
async function startCamera() {
const constraints = {
video: {
//facingMode: "user" // フロントカメラを使用
facingMode: "environment" // 背面カメラを使用
}
};
// カメラの映像を取得
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// <video> 要素にストリームを設定
localVideo.srcObject = stream;
await localVideo.play();
}
映像から画像をBase64で取得
Canvas要素を使い、Video要素から1コマ画像をBase64で取得します。
// video ... 映像が表示されているVideo要素
// canvas ... 作業に使うCanvas要素
// ctx ... canvasの描画に使うcontext
function getBase64Image(video, canvas, ctx) {
// Draw Image
ctx.drawImage(video, 0, 0);
// To Base64
return canvas.toDataURL("image/jpeg");
}
GPT4-V で解析
gpt-4-vision-preview を使って、画像を解析します。従来のChat APIと同様ですが、conentが単なるテキストでなく、テキストと画像URLのセットになっているのが違いです。
// 画像のURL(またはBase64表記)とチャットメッセージを送信し、応答を返す
async function singleChatWithImage(image_url, text) {
// 従来のChat APIと同様だが、conentが単なるテキストでなく、テキストと画像URLのセットになる
const userMessage = {
role: 'user',
content: [
{
"type": "text",
"text": text,
},
{
"type": "image_url",
"image_url": {
"url": image_url,
}
}
]
};
// -- request --
const API_KEY = 'sk-xxxxxxx';
const MODEL = 'gpt-4-vision-preview';
const API_URL = 'https://api.openai.com/v1/chat/completions';
const options = { temperature: 0, max_tokens: 1000 };
const response = await _chatCompletion([messages], API_KEY, MODEL, API_URL, options);
return response;
}
// chat API を呼び出す
async function _chatCompletion(messages, apiKey, chatModel, url, options) {
const bodyJson = {
messages: messages,
model: chatModel,
temperature: options.temperature,
max_tokens: options.max_tokens,
};
const body = JSON.stringify(bodyJson);
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
};
const res = await fetch(url, {
method: "POST",
headers: headers,
body,
});
// 応答を解析
const data = await res.json();
const choiceIndex = 0;
const choices = data.choices;
return choices[choiceIndex].message;
};
今回の試作では、次のプロンプトを渡して画像を解析させています。
'What is this? Please answer in Japanese.'
ちなみに画像についてチャットを続ける場合は、通常のChatと同様に過去のやりとりと新しいメッセージを配列に格納して送ればOKです。
- 最初の画像込みのユーザーメッセージ
- アシスタントの応答メッセージ
- 次のユーザーからのメッセージ
- それに対するアシスタントの応答メッセージ
- さらにユーザーからのメッセージ...
TTSでテキスト読み上げ
OpenAIのTTS(text-to-speech) APIを使って、テキストを音声に変換しています。
async function textToSpeech(text, apiKey) {
const apiUrl = 'https://api.openai.com/v1/audio/speech';
// -- build header --
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
};
// --- build body ---
const bodyJson = {
model: 'tts-1',
input: text,
voice: "alloy",
};
const body = JSON.stringify(bodyJson);
// --- request ---
const res = await fetch(apiUrl, {
method: "POST",
headers: headers,
body,
}).catch(e => {
// エラー処理
});
// エラー判定
if (!res.ok) {
// エラー処理
}
const responseBlob = await res.blob();
return responseBlob;
}
Audio要素で再生
TTS APIで取得したBlobを、Audio要素で再生します。
async function playbacBlobAsync(audioElement, blob) {
const blobUrl = URL.createObjectURL(blob);
audioElement.src = blobUrl;
audioElement.onended = (evt) => {
URL.revokeObjectURL(blobUrl);
};
await audioElement.play();
}
また、iOSではユーザー操作が無いとAudio要素で音声が再生できないので、最初にユーザーがボタン操作をした段階で、次のようにAudio要素で再生を試みておきます。
function preparePlay(audioElement) {
audioElement.play().catch(err => { console.log('prepareAudio'); }); // エラーが発生するが無視OK
audioElement.pause();
}
今後やりたいこと
- 1つキャプチャー画像に対して、複数回のやりとりをしたい
- 目の不自由な人が使えるように、アクセシビリティに配慮した作りにしたい
- 音声で指示が出せる
- 音声で画像について追加の質問ができる
- ※ アクセシビリティはOSの機能を利用するのが良いか、独自に音声でのやりとりを実装するのが良いか、要検討
Discussion