feat(P3): mind/mouth split — separate voice model for the final reply (seam, default off)
The mind (chat backend/model) decides, reasons, and runs tools → a draft; the mouth re-voices that draft in her character. Default: no mouth configured → the mind's draft IS the reply, bit-for-bit the old behavior (and old streaming path untouched). - config: MOUTH_BACKEND / MOUTH_MODEL. The slot for an eventual fine-tuned voice. - chat: _mind_loop (tool/generation loop, non-stream, returns draft + tools_run), _voice_pass / mind.voice_messages (re-voice the draft, keep every fact/number), _mouth_target (active only when configured AND != mind). respond + respond_stream branch: mouth off = stream the mind directly (unchanged); mouth on = mind decides + runs tools, then the mouth streams the re-voiced reply. Falls back to the draft on any mouth failure (chat never breaks). - Key payoff: the mouth needs no tool support (the mind handles tools), so it can be a non-tool character model (Dolphin / Claude / fine-tune). Makes the fine-tune easy: teach a small model to *sound* like Lyra, not to be smart. - tests: mouth target on/off, voice_messages shape, voice_pass revoice+fallback. Suite 96 green, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -290,6 +290,25 @@ def _deliberate_part(ctx: TurnContext) -> TurnContext:
|
||||
PIPELINE = (_perceive, _route, _compose, _deliberate_part)
|
||||
|
||||
|
||||
# --- mouth (the voice pass: re-render the mind's draft in her character) -----
|
||||
|
||||
_VOICE_NOTE = (
|
||||
"↑ That was you working the answer out — a draft Brian has NOT seen. Now say it to him "
|
||||
"in your own voice: warm, direct, specific, in character, opinionated. Keep every fact, "
|
||||
"number, name, and decision exactly as in the draft — change only the wording so it sounds "
|
||||
"like you, not a generic assistant. No preamble, no meta, no 'here's a friendlier version' "
|
||||
"— just your actual message to Brian."
|
||||
)
|
||||
|
||||
|
||||
def voice_messages(messages: list[Message], draft: str) -> list[Message]:
|
||||
"""Prompt for the mouth model: the full turn context + the mind's draft to re-voice."""
|
||||
return messages + [
|
||||
{"role": "assistant", "content": draft},
|
||||
{"role": "system", "content": _VOICE_NOTE},
|
||||
]
|
||||
|
||||
|
||||
def assemble(session_id: str, user_msg: str, backend: Backend,
|
||||
model: str | None = None) -> TurnContext:
|
||||
"""Run the parts over a fresh TurnContext and return it ready for `chat` to speak."""
|
||||
|
||||
Reference in New Issue
Block a user