From 71f2c85aa5ea2aa5fe5f31537f5979459e82fcdd Mon Sep 17 00:00:00 2001 From: historia Date: Wed, 9 Sep 2026 15:55:06 -0400 Subject: feat: instructions options translated automatically by --instructions --- app/converter/clients/audiocpp.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) (limited to 'app/converter/clients/audiocpp.py') diff --git a/app/converter/clients/audiocpp.py b/app/converter/clients/audiocpp.py index 98d64ba..578a13b 100644 --- a/app/converter/clients/audiocpp.py +++ b/app/converter/clients/audiocpp.py @@ -58,6 +58,14 @@ AUDIOCPP_VOICE_SPEAKER = "speaker" # built-in speaker name (Qwen CustomVoice) AUDIOCPP_VOICE_CLONE = "clone" # server-side preset / voice_dir (Base, others) AUDIOCPP_VOICE_DESIGN = "design" # voice described by --instructions (vdes) +# How a family's speech endpoint consumes --instructions: the OpenAI-style +# top-level "instructions" field (the default, read by most design/style +# implementations), or the "instruction" request option inside the +# "options" object (BreezeTTS 2: --request-option instruction=... and the +# endpoint example send it there; its loader ignores the top-level field). +AUDIOCPP_INSTRUCTION_FIELD = "field" +AUDIOCPP_INSTRUCTION_OPTION = "option" + # HTTP error body fragments identifying deterministic request-configuration # problems: the identical request will fail on every retry, so the chunk # loop must give up immediately instead of burning its attempt budget. @@ -531,13 +539,19 @@ class AudioCppFamilyProfile: """ def __init__(self, language_style: str = AUDIOCPP_LANG_OMIT, - script_prefix: Optional[str] = None): + script_prefix: Optional[str] = None, + instruction_channel: str = AUDIOCPP_INSTRUCTION_FIELD): self.language_style = language_style # SCRIPT_PREFIX, when set, formats every request's text as one # ": text" script line (audiocpp_script_input): the # family's server implementation parses the prompt as a # speaker-script and silently drops unprefixed lines (VibeVoice). self.script_prefix = script_prefix + # INSTRUCTION_CHANNEL picks where --instructions go: the OpenAI + # "instructions" field (AUDIOCPP_INSTRUCTION_FIELD) or the + # "instruction" request option (AUDIOCPP_INSTRUCTION_OPTION, + # BreezeTTS 2 — see its profile below). + self.instruction_channel = instruction_channel # Generic profile for families not listed in AUDIOCPP_FAMILY_PROFILES: @@ -563,6 +577,12 @@ AUDIOCPP_FAMILY_PROFILES = { # client flattens each request into one Speaker-1 line (the server # renormalizes the lowest speaker id to zero — the cloned reference). "vibevoice": AudioCppFamilyProfile(script_prefix="Speaker 1"), + # BreezeTTS 2 (text design/clone/direction) reads the instruction as a + # request option, not the OpenAI "instructions" field: upstream's own + # endpoint example sends {"options": {"instruction": "..."}}. The + # model detects the language (zh/en) itself, so no language field. + "breeze_tts": AudioCppFamilyProfile( + instruction_channel=AUDIOCPP_INSTRUCTION_OPTION), } @@ -1191,14 +1211,23 @@ class AudioCppTTSClient(BaseTTSClient): # audio.cpp has no negative "randomize" seed; a negative seed # means "let the server randomize", so the field is omitted. payload["seed"] = self._seed - if self.instructions: - # Explicit voice-design or style instruction (required for task - # "vdes" entries; a Ctrl/style control on families that read it). - payload["instructions"] = self.instructions if self.request_options: # Generic per-model controls (--option KEY=VALUE): forwarded # verbatim; the model ignores keys it does not know. payload["options"] = dict(self.request_options) + if self.instructions: + # Explicit voice-design or style instruction (required for task + # "vdes" entries; a voice/style control on families that read + # it). Families on the option channel take it as a request + # option; an explicit --option instruction=... from the caller + # lands here too and is never overwritten. + if self.profile.instruction_channel == AUDIOCPP_INSTRUCTION_OPTION: + if not (self.request_options or {}).get("instruction", ""): + options = dict(self.request_options) + options["instruction"] = self.instructions + payload["options"] = options + else: + payload["instructions"] = self.instructions request = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST") -- cgit v1.2.3