コールセンターの通話から顧客の怒りをリアルタイム検知するシステムを作っている。文字起こしをLLMで判定するテキスト経路は既に動いていて、バックテストも通った。そこに声のトーン判定を足したら、狙っていなかった現象が出た。
声は、言葉より早く怒る。
合成した怒り通話で、テキスト判定が警報圏(75)に達する2発話前に、声の判定は既に72に入っていた。この記事は、その実測に至るまでの2つの失敗——窓の切り方で差が消えた話と、TTSでは作れない素材があった話——を含めた記録である。
なぜテキストだけでは足りないか
前提として、電話帯域(約300Hz〜3.4kHz)では子音の識別に効く高域が物理的に存在しない。実測では、この録音の3〜4kHz成分は全エネルギーの2.3%しかなかった。「ぶっころ」と「ひっこ」が本質的に紛らわしい世界で、罵倒語の正確な聞き取りに依存する怒り検知は脆い。
一方、トーン(韻律・音量・ピッチ)は帯域制限に強く残る。そこで相手の音声を gpt-audio に聴かせ、声の性質だけから判定させる経路を足した。プロンプトの核心はここになる。
コストは音声の秒数ぶん課金されるので常時は回さない。テキスト判定が45(閾値70より低いトリガ値)に届いたときだけ、直近の音声を切り出して1回聴かせる。安い信号系で網を張り、高価な解析装置は必要な呼にだけ落とす——電話網の信号系と通話路の関係と同じ設計である。
失敗1: 実時間の窓では差が消えた
最初の実装は「直近12秒の音声を切り出して聴かせる」だった。素直な設計に見えるが、対照実験がこれを殺した。
台詞が一字一句同じで、口調だけ違う通話をTTSで2本合成した(穏やかなcalm版と、怒鳴るloud版)。テキスト判定は同じスコアを出すはずなので、差が出ればそれは声からしか来ない——という実験設計である。
結果、APIを直接叩けば25点差が出るのに、パイプラインを通すと差が消えた(calm 55 vs loud 60)。
原因は窓の中身だった。12秒の実時間窓には、無音と、複数の発話の断片が混ざる。相手が黙っている間(こちらが喋っている間)は無音で埋まるから、calmとloudの窓はどちらも「無音多め+似た構成」の材料になり、均されてしまう。
判定材料が、判定対象と一致していなかったのである。
修正: 発話区間で切る
幸い、座標は既に持っていた。文字起こしのVADが返す audio_start_ms / audio_end_ms を、発話ごとの頭出し再生のために保存してあったのだ。同じ座標で判定材料を切り出せば、「何を判定したか」が文字起こしと1対1で揃う。
音声バッファ側は「通話開始からの絶対時刻」で切り出せるようにした。音声は黙っている間も無音が流れ続けるので、総サンプル数がそのまま通話タイムラインになる。
結果: 声がテキストを先行する
窓を発話区間に変えて、合成の怒り通話(段階的にエスカレーションする台本)を流し直した。テキスト判定(T)と声の判定(V)を発話ごとに並べる。
| 発話 | T | V |
|---|---|---|
| 確認ってさっきもそう言いましたよね? | 35 | 65 |
| いつまで待たせるんですか? | 45 | 72 |
| 謝れば。 | 45 | 75 |
| 済むと思ってんのか。 | 75 | 78 |
| 責任者を出せよ。今すぐ。 | 85 | 78 |
テキストが75に達するのは「済むと思ってんのか」の時点だが、声はその2発話前、「いつまで待たせるんですか?」で既に72に入っている。言葉がまだ敬語の形を保っている間に、声はとっくに怒っていた。
人間の声(自分で演じた実架電の録音)でも確認した。V は 35→45→65→72→78→82 と滑らかに追跡し、ピークはテキスト最大の75を上回った。負例(「もしもし」「はい」だけの通話)では、材料が足切り(5秒未満)に掛かって判定自体が走らず、誤検知も課金もゼロだった。
早く気づくことが目的の製品にとって、これは「トーンでしか分からない怒りがある」より強い結論である。トーンは、言葉より先に教えてくれる。
設計への反映: 早い方を警報に繋ぐ
先行検知が取れても、アラートがテキスト起点のままでは意味がない。声が72でもテキストが45なら鳴らない——せっかくの2発話が捨てられる。そこで両者を混ぜて1つの値にした。混ぜ方は2つ判断がある。
1. 平均ではなくmax。 T45/V72の平均は58で閾値(70)未満のまま。早期信号を薄める混ぜ方は目的に反する。検知は敏感側に倒す。
2. 同じ発話に付いた声だけを混ぜる。 古い発話の声72を混ぜ続けると、相手が落ち着いてもゲージが下がらない。ゲージが下がらなければ、オペレータに「もう平常運転に戻っていい」を伝えられない。
声の判定にもアラートを持たせた。
これで画面には「⚠ 相手が強い怒りを示しています(82) — 声のトーン: 声量が大きく、語気が鋭く詰めるような調子で強い怒りが感じられる」のように、声起点であることが分かる形で警報が出る。
失敗2: TTSは「静かな怒り」を演じられない
正直に書いておくべき限界が一つある。対照実験の素材として、calm(穏やか)とcold(静かに抑えた怒り)を同一台詞で合成したのだが、この2本は音響的にほぼ同一だった。有声率は共に24%、RMS差は1dB、基本周波数のレンジもほぼ重なる。
TTSは出力音量を正規化するため、「怒鳴る」という指示は語気の演技には反映されても、物理的な音圧差にはならない。「大声の怒り」は合成できるが、「凍った声の怒り」は合成できない。cold系の検証素材が要るなら人間の録音が必要で、これはTTS合成による検証基盤の明確な限界である。
まとめ
- 電話帯域では子音が落ちる。罵倒語の聞き取りに依存する怒り検知は脆く、トーンは帯域制限に強い
- 音声判定の材料は実時間の窓ではなく発話区間で切る。無音と複数発話が混ざると判定が均される。「何を判定したか」を文字と1対1に揃えること
- 実測の結論: 声は言葉より早い。テキストが閾値に達する2発話前に、声は警戒圏に入っていた
- だから融合はmaxで敏感側に、ただし同じ発話の声だけ(下がるべきときに下がるゲージを守る)
- TTS素材には限界がある。「静かな怒り」は合成できない
怒りの検知は「何を検知するか」の勝負だと思っていたが、実装してみると「いつ検知するか」の勝負だった。そして「いつ」を最も早く教えてくれるのは、言葉ではなく声だった。


コメント