Whisper(AI文字起こし)精度検証:sayコマンドでモデル×VAD閾値×beam sizeを128パターン比較した
はじめに
筆者は個人開発で、オンライン会議の音声をローカルで録音・文字起こしし、議事録を自動生成するmacOSアプリを作っています。ScreenCaptureKitで会議音声を録音し、whisper.cppで文字起こしをして、Ollama(ローカルLLM)で議事録を自動生成する、全工程ローカル完結型のツールです。
この議事録作成ツールの開発過程で、Whisperの文字起こしには繰り返しつまずきました。
- 「火曜日」が「可用日」になるなど、同音異義語の漢字誤変換が頻発する
- 発言していない「ご視聴ありがとうございました」のような幻覚(ハルシネーション)が紛れ込む
- 無音区間のノイズを発話と誤検出し、存在しない発言を文字起こししてしまう
こうした問題に直面すると、まず浮かぶのは「モデルサイズを大きくすれば直るのか?」という疑問です。しかし実際に検証してみると、モデルサイズで直る誤りと直らない誤りがあること、そして幻覚の原因はモデルではなくVAD(発話区間検出)の設計にあることが分かりました。
Whisperの精度に関する解説記事はすでに多く存在しますが、調べた範囲では、既存の録音データを使って「モデルサイズ」だけを比較する記事がほとんどで、beam sizeやno-speech-tholdといった推論パラメータの効果や、VADの誤検出という運用上の落とし穴にまで踏み込んだものは見当たりませんでした。この記事では、macOSのsayコマンドで発話条件を統制した合成音声を使い、モデル×beam size×no-speech-thold×発話速度の128パターン比較と、VAD誤検出の再現実験という2つの検証を、実際のコマンド・実測データとともに紹介します。
なぜ「実録音」ではなく「合成音声」で検証するのか
実録音データの限界
Whisperの精度検証記事の多くは、会議や講演の実録音データを使っています。しかし実録音には2つの限界があります。
- 発話条件を完全に統一できない——同じ人が同じ文章を、毎回同じ速度・同じ声で話すことはできません。「モデルAとモデルBでどちらが正確か」を比べたくても、そもそも入力音声が毎回微妙に違うため、差がモデルの性能差なのか発話のブレなのか切り分けられません
- 同じテストを繰り返し実行できない——パラメータを変えて再検証したくても、同じ発話をもう一度収録することは(厳密には)できません
macOSのsayコマンドで発話条件を統制する
そこで採用したのが、macOS標準の音声合成コマンドsayでテスト音声を生成する方法です。同じテキストを、同じ声質・同じ話速で何度でも再生成できるため、「発話条件を固定したまま、Whisper側の条件(モデル・パラメータ)だけを変える」という比較が可能になります。
テスト文は、Whisperが誤りやすい要素(同音異義語・カタカナ語・数字・固有名詞)を含む8文を用意しました。
| ID | カテゴリ | テキスト |
|---|---|---|
| s01 | 同音異義語(火曜日/可用日) | 次回のミーティングは、来週の火曜日に予定されています。 |
| s02 | 同音異義語(体制/大勢/態勢)+カタカナ語 | 新しい体制のもとで、開発チームはアジャイルな進め方を採用しました。 |
| s03 | カタカナ語+数字(日時) | ステークホルダーへの説明会は、来月三日の午後二時から開催する予定です。 |
| s04 | カタカナ語×2+固有名詞 | このエンドポイントへのデプロイメントは、原さんが担当することになりました。 |
| s05 | 同音異義語(医師/意思)+固有名詞 | 医師である田中先生の意思決定は、いつも迅速で的確です。 |
| s06 | 固有名詞(アマニティ/Claude Code) | アマニティのプロジェクトでは、Claude Codeを使った開発が進んでいます。 |
| s07 | 数字(金額) | 今回の契約金額は、税込みで三百二十八万五千円になります。 |
| s08 | 同音異義語(機械/機会) | 機械学習の導入によって、これまでの業務機会が大きく広がりました。 |
このテキストを、通常速度と早口(-r 300)の2パターンで音声化します。
# say コマンドで音声合成し、whisper.cppが要求する16kHz/モノラル/PCM16のwavに変換する
# (TranscriptionService.swift の m4a→wav 変換(ffmpeg -ar 16000 -ac 1 -c:a pcm_s16le)と
# 同じ前処理条件に揃えている)
# 通常速度
say -v Kyoko -o "$aiff_normal" "$text"
ffmpeg -y -loglevel error -i "$aiff_normal" -ar 16000 -ac 1 -c:a pcm_s16le "$NORMAL_DIR/${id}.wav"
# 早口(-r 300)
say -v Kyoko -r 300 -o "$aiff_fast" "$text"
ffmpeg -y -loglevel error -i "$aiff_fast" -ar 16000 -ac 1 -c:a pcm_s16le "$FAST_DIR/${id}.wav"無料・ローカルで完結し、何度でも同一条件で再生成できる——この再現性こそが、実録音データにはないsayコマンド検証の利点です。
検証1: モデル×beam size×no-speech-thold×発話速度、128パターンのCER比較
検証設計
上記8文 × 発話速度2(通常/早口) × モデル2(base / large-v3-turbo) × beam size 2(1 / 5) × no-speech-thold 2(0.3 / 0.6)の全128パターンで、whisper.cpp(whisper-cli)を実行しました。
WHISPER_CLI="/opt/homebrew/Cellar/whisper-cpp/1.8.4/bin/whisper-cli"
"$WHISPER_CLI" \
-m "$model_path" \
-l ja \
-f "$wav" \
-bs "$bs" \
-nth "$nth" \
-otxt \
-of "$out_base" \
-np \
--suppress-nst-bs: beam size(ビームサーチの探索幅)-nth: no-speech-thold(無音判定の閾値。この値を超えると「発話なし」として扱われる)--suppress-nst: 非音声トークンの抑制
精度の指標には、正解文との編集距離(Levenshtein距離)をもとにしたCER(文字誤り率)を使いました。句読点や全角/半角の表記ゆれによる過大評価を避けるため、比較前に正規化(NFKC変換+句読点除去)をかけています。
def cer(reference: str, hypothesis: str, normalized: bool = True) -> float:
"""CERを算出する。referenceが空文字列の場合は0.0を返す"""
ref = normalize(reference) if normalized else reference
hyp = normalize(hypothesis) if normalized else hypothesis
if len(ref) == 0:
return 0.0
dist = levenshtein(ref, hyp)
return dist / len(ref)結果 — 効くのは「モデルサイズ」だけだった
128パターンの結果を軸別に平均すると、次のようになりました。
| 軸 | 値 | 平均正規化CER | n |
|---|---|---|---|
| model | base | 0.1620 | 64 |
| model | large-v3-turbo | 0.0878 | 64 |
| beam_size | 1 | 0.1264 | 64 |
| beam_size | 5 | 0.1234 | 64 |
| no-speech-thold | 0.3 | 0.1249 | 64 |
| no-speech-thold | 0.6 | 0.1249 | 64 |
| speed | fast | 0.1262 | 64 |
| speed | normal | 0.1236 | 64 |
明確な差が出たのはモデルサイズだけで、baseの0.162に対してlarge-v3-turboは0.088と、CERがほぼ半減しました。一方でbeam size・no-speech-thold・発話速度は、値を変えてもCERがほとんど動きませんでした。beam sizeを5に増やしても、no-speech-tholdを調整しても、体感できるほどの精度改善にはつながらなかったというのが実測結果です。
「パラメータを頑張って調整すれば精度が上がる」というのは、少なくとも今回検証した範囲では成り立たず、精度に効くのはほぼモデル選択だけという、やや拍子抜けする結論になりました。
Before/After具体例① — モデルサイズで直る誤り(医師/意思)
s05「医師である田中先生の意思決定は、いつも迅速で的確です。」は、モデル間の差が最も大きく出たケースです。
- base(fast / bs5 / nth0.3): 「石である田中先生の石決定は、いつも人則出て企画です。」(CER 0.40)
- large-v3-turbo(同条件): 「医師である田中先生の意思決定はいつも迅速で的確です。」(CER ほぼ0)
baseは「医師」を「石」、「意思決定」を「石決定」と誤変換したうえ、後半の「迅速で的確」まで意味の通らない文字列に崩れています。同じ条件でlarge-v3-turboはほぼ完全に正しく認識しており、同音異義語+やや硬い言い回しの組み合わせに対する耐性が、モデルサイズによって大きく変わることが分かります。
Before/After具体例② — モデルサイズで直らない誤り(Claude Code)
一方でs06「アマニティのプロジェクトでは、Claude Codeを使った開発が進んでいます。」は、モデルサイズでは救えなかった例です。
- base(早口): 「アマニティのプロジェクトでは、グロードコードを使った開発が進んでいます。」
- base(通常)・large-v3-turbo(全条件): 「アマニティのプロジェクトでは、クロードコードを使った開発が進んでいます。」
速度条件によって「グロードコード」「クロードコード」と表記の揺れはあるものの、いずれも「Claude Code」を正しく認識できておらずカタカナ表記に崩れてしまい、CERは条件によらず0.27前後で横並びでした。学習データに含まれにくい固有名詞・ブランド名は、モデルサイズを上げても解決しない誤りだと確認できます。運用でこの種の誤りを減らしたい場合は、モデル変更ではなく--promptオプションによる初期プロンプト(固有名詞リストの事前提示)などの別のアプローチが必要になります。
表記ゆれの罠 — CER単体評価の限界
s07「今回の契約金額は、税込みで三百二十八万五千円になります。」のbase出力は、次のようになりました。
- 出力: 「今回の契約金額は税込みで328万5千円になります。」
内容としては正しく認識できていますが、正解文の漢数字表記「三百二十八万五千円」に対して、Whisperの出力は算用数字表記「328万5千円」になっており、CERは0.23〜0.31まで跳ね上がります。意味は合っているのにスコアだけが悪化するこのケースは、CERのような文字列ベースの指標だけで精度を判断することの限界を示しています。実運用で数値の表記ゆれを許容するかどうかは、後段の正規化・後処理の設計で吸収すべき問題だと言えます。
検証2: VAD閾値が生む「幻覚」——議事録作成ツールの実務トラブルから再現する
実運用で起きた問題
検証1のモデル比較とは別に、上記の議事録作成ツールの実機テストでは、Whisper自体の誤字とは異なる種類の問題に直面しました。無音区間しかないはずの録音の末尾に、「お待ちしております」「ご視聴ありがとうございました」という、実際には発言していない文言が文字起こし結果に混入したのです。
これはWhisperの精度問題ではなく、「どこからどこまでが発話区間か」を判定するVAD(Voice Activity Detection)側の設計に原因がありました。
原因調査 — ノイズフロア実測でわかった閾値の機能不全
原因調査のため、MMR_VAD_DEBUG=1環境変数でVADが判定に使うdBFS値を逐次出力するデバッグモードを使い、無音12秒間だけを録音して環境ノイズを実測しました。結果、無音時のノイズフロアが最大-40.88dBFS(99パーセンタイルで-50.5dBFS)に達しており、初期閾値の-40dBFSとほぼ同レベルで機能していなかったことが判明しました。閾値をノイズフロアの実測最大値に対して約10dBの安全マージンを取った-30dBFSに引き上げ、最小発話時間も0.3秒から0.5秒に変更しましたが、それでも幻覚は完全には消えませんでした。
単発ノイズがsplitタイマーをリセットし続ける設計上の欠陥
閾値の調整後も残った1件の幻覚を詳しく調べると、閾値の微調整では対処できない設計上の欠陥が見つかりました。10〜30ms程度の瞬間的なノイズ(クリック音のようなピーク)が-30dBFSを一瞬だけ超えて「発話中」と確定してしまい、その後90秒間に散発する別の微小ノイズのたびに「無音がminSilenceDurationToSplit秒続いたら発話区間を確定する」というタイマーがリセットされ続け、断続的に蓄積した「発話中」時間が最終的に最小発話時間を超えて1つの発話として確定してしまう、という挙動です。閾値をどれだけ厳しくしても、単発ノイズと本物の発話開始を区別できない限りこの問題は解消しません。
オンセット確認ロジックによる根本修正
そこで導入したのが、「発話開始のオンセット確認」ロジックです。閾値を一瞬超えただけでは発話開始と確定させず、一定時間(minSpeechOnsetDuration、既定0.15秒)連続して閾値を超えた場合のみ発話開始と確定する2段階の判定に変更しました。無音フレームが1つでも挟まれば、それまでの候補区間は即座に破棄されます。
以下は、このロジックをPython(標準ライブラリのみ)で再現した検証用コードの抜粋です(パラメータ・ロジックは実装(VoiceActivityDetector.swift)の既定値に合わせています)。
# オンセット確認ロジック(Phase 9修正後の現行実装を踏襲)
# 閾値を一瞬超えただけでは確定させず、minSpeechOnsetDuration分だけ
# 連続して超えた場合のみ発話開始と確定する
if is_speech_frame:
if is_speaking:
accumulated += f.duration_seconds
last_speech_seconds = f.start_seconds + f.duration_seconds
else:
if pending_onset_start is None:
pending_onset_start = f.start_seconds
pending_onset_duration = 0.0
pending_onset_duration += f.duration_seconds
if pending_onset_duration >= MIN_SPEECH_ONSET_DURATION:
is_speaking = True
utterance_start = pending_onset_start
accumulated = pending_onset_duration
pending_onset_start = None
pending_onset_duration = 0.0
else:
# 無音フレームが来たら候補区間は即座に破棄(単発ノイズが捨てられる部分)
pending_onset_start = None
pending_onset_duration = 0.0sayコマンドでの誤検出再現デモ
このロジック変更の効果を、sayコマンドで作った合成音声で再現・検証しました。実発話なしで0.5秒間隔(0.8秒未満)の20msノイズバーストを30回配置したケースと、実発話の前後にノイズバーストを挿入したケースの2パターンです。
# 実発話なし。0.5秒間隔(<0.8秒)で20msノイズバーストを30回配置
# (閾値のみ判定では 20ms×30=0.6秒 が連続した1つの発話として蓄積されうる想定)
concat_args=("$TMP_DIR/sil_1000ms.wav")
for _ in $(seq 1 30); do
concat_args+=("$TMP_DIR/burst_20ms.wav" "$TMP_DIR/sil_500ms.wav")
done
concat_wavs "$VAD_DIR/case_noise_only.wav" "${concat_args[@]}"
# 実発話(sayで生成)の前後にノイズバーストを挿入
say -v Kyoko -o "$TMP_DIR/speech.aiff" "これはテストの発話です。聞こえていますか。"パラメータ(silenceThresholdDb=-30.0, minSpeechOnsetDuration=0.15, minSilenceDurationToSplit=0.8, minUtteranceDuration=0.5)での結果は次のとおりです。
| ケース | 説明 | 閾値のみ判定(旧ロジック) | オンセット確認あり(現行ロジック) | 結果 |
|---|---|---|---|---|
| 実発話なし+ノイズ30回 | 0.5秒間隔でノイズバーストのみ | 1.00s-16.10s(誤検出) | 検出なし | 旧ロジックのみ誤検出を再現 |
| 実発話+前後にノイズ | 実発話の前後にノイズバースト | 1.00s-5.47s | 1.64s-5.47s(実発話を検出) | 両ロジックとも実発話は正しく検出 |
旧ロジックは実発話が存在しないケースでも1.00秒から16.10秒までを「発話」と誤検出しましたが、現行ロジックでは誤検出が0件になりました。同時に、実発話とノイズが混在するケースでは、オンセット確認を入れても実発話の検出自体は妨げられていません。「単発ノイズは捨て、本物の発話開始だけを拾う」という設計変更の効果を、再現可能な形で確認できました。
検証から見えたチューニングの優先順位
2つの検証結果をあわせると、Whisperベースの文字起こしをチューニングする際の優先順位が見えてきます。
- 精度を上げたいなら、まずモデルサイズを見直す——beam sizeやno-speech-tholdの調整に時間をかけるより、
baseからlarge-v3-turboのような上位モデルへの切り替えの方が効果が大きい - 固有名詞・ブランド名はモデルサイズでは救えない——
--promptオプションでの初期プロンプト等、別のアプローチが必要 - 数値の表記ゆれはCERだけで判断しない——意味が合っていてもスコアが悪化することがあるため、後処理・正規化の設計で吸収する
- 幻覚が出たら、まずVADの閾値をノイズフロア実測で見直す——当てずっぽうな値は機能しないことがある
- 閾値の微調整で直らない幻覚は、ロジックの見直しを検討する——単発ノイズと本物の発話開始を区別する「オンセット確認」のような設計変更が根本解決になりうる
用途に応じて使い分けるなら、リアルタイム性を優先する場面では軽量なbase系モデル、精度を優先する場面ではlarge-v3-turboのような上位モデルを選ぶのが妥当です。ただしどちらを選ぶ場合でも、VADの設計(発話区間の切り出し方)は別問題として、実測ベースでチューニングする必要があります。
まとめ・次のアクション
この記事のポイントを整理します。
- 実録音データではなく
sayコマンドで発話条件を統制した合成音声を使うと、モデル・パラメータの効果を再現可能な形で比較できる - モデル・beam size・no-speech-thold・発話速度の128パターン比較で、精度に明確に効いたのはモデルサイズだけだった(CER: base 0.162 → large-v3-turbo 0.088)
- 同音異義語の誤りはモデルサイズで直ることがあるが、固有名詞・ブランド名の誤認識はモデルサイズを上げても直らないことがある
- 数値の表記ゆれ(漢数字↔算用数字)はCERを過大に悪化させるため、CER単体で精度を判断しない
- Whisperの幻覚はモデルの問題ではなくVADの閾値設計に起因することがあり、無音時のノイズフロアを実測してから閾値を決めるべき
- 閾値の微調整で直らない幻覚には、単発ノイズと本物の発話開始を区別する「オンセット確認」ロジックが有効
sayコマンドによる合成音声検証は、Whisper以外の音声認識タスクの精度検証にも応用できる手法です。ぜひ自分のプロジェクトでも試してみてください。
Amanityでは、ローカル完結型の音声認識・議事録自動化ツールの設計・実装をサポートしています。機密性の高い会議のクラウド非依存な自動化にご関心があれば、お気軽にご相談ください。
株式会社Amanity
福岡を拠点に、AIを活用したモバイル・Web・LINEアプリの受託開発を行っています。Whisper・Ollama 等のローカル AI 活用や業務自動化についてお気軽にご相談ください。
無料で相談する