はじめに
私は、MENRENというAI面接練習アプリを開発していて、ユーザーが話した内容をWhisper APIで文字起こしする機能を作っています。
音声の書き出しの能力はとても高い。問題は、無音時のデータ送信の時です。
無音データの送信後に返ってきた文字起こし結果がこちら👇
「ご視聴ありがとうございました」
!?......こわ、ご視聴されたいマンがいるやん!
調べてみると「ハルシネーション(幻覚)」でした。無音の音声データを渡すと、存在しない言葉を勝手にでっちあげるみたいです。
ということで、この記事では、ハルシネーションで何が起きるのか、どうやって対策したのかをまとめてみました。
同じ問題で困っている方の参考になると幸いです!
そもそもWhisper APIのハルシネーションって何?
Whisper APIは、OpenAIが提供している音声を文字に変換してくれるAPIです。普通に音声を渡せばかなり正確に文字起こししてくれますが、無音やBGMだけの音声を渡すと、ハルシネーション(何も話していないのに、それっぽい文章を勝手に生成する)問題が起こります。
ここでポイントなのですが、「完全に無音」だとハルシネーションは起こりません。「階段を上がるトントントン」とう音や「PCから出るBGM」 がマイクに入り込むとハルシネーションが起こります。
どんな言葉を生成するの?
実際に無音の音声を何度もWhisper APIに投げてみて、どんな言葉が返ってくるか集めてみました。
| 生成された幻覚テキスト | 出現頻度 |
|---|---|
| ご視聴ありがとうございました | よく出る |
| ご視聴ありがとうございます | よく出る |
| ありがとうございました | よく出る |
| チャンネル登録お願いします | たまに出る |
| おやすみなさい | たまに出る |
| お疲れ様でした | たまに出る |
| ご清聴ありがとうございました | たまに出る |
| 最後までご視聴ありがとうございました | たまに出る |
... YouTube動画っぽいフレーズばっかだな ![]()
調べたところ、Whisperの学習データにYouTubeの動画が大量に含まれているそうです。無音部分で「動画の終わりだな」と判断して、それっぽい締めの言葉を生成しちゃうそう。
[参考]
What OpenAI's Whisper Teaches Us About The Dependence Of The Large Models Revolution On YouTube
AI Model Biases: What went wrong with Whisper (Gladia)
繰り返しパターンもある
やっかいなのが、同じフレーズを何度も繰り返すパターン。
ご視聴ありがとうございました。ご視聴ありがとうございました。ご視聴ありがとうございました。
Whisper APIでの対策方法
MENRENでは、APIから返ってきた結果を見て、「これは幻覚っぽいな」と判断した箇所をフィルタリングする対策を取りました。
大きく分けて2つの検出方法を組み合わせています。
対策1: no_speech_prob を使った検出
Whisper API に response_format: "verbose_json" を指定すると、文字起こし結果にセグメント(区間)ごとの詳しい情報がついてきます。
response = client.audio.transcribe(
parameters: {
model: "whisper-1",
file: audio_file,
language: "ja",
response_format: "verbose_json"
}
)
返ってくるJSONはこんな感じです。
{
"text": "ご視聴ありがとうございました",
"segments": [
{
"text": "ご視聴ありがとうございました",
"start": 0.0,
"end": 5.0,
"no_speech_prob": 0.95
}
]
}
この中にある no_speech_prob がポイントです。これは「このセグメントに音声が含まれていない確率」を0〜1の数値で表しています。
-
0.95→ 95%の確率で音声なし → ほぼ確実に幻覚 -
0.1→ 10%の確率で音声なし → ちゃんと話してる
なので、全てのセグメントの no_speech_prob が高かったら幻覚と判定するようにしました。
NO_SPEECH_PROB_THRESHOLD = 0.5
def detect_no_speech!(response)
segments = response["segments"]
return if segments.blank?
all_no_speech = segments.all? { |seg| seg["no_speech_prob"].to_f >= NO_SPEECH_PROB_THRESHOLD }
if all_no_speech
raise NoSpeechError, "音声が検出できませんでした。もう一度録音を開始してください。"
end
end
閾値は 0.5(50%)にしています。全セグメントが「まあ音声ないっぽいな」だったらエラーにする、という考え方です。
対策2: パターンマッチによる検出(安全網)
ただ、no_speech_prob だけだとすり抜けるケースがありました。
テストしてみると、無音なのに no_speech_prob が 0.1 とか低い値で返ってくることがあって、「え、話してないけど!?」となりました。
なので、既知の幻覚パターンとの文字列マッチングを安全網として追加しました。
HALLUCINATION_PATTERNS = %w[
ご視聴ありがとうございました
ご視聴ありがとうございます
チャンネル登録お願いします
おやすみなさい
お疲れ様でした
ありがとうございました
ご清聴ありがとうございました
最後までご視聴ありがとうございました
].freeze
文字起こし結果がこのリストに完全一致したら、幻覚と判定します。
def detect_hallucination!(text)
# 句読点とスペースを除去して比較
stripped = text.gsub(/[。、\s\u3000]/, "")
if HALLUCINATION_PATTERNS.include?(stripped)
raise NoSpeechError, "音声が検出できませんでした。もう一度録音を開始してください。"
end
# 繰り返しパターンの検出
sentences = text.split(/[。、]/).map(&:strip).reject(&:empty?)
return if sentences.size < 3
unique_sentences = sentences.uniq
if unique_sentences.size <= 2 && unique_sentences.any? { |s| HALLUCINATION_PATTERNS.include?(s.gsub(/[\s\u3000]/, "")) }
raise NoSpeechError, "音声が検出できませんでした。もう一度録音を開始してください。"
end
end
ここでのポイントは3つあります。
1. 句読点を除去してから比較する
Whisperは同じ幻覚でも「ご視聴ありがとうございました」「ご視聴ありがとうございました。」と句点の有無がバラバラなので、句読点を除去してから比較します。
2. 繰り返しも検出する
「ご視聴ありがとうございました。ご視聴ありがとうございました。ご視聴ありがとうございました。」みたいな繰り返しパターンも検出します。句点や読点で分割して、同じフレーズが3回以上繰り返されていたらアウトです。
3. 正常な回答を間違えて弾かないようにする
「ありがとうございました」は幻覚パターンに入っていますが、「本日はお時間をいただきありがとうございました。私は田中太郎と申します。前職では〜」みたいに長い文章の一部に含まれているだけなら正常な回答です。完全一致チェックなので、長い文章がマッチすることはありません。
2段構えにすることで、no_speech_prob がすり抜けても幻覚パターンでキャッチできますし、未知の幻覚パターンでも no_speech_prob が高ければキャッチできます。
テストもちゃんと書こう
幻覚検出は「正常な回答を間違えて弾いてしまう」のが一番こわいので、テストはしっかり書きました。
# 幻覚パターンに完全一致 → エラーになる
it "既知の幻覚パターンに完全一致する場合は NoSpeechError を raise する" do
allow(@audio_double).to receive(:transcribe).and_return(
verbose_json_response("ご視聴ありがとうございました", no_speech_prob: 0.1)
)
expect { described_class.call(file) }.to raise_error(
described_class::NoSpeechError
)
end
# 幻覚フレーズを含むが長い回答 → スルーする
it "幻覚フレーズを含むが長い回答はスルーする" do
long_answer = "本日はお時間をいただきありがとうございました。私は田中太郎と申します。前職では大手IT企業でバックエンドエンジニアとして5年間勤務しておりました。"
allow(@audio_double).to receive(:transcribe).and_return(
verbose_json_response(long_answer, no_speech_prob: 0.02)
)
result = described_class.call(file)
expect(result).to eq(long_answer)
end
特に大事なのが2つ目のテストです。「ありがとうございました」を含む正常な面接回答がちゃんとスルーされることを確認しています。
フロントエンドでのエラー表示
バックエンドで幻覚を検出したら、フロントエンドには422エラーとしてエラーメッセージを返します。ユーザーには「音声が検出できませんでした。もう一度録音を開始してください。」と表示されます。
「ハルシネーションが検出されました」なんて言われてもユーザーは困りますからね。ユーザー目線では「音声が拾えなかった」という説明がわかりやすいです。
まとめ
| 対策 | 方法 | 向いているケース |
|---|---|---|
no_speech_prob チェック |
APIレスポンスの数値で判定 | Whisper APIを使っている場合 |
| パターンマッチ | 既知の幻覚テキストと照合 |
no_speech_probの安全網として |
Whisper APIを使っている場合は、no_speech_prob + パターンマッチの2段構えがおすすめです。
宣伝:MENREN — AI面接練習アプリ
今回の対策を実装したのは、MENREN(めんれん) というAI面接練習アプリです。
エンジニアの転職面接に特化していて、AIの面接官が質問を出してくれて、あなたの回答を音声で録音 → Whisper APIで文字起こし → AIがフィードバックしてくれます。
- 面接官AIが回答内容を踏まえて深掘り質問してくれる
- 練習を重ねるとスコアが上がって成長が見える
- エンジニアなら「技術説明力」「具体性」など専門的な軸で評価してくれる
無料で使えるので、転職活動中のエンジニアの方はぜひ試してみてください!
間違っている内容やもっと良い対策方法があれば、コメント欄にて(優しく)ご指摘いただけるととても嬉しいです!