実体: templates/line-bot/(このキットのLINE公式アカウントAI社員bot金型)
実証: line-harness-oss(ai-shain.link で実運用中の「りんく」)
りんく
りんくは、写真だけでなく動画や音声メッセージを送っても、中身をちゃんと見て・聞いて反応します。 単に「動画が届きました」で終わらず、映っている内容・聞こえた話の内容をふまえて、 りんくらしい言葉で返事をします。
画像・動画・音声を受け取ると、まず①AIモデルに「内容を客観的に説明して」とだけ頼み、 人格を含まない説明文を得る。その説明文を②「あなたらしく反応してください」という指示付きで 通常の会話パイプラインに渡し、りんくの人格・KB・会話履歴・エスカレーション判定をそのまま使って返信を作る。 この2段構成にすることで、「AIが淡々とした説明文をそのまま返してしまう」事故を防いでいる。
| 種類 | 使うモデル | 方式 | フォールバック |
|---|---|---|---|
| 🖼️ 画像 | 1番手Groq qwen/qwen3.6-27b2番手Gemini gemini-2.5-flash-lite |
OpenAI互換 chat/completions の image_url |
あり(2段) |
| 🎬 動画 | Geminiのみgemini-2.5-flash-lite |
ネイティブ generateContent APIの inline_data |
なし |
| 🎤 音声 | Geminiのみgemini-2.5-flash-lite |
OpenAI互換 chat/completions の input_audio |
なし |
画像だけGroq→Geminiの2段フォールバックがあり、動画・音声はGeminiのみという非対称な構成になっている。 これは仕様の手抜きではなく、実機検証で確定した技術的な制約による。
Groq・Cloudflare Workers AIは、現時点で動画・音声を直接受け取れるAPIを持っていない (画像のみ両プロバイダとも対応)。Gemini側も一枚岩ではなく、実際にAPIを叩いて検証した結果、 次のことが分かった。
chat/completions に video_url というcontent typeで
動画を渡そうとすると、Gemini側から400 Invalid content part typeではっきり拒否される。
動画はGoogleのネイティブ generateContent APIの inline_data 形式でしか受け付けない
(レスポンス形式もOpenAI互換とは異なるため、専用のパーサーを別途実装している)。chat/completions に input_audio というcontent typeで
渡すと、こちらは正しく受理される(クォータ超過時は429が返るが、フォーマット自体は
400にならないことを確認済み)。ただし提供元をGeminiに揃える設計にしている。画像には無い、動画・音声特有の落とし穴が2つあった。どちらも「原因を推測せず、実際にAPIを叩いて 実測値を確認する」ことで初めて特定できたもの。
processing → succeeded / failed)」という仕組みがあり、
webhookを受け取った直後はまだ変換処理中で、コンテンツの取得に失敗することがある。
対策として /content/transcoding エンドポイントで状態を確認してから本体を取得するよう変更し、
processing中は最大約9秒ポーリングして待つ。
audio/x-m4a というcontent-typeで届くことが分かった(audio/mp4ではない)。
この値が対応表から漏れていたため、音声メッセージにだけ一切反応しないという
分かりにくい不具合が実際に発生した。今は対応表に追加済みで解消している。
動画・音声は既定で15MBを上限としている(bot.config.jsonの
llm.video.maxInputBytes / llm.audio.maxInputBytesで調整可)。
超過した場合は理解を諦め、[動画]/[音声]ラベルのみを記録して
返信をしない(誤った内容で答えるくらいなら黙るという、無応答ゼロ化チェーンとは逆方向の
安全側の判断。テキスト・画像の応答自体には影響しない)。
templates/line-bot/worker/src/services/vision-describe.ts — 画像の客観描写生成templates/line-bot/worker/src/services/media-describe.ts — 動画・音声の客観描写生成、音声のcontent-type対応表templates/line-bot/worker/src/services/incoming-image.ts / incoming-media.ts — LINE Content APIからの取得・R2保存・トランスコード待ちtemplates/line-bot/worker/src/services/llm-providers.ts — callGeminiVideo / callGeminiAudio の実装箇所templates/line-bot/bot.config.json — llm.vision / llm.video / llm.audio でモデル・上限を調整
「りんくらしい返事」は、AIに何かをアップロードして学習させているわけではない。
knowledge-pack/persona.md という1枚のテキストファイルに、口調や性格を
文章で書いているだけ。書き換えて再デプロイすれば、その場で反映される。
ポイントは、この1枚のファイルが、テキストの会話だけでなく画像・動画・音声への反応にも
そのまま使われるということ。仕組みは前述の「2段方式」の通りで、①AIが客観的に内容を
説明する部分には人格が入らず、②その説明を受け取って反応を作る部分でだけpersona.md
が使われる。つまり①と②が分かれているおかげで、persona.mdを1つ調整するだけで、
テキスト・画像・動画・音声のすべての返信の口調が同時に揃う。「音声用の人格」「動画用の人格」を
別々に用意する必要はない。
画像への反応も、声のメモへの反応も、同じ「僕」「〜だね」口調になっている。
これはpersona.mdに書いた性格が両方の経路で同じように使われているから。
persona.mdには次の4つを書く。①役割の一文、②話し方の一般原則(既定のひな形を流用可)、
③キャラクターの個性(一人称・文末・絵文字の使い方など)、④よくある質問への案内。
1つの長文で説明しようとせず、③のように短い箇条書きに分けて書くほど、AIが安定して守りやすい。
詳しい書き方の事例(サンプルの人格丸ごと1つ)と調整のコツは
templates/line-bot/README.md の「人格を作る・調整する」の章にまとめてある。
画像・動画は問題なく反応するのに、音声メッセージだけ送っても既読になるだけで返信が来ない、 という報告があった。ログだけでは分からなかったため、D1(会話履歴データベース)を直接確認したところ、 音声メッセージの保存自体(R2へのファイル保存)は成功していたが、その先の「客観描写を作る」処理が 毎回無言で失敗していることが分かった。
Content-Typeヘッダーを直接確認audio/x-m4a。事前に用意していた対応表には audio/mp4 しか
入れておらず、この値が抜けていたaudio/x-m4a を対応表に追加。あわせて、原因調査用に
「未対応のcontent-typeだった場合、その実測値を会話ログに残す」という仕組みも入れて、
次に似た問題が起きてもログイン不要のデータベース確認だけで即座に特定できるようにした。
教訓: 「たぶんこの値だろう」という推測でcontent-typeの対応表を書くと、実機の挙動と ズレたまま気づかないことがある。実際にAPIを叩いて返ってきた値を確認してから対応表を作るのが安全。
app.config.json の lineBot セクションを埋めて enabled: true にtemplates/line-bot/ をアプリの line-bot/ にコピーし、{{...}} を置換npx wrangler r2 bucket create {{shortName}}-line-images でR2バケットを作成
(画像・動画・音声の保存先。作成しなくてもテキストのAI応答には影響しない)LINE_CHANNEL_ACCESS_TOKEN / LINE_CHANNEL_SECRET / GROQ_API_KEYGEMINI_API_KEY を投入する。
Google AI Studioで無料発行できる。
未設定だと動画・音声認識は静かにスキップされる(テキスト・画像の応答には影響しない)wrangler.toml の [[r2_buckets]] バインディングはテンプレートに同梱済み。
バケット名の置換だけ確認するくわしい手順は templates/line-bot/README.md の「画像・動画・音声認識」の章を参照。