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?

音声合成の読み間違いを耳で聴かずに見つける → 候補の読みを合成して DTW 距離で比べる

0
Posted at

読み上げ音声で解説動画を作っていたら、音声合成 API(Fish Audio)が「一行」を「いっこう」と読みました。コードは生成AIに書かせているので、作業の途中で音声を聴く人がいません。そこで、読みを聴かずに確かめる方法を作りました。

複合語になると読みが変わる

書いた語 読まれ方
一行 いっこう(正しくは、いちぎょう)
3D(単体) スリーディー(正しい)
3Dプリンタ サンディープリンタ
AI / QR エーアイ / キューアール(正しい)

厄介なのは 3D です。単体で試すと正しいのに、「3Dプリンタ」になると読みが変わります。単語だけ試して安心すると見逃します。

直し方は簡単で、かなで書きます(字幕に出ない行なら画面に影響しません)。問題は、直すべき語をどう見つけるかです。

方法:候補の読みと音を比べる

  1. 確かめたい語と、候補の読み(かな)を、それぞれ3回ずつ合成する
  2. 対数メルスペクトルにする
  3. 総当たりで DTW 距離を測り、候補ごとに平均する
  4. 対象どうしの距離(基準線)に近い候補が、実際の読み

DTW は、話す速さが違っても時間軸を伸び縮みさせて比べる方法です。

音声の長さの比較は使えません。Fish Audio では同じ語でも 0.74〜0.94 秒と揺れました。

compare_reading.py
# 使い方: python3 compare_reading.py target_*.wav -- yomiA_*.wav -- yomiB_*.wav
import sys, wave
from itertools import product
import numpy as np

def load(path):
    with wave.open(path) as w:
        x = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(float)
        return x / 32768, w.getframerate()

def log_mel(x, sr, n_fft=1024, hop=256, n_mels=40):
    frames = np.lib.stride_tricks.sliding_window_view(x, n_fft)[::hop] * np.hanning(n_fft)
    power = np.abs(np.fft.rfft(frames)) ** 2
    mel = lambda f: 2595 * np.log10(1 + f / 700)
    hz = 700 * (10 ** (np.linspace(0, mel(sr / 2), n_mels + 2) / 2595) - 1)
    bins = np.floor((n_fft + 1) * hz / sr).astype(int)
    fb = np.zeros((n_mels, n_fft // 2 + 1))
    for i in range(n_mels):
        l, c, r = bins[i:i + 3]
        fb[i, l:c] = (np.arange(l, c) - l) / max(c - l, 1)
        fb[i, c:r] = (r - np.arange(c, r)) / max(r - c, 1)
    s = np.log(power @ fb.T + 1e-10)
    return s - s.mean(axis=0)  # 音量差を消す

def dtw(a, b):
    cost = np.linalg.norm(a[:, None] - b[None, :], axis=2)
    d = np.full((len(a) + 1, len(b) + 1), np.inf)
    d[0, 0] = 0
    for i in range(len(a)):
        for j in range(len(b)):
            d[i + 1, j + 1] = cost[i, j] + min(d[i, j], d[i, j + 1], d[i + 1, j])
    return d[-1, -1] / (len(a) + len(b))  # 長さの差で有利不利が出ないように割る

def avg(xs, ys):
    return np.mean([dtw(a, b) for a, b in product(xs, ys) if a is not b])

args = sys.argv[1:]
groups, cur = [], []
for p in args:
    if p == "--":
        groups.append(cur); cur = []
    else:
        cur.append(p)
groups.append(cur)
feats = [[log_mel(*load(p)) for p in g] for g in groups]
print(f"基準線(対象どうしのばらつき): {avg(feats[0], feats[0]):.2f}")
for name, f in zip("ABCDEF", feats[1:]):
    print(f"候補{name}との距離: {avg(feats[0], f):.2f}")

手元で試す(Mac の say)

このコードの動作確認は、API の料金がかからない macOS の say で行いました。say は同じ条件なら同じ音を返すので、話す速さを変えて3本作ります。音声合成 API を使うときは、同じ文を3回合成します。

gen(){ i=0; for r in 165 180 195; do i=$((i+1)); say -v Kyoko -r $r -o "$1_$i.wav" --data-format=LEI16@22050 "$2"; done; }
gen target "一行"; gen a "いちぎょう"; gen b "いっこう"
python3 compare_reading.py target_*.wav -- a_*.wav -- b_*.wav

結果です。

書いた語 基準線 候補A 候補B
一行 1.16 いちぎょう 11.72 いっこう 0.77
三百円 1.13 さんびゃくえん 3.06 さんぜんえん 7.65

say でも「一行」は「いっこう」でした。実際、この3本は、かなで「いっこう」と書いて作った音声とファイルが完全に一致しました。

「三百円」は候補Aのほうが近いものの、基準線からは離れています。どの候補も基準線に届かないときは、正しい読みが候補に無いか、抑揚が違う可能性があります。その語は人が聴きます。

注意点

  • 実際に喋らせる文の形で確かめる。 上の 3D のように、単語だけの結果は文の中の読みを保証しません
  • 候補は人かAIが考えます。読みそのものを当てる仕組みではなく、候補の中から選ぶ仕組みです

おまけ:最後の章が消える

台本を章ごとにシェルで API に投げていたら、5章のうち1章が丸ごと消えました。while read は、改行で終わっていない最終行を読み飛ばします。

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done < chapters.txt

|| [ -n "$line" ] を付けると最終行も処理されます。

まとめ

  • 音声合成の読み間違いは、複合語になると急に出る
  • 候補の読みを合成して DTW 距離で比べれば、聴かずに絞り込める
  • 基準線に届かない語だけ、人が聴く

解説動画を音声合成で作る流れをまとめたテンプレート(有料・980円):https://note.com/kaito_cometa/n/n0e017d760a22

生成AI(Claude Code)に書かせて一人で会社を回している記録を note に書いています。
https://note.com/kaito_cometa

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?