LangChain TTS tool using Audra HTTP — Audra
Once an agent can write the script, the next question is always 'can it voice it?' Yes — wrap Audra as a tool. This is the exact @tool definition, focused on the two failure modes that make LLM-driven TTS flaky: unbounded input and untrusted wait times.
The tool definition
from langchain_core.tools import tool
import requests, os, hashlib
AUDRA_KEY = os.environ["AUDRA_API_KEY"]
@tool
def narrate(text: str, filename: str) -> str:
"""Render narration audio from a script and save it as an MP3 file.
Args:
text: The script to narrate. Keep under 8000 characters per call;
split longer scripts and call once per section.
filename: Output file name ending in .mp3 (no slashes).
"""
if len(text) > 9000:
return "ERROR: text too long — split into sections under 8000 chars"
if "/" in filename or not filename.endswith(".mp3"):
return "ERROR: filename must be a bare .mp3 name"
r = requests.post(
"https://audratalks.com/v2/speech",
headers={
"Authorization": f"Bearer {AUDRA_KEY}",
"Idempotency-Key": hashlib.sha256(text.encode()).hexdigest(),
},
json={"model": "audra-essential", "voice": "june",
"text": text, "format": "mp3"},
timeout=3600,
)
if r.status_code == 429:
return f"ERROR: rate limited, retry after {r.headers['Retry-After']}s"
r.raise_for_status()
open(filename, "wb").write(r.content)
return f"saved {filename} ({len(text)} chars rendered)"
Design decisions that matter for agents
- The 8,000-character guardrail in the docstring is the load-bearing part: agents will happily pass a whole article. Below ~8k the sync endpoint returns audio directly; above it, requests are auto-queued as background jobs — surprising for an agent that expects a file. Keep calls short and let the agent chunk, or point the tool at POST /v2/speech/jobs (500k chars, webhook) for chapter-scale work.
- Deterministic Idempotency-Key from the script hash: agents retry aggressively. A hash-derived key means 'render this exact script twice' bills once and returns the same audio — it also makes the tool safe to call after an in-between crash.
- Returning error strings instead of raising: a raised exception ends the agent's turn; a string the model can read lets it self-correct (split the text, fix the filename, wait and retry).
- timeout=3600: a ~500-char render takes 1–3 minutes. The requests default (none) is fine, but any outer framework timeout — LangGraph recursion, your serverless limit — must exceed render time, or the tool will be killed mid-render.
Wiring it in
Bind it like any other tool: tools=[narrate] in create_react_agent, or as a node in a LangGraph graph that follows your script-writing node. Rate limits apply per API key, not per agent: 60 RPM free, 120 RPM paid — if you fan out parallel narrate calls in a Graph with branches, add a semaphore. Model choice and voice slugs are documented at /docs and /voices.