クリップ1本の粗さが、スタイル全体を嗄れさせる — 5本の選び方
使っているTTSは、感情スタイルを「代表クリップ数本」から作る。喜びスタイルなら、喜びの音声を数本渡すと、その平均的なスタイルベクトルが登録される。
最初は素直に、そのグループのクリップから先頭5本を渡していた。
clips = corpus_clips[emotion][:5]
register_style(model_id, style_name=emotion, clips=clips)
このスタイルで合成すると、声が嗄れた。 全部の文で、常に。
200本のうち1本が効く
原因は、渡した5本の中に粗いクリップが混ざっていたことだった。
生成音声には一定の確率で品質の低いものが出る。声がガサつく、ピッチが不安定に揺れる、途中でオクターブが飛ぶ。品質ゲート(台本との突合)は通る。内容は正しく読めているので、音の粗さは検出されない。
コーパス全体は約200本あるので、粗いのが数本混ざっていても学習全体への影響は薄い。ところがスタイル登録に使うのはたった5本だ。そこに1本混ざると影響が20%になる。
そして平均への希釈が期待ほど効かない。スタイルベクトルは埋め込み空間での平均だが、粗い音声は「嗄れ」という方向に大きく外れた位置にあることが多い。4本の正常なクリップと1本の外れ値を平均すると、重心はしっかり外れ値の方向に引っ張られる。
結果として、そのスタイルで合成した音声すべてに嗄れが乗る。1本の粗さが、そのスタイルの全出力に伝播する。
何を測れば「粗い」と言えるか
聴いて「粗い」と感じるものを分解すると、主に2つだった。
ジッタ(周期の揺らぎ)。声帯振動の周期が安定していない状態。ガサついた印象になる。
オクターブ跳躍。F0推定が隣接フレームで2倍/半分に飛ぶ。実際に声が裏返っている場合と、推定器の誤りの場合があるが、どちらも「不安定な音」として現れる。
この2つを測って、少ないものを選ぶ。
def stability_score(wav_bytes):
"""(jitter, octave_jump_rate) を返す。小さいほど安定。"""
f0 = extract_f0_series(wav_bytes) # 10msごとのF0系列
voiced = f0[f0 > 0]
if len(voiced) < 10:
return None # 有声区間が短すぎて判定不能
# ジッタ: 隣接周期の相対差の平均
periods = 1.0 / voiced
jitter = float(np.mean(np.abs(np.diff(periods))) / np.mean(periods))
# オクターブ跳躍: 隣接フレームで比が2倍/半分に近い箇所の割合
ratio = voiced[1:] / voiced[:-1]
jump = float(np.mean((ratio > 1.8) | (ratio < 0.55)))
return jitter, jump
def stable_top(clips, n=5):
"""安定した順に n 本を選ぶ"""
scored = []
for c in clips:
s = stability_score(c.wav)
if s is None:
continue
jitter, jump = s
scored.append((jitter + jump * 2.0, c)) # 跳躍は重めに見る
scored.sort(key=lambda x: x[0])
return [c for _, c in scored[:n]]
オクターブ跳躍を重め(×2.0)にしているのは、聴感への影響が大きいからだ。ジッタは連続的な粗さだが、跳躍は「声が裏返る」という離散的な破綻として耳につく。
登録側はこう変わった。
# 変更前
clips = corpus_clips[emotion][:5]
# 変更後
clips = stable_top(corpus_clips[emotion], n=5)
これで嗄れが消えた。
有声区間が短いクリップを弾く
if len(voiced) < 10: return None の部分が地味に要る。
短い掛け声(「はい!」「よし!」など)や、無声音が多い文(「ですし」「しっかり」)では、有声フレームが数個しか取れない。そこからジッタを計算しても意味がなく、たまたま小さい値が出て「最も安定している」と誤判定されることがあった。
判定不能なものは候補から外す。5本選べなければ、その時点で選べるだけ選ぶ。
⚠️ 低い声でジッタが過大に出る
この指標には既知の弱点がある。自己相関でF0を取ると、低い声ほど推定を誤る。倍音を基本周波数と取り違えたり、フレーム間で推定が飛んだりする。それがジッタとして計上される。
実測では、男性の声で31〜56%という値が出た。人間の正常な発声のジッタは1%未満なので、明らかに計測誤差が支配的だ。
したがって、
- 同じ話者内で相対比較する分には使える。誤差が一様に乗るので、順位は意味を持つ
- 話者間、特に男女間の比較には使えない。低い声が一律に不利になる
スタイル登録は「同じ話者のクリップの中から選ぶ」用途なので、前者に該当する。問題なく使えた。
別の用途(複数の候補音声から良い声を選ぶ)で同じ指標を重み付きで使ったところ、男性候補が全員マイナススコアになるという事故が起きた(「ナレーターっぽい声」を24候補から機械に選ばせる)。指標の適用範囲を間違えた例で、そちらでは重みを下げて別の判断材料を出す形に直した。
品質ゲートの層を分ける
この件で分かったのは、「内容が正しい」と「音が綺麗」は別のゲートが要るということだった。
| ゲート | 見るもの | 落とすもの |
|---|---|---|
| 台本突合(Whisper) | 内容の一致・台本外の音 | 幻覚、読み間違い、欠落 |
| 語尾忠実 | 台本に無い語尾伸び | 語尾の癖が焼き付く素材 |
| 音響正規化 | 周波数特性・音圧 | チャンネル特性のばらつき |
| 安定選抜 | ジッタ・オクターブ跳躍 | 粗い音・嗄れ |
台本突合は内容しか見ないので、粗い音声を通す。音響正規化(生成するたび「録音場所」が変わるTTS)はスペクトルの形は揃えるが、時間方向の不安定さ(ジッタ)は直せない。それぞれが別の層を見ていて、代替が効かない。
そして安定選抜だけは「落とす」ではなく「選ぶ」ゲートだった。他は不合格を除外するが、これは合格者の中から上位を採る。少数精鋭で使うリソースには、除外ではなく選抜が要る。
影響範囲から逆算して厳しさを決める
一般化すると、こういう考え方になる。
1本あたりの影響が大きい箇所ほど、厳しく選ぶ。
- 学習コーパス200本 → 1本の影響は0.5%。品質ゲートで足切りすれば十分
- スタイル登録5本 → 1本の影響は20%。足切りでは不十分で、上位を選抜する必要がある
同じ素材プールから取っていても、使われ方によって必要な品質が違う。コーパスに入れてよいクリップと、スタイル代表にしてよいクリップは、別の基準で選ぶ。
これを見落としていたのは、「品質ゲートを通ったのだから使える」と考えていたからだった。ゲートは「最低限使える」を保証するもので、「代表として適切」は保証しない。
まとめ
- 少数で使うリソースは、1本の外れ値が全体を壊す。5本中1本なら影響20%で、平均では希釈しきれない
- 粗さはジッタとオクターブ跳躍で測れる。跳躍は聴感への影響が大きいので重めに見る
- 有声区間が短いクリップは判定不能として外す。誤って「最も安定」と判定される
- 低い声でジッタが過大に出る。同一話者内の相対比較にのみ使う
- 除外ゲートと選抜ゲートは別物。少数精鋭で使う箇所には選抜が要る
- 影響範囲から逆算して厳しさを決める。同じ素材でも使われ方で必要な品質が違う
シリーズ: 拡散TTSから実用ボイスを量産する
キャプション1行から声を設計し、学習コーパスを製造して、役割別の実用ボイスを量産するまでの記録です。この記事は第2部 製造にあたります。
← 前: 生成するたび「録音場所」が変わるTTS → 次: AIが「こんにちわー」と伸ばす癖は、どこで身についたのか
シリーズ全18本
- 音質で選んだTTSが、会話には遅すぎた
- 声をガチャで引く
- 「ナレーターっぽい声」を24候補から機械に選ばせる
- 品質ゲートを厳しくするほど、棒読みが生き残る
- 学習後に話速は変えられない
- 生成するたび「録音場所」が変わるTTS 7. クリップ1本の粗さが、スタイル全体を嗄れさせる ← いまここ
- AIが「こんにちわー」と伸ばす癖は、どこで身についたのか
- 「少々」が「しょも」になる — 許可文字リストが日本語を削っていた
- ハルシネーション対策のコードが、ハルシネーションの時だけ動かなかった
- 品質ゲートが許した「3文字」が、モデルの口癖になった
- 治せる欠陥で候補を落としていた
- 文字起こしでは見つからない欠陥がある
- 70分かけた学習素材が、ネットワークの一瞬の瞬きで消えた
- “ja” を “JP” と書いて喃語モデルができるまで
- 登録経路が4つ、管理画面が0
- デプロイのたびに互いの成果を消していた
- 測れていないものを閾値で追い込むと、必ず失敗する
知見の元になったノートは 拡散TTSから実用ボイスを量産する製造パイプライン にまとめてあります。