Every few weeks someone on my team drops a two-hour recording into a chat and writes: "can you pull the decisions out of this?" The first time I did it by hand. The second time I ran the file through a speech-to-text API, got back one 14,000-character paragraph with no punctuation, and realised the raw text was almost as useless as the audio.
That is the part nobody warns you about. Getting words out of audio has been a solved problem for a while. Getting a transcript you can actually work with is a different job, and it comes down to three things: timecodes, speaker labels, and picking the right export format for what happens next.
The three things that make a transcript usable
Timecodes turn the transcript into an index. Without them, finding the moment someone agreed to a deadline means re-listening. With them, you search the text, get 00:47:12, and jump straight there. Any transcript longer than about ten minutes is unusable without them.
Speaker labels (diarization, if you want the formal term) turn a monologue back into a conversation. Compare:
we should ship on friday no we can't the migration isn't done
then we ship monday and announce on tuesday
with:
[00:12:03] Speaker 1: We should ship on Friday.
[00:12:05] Speaker 2: No, we can't — the migration isn't done.
[00:12:09] Speaker 1: Then we ship Monday and announce on Tuesday.
Same words. Only the second one tells you who committed to what.
A summary is the third piece, and the one people underestimate. For a weekly call you rarely need the full text — you need six lines telling you whether anything was decided, and the full text only when the answer is yes.
Formats: what to export and when
This is where most integrations go wrong, because "export the transcript" is not one thing.
| Format | Use it for | Why |
|---|---|---|
| TXT | grep, search indexes, feeding an LLM | no markup to strip |
| SRT | subtitles for video players, YouTube uploads | universally supported, cue-numbered |
| VTT | subtitles on the web (<track> element) |
HTML5-native, supports styling and metadata |
| DOCX | anything a human will edit and comment on | tracked changes, comments, page layout |
| attachments, records, anything that must not be edited | fixed rendering |
The two that get confused are SRT and VTT. They look almost identical, and converting between them is genuinely a few lines of code — which is exactly why people assume they are interchangeable and then wonder why their <track> element renders nothing.
Here is the whole conversion:
import re
def srt_to_vtt(srt: str) -> str:
# VTT wants a header, dots instead of commas in timestamps,
# and it does not need the numeric cue counter SRT puts on every block.
body = re.sub(r"(\d{2}:\d{2}:\d{2}),(\d{3})", r"\1.\2", srt)
body = re.sub(r"^\d+\s*$", "", body, flags=re.MULTILINE)
return "WEBVTT\n\n" + body.strip() + "\n"
And parsing SRT into something you can actually query — the function I end up rewriting in every project, so here it is once:
from dataclasses import dataclass
@dataclass
class Cue:
start: str
end: str
text: str
def parse_srt(srt: str) -> list[Cue]:
cues = []
for block in srt.strip().split("\n\n"):
lines = [l for l in block.split("\n") if l.strip()]
if len(lines) < 2:
continue
# lines[0] is the cue number, lines[1] the timing, the rest is text
timing = lines[1] if "-->" in lines[1] else lines[0]
start, end = [t.strip() for t in timing.split("-->")]
text = " ".join(lines[2:] if "-->" in lines[1] else lines[1:])
cues.append(Cue(start, end, text))
return cues
With that you can do the thing everyone actually wants: find every cue where a term appears and print the timestamps.
def find(cues, term):
return [(c.start, c.text) for c in cues if term.lower() in c.text.lower()]
Thirty seconds of code, and a two-hour recording becomes searchable.
What breaks, and it is usually the language
If your audio is English and the speakers take turns politely, most engines do fine. Real recordings are neither.
Overlapping speech is the main killer of diarization. When two people talk over each other, you get one speaker label for both, or a phantom third speaker. There is no clean fix — but knowing it happens means you check the speaker map before trusting it.
Domain vocabulary is the second. Product names, internal acronyms, surnames: the engine will confidently turn them into the nearest common word. Budget time for a find-and-replace pass, or feed the engine a vocabulary hint if it supports one.
Non-English speech is where the gap is widest. A model trained mostly on English will handle Russian, Kazakh or Armenian technically — it will produce words — but punctuation and proper nouns fall apart, and mixed-language speech ("давайте закоммитим этот branch") tends to come back as nonsense in both languages. If your users speak something other than English, test with their recordings before you pick an engine, not with a clean English sample from the docs.
That last problem is the one my own product exists for: AudioVText transcribes audio and video with Russian as its primary language — the pipeline is tuned for it — while still covering 99 languages, and it returns timecodes, speaker separation and a short summary in the same pass. Files export to TXT, SRT, VTT, DOCX and PDF, so the same transcript works as subtitles or as a document. You can also hand it a link to a video instead of a file.
The workflow that ended up working
- Transcribe once, ask for timecodes and speaker labels up front — retrofitting them later means re-running the audio.
- Export TXT for search and for anything you feed to an LLM.
- Export SRT or VTT only when there is a video, and pick based on where it plays.
- Read the summary first. Open the full transcript only when the summary says something happened.
- Keep the audio. Every transcript has at least one spot where you need to hear the original.
None of this is clever. It is just the difference between a transcript you produce and forget, and one your team actually opens a week later.
If you have your own trick for handling overlapping speakers, I would genuinely like to hear it — that is the piece I still solve by hand.
Top comments (0)