fix(cli): assign w{index} ids when loading JSON transcripts (#3770)

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 <b.santhiprakash@gmail.com>
This commit is contained in:
Santhi Prakash
2026-09-14 07:29:04 +00:00
committed by GitHub
parent 558c11b019
commit dc6d78b462
2 changed files with 28 additions and 13 deletions
+21 -10
View File
@@ -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", () => {
@@ -375,9 +386,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" },
]);
});
+7 -3
View File
@@ -503,17 +503,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 };
}