crossbar_telepath のポストモーテムである。プロジェクトは2026-08-24にアーカイブした。畳んだ経緯(電話番号クォータの180日ルール)は前回書いたので、今回は45日間で何ができたかを総括する。死因の解剖が前回なら、今回は生涯の記録である。
作ったもの
TerraformでコールセンターをフルIaC構築し、通話の両話者をリアルタイムに文字起こし・感情分析して、「相手が怒り始めている」を応対者より先に監視卓へ知らせるシステム。2026年10月施行のカスハラ対策義務化(改正労働施策総合推進法)を見据えた検証プロジェクトとして、7月11日の深夜に構想した。

設計の背骨は電話網のIN(Intelligent Network)である。Amazon Connectを交換機、FastAPIの消費サービスをSCP(サービス制御ポイント)に見立て、通話路(KVS)とシグナリング(Lambda→SQS)を分離した。呼の存在をストリーム一覧から推測するのではなく、コールフローが「いま呼が張られた。音声はこのストリームのここから」を明示的に知らせる。この分離が複数呼の同時処理を支えた。
数字で見る45日
| 項目 | 値 |
|---|---|
| 期間 | 2026-07-12 〜 08-24(構想は07-11深夜) |
| マージしたPR | 16本 |
| コミット | 71 |
| コード | バックエンド約2,900行 + フロント約1,600行(テスト別) |
| テスト | バックエンド53本 + フロント22本(計約1,500行)、CIで強制 |
| 実架電E2E | 3ラウンド(7/12・7/26・8/1) |
| ブログ | 開発記・技術発見あわせて10本 |
| 月額AWSコスト(常設時) | 約$3(番号$0.06/日 + KMS $1/月) |
技術ハイライト
1. KVSの音声はCodecIDが嘘をつく
最難関と目していたKVSからの音声取り出しは、初日に一番の罠を踏んだ。MKVコンテナのCodecIDが“A_AAC”を名乗っているのに、中身は生のL16 PCM 8kHzなのだ。ffmpegは律儀にAACとしてデコードを試みて全フレーム失敗する。EBMLを自前で歩くしかない。
SimpleBlockの先頭4バイトを剥がせば生PCMが出てくる。そしてトラック1=相手側、トラック2=自分側——電話の音声が最初から話者別に分かれて届く。話者分離(ダイアライゼーション)という機械学習の難問が、プロトコルの構造で消えるのがこのアーキテクチャの一番の贅沢である。
2. gpt-audioへの指示は「どこに書くか」で0/5が5/5になる
声のトーン判定にはgpt-audio(Chat Completionsの音声入力)を使った。当初、音声を添えているのに「音声をお聞かせください」と返される事象に悩まされた。変数を分離して各5回実測した結果が、コードのコメントとして焼き込んである。
音声とテキストの順序は効かない。効くのは形式指示がuserターンにあるかだ。詳細は単独記事に書いた。ついでに言うとgpt-audio-miniは静かな声と怒鳴り声を区別できなかった(実測)。トーン判定はフルモデルが要る。
3. 声は言葉より早い
このプロジェクト最大の発見である。同じ通話を「文字起こしテキストの判定」と「音声そのものの判定」の両方に流すと、声のほうが2発話早く怒りを検知した。
- 合成怒り通話: テキストが警報閾値75に達する2発話前に、声が72に到達
- 実呼(本人が演技): テキスト最大75に対し、声は35→82と先行して追い抜いた
人は言葉を選べるが、声量・話速・トーンの制御は先に破綻する。テキスト判定は怒りが語彙まで到達した後の観測であり、韻律はその上流にある。カスハラ対策の文脈では、この時間差が「エスカレート前に介入できる猶予」そのものだ。判定窓をリアルタイムの固定窓ではなく発話区間(VADのaudio_start_ms/end_ms)で切り直したことで差が見えるようになった。実測の詳細はこちら。
4. 怒鳴らずに怒りをテストする
「怒った通話」の検証素材を毎回自分で演じるのは辛い(隣人にも心配される)。そこでTTSで台本から通話を合成し、電話帯域(8kHz)に落とし、KVS互換のMKVとしてパイプラインに流すフィクスチャを作った。実架電と同一の経路を通るので、シグナリング以外の全部が検証できる。作り方は単独記事にした。
副産物として限界も分かった。TTSは音量を正規化するため「静かに抑えた怒り」を演じ分けられない(calmとcoldのRMS差1dB)。cold系の素材は人間の録音が要る——合成データの守備範囲を知ることも検証のうちである。
5. 認可は「ブラウザで捨てる」ではなく「そもそも送らない」
最終フェーズでSPA化(React+TypeScript+Vite)とCognito認証・ロール別認可を入れた。通話内容という機微データを扱う以上、WebSocketのbroadcastを全員に投げてクライアントで間引くのは境界ではない。サーバー側で宛先を絞る。
同じ絞り込みをREST側にも掛け(他人の呼の詳細・録音・カードは403)、実Cognitoトークンで境界E2Eを通した。UIも役割で分けた——応対者の画面にはアラート音も感情の数値も出さない。通話の中にいる本人は相手の怒りを一番よく知っており、割り込みは集中を奪うだけだ。気づくべきは監視卓(SV)である。この「誰に何を見せないか」の設計は、機能を足すより時間を使った。
プロセスの学び
- PR単位+緑のCI+実測をPR本文に、を45日間崩さなかった。書き直しの多い検証プロジェクトほど、テストが仕様書として効く(SPA化のとき、jsdomテスト4本が移植の正解を機械で保証した)
- 合成フィクスチャは開発速度そのものだった。実架電が必要な検証は3ラウンドで済み、残りは全部リプレイと合成で回した。インフラをdestroyしても開発が止まらない体制が、結果的に(番号クォータで)命綱になった
- 検証は変数を分離して数える。「gpt-audioの0/5→5/5」も「声の2発話先行」も、印象ではなく回数を数えたから記事になった
- 一方で、電話番号を使い捨てのリソース扱いした設計判断が命取りになった。この顛末と教訓は前回の記事に詳しい
供養と転生
リポジトリは全履歴を監査のうえ公開した: yoshiharu-ishii/crossbar_telepath。READMEの墓碑銘から各技術記事へ辿れる。
心理分析のコア——話者別ストリーム→文字起こし→テキスト+声のトーン融合判定→リアルタイムUI——はPSTNに依存していない。マイク入力や会議音声に載せ替えれば電話番号なしで生き返る。「声は言葉より早い」は、電話の外でも成り立つはずである。
シリーズ全記事
- Terraformで電話網に「盗聴されない盗聴器」を建てる
- 通話路を覗くな、シグナリングを引け
- 4回電話したのに履歴が2件しかない — KVSから消えた呼を救出する
- 知能は網に埋めるな — 電話網のIN設計がAIの時代に蘇る
- create_allは黙って列を無視する — 後からAlembicを導入する
- 怒った通話をTTSで合成する — KVS互換MKVで架電せずに検証
- gpt-audioに「音声をお聞かせください」と言われる — 直したのはJSONの指示を書く場所
- 声は言葉より早い — 怒り判定がテキストより2発話先に警報を出すまで
- レート制限が永遠に回復しなくなった話 — 自動再接続とカウンタの共鳴
- Terraformで作って壊すたびに電話番号の枠が減る — 180日ルールでプロジェクトを畳んだ話


コメント