字幕は、一文字違うだけで意味が変わってしまうことがあります。「行けない」と「行けた」、「十二時」と「二時」。映像を見ている人は字幕を信じて読むので、小さな誤りでも内容をまるごと取り違えてしまいます。
RealSub は、PC で再生している音声をその場で文字にして、翻訳と一緒に画面へ重ねて表示するアプリです。開発中は、認識の設定や処理の流れを何度も見直してきました。そのたびに困るのが「本当に良くなったのか?」という問いです。何本か動画を流して「なんとなく良くなった気がする」と感じても、それは別の動画でたまたまうまくいっただけかもしれません。
そこで RealSub では、ルールを一つ決めています。「感じが良くなった」は数えない。数字で上がったものだけを採用する。 この記事では、そのための物差しをどう作ったか、そして実際に「良さそうに見えた改善」を数字で不採用にした例を紹介します。
実際の動作は、こちらの画面録画で見られます(早送りなし)。
1. テストセットの作り方
物差しの土台は、答えが分かっている音声の集まり、つまりテストセットです。RealSub では次の 2 グループを使っています。
-
日本語・英語: 8 本
- 日本語: 講演 3 本、日常会話 1 本、アニメ 1 本
- 英語: 講演 3 本
-
中国語: 4 本
- 講演 2 本、配信の雑談 1 本、ニュース 1 本
- 発音は 2 種類(大陸の普通話と、台湾のなまりのある中国語)
1 本の長さは約 2 分です。短すぎると偶然に左右され、長すぎると 1 回の計測に時間がかかって改善のたびに回せなくなります。2 分は「毎回気軽に回せて、それでいて傾向が読める」長さとして選んでいます。切り出すときは字幕の時間情報を見て、文の途中で切れないように区切っています。
いちばん気を使っているのは正解テキスト(参照テキスト)の質です。参照には、人の手で作られた字幕だけを使います。動画サイトの自動字幕は使いません。自動字幕は音声認識の出力そのものなので、それを正解にすると「認識が自動字幕と同じ間違いをしたら満点」になってしまいます。答え合わせの答えが間違っていたら、どれだけ測っても意味がありません。
ただし、困ることもあります。中国語のニュースや会話の動画は、字幕が映像に焼き込まれていて、テキストとして取り出せないものがほとんどでした。その場合は次の手順で参照を作っています。
- 音声認識で文ごとの下書きを作る
- 人が音声を聞きながら、間違っている字だけを直す
- 直し終わったファイルだけを正式な参照として読み込む
下書きのままのファイルは読み込まれない仕組みにしてあるので、「校正し忘れた下書き」が正解として紛れ込むことはありません。
2. 採点: 日本語は「読み」で比べる
テストセットができたら、次は採点です。音声認識の精度は、ふつう文字誤り率(CER)や単語誤り率(WER)で測ります。正解と認識結果を並べて、何文字(何単語)を足したり消したり置き換えたりすれば一致するか(編集距離)を数え、正解の長さで割った値です。この記事の「精度」は 1 − 誤り率 を百分率にしたものです。
日本語の難しさ: 同じ音でも書き方が違う
日本語でそのまま CER を計算すると、困ったことが起きます。たとえば、
- 正解(字幕): 「今日は皆さんと考えたい」
- 認識結果: 「今日はみなさんと考えたい」
耳で聞いた内容は合っているのに、漢字かひらがなかの違いだけで 2 文字分の誤りになってしまいます。字幕の書き方にもともと「正解」が一つあるわけではないので、これを誤りとして数えると、聞き取りの良し悪しではなく「字幕を書いた人と好みが合うか」を測ることになってしまいます。
そこで RealSub の日本語の主指標は、両方を読み(かな)に変換してから比べる CER にしています。形態素解析器 MeCab の Python ラッパー fugashi で単語に分け、それぞれの読みをつなげてから比較します。こうすると「聞き取りは合っているが、漢字の選び方が違う」場合は減点されません。
一方、画面に出す文字の自然さも大事なので、文字どおりに比べたスコアも別に記録しています。主指標は「ちゃんと聞き取れているか」、副指標は「表記として読みやすいか」という役割分担です。
正規化: 比べる前に揃えるもの
読みに変換する前後で、表記ゆれを揃えます(正規化)。主なものは次のとおりです。
- NFKC 正規化: 全角英数字を半角に(「AI」→「AI」)
- 漢数字を算用数字に: 「七年前」と「7年前」を同じものとして扱う
- カタカナをひらがなに
- 句読点・記号・空白を削除
- 音声ではない注記を削除: 「(笑)」「[音楽]」「(Applause)」、話者名のラベルなど
最後の注記削除は地味ですが効きます。字幕には「(拍手)」のように音声に存在しない文字が入っているので、そのままでは認識が正しくても誤りとして数えられてしまいます。
処理の流れを簡略化すると、次のようになります(実際のコードから要点だけを抜き出した示意版です)。
import re
import unicodedata
import fugashi
tagger = fugashi.Tagger()
def strip_annotations(text: str) -> str:
# 音声ではない注記を消す: (笑)[音楽] (Applause) など
text = re.sub(r"[((](?:笑|拍手|音楽|歓声)[))]", "", text)
text = re.sub(r"\[(?:音楽|Music|Applause|Laughter|拍手|笑)\]", "", text, flags=re.I)
text = re.sub(r"\((?:Laughter|Applause|Music)[^)]*\)", "", text, flags=re.I)
text = re.sub(r"([^)]*)", "", text) # 全角かっこ内(話者名など)
return text
def kata_to_hira(text: str) -> str:
return "".join(chr(ord(c) - 0x60) if "ァ" <= c <= "ヶ" else c for c in text)
def to_reading_ja(text: str) -> str:
text = strip_annotations(text)
text = unicodedata.normalize("NFKC", text)
text = normalize_kanji_numbers(text) # 「七年」→「7年」(実装は省略)
kana = "".join(w.feature.kana or w.surface for w in tagger(text))
kana = kata_to_hira(kana)
return re.sub(r"[^\w-ゟ゠-ヿ一-鿿]", "", kana)
def cer_reading(ref: str, hyp: str) -> float:
r, h = to_reading_ja(ref), to_reading_ja(hyp)
return edit_distance(list(r), list(h)) / len(r)
辞書にない単語は読みが取れないので、元の表記のまま残します。
中国語と英語
中国語は、繁体字を簡体字に変換 → 漢数字を算用数字に → 文字単位の CER という順で採点します。台湾の字幕は繁体字で書かれていますし、音声認識のほうも、話の内容や発音によって段ごとに繁体字と簡体字が入れ替わることがあります。両方を簡体字に揃えておけば、「臺灣」と「台湾」の違いで減点されることはありません。
英語は、小文字化と句読点の削除をしたうえで、単語単位の WER で採点します。
あえて「言いよどみ」を消さない理由
正規化では、あえてやらないことも決めています。それが、繰り返しの削除です。以前は「同じ語が連続したら一つにまとめる」処理を入れていましたが、外しました。
人が話の中で同じ言葉を繰り返した場合、その繰り返しは字幕にも認識結果にも出るので、自然に一致します。一方で、音声認識モデルには、同じフレーズを何度も出力してしまう「幻覚」があり、こちらは認識結果の側にしか現れません。繰り返しをまとめる処理を入れると、このいちばん目立つ失敗が採点から消えてしまいます。見ている人にとっては明らかな誤りなので、きちんと誤りとして数えるようにしています。
3. 本番と同じ条件で流す
採点の方法が正しくても、テストの流し方が本番と違っていたら意味がありません。たとえば音声ファイルを丸ごと一度に認識させると、前後の文脈が全部使えるので、実際の使い方よりも良い数字が出てしまいます。
そこでベンチマークでは、音声ファイルを 0.1 秒ずつの小さなかたまりに切り、アプリ本体とまったく同じ処理モジュールに流し込んでいます。音声の区間検出(VAD: 話している区間を見つける処理)も、区切り方も、重複の除去も、本番と同じコードを通ります。テストのためだけの近道はありません。
さらに --realtime オプションを付けると、実際の再生と同じ速さで音声を送ります。処理が追いつかずに受け取り側のキューがいっぱいになったら、そのかたまりは捨てます。これは実際の音声取得と同じふるまいです。ふだんの計測は「どこまで正確に認識できるか」を見るために全部を待って流し、--realtime では「実際に使ったときにどうなるか」を見る、という使い分けです。
細かいところでは、終わり方にも気を配っています。
- 末尾に無音を足す: 音声の最後が短い段だと、次の発話を待って保留されたままになることがあります。最後に数秒の無音を足して、最後の段まで確実に押し出します。
- 「処理が終わった」を状態で判定する: 以前は「15 秒間新しい結果が出なければ終わり」と時間で推測していました。しかしこれでは、長い段の認識に時間がかかったときに、途中で打ち切って結果を取りこぼしてしまいます。今は「キューが空」「認識処理が待機中」「翻訳の処理中のものがない」の 3 条件を 0.1 秒ごとに確認し、5 回続けて満たしたら終わりと判定しています。
stable = 0
while stable < 5:
if audio_q.empty() and not worker.busy and not worker.pending_translations:
stable += 1
else:
stable = 0
time.sleep(0.1)
4. 回帰ゲート: 数字が動いたら理由を説明する
物差しができると、「変更で何も壊していないこと」を確かめられるようになります。これを回帰ゲートと呼んでいます。
たとえば、アプリの構成を大きく整理して、認識処理のクラスを別のパッケージへ移したことがあります。中身は変えずに場所を移すだけのつもりでしたが、それでも本当に何も変わっていないかを確かめるために、移す前と移した後で 8 本すべてを流して比べました。結果は 1 本ずつの精度がすべて完全に一致し、比較の出力は ALL 8 IDENTICAL: True でした。
その後は比較用のスクリプトも用意し、2 回分の結果の認識テキストそのものを 1 本ずつ突き合わせるようにしています。精度の数字が同じでも、別の場所で誤りが一つ増えて一つ減っていれば中身は違うからです。「処理の順番を整えただけ」「表示を変えただけ」の変更なら、認識テキストは 1 バイトも変わらないはずなので、違いが出たら原因を調べます。
ただし、ゲートは「数字を絶対に下げない」ためのものではありません。下げるなら、理由を言葉にしてから下げるためのものです。
その例が、音声を文ごとに短く区切るように変えたときです。中国語を続けて話す場面では、区切りまで 8〜12 秒もかかり、字幕がまとめて何枚も出てきて読む側が追いつけませんでした。そこで、話し始めて 4 秒を過ぎたら短い間(ま)でも区切るように変えたところ、表示はぐっと追いかけやすくなりました。一方、区切りが増えたぶん前後の文脈が減り、全体の精度は 95.1 → 94.3 と下がりました。
この 0.8 ポイントは「1〜2 ポイント程度なら、すぐに字幕が出てくることのほうが大事」とはっきり判断したうえで受け入れ、ここを新しい基準値にしました。数字を見たうえで選んだので、後から「いつの間にか精度が落ちていた」ということにはなりません。
5. データで捨てた改善、データで採用した改善
ここからが本題です。物差しがあって良かったと感じた例を 2 つ紹介します。
捨てた例: VAD のしきい値を下げる
VAD は、音声の各部分について「人が話している確率」を出し、しきい値を超えた部分だけを認識に回します。RealSub のしきい値は 0.40 です。
講演の動画を見ていると、小さな声や語尾が拾われていないことがありました。しきい値を下げれば拾えるはずです。そこで 0.30 に下げて全体を流してみました。
日本語の講演 3 本はそろって +0.4〜+1.3 ポイント上がりました。ここまでは期待どおりです。ところが、アニメは 91.7 → 88.2 と 3.5 ポイントも下がりました。
原因を調べると、BGM の流れている区間が「話している」と判定され、音声認識に送られていました。音声認識モデルは、音楽しかない音声を渡されても何か言葉を出そうとしてしまい、存在しないせりふを作ってしまいます(幻覚)。小さな声を拾える分よりも、音楽をせりふにしてしまう分のほうが、見ている人にとってはずっと困ります。
講演だけを見ていたら、迷わず採用していたはずの変更です。種類の違う素材をテストセットに入れておいたからこそ気づけました。しきい値は 0.40 のままにしています。
採用した例: AGC(自動音量調整)
反対に、データで効果がはっきりした例が AGC(Automatic Gain Control、自動音量調整)です。PC の音量をかなり小さくして動画を見ている場合、アプリに届く音声も小さくなります。そのような小さな音声を一定の大きさまで自動で持ち上げてから、認識に渡す仕組みです。
「音量を上げれば認識しやすくなるはず」というのは直感的には分かりますが、本当にそうかは測ってみないと分かりません。VAD は音量に関係なく判定できることが分かっていたので、「AGC は要らないのでは」という見方もありました。そこで、テストセットの 8 本の音量を実際の使用で記録された小ささまで下げて、AGC のオンとオフで比べました。基準(元の音量)は 94.3 です。
| 入力の音量 | AGC オン | AGC オフ |
|---|---|---|
| 元の 1/100(×0.01) | 94.4 | 86.0(アニメは 45.1) |
| 元の約 1/330(×0.003) | 93.4 | 50.5 |
AGC がないと、音量を 1/100 にしただけでアニメの精度は 45.1 まで落ち、さらに小さくすると全体でも 50.5 になりました。小さな音声を認識モデルに直接渡すと、言葉としてほとんど取れなくなるのです。AGC をオンにすると、1/100 では元の音量とほぼ同じ 94.4、さらに小さい音量でも 93.4 を保ちました。
これだけ差がはっきりしていれば、迷う理由はありません。AGC は採用し、音量が小さいときも利用者が特に何もしなくてよいようにしました。
まとめ
現在の RealSub の成績は、日本語 92 / 英語 98 / 中国語 93 です。ただし、これはこの記事で紹介した独自のテストセットでの数字で、あくまで参考値です。素材が変われば数字も変わります。
それでもこの物差しを大切にしているのは、判断を数字で説明できるようになるからです。
- 正解には人の手で作られた字幕だけを使う
- 日本語は読みで比べ、「聞き取りは合っているのに書き方が違う」を減点しない
- 言いよどみはまとめず、繰り返しの幻覚はきちんと誤りとして数える
- 本番と同じ処理に、本番と同じ速さで流す
- 数字が動いたら理由を説明し、下げるときは言葉にしてから下げる
「良さそうな改善」が本当に良いのかは、測ってみないと分かりません。VAD のしきい値のように、ある素材では良くても別の素材では悪くなる変更もあれば、AGC のように、思っていた以上に効く変更もあります。音声認識に限らず、「なんとなく良くなった」を数字に置き換えてみると、意外な発見があるかもしれません。
この記事の仕組みは、リアルタイム字幕アプリ RealSub に入っています。PC で再生している音声に、その場で字幕と翻訳を重ねて表示します。2026年10月28日発売予定です。
- Steam: Steam のストアページ (ウィッシュリスト登録をお願いします)
- 公式サイト: https://realsub.app/

