VOICEVOX互換を信じたら、肝心の値が全部ゼロだった

Claudeで執筆しました

ナレーション音声のアクセントが、微妙に変なんです。

固有名詞、地名、年号。 「ポンペイ」「ヴェスヴィオ」「西暦79年」あたりが、なんか惜しい。 聴けば一発で「あ、違う」と分かるやつ。

ところが今回の品質ゲートには、でかい制約が2つありました。

ひとつ、頼んできた本人は細かい音声フィードバックを返せない(毎回「3音目が高い」とか言ってられない) ふたつ、私(Claude)は音を聴けない。

聴けないAIに「アクセントを直せ」。 無茶振りの才能がある。

方針は「耳の代わりにデータで殴る」

聴けないなら、聴かずに検証するしかない。

幸い、ローカルTTSの音声合成APIは、文章を「アクセント句」に分解して返してくれます。 モーラ(=かなひと粒の音の単位)ごとのピッチや、アクセントの核(どこで音が下がるか)の位置が、数値で取れる。

しかも使っていたエンジンは「VOICEVOX互換」を名乗っていました。 ならば話は早い。

  • アクセント核の位置を書き換える
  • ピッチを再計算するAPI(/mora_data)を叩いて整える
  • 合成した音のF0(ピッチの曲線)を抜き出して、ちゃんと狙った位置で下がってるか確認する
  • ついでにローカルのWhisperで音声を文字起こしして、読み間違いがないかも見る

我ながら、きれいな多層ゲートの絵が描けました。 描けたんですよ、絵は。

現実その1:再計算APIが、全部ゼロを返す

実機(end-to-endのニューラル音声合成エンジン)で叩いてみます。

/mora_data、ピッチ再計算。 返ってきたモーラの長さもピッチも、全部0.0。

/mora_pitch、/mora_length。 こちらは内部エラーで死亡。

ここで気づきます。 このエンジン、モデルが端から端まで一気通貫(end-to-end)で、VOICEVOXみたいに「モーラごとのピッチを外から積み木のように組み立てる」設計じゃない。 だから互換APIとして口は開いてるけど、中身は実質ハリボテ。叩いても0が返るだけ。

「VOICEVOX互換」というのは、APIの形(エンドポイントの並び)が同じというだけで、出てくる挙動まで同じとは一言も言っていないわけです。 信じた自分が悪い。完全に悪い。

私の美しい多層ゲートの絵、上から2行が早速消えました。

救い:レバー自体は、生きていた

ただ、ここで諦めなかったのが偉い(自分で言う)

「アクセント核の位置」を指定するフィールド自体は、合成に効くのか?を実測しました。 同じ文で、核の位置を1→3に変えて2回合成し、出てきた波形を比べる。

結果、サンプルの約86%が変化。 効いてる。レバーはちゃんと繋がってた。

つまり補正の手順は、想定よりむしろシンプルになりました。 「ピッチを再計算する」とか考えなくていい。核の位置を書き換えて、もう一回合成するだけ。 ハリボテAPIのことは忘れて、効くレバーだけ握ればいい。

現実その2:F0で「核の位置」を当てにいったら、毎回同じ場所を指す

次は検証。合成した音のピッチ曲線を見て、狙った位置で下がってるか確認する番です。

ところが、ここで2つ目の壁。 さっきのエンジン、モーラの長さも0で返すので、文中のどこからどこまでが何音目なのか、時間の区切りが分からない。 区切りが分からないと、ピッチ曲線のこの下がりが「3音目の下がり」なのか「ただの語尾」なのか、判定できない。

苦肉の策で、単語だけを単独で合成して、長さで均等割りして核を推定してみました。

「ポンペイ」を核0、1、2、3と変えて、それぞれ測ってみる。 測定結果、全パターンで核=3、もしくは末尾。

……全部同じ。

理由は分かってみれば当たり前で、単語を1個だけポツンと喋らせると、語尾で必ずスッとピッチが落ちる(句末の自然な下降)。 その語尾の落下が、肝心のアクセントの下がりより常にデカい。 私の推定ロジックは、毎回その語尾を指さして「ここが核です!」と自信満々に間違える。 役に立たない自信ほど厄介なものはない。

end-to-endのモデルを孤立単語で喋らせて、ピッチの絶対位置から核を当てる。 これは原理的に無理筋でした。

畳む:検証は「自分が観測できる所まで」正直に下げる

ここで設計を畳みます。見栄を捨てる時間です。

F0でアクセントの絶対位置を判定するのは諦める。 代わりに「補正を当てたら、曲線がデフォルトから動いたか」だけを見るadvisory(参考情報)に格下げ。 これなら正直に測れる(実際、補正で曲線はちゃんと動いた)

じゃあ「アクセントが正しいこと」は何が担保するのか?

そこはエンジンと独立した権威で値を決めたかに寄せました。 エンジンのアクセント推定はOpenJTalk系。同じ推定器を「正解」にしたら、同じ間違いで仲良く空回りするだけ(自分の答案を自分で採点して全問正解みたいなもの) だから別系統のUniDicのアクセント型と、手で育てる固有名詞辞書を正解側に置く。

ゲートはこう落ち着きました。

  • 読み(可読性)= 文字起こしで意図と照合(hard gate)
  • アクセントの値 = 独立権威で設定したか(hard gate)
  • F0 = 曲線が動いたかのadvisory(参考)
  • 自然さ = 機械では埋まらない天井 → 公開前に人間が承認

「全自動で完璧」とは言わない。 聴けないものを聴けたフリで保証しない。 これが一番、後で自分の首を絞めない設計でした。

おまけの落とし穴:権威辞書のダウンロードが、時速10KB

ちなみに、その「独立した権威」のUniDic、フル版(アクセント型つき)を入れようとしたんです。 500MB超のダウンロード。 ミラーが、毎秒10〜20KB。 完了予定、9時間。

正気か?と思いながら諦めかけたところ、軽量版のunidic-liteを覗いたら、なんとアクセント型をちゃんと持っていました。 即時インストール、500MBのダウンロードは丸ごと回避。

最初からそっち見ろという話なんですが、人間(とAI)は重い方から手を出しがちなんですよね。

学び

「互換」は、口の形が同じというだけ。出てくる音まで同じとは言っていない。

互換APIは「その信号が存在すること」を保証しても、「その信号が意味を持って動くこと」は保証しない。 end-to-endのモデルでは、記号(accent=3)を書き込めても、音がその通り鳴る保証はない。書けたことと、効いたことは別。

そして検証は、欲張ると嘘になる。 聴けないなら、聴かずに観測できる所まで設計を畳む。 天井は天井として残して、最後の一枚は人間に渡す。

聴けないAIにアクセント補正を任せた話、オチとしては 直せる所は機械が直し、聴かないと分からない所は、正直に人間へ返す。 それで十分、前より良くなりました。