#音声合成#TTS#Whisper#データセット

AIが「こんにちわー」と伸ばす癖は、どこで身についたのか


学習させた音声モデルに「こんにちは。」と読ませると、「こんにちわぁ」と伸びた。台本には伸ばす指示など書いていない。

指摘はこうだった。

こんにちはですが、「こんにちわぁ」と発音し語尾にアクセントがつきます。何か混ざったような感じでしょうか。

「何か混ざったような」というのが正確で、実際に混ざっていた。学習コーパスに、語尾の伸びたクリップが入っていた。

問題は、それを検出する仕組みが原理的に機能しない作りだったことだ。

台本突合はしていた

コーパス生成では、TTSで読ませた音声をWhisperで書き起こし、台本と突き合わせている。

def _kana(s: str) -> str:
    # カタカナ→ひらがな
    return "".join(chr(ord(c) - 0x60) if "ァ" <= c <= "ヶ" else c for c in s)

_PUNCT_RE  = re.compile(r"[、。!?!?…・\s「」ー〜,\.]")
_REPEAT_RE = re.compile(r"(.)\1+")

def _collapse(s):
    return _REPEAT_RE.sub(r"\1", _PUNCT_RE.sub("", s or ""))

def judge_transcript(script_text, transcript, ...):
    a = _kana(_collapse(script_text))
    b = _kana(_collapse(transcript))
    sm = difflib.SequenceMatcher(None, a, b)
    ...

正規化してから比較する。素直な実装だ。

ここで _PUNCT_RE を見てほしい。落とす文字の中に (長音記号) が入っている。そして _REPEAT_RE は同じ文字の連続を1つに圧縮する。

台本: こんにちは
転写: こんにちわー

正規化後:
  台本 → こんにちは
  転写 → こんにちわ      ← 「ー」が消える

一致率は高い。「は」と「わ」の1文字違いだけになる。語尾が伸びていたという情報が、正規化の段階で捨てられている。

母音の連続でも同じことが起きる。

転写: こんにちわあ  →  _REPEAT_RE で「あ」の連続が圧縮 →  こんにちわ

つまりこの検証は、どうやっても語尾の伸びを検出できない。長音を無視するために書いた正規化が、長音を検出したい場面でも同じように働いていた。

正規化としては正しい。表記ゆれを吸収して内容の一致を見るなら、長音は落とすべきだ。目的が2つあるのに、正規化が1つしかなかったのが問題だった。

生の書き起こしで判定する

内容の一致を見る判定と、語尾の伸びを見る判定を分けた。後者は正規化前の文字列を使う。

_TAIL_LONG_RE = re.compile(r"[ーぁ-ん]$")

def trailing_elongation_mismatch(script_text: str, raw_transcript: str) -> bool:
    """台本に無い語尾伸びを検出する。

    ⚠️ raw_transcript は whisper の生の書き起こしを渡すこと。
    かな正規化は長音を捨てるため、正規化後の文字列では検出できない。
    """
    script = (script_text or "").rstrip("。、!?!? ")
    trans  = (raw_transcript or "").rstrip("。、!?!? ")
    if not script or not trans:
        return False

    # 台本の末尾文字が、転写では「伸ばされて」いないか
    tail_script = script[-1]
    # 長音記号が台本に無く転写にある
    if "ー" not in script and trans.endswith("ー"):
        return True
    # 同じ母音の重複が転写にだけある(例: 「です」→「ですぅ」「ですう」)
    if len(trans) > len(script) and trans[len(script)-1:].startswith(tail_script):
        extra = trans[len(script):]
        if extra and all(c in "ぁぃぅぇぉあいうえおー" for c in extra):
            return True
    return False

判定を分けたので、呼び出し側は2つを別々に見る。

res  = judge_transcript(text, tr["text"])                  # 内容の一致(正規化あり)
tail = trailing_elongation_mismatch(text, tr["text"])      # 語尾の伸び(生の文字列)

if tail:
    continue          # 語尾が伸びていたら即再抽選(内容が合っていても採らない)
if res.ok:
    save(wav)

語尾の伸びは、内容が合っていても不合格にする。 内容の一致度がどれだけ高くても、伸びていたら学習素材に入れない。ここを緩めると癖が焼き付く。

許容の設計

とはいえ完全にゼロにすると歩留まりが落ちる。感情の乗った音声では、多少の伸びは自然に出るからだ。

最終的にこういう2条件のORにした。

def accept(text, transcript):
    v    = judge_transcript(text, transcript)
    tail = trailing_elongation_mismatch(text, transcript)
    return (v.ratio >= 0.82 and tail <= 2) or (v.ratio >= 0.70 and tail == 0)
  • 内容がよく合っている(0.82以上)なら、語尾の伸びを2つまで許す
  • 内容の一致がやや低い(0.70以上)なら、語尾の伸びは0でなければならない

「内容が怪しくて、かつ語尾も伸びている」クリップを確実に落とす形だ。感情表現による認識低下は許容しつつ、伸びの癖だけは通さない。

キャプション側でも殴る

もう1つ効いたのが、生成時のキャプションに書いてしまうことだった。

役割別の声を設計するとき、キャプションにこう入れた。

ニュース原稿を正確に読み上げる女性アナウンサーの声。明瞭で聞き取りやすく、
落ち着いた知的なトーンで、語尾まではっきりと発音する。

最後の「語尾まではっきりと発音する」がそれだ。

効果は明確だった。24候補×5プローブ文=120クリップを生成して測ったところ、語尾の伸びが全件ゼロ。品質ゲートで弾く前に、そもそも伸びた音声が生成されなくなった。

役割込みcaption(語尾まではっきり) → 語尾伸び 0/120

ゲートは「悪いものを捨てる」仕組みだが、捨てれば歩留まりが落ちる。上流で出さないようにできるなら、そのほうが安い。 キャプションで指示できるTTSなら、品質ゲートが見ている項目をキャプションにも書いておく価値がある。

話体との関係

この現象を追ううちに、もう一段深い構造が見えた。語尾の伸び方は用途によって望ましさが違う

  • ナレーター、アナウンサー、コールセンター → 語尾を締めてほしい
  • VTuber、配信 → 伸ばしたほうが自然

そこで用途ごとに「話体」を定義し、コーパスの台本と品質ゲートの厳しさを話体ごとに変えるようにした。業務系の話体では語尾ゲートを厳格に適用し、カジュアル系では緩める。

重要なのは、話体はコーパスに焼き付いて、合成時には変えられないことだ(学習後に話速は変えられない)。同じ声で「締まった語尾」と「伸びる語尾」の両方が欲しければ、同じ設計値(キャプション+seed)で話体だけ変えて2本焼く。実際そうしている。

まとめ

正規化は目的ごとに分ける。 内容の一致を見る正規化と、特定の異常を見る正規化は別物。1つで兼ねようとすると、どちらかが機能しなくなる。

捨てる情報を意識する。 _PUNCT_RE を入れたのは正しい判断だったが、その情報を必要とする判定が後から生まれた。正規化のコメントに「何を捨てているか」を書いておくと、次に触る人が気づける。

上流で出さないほうが安い。 ゲートで捨てると歩留まりが落ちる。生成側で指示できるなら、そちらで殴る。

症状はモデルの振る舞いに出る。 コーパスのデータを見ても「語尾が伸びたクリップが数本ある」だけで、異常には見えない。学習して喋らせて初めて癖として現れる。データセットの検査だけでは足りず、学習後の出力を確認する工程が要る。


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

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

← 前: クリップ1本の粗さが、スタイル全体を嗄れさせる → 次: 「少々」が「しょも」になる — 許可文字リストが日本語を削っていた

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

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

← 記事一覧へ