- Add detailed perf baseline Pi5 (live draft 800ms/8KB, quick ack >30 chars, instant audio Alex<Kokoro, macmini-live-final 753ms vs 11s fallback) - Rank pain points P0-P3: Mac mini SPOF :7331 down now, patch fragility 1675 lines, ground-truth lock boy->voice, disk IO storm, no sentence TTS queue, single VAD vs adaptive FreeFlow effectiveSilenceThreshold, empty trigger - Deep dive mrinalwadhwa/freeflow 91★: PolishPipeline 43 rules, <keep> tags, collapseAdjacent, cleanSpuriousCommas, stripNoise/fillers, knownTerms, guard_against_truncation 7f96ccd, skip 83% 0.55s median, warm backup 91% zero handshake, adaptive silence far=0.001 near=ambient*1.2 capped, DictationPipeline actor detached AVAudioEngine + BT SCO 3s timeout, chunk buffer sentence boundary, SessionTiming - OSS V2V 2025-26: Pipecat 6.5k★ TurnTracker/aggregator/barge-in/e2e tester, LiveKit/TEN, Moshi 7B Mimi codec 12.5Hz 160ms full-duplex dual-stream, Qwen2.5-Omni 2.8-4s — borrow ideas + why stay pipeline text middle = tool-use - 3-layer ground-truth fix: L1 deterministic 0ms, L2 semantic ANE 3B with precedingText + KNOWN_TERMS + alternatives + overlap 0.45 truncate 0.25, L3 Hermes disclaimer + paraphrase confirmation builder build_voice_user_message() - Native M2 target WS /api/voice/live single endpoint spec: binary PCM s16le 16k / empty <1600B trigger / JSON start/stop/ping, events ready→listening→draft volatile 60ms 400ms→transcript final polished+raw+alts→instant_response contextual>30→instant_audio Kokoro 200ms→response_text streaming sentence buffer timed 300ms tail + crossfade→done background, VAD dual 1250ms adaptive, barge-in level>0.12, multi-partial while tool runs - Power truth M2 Air 14,2 macOS 26.5.2: SpeechAnalyzer 0mW ANE 7.3W spike 33% XPC 27-38x RTF 196M EN, Foundation 3B ANE 258-498ms 2GB quant - Phased plan P0→P3 quick wins: disclaimer wrapper + knownTerms + alt transcriptions + guard tune + in-mem ByteArray + adaptive silence + empty trigger + sentence queue + warm daemons + desktop hook ChatGPT parity - Refs: patches, hermes_plugin/__init__.py, pythonwhisper ApplePipeTranscriber pipe volatile 31 drafts/6.3s, apple-speech-api skill ANE traces, HTML diagrams hermes-live-voice-native + oss-comparison + imperfect-fix
Hermes ESP32 Voice Gateway
Durable custom voice gateway for Reyna family ESP32 + iPhone Watch devices. Survives hermes update (which wipes in-tree patches to gateway/platforms/api_server.py).
Upstream issue: ESP32 push-to-talk endpoints POST /api/esp32/voice and WS /api/esp32/voice/ws with:
- live draft ingestion via Mac mini
speech_live_transcribe(ApplePipeTranscriber volatile drafts, ~800ms, 2.12% WER) - instant smart reply via Mac mini
apple_llm_quick_reply(Apple 3B ANE ~300ms contextual, not generic template) - final transcription prefers
macmini-live-finalover local faster-whisper base (753ms vs 11-13s cold) - parallel instant future started at STOP before final transcribe
- persistence flags
persistent=True,is_instant=True,keep_instant=Truefor iPhone client sticky UI - screen animation + TTS via
play_audio_base64/play_mp3_base64tohttp://<source_ip>/api/mcpscreens - audit log
~/.hermes/.../audio_cache/esp32/voice_requests.jsonl
Why a separate repo + plugin
hermes update does git stash --include-untracked + pull origin/main, dropping local modifications to api_server.py. The plugin esp32-voice-gateway lives in ~/.hermes/plugins/ (user plugin dir, never overwritten), and on every Hermes start auto-checks whether the custom routes are present. If missing (after an update), it auto-restores them from the bundled patch and verifies.
What's tracked here
patch/api_server_esp32_live_draft_with_llm.patch— full 1675-line patch that is currently running on Adolfo's Pi (verified 2026-07-14)hermes_plugin/— Hermes user plugin (plugin.yaml+__init__.py) installable viahermes plugins installscripts/restore_after_update.py— manual restore + verification (400 not 404 check)docs/— original excerpts + audit references
Quick start
1. Create repo on Gitea
The token in ~/.git-credentials only has write:repository, not write:user, so repo creation via API is blocked. Create an empty repo via Gitea UI:
https://git.reynafamily.com/repo/create
- Name:
hermes-esp32-voice-gateway - Visibility: private (contains no secrets, but references family device IPs)
- Uncheck initialize README
Then:
cd ~/Projects/hermes-esp32-voice-gateway
git remote add origin https://adolforeyna:TOKEN@git.reynafamily.com/adolforeyna/hermes-esp32-voice-gateway.git
git push -u origin main
(TOKEN is the existing ~/.git-credentials token — git credential helper already does this)
2. Install the durable plugin
# From this repo
cp -r hermes_plugin ~/.hermes/plugins/esp32-voice-gateway
# or symlink for dev
# ln -s ~/Projects/hermes-esp32-voice-gateway/hermes_plugin ~/.hermes/plugins/esp32-voice-gateway
hermes plugins enable esp32-voice-gateway
hermes gateway restart
3. Verify
# Routes should be 400 (not 404) when hit without WS upgrade:
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $API_SERVER_KEY" http://127.0.0.1:8642/api/esp32/voice/ws
# -> 400 (means route exists, missing Upgrade header) — NOT 404
# POST empty should be 400 not 404:
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $API_SERVER_KEY" http://127.0.0.1:8642/api/esp32/voice -X POST --data-binary @/dev/null
# -> 400
4. After hermes update
The plugin auto-restores on next import (gateway start). Check logs:
grep -i "esp32.*restore\|esp32.*patch\|ESP32" ~/.hermes/logs/gateway.log | tail -20
# If auto-restore happened, it logs patched file + needs restart:
systemctl --user status hermes-gateway-kids hermes-gateway --no-pager
# The plugin will request one restart cycle; if stuck in deactivating, run:
# pkill -9 -f "hermes_cli.main gateway run"; systemctl --user reset-failed hermes-gateway*; systemctl --user start hermes-gateway
Manual restore:
~/Projects/hermes-esp32-voice-gateway/scripts/restore_after_update.py
Architecture (for reference)
- Incoming: iPhone 192.168.68.150:8080
iphone6(WAV POST) + ESP32 tablets 192.168.68.129grace-esp32/elias-esp32(WS PCM s16le 16k mono) - Gateway:
gateway/platforms/api_server.pyAPIServerAdapterPOST /api/esp32/voice→ saves WAV, transcribes (macmini-live-final preferred), runs Hermes agent turn, TTS → WAV (ffmpeg to 16k mono),FileResponseWS /api/esp32/voice/ws→ ingest s16le binary frames, every 800ms snapshot →_s16le_to_wav_bytes→speech_live_transcribe(Mac mini pipe volatile) fallback tiny → emitdraft/interim_transcript, on STOP → parallel instant futureapple_llm_quick_reply(must be >30 chars, else use final transcript for context), final transcribe Mac mini full buffer (11588ms fallback if Mac down), emitinstant_response persistent=True+response_text is_instant=True+done instant_ok+thinking keep_instant, background_run_esp32_voice_background(screen animation/text/image/audio viahttp://<source_ip>/api/mcp)
- MacMiniMCP:
~/Projects/MacMiniMCP(canonical on Mac) 37 tools includingspeech_live_transcribe,apple_llm_quick_reply,speech_synthesize_base64,voicebox_quick_reply(Aiden boy) - Pitfalls documented in
hermes-multi-profile-voice-gatewaysskill references:esp32-voice-500-import-loop.md,esp32-voice-gateway-update-survival.md, etc.
Contributing
This repo is Reyna family private. Changes to the voice gateway should update both:
patch/api_server_esp32_live_draft_with_llm.patch(regenerated fromgit diffof~/.hermes/hermes-agent/gateway/platforms/api_server.py)- The plugin's restore logic (it just reapplies patch, so keeping patch current is enough)
Regenerate patch:
cd ~/.hermes/hermes-agent
git diff gateway/platforms/api_server.py > ~/Projects/hermes-esp32-voice-gateway/patch/api_server_esp32_live_draft_with_llm.patch
cp ~/.hermes/esp32_live_draft_with_llm_patch.patch ~/Projects/hermes-esp32-voice-gateway/patch/ 2>/dev/null || true
TODO
- Extract to true standalone plugin that injects routes at runtime without touching file (eliminate patch reapply entirely). Requires hooking
APIServerAdapter.connect— current plugin does auto-restore via file patch for reliability. - Add integration test that curls both endpoints after restore.
License
Private - Reyna family.