Source code for sdp.processors.inference.asr.utils.whisper_hallucinations
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from sdp.processors.base_processor import BaseParallelProcessor, DataEntry
[docs]
class DetectWhisperHallucinationFeatures(BaseParallelProcessor):
"""
Computes hallucination-related features for ASR model outputs (e.g., Whisper transcripts).
This processor analyzes the transcript text and flags common hallucination patterns by computing
boolean features such as:
- Repeated or low-diversity n-grams (`hall_repeated_ngrams`)
**Example:**
.. code-block:: text
yes yes yes yes yes yes yes yes yes yes yes yes
- Unusually long or disproportionately long words (`hall_long_word`)
**Example:**
.. code-block:: text
short mid reallyreallyreallyreallyreallyreallyreallylong
- Matches with known hallucinated phrases (`hall_frequent_single_word`)
**Example:**
.. code-block:: text
lorem ipsum dolor sit amet
It appends these features to each entry in the manifest for downstream filtering or analysis.
Args:
common_hall_file (str): Path to a file with known hallucinated phrases, one per line.
unique_words_threshold (float): Maximum allowed share of unique words before marking as repeated. Default is 0.4.
long_word_threshold (int): Minimum character length for a word to be considered "long". Default is 25.
long_word_rel_threshold (float): Relative length ratio between the longest and second-longest word. Default is 3.
char_rate_threshold (float): [Unused in current logic, retained for compatibility]. Default is 4.
text_field (str): Key in the data entry that contains the transcript. Default is 'text'.
**kwargs: Additional keyword arguments passed to `BaseParallelProcessor`.
Returns:
A manifest with additional boolean fields for hallucination detection.
"""
def __init__(
self,
common_hall_file,
unique_words_threshold=0.4,
long_word_threshold=25,
long_word_rel_threshold=3,
char_rate_threshold=4,
text_field='text',
**kwargs,
):
super().__init__(**kwargs)
self.unique_words_threshold = unique_words_threshold
self.long_word_threshold = long_word_threshold
self.long_word_rel_threshold = long_word_rel_threshold
self.char_rate_threshold = char_rate_threshold # Currently unused
self.text_field = text_field
# Load common hallucination phrases into memory
with open(common_hall_file, 'r') as f:
self.common_hall_phrases = [line.strip() for line in f]
def repeated_ngrams(self, words):
"""
Flags entries with low lexical diversity (i.e., repeated n-grams).
Returns True if the fraction of unique words is below the threshold.
"""
unique_words_share = len(set(words)) / len(words)
return unique_words_share <= self.unique_words_threshold
def long_word(self, words):
"""
Detects unusually long words or sharp differences in word lengths.
Returns True if the longest word is above the absolute threshold or much longer
than the second-longest word.
"""
word_lengths = sorted([len(word) for word in words])
if word_lengths[-1] >= self.long_word_threshold:
return True
if len(words) > 1:
diff = (word_lengths[-1] - word_lengths[-2]) / word_lengths[-2]
return diff >= self.long_word_rel_threshold
return False
def frequent_single_word(self, text):
"""
Checks if the cleaned transcript matches any known hallucinated phrase.
"""
cleaned_text = text.strip().replace('.', '').replace('?', '').replace('!', '')
return cleaned_text in self.common_hall_phrases
def process_dataset_entry(self, data_entry):
"""
Processes a single manifest entry and appends hallucination features.
"""
text = data_entry[self.text_field]
words = text.split()
# Compute hallucination indicators
data_entry['hall_repeated_ngrams'] = self.repeated_ngrams(words)
data_entry['hall_long_word'] = self.long_word(words)
data_entry['hall_frequent_single_word'] = self.frequent_single_word(text)
return [DataEntry(data=data_entry)]