0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「ナレーターっぽい声」を24候補から機械に選ばせる — 耳で選ぶのをやめた日

0
Posted at

📝 この記事は forge.workstyle.tech に掲載した記事の転載です。

キャプションと乱数種で声が決まるTTSを使っている。キャプションを固定してseedを振ると、同系統だが異なる声が次々に出てくる。要はガチャだ。

"落ち着いた大人の女性ナレーターの声。ゆったりとした語り口で、
 温かみと信頼感があり、長い文章を丁寧に読み上げる。"

seed 7      → 声A
seed 42     → 声B
seed 1042   → 声C
...

役割ごとに良い声を引き当てたい。ナレーター、カウンセラー、営業、プレゼンター、コールセンター、司会——7役割ぶん、男女それぞれ。

最初は全部聴いていた。12seed × 男女 = 24候補、それぞれ5文ずつ喋らせるので120クリップ。1役割でこれをやると30分は溶ける。7役割で3時間半。続かない。

何を測れば「役割に合う」と言えるか

聴きながら何を判断しているのかを言語化した。

  • ナレーター: ゆったり喋っているか。落ち着いた起伏があるか
  • カウンセラー: 遅いか。柔らかいか(音圧が穏やか)
  • 営業: 明るいか。テンポが良いか。安定しているか
  • プレゼンター: 明瞭か。強調のメリハリがあるか
  • 司会: エネルギーがあるか(音圧・F0レンジが広い)。それでいて破綻していないか

だいたい話速・抑揚幅・音圧・安定性の組み合わせに落ちる。これなら測れる。

計測する

プローブ文を5本読ませて、1候補あたりこれだけ取る。

