← Choice Health 2026-06-30-voice-capability-hierarchy.md raw .md

Voice + Render-Mode Capability Hierarchy

Date: 2026-06-30 Status: Proposal (design before build). Related: LLM-POLICY.md (the existing cascade this mirrors), preflow-architecture.

Implementation status — RESUME HERE

Branch: voice-capability-hierarchy (off main). Pushed? local-only so far.

Done (committed):

Next (do in order):

Honor the Codex review findings below (per-state live voice is impossible; ad-lib = non-play_audio state; capability lives on StateDef/GoalSegment; use a generic cascade helper, not the LLM resolver).


The idea (from the operator)

"the capabilities should be able to use the LLM at a level of overall, by flow or by group or by state — this would give us a hierarchy." "prerender the voice in the preflow … but also have an option to go to gemini-live in a state … so we could test adlib-type workings."

RIFF already has this hierarchy for the LLM (state → group → flow → global cascade; FlowLlmConfig.classes + default_class; per-state llm:; inherit walks up). This proposal makes voice and render-mode first-class citizens of the same cascade.

Two new per-state capabilities

1. Voice (name + style), resolved via the cascade

A voice is a Gemini voice name + an optional style/accent instruction:

voice:
  default: anchor
  classes:
    anchor:     { name: Umbriel, style: "Polished British RP news anchor, warm and clear" }
    gatekeeper: { name: Charon,  style: "Deep, formal station-ops voice" }
states:
  some_state:
    play_audio:
      voice: gatekeeper      # class name, explicit voice, or "inherit" → group → flow → global

Resolution = state → group → flow.voice.default → global, most-specific wins — identical shape to the LLM resolver. The style maps to the TTS system_instruction (that plumbing already landed in openings.py).

2. Render mode: prerendered vs live (ad-lib)

Scope note (2026-07-10): per the adopted boundary-architecture ADR (C7), call-time synthesized narration of validated slots/runtime results — dynamic_announce — is a distinct compiled audio-only state type with its own delivery receipts and input-hub contracts, not a play_audio.render_mode value. The render-mode dial below covers voice/synthesis identity for a state's own audio, and stays.

Per state, where does the audio come from:

This is the dial that lets a flow mix scripted briefs (prerendered) with ad-lib turns (live), per state.

Why this is now tractable (foundation already in place)

Implementation plan (opt-in; existing flows unchanged)

The default voice/mode stays exactly as today, so droid flows and all current tests are unaffected. A flow opts in by adding a voice: block.

  1. Types + loader — add FlowVoiceConfig (default, classes: {name, style}) parallel to FlowLlmConfig; parse a voice: flow block and per-state play_audio.voice / render_mode: live.
  2. Resolverresolve_voice(flow, state_id) and resolve_render_mode(...) reusing the LLM cascade walk (state → group → flow → global).
  3. Preflow — render each prerendered state with its resolved voice + style (replace the hardcoded space_channel synth with cascade resolution).
  4. Transport — for render_mode: live, set the Live session's voice + system_instruction to the resolved voice/style for that state instead of playing a prerendered WAV.
  5. Adopt in Space Channel — delete the droid voice, set voice.default: anchor (Umbriel + British). Migrate droid_full_buffer states to the prerendered static path. Update the droid-coupled tests.
  6. Verify on a real call — the operator dials the line and confirms British Umbriel plays (prerendered) and any live state ad-libs.

Codex plan review (2026-06-30) → REVISE; design reshaped

A pre-build Codex review found 9 substantive issues. The feasible design after folding them in:

Revised, decomposed plan

Open design question

Keep voice: and llm: as parallel flow blocks (each with its own classes/cascade), or unify into one capabilities: block with llm / voice / mode sub-keys sharing a single resolver? Parallel is less refactor; unified is cleaner long-term. Recommend parallel now, unify later if a third capability appears.

Risk / discipline