TL;DR
- 解説動画の読み上げ品質を上げるために、最初はユーザー辞書に語を登録していました。辞書で潰せないものが3種類あります
- 潰せないもの1: 複合語での再発。単独では正しく読めるのに、長い複合語に入ると別の読みになる
- 潰せないもの2: 記号混じりのLatin語。トークナイザが分割するので語として一致せず、辞書に登録しても無視される
- 潰せないもの3: 文脈依存の同形異音語(
方=かた/ほう)。辞書で片方に固定すると、もう片方の用法が壊れる - 結果、読み品質を3層で作りました。語単位辞書 / アクセント推定 / 文脈依存の文単位オーバーライド
- 人手の正解読み表を丸ごと辞書に入れてはいけません。96語のうちズレていたのは6語で、残り90語を上書きすると正しい読みを壊します
- そしてアクセント(高低)はカナからは見えないので、最後は音声を生成して耳で確定します
観測期間: 2026-07 〜 2026-08
はじめに
スライドと原稿から解説動画を自動生成しています。音声合成にはVOICEVOX(四国めたん、エンジン0.25.2)を使っています。
日本語の音声合成は、普通の文はかなり自然に読みます。壊れるのは固有名詞と専門用語です。動画は人に見せるものなので、社名や人名を間違えたまま公開するわけにはいきません。
最初はユーザー辞書に登録していけばよいと思っていました。実際にはそれでは足りませんでした。この記事は、何が辞書で潰せて何が潰せないか、という整理です。
なお、この記事は「LLMに読みを判定させたら規模で壊れた」という別の記事の前提になっています。3層のうち3層目がその工程です。
1. まずエンジンのバージョンを上げる
品質の話をする前に、これが一番効きました。
古いバージョン(2021年のもの)を使っていて、不自然さの主因がそれでした。0.25.2に上げたら別物になりました。チューニングの前にバージョンを確認する のは、当たり前ですが書いておきます。
あわせて実運用のための設定です。
- GUIを出さずにヘッドレス起動する(エンジンの実行ファイルにホストとポートを指定する)
- 未起動だとポートが無応答になるので、ビルド前に起動確認を入れる
2. 文単位で合成する
長文を一括で合成すると、のっぺりします。文ごとに合成して結合すると、韻律がリセットされて自然になります。
確定した構成はこれです。
文分割(。!?)して合成
+ intonationScale = 1.2
+ 文末ポーズ 0.25 秒
ここで1つバグを踏みました。
「…使われるの?」です
→ ? で文が割れる
→ 「です」が単独で読まれる
? で分割すると、閉じ括弧の後の「です」が孤立します。閉じ括弧の直前では分割しない、という規則に直しました。日本語の文分割は、句点や疑問符だけを見ていると括弧で破れます。
3. 第1層: 語単位の辞書
固有名詞と熟語の訓読み誤りは、ユーザー辞書で潰せます。
発見例を1つ。
利益相反 → リエキ・アイハン
("相反" を訓読みしてしまう。正しくは リエキ・ソウハン)
これは辞書に登録して解決します。登録時のパラメータで効くのは2つです。
- アクセント核の位置(0を指定すると平板になる)
- 優先度(長い複合語を優先させる)
落とし穴: 複合語での再発
一番驚いたのがこれです。
「七夕」を単独で読ませる → タナバタ(正しい)
「株式会社七夕研究所」を読ませる → カブシキガイシャ・ナナユウ・ケンキュウショ
単独では正しく読める語が、長い複合語に入ると別の読みで再発します。 トークナイザが複合語を別の区切りで解析して、別の読みを当てるからです。
対策は、複合語ごと登録することです。「七夕」を直しても「株式会社七夕研究所」は直りません。実際に使う形で登録します。
これは辞書運用の考え方を変えました。「語を登録する」のではなく「原稿に出てくる文字列を登録する」と考えたほうが正確です。
落とし穴: 記号混じりのLatin語は辞書で直らない
これは辞書では原理的に無理でした。
J-SOX → 辞書に登録しても効かない
(トークナイザが J / - / SOX に分割するので、語として一致しない)
対策は、合成前のテキスト置換です。辞書の手前に、単純な文字列置換のテーブルを置きました。
J-SOX → ジェイソックス
Privacy J-SOX → プライバシージェイソックス
vERA → ブイエラ
SaaS → サース
つまり第1層は実質2段構えです。
replacements(事前テキスト置換)
↓
words(ユーザー辞書)
↓
合成
replacements が要るのは、辞書がトークナイザの語境界に依存するからです。語として認識されないものは、辞書には入りません。
語の前後に「間」を作る方法
新しい固有名詞は、前後に少し間があると聞き取りやすくなります。これの実装で1つ罠がありました。
エンジンには句のポーズを指定する機能があります。それでは駄目でした。
「統計作成等特例」に続けて助詞「と」がある場合、
エンジンは助詞を語句に吸着させる
→ 「統計作成等特例と」を1つの語句として扱う
→ 辞書の語一致が崩れる
正解はこうでした。
1. 語の前後でテキストを割って、別々に合成する
2. prePhonemeLength / postPhonemeLength = 0 にして、
エンジンが自動で入れる余白を殺す
3. 自前で無音 0.3 秒を挟む
自動で入る余白を残したまま自前の無音を足すと、間が長すぎます。まず自動を切って、それから自分で入れるという順番です。
4. 第2層: アクセント推定
カナが正しくても、高低が違うと不自然に聞こえます。
ここは機械学習によるアクセント推定を入れました。Apache-2.0のライブラリを常駐サービスとして立て、合成側からHTTPで叩く構成です。モデルのロードが重いので、プロセスを都度起動する形にはできませんでした。
サービスが上がらなければ自動で無効化するようにしています。アクセント推定はあくまで品質向上で、これが原因でビルド全体が止まると本末転倒だからです。
アクセントはカナからは見えない
これが第2層の一番重要な性質です。
読みの確認をカナの一覧で行っていると、アクセントの誤りは1件も見つかりません。表記が同じで高低だけ違うからです。
なので工程にこれを入れました。
描画を抜いて全スライドの音声だけを生成し、耳で確認する。
動画の描画は1本で1時間以上かかります。音声だけなら数分です。読み合わせのループを高速化するために、音声だけ生成するモードを作りました。
実際に耳で確定した語があります。ある固有名詞のアクセント核の位置と、その語の前後の無音の長さは、聞いて決めました。カナの点検では絶対に出てきません。
5. 第3層: 文脈依存の文単位オーバーライド
辞書で潰せない最後の種類が、同形異音語です。
方 → かた(人)/ ほう(方向)
行った → おこなった / いった
辞書に 方→かた と登録すると、「方向のほう」まで壊れます。語単位では解けません。
そこで、文単位で「この文はこう読む」という修正を持つ層を作りました。キーが文、値がその文の誤読部分だけをカナにしたものです。
words(語単位) ← 語として一意に決まるもの
reading_overrides(文単位) ← 文脈で変わるもの
この層に入れる修正をどう見つけるかが、実は一番難しい問題でした。全文を耳で聞くのは現実的ではないので、LLMに判定させることにしました。
そして、それが規模で壊れました。 79枚に広げたら、検証を通過した23件のうち18件が誤修正でした。その話は分量があるので別記事にしています(「LLM-as-judgeを79枚に広げたら、通過23件のうち18件が誤修正だった」)。
この記事の文脈で書いておくべき結論は1つです。
第3層は「自動で埋まる層」ではない。
機械の提案を、決定論のガードで削ってから入れる層である。
6. 人手の正解表を丸ごと辞書に入れてはいけない
案件によっては、人が作った正解読み表があります。講義資料の固有名詞リストのようなものです。
これを見つけたとき、素朴に「全部辞書に入れればいい」と考えました。間違いでした。
実測すると、96語の正解表のうち、エンジンの読みとズレていたのは6語だけでした。残り90語は元から正しく読めていました。
全部登録すると何が起きるか。
正解表の 96 語をすべて辞書に登録
→ 元から正しかった 90 語にも
辞書のアクセント指定(平板など)が適用される
→ 正しかった読みが壊れる
辞書登録は「読みを与える」だけでなく「アクセントを固定する」操作です。指定しなければ既定値が入り、それがエンジンの推定より悪いことがあります。
なので工程を1段足しました。
正解表 と エンジンの実読み を突き合わせ、
ズレた語だけを辞書に入れる。
出力は「一致」「辞書で解決済み」「ズレ」の3分類にしました。何を触らなかったかも記録しておかないと、次に同じ判断ができません。
「正解データがあるなら全部入れる」は、既存の挙動が正しいかを確認していません。 差分だけ触る、という原則がここでも効きます。
7. まとめると層の順番が防御になっている
3層を並べると、こうなります。
| 層 | 何を潰すか | 潰せないもの |
|---|---|---|
| 0. 事前テキスト置換 | 記号混じりのLatin語 | 語として認識されるもの全部 |
| 1. 語単位辞書 | 固有名詞、熟語の訓読み誤り、複合語 | 文脈で変わるもの、高低 |
| 2. アクセント推定 | 高低 | カナそのものの誤り |
| 3. 文単位オーバーライド | 文脈依存の同形異音語 | (機械の提案を人がガードする) |
そして順番に意味があります。
上の層で人が確定したものは、下の層の機械的な提案から保護されます。 第1層の辞書に人名を登録しておけば、第3層のLLMがその人名を蒸し返しても、辞書と重なる候補として自動的に落とせます。
逆に、第3層から先に手を付けると、人が確定した結論が機械の判定にさらされます。
まとめ
- チューニングの前にエンジンのバージョンを確認する。ここが一番効くことがある
- 長文一括ではなく文単位で合成する。文末にポーズを入れる
- 日本語の文分割は、閉じ括弧の直前で割らない
- 単独で正しく読める語が、複合語では別の読みで再発する。 実際に使う文字列で登録する
- 記号混じりのLatin語は辞書で直らない。語として認識されないものは辞書に入らない
- 語の前後の「間」は、句のポーズ指定では作れない。テキストを割って自前の無音を挟む
- 自動で入る余白を殺してから、自分で余白を入れる
- アクセント(高低)はカナからは見えない。 音声を生成して耳で確認する工程が必要
- そのために、描画を抜いて音声だけ生成する高速ループを用意する
- 文脈依存の同形異音語は語単位では解けない。文単位の層が要る
- 人手の正解表を丸ごと辞書に入れてはいけない。 ズレた語だけを入れる
- 辞書登録は読みを与えるだけでなく、アクセントを固定する操作でもある
- 層の順番が防御になる。上の層で人が確定したものが、下の層の機械的提案から守られる
日本語の読み上げ品質は、「辞書に登録する」という単一の作業では作れませんでした。何が語として認識されるか、何が文脈に依存するか、何がカナに現れないか。性質ごとに層を分けて、それぞれ違う方法で確認するという形になりました。
そしてこの構造は、前に書いたインフラ側の記事と同じ形だと思っています。単一の強い関門ではなく、見落としの方向が違う関門を重ねる。品質の作り方としては、領域が変わっても同じ形に落ち着くようです。
この記事は 七夕研究所 Qiita Organization の記事です。開発と運用で実際に踏んだものを順次書いています。