// The evidence cutter: where a cited span's media is found, the span it cuts, // the cut itself and the cache in front of it. // // Real ffmpeg over tiny lavfi containers (a few frames at 160×90, a second of // sine), so a run costs little memory and a second or two. // // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test lib/evidenceClip-server.test.ts import { after, before, test } from "node:test"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { copyFile, mkdir, mkdtemp, readdir, rm, symlink, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { evidenceCutArgs, evidenceHash, evidenceSpan, prepareEvidenceClip, probeMedia, resolveEvidenceSource, widerPad, } from "./evidenceClip-server"; const SLUG = "demo-channel"; const ID = "abc123"; let ROOT = ""; // A 6 s 160×90 container with sound, a 1 s 1440×810 one, and 6 s of sound. let SMALL = ""; let WIDE = ""; let SOUND = ""; function ff(args: string[]): void { execFileSync("ffmpeg", ["-nostdin", "-v", "error", "-y", ...args]); } before(async () => { ROOT = await mkdtemp(path.join(tmpdir(), "evidence-clip-")); const fx = path.join(ROOT, "fixtures"); await mkdir(fx, { recursive: true }); SMALL = path.join(fx, "small.mp4"); WIDE = path.join(fx, "wide.mp4"); SOUND = path.join(fx, "sound.m4a"); ff([ "-f", "lavfi", "-i", "testsrc=size=160x90:rate=10:duration=6", "-f", "lavfi", "-i", "sine=frequency=440:duration=6", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", SMALL, ]); ff([ "-f", "lavfi", "-i", "testsrc=size=1440x810:rate=5:duration=1", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", WIDE, ]); ff(["-f", "lavfi", "-i", "sine=frequency=220:duration=6", "-c:a", "aac", SOUND]); }); after(() => rm(ROOT, { recursive: true, force: true })); // A fresh channels tree per test: `/channels//data//`. async function corpus(): Promise<{ channelsDir: string; videoDir: string; cacheDir: string; root: string }> { const root = await mkdtemp(path.join(ROOT, "t-")); const channelsDir = path.join(root, "channels"); const videoDir = path.join(channelsDir, SLUG, "data", ID); await mkdir(videoDir, { recursive: true }); return { channelsDir, videoDir, cacheDir: path.join(root, "report-media"), root }; } test("evidenceSpan: the moment's rounded seconds, widened by the pad, clamped at 0", () => { assert.deepEqual(evidenceSpan({ start: 12.345, end: 20 }), { from: 12.35, to: 20 }); assert.deepEqual(evidenceSpan({ start: 12, end: 20, pad: { before: 5, after: 2.5 } }), { from: 7, to: 22.5 }); assert.deepEqual(evidenceSpan({ start: 3, end: 9, pad: { before: 5 } }), { from: 0, to: 9 }); // A negative pad is no pad. assert.deepEqual(evidenceSpan({ start: 3, end: 9, pad: { before: -2, after: -1 } }), { from: 3, to: 9 }); }); test("widerPad: two citations of one moment get the wider context on each side", () => { assert.equal(widerPad(undefined, undefined), undefined); assert.deepEqual(widerPad({ before: 2 }, undefined), { before: 2 }); assert.deepEqual(widerPad({ before: 2, after: 1 }, { before: 1, after: 4 }), { before: 2, after: 4 }); }); test("tiers: a clip window, else the saved container, else nothing; the window wins when both hold the span", async () => { const { channelsDir, videoDir, root } = await corpus(); const span = { from: 11, to: 13 }; const ask = (audio = false) => resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span, audio }); assert.equal(await ask(), null); // The saved-video store's pointer: the container is [0, its duration]. const store = path.join(root, "saved-videos", SLUG, ID); await mkdir(store, { recursive: true }); await copyFile(SMALL, path.join(store, "source-media.mp4")); await writeFile(path.join(videoDir, "saved-video.json"), JSON.stringify({ dir: store, file: "source-media.mp4" })); // 6 s does not reach 11–13. assert.equal(await ask(), null); const early = await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span: { from: 1, to: 3 }, audio: false }); assert.equal(early?.kind, "saved-video"); assert.equal(early?.windowStart, 0); // A window named for the seconds it holds. await mkdir(path.join(videoDir, "clips"), { recursive: true }); await copyFile(SMALL, path.join(videoDir, "clips", "10.00-16.00.mp4")); const win = await ask(); assert.equal(win?.kind, "corpus-window"); assert.equal(win?.windowStart, 10); // Both hold 1–3 once a window does: the window wins. await copyFile(SMALL, path.join(videoDir, "clips", "0.00-6.00.mp4")); const both = await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span: { from: 1, to: 3 }, audio: false }); assert.equal(both?.kind, "corpus-window"); }); test("tiers: the sound is asked only when allowed, and never beats a picture", async () => { const { channelsDir, videoDir } = await corpus(); const span = { from: 1, to: 3 }; await copyFile(SOUND, path.join(videoDir, "audio.m4a")); assert.equal(await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span, audio: false }), null); assert.equal((await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span, audio: true }))?.kind, "audio"); await mkdir(path.join(videoDir, "clips"), { recursive: true }); await copyFile(SMALL, path.join(videoDir, "clips", "0.00-6.00.mp4")); assert.equal( (await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span, audio: true }))?.kind, "corpus-window", ); }); test("tiers: a dangling link (an unmounted drive) is not there — never a throw — and the next tier answers", async () => { const { channelsDir, videoDir, root } = await corpus(); const span = { from: 1, to: 3 }; await mkdir(path.join(videoDir, "clips"), { recursive: true }); await symlink(path.join(root, "gone", "0.00-6.00.mp4"), path.join(videoDir, "clips", "0.00-6.00.mp4")); // The media tier's relative link into media/, itself pointing nowhere. await symlink("../../media/abc123/source-media.mp4", path.join(videoDir, "source-media.mp4")); assert.equal(await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span, audio: false }), null); // A live relative link into media/ is followed. const media = path.join(channelsDir, SLUG, "media", ID); await mkdir(media, { recursive: true }); await copyFile(SMALL, path.join(media, "source-media.mp4")); const hit = await resolveEvidenceSource({ channelsDir, slug: SLUG, id: ID, span, audio: false }); assert.equal(hit?.kind, "saved-video"); assert.equal(hit?.name, "source-media.mp4"); }); test("the cut: the span exactly, a small source keeps its size, metadata dropped", async () => { const { channelsDir, videoDir, cacheDir } = await corpus(); await mkdir(path.join(videoDir, "clips"), { recursive: true }); await copyFile(SMALL, path.join(videoDir, "clips", "10.00-16.00.mp4")); const r = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span: { from: 11, to: 13.5 }, cacheDir, }); assert.ok(r.ok, JSON.stringify(r)); assert.equal(r.cached, false); assert.equal(r.media.kind, "video"); assert.match(r.media.file, /^[0-9a-f]{32}\.mp4$/); assert.equal(r.media.width, 160); assert.equal(r.media.height, 90); assert.ok(Math.abs((r.media.durationSec ?? 0) - 2.5) < 0.15, `duration ${r.media.durationSec}`); assert.match(r.media.sha256, /^[0-9a-f]{64}$/); const probe = await probeMedia(path.join(cacheDir, r.media.file)); assert.equal(probe?.hasAudio, true); // The clip and its sidecar, and no temp file left behind. assert.deepEqual((await readdir(cacheDir)).sort(), [r.media.file, r.media.file.replace(/\.mp4$/, ".json")].sort()); }); test("the cut: a larger source is fitted inside 1280×720, sides even", async () => { const { channelsDir, videoDir, cacheDir } = await corpus(); await mkdir(path.join(videoDir, "clips"), { recursive: true }); await copyFile(WIDE, path.join(videoDir, "clips", "0.00-1.00.mp4")); const r = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span: { from: 0, to: 1 }, cacheDir }); assert.ok(r.ok, JSON.stringify(r)); assert.equal(r.media.width, 1280); assert.equal(r.media.height, 720); const args = evidenceCutArgs("in.mp4", 1, 2, "video", "out"); assert.ok(args.includes("libx264") && args.includes("23") && args.includes("+faststart")); assert.match(args[args.indexOf("-vf") + 1], /min\(1280,iw\).*min\(720,ih\).*force_divisible_by=2/); }); test("audio: an audio citation is an .m4a with no picture; a video citation of a sound-only record too", async () => { const { channelsDir, videoDir, cacheDir } = await corpus(); await copyFile(SOUND, path.join(videoDir, "audio.m4a")); const span = { from: 1, to: 3 }; const a = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "audio", span, cacheDir }); assert.ok(a.ok, JSON.stringify(a)); assert.equal(a.media.kind, "audio"); assert.match(a.media.file, /\.m4a$/); assert.equal(a.media.width, null); assert.equal((await probeMedia(path.join(cacheDir, a.media.file)))?.hasVideo, false); // A video citation does not reach the sound unless the record has no picture. const v = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span, cacheDir }); assert.equal(v.ok, false); assert.equal(!v.ok && v.reason, "missing"); const p = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span, cacheDir, audioOnlyRecord: true }); assert.ok(p.ok, JSON.stringify(p)); assert.equal(p.media.kind, "audio"); // The same source, span and kind: the same file. assert.equal(p.media.file, a.media.file); }); test("cache: a hit by source identity + span; a changed source or span is a new cut", async () => { const { channelsDir, videoDir, cacheDir } = await corpus(); await mkdir(path.join(videoDir, "clips"), { recursive: true }); const win = path.join(videoDir, "clips", "0.00-6.00.mp4"); await copyFile(SMALL, win); const span = { from: 1, to: 2 }; const first = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span, cacheDir }); const again = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span, cacheDir }); assert.ok(first.ok && again.ok); assert.equal(again.cached, true); assert.deepEqual(again.media, first.media); const other = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span: { from: 1, to: 2.5 }, cacheDir }); assert.ok(other.ok); assert.equal(other.cached, false); assert.notEqual(other.media.file, first.media.file); // A re-fetched window (a new mtime) is a new identity. await utimes(win, new Date(), new Date(Date.now() + 60_000)); const refetched = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span, cacheDir }); assert.ok(refetched.ok); assert.equal(refetched.cached, false); assert.notEqual(refetched.media.file, first.media.file); const id = { path: "/x/a.mp4", bytes: 10, mtimeMs: 5 }; assert.equal(evidenceHash(id, span, "video"), evidenceHash({ ...id }, { ...span }, "video")); assert.notEqual(evidenceHash(id, span, "video"), evidenceHash(id, span, "audio")); assert.notEqual(evidenceHash(id, span, "video"), evidenceHash({ ...id, bytes: 11 }, span, "video")); }); test("size: a clip over the limit is refused, with its size, and nothing is kept", async () => { const { channelsDir, videoDir, cacheDir } = await corpus(); await mkdir(path.join(videoDir, "clips"), { recursive: true }); await copyFile(SMALL, path.join(videoDir, "clips", "0.00-6.00.mp4")); const r = await prepareEvidenceClip({ channelsDir, slug: SLUG, id: ID, kind: "video", span: { from: 0, to: 4 }, cacheDir, maxBytes: 1000, }); assert.equal(r.ok, false); assert.equal(!r.ok && r.reason, "too-big"); assert.ok(!r.ok && (r.bytes ?? 0) > 1000); assert.deepEqual(await readdir(cacheDir), []); }); test("clampSpanToDuration: padding past a recording's end stops at the end; nothing else moves", async () => { const { clampSpanToDuration } = await import("./evidenceClip-server"); assert.deepEqual(clampSpanToDuration({ from: 189.02, to: 203.64 }, 201.48), { from: 189.02, to: 201.48 }); assert.deepEqual(clampSpanToDuration({ from: 10, to: 20 }, 201.48), { from: 10, to: 20 }); assert.deepEqual(clampSpanToDuration({ from: 202, to: 205 }, 201.48), { from: 202, to: 205 }, "a span starting past the end is not invented"); assert.deepEqual(clampSpanToDuration({ from: 189, to: 203 }, null), { from: 189, to: 203 }); });