From e2f279d57a7af16240977d2261815862071f51ad Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Fri, 5 Jun 2026 13:06:54 -0400 Subject: [PATCH] zebra-spaces: filter Whisper silence-hallucinations (RMS gate + stop-word skip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fox 2026-06-05: "fxhp and fxhp-chrome both are triggering clients to transcode `you` on a new line over and over could you sort that out? … this happens even when mic is closed." whisper-tiny.en is notorious for hallucinating common stop-words on silent / low-energy audio: "you", "thank you", "thanks for watching", a lone period. Mic muted or peer dead-silent → the model still produces a chunk of audio (silence frames from Opus / WebRTC) and spits one of these phrases out. Two filters: 1. RMS-based silence gate IN the capture worklet (cheaper — no pipeline invocation at all). Compute RMS of the 5s/16kHz chunk before posting; skip if below 0.005 (~-46 dBFS, well below any real speech). The vast majority of "mic-closed" hallucinations stop here. 2. JS-side filter against a known-hallucination set. Catches the rest (quiet-room ambient that passes the RMS gate). Normalizes lowercase + strips trailing punctuation so "You." / "you " / "YOU!" all collapse to "you" and match the set. Easy to extend as new hallucinations are observed. 3. Per-uuid consecutive-duplicate suppression. Even non-hallucination text sometimes re-emits the same short phrase across consecutive chunks ("uh huh" "uh huh") — drop the dupe. CPU win: silent chunks no longer go through ONNX inference (~50-200ms per chunk on tiny.en). --- web/zebra-spaces.html | 55 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 69774aa..c2d1608 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -2048,8 +2048,23 @@ class WhisperCaptureProcessor extends AudioWorkletProcessor { } this.decimateCounter = (this.decimateCounter + ch.length) % this.decimationFactor; if (this.bufPos >= this.chunkTargetSamples){ - const chunk = this.buf.slice(0, this.bufPos); - this.port.postMessage({ chunk }, [chunk.buffer]); + /* RMS-based silence gate. whisper-tiny.en hallucinates common + * stop-words ("you", "thank you", "thanks for watching", ".") + * when fed silence — even with the mic closed. Skip chunks + * whose energy is below the speech threshold entirely. Saves + * CPU AND suppresses the hallucinated lines. + * Fox 2026-06-05: "this happens even when mic is closed." */ + let sumSq = 0; + for (let i = 0; i < this.bufPos; i++){ + const s = this.buf[i]; + sumSq += s * s; + } + const rms = Math.sqrt(sumSq / this.bufPos); + const SILENCE_THRESHOLD = 0.005; /* ~ -46 dBFS, well below speech */ + if (rms >= SILENCE_THRESHOLD){ + const chunk = this.buf.slice(0, this.bufPos); + this.port.postMessage({ chunk }, [chunk.buffer]); + } this.bufPos = 0; } return true; @@ -2104,6 +2119,26 @@ async function ensureWhisperPipeline(){ } let transcribeEnabled = false; +/* whisper-tiny.en hallucinations on silence / low-energy chunks. The + * RMS gate in the capture worklet catches dead silence; this set + * catches what gets past it — quiet-room ambient with the same + * spurious outputs ("you", "thanks for watching", "thank you.", a + * lone period). Lowercased + trimmed of trailing punctuation for + * matching. List grows as new hallucinations are observed. */ +const WHISPER_HALLUCINATIONS = new Set([ + 'you', 'thank you', 'thanks', 'thanks for watching', + 'thanks for watching!', 'thank you for watching', + 'thank you for watching.', 'thanks for watching.', + 'bye', 'okay', 'mm', 'mhm', 'uh huh', 'ah', 'oh', + 'um', 'uh', 'hmm', 'yeah', 'yes', 'no', 'ok', 'so', + 'and', 'the', 'a', 'i', 'it', 'we', + '.', '..', '...', '!', '?' +]); +/* per-uuid dedupe of the most recent emitted line. whisper sometimes + * re-emits the same short phrase across consecutive chunks even when + * the speaker has paused — looks like spam in the log. */ +const _lastTranscriptByUuid = new Map(); + function appendTranscriptLine(uuid, text){ const log = document.getElementById('transcript-log'); if (!log) return; @@ -2142,8 +2177,16 @@ async function startCaptureForUuid(uuid){ try { const result = await pipeline(e.data.chunk, { sampling_rate: 16000 }); const txt = (result && result.text ? result.text : '').trim(); - /* skip empty / placeholder transcriptions ("." " " "[BLANK_AUDIO]" etc.) */ - if (txt.length >= 2 && !/^\[/.test(txt)) appendTranscriptLine(uuid, txt); + if (txt.length < 2) return; + if (/^\[/.test(txt)) return; /* "[BLANK_AUDIO]" etc. */ + /* normalize for hallucination match: lowercase, strip trailing + * punctuation. "You." / "you" / " YOU!" all collapse to "you". */ + const norm = txt.toLowerCase().replace(/[.!?,;:\s]+$/,'').trim(); + if (WHISPER_HALLUCINATIONS.has(norm)) return; + /* consecutive-duplicate suppression per speaker */ + if (_lastTranscriptByUuid.get(uuid) === txt) return; + _lastTranscriptByUuid.set(uuid, txt); + appendTranscriptLine(uuid, txt); } catch (err){ logLine('err', 'whisper transcribe '+uuid.slice(0,4)+': '+err.message); } @@ -7117,8 +7160,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');