Merge pull request #229 from calesthio/codex/expressive-tts-governance
[codex] Add expressive TTS governance
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user