---
name: Anti-hallucination filter
type: architecture-map-page
slice: 04-llm-formatting-mcp
last_verified: 2026/05/09
---

# Anti-hallucination filter

> **Where you are:** [Architecture map](../README.md) → [LLM, Formatting, MCP](README.md) → Anti-hallucination

**Plain English summary.** Whisper hallucinates on silence. It produces things like `[blank_audio]`, `Thanks for watching!`, `♪♪♪`, or a single token cascading 8 times in a row. `is_hallucination` returns true on any of those, and the pipeline drops the segment entirely. Three independent passes — bracketed markers, exact-match subtitle leakage, and a token-repetition detector.

## At a glance

- Crate: `lumotia-ai-formatting`
- Path: `crates/ai-formatting/src/rule_based.rs:374`
- LOC: ~50 for `is_hallucination` and the helper, plus ~70 lines of pattern tables
- Public surface: `pub fn is_hallucination(text: &str) -> bool` (`crates/ai-formatting/src/rule_based.rs:374`)
- External deps that matter: none — pure `str` work
- Tauri command that calls this (slice 2, best guess): not called directly. Reaches Tauri only via `post_process_segments`'s anti-hallucination retain-loop (`crates/ai-formatting/src/pipeline.rs:43`).

## What's in here

### Three passes (the function body)

1. **Empty-after-trim → true.** A blank segment is, by convention, treated as a hallucination so it gets dropped.
2. **Contains-match on `HALLUCINATION_MARKERS`** (`crates/ai-formatting/src/rule_based.rs:282`). Substring match on the lowercased trimmed text, so `[Music]`, `[MUSIC]`, and `[blank_audio]` all hit. Markers covered:
   - Bracketed annotations: `[blank_audio]`, `[blank audio]`, `[silence]`, `[music]`, `[applause]`, `[laughter]`, `[laughs]`, `[inaudible]`, `[background noise]`, `[sounds]`, `(music)`, `(silence)`, `(applause)`, `(laughter)`.
   - Musical notation: `♪`, `♫` — Whisper interprets sustained room tone as song.
   - The contains-match catches `♪♪♪ thanks for watching ♪♪♪` even though neither half alone is exact.
3. **Exact-match on `HALLUCINATION_TRAIL_PHRASES`** (`crates/ai-formatting/src/rule_based.rs:312`). The full lowercased trimmed text must equal one of the phrases. Used for the YouTube / subtitle-training leakage that Whisper imports from its training data:
   - Minimalist false-positives on silence: `thank you.`, `thank you`, `thanks.`, `thanks`, `you.`, `you`, `bye.`, `bye`.
   - YouTube subtitle sign-offs: `thank you for watching.`, `thanks for watching!`, `thanks for watching, bye.`, `thanks for listening.`, `please subscribe.`, `please subscribe to our channel.`, `don't forget to subscribe.`, `don't forget to like and subscribe.`, `like and subscribe.`, `see you in the next video.`, `see you next time.`.
   - Subtitle-credit leakage: `subtitles by the amara.org community`, `subtitles by the`, `subtitled by`, `subtitles by`, `translated by`.
   - Non-English sign-offs: Japanese `ご視聴ありがとうございました`, `字幕作成者`, `字幕by`, `字幕`, Korean `mbc 뉴스 김수영입니다`. Lowercase exact-match consistency is preserved across scripts.

   Exact-match is deliberate: a real sentence containing "thanks for the heads up on the migration" must pass.

4. **Consecutive-repetition detector** (`crates/ai-formatting/src/rule_based.rs:403`). Whisper's prompt-loop failure mode (ufal/whisper_streaming #161) is a single token cascading 5–10+ times. Threshold is 4 — caught at `REPETITION_RUN_THRESHOLD = 4` (`:358`). Three-in-a-row is common in natural speech ("no no no, that's wrong"), four-in-a-row almost never is. Case-insensitive token comparison.

### Provenance of the pattern lists

The trail phrases trace back to specific upstream issues:

- WhisperLive #185 and #246 — silence triggering `Thank you for watching` and similar.
- ufal/whisper_streaming #121 — caption-dataset leakage on room tone.
- ufal/whisper_streaming #161 — prompt-loop cascade.

Comments on each pattern list cite the exact source so a future contributor knows where each entry came from and why removing it might let the failure mode return.

### `has_consecutive_repetition` (`crates/ai-formatting/src/rule_based.rs:403`)

Linear pass. Walk whitespace-separated tokens, lowercase each, increment a `run` counter when the current matches the previous, reset when it does not. Return true the moment `run >= min_run`.

Tests at `crates/ai-formatting/src/rule_based.rs:551` cover:

- The cascade case: `"I I I I I I I I I"`, `"hello hello hello hello world"`, `"the the the the quick brown fox"`.
- The case-insensitive case: `"Hello HELLO hello hello"`.
- The legitimate-triple case: `"no no no, that's wrong"` (returns false — three is below threshold).
- Alternating patterns: `"I am I am I am I am"` (returns false — never four-in-a-row).

## Data flow

```
text: &str
  → trimmed = text.trim().to_lowercase()
  → empty? → true
  → for marker in HALLUCINATION_MARKERS:
       if trimmed.contains(marker) → true
  → for phrase in HALLUCINATION_TRAIL_PHRASES:
       if trimmed == phrase → true
  → has_consecutive_repetition(&trimmed, 4) → true if any run >= 4
  → otherwise false

post_process_segments behaviour: segments.retain(|s| !is_hallucination(&s.text))
  — segments returning true are dropped from the output list entirely.
```

## Watch-outs

- **Drop is permanent.** A segment removed by anti-hallucination is gone before any other filter or LLM cleanup runs. If a real-world transcript ever has a legitimate segment that exactly matches "Thanks." (e.g. in a meeting where someone said only "Thanks." in response to a question), it gets dropped. The exact-match policy on `HALLUCINATION_TRAIL_PHRASES` is the trade-off — substring-match would have a much larger false-positive rate.
- **Threshold of 4 for repetition is conservative.** Some Whisper failures cascade to dozens of tokens, well past the threshold; the detector catches those easily. The risk is on the other side: legitimate four-in-a-row chants ("go go go go", "yes yes yes yes!") get dropped. Acceptable for dictation; would be wrong for music transcription, but Lumotia's scope is dictation.
- **Multi-token phrase repetition is not yet detected.** "thank you thank you thank you thank you thank you" (five `thank you` in a row) does not trigger the detector — the comparison is per-token, not per n-gram. The test comment at `crates/ai-formatting/src/rule_based.rs:553` calls this out explicitly as a future enhancement requiring sliding n-gram matching.
- **Non-English sign-offs are lowercased trimmed exact match.** A future Japanese ASR engine that uses different sign-off phrasing would slip through. Update the table when new ASR backends are added.
- **No alphabet-class detection.** A long burst of mojibake (`� � � � �`) where every char is the replacement codepoint would not trigger any of the three passes. Whisper does not produce this in practice; if a future codec change made it possible, a fourth pass would be needed.
- **`HALLUCINATION_MARKERS` is contains-match.** A real meeting transcript containing the phrase "the team will [music]play in October" would be dropped. Markers are deliberately niche enough that real text containing them is improbable; the cost of substring match is accepted.

## See also

- [Pipeline overview — anti-halluc as a drop step](formatting-pipeline.md)
- [Filler removal and British English (sibling functions in same file)](formatting-filler-and-british.md)
- [Slice README](README.md)
