commit 62d441921ca3b54fd5303b9c127e3a994f78ab29
parent 6651ce562afc92cc0f460b8e4a51adfd435da752
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 16:16:53 -0400
report-to-video: a muteFrom past the cut's end is noted as not heard, not as muted from there
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md
@@ -208,6 +208,8 @@ window:
the sound is digital silence, and a hold on that clip stays silent. It is made
where the cut is joined (see [The cut's edits](#the-cuts-edits-mutefrom-and-renderendfade)),
so changing it rebuilds no segment: `--chrome-only` applies it under the deck.
+ Only the played window is in the segment, so a mark past `cutEnd` (inside the
+ extent, so it validates) mutes nothing; the build says it will not be heard.
`render.endFade` (seconds, default 0 = off, at most 10) fades the cut's LAST
segment — whatever it is — picture to `palette.bg` and sound to silence over its
diff --git a/umtool/report-to-video/build-video.mjs b/umtool/report-to-video/build-video.mjs
@@ -2490,12 +2490,19 @@ export async function cutJoins({ schedule = null, entries, segments, render }) {
for (let i = 0; i < entries.length; i += 1) {
const e = entries[i];
if (e.type !== "clip" || e.muteFrom == null) continue;
- const m = muteSegmentSeconds({
- entry: e, record: await readCutRecord(segments[i]), render,
- seconds: await probeDuration(segments[i], render.fps),
- });
+ const seconds = await probeDuration(segments[i], render.fps);
+ const m = muteSegmentSeconds({ entry: e, record: await readCutRecord(segments[i]), render, seconds });
if (m.note) EMIT("note", { id: e.id, message: m.note });
- EMIT("note", { id: e.id, message: `${e.id}: muted from ${m.at}s into its segment (muteFrom ${e.muteFrom}, ${m.source === "record" ? "from its cut record" : "from the unsnapped start"})` });
+ const from = m.source === "record" ? "from its cut record" : "from the unsnapped start";
+ // Validation allows the clip's whole extent, but only the played window is
+ // in the segment: a mark past the cut's end mutes nothing, and saying
+ // "muted from" would claim otherwise.
+ EMIT("note", {
+ id: e.id,
+ message: m.at >= seconds
+ ? `${e.id}: muteFrom ${e.muteFrom} will not be heard -- it lies ${m.at}s into a segment ${seconds}s long, past the cut's end (${from})`
+ : `${e.id}: muted from ${m.at}s into its segment (muteFrom ${e.muteFrom}, ${from})`,
+ });
mutes.set(i, m.at);
}
const seconds = endFadeOf(render);
diff --git a/umtool/report-to-video/cut-edits.test.mjs b/umtool/report-to-video/cut-edits.test.mjs
@@ -371,3 +371,30 @@ test("cutJoins: the record beside the segment places muteFrom; the end fade coun
rmSync(dir, { recursive: true, force: true });
}
});
+
+test("cutJoins: a muteFrom past the cut's end says it will not be heard, not that the clip is muted from there",
+ { skip: !haveFfmpeg }, async () => {
+ const dir = mkdtempSync(path.join(tmpdir(), "cut-latemute-"));
+ const said = [];
+ const log = console.log;
+ console.log = (line) => said.push(String(line));
+ try {
+ const segs = segments(dir, "mp4");
+ // b's segment is 2 s, cut from 200; the clip's extent runs to 203, so a
+ // mark at 202.5 validates but lies 2.5 s into a 2 s segment.
+ writeFileSync(cutRecordPath(segs[1]), JSON.stringify({ version: 1, id: "b", video: "vb", start: 200, end: 202 }));
+ const entries = [
+ { id: "a", type: "clip", video: "va", start: 100, end: 102, muteFrom: 101 },
+ { id: "b", type: "clip", video: "vb", start: 200, end: 203, cutEnd: 202, muteFrom: 202.5 },
+ ];
+ const j = await cutJoins({ entries, segments: segs.slice(0, 2), render: R });
+ assert.equal(j[1].mute, 2.5);
+ const b = said.filter((l) => l.startsWith("b:"));
+ assert.equal(b.length, 1, b.join("\n"));
+ assert.match(b[0], /^b: muteFrom 202\.5 will not be heard -- it lies 2\.5s into a segment 2s long, past the cut's end/);
+ assert.ok(said.some((l) => /^a: muted from 1s into its segment/.test(l)), said.join("\n"));
+ } finally {
+ console.log = log;
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });