Merge pull request #229 from calesthio/codex/expressive-tts-governance

[codex] Add expressive TTS governance
This commit is contained in:
Calesthio
2026-06-29 23:04:56 -07:00
committed by GitHub
18 changed files with 489 additions and 24 deletions
+29 -6
View File
@@ -96,6 +96,16 @@ class ElevenLabsTTS(BaseTool):
"minimum": 0,
"maximum": 1,
},
"speed": {
"type": "number",
"default": 1.0,
"minimum": 0.7,
"maximum": 1.2,
},
"use_speaker_boost": {
"type": "boolean",
"default": True,
},
"output_path": {"type": "string"},
"output_format": {
"type": "string",
@@ -109,7 +119,16 @@ class ElevenLabsTTS(BaseTool):
cpu_cores=1, ram_mb=256, vram_mb=0, disk_mb=50, network_required=True
)
retry_policy = RetryPolicy(max_retries=2, retryable_errors=["rate_limit", "timeout"])
idempotency_key_fields = ["text", "voice_id", "model_id"]
idempotency_key_fields = [
"text",
"voice_id",
"model_id",
"stability",
"similarity_boost",
"style",
"speed",
"use_speaker_boost",
]
side_effects = ["writes audio file to output_path", "calls ElevenLabs API"]
user_visible_verification = ["Listen to generated audio for natural speech quality"]
@@ -145,6 +164,13 @@ class ElevenLabsTTS(BaseTool):
voice_id = inputs.get("voice_id", self.DEFAULT_VOICE_ID)
model_id = inputs.get("model_id", "eleven_multilingual_v2")
output_format = inputs.get("output_format", "mp3_44100_128")
voice_settings = {
"stability": inputs.get("stability", 0.5),
"similarity_boost": inputs.get("similarity_boost", 0.75),
"style": inputs.get("style", 0.0),
"speed": inputs.get("speed", 1.0),
"use_speaker_boost": inputs.get("use_speaker_boost", True),
}
response = requests.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
@@ -156,11 +182,7 @@ class ElevenLabsTTS(BaseTool):
json={
"text": text,
"model_id": model_id,
"voice_settings": {
"stability": inputs.get("stability", 0.5),
"similarity_boost": inputs.get("similarity_boost", 0.75),
"style": inputs.get("style", 0.0),
},
"voice_settings": voice_settings,
},
params={"output_format": output_format},
timeout=120,
@@ -178,6 +200,7 @@ class ElevenLabsTTS(BaseTool):
"provider": self.provider,
"model": model_id,
"voice_id": voice_id,
"voice_settings": voice_settings,
"text_length": len(text),
"output": str(output_path),
"format": output_format,
+29 -3
View File
@@ -78,6 +78,12 @@ class GoogleTTS(BaseTool):
"required": ["text"],
"properties": {
"text": {"type": "string", "description": "Text to convert to speech"},
"input_type": {
"type": "string",
"default": "text",
"enum": ["text", "ssml"],
"description": "Set to 'ssml' when text contains SSML tags such as <speak> or <break>.",
},
"voice": {
"type": "string",
"default": "en-US-Chirp3-HD-Orus",
@@ -92,7 +98,7 @@ class GoogleTTS(BaseTool):
"type": "number",
"default": 1.0,
"minimum": 0.25,
"maximum": 4.0,
"maximum": 2.0,
"description": "Speaking speed. 1.0 = normal, 0.5 = half speed, 2.0 = double speed",
},
"pitch": {
@@ -116,7 +122,7 @@ class GoogleTTS(BaseTool):
cpu_cores=1, ram_mb=256, vram_mb=0, disk_mb=50, network_required=True
)
retry_policy = RetryPolicy(max_retries=2, retryable_errors=["rate_limit", "timeout"])
idempotency_key_fields = ["text", "voice", "language_code", "speaking_rate", "pitch"]
idempotency_key_fields = ["text", "input_type", "voice", "language_code", "speaking_rate", "pitch"]
side_effects = ["writes audio file to output_path", "calls Google Cloud TTS API"]
user_visible_verification = ["Listen to generated audio for natural speech quality"]
@@ -200,14 +206,33 @@ class GoogleTTS(BaseTool):
import requests
text = inputs["text"]
input_type = inputs.get("input_type", "text")
voice_name = inputs.get("voice", "en-US-Chirp3-HD-Orus")
language_code = inputs.get("language_code", "en-US")
speaking_rate = inputs.get("speaking_rate", 1.0)
pitch = inputs.get("pitch", 0.0)
audio_encoding = inputs.get("audio_encoding", "MP3")
if not 0.25 <= speaking_rate <= 2.0:
return ToolResult(
success=False,
error="Google TTS speaking_rate must be between 0.25 and 2.0.",
)
if not -20.0 <= pitch <= 20.0:
return ToolResult(
success=False,
error="Google TTS pitch must be between -20.0 and 20.0 semitones.",
)
if input_type == "ssml":
stripped = text.strip()
ssml = stripped if stripped.startswith("<speak") else f"<speak>{stripped}</speak>"
synthesis_input = {"ssml": ssml}
else:
synthesis_input = {"text": text}
payload = {
"input": {"text": text},
"input": synthesis_input,
"voice": {
"languageCode": language_code,
"name": voice_name,
@@ -253,6 +278,7 @@ class GoogleTTS(BaseTool):
"voice": voice_name,
"language_code": language_code,
"text_length": len(text),
"input_type": input_type,
"output": str(output_path),
"format": audio_encoding,
"speaking_rate": speaking_rate,
+34 -4
View File
@@ -80,10 +80,24 @@ class OpenAITTS(BaseTool):
"type": "string",
"default": "mp3",
"enum": ["mp3", "wav", "pcm"],
"description": "Backward-compatible alias for response_format.",
},
"response_format": {
"type": "string",
"default": "mp3",
"enum": ["mp3", "opus", "aac", "flac", "wav", "pcm"],
"description": "OpenAI speech response_format.",
},
"instructions": {
"type": "string",
"description": "Optional delivery instructions for the voice",
"description": "Optional delivery instructions. Supported by gpt-4o-mini-tts.",
},
"speed": {
"type": "number",
"default": 1.0,
"minimum": 0.25,
"maximum": 4.0,
"description": "OpenAI speech speed multiplier.",
},
"output_path": {"type": "string"},
},
@@ -93,7 +107,7 @@ class OpenAITTS(BaseTool):
cpu_cores=1, ram_mb=256, vram_mb=0, disk_mb=50, network_required=True
)
retry_policy = RetryPolicy(max_retries=2, retryable_errors=["rate_limit", "timeout"])
idempotency_key_fields = ["text", "voice", "model", "format"]
idempotency_key_fields = ["text", "voice", "model", "format", "response_format", "instructions", "speed"]
side_effects = ["writes audio file to output_path", "calls OpenAI API"]
user_visible_verification = ["Listen to generated audio for intelligibility and tone"]
@@ -105,6 +119,10 @@ class OpenAITTS(BaseTool):
def estimate_cost(self, inputs: dict[str, Any]) -> float:
return round(len(inputs.get("text", "")) * 0.000015, 4)
@staticmethod
def _supports_instructions(model: str) -> bool:
return model.startswith("gpt-4o-mini-tts")
def execute(self, inputs: dict[str, Any]) -> ToolResult:
if not os.environ.get("OPENAI_API_KEY"):
return ToolResult(success=False, error="No OpenAI API key. " + self.install_instructions)
@@ -124,11 +142,20 @@ class OpenAITTS(BaseTool):
from tools.analysis.audio_probe import probe_duration
client = OpenAI()
text = inputs["text"]
model = inputs.get("model", "gpt-4o-mini-tts")
voice = inputs.get("voice", "alloy")
fmt = inputs.get("format", "mp3")
fmt = inputs.get("response_format") or inputs.get("format", "mp3")
if inputs.get("instructions") and not self._supports_instructions(model):
return ToolResult(
success=False,
error=(
"OpenAI TTS instructions are only supported by "
"gpt-4o-mini-tts. Use that model or omit instructions."
),
)
client = OpenAI()
output_path = Path(inputs.get("output_path", f"openai_tts.{fmt}"))
output_path.parent.mkdir(parents=True, exist_ok=True)
@@ -155,6 +182,9 @@ class OpenAITTS(BaseTool):
"model": model,
"voice": voice,
"format": fmt,
"response_format": fmt,
"instructions": inputs.get("instructions"),
"speed": inputs.get("speed", 1.0),
"text_length": len(text),
"audio_duration_seconds": round(audio_duration, 2) if audio_duration else None,
"output": str(output_path),
+37
View File
@@ -61,6 +61,43 @@ class TTSSelector(BaseTool):
"type": "number", "minimum": 0, "maximum": 1,
"description": "Style exaggeration (ElevenLabs). Higher = more expressive.",
},
"instructions": {
"type": "string",
"description": "Provider-level delivery instructions for expressive narration when supported.",
},
"speaking_rate": {
"type": "number",
"minimum": 0.25,
"maximum": 2.0,
"description": "Google-style speakingRate control. Use speed for OpenAI/ElevenLabs-style controls.",
},
"speed": {
"type": "number",
"minimum": 0.25,
"maximum": 4.0,
"description": "Alias for speaking speed used by some providers.",
},
"pitch": {
"type": "number",
"minimum": -50,
"maximum": 50,
"description": "Provider-specific pitch control. Google TTS accepts -20..20; HeyGen-style providers may accept wider ranges.",
},
"input_type": {
"type": "string",
"enum": ["text", "ssml"],
"default": "text",
"description": "Use 'ssml' only when the selected provider supports tags such as <break>.",
},
"voice_performance": {
"type": "object",
"description": "Structured voice-performance plan or section delivery cues from the script artifact.",
},
"sample_mode": {
"type": "boolean",
"default": False,
"description": "True when generating an approval sample before batch narration.",
},
"output_format": {
"type": "string",
"description": "Audio output format (e.g. mp3_44100_128). Passed through to provider.",