<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bill King</title>
    <description>The latest articles on DEV Community by Bill King (@bill_king_d4cd78085ee37d2).</description>
    <link>https://dev.to/bill_king_d4cd78085ee37d2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096597%2Ff2c6c452-afab-4df0-85fb-ed7c327c6867.jpg</url>
      <title>DEV Community: Bill King</title>
      <link>https://dev.to/bill_king_d4cd78085ee37d2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bill_king_d4cd78085ee37d2"/>
    <language>en</language>
    <item>
      <title>Transparent video exports: why one MP4 URL isn't enough</title>
      <dc:creator>Bill King</dc:creator>
      <pubDate>Tue, 08 Sep 2026 05:35:06 +0000</pubDate>
      <link>https://dev.to/bill_king_d4cd78085ee37d2/transparent-video-exports-why-one-mp4-url-isnt-enough-3kko</link>
      <guid>https://dev.to/bill_king_d4cd78085ee37d2/transparent-video-exports-why-one-mp4-url-isnt-enough-3kko</guid>
      <description>&lt;p&gt;A background-removal API can finish successfully while the exported file is still wrong for the next step. A video that plays on a black background might have lost its transparency—or the player might simply be displaying it against black. Those are different problems.&lt;/p&gt;

&lt;p&gt;I work on &lt;a href="https://unbg.video/" rel="noopener noreferrer"&gt;unbg.video&lt;/a&gt;, a video background remover and editor. The project has export paths for WebM, ProRes 4444 MOV, and separate H.264 color and alpha files. Here is a practical way to reason about those outputs, with a small FFmpeg experiment you can run without an AI service.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with what the recipient needs
&lt;/h2&gt;

&lt;p&gt;“Download video” hides several different jobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;th&gt;Output to consider&lt;/th&gt;
&lt;th&gt;What to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Display transparent footage on a website&lt;/td&gt;
&lt;td&gt;WebM with alpha&lt;/td&gt;
&lt;td&gt;Actual transparency in the target browser and device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue compositing in an editor&lt;/td&gt;
&lt;td&gt;ProRes 4444 MOV with alpha&lt;/td&gt;
&lt;td&gt;Import behavior and alpha interpretation in that editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer color and a separate matte&lt;/td&gt;
&lt;td&gt;H.264 color + grayscale alpha files&lt;/td&gt;
&lt;td&gt;Matching dimensions, timestamps, duration, and mask convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish a finished scene with its new background&lt;/td&gt;
&lt;td&gt;Flattened H.264 MP4&lt;/td&gt;
&lt;td&gt;The final composition; transparency is no longer needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The extension alone does not describe the whole contract. A MOV file does not automatically contain alpha. An ordinary H.264 MP4 export is not a portable RGBA deliverable. In the two-file workflow, the second MP4 stores a grayscale matte as picture data; it is not an embedded alpha channel in the color file.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A matte is data, not a black background
&lt;/h2&gt;

&lt;p&gt;For straight alpha, the basic compositing relationship is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output = alpha * foreground + (1 - alpha) * background
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here alpha is normalized from 0 to 1: zero reveals the background, one keeps the foreground, and intermediate values produce partial coverage. For physically meaningful blending, color-space handling also matters; this equation is a conceptual starting point.&lt;/p&gt;

&lt;p&gt;A useful consequence: you cannot safely recover transparency by deleting every black pixel. Black clothing and shadows are legitimate foreground content. Keep the matte.&lt;/p&gt;

&lt;p&gt;Also establish whether color is straight or premultiplied. With premultiplied color, RGB already includes multiplication by alpha. Multiplying again can darken the edges. If the silhouette looks right but has a dark fringe, inspect this convention before blaming the segmentation model.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Reproduce the export step without a model
&lt;/h2&gt;

