say to the embedded Kokoro engine.
Modes (CHATCLI_GATEWAY_VOICE_REPLY)
Legacy boolean values still work:
true → always, false → never. An unknown value falls back to auto — a typo never silences the gateway.
Per-conversation control — the @voice tool
Each conversation owns its preference by asking in natural language: the model calls the @voice tool, and the choice is stored per session (survives daemon restarts) with precedence over the global mode.
The preference lives in
~/.chatcli/gateway_voice_prefs.json (atomic writes), keyed by platform:chat. Decision hierarchy: conversation preference → global mode → in-kind.
Written for the ear (speech-aware)
When a reply will become audio, two layers guarantee natural speech:- The model knows before writing: a directive says the answer will be spoken — conversational prose, short sentences, no emojis, no lists/tables/markdown.
- A hard guarantee in the sanitizer: before synthesis,
StripForSpeechflattens markdown (code dropped, links collapse to labels, tables become prose) and removes emoji and pictographs — TTS engines read their Unicode names out loud, burying the message. Portuguese accents stay intact.
A voice note that actually plays (transcode)
Backends that ignore the format hint (macOSsay emits aiff, espeak emits wav) used to produce a file Telegram shows with a size but cannot play. With ffmpeg on PATH, the gateway transcodes wav/aiff → OGG/Opus (voice-note profile: 48 kHz mono) for every provider; already-compressed formats pass through, and without ffmpeg the original clip is sent as an audio file — visible degradation, never a lost reply.
The full pipeline
Troubleshooting
Related
- Text-to-Speech — backends, embedded Kokoro engine,
@speak - Chat Gateway — inbound transcription, platforms, daemon
- Conversation Hub — cross-channel continuity