def acoustics(wav: bytes):
    """(発話秒, 音圧dB, F0中央値, 抑揚幅st) を返す"""
    w = wave.open(io.BytesIO(wav)); sr = w.getframerate()
    x = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16) / 32768

    # 有音区間だけを対象にする(無音を含めると音圧が過小になる)
    W, H = int(sr*0.025), int(sr*0.010)
    rms = np.array([np.sqrt(np.mean(x[i*H:i*H+W]**2))
                    for i in range(max(0, (len(x)-W)//H))])
    act = rms > 0.01
    speech = float(act.sum() * 0.010)
    level  = 20*np.log10(float(np.mean(rms[act])) + 1e-9) if act.any() else -99.0

    # F0は自己相関で取る
    f0 = []
    Wf, Hf = int(sr*0.030), int(sr*0.010)
    lo, hi = int(sr/950), int(sr/60)          # 60〜950Hz を探索
    for i in range(0, len(x)-Wf, Hf):
        fr = x[i:i+Wf]
        if np.sqrt(np.mean(fr**2)) < 0.01:
            continue
        fr = fr - fr.mean()
        ac = np.correlate(fr, fr, "full")[Wf-1:]
        ac = ac / (ac[0] + 1e-12)
        seg = ac[lo:min(hi, len(ac)-1)]
        if len(seg) >= 3:
            f0.append(sr / (lo + int(np.argmax(seg))))
    f0 = np.array(f0)
    med   = float(np.median(f0)) if len(f0) else 0.0
    # 抑揚幅は半音(semitone)で見る。Hzの差だと高い声ほど大きく出てしまう
    rng   = float(12*np.log2(np.percentile(f0,90)/np.percentile(f0,10))) if len(f0) else 0.0
    return speech, level, med, rng

話速はモーラ/秒で出す。文字数ではなく音の数で数えたいので、小書き文字と句読点を除いた文字数を発話秒で割る。

def mora(t):
    small = "ぁぃぅぇぉゃゅょ"
    return max(1, len([c for c in t if c not in "、。!?!?・… " + small]))

rate = mora(text) / max(speech_sec, 0.3)

抑揚幅は半音で測るのが重要だった。F0のHz差で測ると、高い声ほど数値が大きくなって男女比較ができない。90パーセンタイルと10パーセンタイルの比を対数で取る。

**安定性(ジッタ)**は、周期の揺らぎとオクターブ跳躍を見る。粗い声、嗄れた声を落とすため。

役割ごとに重みを変える

同じ計測値を、役割ごとに違う重みでスコア化する。

PRESETS = {
    "narration": {
        "captions": {"F": "落ち着いた大人の女性ナレーターの声。ゆったりとした語り口で…",
                     "M": "深く渋い男性ナレーターの声。低音で落ち着いた語り口…"},
        "probes": ["物語は、ここから始まります。",
                   "創業以来、私たちは品質にこだわり続けてきました。", ...],
        "rate_target": (4.5, 6.0),          # モーラ/秒の目標帯
        "weights": {"ratio": 1.0, "elong": 0.25, "rate_cv": 0.8,
                    "jitter": 1.5, "rate_fit": 1.0, "range": 0.3, "level": 0.0},
    },
    "mc": {
        ...
        "rate_target": (6.0, 8.0),          # 速め
        "weights": {"ratio": 0.8, "elong": 0.10, "rate_cv": 0.3,
                    "jitter": 1.5, "rate_fit": 0.5, "range": 1.0, "level": 0.0},
        #                                                    ↑ 抑揚を最重視
    },
    "counseling": {
        ...
        "rate_target": (4.0, 5.5),          # 最もゆっくり
        "weights": {..., "rate_fit": 1.2, "level": 0.4},
        #                              ↑ 音圧が高すぎると減点(穏やかさ)
    },
}

スコアはこう。

rate_fit = 0.0 if lo <= rate <= hi else min(abs(rate-lo), abs(rate-hi)) / 2.0

score = (w["ratio"]   * mean_ratio
       - w["elong"]   * elongation_count
       - w["rate_cv"] * rate_cv                      # 文ごとの話速のばらつき
       - w["jitter"]  * jitter
       - w["rate_fit"]* rate_fit                     # 目標帯からの逸脱
       + w["range"]   * (range_st / 12.0)            # 抑揚は加点(1oct=1.0換算)
       - w["level"]   * max(0.0, (level + 12.0)/6.0))  # 音圧が高すぎると減点

目標帯からの距離という考え方が効いた。「速いほど良い」でも「遅いほど良い」でもなく、役割ごとに帯があって、そこから外れた分だけ減点する。ナレーターは4.5〜6.0、司会は6.0〜8.0、カウンセラーは4.0〜5.5。

**話速のばらつき(cv)**も入れた。文によって速さが極端に変わる候補は、実運用で扱いにくい。

出力はランキングと、上位3候補×男女の試聴ページ。24候補を聴くのではなく、6候補を聴けばよくなった。 1役割あたり30分が5分になった。

実際に選んだ結果

ナレーターの上位はこうなった。

1位 2位 3位
女性 seed 55555(一致0.98・話速6.1) seed 7(抑揚19.8st) seed 864200(一致1.00)
男性 seed 3407(話速5.2=最もゆったり) seed 101(一致1.00) seed 2025

話速が5.2〜6.1に収束していて、アナウンサー役(6.2〜6.6)より明確にゆっくりだった。キャプションの「ゆったりとした語り口」が効いている。

そして最終的な採用は耳で決めた。上位3つの中から選ぶので、聴く量は激減したが、判断は人が下す。指標は候補を絞る道具であって、決める道具ではない。

指標の限界: 男性候補が全員マイナスになった

カウンセラーの男性候補を出したら、12候補全部がマイナススコアになった。

M/seed7:      score=-0.68   一致1.00 話速6.3 抑揚37.6st jitter45%
M/seed42:     score=-0.229  一致0.97 話速5.7 抑揚36.4st jitter41%
M/seed101:    score= 0.042  一致1.00 話速5.5 抑揚35.0st jitter31%
...
M/seed55555:  score=-1.216  一致1.00 話速6.7 抑揚40.6st jitter56%

一致率は0.97〜1.00で完璧。語尾の伸びもゼロ。それでもスコアが沈む。犯人は jitter 31〜56% だった。

自己相関でF0を取ると、低い声ほど周期の推定を誤りやすい。倍音を基本周波数と取り違えたり、隣接フレームで推定が飛んだりする。それがジッタとして計上される。

そしてカウンセラーのプリセットは jitter の重みを 2.5 と全役割で最大にしていた(穏やかで安定した声が欲しいので)。しかもキャプションが「低めでゆっくり」なので、F0がさらに下がって計測誤差が悪化する。三重に効いて全員マイナスになった。

つまりこのランキングは、声質ではなく計測誤差の少なさを測っていた。同じ選抜の仕組みでは、別の指標でも似た誤りを踏んでいる(治せる欠陥で候補を落としていた)。

対処は2つ。

男女を同じ指標で比較しない。 抑揚幅も同様で、男性は25〜40st、女性は10〜25stと出る。同じ土俵に乗せると必ず男性が有利/不利になる。ランキングは性別ごとに独立して作る。

その指標が信頼できない範囲を明示する。 スクリプトの docstring に書いた。

# ⚠️ 低音男声のジッタは自己相関トラッカーが過大評価する。男女の直接比較には使わない。

そして男性カウンセラーは、ジッタの重みを下げてF0と話速を表に出し、耳で選んでもらう形にした。指標が壊れる領域では、指標を無理に使わない。

まとめ

  • 聴いて判断していることを言語化すると、だいたい測れる。話速・抑揚・音圧・安定性で大半はカバーできた
  • 役割ごとに「目標帯」を置く。速い/遅いの一方向ではなく、帯から外れた分を減点する
  • 抑揚は半音で測る。Hz差だと高い声が有利になる
  • 指標は候補を絞る道具で、決める道具ではない。24→6に絞って、最後は聴く
  • 指標が壊れる領域を知っておく。低音のF0推定は誤差が大きい。壊れる領域では重みを下げるか、その指標を使わない

最後の点が一番の学びだった。全候補がマイナスになったら、候補ではなく指標を疑う。


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

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

← 前: 声をガチャで引く
→ 次: 品質ゲートを厳しくするほど、棒読みが生き残る

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

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


元記事: https://forge.workstyle.tech/blog/screening-voices-by-metrics-not-ears/?utm_source=qiita&utm_medium=crosspost&utm_campaign=screening-voices-by-metrics-not-ears

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?