Device streams continuous 20ms Opus frames with no turn markers. The loop
now decodes Opus, feeds the VAD, fires one turn on end-of-speech, and
replies with the firmware JSON contract (tts.state / stt.text) + Opus audio.
- app/audio/opus.py: RealOpus/Opus16kEncoder/PassthroughOpus; fix add_dll_directory
to use absolute paths (WinError 87)
- app/xiaozhi/websocket.py: DeviceLoop.run() = decode -> VAD -> run_turn -> reply
- app/config.py: AudioConfig (opus kind + VAD thresholds)
- config.yaml: audio.opus=real, VAD 5/8/3000 frames
- tests/test_app.py: utterance = loud burst + silence; asserts stt + audio + tts stop
- requirements.txt: opuslib>=3
21/21 tests; live handshake verified via zhi.moreminimore.com on venv python.
- main() previously used Config() defaults, silently ignoring config.yaml
(port + token); now loads config.yaml from repo root (BRIDGE_CONFIG override)
- config.yaml: port 8766 (8765 taken on this host), real sha256 token for xiaozhi-main
- smoke_live.py: live WS verification (health, auth rejection x2, handshake, full turn)
- README: correct run command (python -m app.main)
- HANDOFF/plan: Phase 1.5 live-verified evidence