aboutsummaryrefslogtreecommitdiff
path: root/app/converter/clients/audiocpp.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-09 15:55:06 -0400
committerhistoria <historiavg@proton.me>2026-09-09 15:55:06 -0400
commit71f2c85aa5ea2aa5fe5f31537f5979459e82fcdd (patch)
tree2bdb01e2d2e4d3bc334c9014cff4cffbb0bcda08 /app/converter/clients/audiocpp.py
parent0157ce4a347f9625e1e9d09e2bbf0fbfad722557 (diff)
downloadtts-audiobook-generator-71f2c85aa5ea2aa5fe5f31537f5979459e82fcdd.tar.gz
feat: instructions options translated automatically by --instructions
Diffstat (limited to 'app/converter/clients/audiocpp.py')
-rw-r--r--app/converter/clients/audiocpp.py39
1 files changed, 34 insertions, 5 deletions
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
# "<prefix>: 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")