meishi.png という名前のファイル
利用者が、名刺の写真だと言ってファイルを送ってきました。名前は meishi.png です。でも中身が本当に画像かどうかは、名前からは分かりません。
このレッスンでは、画像をモデルに送る要求の形と、送る前にかける三つの確認を学びます。
このレッスンのゴール
メモ
画像を安全に運ぶ要求を、送る前の確認つきで組み立てられます。
キーワード
| ことば | いみ | れい |
|---|---|---|
| base64 | バイトを、文字だけで書き表す方法 | |
| メディアタイプ | 中身の種類を表す名前 | image/png |
| 先頭のバイト | ファイルの最初の数バイト。種類の目印になる | |
| 上限 | 受け取れる最大の大きさ | |
| 要求 | モデルに送る一回分のメッセージ |
学生が入力するセルは、このプレビューでは空白です。
画像は文字になって運ばれる
要求は、文字で書かれた JSON です。画像のバイトは、そのままでは JSON に入れられません。そこで base64 を使い、バイトを英数字の文字に書き換えます。
3 バイトが 4 文字になるので、運ぶ量は約三分の四倍に増えます。
base64 でどれだけ増えるか
| 元のバイト数 | base64 の文字数 |
|---|---|
| 3 | 4 |
| 30 | 40 |
| 100 | 136 |
| 1000 | 1336 |
学生が入力するセルは、このプレビューでは空白です。
名前ではなく先頭のバイトを信じる
名前は、送った人が好きに付けられます。名前が .png でも、中身は別のファイルかもしれません。
PNG は先頭の 8 バイトが決まっています。この目印を読めば、名前に関係なく本当の種類が分かります。名前と中身が食い違うファイルは、断ります。
よくある間違い
注意
「名前が .png なら、PNG の画像だ」と考えがちです。名前は送る人が決めるので、信じられません。先頭のバイトで種類を決め、名前はそれと合うかどうかの確認にだけ使います。
確認してから送る
断られた呼び出しも、時間と手間を使います。だから、送る前に自分で確認します。問題が一つでもあれば送らず、理由を利用者に伝えます。
問題がなければ、文章と画像を一つのメッセージの部品として並べ、base64 の文字を入れて送ります。
モデル用の補助を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答に含まれるトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用の補助を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
送る前の確認を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const head = [137, 80, 78, 71, 13, 10, 26, 10];' 'const png = Buffer.concat([Buffer.from(head), Buffer.alloc(40, 7)]);' 'const fake = Buffer.concat([Buffer.from(\"MZ\"), Buffer.alloc(40, 7)]);' 'const huge = Buffer.concat([Buffer.from(head), Buffer.alloc(142, 7)]);' 'const LIMIT = 100;' > send2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
送る前の確認を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function sniff(b) {' ' if (b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) return \"png\";' ' if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return \"jpeg\";' ' return \"不明\";' '}' 'function check(b) {' ' const problems = [];' ' if (b.length > LIMIT) problems.push(\"大きすぎる\");' ' if (sniff(b) === \"不明\") problems.push(\"許可されない形式\");' ' return problems;' '}' >> send2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
送る前の確認を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const list = [[\"neko.png\", png], [\"memo.png\", fake], [\"okii.png\", huge]];' ' for (const item of list) {' ' const b = item[1];' ' console.log(item[0] + \": \" + sniff(b) + \"、\" + b.length + \" バイト、base64 で \" + b.toString(\"base64\").length + \" 文字\");' ' console.log(\" 問題: \" + JSON.stringify(check(b)));' ' }' ' const part = { type: \"image\", media_type: \"image/png\", data: png.toString(\"base64\").slice(0, 12) + \"...\" };' ' console.log(JSON.stringify({ messages: [{ role: \"user\", content: [{ type: \"text\", text: \"写っているものを説明して。\" }, part] }] }));' ' const q = \"画像が大きすぎて送れなかった利用者に、やさしい一文で伝えてください。\";' ' const r = await ask([{ role: \"user\", content: q }], \"画像が大きすぎて送れませんでした。小さい画像を選んでください。\", 60);' ' console.log(\"返事: \" + r.text);' '}' 'main();' >> send2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
三つのファイルを確認する
docker exec -w /home/node/project agent node send2.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の例
| 行 | 示していること |
|---|---|
| neko.png: png、48 バイト | 先頭のバイトが PNG で、上限の 100 以内 |
| memo.png: 不明、42 バイト | 名前は png でも、中身は PNG ではない |
| okii.png: png、150 バイト | 上限の 100 を超える |
| {"messages": ...} | 送る要求の形。画像の文字は先頭だけを見せている |
| 返事: ... | モデルの一文。先に「記録済みサンプル」の行が出ることもある |
学生が入力するセルは、このプレビューでは空白です。