Podcast post-production is rarely one operation. Before an episode can be published, someone must transcribe the recording, identify chapters, write show notes, create promotional copy, and produce a trailer. Each step depends on the one before it, which makes the workflow vulnerable to interruptions.
If processing fails after transcription, should the application transcribe the entire recording again?
I built Show Wrap Desk to explore a more durable approach.
It is a TypeScript application running on Telnyx Edge Compute that turns one podcast recording into:
The workflow is owned by one Stateful Actor per show, so completed stages remain recorded when later work fails.
A producer starts the workflow with one request:
curl -X POST https://<your-worker>/wrap \
-H "Content-Type: application/json" \
-d '{
"show": "engineering-notes",
"audioUrl": "https://example.com/episode.mp3"
}'
The endpoint immediately returns an episode ID and a 202 Accepted response while the actor continues processing:
transcribing → chapterizing → notes → wrapped
You can inspect progress at any time:
curl "https://<your-worker>/wrapstatus/<episodeId>?show=engineering-notes"
The worker derives a deterministic actor name from the show:
const desk = env.DESKS.idFromName(`show:${show}`);
const result = await desk.onWrap(show, audioUrl);
Every episode submitted for the same show reaches the same WrapDesk actor.
The actor stores its workflow data in embedded SQL tables:
episodes tracks status, errors, and completion times.chapters stores titles and timestamps.assets stores transcripts, show notes, and trailer audio.
This gives the workflow a durable ledger without requiring a separate orchestration database.
Each episode is processed by a scheduled task with a stable ID:
await this.schedule(
0,
"wrapTick",
{ episodeId },
{ id: `wrap:${episodeId}` }
);
Scheduling the same ID again replaces the pending task instead of creating another copy.
After each successful stage, the actor updates the episode status and re-arms that task for the next stage. If the same audio URL is submitted again while processing is incomplete, the actor finds the existing episode and resumes it.
The individual stages also check for persisted output before doing work. A stored transcript is not transcribed again, existing chapters are preserved, and generated notes or trailer audio are reused.
A failure can repeat the currently executing external operation if it occurred before the result was saved. However, completed and persisted stages are not restarted.
The first stage downloads the recording and submits it to the Telnyx Speech-to-Text API using Deepgram Nova-3:
const form = new FormData();
form.append("file", blob, "episode.mp3");
form.append("model", "deepgram/nova-3");
form.append("response_format", "verbose_json");
form.append(
"model_config",
JSON.stringify({
diarize: true,
smart_format: true,
punctuate: true,
})
);
The response includes word-level timestamps and speaker labels. The actor normalizes those words and stores them as the transcript asset before advancing the episode.
The application groups consecutive words from the same speaker into turns. A silence longer than 60 seconds begins a new chapter:
const turns = buildTurns(words);
const chapters = splitIntoChapters(turns, 60);
Boundary detection happens locally, so the application does not need an AI request to determine every timestamp.
Telnyx AI Inference then receives short summaries of the resulting sections and returns chapter titles. The sample uses zai-org/GLM-5.3-Flash through the Edge Compute TELNYX binding.
The next inference request creates Markdown show notes containing:
The transcript sent to the model is bounded to keep the example inside a manageable context window. A production implementation should introduce chunking or hierarchical summarization for long recordings.
The model selects a promising chapter and writes a trailer script capped at 400 characters.
That script is sent to the Telnyx Text-to-Speech WebSocket:
socket.send(JSON.stringify({ text: " " }));
socket.send(JSON.stringify({ text: script }));
socket.send(JSON.stringify({ text: "" }));
The actor collects base64 audio frames until the service sends:
{ "isFinal": true }
The sample defaults to azure.en-US-AvaMultilingualNeural, but HOST_VOICE can be changed to another supported voice.
The project does not automatically clone the podcast host’s voice from the submitted recording. A custom or cloned voice must be configured separately.
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/show-wrap-desk
npm install
cp .env.example .env
Add your Telnyx API key, then generate the bindings and deploy:
npm run types
npm run deploy
Set the generated worker URL as DEPLOY_URL and run:
npm run bootstrap
The included demo uses a public-domain LibriVox recording.
Before using this architecture in production, add authentication, audio-size limits, URL validation, retention controls, workflow telemetry, and human review for generated promotional content.
Large transcripts and audio files should also move to durable object storage instead of being kept directly in actor SQL.
The reusable idea is bigger than podcasting:
That pattern can support transcription batches, media processing, document enrichment, report generation, and other long-running AI workflows.
The full source is available here:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/show-wrap-desk