4回電話したのに履歴が2件しかない — 消えた呼の救出記 crossbar_telepath 開発記 (3)

前回、通話路(KVS)とシグナリング(Lambda→SQS)を分離して、呼単位のリアルタイム文字起こしができるようになった。今回はその続きで、呼を「第一級オブジェクト」に昇格させる改修をした——ら、途中で呼が2本消えた。本稿の主役はその救出劇である。

呼を第一級オブジェクトにする

改修前のUIは、文字起こしがチャット的に流れていくだけの画面だった。動くには動くが、「さっきの通話をもう一度見たい」「録音を聴きたい」ができない。コールセンターの道具として考えると、欲しいのは交換機でいうCDR(呼詳細記録)+録音アーカイブの構造だ。

そこで呼(ContactId)をキーに、すべてを紐づけた。

  • recordings/calls/<ContactId>.mkv — KVSから受信した生のバイト列をそのままteeした録音
  • recordings/calls/<ContactId>.json — メタ情報(発信者番号・開始/終了時刻)と確定した発言

設計判断としていちばん効いたのは、録音を「原本」、文字起こしを「派生データ」と位置づけたことだ。生MKVは話者別2トラックのまま残っているので、文字起こしはいつでも作り直せる。UIには「再文字起こし」ボタンを付けた——同じContactIdのまま、録音をパイプラインに流し直して発言を上書きする。将来、文字起こしモデルを乗り換えたときに過去の全通話で聞き比べができるし、次フェーズの怒り検知も過去の呼に遡って適用できる。

録音の再生には一手間いる

「録音を聴く」ボタンは、実は素直には作れない。KVSのMKVはCodecIDがA_AACを詐称していて中身は生L16 PCMなので(開発記(1)参照)、ブラウザに直接渡しても再生できない。サーバー側でEBMLをパースしてWAVに変換するエンドポイントを立てた。

ここで小技をひとつ。話者別2トラックを左チャンネル=相手、右チャンネル=こちらのステレオWAVに振った。イヤホンで聴くと、話者分離がちゃんとできていることが耳で確認できる。デバッグ用の可視化ならぬ「可聴化」である。

事件: 履歴が2件しかない

一通り作って実架電テストを繰り返した後、気づいた。今日4回電話したはずなのに、履歴に2件しか残っていない。

原因はバグではなく、実装タイミングの境界だった。1・2本目の呼を受けたときのサーバーは永続化機能が入る前の版で、呼の記録をメモリにしか持っていなかった。その後、永続化を入れた新しいコードでサーバーを再起動した瞬間、メモリ上の2本は消えた。正確には「消えた」のではなく、最初から保存される仕組みが存在しなかったのだが、ユーザーから見れば同じことである。

救出作戦

ただし、詰んではいなかった。音声の原本はまだKVSに残っている。ストリームの保持期間を24時間にしてあったからだ。

必要なのは「どの時間帯のフラグメントが、どの呼のものか」の対応付けで、これは2段で復元できた。

  1. ContactIdの復元: Amazon ConnectのSearchContacts APIで当日の通話一覧を引くと、4本の呼のContactIdと開始時刻が正確に出てくる。交換機は呼の記録をちゃんと持っている
  2. 音声の切り出し: KVSのListFragmentsでフラグメントをproducer timestampつきで列挙し、20秒以上の空白を呼の境界として分割する。区間はきれいに2つに割れ、開始時刻もSearchContactsの記録と一致した

あとは区間ごとにGetMediaForFragmentListでMKVを取得し、recordings/calls/<ContactId>.mkvとして保存、メタ情報のJSONを添えれば、履歴に4本並ぶ。発言は空のまま保存しておき、「再文字起こし」ボタンで音声から再生成する——原本さえ救えば派生データは後からいくらでも作れる。この設計にしておいて本当によかった。

救出した2本は40秒と51秒。両チャンネルの音量を測って、左に自分の声・右にIVRのアナウンスが実在することも確認できた。

恒久対策: シグナリングは留守番電話になる

さて、同じ事故を二度と起こさないために何が要るか。実は、仕組みの9割はすでにあった。

前回作ったシグナリング経路を思い出してほしい。呼が張られるとLambdaが「ContactId・StreamARN・開始フラグメント番号」をSQSに積み、消費サービス(SCP)がそれを受けて処理を始める。ここで重要なのは、SQSのメッセージは消費されるまで残ることと、開始フラグメント番号があればKVSのアーカイブを遡って通話の先頭から読めることだ。

つまり、SCPが止まっている間に呼が来ても——

  1. 呼イベントはSQSで待っている
  2. SCPが起動した瞬間にそれを受信する
  3. 開始フラグメントからKVSアーカイブを読む(通話はとっくに終わっていても、音声は丸ごと取れる)

シグナリングのキューが、そのまま留守番電話になる。

呼イベントがSQSで24時間待ち、SCP起動時にKVSアーカイブを開始フラグメントから遡って読む構成図
SCPが寝ていても、呼はキューで待ち、音声はアーカイブに残る

穴は保持期間の不整合だけだった。SQSのメッセージ保持を1時間にしていたのに対し、KVSの音声保持は24時間。イベントの方が音声より先に消える設定になっていた。SQS側を86400秒に揃えて一行修正、terraform applyで完了。これで「サーバーを止めていた日に呼が来た」ケースは、翌日サーバーを起動するだけで自動的に履歴へ現れる。

今回の救出劇のような「イベントは消費したがコードが保存しなかった」ケースは自動では救えないが、それもKVSの24時間以内なら今回のスクリプトで手動復旧できる。

おまけ: 同じ罠を二度踏む

UI改修の直後、「画面が反応しなくなった」という症状が出た。サーバーのログは完全に正常。原因はブラウザが古いapp.jsをキャッシュしたまま新しいHTMLを表示していたことで、旧スクリプトが存在しないボタンIDを触って例外死していた。

これ、前作realtime_voiceで踏んでno-cacheミドルウェアを入れた、まったく同じ罠である。前作のコードにはご丁寧に「この事故の再発防止」というコメントまで書いてあった。移植し忘れた。静的ファイルにCache-Control: no-cacheを付けて再発防止としたが、教訓はミドルウェアではなく「前作の再発防止コメントは移植チェックリストである」のほうだと思う。

まとめ

  • 呼をキーに録音(原本)と文字起こし(派生)を分けると、再文字起こし・モデル比較・遡り分析が全部タダで手に入る
  • 録音の再生はサーバー側でWAV変換が要る。左右チャンネルに話者を振ると耳でデバッグできる
  • 取り逃した呼は、ContactIdはSearchContactsから、音声はKVSアーカイブから復元できる。保持期間内なら詰まない
  • シグナリングのキュー保持を音声の保持期間(24h)に揃えると、停止中の呼が自動で救済される。共通線が留守番電話を兼ねる
  • 前作の「再発防止コメント」は移植チェックリスト

次回はいよいよPH3、相手がキレているかどうかをAIに判定させる。録音が原本として揃ったので、怒った通話を1本録れば検証データは完成する。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール