<?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: Joy</title>
    <description>The latest articles on DEV Community by Joy (@joy_transcribes).</description>
    <link>https://dev.to/joy_transcribes</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%2F4238880%2F8b1b1115-29e6-4b7d-bd9a-b71272506229.png</url>
      <title>DEV Community: Joy</title>
      <link>https://dev.to/joy_transcribes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joy_transcribes"/>
    <language>en</language>
    <item>
      <title>I Recorded a Screen-Reader Walkthrough, Then Used Audio to Text to File the Issues</title>
      <dc:creator>Joy</dc:creator>
      <pubDate>Fri, 13 Nov 2026 14:22:13 +0000</pubDate>
      <link>https://dev.to/joy_transcribes/i-recorded-a-screen-reader-walkthrough-then-used-audio-to-text-to-file-the-issues-5gbo</link>
      <guid>https://dev.to/joy_transcribes/i-recorded-a-screen-reader-walkthrough-then-used-audio-to-text-to-file-the-issues-5gbo</guid>
      <description>&lt;p&gt;I do accessibility reviews for our own product, not as a specialist hired from outside, but as the engineer who was willing to keep a checklist and a copy of a screen reader on their laptop. For a long time my review notes were a mess of screenshots and half sentences I had typed with one hand while listening with both ears. I would hear a problem, reach for the keyboard, lose my place in the page, and then write “button, somewhere near checkout, unclear” — which is a terrible ticket. In May I started recording the walkthrough as I spoke, and I ran audio to text afterward so the ticket could quote the moment I was actually in, not the moment I thought I had been in. This is that workflow, including the parts that did not work.&lt;/p&gt;

&lt;p&gt;I only record my own voice, plus the screen reader’s output if it is coming through the same headphones I have looped back on purpose. I do not record teammates who happen to be in the room, and I do not record users. If someone sits in on a review, the recorder stays off unless they agree, and even then I prefer they not sit in. This is a private engineering artifact, not a research session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The walkthrough I actually run
&lt;/h2&gt;

&lt;p&gt;I use the same route every time, because a random wander makes a worse transcript and a worse ticket. Mine is boring on purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Land on the page with the screen reader running, and say the page title I hear&lt;/li&gt;
&lt;li&gt;Move through the main landmarks, and say each one out loud if the software does not&lt;/li&gt;
&lt;li&gt;Try the primary task (in this case, starting a checkout)&lt;/li&gt;
&lt;li&gt;Try the error state (submit the form empty)&lt;/li&gt;
&lt;li&gt;Stop, and say “end of walkthrough”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last line is for me. It gives me a marker when I search the text later. I used to forget to say where I was on the page. The first transcript I got was a stream of complaints with no headings, and I had to play the audio back anyway. Now I announce the place: “I am on the shipping form. The field after postal code has no spoken label.” That sentence is already most of a ticket title.&lt;/p&gt;

&lt;p&gt;I record with the in-browser recorder sometimes, and with a local m4a when I want the file next to the video I take of the session. Both have worked. The tool accepts thirty-plus audio and video formats, and it also accepts a public YouTube or TikTok or X link, which I do not use here; these reviews are not public videos. A typical walkthrough is fifteen or twenty minutes. In Standard mode that is 15 or 20 credits, against 240 free credits a day when I am signed in. I leave speaker identification off when it is only my voice. If the screen reader is in the same file and I want those lines marked separately, I will turn it on, at two credits a minute, and then I still relabel anything that looks off.&lt;/p&gt;

&lt;p&gt;A one-hour file is described as often finishing in about two minutes, depending on quality and language. These short files are done before I have finished closing the extra tabs I opened for the review.&lt;/p&gt;

&lt;h2&gt;
  
  
  From transcript to tickets
&lt;/h2&gt;

&lt;p&gt;I export SRT or VTT, not just a wall of TXT, because I want a timestamp I can paste into the ticket and, if needed, match to the video. I do not need a subtitle file for a public player. I need “at 06:12 I was on the shipping form.”&lt;/p&gt;

&lt;p&gt;My issue template is small. I have pasted it at the bottom of this post. The only parts I take from the transcript are the spoken description and the timestamp. The rest — browser, screen reader version, expected result, actual result — I fill in by hand, looking at the page. I do not let the text invent an expected result. It is very willing, if I am not careful, to sound more precise than I was in the moment.&lt;/p&gt;

&lt;p&gt;Accuracy is described as up to 99% on clear recordings. A headset mic in a quiet room is close to that. The screen reader’s voice can still be chopped, especially when I talk over it, which I try not to do and sometimes do. Control names, in particular, I check against the page. If I heard “continue” and the accessible name is “Continue to payment,” the ticket uses the name on the page.&lt;/p&gt;

&lt;p&gt;I do not use an automatic summary for this. A summary of a walkthrough tends to smear three issues into one paragraph and drop the timestamp. I want three tickets, or one ticket with three numbered steps, and I want the clock on each.&lt;/p&gt;

&lt;p&gt;When the tickets are filed, I delete the audio and the transcript from my machine and from the library in the tool. The tickets are the record. If a reviewer wants to hear the stretch, they can ask me to reproduce it; they should not need a copy of my voice sitting in a folder.&lt;/p&gt;

&lt;p&gt;Here is the template I actually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Title
&lt;/span&gt;[page] [control] [what I heard or didn’t hear]

&lt;span class="gu"&gt;## Environment
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Browser:
&lt;span class="k"&gt;- &lt;/span&gt;Screen reader and version:
&lt;span class="k"&gt;- &lt;/span&gt;URL (internal):

&lt;span class="gu"&gt;## Timestamp
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Walkthrough clock: (from the SRT cue)

&lt;span class="gu"&gt;## Steps
&lt;/span&gt;1.
2.

&lt;span class="gu"&gt;## Expected
## Actual
## Notes
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;Quote from my spoken notes, checked against the page:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first week I used this, I filed four issues from a single checkout walkthrough, and two of them were things I would have written as “confusing form” and then never been able to reproduce. One was a heading level that I had felt, vaguely, and then could point to at 03:41. The other was an error message that was visible but never spoken. I had heard myself say “I submitted, and nothing was announced,” which is a better ticket than “error??” with a screenshot of a red box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I will not do with this
&lt;/h2&gt;

&lt;p&gt;I will not attach the transcript to a public issue tracker. I will not record a customer. I will not treat this as a substitute for testing by people who use a screen reader every day. This is a way for me, as an engineer, to keep my place and to write a ticket that another engineer can follow. It is not a complete accessibility practice, and I do not want it read as one.&lt;/p&gt;

&lt;p&gt;If you already do a spoken walkthrough and you already lose your notes, you can &lt;a href="https://audiototext.tools/" rel="noopener noreferrer"&gt;transcribe the walkthrough&lt;/a&gt; and file from the cues instead of from the mess on your second monitor. Try it on one route, one day, and compare the tickets to the last set you wrote from one-handed typing. Mine were shorter, and they were easier to reopen a month later.&lt;/p&gt;

&lt;p&gt;I still keep the checklist. The recorder does not know the route. I do, and I still forget a step when I am annoyed at a dropdown. The difference is that when I say “end of walkthrough” and something is missing, I can see the gap in the text and go back, instead of discovering the gap in front of a reviewer.&lt;/p&gt;

&lt;p&gt;If you try &lt;a href="https://audiototext.tools/" rel="noopener noreferrer"&gt;audio to text&lt;/a&gt; for this, start with your own voice, one route, and a template that already has a timestamp field. The file is a means to a ticket. The ticket is the thing that should last.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm involved with AudioToText, the tool linked in this post. The walkthrough route, the template, and the checkout examples are from my own reviews. I have not included product UI, customer data, or a real internal URL.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>documentation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Debugged a Timezone Bug by Talking to Myself, Then Ran the Audio to Text and Got a Bug Report</title>
      <dc:creator>Joy</dc:creator>
      <pubDate>Thu, 12 Nov 2026 11:44:07 +0000</pubDate>
      <link>https://dev.to/joy_transcribes/i-debugged-a-timezone-bug-by-talking-to-myself-then-ran-the-audio-to-text-and-got-a-bug-report-3k00</link>
      <guid>https://dev.to/joy_transcribes/i-debugged-a-timezone-bug-by-talking-to-myself-then-ran-the-audio-to-text-and-got-a-bug-report-3k00</guid>
      <description>&lt;p&gt;Rubber-duck debugging works for me, but I've always lost the useful part. I talk through the problem, I find the bug, and an hour later I can't explain to my team &lt;em&gt;how&lt;/em&gt; I found it. Last month I tried recording the monologue and running the audio to text afterwards. The transcript turned into the first draft of my bug report.&lt;/p&gt;

&lt;p&gt;This is what happened, what the script looks like, and where it fell short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: a test that failed only after 4 p.m.
&lt;/h2&gt;

&lt;p&gt;We had a test in a Node service that checked "orders created today." It passed in CI every morning and failed for anyone who ran it after about 4 p.m. Pacific.&lt;/p&gt;

&lt;p&gt;You can probably guess the cause. I couldn't, at the time, and I spent forty minutes going in circles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording myself (and only myself)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69l1019erntm7z75fb7b.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69l1019erntm7z75fb7b.jpg" alt="Close-up of a rubber duck next to a small desk microphone" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I work from home, so the only voice on the recording was mine. That matters. If you try this while pairing, ask your pair first, and stop recording when someone else drops into the call without knowing.&lt;/p&gt;

&lt;p&gt;I didn't bother with a separate app. The tool I used has in-browser recording, so I opened a tab, hit record, and started talking at my second monitor like a person who has been alone too long.&lt;/p&gt;

&lt;p&gt;The one rule I gave myself: say a marker word out loud before anything worth keeping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Hypothesis:"&lt;/strong&gt; when I had a theory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Ruled out:"&lt;/strong&gt; when a theory died&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Note:"&lt;/strong&gt; for anything I'd want in the write-up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Fix:"&lt;/strong&gt; for the final change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It felt silly for about three minutes. Then I forgot I was doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forty minutes of talking, condensed
&lt;/h2&gt;

&lt;p&gt;Here's a cleaned-up slice of the transcript, to give you the flavor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hypothesis: the fixture dates are hard-coded and drift. … Ruled out: fixtures use &lt;code&gt;Date.now()&lt;/code&gt;. … Okay, so it fails at 4:10 and passes at 3:50. Note: that's suspiciously close to midnight UTC. Hypothesis: &lt;code&gt;startOfDay&lt;/code&gt; is running in UTC on my machine but the query uses local time. … Fix: pass the tenant timezone into &lt;code&gt;startOfDay&lt;/code&gt; instead of relying on the process TZ.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pacific time is UTC−8 in winter, so 4 p.m. local is midnight UTC. Once I said that sentence out loud, the bug was obvious. The recording caught the exact moment, which is what I always lose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audio to text part
&lt;/h2&gt;

&lt;p&gt;When I stopped the recording, I ran the transcription right there in the browser. The free guest run covers up to 60 minutes once, which was plenty for a 40-minute ramble. Since then I've signed in. In Standard mode without speaker identification, each minute costs one of the 240 daily free credits, and a solo monologue doesn't need speakers anyway.&lt;/p&gt;

&lt;p&gt;If you want to try it on your own next debugging session, you can &lt;a href="https://audiototext.tools/" rel="noopener noreferrer"&gt;record and transcribe in the browser&lt;/a&gt; without installing anything, then export the transcript as a TXT file.&lt;/p&gt;

&lt;p&gt;The transcript wasn't perfect. &lt;code&gt;startOfDay&lt;/code&gt; came back as "start of day," and &lt;code&gt;TZ&lt;/code&gt; as "tease." Accuracy is up to 99% on clear recordings, and me mumbling at a monitor with a fan running is not that. I fixed the code identifiers by hand with the editor's search before exporting. It took about three minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 35-line script
&lt;/h2&gt;

&lt;p&gt;The export is plain text. I wrote a small Python script that splits it into sentences and groups anything that starts with a marker word. No dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="s2"&gt;"""Pull marker sentences out of a spoken debugging transcript."""&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;

&lt;span class="no"&gt;MARKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hypothesis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ruled out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fix"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Split on ., ?, ! followed by whitespace. Good enough for speech.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"(?&amp;lt;=[.?!])\s+"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&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;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;MARKERS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"^(hypothesis|ruled out|note|fix)\s*[:,-]?\s*(.+)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&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;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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="n"&gt;found&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;titles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"hypothesis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Hypotheses tried"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ruled out"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ruled out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Fix"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"# Bug report (draft)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;MARKERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"## {titles[key]}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"- {line}"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"- (none)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"\n"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;Run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 duck.py debugging-2026-10-14.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bug-report-draft.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output for my timezone bug looked like this (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Hypotheses tried
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;the fixture dates are hard-coded and drift.
&lt;span class="k"&gt;- &lt;/span&gt;startOfDay is running in UTC on my machine but the query uses local time.

