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