&lt;p&gt;This synthetic example creates one second of red video and a horizontal grayscale ramp, combines them, and writes ProRes 4444. It needs an FFmpeg build with &lt;code&gt;prores_ks&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'color=c=red:s=64x64:r=10:d=1,format=rgb24'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'nullsrc=s=64x64:r=10:d=1,format=gray,geq=lum=255*X/W'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s1"&gt;'[0:v][1:v]alphamerge,format=yuva444p10le[v]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s1"&gt;'[v]'&lt;/span&gt; &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v prores_ks &lt;span class="nt"&gt;-profile&lt;/span&gt;:v 4 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuva444p10le &lt;span class="se"&gt;\&lt;/span&gt;
  foreground.mov
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extract a frame of the stored alpha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; foreground.mov &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s1"&gt;'alphaextract,format=gray'&lt;/span&gt; &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 &lt;span class="nt"&gt;-update&lt;/span&gt; 1 mask.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected mask is a horizontal dark-to-light ramp, not a solid white frame. The red image should become progressively more opaque when composited over a background.&lt;/p&gt;

&lt;p&gt;This example was run locally with FFmpeg 8.0.1 for this article: the output decoded as ProRes 4444 with an alpha-capable pixel format, and the extracted 8-bit alpha ramp ranged from 0 to 251 (the last column is 63/64 of full scale). It is an export smoke test, not evidence of browser compatibility or segmentation quality. Audio is deliberately omitted.&lt;/p&gt;

&lt;p&gt;FFmpeg documents &lt;a href="https://ffmpeg.org/ffmpeg-filters.html#alphamerge" rel="noopener noreferrer"&gt;alpha extraction and merging&lt;/a&gt; and the &lt;a href="https://www.ffmpeg.org/ffmpeg-codecs.html#ProRes" rel="noopener noreferrer"&gt;ProRes encoder options&lt;/a&gt;. When replacing the synthetic inputs with real color and matte files, first align their dimensions and timeline. Resetting a timestamp does not repair missing or mismatched frames.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Represent the result as a bundle
&lt;/h2&gt;

&lt;p&gt;The project stores multiple video assets with color/alpha variants and can package the H.264 pair into a ZIP. That matters: a single &lt;code&gt;videoUrl&lt;/code&gt; cannot tell a consumer whether it has a finished composition or only half of a compositing input.&lt;/p&gt;

&lt;p&gt;For a new API, a small discriminated union can make that distinction explicit. This is an illustrative design, not a copy of the project's implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VideoExport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prores4444&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;matte-pair&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;colorUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;alphaUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flattened&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only mark a pair ready when both assets are available. Give files explicit names such as &lt;code&gt;clip_color.mp4&lt;/code&gt; and &lt;code&gt;clip_alpha.mp4&lt;/code&gt;, and document which mask values mean opaque. Otherwise the ZIP merely moves the ambiguity to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test the handoff, not just the download
&lt;/h2&gt;

&lt;p&gt;My suggested acceptance checklist is short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composite the result over black, white, and a saturated background. Each reveals different edge problems.&lt;/li&gt;
&lt;li&gt;Scrub moving edges near the beginning and end. A still frame will not reveal matte drift.&lt;/li&gt;
&lt;li&gt;Open the exported artifact in the intended destination, not only the app's own preview.&lt;/li&gt;
&lt;li&gt;For paired assets, verify both files and their timing before declaring success.&lt;/li&gt;
&lt;li&gt;Keep a transparent export test separate from a flattened MP4 test. They promise different things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful product question is: &lt;strong&gt;Can the recipient use this file for their next step without guessing?&lt;/strong&gt; That question should shape the export format, the API response, and the download label.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Reliable AI Transcription Pipeline on Cloudflare Workers</title>
      <dc:creator>Bill King</dc:creator>
      <pubDate>Thu, 27 Aug 2026 05:26:21 +0000</pubDate>
      <link>https://dev.to/bill_king_d4cd78085ee37d2/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers-5hie</link>
      <guid>https://dev.to/bill_king_d4cd78085ee37d2/building-a-reliable-ai-transcription-pipeline-on-cloudflare-workers-5hie</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://hitranscript.com/" rel="noopener noreferrer"&gt;HiTranscript&lt;/a&gt;, a web app that turns public video URLs and local media uploads into searchable transcripts and subtitle files.&lt;/p&gt;

