Auto-call now forwards the scene to /debug/ring so the phone plays
that scene's hint from its local SD pack after the greeting. Bumps
ESP32_ZACUS for the chained-hint firmware.
macOS say inserts a ~4 KB FLLR filler chunk between fmt and data. The
PLIP firmware parses only a small header buffer and never reaches a
data chunk pushed past it (WAV parse failed -> silent). Rebuild a
canonical 44-byte-header RIFF/fmt/data WAV before staging. Verified:
Zacus greeting plays 5.05s from /sdcard on pickup, no gateway/model.
Pre-render directory greetings + scene hints with macOS say (16 kHz
mono FR) and stage them on the PLIP microSD via POST /game/file — gives
the phone local voice content with no model in RAM. Reusable/idempotent
(tools/tts/generate_plip_sd_pack.py). Bumps ESP32_ZACUS for the SD write
path. Generated WAVs gitignored (reproducible).
The phone now rings on cue from the game with NO firmware change. A
call_on_scene map (phone_directory.yaml: {SCENE_X: number}) tells the
gateway which scenes are scripted incoming calls; since the runtime
already pushes the scene to /game/step, the gateway auto-rings the PLIP
with that NPC on scene entry (deduped, once per entry). Factored a
_ring_board helper (shared by /game/call + auto-ring). Map ships empty
(no surprise rings) with commented examples for the designer.
The gateway now hints on the SCENE the runtime pushes (idf_zacus POSTs
its native SCENE_* to /game/step?scene=…) — no step<->scene mapping
needed since both sides use the same SCENE_* ids. _active_scene() uses
the pushed scene (falling back to the current step's scene). Bump
ESP32_ZACUS to 653a299 (npc_engine pushes scene changes).
The runtime triggers a scenario phone call: POST /game/call?number=18
[&step=STEP_X] rings the PLIP handset with the chosen NPC (resolved via
the boards registry) and sets the current step so the NPC disguises that
step's hint. Completes the game->phone trigger path (chantier #2).
Wire the phone NPCs to the scenario state machine + hint bank so a call
ADVANCES the puzzle instead of being generic chat. load_scenario_hints
maps each step (zacus_v3.yaml screen_scene_id) to npc_phrases.yaml
hints[scene][level]; voice/reply weaves the CURRENT step's hint into the
NPC persona, escalating the level (subtle->near-explicit) with the
conversation. New GET/POST /game/step lets the runtime set the current
step as the player progresses.
Condition the handset capture by RMS (not peak) before whisper: the
voice body is ~0.5-1.5% FS and a transient peak defeated peak-normalise,
leaving it inaudible. RMS-normalise to ~20% FS + clip. Recovers weak
captures whisper returned '' for (verified: -> 'C'est parti !').
Switch the voice-loop STT from Kyutai to whisper.cpp large-v3-turbo
(subprocess, high-pass conditioned): it decodes the weak/noisy SLIC
handset captures Kyutai returned '' for. Filter whisper's silence/noise
hallucinations (sous-titrage, radio-canada, tipeurs, '...') so the NPC
never replies to a phantom line. Bump ESP32_ZACUS to 54022ed (VAD
recalibration for the quiet voice + bell stop on pickup).
Gateway: drop the remote ailiance fallback — the NPC LLM is now the
on-box granite ONLY (local spoken line on failure, never remote).
Prewarm the persona KV-cache in the background at greeting time so the
first reply is fast instead of paying ~17 s of cold prompt processing.
Add temperature + repetition_penalty so granite4-tiny stops looping.
STT: cut the trailing-silence pad 2 s -> 0.5 s (callers already pad),
shaving STT latency. Bump ESP32_ZACUS to cfe429d (hook debounce).
Bump ESP32_ZACUS to 82759ee (voice-activated two-phase capture, mic
diag). Gateway: drop the local-LLM (granite) timeout 70s -> 8s so a
cold/hung local model fails over to the remote backend fast instead of
stalling the whole turn past the firmware's reply timeout.
Pull in ESP32_ZACUS fix(plip) 8c076d8: gate audio playback on the real
SHK line and remove all hook forcing (s_hook_override,
phone_force_offhook, /debug/offhook). Fixes the silent incoming-call
greeting caused by the two off-hook flags desyncing during ring.
The Lm holds a persistent rotating transformer_cache shared by every
LmGen. LmGen only resets its own gen_sequence/step_idx, so across
requests the cache kept accumulating stream positions until it
saturated and the model emitted only padding tokens, returning empty
transcriptions ('') for every request. Reset transformer_cache (and
depformer_cache) per request, mirroring Lm.warmup(). Validated: 40
consecutive requests of a known-good sample, 0 empty.
SLIC handset injects low-frequency rumble/DC drift that swamps speech
and causes Kyutai STT to output nothing. Apply a box moving-average
high-pass (~110 Hz) before peak-normalisation. Also raise gain cap to
x40 and target_peak to 0.7 (mic is still very quiet post-ES8388 fix).
Verified: same capture transcribes empty without HP, clean French with.
Add _wav_pcm(), _build_wav(), _normalize_wav_for_stt() (peak-normalize
with capped gain) to handle the PLIP handset mic low signal level and
placeholder 0xFFFFFFFF WAV headers from /voice/capture stream.
Rewire _pad_wav_silence() and _transcribe_kyutai via these helpers.
- Replace Kyutai TTS with macOS say (16 kHz mono FR);
NPC voice map _SAY_VOICE_MAP; helper _tts_say_sync
- Local vllm-mlx :8520 as primary LLM (_chat_one),
ailiance as fallback, spoken phrase as last resort;
_chat_reply never raises (loop stays alive on 502)
- Pad 800 ms silence before /transcribe (_pad_wav_silence)
to prevent last-word truncation
- Background _keep_llm_warm ping every 600 s (cold start)
- Root log level INFO; X-Zacus-Heard/Said headers
- test_chat_reply: empty_choices expects spoken fallback