<?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: Tim</title>
    <description>The latest articles on DEV Community by Tim (@tim_builds).</description>
    <link>https://dev.to/tim_builds</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%2F4237484%2Fbfe887b5-e5f4-4158-884f-c31d6b0c3848.png</url>
      <title>DEV Community: Tim</title>
      <link>https://dev.to/tim_builds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tim_builds"/>
    <language>en</language>
    <item>
      <title>Your TTS Captions Should Come From the Render, Not From a Transcript of It</title>
      <dc:creator>Tim</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:49:43 +0000</pubDate>
      <link>https://dev.to/tim_builds/your-tts-captions-should-come-from-the-render-not-from-a-transcript-of-it-17lo</link>
      <guid>https://dev.to/tim_builds/your-tts-captions-should-come-from-the-render-not-from-a-transcript-of-it-17lo</guid>
      <description>&lt;p&gt;If you generate voiceovers with text to speech, you already have the two things a caption file needs: the exact text and an engine that knows when each word is spoken. Yet a lot of pipelines throw both away, export an MP3 and then run speech recognition on it to get captions back. You can use a tool that returns &lt;a href="https://ttslark.com/#subtitles" rel="noopener noreferrer"&gt;SRT and VTT subtitle files from the same TTS render&lt;/a&gt; instead. This post explains why that's better, what the two formats look like and what tends to break when captions come from transcription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two pipelines, one of them backwards
&lt;/h2&gt;

&lt;p&gt;Here's the common setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;script.txt -&amp;gt; TTS -&amp;gt; voiceover.mp3 -&amp;gt; ASR -&amp;gt; captions.srt -&amp;gt; manual fixes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the one to aim for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;script.txt -&amp;gt; TTS -&amp;gt; voiceover.mp3 + captions.srt + captions.vtt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first pipeline the speech recognition step is solving a problem you created. You had the ground truth text in &lt;code&gt;script.txt&lt;/code&gt;. The ASR model has to guess it back from audio, and it also has to guess the timing. In the second pipeline nothing is guessed. The text comes from your script, and the timing comes from the synthesis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes wrong with transcription-based captions
&lt;/h2&gt;

&lt;p&gt;None of these are exotic. You'll hit all of them on a normal week of videos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proper nouns and jargon.&lt;/strong&gt; Names, numbers and technical words are exactly where recognition fails. A library name, a CLI flag or a version number gets turned into the closest common word. You wrote it correctly once in the script, and now you're correcting it again in the captions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pronunciation overrides make it worse.&lt;/strong&gt; If you respell a word so the voice says it right, a transcriber will write down the respelling. With same-render captions the override lives in a tag, and the subtitle keeps your written spelling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The team at &amp;lt;say:en-jin-ex&amp;gt;Nginx&amp;lt;/say&amp;gt; shipped the fix overnight.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audio says "en-jin-ex". The caption says "Nginx".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cue boundaries in the wrong place.&lt;/strong&gt; A transcriber has to decide where one caption ends and the next begins. When that's done on fixed or estimated intervals, cues split mid-phrase and readers lose the thread. TTSLark writes cues from the synthesis timeline, so cue boundaries land on real pauses, with lines capped at 42 characters so they fit a phone screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drift on long files.&lt;/strong&gt; A subtitle file generated separately from the audio drifts a little more with every minute. Minute one looks fine in review. Minute twenty doesn't. When both files come out of one job, minute twenty lines up as well as minute one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edits mean redoing the whole chain.&lt;/strong&gt; Change a sentence, and the transcription pipeline needs a new ASR pass plus another round of fixes. With same-render captions you edit the script, render again and the cues move with the audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the timing actually gets produced
&lt;/h2&gt;

&lt;p&gt;On TTSLark's standard English voices, the engine reports the start and end of every word while it renders. Cues are built from those word timings, so a caption changes exactly when its first word is spoken. This also holds for blended voices, because the English primary voice sets the timing while the second voice only contributes timbre.&lt;/p&gt;

