Files
ratatoskr/persistent-memory.d/2026-08-12-config-voices-two-voice.md
vh 6e2822cfcb memory: snapshot — dots-tts + config-voices/two-voice split shipped; WT #401 personal-WT deadlock routed
Captures this session's durable state: the RP-voice rebuild (chatterbox->dots
v0.22.2, config-driven voices + two-voice dialogue/narration split v0.22.3) and
the personal WT :8081 deadlock recovery + root-cause routing to worldtree-dev
(WT #401). New substrate foot-guns: dots :8198 is a fleet switchboard, dots'
language validator is an alias-mapper, per-origin TTS toggle, WT hang signature,
dots-streaming-pcm-not-headerless. Three detail files added under
persistent-memory.d/.
2026-08-13 22:59:28 -07:00

2.4 KiB

[2026-08-12] Config-driven voices + two-voice dialogue/narration split SHIPPED (v0.22.3, 5c3d0ad).

Operator ask: "we need rata side config of voices too" + "tempted to do two voices — one for dialogue, one for non-dialogue." Chose (via AskUserQuestion) "config + two-voice together now."

Config (retires the hardcoded _TTS_VOICE_MAP): ~/.config/ratatoskr/voices.json, beside local_agents.json/provider.env (NOT folded into the agent-index schema — isolated from its v1→v2 silent-drop foot-gun). Schema: {"default":"<v>","agents":{"<agent_id>":{"voice":"<v>","narration_voice":"<v>"?}}}. Absent/malformed → built-in DEFAULT_VOICE_CONFIG, logged, never crashes (INV-TTS-5). Initial: donut→donut; sindra→miranda dialogue + emmie narration.

Two-voice = the presence of narration_voice is the split switch (dropped the operator's proposed dialogue_only flag — an agent with no narration_voice is single-voice, which is why dialogue-only Donut needs no special-casing). segment_dialogue splits on quotes (straight "

  • curly “ ”; apostrophes never split a word): quoted = dialogue → voice, rest = narration → narration_voice. Known limit: if a mixed persona speaks WITHOUT quotes, it reads as narration.

Synthesis (tts_stream_stitched replaced tts_stream): spans synth SERIALLY (dots single-consumer), span 0 verbatim (wav header+PCM), spans 1..N header-stripped (accumulate-until-data) → one gapless 48kHz stream with a single leading header (INV-TTS-7). A single-span list is a byte-identical passthrough (INV-TTS-6, no single-voice regression). NOTE the dots-pcm-not-headless-in-stream foot-gun that forced this design (see Tried and abandoned).

New module src/ratatoskr/voices.py (load_voice_config / segment_dialogue / resolve_voice_spans); create_app gains voice_config; entrypoint loads it. Contract DEC-11

  • INV-TTS-5/6/7; new tests/test_voices.py. 545 tests green, new code ruff-clean. Discipline: contract-first + TDD + seam-check + local bug-hunt + live-verify; heid async gates SKIPPED (offered). Live-verified on :8765: Sindra mixed turn → 2 dots calls (emmie+miranda) stitched to one 48kHz WAV (single RIFF header); Donut single-voice unchanged. Cut as patch (self-contained feature, no downstream coordination → patch-defensible → tie-break to patch; operator declined the v0.23.0-minor option by saying "push it"). Builds on 2026-08-11-tts-dots-migration.