#TTS#日本語処理#Unicode#音声合成

「少々」が「しょも」になる — 許可文字リストが日本語を削っていた


問い合わせ窓口のアバターについて、こういう報告を受けた。

「しょうしょうおまちください」が「しょもおまちください」となります。

音声モデルを何度も焼き直した直後だったので、まずモデルを疑った。結論から言うと、モデルもパラメータもキャッシュも正常で、TTSに渡る入力テキストが壊れていた

川下から順に潰した

疑った順に書く。この順番自体が反省点になる。

モデル。窓口サイトがどのモデルを引くかをDBで辿った。最新の学習済みモデルを正しく引いていた。

キャッシュ。TTSキャッシュのテーブルがあったので、古い音声を返している可能性を見た。中身は1か月以上前の別プロバイダのエントリだけで、無関係だった。

合成パラメータ。ランタイムは style_weight=2.0sdp_ratio=0.8 を使っていた。私の検証は 1.0 / 0.4 だったので、ここが原因ではないかと考えた。振って測った。

style_weight 1.0 / 2.0 / 3.0 / 4.0   → 全部 ratio 1.00
sdp_ratio    0.2 / 0.4 / 0.6 / 0.8   → 全部 ratio 1.00
全12スタイル × weight 2.0             → 全部 ratio 1.00

感情スタイル。12スタイル全部で「少々お待ちください。」を合成したが、どれも正常だった。

経路。音声パイプラインを別サービスに移していたので、そちらが独自に合成している可能性を見た。合成はバックエンド側で行っており、経路は同じだった。

ここまでで数時間。全部シロだった。

前処理の出力を1回 print した

残っていたのは入力テキストだけだった。

>>> _clean_tts_text('少々お待ちください。')
'少お待ちください。'

「々」が消えている。そして消えた文を合成すると、こう読まれた。

入力 '少々お待ちください'  → 聞こえ '少々お待ちください'      (正常)
入力 '少お待ちください'    → 聞こえ 'ショーをお待ちください'   ← これ

「ショーを」が「しょも」に聞こえていた。5分で終わる確認を、最後にやった。

原因: 漢字の範囲に「々」が入っていない

前処理には、顔文字・絵文字・特殊記号を落とすホワイトリストがあった。

# TTS前処理: 読み上げに不要な記号・顔文字を除去
_TTS_ALLOWED_RE = re.compile(
    r"[^぀-ゟ"   # ひらがな
    r"゠-ヿ"     # カタカナ
    r"一-鿿"     # 漢字
    r"ヲ-゚"     # 半角カタカナ
    r"a-zA-Za-zA-Z"
    r"0-90-9"
    r"、。!?,."
    r"\s"
    r"]"
)

意図は明快で、実装も素直だ。問題は 一-鿿(CJK統合漢字)に**「々」が含まれないことにある。「々」は で、CJK記号・句読点ブロックにいる。漢字ではなく記号**として分類されている。

日本語を書く人間にとって「々」は漢字の仲間だが、Unicodeの分類はそうなっていない。

網羅したら7種類あった

1文字見つかったということは、他にもあるはずだ。日本語で使うのにCJK統合漢字の外にある文字を総当たりした。

文字 結果 実害
少々・日々 少・日 「しょも」
〆切 「きり」
〇月〇日 月日 日付が消える
𠮷(CJK拡張A) 𠮷野家 野家 固有名が壊れる
髙 﨑(互換漢字) 﨑山 人名が壊れる
10〜20分 1020分 数値が別物
: 3:30 330 時刻が別物

接客窓口で「﨑山さま」が「山さま」になるのは、かなりまずい。「10〜20分」が「せんにじゅっぷん」になるのも同様だ。

一方 % & 「」 も落ちるが、これは読み上げに不要なので意図どおりだった。全部を残せばいいという話ではない。

記号は温存しても直らなかった

素直に許可リストへ : を足した。悪化した。

'午後330に開始します'   → 聞こえ '午後330に…'          (数値が誤り)
'午後3:30に開始します'  → 聞こえ '5も30、30に開始します'  ← 温存すると更に悪い

TTSは : を時刻として解釈できず、「ごも」のような雑音を出す。 も「から」ではなく読点として扱われる。

除去すると意味が変わり、温存すると読めない。 どちらも不正解だった。

日本語に開く

正解は3つ目の選択肢だった。前処理の段階で日本語に変換してしまう。

_TTS_CLEAN_PATTERNS = [
    # ⚠️ 数値の意味を持つ記号は、除去でも温存でもなく「日本語に開く」。
    # 除去すると「10〜20分」→「1020分」、温存しても TTS が読めず
    # 「3:30」→「5も30、30」のような雑音になる(実測)。
    (re.compile(r"(\d)\s*[〜~~]\s*(\d)"), r"\1から\2"),   # 10〜20 → 10から20
    (re.compile(r"(\d{1,2})\s*[::]\s*(\d{2})"), r"\1\2"),      # 3:30 → 3時30分
    ...
]

