feat(ai-formatting A.1 #26): detect prompt-loop repetition cascades
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

ufal/whisper_streaming #161 documents the classic Whisper streaming
failure: on ambiguous audio the model falls into a prompt loop,
cascading a single token for 10+ words ("I I I I I I I I I I I…").
The chunk-boundary duplicate detector in live.rs doesn't catch
this — the repeat is within a single chunk, and the text is
technically novel so FTS is happy to keep it.

Fold the detection into is_hallucination as a third pass (after
HALLUCINATION_MARKERS substring-match and HALLUCINATION_TRAIL_PHRASES
exact-match). has_consecutive_repetition walks the token stream
(whitespace-split, lowercased) and returns true when any run of
≥REPETITION_RUN_THRESHOLD (4) identical tokens is found.

Threshold chosen deliberately: three consecutive matches appear in
normal speech ("no no no, that's wrong"), four almost never does.
Tests pin both sides — "I I I I I" detected, "no no no" allowed,
alternating patterns ("I am I am I am I am") allowed regardless of
length.

Phrase-level repetition ("thank you thank you thank you thank you")
is a documented companion failure mode but needs a sliding n-gram
matcher — deferred with a code comment flagging it.

No caller changes: post_process_segments already drops
is_hallucination hits when anti_hallucination is enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 16:44:22 +01:00
parent f525004d05
commit 1dd09e14ca

View File

@@ -349,9 +349,17 @@ static HALLUCINATION_TRAIL_PHRASES: &[&str] = &[
"mbc 뉴스 김수영입니다",
];
/// Minimum run length for the token-repetition detector (brief item
/// A.1 #26). Whisper's prompt-loop failure mode (ufal #161) typically
/// produces 510+ consecutive identical tokens; requiring 4 catches
/// those cleanly while leaving natural dialogue alone — three-in-a-row
/// is common speech ("no no no, that's wrong"), four-in-a-row almost
/// never is.
const REPETITION_RUN_THRESHOLD: usize = 4;
/// Returns true if a segment's text looks like a hallucination.
///
/// Two passes:
/// Three passes:
/// - **Contains-match on HALLUCINATION_MARKERS** — catches bracketed
/// and musical markers even when Whisper surrounds them with other
/// noise ("♪♪♪ thanks for watching ♪♪♪").
@@ -359,6 +367,10 @@ static HALLUCINATION_TRAIL_PHRASES: &[&str] = &[
/// well-documented subtitle-training leakage without false-positiving
/// on legitimate dialogue that happens to mention "thanks" or
/// "subscribe" mid-sentence.
/// - **Consecutive-repetition detector** — Whisper occasionally enters
/// a prompt-loop where a single token cascades for dozens of words.
/// Flagging it here lets the existing anti_hallucination pipeline
/// drop the chunk rather than emitting "I I I I I I I I I …".
pub fn is_hallucination(text: &str) -> bool {
let trimmed = text.trim().to_lowercase();
if trimmed.is_empty() {
@@ -374,6 +386,38 @@ pub fn is_hallucination(text: &str) -> bool {
return true;
}
}
if has_consecutive_repetition(&trimmed, REPETITION_RUN_THRESHOLD) {
return true;
}
false
}
/// Returns true when `text` contains at least `min_run` consecutive
/// identical whitespace-separated tokens (case-insensitive).
///
/// Detects the prompt-loop failure mode that Whisper falls into on
/// ambiguous audio (ufal #161) without flagging normal triple-repeats
/// that appear in everyday speech ("no no no, that's wrong"). The
/// threshold is deliberately conservative — four-in-a-row is almost
/// never organic.
fn has_consecutive_repetition(text: &str, min_run: usize) -> bool {
if min_run < 2 {
return false;
}
let mut run: usize = 1;
let mut last: Option<String> = None;
for token in text.split_whitespace() {
let token_lower = token.to_lowercase();
if last.as_deref() == Some(token_lower.as_str()) {
run += 1;
if run >= min_run {
return true;
}
} else {
run = 1;
last = Some(token_lower);
}
}
false
}
@@ -499,4 +543,27 @@ mod tests {
assert!(!is_hallucination("Thanks for the heads up on the migration"));
assert!(!is_hallucination("Please subscribe to the RSS feed and tell me when it updates"));
}
#[test]
fn is_hallucination_detects_prompt_loop_repetition() {
// ufal #161: Whisper prompt-loop cascade, the classic
// streaming failure mode. Single-token runs only for now —
// multi-token phrase repetition ("thank you thank you thank
// you...") is a documented companion failure mode but needs
// sliding n-gram matching, which is a future enhancement.
assert!(is_hallucination("I I I I I I I I I"));
assert!(is_hallucination("hello hello hello hello world"));
assert!(is_hallucination("the the the the quick brown fox"));
// Case-insensitive.
assert!(is_hallucination("Hello HELLO hello hello"));
}
#[test]
fn is_hallucination_allows_natural_triple_repeats() {
// Threshold is 4, so natural speech patterns pass.
assert!(!is_hallucination("no no no, that's wrong"));
assert!(!is_hallucination("do do do the thing"));
// Alternating patterns never trigger regardless of length.
assert!(!is_hallucination("I am I am I am I am"));
}
}