&lt;p&gt;For Japanese and Mandarin voices the approach differs a bit: a worker detects the pauses in the finished audio and anchors each cue to them, so a caption changes as each sentence begins. Either way, the text is yours and the timing comes from the actual audio file you'll ship, not from a separate guess.&lt;/p&gt;

&lt;p&gt;Long scripts don't change this. There's no per-job character cap, and the engine works through the script in blocks and stitches them into one MP3 with one continuous subtitle track.&lt;/p&gt;

&lt;h2&gt;
  
  
  SRT vs VTT, briefly
&lt;/h2&gt;

&lt;p&gt;You get both formats from every job, so the question is only which one to hand to which tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SRT&lt;/strong&gt; is the plain, widely supported one. Numbered cues, a timestamp line with a comma before the milliseconds, the text, then a blank line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
00:00:00,000 --&amp;gt; 00:00:02,640
Every caption in this file

2
00:00:02,640 --&amp;gt; 00:00:05,180
was timed during synthesis,

3
00:00:05,700 --&amp;gt; 00:00:08,320
not guessed from the audio later.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WebVTT&lt;/strong&gt; is the web format. It starts with a &lt;code&gt;WEBVTT&lt;/code&gt; header, uses a period before the milliseconds, makes cue numbers optional and supports cue settings for position and alignment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WEBVTT

00:00:00.000 --&amp;gt; 00:00:02.640
Every caption in this file

00:00:02.640 --&amp;gt; 00:00:05.180
was timed during synthesis,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Video editors and YouTube Studio:&lt;/strong&gt; SRT. Premiere Pro, DaVinci Resolve and CapCut all import it, and YouTube Studio accepts it when you add subtitles with timing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own site:&lt;/strong&gt; VTT, because the HTML &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; element expects it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/media/episode-12.mp4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;track&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"captions"&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/media/episode-12.vtt"&lt;/span&gt; &lt;span class="na"&gt;srclang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"English"&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't convert one into the other by hand when you already have both. Every manual conversion is one more chance to break a timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist before shipping
&lt;/h2&gt;

&lt;p&gt;Run these checks on every new render. They take a minute.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Align at zero.&lt;/strong&gt; Put the MP3 and the SRT on the timeline at the same start point and don't nudge either one. If you find yourself sliding captions around, something upstream is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the last cue to the audio length.&lt;/strong&gt; The end time of the final cue should sit right at the end of the speech.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ffprobe -v error -show_entries &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duration \
     -of &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;noprint_wrappers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1:nokey=1 voiceover.mp3
   tail -n 4 voiceover.srt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spot check names.&lt;/strong&gt; Listen to the first few minutes and every section with proper nouns. If a name sounds wrong, fix it with a pronunciation tag in the script and render again. Don't patch the caption text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pauses are part of the script.&lt;/strong&gt; A &lt;code&gt;&amp;lt;pause:2&amp;gt;&lt;/code&gt; tag (up to five seconds) creates real silence, and the next cue starts after it. That's better than stretching a cue to cover a gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-render, don't re-time.&lt;/strong&gt; If the script changed, render the changed chapter again. Hand-shifting cues is how drift gets back in.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cost, since someone always asks
&lt;/h2&gt;

&lt;p&gt;Each credit covers 10,000 bytes of text on standard voices and makes up to 10 minutes of audio, with SRT and VTT included. English takes one byte per letter, while Chinese, Japanese and Hindi characters take three, so the same length of speech costs more bytes in those scripts. Free accounts get 5 credits that never expire, and commercial use is included on that plan too, with no watermark. For long narration, the &lt;a href="https://ttslark.com/narrator-text-to-speech" rel="noopener noreferrer"&gt;narrator text to speech page&lt;/a&gt; covers the one-job long render in more detail.&lt;/p&gt;

&lt;p&gt;The general point stands no matter which tool you use. If your TTS engine knows when each word was spoken, ask it for the captions. transcribing your own synthetic audio is extra work that makes the captions worse.&lt;/p&gt;

</description>
      <category>tts</category>
      <category>a11y</category>
      <category>video</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