⚠️ 置換は除去より前に置く。 後だと 10〜20 が先に 1020 になってしまい、置換対象が消えている。

そして読みに必要な文字は許可リストに足す。

r"一-鿿"     # 漢字
# ⚠️ 日本語の読みに必要なのに、CJK統合漢字の外にある文字群。
# 実機で「少々お待ちください」が「少お待ちください」になり、
# 「ショーをお待ちください」と発音された。
r"々〆〻"  # 々 〆 〻(繰り返し・略字)
r"〇"           # 〇(漢数字ゼロ)
r"㐀-䶿"    # CJK拡張A(異体字・人名)
r"豈-﫿"    # CJK互換漢字(髙 﨑 等の人名異体字)

実音声で確認した。

'少々お待ちください'          → '少々お待ちください'       ✅
'髙橋・﨑山'                 → '髙橋・﨑山'              ✅
'10から20分ほどかかります'     → '10から20分ほどかかります'  ✅
'午後3時30分に開始します'      → '午後3時30分に開始します'   ✅
'受付は9時00分から17時00分です' → '9時0分から17時0分'        ✅

⚠️ 波ダッシュには別の落とし穴がある。(U+301C) と (U+FF5E) は別文字で、環境によって使われる方が違う。片方だけ書くと片方が漏れる。両方入れた。

ついでに見つかった: 発音辞書が効いていなかった

原因を追う過程で、この窓口サイトに発音辞書が1件も適用されていないことが分かった。辞書はプロジェクト単位でスコープされていて、既存の44件は全部別プロジェクトに紐づいていた。

窓口の声で読ませてみた。

表記 正しい読み 実際の読み
液冷 エキレイ できげ
主な オモナ オーナー
従量課金 ジューリョーカキン 重量価値
行っています オコナッテイマス 言っています

**「行っています」が「言っています」**になるのは、接客文で頻出する上に意味が反転する。

44件のうち自社製品名・人名の10件を除いた34件は、どのサイトでも必要な汎用語(技術用語と、訓読み/音読みが割れる日本語)だった。他の3プロジェクトへ展開した。

学んだこと

入力を最初に見る。 モデル・パラメータ・キャッシュ・経路を数時間かけて潰し、最後に前処理の出力を1回 print して終わった。順番が逆だった。

ホワイトリストは「何を通すか」を決めているつもりで、「何を落とすか」を決めている。 落とすものを列挙する方式なら、想定外の文字は通る。通すものを列挙すると、想定漏れが即座に欠落になる。日本語のように文字種が多い言語では、ホワイトリストの網羅は難しい。

除去か温存かの二択に見えるとき、三つ目がある。 記号は「落とすと意味が変わり、残すと読めない」という板挟みだったが、日本語に開くという選択肢があった。前処理は「不要なものを消す場所」だと思い込んでいて、「意味を保ったまま形を変える場所」だとは考えていなかった。

同じ分類の文字を総当たりする。 「々」が1つ見つかった時点で、他にも同じ理由で落ちる文字があると考えて調べた。7種類出た。1つ直して終わりにしていたら、次は人名の異体字で同じ報告を受けていたはずだ。


シリーズ: 拡散TTSから実用ボイスを量産する

キャプション1行から声を設計し、学習コーパスを製造して、役割別の実用ボイスを量産するまでの記録です。この記事は第3部 品質ゲートにあたります。

← 前: AIが「こんにちわー」と伸ばす癖は、どこで身についたのか → 次: ハルシネーション対策のコードが、ハルシネーションの時だけ動かなかった

シリーズ全18本
  1. 音質で選んだTTSが、会話には遅すぎた
  2. 声をガチャで引く
  3. 「ナレーターっぽい声」を24候補から機械に選ばせる
  4. 品質ゲートを厳しくするほど、棒読みが生き残る
  5. 学習後に話速は変えられない
  6. 生成するたび「録音場所」が変わるTTS
  7. クリップ1本の粗さが、スタイル全体を嗄れさせる
  8. AIが「こんにちわー」と伸ばす癖は、どこで身についたのか 9. 「少々」が「しょも」になる — 許可文字リストが日本語を削っていた ← いまここ
  9. ハルシネーション対策のコードが、ハルシネーションの時だけ動かなかった
  10. 品質ゲートが許した「3文字」が、モデルの口癖になった
  11. 治せる欠陥で候補を落としていた
  12. 文字起こしでは見つからない欠陥がある
  13. 70分かけた学習素材が、ネットワークの一瞬の瞬きで消えた
  14. “ja” を “JP” と書いて喃語モデルができるまで
  15. 登録経路が4つ、管理画面が0
  16. デプロイのたびに互いの成果を消していた
  17. 測れていないものを閾値で追い込むと、必ず失敗する

知見の元になったノートは 拡散TTSから実用ボイスを量産する製造パイプライン にまとめてあります。

← 記事一覧へ