DEV Community

seller-mind
seller-mind

Posted on

How I Built a Lightweight Edge Pipeline for Podcast Metadata

When I first started recording my own technical podcast, I thought the hard part was the recording itself. I was wrong. The real grind started the moment I hit "stop" on the audio interface.

I'd listen back to 45 minutes of rambling, trying to extract a catchy title, write a coherent summary, and pull out timestamps. It was exhausting. The cognitive switch from "creative host" to "analytical marketer" completely drained my energy. I found myself dreading the post-production admin more than the actual recording.

So I decided to build something.

The Post-Recording Hangover

The post-recording hangover is a real problem for indie creators. If you don't write good show notes or a compelling title, your episode gets buried in directories. You might spend five hours recording and editing, just for the episode to sit unnoticed because the metadata didn't hook anyone.

I looked at existing AI tools to help with this, but they were either generic text spinners that hallucinated timestamps, or massive enterprise suites priced for media companies, not solo developers. I needed a focused, fast tool that actually understood podcast structure without requiring a PhD in prompt engineering to get decent results.

The Pipeline, Not the Model

The first version I built was one function: take the transcript, send one enormous prompt, get everything back. It was bad. The model would write a fine summary, then invent three guests, then hallucinate timestamps that pointed at nothing. When the output is wrong in one section, you throw away the whole thing and pay for the tokens twice.

So I rebuilt it as a pipeline of small, single-purpose steps. Each step has one job, one output shape, and its own failure mode:

  1. Transcribe — audio in, plain text plus segment timings out.
  2. Summarize — transcript in, a 2-3 sentence summary and 5-7 takeaways out.
  3. Extract entities — transcript in, people/books/tools actually mentioned out, as a list with no prose.
  4. Name — summary plus transcript in, title candidates out.
  5. Describe — everything above in, a description with keywords folded in naturally.

Splitting it this way made every step testable in isolation, which is the whole reason it eventually became reliable. If titles are bad, I look at step 4 and nothing else.

Where the Edge Actually Helps

I chose Cloudflare Workers for the backend, and I want to be precise about why, because "edge AI" gets used loosely.

Worker isolates do not run a model. There is no GPU in the edge. What runs at the edge is the pipeline logic: validating the transcript, chunking it, assembling prompts, calling the inference endpoint, parsing and repairing the response, and writing results to KV. The model itself is a normal remote API call.

The edge is a good home for that logic for three reasons:

  • No cold-start penalty on the request path. A Worker starts in milliseconds, which matters when the user is waiting on a chain of five sequential model calls.
  • The orchestration is close to the user, so the network hops around the model call are small even when the model call itself is slow.
  • No server to keep alive, so a 20,000-word transcript does not occupy a process for the length of the job.

Here is one step of that pipeline, with the model name kept in config so a provider swap is a variable change rather than a redeploy:

// One step of the pipeline: transcript -> structured summary.
// The Worker is orchestration; inference happens at a remote endpoint.
async function summarize(transcript, env) {
  const baseUrl = env.DEEPSEEK_API_BASE_URL || 'https://api.deepseek.com';
  const model = env.AI_MODEL || 'deepseek-chat';

  const res = await fetch(`${baseUrl}/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${env.DEEPSEEK_API_KEY}`,
    },
    body: JSON.stringify({
      model,
      messages: [
        {
          role: 'system',
          content: `Return ONLY this markdown structure. No preamble.
## Summary
2-3 sentences.
## Takeaways
5-7 bullets, each under 20 words.
Use only facts present in the transcript. If something is unclear, omit it.`,
        },
        { role: 'user', content: transcript },
      ],
      max_tokens: 900,
      temperature: 0.2,
    }),
  });

  if (!res.ok) throw new Error(`summarize failed: ${res.status}`);
  return (await res.json()).choices[0].message.content;
}
Enter fullscreen mode Exit fullscreen mode

Two details in that prompt do most of the work. Return ONLY this markdown structure. No preamble. kills the "Sure! Here are your show notes:" opener that otherwise ends up in the final description. And If something is unclear, omit it is the cheapest hallucination guard I have found — far more effective than telling the model not to hallucinate, which it will happily ignore.

Transcription is deliberately a separate pipeline with a provider chain, because it has a completely different cost profile from text generation and is the step most likely to fail for reasons outside my control. The Worker tries Groq's whisper-large-v3 first, falls back to OpenAI's whisper-1, and keeps AssemblyAI available as a third path. Whichever key is configured gets used, so one provider's outage degrades latency instead of killing the job.

Practical Results and Lessons Learned

Breaking the pipeline into smaller, constrained prompts worked far better than one giant prompt. The edge deployment made the UI feel snappy, which matters a lot when you are iterating on metadata and re-running a step five times in a row.

What surprised me most was that the single biggest quality jump came not from a better model but from removing sections. Every field I asked for in the same call made the other fields worse. One call, one job.

If I were to rebuild this from scratch today, I would fix timestamp estimation first. Audio pacing varies wildly between hosts, and asking a model to guess timestamps purely from text flow is inherently flawed — it produces confident, plausible, wrong numbers. I would detect silence and topic boundaries with a lightweight audio analysis step, then hand real segment boundaries to the model instead of asking it to imagine them.

I also wrote up the naming side separately, because that is where the model's urge to write clickbait was hardest to suppress: how to get usable podcast titles out of a constrained prompt.


Full disclosure: I'm the developer of PodCrisp. I built this because I needed it myself.

Top comments (0)