&lt;span class="gu"&gt;## Ruled out
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;fixtures use Date.now().

&lt;span class="gu"&gt;## Notes
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;that's suspiciously close to midnight UTC.

&lt;span class="gu"&gt;## Fix
&lt;/span&gt;&lt;span class="k"&gt;- &lt;/span&gt;pass the tenant timezone into startOfDay instead of relying on the process TZ.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That went into the PR description almost unchanged. My reviewer said it was the first bug write-up of mine she'd actually enjoyed reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fell short
&lt;/h2&gt;

&lt;p&gt;A few honest caveats after doing this about a dozen times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markers get mangled.&lt;/strong&gt; "Ruled out" occasionally became "rule doubt." I added a fuzzy match for that one, then gave up and just fix it by hand in the editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speech doesn't punctuate like writing.&lt;/strong&gt; Some "sentences" run for 80 words. The regex split is crude. It's fine for a draft and useless for anything you'd publish as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code doesn't survive speech.&lt;/strong&gt; Never trust a transcribed identifier. I read every backticked name against the actual code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every session is worth it.&lt;/strong&gt; For a five-minute bug, this is overkill. I use it when I've been stuck for 20 minutes and can feel myself starting to loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I kept doing it
&lt;/h2&gt;

&lt;p&gt;The script isn't the point, and neither is the audio to text step on its own. The point is that speaking a hypothesis out loud forces me to finish the thought, and the recording means I don't lose it.&lt;/p&gt;

&lt;p&gt;I also noticed a pattern in my own transcripts: I say "Ruled out" far less often than "Hypothesis." I jump to the next theory without killing the last one. Seeing that on paper has made me a slightly more disciplined debugger.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to know what marker words you end up using. Mine have grown to include &lt;strong&gt;"Weird:"&lt;/strong&gt;, which so far has preceded every real bug I've found.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm involved with AudioToText, the transcription tool used in this post. The bug, the script and the opinions are my own.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>python</category>
      <category>productivity</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Captions for Your Product Demo in One Afternoon: Audio to Text, WebVTT and the HTML `&lt;track&gt;` Element</title>
      <dc:creator>Joy</dc:creator>
      <pubDate>Wed, 11 Nov 2026 10:43:37 +0000</pubDate>
      <link>https://dev.to/joy_transcribes/captions-for-your-product-demo-in-one-afternoon-audio-to-text-webvtt-and-the-html--b8g</link>
      <guid>https://dev.to/joy_transcribes/captions-for-your-product-demo-in-one-afternoon-audio-to-text-webvtt-and-the-html--b8g</guid>
      <description>&lt;p&gt;Our landing page had a three-minute product demo video. It autoplayed muted, like most demo videos do. Which meant that for most visitors, it was a silent movie of a cursor clicking things.&lt;/p&gt;

&lt;p&gt;A teammate who is hard of hearing pointed it out first, kindly. Then our analytics confirmed it: almost nobody unmuted.&lt;/p&gt;

&lt;p&gt;So I spent an afternoon adding real captions and chapter markers. No video platform, no paid captioning service, just a file, a text editor and the HTML &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; element. Here is the whole process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;captions.vtt&lt;/code&gt; file with accurate, readable cues&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;chapters.vtt&lt;/code&gt; file so users can jump to sections&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element that loads both, with captions on by default when the video starts muted&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get a timestamped transcript
&lt;/h2&gt;

&lt;p&gt;You need text with timings, which is exactly what audio to text output gives you. I uploaded the MP4 export of the demo to an &lt;a href="https://audiototext.tools/" rel="noopener noreferrer"&gt;audio to text tool&lt;/a&gt; and exported VTT directly. It accepts 30+ audio and video formats, so I did not need to strip the audio track first. If your demo is already on YouTube, a public link works too.&lt;/p&gt;

