Same narrator every week: pin the voice, language and speed
Keep one narrator across a weekly series on Sume TTS: pin avatar_handle with voice.id, language, speed and format in one config, with one key per episode.
To keep one narrator across a weekly series, put the voice, language, speed and output format in one config object, merge it into every request, and give each episode its own Idempotency-Key. Send both avatar_handle and voice.id: the schema says they must match or the request fails with 400, so a changed avatar voice stops the run instead of changing the narrator quietly. Sume documents which settings were used, not that two jobs sound identical, so the sameness check is your ear.
The request fields and the job result shape come from the Sume API reference; the key rule from Generation admission, read on 2026-10-03, with polling from Jobs and results. Reading settings back from a finished job is in One narrator for 8 episodes; this page is about the config, the keys, and avatar handle versus voice id.
Should I pin avatar_handle or voice.id?
The schema says that when an avatar is provided, Sume resolves its TTS voice at submit time. A handle alone is therefore looked up each week, so if the avatar's voice is ever changed, later episodes would follow it. A voice.id stays what you saved. Send both and the 400 on a mismatch turns that drift into a loud error. Run episode one with the handle only, read voice.id from its result, and save that as the pin.
What goes in the config?
Every field you leave out is a field the service chooses for you, and a later change to a default would reach your series unseen.
| Field | Pin it as | Why |
|---|---|---|
| avatar_handle and voice.id | Both, matching | Mismatch returns 400 |
| language | en or your code | Omitted means English, with ko or ja inferred |
| generation_config.speed | 0.6 to 1.5 number | The enum speed field is deprecated |
| generation_config.volume | 0.5 to 2.0 number | Level between episodes |
| output_format | mp3 44100 Hz 128000 bit/s | The documented default, stated explicitly |
How do I key each episode?
Build the key from the episode, such as weekly-2026-w41. A retry with the same key and the same body returns the original job instead of billing again. A different body under the same key is a 409 idempotency_conflict, so a changed script needs a new key, such as weekly-2026-w41-v2. The script below submits an episode, polls it, and compares what the result reports with the config.
import os, time, requests
API, H = "https://api.sume.com", {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
SHOW = {"avatar_handle": "weekly_host", "voice": {"id": os.environ["SHOW_VOICE_ID"]},
"language": "en", "generation_config": {"speed": 1.0, "volume": 1.0},
"output_format": {"container": "mp3", "sample_rate": 44100, "bit_rate": 128000}}
def episode(week, script):
r = requests.post(f"{API}/v1/tts-1.0/generate", json={**SHOW, "transcript": script},
headers={**H, "Idempotency-Key": f"weekly-{week}"})
r.raise_for_status()
d = r.json()["data"]
while not requests.get(d["status_url"], headers=H).json()["data"]["terminal"]:
time.sleep(2)
res = requests.get(d["result_url"], headers=H).json()["data"]["result"]
print("voice ok:", res["voice"]["id"] == SHOW["voice"]["id"],
"| settings ok:", res.get("generation_config") == SHOW["generation_config"])
return res["audio_url"]
print(episode("2026-w41", "Welcome back. Here is this week's update."))What do I compare by ear?
Re-render the same test sentence before each season and listen to it beside the last episode of the previous one.
- Pace and pauses on the same sentence.
- Pitch and warmth of the voice.
- How names and numbers are read.
- Level: see matching loudness between takes.
Sources
Related posts
More in Developers
- Scalar API Reference for the Sume OpenAPI JSON, with a Try It key
Scalar renders an OpenAPI document as an interactive reference with a test client. Point it at the Sume spec, and keep the Bearer key out of the page source.
- A Sume scheduled run is not in /v1/jobs: where to read it back
A scheduled Sume run never shows up in /v1/jobs. Read it from /v1/action-runs instead; the table maps start, read, status and overlap rules to each side.
- Sume SDK wait timeouts: 20 min, 10 min, and the 90-minute run
subscribeFormatRun waits 20 minutes, waitForRun 10, waitForJob 20, yet a run lives up to 90. Which clock fires first and how to resume after a timeout.
- waitForJob resolves for failed jobs: read job.status, not catch (TS)
In the Sume SDK on main, waitForJob returns the job for completed, failed and canceled alike. Branch on job.status and job.error, and keep catch for timeouts.
Written by Sume