From 6ac43580442a4240458b306ac42b11dfcc996e01 Mon Sep 17 00:00:00 2001 From: Akshar Patel Date: Fri, 23 Oct 2026 23:53:46 -0400 Subject: [PATCH] fix(cli): preserve caption arrays during transcript reimport --- bun.lock | 1 + docs/guides/captions-and-recuts.mdx | 3 +++ packages/cli/package.json | 1 + packages/cli/src/whisper/normalize.test.ts | 23 ++++++++++++++++++ packages/cli/src/whisper/normalize.ts | 27 ++++++++++++++++++---- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 140921f95ea..f43c23b7a7b 100644 --- a/bun.lock +++ b/bun.lock @@ -71,6 +71,7 @@ "dependencies": { "@hono/node-server": "^2.0.5", "@puppeteer/browsers": "^3.2.2", + "acorn": "^8.17.0", "adm-zip": "^0.6.0", "citty": "^0.2.1", "compare-versions": "^6.1.1", diff --git a/docs/guides/captions-and-recuts.mdx b/docs/guides/captions-and-recuts.mdx index b1e11c31f5b..c379d4546bd 100644 --- a/docs/guides/captions-and-recuts.mdx +++ b/docs/guides/captions-and-recuts.mdx @@ -69,6 +69,9 @@ inspect synchronization and save position, scale, or rotation corrections. Ask the agent or edit the caption source for corrected words and durable timing or style changes. +To import replacement words, run `hyperframes transcribe transcript.json --dir ./my-video`. +This replaces the full transcript in existing caption HTML and keeps the surrounding caption code. + Review the chosen caption, overlay, or recut path, then render and watch the final file. diff --git a/packages/cli/package.json b/packages/cli/package.json index c9cf1a5b423..7e9e4ef6e86 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -55,6 +55,7 @@ "dependencies": { "@hono/node-server": "^2.0.5", "@puppeteer/browsers": "^3.2.2", + "acorn": "^8.17.0", "adm-zip": "^0.6.0", "citty": "^0.2.1", "compare-versions": "^6.1.1", diff --git a/packages/cli/src/whisper/normalize.test.ts b/packages/cli/src/whisper/normalize.test.ts index 73717da8afa..911278bdcf8 100644 --- a/packages/cli/src/whisper/normalize.test.ts +++ b/packages/cli/src/whisper/normalize.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, afterEach } from "vitest"; import { writeFileSync, readFileSync, mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { Script } from "node:vm"; import { Window } from "happy-dom"; import { loadTranscript, @@ -734,6 +735,28 @@ describe("whisper-cpp zero-duration interpolation", () => { }); describe("patchCaptionHtml", () => { + it.each(["script", "TRANSCRIPT"])( + "replaces the complete %s array when old caption text contains an array terminator", + (name) => { + const dir = mkdtempSync(join(tmpdir(), "hf-patch-test-")); + dirs.push(dir); + const file = join(dir, "captions.html"); + const oldWords = [{ text: "items[i];", start: 0, end: 1 }]; + const prefix = `const ${name} = `; + const suffix = ";\nconst sentinel = true;"; + writeFileSync(file, ``); + const words = [{ id: "w0", text: "Replacement caption", start: 0, end: 1 }]; + + patchCaptionHtml(dir, words); + + const html = readFileSync(file, "utf8"); + const source = html.slice("".length); + expect(() => new Script(source)).not.toThrow(); + expect(source.endsWith(suffix)).toBe(true); + expect(JSON.parse(source.slice(prefix.length, -suffix.length))).toEqual(words); + }, + ); + it.each([ "caption", "caption", diff --git a/packages/cli/src/whisper/normalize.ts b/packages/cli/src/whisper/normalize.ts index 497b8bb3fb1..4da1b35c051 100644 --- a/packages/cli/src/whisper/normalize.ts +++ b/packages/cli/src/whisper/normalize.ts @@ -1,5 +1,6 @@ import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { extname, join } from "node:path"; +import { parseExpressionAt, tokenizer } from "acorn"; import { parseHTML } from "linkedom"; export interface Word { @@ -543,6 +544,22 @@ export function loadTranscript(filePath: string): { words: Word[]; format: Trans return { words, format }; } +function findCaptionArray(source: string, name: "script" | "TRANSCRIPT"): string | undefined { + const prefix = `const ${name} = `; + const start = source.indexOf(`${prefix}[`); + if (start < 0) return undefined; + + try { + const array = parseExpressionAt(source, start + prefix.length, { ecmaVersion: "latest" }); + if (array.type !== "ArrayExpression") return undefined; + const terminator = tokenizer(source.slice(array.end), { ecmaVersion: "latest" }).getToken(); + if (terminator.type.label !== ";") return undefined; + return source.slice(start, array.end + terminator.end); + } catch { + return undefined; + } +} + export function patchCaptionHtml(dir: string, words: Word[]): void { if (words.length === 0) return; @@ -563,16 +580,16 @@ export function patchCaptionHtml(dir: string, words: Word[]): void { for (const file of htmlFiles) { let content = readFileSync(file, "utf-8"); const scriptBlocks = content.match(/