クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

画像を安全に送る要求

このレッスンのあとで: 画像を安全に運ぶ要求を、送る前の確認つきで組み立てられます。

70分のレッスンコマンド6件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

meishi.png という名前のファイル

利用者が、名刺の写真だと言ってファイルを送ってきました。名前は meishi.png です。でも中身が本当に画像かどうかは、名前からは分かりません。

このレッスンでは、画像をモデルに送る要求の形と、送る前にかける三つの確認を学びます。

このレッスンのゴール

メモ

画像を安全に運ぶ要求を、送る前の確認つきで組み立てられます。

キーワード

ことばいみれい
base64バイトを、文字だけで書き表す方法
メディアタイプ中身の種類を表す名前image/png
先頭のバイトファイルの最初の数バイト。種類の目印になる
上限受け取れる最大の大きさ
要求モデルに送る一回分のメッセージ

学生が入力するセルは、このプレビューでは空白です。

画像は文字になって運ばれる

要求は、文字で書かれた JSON です。画像のバイトは、そのままでは JSON に入れられません。そこで base64 を使い、バイトを英数字の文字に書き換えます。

3 バイトが 4 文字になるので、運ぶ量は約三分の四倍に増えます。

base64 でどれだけ増えるか

元のバイト数base64 の文字数
34
3040
100136
10001336

学生が入力するセルは、このプレビューでは空白です。

名前ではなく先頭のバイトを信じる

名前は、送った人が好きに付けられます。名前が .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": ...}送る要求の形。画像の文字は先頭だけを見せている
返事: ...モデルの一文。先に「記録済みサンプル」の行が出ることもある

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。