&lt;p&gt;The transcription model was not the hardest part.&lt;/p&gt;

&lt;p&gt;The hard part was building a pipeline that stays correct when uploads are large, requests are retried, callbacks arrive twice, a batch partially fails, or a deployment needs to be rolled back.&lt;/p&gt;

&lt;p&gt;This post covers the architecture patterns that made the system more reliable: explicit job states, durable media handoffs, idempotent callbacks, item-level batch tracking, and a single normalized timeline for every output format.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture at a glance
&lt;/h2&gt;

&lt;p&gt;The application uses TanStack Start and TypeScript for the web layer, PostgreSQL for durable task state, and Cloudflare Workers, Queues, and R2 for orchestration and media storage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
  |
  v
TanStack Start API
  |
  +--&amp;gt; PostgreSQL task record
  |
  +--&amp;gt; private R2 object
  |
  v
Cloudflare Queue
  |
  v
media preparation / transcription worker
  |
  v
signed callback
  |
  v
normalized word timeline
  |
  +--&amp;gt; readable transcript
  +--&amp;gt; subtitle cues
  +--&amp;gt; TXT / DOCX / PDF
  +--&amp;gt; SRT / WebVTT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision is that the HTTP request does not try to finish the transcription. It only validates the request, persists the intent, and creates a durable handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treat job states as a public contract
&lt;/h2&gt;

&lt;p&gt;A boolean such as isProcessing is not enough for a media pipeline.&lt;/p&gt;

&lt;p&gt;A real task can be waiting for media, preparing media, queued for transcription, actively processing, completed, or failed. Each state has different retry and UI behavior.&lt;/p&gt;

&lt;p&gt;A simplified state model looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TranscriptStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;awaiting_media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;media_preparing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of explicit states is not the union type itself. The value is being able to define valid transitions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;awaiting_media can move to media_preparing or failed&lt;/li&gt;
&lt;li&gt;media_preparing can move to queued or failed&lt;/li&gt;
&lt;li&gt;queued can move to processing or failed&lt;/li&gt;
&lt;li&gt;completed and failed are terminal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When every write checks the previous state, stale workers cannot move a finished task backward.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use durable handoffs for large media
&lt;/h2&gt;

&lt;p&gt;Passing large audio or video bodies through several HTTP requests creates unnecessary failure points.&lt;/p&gt;

&lt;p&gt;The safer pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a task record.&lt;/li&gt;
&lt;li&gt;Upload the media to private object storage.&lt;/li&gt;
&lt;li&gt;Put a small message on the queue containing the task ID and object key.&lt;/li&gt;
&lt;li&gt;Let the worker fetch the object when capacity is available.&lt;/li&gt;
&lt;li&gt;Delete or retain the object according to an explicit retention policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The queue message should describe work, not carry the work itself.&lt;/p&gt;

&lt;p&gt;This also keeps the web process responsive. The browser can display upload progress, while the backend independently reports preparation and transcription progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Design every callback for duplicate delivery
&lt;/h2&gt;

&lt;p&gt;Retries are normal in distributed systems. A callback may arrive twice because a worker timed out after completing the request, a queue retried the message, or the provider repeated a webhook.&lt;/p&gt;

&lt;p&gt;The callback handler therefore has to be idempotent.&lt;/p&gt;

&lt;p&gt;A simplified version of the rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;completeTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CompletionPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateTaskWhereStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;findTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;settleBillingOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database transition, result persistence, and billing settlement belong to one consistency boundary. A duplicate callback should return the existing result instead of charging twice or creating a second output.&lt;/p&gt;

&lt;p&gt;Signed callbacks are equally important. Idempotency prevents accidental duplication; signature verification prevents unauthorized state changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A batch is not just a bigger loop
&lt;/h2&gt;

&lt;p&gt;It is tempting to model a batch as one task containing an array of URLs. That becomes painful when one item fails and the other 49 succeed.&lt;/p&gt;

