AI disclosure: Prepared by the PixMind team with AI-assisted writing. The sample transcript is synthetic. The parser was checked locally with Node.js v24.20.0; no AI-generated images are used.
A timestamped transcript is useful after the transcription job has finished. It can become a list of search results with jump-back points, an interview index, or data for an internal review screen. You do not need to send the text through another model just to identify its time prefixes.
This developer adaptation of our video-to-text tutorial starts with an exported TXT file and ends with plain JSON. It deliberately keeps the original timestamp string alongside a numeric start time.
Define the input before writing a parser
The current TXT export in our example workflow places a start timestamp in square brackets before each segment. Short recordings use minutes and seconds; after an hour, the formatter includes hours. Download the timestamped TXT rather than assuming that a plain-text copy button preserves timing.
For this tutorial, save the following synthetic text as transcript.txt:
[0:03] Open the settings panel.
[1:02] Save the updated profile.
[1:02:03] Return to settings after the long demonstration.
The supported forms here are [m:ss] and [h:mm:ss], with optional leading zeros. Seconds, and the middle field in a three-part time, must be between 00 and 59. This is an input contract for the script below, not a claim that every transcription service exports the same text syntax.
A line without a timestamp is treated as a continuation of the preceding cue. Blank lines are ignored and surrounding whitespace is trimmed. If your source has untimed headings or speaker labels before the first cue, remove them deliberately or extend the parser's contract; do not silently assign a guessed time.
Parse start times while preserving the source text
Save this as transcript-parser.mjs:
export function parseTimestampedTxt(input) {
const cues = [];
let lineNumber = 0;
for (const raw of input.replace(/^\uFEFF/u, "").split(/\r?\n/u)) {
lineNumber += 1;
const line = raw.trim();
if (!line) continue;
const match = /^\[(\d+:[0-5]\d(?::[0-5]\d)?)\]\s*(.*)$/u.exec(line);
if (match) {
const startSeconds = match[1].split(":")
.reduce((total, part) => total * 60 + Number(part), 0);
if (!Number.isSafeInteger(startSeconds)) {
throw new Error("Unsafe timestamp on line " + lineNumber);
}
const previous = cues.at(-1);
if (previous && startSeconds < previous.startSeconds) {
throw new Error("Out-of-order timestamp on line " + lineNumber);
}
cues.push({ startSeconds, timestamp: match[1], text: match[2].trim() });
} else {
if (/^\[\d+:/u.test(line) || !cues.length) {
throw new Error("Expected a timestamp on line " + lineNumber);
}
cues.at(-1).text += " " + line;
}
}
return cues;
}
The time calculation accumulates base-60 fields: 1:02 becomes 62 seconds, and 1:02:03 becomes 3723 seconds. The explicit zero in reduce is the initial total; the MDN reduce reference explains that accumulator behavior.
The parser also removes an optional leading Unicode byte-order mark, accepts LF or CRLF line endings, and reports the line of a malformed numeric time prefix. It rejects decreasing start times instead of silently sorting them, because sorting could hide an export or editing mistake.
Equal start times remain separate cues in source order. That matters when two exported segments share a whole-second display time. Nothing in the TXT gives us enough information to merge those segments reliably.
Notice what the object does not contain: an end time, a speaker identity, or millisecond precision. This TXT format provides start labels. Do not manufacture missing subtitle timings from them; export SRT or VTT from the original result when that is the required deliverable.
Read UTF-8 and filter the cue text
Save a second file, search-transcript.mjs, beside the parser:
import { readFile } from "node:fs/promises";
import { parseTimestampedTxt } from "./transcript-parser.mjs";
const path = process.argv[2];
if (!path) throw new Error("Usage: node search-transcript.mjs transcript.txt [query]");
const query = (process.argv[3] ?? "").trim().toLowerCase();
const cues = parseTimestampedTxt(await readFile(path, "utf8"));
const results = query
? cues.filter(cue => cue.text.toLowerCase().includes(query))
: cues;
console.log(JSON.stringify(results, null, 2));
The UTF-8 encoding argument makes readFile return text. The Node.js file-system reference documents that read operation. The script reads one local file and prints JSON; it does not make a network request or overwrite the transcript.
Run it with a query:
node search-transcript.mjs transcript.txt settings
For the synthetic input, the results are:
[
{
"startSeconds": 3,
"timestamp": "0:03",
"text": "Open the settings panel."
},
{
"startSeconds": 3723,
"timestamp": "1:02:03",
"text": "Return to settings after the long demonstration."
}
]
Omit the query to print every cue. A query containing spaces should be passed as one quoted shell argument. This is a case-insensitive substring filter, not ranked search, language-aware tokenization, or semantic retrieval. For small review tasks that simple behavior is often easier to inspect.
If you build a browser results panel later, keep these values as data and render transcript text as text content. Do not concatenate arbitrary transcript text into HTML markup. A result's startSeconds can be a seek target after the associated media has loaded, but the application still needs to handle the player's available seek range and source version.
Check boundaries before using a real transcript
The parser was exercised with ten local behavior checks: ordinary minute/second cues, hour-bearing cues, BOM and CRLF handling with Unicode and continuations, invalid seconds, invalid middle minutes, decreasing timestamps, untimed leading text, unsafe numeric values, empty input, and the transition from 59:59 to 1:00:00. Those synthetic checks passed with Node.js v24.20.0.
They establish the demonstrated parsing behavior, not transcription accuracy. Start by comparing a few parsed cue times against the recording you actually intend to index. A correct parser cannot repair a transcript aligned to a different edit of the video.
Keep the raw TXT as your reference and use a separate reviewed copy for spelling changes. If you replace a name, preserve its time prefix. If the recording changes, regenerate or realign the timing rather than retaining old seek points by habit.
Keep the next step small
A useful first deliverable is a JSON file containing only startSeconds, timestamp, and text. Add the recording identifier in the application that knows which media file it belongs to. Avoid inferring details that the TXT did not encode.
That gives a downstream search screen a clear boundary: parsing preserves explicit time labels, search chooses matching text, and playback decides whether the requested point is available. Each part can be checked independently.
Adapted from PixMind: How to Transcribe Video to Text. This edition replaces the product walkthrough with a local Node.js parsing and search workflow.
About the author
- The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/
Comments
Join the discussionNo comments yet. Be the first to comment.