5c650049b2
Snapshot of state after this session + actionable plan for the five remaining chantiers (glossary biasing, server-side EOU, TTS fallback, ailiance Realtime wrap, baby-brain migration). Each chantier documents what was discovered (e.g. Kyutai Rust server has no TextPrompt message, Step VAD prs are undocumented), why we did/didn't act on it this session, and a precise entry point for the next session — so the next person/agent doesn't have to re-grep the moshi-server source to know what's possible.
181 lines
8.1 KiB
Markdown
181 lines
8.1 KiB
Markdown
# Voice pipeline roadmap — 2026-05-24
|
|
|
|
Snapshot of the voice stack after the macM1 hints + Kyutai STT + WS
|
|
fallback session. Lives here so the next session (this repo or others)
|
|
can pick up the right context without re-discovering every gotcha.
|
|
|
|
## Current state
|
|
|
|
```
|
|
ESP32 firmware (ESP-IDF feat/idf-migration)
|
|
│
|
|
│ WebSocket /voice/ws (PCM16 16 kHz, json control frames)
|
|
▼
|
|
voice-bridge :8200 (FastAPI, F5-TTS-MLX in-process, MacStudio)
|
|
│ STT_BACKEND = kyutai (default since commit 43b5ddc)
|
|
│
|
|
├── /voice/ws STT → kyutai_stt.py → ws://localhost:8304
|
|
│ (msgpack, /api/asr-streaming, partials forwarded)
|
|
│ ↳ fallback whisper.cpp on KyutaiSttError
|
|
│
|
|
├── /voice/ws intent → LiteLLM :4000 → npc-fast (MLX Qwen2.5-7B Q4 :8501)
|
|
│
|
|
├── /voice/ws TTS → cache → F5 → Piper Tower :8001 (commit a5b00aa)
|
|
│ (Piper EN-only today, plugs the silence)
|
|
│
|
|
└── /usage/stats → polled by dashboard useVoiceUsage (5 s)
|
|
|
|
hints engine :8311 (FastAPI, macM1)
|
|
└── /hints/ask → LiteLLM Studio :4000 → hints-deep (MLX 32B Q4 :8500)
|
|
|
|
Studio crontab @reboot: whisper.cpp :8300, MLX :8500/:8501, moshi-server :8304
|
|
macM1 LaunchAgents: cc.zacus.hints (:8311), cc.ailiance.whisperx (:9500)
|
|
```
|
|
|
|
Runbooks :
|
|
- `tools/macstudio/MACM1_HINTS_DEPLOY.md`
|
|
- `tools/macstudio/MOSHI_STT_DEPLOY.md`
|
|
|
|
## Open chantiers
|
|
|
|
Ordered by ROI / effort, not by priority — pick what matters next.
|
|
|
|
### ⭐ Glossary biasing for Kyutai STT
|
|
|
|
**Goal** : fix `U-SON → eu son nez`, `Cherchez → chercher`, all the
|
|
metier vocabulary holes the generic 1B model can't know about.
|
|
|
|
**Constraint discovered this session** : `moshi-server 0.6.4` Rust does
|
|
**not** expose any `TextPrompt` / `InitialPrompt` message via the WS
|
|
(`rust/moshi-server/src/asr.rs:17` enum `InMsg = Init|Marker|Audio|OggOpus`
|
|
only). The PyTorch prompt mechanism uses `on_text_logits_hook` which is
|
|
in-process only.
|
|
|
|
**Three viable alternatives** :
|
|
|
|
| Approach | Latency | Maintenance | Notes |
|
|
|----------|---------|-------------|-------|
|
|
| Post-correction LLM (`npc-fast` "rewrite this transcript knowing ...") | +500 ms | nil | use the same model already in the chain |
|
|
| Audio prompt pre-pended (cf. `stt_from_file_with_prompt_pytorch.py`) | +2-3 s | regen TTS cache | dose pre-roll = glossary spoken aloud, skip N first Word events |
|
|
| Fuzzy alias dictionary (firmware `voice_dispatcher` already has one) | 0 | manual | extend per-puzzle; **smallest scope, highest ROI** |
|
|
|
|
**Recommended** : start with the fuzzy alias dictionary extension. The
|
|
firmware already runs this on the intent path (see core mem 20793 +
|
|
`voice_dispatcher fuzzy alias matching for Whisper transcription
|
|
errors`). Adding per-puzzle metier terms there is ~30 lines + a small
|
|
YAML. Reserve post-correction LLM for the day fuzzy aliases plateau.
|
|
|
|
### #3 Server-side end-of-utterance
|
|
|
|
**Status** : skipped 2026-05-24. The firmware already sends
|
|
`{"type":"end"}` via its own VAD AFE (`voice_pipeline_ws.c:332`).
|
|
Adding Silero on the server = double VAD + ~500 MB torch dep, almost
|
|
no gain.
|
|
|
|
**If reopened** : the cleanest path is to consume the **Step events**
|
|
already emitted by `moshi-server` (`{type:"Step", prs:[f32],
|
|
buffered_pcm}`) — Kyutai has a built-in semantic VAD. `prs` semantics
|
|
are undocumented (it's a projection `p[0]` of the Mimi LM logits),
|
|
needs empirical reverse-engineering. Budget: 1-2 h of test sessions
|
|
with a printed-Step client, then a tiny `auto-marker on N consecutive
|
|
low-pr frames` rule in `kyutai_stt.py`.
|
|
|
|
### #4 Kokoro / TTS fallback
|
|
|
|
**Status** : Piper fallback wired into `/voice/ws` (commit a5b00aa).
|
|
Kokoro evaluated and rejected for Zacus :
|
|
|
|
- 1 FR voice only (`af` + a handful), maintainer flags G2P FR as weak
|
|
- Mediocre quality on metier terms
|
|
- Piper already in place as fallback infrastructure
|
|
|
|
**Real follow-up** : deploy a **Piper FR model** on Tower (or on
|
|
Studio next to whisper.cpp). The Tower instance is EN-only today, so
|
|
the fallback currently produces an English pronunciation of French
|
|
input — better than silence, but worth fixing. ~30 min :
|
|
|
|
```bash
|
|
# On Tower (or wherever you want the FR fallback):
|
|
mkdir -p /opt/piper-fr/models
|
|
cd /opt/piper-fr/models
|
|
curl -L -o fr_FR-siwis-medium.onnx \
|
|
https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx
|
|
curl -L -o fr_FR-siwis-medium.onnx.json \
|
|
https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx.json
|
|
# Wire a separate piper-server (OpenAI-compatible) on :8002 then
|
|
# set PIPER_URL=http://192.168.0.120:8002 in voice-bridge env.
|
|
```
|
|
|
|
### Plus loin : ailiance OpenAI-Realtime wrap
|
|
|
|
**Goal** : expose Kyutai (and later Moshi end-to-end) behind an
|
|
OpenAI-Realtime-compatible API endpoint inside the `ailiance` gateway.
|
|
This is a *product differentiator* — ailiance becomes "sovereign EU
|
|
Realtime API" vs OpenAI's hosted offering.
|
|
|
|
**Scope** :
|
|
- ailiance gateway already runs on `electron-server:9300` (FastAPI,
|
|
source in `ailiance/ailiance` private repo, see `/home/electron/ailiance/`)
|
|
- Add a `/v1/realtime` WebSocket endpoint that speaks OpenAI's Realtime
|
|
protocol (session events, input_audio_buffer, response.create, etc.)
|
|
- Internally adapt:
|
|
- incoming `input_audio_buffer.append` → `moshi-server :8304` Audio frames
|
|
- moshi-server Word/EndWord → emit OpenAI `conversation.item.input_audio_transcription.completed`
|
|
- response generation: call LiteLLM `npc-fast` (or Helium when Moshi
|
|
full-duplex lands), stream tokens, generate TTS via F5 or Kokoro/Piper
|
|
- ~400-600 lines Python, ~3 days
|
|
|
|
**Pre-requisites** :
|
|
- moshi-server STT live on Studio (done)
|
|
- A spec mapping OpenAI Realtime events ↔ moshi-server msgpack events
|
|
(write this first, it's the hard part)
|
|
|
|
**Next session entry point** : `cd ailiance/ailiance && code .` ; the
|
|
adapter goes in `ailiance/realtime/` as a sub-router of the gateway.
|
|
|
|
### Plus loin : baby-brain migration whisperx → Kyutai
|
|
|
|
**Goal** : replace `whisperx-server :9500` on macM1 with Kyutai STT
|
|
for the conversational path. Keep whisperx for diarisation/speaker ID
|
|
since Kyutai doesn't do that.
|
|
|
|
**Scope** :
|
|
- Two STT endpoints in baby-brain: one streaming (Kyutai) for the live
|
|
avatar/voice agent, one batched-with-diarisation (whisperx) for the
|
|
WML clustering path that needs speaker labels
|
|
- Touch points : `baby_brain/identity/stt_bridge.py` (the WS client),
|
|
`baby_brain/identity/visual_bridge.py` (avatar mood from voice)
|
|
- Decision needed : run a 2nd `moshi-server` on macM1 (local low
|
|
latency, ~600 MB extra RAM in an already-tight env), or point at the
|
|
Studio :8304 instance (1 RTT extra)
|
|
- ~1 day
|
|
|
|
**Pre-requisites** :
|
|
- baby-brain repo cleanup (last session was at PR/refactor stage)
|
|
- benchmark whisperx vs Kyutai on baby-brain's actual conversational
|
|
corpus (which is closer to human voice than the Zacus F5 WAVs)
|
|
|
|
**Next session entry point** : `cd ~/code/baby-brain && code .` ; talk
|
|
to the `stt_bridge.py` author about whether they prefer the WSc proxy
|
|
approach or a direct in-process Python client (the Python `moshi-mlx`
|
|
package would work on macM1 since baby-brain already runs Python).
|
|
|
|
## Cross-cutting things learned this session
|
|
|
|
- **MLX workers on Studio bind to 127.0.0.1** (see `lsof` proof in
|
|
`MACM1_HINTS_DEPLOY.md`). Anything Tailnet-reachable on Studio has
|
|
to be explicitly `0.0.0.0` in its launch args. Today the only
|
|
exceptions are the gateway-routed services (`:9301`, `:9303`, `:9327`
|
|
etc.) and the new `:8304` Kyutai.
|
|
- **Non-interactive SSH on Studio doesn't see Homebrew** (cmake, etc.).
|
|
Always prepend `PATH=/opt/homebrew/bin:$PATH` in cargo/make invocations
|
|
shipped over `ssh ...`.
|
|
- **pyo3 0.23.5 + Python 3.14+** needs `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`
|
|
at build time. Bit anyone building Rust packages with Python bindings.
|
|
- **Voice-bridge venv has no pip** (uv-style minimal venv). Always use
|
|
`uv pip install --python /Users/clems/voice-bridge/.venv/bin/python …`
|
|
to add deps.
|
|
- **WS `/voice/ws` is already implemented end-to-end** (server +
|
|
ESP-IDF client) — don't reinvent the protocol, just add backends to
|
|
the existing branches.
|