Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.
transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
-> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
as immutable audit trail). Includes architecture-map references
to magnotia_core::*, magnotia_storage::*, etc. now pointing at
lumotia_*; dev-setup.md tracing output examples (lumotia_startup
target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
("lumotia_task_sync"); magnotia_locale i18n localStorage key
renamed + migration shim added; CSS keyframe names
magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
to "lumotia era" earlier — restored).
Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
legacy detection strings in legacy_and_target_paths() so the
migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
— migration call sites reference the legacy magnotia keys
deliberately.
- docs/handovers/ — historical audit trail.
cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
7.4 KiB
Markdown
94 lines
7.4 KiB
Markdown
---
|
||
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 (`<60> <20> <20> <20> <20>`) 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)
|