From 6469247218844b972a44aa680c68439ea777564f Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Fri, 9 Oct 2026 11:36:37 +0800 Subject: [PATCH 01/11] fix(cli): preserve media after the last transcript word --- .../cli/src/media-use/lib/cutlist.test.mjs | 2 - skills-manifest.json | 2 +- skills/media-use/references/operations.md | 2 +- skills/media-use/scripts/transcript-cut.mjs | 10 ++- .../media-use/scripts/transcript-cut.test.mjs | 84 ++++++++++++++++--- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/media-use/lib/cutlist.test.mjs b/packages/cli/src/media-use/lib/cutlist.test.mjs index 98e7710a27b..4bc526a7d44 100644 --- a/packages/cli/src/media-use/lib/cutlist.test.mjs +++ b/packages/cli/src/media-use/lib/cutlist.test.mjs @@ -132,8 +132,6 @@ test("--plan on a fixture transcript prints the exact segment JSON", () => { process.execPath, [ SCRIPT, - "--input", - "ignored.mp4", "--transcript", transcriptPath, "--remove", diff --git a/skills-manifest.json b/skills-manifest.json index 49ddfb2a1e9..355914c57b5 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,7 +54,7 @@ "files": 1 }, "media-use": { - "hash": "c8800249da2d1ce6", + "hash": "f0396dd8114cd379", "files": 109 }, "motion-graphics": { diff --git a/skills/media-use/references/operations.md b/skills/media-use/references/operations.md index 491abebf40e..454cddb0d63 100644 --- a/skills/media-use/references/operations.md +++ b/skills/media-use/references/operations.md @@ -143,7 +143,7 @@ node /scripts/transcript-cut.mjs \ resolve --from talk.cut.mp4 --type video ``` -Use `--plan` first when you want to inspect the kept segment JSON before encoding. +Use `--plan` with `--input` first when you want to inspect the kept segment JSON before encoding. The source's measured duration bounds the edits, including footage after the last word. A transcript-only `--plan` needs no media tools and uses the last word's end as its duration. ## Ducking (declare in-composition / bake for export) diff --git a/skills/media-use/scripts/transcript-cut.mjs b/skills/media-use/scripts/transcript-cut.mjs index b7e4f468e03..36f406fe9df 100644 --- a/skills/media-use/scripts/transcript-cut.mjs +++ b/skills/media-use/scripts/transcript-cut.mjs @@ -42,7 +42,7 @@ Options: --cut-silence Remove inter-word gaps longer than this many seconds --keep Inverse mode: direct kept ranges, mutually exclusive with removal --copy Use stream copy for faster, keyframe-snapped cuts - --plan Print kept segment JSON and exit without ffmpeg + --plan Print kept segment JSON; uses source duration when --input is supplied --out Output file --json Output JSON status --help, -h Show this help`); @@ -66,8 +66,12 @@ try { function run() { if (!args.transcript) throw new Error("--transcript is required"); + const inputPath = args.input ? resolve(args.input) : null; + // A last word at 4s must not drop a 6s source's outro when removing a filler. + const totalSeconds = inputPath ? probeDuration(inputPath) : null; const transcript = JSON.parse(readFileSync(resolve(args.transcript), "utf8")); const segments = compileCutList(transcript, { + duration: totalSeconds, remove: args.remove, removeWords: args["remove-words"], removeFillers: args["remove-fillers"], @@ -80,16 +84,14 @@ function run() { return; } - if (!args.input || !args.out) + if (!inputPath || !args.out) throw new Error("--input and --out are required unless --plan is set"); if (segments.length === 0) throw new Error("cut list has no kept segments"); - const inputPath = resolve(args.input); const outPath = resolve(args.out); mkdirSync(dirname(outPath), { recursive: true }); const tmpDir = mkdtempSync(join(tmpdir(), "media-use-cut-")); const keptSeconds = sumDurations(segments); - const totalSeconds = probeDuration(inputPath); try { const parts = segments.map((segment, index) => { diff --git a/skills/media-use/scripts/transcript-cut.test.mjs b/skills/media-use/scripts/transcript-cut.test.mjs index 99b52dce424..6025603c279 100644 --- a/skills/media-use/scripts/transcript-cut.test.mjs +++ b/skills/media-use/scripts/transcript-cut.test.mjs @@ -28,6 +28,20 @@ function run(args) { return spawnSync(process.execPath, [SCRIPT, ...args], { encoding: "utf8" }); } +function writeTone(source) { + execFileSync("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, + source, + ]); +} + function readPcm(filePath) { const raw = execFileSync("ffmpeg", [ "-hide_banner", @@ -47,6 +61,64 @@ function readPcm(filePath) { return samples; } +for (const { name, edits, segments, duration } of [ + { + name: "preserves the source tail when removing a filler", + edits: ["--remove-fillers", "um"], + segments: [ + { start: 0, end: 1 }, + { start: 1.5, end: 6 }, + ], + duration: 5.5, + }, + { + name: "keeps an explicit range after the last spoken word", + edits: ["--keep", "4-6"], + segments: [{ start: 4, end: 6 }], + duration: 2, + }, +]) { + test(name, { skip: !HAS_FFMPEG }, (t) => { + const { dir, cleanup } = fixture(); + t.after(cleanup); + const source = join(dir, "tone.wav"); + writeTone(source); + const transcriptPath = join(dir, "transcript.json"); + writeFileSync( + transcriptPath, + JSON.stringify({ + words: [ + { text: "hello", start: 0, end: 1 }, + { text: "um", start: 1, end: 1.5 }, + { text: "goodbye", start: 1.5, end: 4 }, + ], + }), + ); + const output = join(dir, "out.wav"); + const result = run([ + "--input", + source, + "--transcript", + transcriptPath, + ...edits, + "--out", + output, + "--json", + ]); + assert.equal(result.status, 0, result.stderr || result.stdout); + const status = JSON.parse(result.stdout); + assert.deepEqual(status.segments, segments); + assert.equal(status.kept_s, duration); + assert.equal(status.total_s, 6); + const plan = run(["--input", source, "--transcript", transcriptPath, ...edits, "--plan"]); + assert.equal(plan.status, 0, plan.stderr || plan.stdout); + assert.deepEqual(JSON.parse(plan.stdout), segments); + const samples = readPcm(output); + assert.equal(samples.length, duration * SAMPLE_RATE); + assert.ok(samples.slice(-SAMPLE_RATE).some((sample) => Math.abs(sample) > 1000)); + }); +} + test( "keeps a spliced tone continuous at every cut, with no raw phase jump or silence gap", { skip: !HAS_FFMPEG }, @@ -55,17 +127,7 @@ test( t.after(cleanup); const source = join(dir, "tone.wav"); - execFileSync("ffmpeg", [ - "-y", - "-hide_banner", - "-loglevel", - "error", - "-f", - "lavfi", - "-i", - `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, - source, - ]); + writeTone(source); const transcriptPath = join(dir, "transcript.json"); writeFileSync(transcriptPath, JSON.stringify({ words: [{ text: "tone", start: 0, end: 6 }] })); From cff43281ee076c3d784aa2f1a75ed775e1613681 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Wed, 14 Oct 2026 04:30:08 +0800 Subject: [PATCH 02/11] chore: prepare conflict resolution against current main --- skills-manifest.json | 40 +-- .../media-use/scripts/transcript-cut.test.mjs | 302 ++++++++++++++---- 2 files changed, 252 insertions(+), 90 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 355914c57b5..960e5a88ac8 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -2,35 +2,35 @@ "source": "heygen-com/hyperframes", "skills": { "embedded-captions": { - "hash": "643579aed5919847", - "files": 144 + "hash": "1dc7ec2fd4a6e3fc", + "files": 145 }, "faceless-explainer": { - "hash": "1f10a69e0f879698", - "files": 28 + "hash": "bb2225ebaf7f9622", + "files": 29 }, "figma": { "hash": "f8cf9f2c78ea8374", "files": 2 }, "general-video": { - "hash": "7194a47a82647a88", - "files": 5 + "hash": "17dfbb1d34efb012", + "files": 6 }, "hyperframes": { - "hash": "589ae40d15628917", - "files": 29 + "hash": "9787b91e4c16bf7f", + "files": 31 }, "hyperframes-animation": { - "hash": "3bd80388da91c689", + "hash": "a5d09375a50f71c8", "files": 122 }, "hyperframes-audio": { - "hash": "e96de47ed2e6097a", - "files": 7 + "hash": "780c9d4d8d23ab40", + "files": 8 }, "hyperframes-cli": { - "hash": "c053e99be2d209b1", + "hash": "56a46bc15c308d98", "files": 11 }, "hyperframes-core": { @@ -54,11 +54,11 @@ "files": 1 }, "media-use": { - "hash": "f0396dd8114cd379", - "files": 109 + "hash": "c4c3c723aa04bb7d", + "files": 112 }, "motion-graphics": { - "hash": "6db77031ad839f6a", + "hash": "7e4af0881390ac2e", "files": 23 }, "music-to-video": { @@ -66,12 +66,12 @@ "files": 169 }, "pr-to-video": { - "hash": "88f4c68788eaba9e", - "files": 34 + "hash": "d377b802ef54bc46", + "files": 35 }, "product-launch-video": { - "hash": "76149a9f3514626e", - "files": 36 + "hash": "965b9b93bf19b173", + "files": 37 }, "remotion-to-hyperframes": { "hash": "d02be65b7f3f4a63", @@ -82,7 +82,7 @@ "files": 2 }, "talking-head-recut": { - "hash": "9aac076ceddd2499", + "hash": "4b2cf6d7b6e336b4", "files": 28 } } diff --git a/skills/media-use/scripts/transcript-cut.test.mjs b/skills/media-use/scripts/transcript-cut.test.mjs index 6025603c279..95852adb825 100644 --- a/skills/media-use/scripts/transcript-cut.test.mjs +++ b/skills/media-use/scripts/transcript-cut.test.mjs @@ -28,20 +28,6 @@ function run(args) { return spawnSync(process.execPath, [SCRIPT, ...args], { encoding: "utf8" }); } -function writeTone(source) { - execFileSync("ffmpeg", [ - "-y", - "-hide_banner", - "-loglevel", - "error", - "-f", - "lavfi", - "-i", - `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, - source, - ]); -} - function readPcm(filePath) { const raw = execFileSync("ffmpeg", [ "-hide_banner", @@ -61,63 +47,154 @@ function readPcm(filePath) { return samples; } -for (const { name, edits, segments, duration } of [ - { - name: "preserves the source tail when removing a filler", - edits: ["--remove-fillers", "um"], - segments: [ - { start: 0, end: 1 }, - { start: 1.5, end: 6 }, - ], - duration: 5.5, - }, - { - name: "keeps an explicit range after the last spoken word", - edits: ["--keep", "4-6"], - segments: [{ start: 4, end: 6 }], - duration: 2, - }, -]) { - test(name, { skip: !HAS_FFMPEG }, (t) => { - const { dir, cleanup } = fixture(); - t.after(cleanup); - const source = join(dir, "tone.wav"); - writeTone(source); - const transcriptPath = join(dir, "transcript.json"); - writeFileSync( - transcriptPath, - JSON.stringify({ - words: [ - { text: "hello", start: 0, end: 1 }, - { text: "um", start: 1, end: 1.5 }, - { text: "goodbye", start: 1.5, end: 4 }, +// Three kept ranges, none starting or ending on a frame boundary of any rate below. +const UNALIGNED_KEEP = "0.137-1.913,2.845-4.111,4.9-5.47"; +const UNALIGNED_KEPT_SECONDS = 1.776 + 1.266 + 0.57; + +function cutVideo(t, rate, keep = UNALIGNED_KEEP) { + const { dir, cleanup } = fixture(); + t.after(cleanup); + + const source = join(dir, "source.mp4"); + execFileSync("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + `testsrc2=size=160x90:rate=${rate}:duration=6`, + "-f", + "lavfi", + "-i", + `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, + "-c:v", + "libx264", + "-preset", + "ultrafast", + "-pix_fmt", + "yuv420p", + "-c:a", + "aac", + source, + ]); + + const transcriptPath = join(dir, "transcript.json"); + writeFileSync(transcriptPath, JSON.stringify({ words: [{ text: "tone", start: 0, end: 6 }] })); + + const output = join(dir, "out.mp4"); + const result = run([ + "--input", + source, + "--transcript", + transcriptPath, + "--keep", + keep, + "--out", + output, + "--json", + ]); + assert.equal(result.status, 0, result.stderr || result.stdout); + return output; +} + +function readVideoTimestamps(filePath) { + const probe = JSON.parse( + execFileSync( + "ffprobe", + [ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=time_base:packet=pts", + "-of", + "json", + "--", + filePath, + ], + { encoding: "utf8" }, + ), + ); + const [tickNum, tickDen] = probe.streams[0].time_base.split("/").map(Number); + // Packets come in decode order; B-frames put that out of presentation order. + const pts = probe.packets.map((packet) => Number(packet.pts)).sort((a, b) => a - b); + return { tickNum, tickDen, pts }; +} + +for (const rate of ["30", "30000/1001", "60"]) { + test( + `puts every frame of a ${rate} fps video cut on the frame grid, with no gaps`, + { skip: !HAS_FFMPEG }, + (t) => { + const [rateNum, rateDen = 1] = rate.split("/").map(Number); + const { tickNum, tickDen, pts } = readVideoTimestamps(cutVideo(t, rate)); + assert.ok(pts.length > rateNum / rateDen, "expected several seconds of video"); + + // Frame i belongs at exactly i / rate seconds. Compared as integers, so a + // timestamp rounded to the millisecond (0.067 for 2/30) cannot pass. + const offGrid = pts.filter( + (value, index) => value * tickNum * rateNum !== index * rateDen * tickDen, + ); + assert.equal( + offGrid.length, + 0, + `${offGrid.length} of ${pts.length} frames are off the ${rate} fps grid (timestamps in units of ${tickNum}/${tickDen} s)`, + ); + }, + ); +} + +test( + "keeps the keyframes of a video cut no more than a second apart", + { skip: !HAS_FFMPEG }, + (t) => { + // One kept range of almost four seconds, so only the encoder decides where keyframes go. + const output = cutVideo(t, "30", "0.137-4.111,4.9-5.47"); + const probe = JSON.parse( + execFileSync( + "ffprobe", + [ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "packet=pts_time,flags", + "-of", + "json", + "--", + output, ], - }), + { encoding: "utf8" }, + ), ); - const output = join(dir, "out.wav"); - const result = run([ - "--input", - source, - "--transcript", - transcriptPath, - ...edits, - "--out", - output, - "--json", - ]); - assert.equal(result.status, 0, result.stderr || result.stdout); - const status = JSON.parse(result.stdout); - assert.deepEqual(status.segments, segments); - assert.equal(status.kept_s, duration); - assert.equal(status.total_s, 6); - const plan = run(["--input", source, "--transcript", transcriptPath, ...edits, "--plan"]); - assert.equal(plan.status, 0, plan.stderr || plan.stdout); - assert.deepEqual(JSON.parse(plan.stdout), segments); - const samples = readPcm(output); - assert.equal(samples.length, duration * SAMPLE_RATE); - assert.ok(samples.slice(-SAMPLE_RATE).some((sample) => Math.abs(sample) > 1000)); - }); -} + const keyframes = probe.packets + .filter((packet) => packet.flags.startsWith("K")) + .map((packet) => Number(packet.pts_time)) + .sort((a, b) => a - b); + const gaps = keyframes.slice(1).map((time, index) => time - keyframes[index]); + const widest = Math.max(...gaps); + // A frame of slack: the forced keyframe is the first frame at or after each second. + assert.ok(widest < 1.05, `keyframes are up to ${widest.toFixed(3)}s apart`); + }, +); + +test( + "does not pad the audio of a video cut with encoder priming at each splice", + { skip: !HAS_FFMPEG }, + (t) => { + const seconds = readPcm(cutVideo(t, "30")).length / SAMPLE_RATE; + // The single AAC encode may round the end up by a frame or two (23 ms each). + // Encoding every segment separately instead adds about 25 ms per splice on top. + assert.ok( + Math.abs(seconds - UNALIGNED_KEPT_SECONDS) < 0.05, + `audio runs ${seconds.toFixed(3)}s for ${UNALIGNED_KEPT_SECONDS.toFixed(3)}s kept`, + ); + }, +); test( "keeps a spliced tone continuous at every cut, with no raw phase jump or silence gap", @@ -127,7 +204,17 @@ test( t.after(cleanup); const source = join(dir, "tone.wav"); - writeTone(source); + execFileSync("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, + source, + ]); const transcriptPath = join(dir, "transcript.json"); writeFileSync(transcriptPath, JSON.stringify({ words: [{ text: "tone", start: 0, end: 6 }] })); @@ -177,3 +264,78 @@ test( ); }, ); + +// Frame 32 at 30 fps sits at 1.0666666 s. A -ss rounded to 1.067 lands just past +// it, so ffmpeg's accurate seek starts that segment on frame 33 and the cut comes +// out a frame short from the join on. +const THIRTY_FPS_FRAME_32 = 32 / 30; + +test("a kept range starting on a 30 fps frame keeps that frame", { skip: !HAS_FFMPEG }, (t) => { + const { dir, cleanup } = fixture(); + t.after(cleanup); + + const source = join(dir, "src.mp4"); + execFileSync("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + "testsrc2=size=320x180:rate=30", + "-f", + "lavfi", + "-i", + `sine=frequency=440:sample_rate=${SAMPLE_RATE}`, + "-t", + "4", + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-c:a", + "aac", + source, + ]); + + const transcriptPath = join(dir, "transcript.json"); + writeFileSync(transcriptPath, JSON.stringify([{ text: "a", start: 0.1, end: 3.9 }])); + + const output = join(dir, "out.mp4"); + const result = run([ + "--input", + source, + "--transcript", + transcriptPath, + "--keep", + `0-1,${THIRTY_FPS_FRAME_32}-3`, + "--out", + output, + "--json", + ]); + assert.equal(result.status, 0, result.stderr || result.stdout); + + // Frames 0-29 for 0-1s, then frames 32-89 for the second segment. Rounding + // -ss to the millisecond drops frame 32 and yields 87. + const frames = Number( + execFileSync( + "ffprobe", + [ + "-v", + "error", + "-select_streams", + "v", + "-count_packets", + "-show_entries", + "stream=nb_read_packets", + "-of", + "csv=p=0", + "--", + output, + ], + { encoding: "utf8" }, + ).trim(), + ); + assert.equal(frames, 88, `expected 88 frames, got ${frames}`); +}); From 7771e33ea8fbf257e32aa4f6276e9e200c57d531 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Wed, 14 Oct 2026 04:30:15 +0800 Subject: [PATCH 03/11] chore: preserve reviewed fix on current main --- skills-manifest.json | 2 +- .../media-use/scripts/transcript-cut.test.mjs | 84 ++++++++++++++++--- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 960e5a88ac8..652f38e8062 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,7 +54,7 @@ "files": 1 }, "media-use": { - "hash": "c4c3c723aa04bb7d", + "hash": "467e2b7397f1bdbe", "files": 112 }, "motion-graphics": { diff --git a/skills/media-use/scripts/transcript-cut.test.mjs b/skills/media-use/scripts/transcript-cut.test.mjs index 95852adb825..96593a2ed11 100644 --- a/skills/media-use/scripts/transcript-cut.test.mjs +++ b/skills/media-use/scripts/transcript-cut.test.mjs @@ -28,6 +28,20 @@ function run(args) { return spawnSync(process.execPath, [SCRIPT, ...args], { encoding: "utf8" }); } +function writeTone(source) { + execFileSync("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, + source, + ]); +} + function readPcm(filePath) { const raw = execFileSync("ffmpeg", [ "-hide_banner", @@ -47,6 +61,64 @@ function readPcm(filePath) { return samples; } +for (const { name, edits, segments, duration } of [ + { + name: "preserves the source tail when removing a filler", + edits: ["--remove-fillers", "um"], + segments: [ + { start: 0, end: 1 }, + { start: 1.5, end: 6 }, + ], + duration: 5.5, + }, + { + name: "keeps an explicit range after the last spoken word", + edits: ["--keep", "4-6"], + segments: [{ start: 4, end: 6 }], + duration: 2, + }, +]) { + test(name, { skip: !HAS_FFMPEG }, (t) => { + const { dir, cleanup } = fixture(); + t.after(cleanup); + const source = join(dir, "tone.wav"); + writeTone(source); + const transcriptPath = join(dir, "transcript.json"); + writeFileSync( + transcriptPath, + JSON.stringify({ + words: [ + { text: "hello", start: 0, end: 1 }, + { text: "um", start: 1, end: 1.5 }, + { text: "goodbye", start: 1.5, end: 4 }, + ], + }), + ); + const output = join(dir, "out.wav"); + const result = run([ + "--input", + source, + "--transcript", + transcriptPath, + ...edits, + "--out", + output, + "--json", + ]); + assert.equal(result.status, 0, result.stderr || result.stdout); + const status = JSON.parse(result.stdout); + assert.deepEqual(status.segments, segments); + assert.equal(status.kept_s, duration); + assert.equal(status.total_s, 6); + const plan = run(["--input", source, "--transcript", transcriptPath, ...edits, "--plan"]); + assert.equal(plan.status, 0, plan.stderr || plan.stdout); + assert.deepEqual(JSON.parse(plan.stdout), segments); + const samples = readPcm(output); + assert.equal(samples.length, duration * SAMPLE_RATE); + assert.ok(samples.slice(-SAMPLE_RATE).some((sample) => Math.abs(sample) > 1000)); + }); +} + // Three kept ranges, none starting or ending on a frame boundary of any rate below. const UNALIGNED_KEEP = "0.137-1.913,2.845-4.111,4.9-5.47"; const UNALIGNED_KEPT_SECONDS = 1.776 + 1.266 + 0.57; @@ -204,17 +276,7 @@ test( t.after(cleanup); const source = join(dir, "tone.wav"); - execFileSync("ffmpeg", [ - "-y", - "-hide_banner", - "-loglevel", - "error", - "-f", - "lavfi", - "-i", - `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`, - source, - ]); + writeTone(source); const transcriptPath = join(dir, "transcript.json"); writeFileSync(transcriptPath, JSON.stringify({ words: [{ text: "tone", start: 0, end: 6 }] })); From 8fd458763865a39eb6a15374d537fd703deaf915 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sat, 24 Oct 2026 12:55:10 +0800 Subject: [PATCH 04/11] chore: prepare conflict resolution against current main --- skills-manifest.json | 78 +++++++------------------------------------- 1 file changed, 11 insertions(+), 67 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 652f38e8062..e6442718b8a 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -1,89 +1,33 @@ { "source": "heygen-com/hyperframes", "skills": { - "embedded-captions": { - "hash": "1dc7ec2fd4a6e3fc", - "files": 145 - }, - "faceless-explainer": { - "hash": "bb2225ebaf7f9622", - "files": 29 - }, "figma": { - "hash": "f8cf9f2c78ea8374", + "hash": "af57c9b6411c81f5", "files": 2 }, - "general-video": { - "hash": "17dfbb1d34efb012", - "files": 6 - }, "hyperframes": { - "hash": "9787b91e4c16bf7f", - "files": 31 - }, - "hyperframes-animation": { - "hash": "a5d09375a50f71c8", - "files": 122 - }, - "hyperframes-audio": { - "hash": "780c9d4d8d23ab40", - "files": 8 + "hash": "8fc4b05db245cbbb", + "files": 77 }, "hyperframes-cli": { - "hash": "56a46bc15c308d98", - "files": 11 - }, - "hyperframes-core": { - "hash": "010a65915fc7b45e", - "files": 11 - }, - "hyperframes-creative": { - "hash": "8d0601ce26adc560", - "files": 79 - }, - "hyperframes-keyframes": { - "hash": "a3d43b98955b1366", - "files": 3 + "hash": "d82c156c1b2069d1", + "files": 13 }, "hyperframes-registry": { - "hash": "d7c1885fb4cce2cf", - "files": 12 - }, - "hyperframes-studio": { - "hash": "f5e0b44d68152c12", - "files": 1 + "hash": "221af9e7fbffe732", + "files": 13 }, "media-use": { - "hash": "467e2b7397f1bdbe", - "files": 112 - }, - "motion-graphics": { - "hash": "7e4af0881390ac2e", - "files": 23 - }, - "music-to-video": { - "hash": "b775f27b16efb1f2", - "files": 169 - }, - "pr-to-video": { - "hash": "d377b802ef54bc46", - "files": 35 - }, - "product-launch-video": { - "hash": "965b9b93bf19b173", - "files": 37 + "hash": "1082bd1c4e3f033b", + "files": 116 }, "remotion-to-hyperframes": { - "hash": "d02be65b7f3f4a63", + "hash": "3bf96d306215e384", "files": 77 }, "slideshow": { - "hash": "efc3c0be622fa9a1", + "hash": "c8a591e2aa7a6d86", "files": 2 - }, - "talking-head-recut": { - "hash": "4b2cf6d7b6e336b4", - "files": 28 } } } From 3a3991bce1e18f6ed313d3873559110871ccd711 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sat, 24 Oct 2026 12:55:18 +0800 Subject: [PATCH 05/11] fix(cli): refresh transcript source-duration fix against main --- docs/guides/voice-and-audio.mdx | 11 +++++++++++ skills-manifest.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/guides/voice-and-audio.mdx b/docs/guides/voice-and-audio.mdx index 4687b19db4b..b4e49b871d0 100644 --- a/docs/guides/voice-and-audio.mdx +++ b/docs/guides/voice-and-audio.mdx @@ -76,6 +76,17 @@ JSON file: npx hyperframes transcribe subtitles.srt ``` +## Keep the ending when cutting speech + +When asking the agent to remove fillers or cut spoken passages, supply the +original audio or video along with its transcript. The source duration keeps +footage and sound after the last spoken word, and lets you keep an explicit +range in that ending. For example, removing a filler from a six-second clip +whose last word ends at four seconds preserves the remaining two seconds. + +A plan made from a transcript alone ends at the last word. Include the source +when reviewing the plan to account for the complete clip before encoding. + ## Turn the transcript into captions 1. Read the complete transcript. diff --git a/skills-manifest.json b/skills-manifest.json index e6442718b8a..6ed8d9b47d9 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "1082bd1c4e3f033b", + "hash": "4ac4f144fd72399c", "files": 116 }, "remotion-to-hyperframes": { From 4b50373bed52384181e1829d77a1887a407a47b4 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sun, 25 Oct 2026 10:05:31 +0800 Subject: [PATCH 06/11] chore: prepare conflict resolution against current main --- skills-manifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 6ed8d9b47d9..75aabb96626 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 2 }, "hyperframes": { - "hash": "8fc4b05db245cbbb", + "hash": "63994723e5533606", "files": 77 }, "hyperframes-cli": { @@ -18,11 +18,11 @@ "files": 13 }, "media-use": { - "hash": "4ac4f144fd72399c", + "hash": "6550641984b19843", "files": 116 }, "remotion-to-hyperframes": { - "hash": "3bf96d306215e384", + "hash": "b585361591366636", "files": 77 }, "slideshow": { From bcab75f40044736d256f1e5c4be720eaa36f87c8 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sun, 25 Oct 2026 10:05:38 +0800 Subject: [PATCH 07/11] chore: merge main and refresh transcript-cut skill manifest --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index 75aabb96626..07b6e364616 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "6550641984b19843", + "hash": "66117d8c31118930", "files": 116 }, "remotion-to-hyperframes": { From a5af6523ad69d94c1c4fb2fc9081f83294547427 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Mon, 26 Oct 2026 02:41:02 +0800 Subject: [PATCH 08/11] chore: prepare conflict resolution against current main --- skills-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 07b6e364616..a1564a8b5ff 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 2 }, "hyperframes": { - "hash": "63994723e5533606", + "hash": "76c914f47480032c", "files": 77 }, "hyperframes-cli": { @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "66117d8c31118930", + "hash": "0f1969abb1722e1a", "files": 116 }, "remotion-to-hyperframes": { From 4aedf8168b702feb40a5f439f704dfd316abba5d Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Mon, 26 Oct 2026 02:41:08 +0800 Subject: [PATCH 09/11] chore: merge main and refresh transcript skill manifest --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index a1564a8b5ff..e770fb78124 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "0f1969abb1722e1a", + "hash": "c63869eff3937c74", "files": 116 }, "remotion-to-hyperframes": { From 539239b5f6cbe86d530b5af61b32f492482319cf Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Mon, 26 Oct 2026 07:07:55 +0800 Subject: [PATCH 10/11] chore: prepare conflict resolution against current main --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index e770fb78124..4bc1461bb5e 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "c63869eff3937c74", + "hash": "523f018498ca8fdd", "files": 116 }, "remotion-to-hyperframes": { From e75f1022675c7d9a785f4d2411e02a69787ac956 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Mon, 26 Oct 2026 07:08:02 +0800 Subject: [PATCH 11/11] chore: refresh transcript skill manifest after upstream merge --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index 4bc1461bb5e..d5725432328 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "523f018498ca8fdd", + "hash": "8bf648a1652b48ab", "files": 116 }, "remotion-to-hyperframes": {