A wake threshold nobody can see or move is wrong in every room but one #90

Open
opened 2026-08-15 12:16:17 +00:00 by aiko · 1 comment
Owner

A wake word with a fixed threshold is wrong in every room but the one it was tuned in. Hard-floors and soft furnishings, a headset against a laptop array, a phone in a pocket — the same phrase scores differently in each, and the only person who can tell whether it is firing too much or too little is the one in the room.

So it needs the two controls the microphone already has for silero, in the same place and the same shape.

What it is

Add to VADSettings (src/renderer/src/context/vad-context.tsx:33) and DEFAULT_VAD_SETTINGS (:144), persisted through the same useLocalStorage('vadSettings', ...) (:182) as the thresholds beside it:

  • Wake phrase on/off. Off by default until #91 has run on a phone. When off, hands-free behaves exactly as it does today.
  • Sensitivity, as a percentage, defaulting to whatever #87's report recommends rather than to a number someone liked the look of.

The plumbing already exists and should not be reinvented: settings reach Rust as fractions through invoke('mic_start', ...) (:500), and updateSettings (:467) cycles an open microphone so a change takes effect without a restart. The wake threshold rides the same route.

There is a trap in that file worth reading first. updateSettings reaches startMic through a ref and reads settingsRef, and the comment at :471 explains why — a captured closure held render-0's settings and changed thresholds restarted the microphone with the old ones, permanently. A new field added carelessly gets the same bug back.

The meter is the point

#88 publishes denpa://wake-score. Draw it, next to or in place of the existing level meter (denpa://mic-level, previousTriggeredProbability), while the setting is open.

A sensitivity slider with no visible score is a guessing game: the operator moves it, says the phrase, and gets a binary answer with no gradient. With the score on screen the tuning takes thirty seconds — say it three times, see where it peaks, set the threshold under that. This is the difference between a control that gets used and one that gets left at default forever.

Where it lives

ASR settings (src/renderer/src/components/sidebar/setting/asr.tsx:15) with its use-asr-settings hook (src/renderer/src/hooks/sidebar/setting/use-asr-settings.ts:4) is the existing pattern for exactly this kind of pair — SwitchField and NumberField, already translated, already wired.

Follow it. Do not build a new settings surface for two fields.

A caution about redemptionFrames

VADSettings still carries redemptionFrames, and the comment at vad-context.tsx:40 records that it has been inert since capture moved to Rust — kept so a stored preference does not vanish. It is a live example of what happens when a control outlives the thing it controlled.

The wake settings must not join it. If a knob added here stops reaching Rust, it should be deleted, not documented.

Done means

  • Both controls persist, survive a restart, and take effect on an open microphone without one.
  • Off by default; with it off, nothing about today's behaviour changes.
  • The score is visible while tuning.
  • Strings extracted through npm run extract-translations like everything else in that panel.
  • #87 — where the default threshold comes from
  • #88 — the score being drawn
  • #89 — what the switch actually turns on
  • #91 — the reason it ships off
A wake word with a fixed threshold is wrong in every room but the one it was tuned in. Hard-floors and soft furnishings, a headset against a laptop array, a phone in a pocket — the same phrase scores differently in each, and the only person who can tell whether it is firing too much or too little is the one in the room. So it needs the two controls the microphone already has for silero, in the same place and the same shape. ## What it is Add to `VADSettings` (`src/renderer/src/context/vad-context.tsx:33`) and `DEFAULT_VAD_SETTINGS` (`:144`), persisted through the same `useLocalStorage('vadSettings', ...)` (`:182`) as the thresholds beside it: - **Wake phrase on/off.** Off by default until `#91` has run on a phone. When off, hands-free behaves exactly as it does today. - **Sensitivity**, as a percentage, defaulting to whatever `#87`'s report recommends rather than to a number someone liked the look of. The plumbing already exists and should not be reinvented: settings reach Rust as fractions through `invoke('mic_start', ...)` (`:500`), and `updateSettings` (`:467`) cycles an open microphone so a change takes effect without a restart. The wake threshold rides the same route. There is a trap in that file worth reading first. `updateSettings` reaches `startMic` through a ref and reads `settingsRef`, and the comment at `:471` explains why — a captured closure held render-0's settings and changed thresholds restarted the microphone with the old ones, permanently. A new field added carelessly gets the same bug back. ## The meter is the point `#88` publishes `denpa://wake-score`. Draw it, next to or in place of the existing level meter (`denpa://mic-level`, `previousTriggeredProbability`), while the setting is open. A sensitivity slider with no visible score is a guessing game: the operator moves it, says the phrase, and gets a binary answer with no gradient. With the score on screen the tuning takes thirty seconds — say it three times, see where it peaks, set the threshold under that. This is the difference between a control that gets used and one that gets left at default forever. ## Where it lives `ASR settings` (`src/renderer/src/components/sidebar/setting/asr.tsx:15`) with its `use-asr-settings` hook (`src/renderer/src/hooks/sidebar/setting/use-asr-settings.ts:4`) is the existing pattern for exactly this kind of pair — `SwitchField` and `NumberField`, already translated, already wired. Follow it. Do not build a new settings surface for two fields. ## A caution about `redemptionFrames` `VADSettings` still carries `redemptionFrames`, and the comment at `vad-context.tsx:40` records that it has been inert since capture moved to Rust — kept so a stored preference does not vanish. It is a live example of what happens when a control outlives the thing it controlled. The wake settings must not join it. If a knob added here stops reaching Rust, it should be deleted, not documented. ## Done means - Both controls persist, survive a restart, and take effect on an open microphone without one. - Off by default; with it off, nothing about today's behaviour changes. - The score is visible while tuning. - Strings extracted through `npm run extract-translations` like everything else in that panel. ## Related - `#87` — where the default threshold comes from - `#88` — the score being drawn - `#89` — what the switch actually turns on - `#91` — the reason it ships off
Author
Owner

The switch this issue asks for should replace the existing always-listening control rather than sit beside it. Operator's call, 2026-08-17.

Why the two-boolean shape in the issue body is wrong

The issue specifies a wake-phrase on/off switch added next to what is already there. That leaves two booleans — always listening and wake phrase — and four combinations, of which only three mean anything:

always listening wake phrase what it means
off off push to talk
on off she hears everything
on on she waits to be called
off on nothing — a gate on a closed microphone

The fourth is unreachable nonsense that both the UI and the code would have to keep defending against forever. Two booleans encoding three states is the bug; the fix is not to validate the combination but to stop being able to express it.

The shape instead

One three-way control, replacing the existing always-listening switch:

  1. Push to talk — the microphone is closed. Hold the key. This is what #33 protects and it must stay reachable.
  2. Wake phrase — the microphone is open and gated. She transmits only what arrived inside #89's window.
  3. Always listening — the microphone is open and ungated. Present behaviour, unchanged.

These map onto machinery that already exists, which is the point: 2 and 3 differ only by #89's enabled flag, and 1 is the microphone being shut. No new concept enters the system; a control that could express an impossible state is replaced by one that cannot.

Wake phrase becomes the sensible default once #91 has run, but it must not become the default before that, because nobody has measured what continuous inference costs on a phone.

Still needed alongside it

The sensitivity number and the live score readout from the issue body stand as written. Sensitivity is only meaningful in mode 2, so it should be visibly inert or hidden in the other two rather than sitting there implying it does something — the exact trap redemptionFrames fell into and which the issue body already warns about.

Default sensitivity is #87's measured operating point: 0.70, which is where it recorded 98.75% detection at 0.19 false positives per hour over 10.7 hours of unrelated speech. Not a number anyone liked the look of.

Migration

The stored preference is a boolean today. A true becomes always listening and a false becomes push to talk, so nobody's microphone changes behaviour on upgrade. Whatever reads the old key has to keep understanding it, or a stored true silently reads as mode 0 and the microphone stops opening for someone who never touched a setting.

**The switch this issue asks for should replace the existing always-listening control rather than sit beside it.** Operator's call, 2026-08-17. ## Why the two-boolean shape in the issue body is wrong The issue specifies a wake-phrase on/off switch added next to what is already there. That leaves two booleans — *always listening* and *wake phrase* — and four combinations, of which only three mean anything: | always listening | wake phrase | what it means | |---|---|---| | off | off | push to talk | | on | off | she hears everything | | on | on | she waits to be called | | off | on | **nothing** — a gate on a closed microphone | The fourth is unreachable nonsense that both the UI and the code would have to keep defending against forever. Two booleans encoding three states is the bug; the fix is not to validate the combination but to stop being able to express it. ## The shape instead One three-way control, replacing the existing always-listening switch: 1. **Push to talk** — the microphone is closed. Hold the key. This is what `#33` protects and it must stay reachable. 2. **Wake phrase** — the microphone is open and gated. She transmits only what arrived inside `#89`'s window. 3. **Always listening** — the microphone is open and ungated. Present behaviour, unchanged. These map onto machinery that already exists, which is the point: 2 and 3 differ only by `#89`'s `enabled` flag, and 1 is the microphone being shut. No new concept enters the system; a control that could express an impossible state is replaced by one that cannot. **Wake phrase becomes the sensible default once `#91` has run**, but it must not become the default before that, because nobody has measured what continuous inference costs on a phone. ## Still needed alongside it The sensitivity number and the live score readout from the issue body stand as written. Sensitivity is only meaningful in mode 2, so it should be visibly inert or hidden in the other two rather than sitting there implying it does something — the exact trap `redemptionFrames` fell into and which the issue body already warns about. Default sensitivity is `#87`'s measured operating point: **0.70**, which is where it recorded 98.75% detection at 0.19 false positives per hour over 10.7 hours of unrelated speech. Not a number anyone liked the look of. ## Migration The stored preference is a boolean today. A `true` becomes *always listening* and a `false` becomes *push to talk*, so nobody's microphone changes behaviour on upgrade. Whatever reads the old key has to keep understanding it, or a stored `true` silently reads as mode 0 and the microphone stops opening for someone who never touched a setting.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#90
No description provided.