You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[bug] The #490 noise gate is wrong in both directions — no_speech_prob is dead code on the default model, and the adaptive floor discards peaks the same file calls speech #511
#490's noise gate is wrong in both directions: it never runs on the default model, and its floor discards speech the same file calls speech
#490 shipped three layers to stop phantom Whisper turns. Measured against production, the phantoms still arrive and real speech is at risk. Both halves are verified.
1. The no_speech_prob layer is dead code on the default model
packages/sdk/src/voice/stt.ts:414
conststreaming=this.model!=="whisper-1";
response_format=verbose_json and the no_speech_prob > 0.6 discard live in the else branch (stt.ts:417-421, :461-473). The default model is gpt-4o-transcribe (stt.ts:10; defaultVoiceSettings() → STT_MODELS[0], workers/api/src/lib/preferences.ts:39,234), which takes the streaming branch. So the model-side confidence gate never executes unless the user has explicitly selected legacy whisper-1.
Confirmed live.deploy-host.yml succeeded on 1449ee1 at 2026-08-10 12:26:33 UTC. Phantom transcripts still reached Whisper afterwards:
UTC
row
Aug 11 01:25:42
client:voice "rejected as noise — nothing was heard, discarded" · "Thank you for watching!"
Aug 11 08:03:17
client:voice "result ignored" · "Thank you for watching."
13+ hours after the fix deployed. The phantom path #490 was opened for is not closed for the default model.
2. The adaptive floor discards a peak the same file defines as speech
with roomNoise = 0.08. And fifty lines up in the same file, vad.ts:99-101 states:
"0.2 separates typical unambiguous speech peaks (~0.2–0.5) from ambient noise"
So a shipped, passing test asserts that a clip peaking at the bottom of the file's own stated speech range is thrown away in a room with moderate ambient noise. Quiet speech, a distant mic, or a fan running puts a real utterance under the floor.
Coverage gap: every test in the #490 block uses a 0.40 spike against a 0.12 floor. Nothing tests the 0.15–0.3 band against a 0.07–0.12 floor — precisely the case that matters.
Input trace, fully walked: noiseFloor ← vadStep (vad.ts:157-164, minimum of the first 5 frames) ← level ← computeRmsLevel (audio.ts:23-28) ← analyser, called from the rAF tick at use-voice.ts:421-446, pushed onto the recorder at :433-434, consumed at stt.ts:369 and :408.
3. The onset gate got strictly stricter in two independent ways
vad.ts:183-193 now requires the instantaneous level to clear max(0.1, 3 × noiseFloor). Before #490, onset was satisfied by the ratcheted peak clearing a fixed 0.1 (git show 1449ee1 -- vad.ts, removed: const heardVoice = s.peak > VOICE_FLOOR; if (heardVoice && speaking) { … if (!s.seen) s.seen = true; }). Instantaneous-vs-peak and fixed-vs-adaptive are two separate tightenings shipped together. When onset never fires the turn goes to the silent idle-recycle path (filed separately).
4. Three dated cases of "I spoke and it wasn't transcribed" — independent of the above
"voice turn rejected as noise — the live capture was kept" is emitted only when planNoiseRejection finds gate.isAlive && gate.heardSpeech (turn.ts:176-180) — i.e. the browser recognizer proved real words were spoken this turn, and Whisper returned a silence-hallucination phrase anyway. Recorded: Aug 8 02:47:02, Aug 10 10:33:40, Aug 11 01:35:01 (the last one post-#490).
hadSpeech does not discard peaks inside the range vad.ts:99-101 defines as speech. If the adaptive floor is kept, the ratio or the floor is re-derived and the reasoning is written down.
Tests cover the 0.15–0.3 peak band against a 0.07–0.12 noise floor, in both directions.
Where the Web Speech gate proves words were spoken, that evidence outranks an energy heuristic — as planNoiseRejection already does at turn.ts:176-180.
State explicitly which of the two tightenings in §3 is load-bearing; shipping both together is why this is hard to attribute.
Verified vs inferred
Verified: every file:line, the test assertion, the deploy timestamp, and all five dated error rows.
Inferred: that the adaptive floor has actually eaten one of the owner's real utterances. The error rows carry no peakLevel/noiseFloor, so it is not decidable from the log as it stands — which is itself a finding, covered in the silent-drops issue.
#490's noise gate is wrong in both directions: it never runs on the default model, and its floor discards speech the same file calls speech
#490 shipped three layers to stop phantom Whisper turns. Measured against production, the phantoms still arrive and real speech is at risk. Both halves are verified.
1. The
no_speech_problayer is dead code on the default modelpackages/sdk/src/voice/stt.ts:414response_format=verbose_jsonand theno_speech_prob > 0.6discard live in theelsebranch (stt.ts:417-421,:461-473). The default model isgpt-4o-transcribe(stt.ts:10;defaultVoiceSettings()→STT_MODELS[0],workers/api/src/lib/preferences.ts:39,234), which takes the streaming branch. So the model-side confidence gate never executes unless the user has explicitly selected legacywhisper-1.Confirmed live.
deploy-host.ymlsucceeded on1449ee1at 2026-08-10 12:26:33 UTC. Phantom transcripts still reached Whisper afterwards:client:voice"rejected as noise — nothing was heard, discarded" ·"Thank you for watching!"client:voice"result ignored" ·"Thank you for watching."13+ hours after the fix deployed. The phantom path #490 was opened for is not closed for the default model.
2. The adaptive floor discards a peak the same file defines as speech
packages/sdk/src/voice/vad.ts:115-117The repo's own green test,
packages/sdk/src/voice/vad.test.ts:153-154:with
roomNoise = 0.08. And fifty lines up in the same file,vad.ts:99-101states:So a shipped, passing test asserts that a clip peaking at the bottom of the file's own stated speech range is thrown away in a room with moderate ambient noise. Quiet speech, a distant mic, or a fan running puts a real utterance under the floor.
Coverage gap: every test in the #490 block uses a 0.40 spike against a 0.12 floor. Nothing tests the 0.15–0.3 band against a 0.07–0.12 floor — precisely the case that matters.
Input trace, fully walked:
noiseFloor←vadStep(vad.ts:157-164, minimum of the first 5 frames) ←level←computeRmsLevel(audio.ts:23-28) ← analyser, called from the rAF tick atuse-voice.ts:421-446, pushed onto the recorder at:433-434, consumed atstt.ts:369and:408.3. The onset gate got strictly stricter in two independent ways
vad.ts:183-193now requires the instantaneous level to clearmax(0.1, 3 × noiseFloor). Before #490, onset was satisfied by the ratcheted peak clearing a fixed 0.1 (git show 1449ee1 -- vad.ts, removed:const heardVoice = s.peak > VOICE_FLOOR; if (heardVoice && speaking) { … if (!s.seen) s.seen = true; }). Instantaneous-vs-peak and fixed-vs-adaptive are two separate tightenings shipped together. When onset never fires the turn goes to the silent idle-recycle path (filed separately).4. Three dated cases of "I spoke and it wasn't transcribed" — independent of the above
"voice turn rejected as noise — the live capture was kept"is emitted only whenplanNoiseRejectionfindsgate.isAlive && gate.heardSpeech(turn.ts:176-180) — i.e. the browser recognizer proved real words were spoken this turn, and Whisper returned a silence-hallucination phrase anyway. Recorded: Aug 8 02:47:02, Aug 10 10:33:40, Aug 11 01:35:01 (the last one post-#490).Acceptance criteria
no_speech_probgate runs on the default streaming model, or [bug] STT transcribes silence in hands-free mode — Whisper hallucinations posted as phantom user turns ("Pottery Barn", "Thank you for watching") — gate fails on iOS Safari and when SpeechRecognition stalls #490's phantom protection is re-implemented for it. Today it protects only a model nobody is using by default.hadSpeechdoes not discard peaks inside the rangevad.ts:99-101defines as speech. If the adaptive floor is kept, the ratio or the floor is re-derived and the reasoning is written down.planNoiseRejectionalready does atturn.ts:176-180.Verified vs inferred
file:line, the test assertion, the deploy timestamp, and all five dated error rows.peakLevel/noiseFloor, so it is not decidable from the log as it stands — which is itself a finding, covered in the silent-drops issue.