Fish Audio v2/v3とElevenLabsで教材ナレーション音声を一括生成した記録
朝の時点では「今日中にChapter 04の音声を揃えよう」くらいの見積もりだった。蓋を開けてみると、Chapter 01の全68件を新モデルで再生成し、テキストの表記揺れを潰し、音声バリデーターをゼロから書き、R2にアップロードしてデプロイまで走り切っていた。キーボードから手を離した回数を数えられるくらい、端末に張り付いた一日だった。
Fish Audio v2でChapter 04の音声生成
まずFish Audio v2(モデルID: 85148776ce3c46099a974a2b33a75d01)で、教材のChapter 04に登場する講師キャラクターのセリフ33件を生成した。前日に作ったボイスクローンモデルと生成スクリプトがそのまま使えたので、ここはスムーズに進んだ。
v3モデルへの切り替えとテスト
v2で生成した音声を聴いていると、会計用語の発音が気になる箇所がいくつかあった。「貸借対照表」や「損益計算書」のイントネーションが、耳で聞いたときに違和感が残る。
Fish Audio v3(モデルID: b8dbcd3cb5fa4675949c5528f357222a)が使えるようになっていたので、同じテキストで比較テストした。v3の方が専門用語の発音が安定していた。v3をメインに切り替えることにした。
ボイスクローン用スクリプトの設計
v3でクローンモデルを作り直すにあたり、サンプル音声用のスクリプトを設計した。Fish Audioのボイスクローンは90秒のサンプル音声が上限になる。この制約の中で会計用語をできるだけ網羅する必要があった。
「貸借対照表」「損益計算書」「減価償却」「売掛金」「買掛金」など、教材に頻出する用語を自然な文脈に埋め込んだ読み上げスクリプトを作成した。90秒に収まるよう文字数を調整し、メモとして保存した。
ElevenLabsでのユイ音声再生成
講師キャラクターとは別に、生徒キャラクターの音声をElevenLabsのSarahボイスで再生成した。以前の生成分で品質にばらつきがあったものを差し替える作業だった。
Chapter 01の全セリフをv3で生成
Chapter 01の講師キャラクターのセリフ全68件をFish Audio v3で生成した。ここが今日の作業で一番ボリュームがあった。
テキストの整備
68件のセリフをAPIに投げる前に、テキストデータの整備が必要だった。ここで試行錯誤が発生した。
です・ます調への統一: 元のテキストは「だ・である調」が混在していた。教材のナレーションとしてはです・ます調の方が自然なので、全68件を変換した。
カギカッコの除去: セリフデータにカギカッコ(「」)が残っていると、TTSが不自然な間を入れることがある。全件からカギカッコを除去した。
ダブルクォーテーション問題の発見と除去: これが一番厄介だった。APIに投げた音声の一部で、生成結果が明らかにおかしい -- 途中で音声が途切れたり、発音が崩壊したりする。原因を切り分けていくと、テキスト中のダブルクォーテーション(")がAPIリクエストのJSONを壊していた。エスケープ処理が不十分だったのではなく、TTS側がダブルクォーテーションを含むテキストを正しく処理できていなかった。全セリフからダブルクォーテーションを除去したら、生成不良がなくなった。
文末の句点追加: 文末に「。」がないセリフがあり、TTSが文末を尻切れに読む原因になっていた。全セリフの文末に句点を追加した。
会計用語の読み替え: 「借方」「貸方」は初学者向け教材では馴染みが薄い。「仕訳の左側」「仕訳の右側」に統一した。これは発音の問題ではなく教材としてのわかりやすさの問題だが、テキスト整備のタイミングでまとめて対応した。
読み間違い対策
TTSエンジンは漢字の読みを間違えることがある。今回は「いし」が意図しない読みになるケースが見つかった。「小石」のように文脈を明示する表現に書き換えて対処した。前日に作った to_tts_text() 関数にもパターンを追加した。
VOICEVOX音声の欠落補完
講師キャラクターの音声をFish Audioで生成している途中で、ナレーターと生徒キャラクターのVOICEVOX音声に欠落があることに気づいた。37件分が未生成だった。VOICEVOXのAPIを叩いて37件を一括生成し、穴を埋めた。
TTS音声バリデーターの開発
音声ファイルが増えてくると、目視(耳視?)での品質チェックが追いつかなくなる。全ファイルを聴いて確認するのは現実的ではないので、Pythonで自動バリデーターを書いた。
検出ロジック
2つの観点で異常を検出する:
波形解析による途中切れ検出: wav/mp3ファイルの波形データを読み込み、末尾が不自然に切れていないかをチェックする。正常な音声は文末で波形が徐々に減衰するが、途中切れの音声は振幅が残ったまま突然終わる。
文字数対ファイルサイズの異常検出: 同じTTSエンジンで生成した音声なら、テキストの文字数とファイルサイズにはおおよそ相関がある。文字数あたりのファイルサイズが中央値から大きく外れているファイルを異常として検出する。
閾値の試行錯誤
ファイルサイズの異常検出の閾値を決めるのに3回調整した:
- 40% -- 中央値の40%未満をフラグ。検出漏れが多すぎた。明らかに短い音声が素通りする
- 60% -- 検出精度が上がったが、まだ怪しいファイルが数件すり抜ける
- 75% -- 中央値の75%未満で落ち着いた。誤検出(正常なのにフラグされる)が許容範囲に収まり、途中切れファイルをほぼ捕捉できた
最終的に75%を閾値として採用した。完璧ではないが、全ファイルを手で聴くよりは桁違いに速い。
R2アップロードとデプロイ
生成した音声ファイルをCloudflare R2にアップロードし、Cloudflare Pagesにデプロイした。
CDNキャッシュ問題の発覚
デプロイ後にブラウザで確認すると、差し替えたはずの音声が古いまま再生される。ブラウザキャッシュを疑ってハードリロードしても変わらない。
原因はCDNキャッシュだった。R2のファイルを更新しても、CDNエッジにキャッシュされた古いファイルが配信され続ける。キャッシュのパージが必要だということがわかった。同じファイル名で差し替える運用をしている以上、今後も同じ問題が起きる。キャッシュバスティング(ファイル名にハッシュを含める等)の対策が必要になりそうだ。
振り返り
テキストデータの整備に一番時間を吸われた。TTSの音声品質は、モデルの性能だけでなく入力テキストの品質に直結する。ダブルクォーテーション1つでAPIの生成結果が壊れるのを目の当たりにして、「前処理を甘く見るな」という教訓が骨身に染みた。
バリデーターの閾値を40%から75%まで3段階で引き上げていく過程は、機械学習のハイパーパラメータ調整に似ていた。「偽陰性を減らしたい、でも偽陽性が増えすぎると使い物にならない」というトレードオフを、小さなスクリプトの中で体感した。
CDNキャッシュの問題は、ローカルで動作確認してからデプロイする開発フローでは見落としやすい。「デプロイしたら終わり」ではなく、「CDNの先で何が配信されているか」まで確認するステップを入れる必要がある。