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');