通話音声から声のトーンを判定させようと、OpenAIの音声入力対応モデル(gpt-audio)を Chat Completions API から叩いていて、妙な壁にぶつかった。WAVをちゃんと添えているのに、モデルが「音声をお聞かせください」と返してくる。
添えている。Base64にして input_audio に入れて、HTTP 200 も返ってきている。それでも「まだもらっていない」と言われる。
同じことは音声を扱うマルチモーダルAPI全般で起こりうるが、以下はすべて gpt-audio / gpt-audio-mini での実測である。
結論から書くと、原因は音声の側ではなかった。出力形式の指示をどこに書くかだった。変数を分離して測ったら、はっきり出た。
| 条件 | JSONが返る | 「音声をください」 |
|---|---|---|
| A テキスト先・形式指示なし | 0/5 | 5/5 |
| B 音声先・形式指示なし | 0/5 | 5/5 |
| C テキスト先・形式指示あり | 4/5 | 1/5 |
| D 音声先・形式指示あり | 5/5 | 0/5 |
順序は関係なかった。効いたのは形式の指示をユーザーターンに置くことである。
そもそも構造化出力が使えない
まず試したのは当然 Structured Outputs だった。JSONスキーマを渡して型で縛る、いつものやつだ。
400が返ってきた。
ならば緩い方、と {"type": "json_object"} に落とした。これも400。
音声入力に対応したモデルは response_format を一切受け付けなかった。 テキストのモデルなら「スキーマで縛れば済む」話が、ここでは使えない。プロンプトで頼んで、返ってきた文字列を自分でパースするしかない。
これ自体は珍しくもない制約だが、この先で効いてくる。構造化出力という逃げ道が塞がれているせいで、形式をどう指示するかがそのまま挙動を左右するからだ。
システムプロンプトに書いても効かない
そこでシステムプロンプトに出力形式を書いた。判定基準を丁寧に説明したうえで、最後にこう添えた。
ユーザーターンには素直な依頼だけを置いた。
これが冒頭の症状を引き起こす。5回叩いて5回とも「音声をお聞かせください」だった。
しかも文面が毎回違う。定型の拒否ではない。
「再生してください」と言われても、こちらはAPIである。再生する場所がない。
順序のせいだと思った
最初に疑ったのは並び順だった。テキストの後に音声を置いているから、「この音声」と言った時点ではまだ音声が来ていない、という読まれ方をしているのではないか。人間の会話としては筋の通る仮説である。
そこで音声を先に置いてみた。だが結果は変わらなかった。0/5のままである。
これで仮説は死んだ。並び順の問題ではない。
形式を指示すると通る
半信半疑で、ユーザーターンのテキストに出力形式を書き足した。システムプロンプトに書いてあるものと、同じ内容である。
通った。5/5。
同じ音声、同じシステムプロンプト、同じモデル。違うのは、同じ指示をユーザーターンにも書いたかどうかだけである。
冒頭の表がその実測で、A→C(テキスト先のまま形式指示を足す)で 0/5 → 4/5、B→D(音声先のまま足す)で 0/5 → 5/5。形式指示の有無だけが効いている。
なぜ効くのか
ここからは測定ではなく解釈になるが、筋は通っていると思う。
「音声が聞こえない」はモデルの状態報告ではない。生成された文である。
つい、モデルが内部状態を点検して「添付なし」と判断して報告している、と読んでしまう。だが実際には、モデルは次に来る文字列を予測しているだけだ。自分に音声が渡っているかどうかを内省して答えているわけではない。
そう考えると、あの応答は自然に説明がつく。「この音声を判定してください」という文の直後に来る発話として、「音声をお聞かせください」は極めてありふれている。 人間同士のやりとりでは、ファイルを添え忘れる方が普通だからだ。学習データの中で、この依頼文に続く返事として最も高い確率を持つ候補のひとつがそれになる。
音声はちゃんと渡っている。だがテキストの文脈だけを見れば「添付を忘れた依頼」に見える。モデルはその見え方に引きずられる。
では形式指示がなぜ効くのか。「JSONだけを返してください」の直後に来る発話として、丁寧な催促は成立しないからだ。 出力の形を固定すると、その分岐が消える。残るのは実際に音声を聴いて中身を埋める道だけになる。
構造化出力が使えていれば、この話は起きなかった。デコード時に形式が強制されるので、催促の文が生成される余地がそもそもない。response_format が使えないモデルでは、その強制をプロンプトで代替することになり、書く場所が挙動に響く。
システムプロンプトでは弱かった理由も同じ筋で説明できる。システムは会話全体の性格づけとして効くが、いま直前にある指示ほど次の一手を強く縛らない。催促という選択肢を潰すには、直前に置く必要があった。
本当に聴いているのか
ここで疑うべきことがある。形式を強制したせいで、音声を聴かずにそれらしいJSONを埋めているだけかもしれない。催促が出なくなったのは、催促が言えなくなっただけとも読める。
確かめるには、音声を変えて出力が変わるかを見ればいい。台詞は一字一句同じで、口調だけ違う音声をTTSで2本作った。片方は穏やかに、片方は怒鳴って。
テキストが同一なので、文字だけ読んでいるなら差は出ないはずである。
| 3回の判定 | |
|---|---|
| 穏やかに読んだ版 | 45, 55, 45 |
| 怒鳴った版 | 75, 72, 65 |
差が出た。約25点。 音声はちゃんと聴かれている。
ついでに分かったこともある。同じ比較を小さいモデル(gpt-audio-mini)でやると、穏やか 45/55/45 に対し怒鳴り 45/55/55 で、まったく差が出なかった。応答は返るし、それらしいトーンの描写も付く。だが声を聴き分けてはいない。
「動いている」と「効いている」は別物で、壊れ方が静かな側である。形式さえ整っていれば、それらしい数字は返り続ける。
まとめ
音声入力を扱っていて似た症状に当たったら、順に確認するとよい。
response_formatが使えるか。音声対応モデルでは弾かれることがある- 出力形式をユーザーターンに書いているか。システムプロンプトだけでは足りない場合がある
- 入力を変えて出力が変わるか。形式が整っているだけの応答は、素通しでも同じ顔をしている
3番が一番大事だと思う。1番と2番は400や明らかな異常応答として現れるので気づける。だが入力を無視した「それらしい応答」は、目視では正常と区別がつかない。同じ入力を条件だけ変えて2本用意し、差が出ることを確かめる——それをやるまでは、動いていることの証明にはならない。


コメント