&lt;p&gt;A few settings that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt; set explicitly instead of auto, because our voiceover drops product terms that confuse detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speaker recognition off.&lt;/strong&gt; One narrator, so there was nothing to separate, and Standard mode without speakers uses the fewest credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean Up Speech on&lt;/strong&gt; for this job. It removes filler words and repetitions, which is what you want in captions for a scripted demo. For interviews I would keep the verbatim text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site says up to 99% accuracy on clear recordings. Our voiceover was recorded in a closet with a decent mic, so the draft was close, but it still spelled our product name three different ways. Budget ten minutes for proofreading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Tidy the cues
&lt;/h2&gt;

&lt;p&gt;Raw transcript cues are often too long for comfortable reading. My rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Max two lines per cue, about 42 characters per line&lt;/li&gt;
&lt;li&gt;Each cue on screen at least one second&lt;/li&gt;
&lt;li&gt;Break lines at natural phrase boundaries, not mid-noun&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tiny Node script flags anything too long:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// check-vtt.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"node:fs"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;"captions.vtt"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"utf8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;\&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;\&lt;span class="n"&gt;n&lt;/span&gt;\&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;\&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&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;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;\&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;\&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;timing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--&amp;gt;"&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;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="n"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tooLong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;tooLong&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${timing}\n  ${text.join("\n  ")}\n`&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;node check-vtt.mjs captions.vtt&lt;/code&gt; and fix whatever it prints. On a three-minute video this took me two passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write chapters by hand
&lt;/h2&gt;

&lt;p&gt;Chapters are just another VTT file with &lt;code&gt;kind="chapters"&lt;/code&gt;. I skimmed the transcript to find where each section started. This was the moment the transcript paid off: searching for "dashboard" or "export" gave me exact timestamps without scrubbing the video.&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:21.000
Intro

00:00:21.000 --&amp;gt; 00:01:10.000
Importing a file

00:01:10.000 --&amp;gt; 00:02:15.000
Reviewing results

00:02:15.000 --&amp;gt; 00:03:02.000
Sharing and export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Wire it up
&lt;/h2&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;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"demo"&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt; &lt;span class="na"&gt;playsinline&lt;/span&gt;
       &lt;span class="na"&gt;poster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/img/demo-poster.jpg"&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"960"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&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;"/video/demo.mp4"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"video/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;"/video/captions.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;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;"chapters"&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;"/video/chapters.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="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;The &lt;code&gt;default&lt;/code&gt; attribute turns captions on. Browsers render chapter tracks inconsistently, so I also built a simple chapter list from the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textTracks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// chapters&lt;/span&gt;
&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hidden"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"loadedmetadata"&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;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="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chapters"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cue&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"button"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&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="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;btn&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;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;h2&gt;
  
  
  Step 5: Serve VTT correctly
&lt;/h2&gt;

&lt;p&gt;Two gotchas I hit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The file must be served as &lt;code&gt;text/vtt&lt;/code&gt;. Our static host defaulted to &lt;code&gt;text/plain&lt;/code&gt;, and Safari silently ignored the track.&lt;/li&gt;
&lt;li&gt;If the VTT lives on a different origin than the page, add &lt;code&gt;crossorigin="anonymous"&lt;/code&gt; to the &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; and send CORS headers. Same origin is simpler.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;Roughly an afternoon, most of it on proofreading and line breaks. The transcription step itself took a couple of minutes. A one-hour file typically finishes in about two minutes, and our three-minute demo was nearly instant. As a guest you can try up to 60 minutes once; signed in, the daily 240 credits cover up to 240 minutes in Standard mode without speaker recognition (other modes use more credits per minute).&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Transcript proofread against the audio, especially product names&lt;/li&gt;
&lt;li&gt;[ ] Cues under two lines and about 42 characters&lt;/li&gt;
&lt;li&gt;[ ] Captions &lt;code&gt;default&lt;/code&gt; when video autoplays muted&lt;/li&gt;
&lt;li&gt;[ ] Chapters derived from transcript timestamps&lt;/li&gt;
&lt;li&gt;[ ] VTT served as &lt;code&gt;text/vtt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Captions built from audio to text output are one of those accessibility fixes that also help everyone watching in an open-plan office. If your demo video is still a silent movie, &lt;a href="https://audiototext.tools/" rel="noopener noreferrer"&gt;get a timestamped transcript&lt;/a&gt; and give it a voice on screen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I'm involved with AudioToText, the transcription tool used in step 1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>html</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