&lt;p&gt;A more useful model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one batch record for ownership and aggregate status&lt;/li&gt;
&lt;li&gt;one item record per source&lt;/li&gt;
&lt;li&gt;independent state and error information for every item&lt;/li&gt;
&lt;li&gt;an aggregate state derived from the items&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes partially_completed a first-class outcome rather than an exception.&lt;/p&gt;

&lt;p&gt;It also improves fairness. A scheduler can take a capacity snapshot and dispatch work across batches instead of letting one large batch block every single-item request.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Preserve word timing as primary data
&lt;/h2&gt;

&lt;p&gt;A transcript paragraph, an SRT file, and a short-form caption layout are different views of the same timing data.&lt;/p&gt;

&lt;p&gt;Instead of storing only a large text blob, the pipeline keeps a normalized word timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TimedWord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;startMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;endMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;speakerId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that timeline, the application can derive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;readable paragraphs&lt;/li&gt;
&lt;li&gt;speaker-aware sections&lt;/li&gt;
&lt;li&gt;standard subtitle cues&lt;/li&gt;
&lt;li&gt;shorter social caption cues&lt;/li&gt;
&lt;li&gt;search matches&lt;/li&gt;
&lt;li&gt;TXT, document, and timed subtitle exports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This avoids running transcription again when the user changes an output option. It also keeps every view aligned to the same source data.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Failure handling is part of the product
&lt;/h2&gt;

&lt;p&gt;The internal error may say that a decoder failed, a queue exhausted its retries, or an upstream service rejected a media file. That detail is useful in logs but often harmful in the UI.&lt;/p&gt;

&lt;p&gt;The public contract should expose stable, actionable categories such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unsupported source&lt;/li&gt;
&lt;li&gt;inaccessible or private media&lt;/li&gt;
&lt;li&gt;no audio track&lt;/li&gt;
&lt;li&gt;file too large&lt;/li&gt;
&lt;li&gt;transcription failed&lt;/li&gt;
&lt;li&gt;processing timed out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Internally, retain the detailed diagnostic code and attempt history. Externally, show a message the user can act on.&lt;/p&gt;

&lt;p&gt;This separation also lets you change providers without changing the product's error language.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Deploy the exact version you tested
&lt;/h2&gt;

&lt;p&gt;For this kind of pipeline, deployment safety matters as much as code correctness.&lt;/p&gt;

&lt;p&gt;My preferred release flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build and upload an immutable Worker version with zero production traffic.&lt;/li&gt;
&lt;li&gt;Validate the changed routes against that exact version.&lt;/li&gt;
&lt;li&gt;Record the source commit and Worker version ID together.&lt;/li&gt;
&lt;li&gt;Promote the same version without rebuilding.&lt;/li&gt;
&lt;li&gt;Run focused production smoke checks.&lt;/li&gt;
&lt;li&gt;Roll back to the previous Worker version if the code is unhealthy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rebuilding between validation and promotion breaks the evidence chain. The artifact that reaches users should be the artifact that was tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;The next reliability gains are less about adding more providers and more about strengthening the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider-neutral result validation&lt;/li&gt;
&lt;li&gt;better timestamp-density checks&lt;/li&gt;
&lt;li&gt;queue age and stuck-task observability&lt;/li&gt;
&lt;li&gt;automated retention verification&lt;/li&gt;
&lt;li&gt;multilingual and long-audio evaluation sets&lt;/li&gt;
&lt;li&gt;cost tracking at the task level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A successful provider response is not the same as a usable transcript. Output shape, timestamp coverage, language behavior, and retry semantics all need validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;An AI transcription product is a distributed media system before it is an AI demo.&lt;/p&gt;

&lt;p&gt;The durable design comes from treating state transitions, storage handoffs, callbacks, billing, and deployment artifacts as explicit contracts. Once those boundaries are reliable, switching models or adding output formats becomes much less risky.&lt;/p&gt;

&lt;p&gt;If you are building a similar workflow, I would start with the state machine and idempotency rules before optimizing model latency. Those two decisions will shape almost every failure you have to handle later.&lt;/p&gt;

&lt;p&gt;What has been the hardest reliability problem in your own asynchronous pipeline?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
