From d49a420dea2f4a97ebf1c2ab3dfaed43de34afa9 Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Mon, 24 Aug 2026 14:15:47 +0000 Subject: [PATCH] fix(cli): assign w{index} ids when loading JSON transcripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadTranscript assigns id: w{index} on the srt/vtt branches but never on the JSON branches: parseWhisperCpp and parseOpenAI drop the field and the words-json branch defaults it to "". Every engine funnels through loadTranscript, and transcribeAudio rewrites transcript.json from its output, so CLI-produced transcripts ship without the stable word ids that transcribe.md documents for caption overrides — per-word overrides have nothing to key on. Assign id: w.id || `w{index}` across the JSON branches, matching the srt/vtt behavior. || also repairs the empty-string ids older CLIs wrote to words-json files, which otherwise collapse every word onto one key. Signed-off-by: Santhi Prakash --- packages/cli/src/whisper/normalize.test.ts | 31 +++++++++++++++------- packages/cli/src/whisper/normalize.ts | 10 ++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/whisper/normalize.test.ts b/packages/cli/src/whisper/normalize.test.ts index 8ee305a615..9391eabbbe 100644 --- a/packages/cli/src/whisper/normalize.test.ts +++ b/packages/cli/src/whisper/normalize.test.ts @@ -103,8 +103,8 @@ describe("loadTranscript", () => { const { words, format } = loadTranscript(path); expect(format).toBe("whisper-cpp"); expect(words).toEqual([ - { text: "Hello,", start: 0, end: 0.55 }, - { text: "world.", start: 0.6, end: 1.25 }, + { text: "Hello,", start: 0, end: 0.55, id: "w0" }, + { text: "world.", start: 0.6, end: 1.25, id: "w1" }, ]); }); @@ -142,8 +142,8 @@ describe("loadTranscript", () => { const { words, format } = loadTranscript(path); expect(format).toBe("openai"); expect(words).toEqual([ - { text: "Hello", start: 0, end: 0.5 }, - { text: "world", start: 0.6, end: 1.2 }, + { text: "Hello", start: 0, end: 0.5, id: "w0" }, + { text: "world", start: 0.6, end: 1.2, id: "w1" }, ]); }); @@ -205,7 +205,7 @@ Short format expect(words[0]?.text).toBe("Bold and italic"); }); - it("passes through normalized word arrays", () => { + it("assigns w{index} ids to normalized word arrays", () => { const input = [ { text: "Hello", start: 0.0, end: 0.5 }, { text: "world", start: 0.6, end: 1.2 }, @@ -214,10 +214,21 @@ Short format const { words, format } = loadTranscript(path); expect(format).toBe("words-json"); expect(words).toEqual([ - { text: "Hello", start: 0, end: 0.5, id: "" }, - { text: "world", start: 0.6, end: 1.2, id: "" }, + { text: "Hello", start: 0, end: 0.5, id: "w0" }, + { text: "world", start: 0.6, end: 1.2, id: "w1" }, ]); }); + + it("preserves existing ids and repairs empty-string ids from legacy files", () => { + const input = [ + { text: "Hello", start: 0.0, end: 0.5, id: "keep-me" }, + { text: "world", start: 0.6, end: 1.2, id: "" }, + { text: "again", start: 1.3, end: 1.8 }, + ]; + const path = tmpFile("legacy.json", JSON.stringify(input)); + const { words } = loadTranscript(path); + expect(words.map((w) => w.id)).toEqual(["keep-me", "w1", "w2"]); + }); }); describe("caption formatting", () => { @@ -328,9 +339,9 @@ describe("whisper-cpp contraction merging", () => { ); const { words } = loadTranscript(path); expect(words).toEqual([ - { text: "I", start: 0, end: 0.2 }, - { text: "didn't", start: 0.2, end: 0.7 }, - { text: "know", start: 0.7, end: 1 }, + { text: "I", start: 0, end: 0.2, id: "w0" }, + { text: "didn't", start: 0.2, end: 0.7, id: "w1" }, + { text: "know", start: 0.7, end: 1, id: "w2" }, ]); }); diff --git a/packages/cli/src/whisper/normalize.ts b/packages/cli/src/whisper/normalize.ts index 2599d8ab9b..9cdb601e7f 100644 --- a/packages/cli/src/whisper/normalize.ts +++ b/packages/cli/src/whisper/normalize.ts @@ -462,17 +462,21 @@ export function loadTranscript(filePath: string): { words: Word[]; format: Trans const parsed = JSON.parse(content); const format = detectJsonFormat(parsed); - const words = + // JSON parsers never set id — assign w{index} like the srt/vtt branches above + // so caption overrides always have a stable key. `||` (not `??`) also repairs + // the empty-string ids older CLIs wrote to words-json transcript files. + const words = ( format === "whisper-cpp" ? parseWhisperCpp(parsed) : format === "openai" ? parseOpenAI(parsed) : (parsed as Word[]).map((w) => ({ - id: w.id ?? "", + id: w.id, text: w.text.trim(), start: round3(w.start), end: round3(w.end), - })); + })) + ).map((w, i) => ({ ...w, id: w.id || `w${i}` })); return { words, format }; }