Spaces:
Running
A newer version of the Gradio SDK is available: 6.22.0
Voice Mode Plan β make the bot hear like OpenWebUI's browser voice mode
Status: PLAN (nothing here is implemented yet except where marked DONE)
Repo: T:\reachy-mini\HF_Repos\Reachy_OpenWebUI_v2 β Space Jacid23/Reachy_OpenWebUI
Bots: .203 reachy-wireless (desk), .204 reachy-mini (Pi 5)
Rule: NO remote daemon/app restarts β the user reboots the bot himself.
After any app update from the dashboard: check camera frames; if dead run
/venvs/apps_venv/bin/pip install -U reachy-mini==1.9.0 (user or assistant-with-permission) β see memory notes.
1. The riddle, answered
Browser voice mode, Conduit, and the bot all use the SAME OpenWebUI backend:
batch Whisper STT (/api/v1/audio/transcriptions), streaming chat, sentence-level
TTS (/api/v1/audio/speech). None of them are realtime models. They are all
cascades. The browser and Conduit feel realtime because of three things the bot
lacked β all fixable:
- A working VAD with proper hysteresis. Conduit uses Silero VAD (the
vadDart package) with a dual-threshold envelope. The bot's Silero ONNX port was broken from day one (missing the v5 64-sample context β returned prob ~0.001 on real speech) so it silently ran on a crude RMS energy gate. FIXED in v0.6.5.7 (2026-07-19), verified on-bot: speech now scores mean 0.86. - A clean audio front-end. Phone/browser mics get OS-level AEC, noise
suppression, and AGC for free. The bot's mic array is BETTER hardware β an
XMOS-class DSP with on-chip AEC, beamforming, and AGC β but it is barely
configured (only
PP_AGCGAIN=5.0is applied). - Tuned endpointing + latency masking. Conduit ships proven constants; the bot's were guesses made to compensate for the broken VAD.
Conclusion: there is no architectural reason the bot can't feel like browser voice mode. Torch is NOT required β the fixed ONNX VAD is now provably correct.
2. Evidence collected (2026-07-19)
Conduit's proven VAD envelope
From T:\reachy-mini\openwebui_related\conduit\lib\features\chat\services\voice_input_service.dart:
| Constant | Value | Meaning |
|---|---|---|
| sample rate / frame | 16000 / 512 | same as bot |
| positiveSpeechThreshold | 0.6 | prob to ENTER speech |
| negativeSpeechThreshold | 0.35 | prob to STAY in speech (hysteresis!) |
| preSpeechPadFrames | 16 (~512 ms) | pre-roll kept before trigger |
| minSpeechFrames | 8 (~256 ms) | discard shorter blips |
| endSpeechPadFrames | 6 (~192 ms) | audio kept after end |
| redemptionFrames | 4..max (~128 ms+) | silence tolerated before ending |
Controller: .../voice_mode/chat_voice_mode_controller.dart (1650 lines) β
pauses mic during assistant speech (no barge-in), batch STT, sentence TTS.
Bot hardware audio DSP (huge, untapped)
Daemon exposes runtime audio-DSP control:
POST http://<bot>:8000/api/audio/config/apply(ApplyAudioConfigRequest)GET http://<bot>:8000/api/audio/config/parameter/{name}Parameter families found in/venvs/mini_daemon/.../reachy_mini/media/audio_control_utils.py:AEC_*(echo canceller: AECSILENCELEVEL, FILTER_LENGTH, PATHCHANGE, β¦),AEC_FIXEDBEAMS*(beamforming: azimuth/elevation/gating),PP_AGCGAIN, HPF etc. Currently onlyPP_AGCGAIN=5.0is applied at app start.
Current bot pipeline state (post today's fixes)
- Silero ONNX VAD fixed (context window) β Space v0.6.5.7.
- Single VAD threshold now 0.3, RMS fallback raised to 0.10 (was falsely
triggering at 0.014), mic capture
Headset,0back at 37/60 (62%). - Echo handling = VAD fully suppressed while TTS plays
(
_suppress_vad_until/_active_pipeline_countinsrc/Reachy_OpenWebUI/sub_apps/conversation_app/local/handler.pystep 5) β no barge-in, and trailing suppression windows eat the user's next utterance. - STT observed slow: 4563 ms for 9 s audio on the GB10 (needs investigation β browser voice hits the same endpoint; short utterances + warm model are fast).
- Known infra gotchas: SDK-downgrade-on-update, media-stream wedge on remote restart (hence the no-restart rule), pipewire masked on both bots.
3. Gap analysis
| Piece | Browser/Conduit | Bot today | Gap |
|---|---|---|---|
| VAD model | Silero, working | Silero ONNX, working since v0.6.5.7 | none |
| VAD envelope | dual threshold + pads | single threshold + chunk counts | Phase 1 |
| Mic front-end | OS AEC/NS/AGC | XMOS AEC/beamform barely configured | Phase 2 |
| Echo/barge-in | mic paused during TTS | VAD hard-suppressed during TTS | Phase 3 (can EXCEED them) |
| STT | same backend | same backend, seen slow | Phase 4 |
| Latency masking | call UI feedback | none | Phase 4 |
| Transport | none (local mic) | daemon media stream (fragile) | out of scope; reboot rule |
4. The plan
Phase 1 β Adopt Conduit's VAD envelope β DONE, shipped v0.6.5.8
Implemented 2026-07-19: dual-threshold hysteresis in vad.py (enter=threshold, stay=threshold-0.25 floor 0.15; RMS gate diagnostic-only when model loaded); defaults now threshold 0.6, onset 3, silence-end 16 (~512ms), min-speech 8. On-bot verified: speech 127/149 chunks / 3 segments; 4s noise 0 triggers. NOTE: user's persisted vad_threshold=0.3 overrides the new default β after updating the app, POST /vad {"vad_threshold": 0.6} (or set in settings UI).
(original Phase 1 text follows)
File: src/Reachy_OpenWebUI/sub_apps/conversation_app/vad.py + local/handler.py.
- Add dual-threshold hysteresis to
SileroVAD.is_speech: enter atthreshold_enter(default 0.6), remain while prob β₯threshold_stay(default 0.35). Keep RMS fallback only as a diagnostic (log when it WOULD have fired; do not let it trigger). - Map envelope to Conduit values in handler chunking (512 @16 kHz frames):
pre-roll (
lookback_buffer) β₯ 16 frames, min speech 8 frames, end pad 6, silence-end (redemption) configurable 4β30 frames (expose in settings as today'svad_*_chunks). - Keep everything settable via the existing
/vadendpoint + settings UI; change only the defaults. - Bump version, push. User updates bots (then SDK-check ritual). Acceptance: VAD probe shows silence β€0.05 prob, speech β₯0.6; no clipped first syllables; utterance ends within ~0.5 s of stopping; zero triggers from room noise at normal mic gain (62%).
Phase 2 β Wake the XMOS front-end β CORE DONE, shipped v0.6.5.9
2026-07-19: root cause was the app's own inherited startup config
(audio/startup_config.py): MIN_NS/NN 0.8 (suppressor passing 80% of noise)
- AGC max gain 10x. Now MIN_NS/NN 0.15, AGC gain/max 4.0 β user-verified live:
noise floor collapsed to faint fan hum. HPF already at mode 2 (max).
Remaining Phase 2 items: beamforming (AEC_FIXEDBEAMS*) experiments, verify
persistence after user's reboot+update, STT latency (11s for 9s clip on GB10 β
CPU whisper; fix is OpenWebUI-side GPU STT server or Deepgram).
User's hand-tuned analog mixer sweet spot (card 0, alsamixer, 2026-07-19):
Headset,0β β8 dB,Headset,1β β22 dB. Persist withsudo alsactl store. Caution: the app's mic-volume slider overwrites Headset,0.
(original Phase 2 text follows)
Read current values first with GET /api/audio/config/parameter/{name}.
- Inventory: dump all
AEC_*andPP_*current values into a file for baseline (script it; read-only). - Experiment matrix (apply one at a time via
POST /api/audio/config/apply, verify with the VAD probe logs + recorded STT quality):AEC_HPFONOFFon (kill low-frequency rumble/fan)PP_AGCGAINsweep (current 5.0; try 2β8) with capture fixed at 62%AEC_FIXEDBEAMSONOFF+ azimuth toward the user's usual position- AEC on with far-end reference (see Phase 3)
- Whatever wins: persist by having the app apply it at startup (the app
already applies
PP_AGCGAINβ extend that config block; find it viagrep -rn PP_AGCGAINin the app/daemon startup path) or via daemon config. Caution: apply-parameter is live but survives β reboot semantics unknown β verify persistence after the user's next reboot before relying on it. Acceptance: speech probs at conversation distance β₯0.8; noise floor rms < 0.01 at 62% gain; STT word error noticeably down (subjective A/B is fine).
Phase 3 β Barge-in β FIRST CUT SHIPPED v0.6.6.0 (untested on hardware)
2026-07-19: handler no longer discards mic audio during playback. VAD keeps running on the live stream; ~320ms sustained speech (BARGE_IN_CHUNKS=10 at 0.6 enter threshold) calls _interrupt_current_response and listening resumes instantly (lookback keeps the interjection's start). Env knobs: REACHY_BARGE_IN=0 reverts to half-duplex; REACHY_BARGE_IN_CHUNKS tunes sensitivity. RISK: if the XMOS AEC leaks our own voice, the bot may interrupt itself β raise chunks or disable, then investigate AEC far-end (AEC_AECCONVERGED probe while TTS plays). Test protocol after update+reboot: (1) normal turn works; (2) talk over the bot mid-reply β it stops and handles the interjection; (3) stay silent through a long reply β it must NOT self-interrupt.
(original Phase 3 text follows)
Today the app deafens itself while speaking (handler step 5). With the XMOS AEC cancelling the bot's own speaker from the mic signal, we can listen while talking β which neither browser voice mode nor Conduit does.
- Verify AEC has a far-end reference on this hardware (speaker loopback):
check
AEC_NUM_FARENDS,AEC_FAR_MIC_INDEX,AEC_AECCONVERGEDwhile TTS plays (read-only probes while user runs a conversation). - If converged AEC is real: replace the hard
_suppress_vad_untilwindow with "VAD active during playback, but require prob β₯ enter-threshold for N consecutive frames (e.g. 10) to interrupt"; on trigger, call the existing interrupt path (_interrupt_current_response) then treat as new utterance. - If AEC is not usable: fall back to half-duplex but shorten the trailing suppression (currently eats speech after TTS ends) to β€300 ms. Acceptance: user can talk over the bot and it stops and listens (like ChatGPT realtime); no self-triggering from its own voice.
Phase 4 β Latency: measure, then mask
CORRECTED 2026-07-19: NO network problem β the running apps on both bots already use the direct LAN route http://172.30.30.15:3001 (9.9ms), set via the app UI (UI value overrides the .env OPENWEBUI_URL fallback; do not diagnose from the .env file β ask the running app: GET :7860/status β openwebui_url). The .env fallbacks were aligned to :3001 anyway. Other routes that exist: openweb.sunrisecablema.com β 172.30.30.200 (LAN reverse proxy) and gb10.sunrisecablema.com (Cloudflare-proxied, WAN) β for external clients, not the bots. Network is NOT a latency lever; whisper speed is.
- Investigate the 4.5 s STT: time
POST /api/v1/audio/transcriptionsfrom the bot with a 3 s wav (script exists in session history). If slow on GB10, check OpenWebUI's STT engine setting (faster-whisper model size / GPU use) β same win applies to every client. - Use the ported LatencyTracker (
REACHY_LATENCY_DETAIL=1) to get per-stage numbers for 10 turns; attack the biggest stage only. - Masking: on
Speech detectedend (STT upload start), fire an instant "listening/thinking" cue β antenna twitch or attitude accent move via the existing reactions/moves layer. Cheap and transforms perceived latency. - Optional eager-STT: at speech-END, we already upload immediately; consider ALSO uploading a partial at 2 s into long utterances so Whisper is warm (needs care with OpenWebUI; low priority).
Phase 5 (optional, the real realtime) β streaming ASR transplant
Port lyon_chatbox cascade's streaming ASR provider layer
(T:\reachy-mini\HF_Repos\lyon_chatbox\src\lyon_chatbox\cascade\asr\ β
base_streaming.py, deepgram.py) into conversation_app as an alternative
STT path: audio streams continuously, Deepgram does server-side endpointing
and partials, local VAD leaves the critical path entirely. Partials also feed
the transcript-reactions layer for mid-sentence robot reactions.
Cost: Deepgram API key + cloud dependency for STT only (LLM/TTS stay local).
This is the only phase that changes the architecture; Phases 1β4 likely make
it unnecessary.
5. Order of work & effort
- Phase 1 β one sitting, app-only, ships via Space update.
- Phase 2 β experiments on
.203with user present (mic A/B), an afternoon. - Phase 4.1/4.3 β quick wins alongside Phase 2.
- Phase 3 β after Phase 2 confirms AEC; the flagship feature.
- Phase 5 β only if still unsatisfied.
5b. Torch on the bots (if ever needed β user-verified command)
Not required by any current phase (ONNX VAD works). If a phase needs torch
(torch-native Silero, GLiNER entity reactions, torchaudio):
uv pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cpu
(run inside /venvs/apps_venv; CPU wheels, aarch64-compatible.)
6. Hard-won constraints (do not relearn these)
- Do NOT restart daemon/app remotely; wedges media streams. User reboots.
- Dashboard app updates may downgrade
reachy-minito 1.8.0 β camera dies β pip-upgrade to 1.9.0 + user restarts. - pipewire is masked on both bots β leave it masked.
_HTML_TAG_REin handler preserves<|...|>TTS tags (Higgs) β keep it.- Mic capture control is
Headset,0on card 0 (0β60 scale); the app's mic_volume writes it. 37 (62%) is the sane baseline. - Journald is volatile on the bots β capture evidence before reboots.