Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 13998fd903f461d9b4fab6063297af6fcaa28f3e
parent 320eda0583d00580cbde2e13d52fb20d50777ac4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun,  4 Oct 2026 17:13:33 -0400

report-to-video: tests for src/cues and the audio poster; the poster's waveform is bounded and drawn at full colour

The waveform overlay passes the still through at its end (eof_action=pass):
the default repeat handed the cut's fps filter a frame stamped far ahead,
and the segment ran for an hour behind two seconds of sound.

local-media.test.mjs: both cue shapes normalise identically, the path rules,
validation naming each entry, the reader's metadata, a missing src refused
before the build runs. audio-poster.test.mjs: a lavfi mp3 as a src clip
builds a 320x180 segment whose stream matches a card's, with the poster at
the picture box's size and a moving waveform; under the deck the poster is
the footage box's size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Aumtool/report-to-video/audio-poster.test.mjs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/report-to-video/build-video.mjs | 7+++++--
Aumtool/report-to-video/local-media.test.mjs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 334 insertions(+), 2 deletions(-)

diff --git a/umtool/report-to-video/audio-poster.test.mjs b/umtool/report-to-video/audio-poster.test.mjs @@ -0,0 +1,159 @@ +// A clip whose source is SOUND ALONE -- here a `src` mp3 beside the manifest -- +// builds a segment with a picture: a poster at the picture box's size, the +// waveform moving along its foot, and a stream the concat cannot tell from any +// other segment's (size, fps, pixel format, SAR, audio layout). +// +// Real encodes, kept tiny: a three-second lavfi mp3 and a 320x180 cut. +// +// Run with: pnpm test:scripts +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { audioPosterParts, buildVideo, deckFraming, posterWaveGeometry } from "./build-video.mjs"; +import { MONO_FONT_FILE } from "./brand.mjs"; + +const have = ["ffmpeg", "ffprobe", "magick"].every((b) => spawnSync(b, ["-version"]).status === 0); +const PALETTE = { bg: "#15121c", fg: "#ece8f4", muted: "#9a93ad", accent: "#7c5cff", amber: "#f2b84b" }; +const RENDER = { + width: 320, height: 180, fps: 30, audioRate: 48000, audioChannels: 2, preset: "ultrafast", + transition: 0, headerHeight: 24, palette: PALETTE, fontRegular: MONO_FONT_FILE, +}; + +function probe(file) { + const r = spawnSync("ffprobe", ["-v", "error", "-show_streams", "-show_format", "-of", "json", file]); + assert.equal(r.status, 0, String(r.stderr)); + const doc = JSON.parse(String(r.stdout)); + const v = doc.streams.find((s) => s.codec_type === "video"); + const a = doc.streams.find((s) => s.codec_type === "audio"); + return { v, a, duration: Number(doc.format.duration) }; +} + +/** What a stream-copy concat needs to be equal between two segments. */ +const concatKey = ({ v, a }) => ({ + vcodec: v.codec_name, width: v.width, height: v.height, pix_fmt: v.pix_fmt, + fps: v.r_frame_rate, sar: v.sample_aspect_ratio ?? "1:1", tb: v.time_base, + acodec: a.codec_name, rate: a.sample_rate, channels: a.channels, layout: a.channel_layout, +}); + +async function project(render = RENDER) { + const root = await mkdtemp(path.join(os.tmpdir(), "rtv-audio-poster-")); + await mkdir(path.join(root, "media"), { recursive: true }); + // Three seconds of a tone whose loudness swells: a waveform that moves. + const mp3 = path.join(root, "media", "episode.mp3"); + const r = spawnSync("ffmpeg", [ + "-v", "error", "-y", "-f", "lavfi", "-i", "sine=f=220:r=44100:d=3", + "-af", "volume='0.2+0.8*t/3':eval=frame", "-c:a", "libmp3lame", "-b:a", "64k", mp3, + ]); + assert.equal(r.status, 0, String(r.stderr)); + await writeFile(path.join(root, "media", "episode.json"), JSON.stringify({ + duration_seconds: 3, + chunk_data: [{ start_time: 0.2, end_time: 2.8, text: "A tone, held." }], + })); + const manifestPath = path.join(root, "video.manifest.json"); + await writeFile(manifestPath, JSON.stringify({ + slug: "demo", + title: "Demo", + render, + timeline: [ + { id: "t0", type: "card", style: "chapter", heading: "A card", seconds: 1 }, + { + id: "p1", type: "clip", src: "media/episode.mp3", cues: "media/episode.json", + start: 0.5, end: 2.5, title: "Episode twelve", channelTitle: "Demo Feed", date: "2026-01-02", + }, + ], + })); + return { root, manifestPath }; +} + +test("posterWaveGeometry: even, inside the card's margins, along the foot", () => { + const g = posterWaveGeometry(1574, 886); + assert.equal(g.width % 2, 0); + assert.equal(g.height % 2, 0); + assert.ok(g.x > 0 && g.x + g.width <= 1574); + assert.ok(g.y > 886 / 2 && g.y + g.height < 886); +}); + +test("audioPosterParts: the still alone, or the sound's waveform over it", () => { + const box = { width: 320, height: 156 }; + assert.deepEqual( + audioPosterParts({ posterIdx: 3, box, render: { ...RENDER, audioPoster: { waveform: false } } }), + ["[3:v]null[pic]"], + ); + const parts = audioPosterParts({ posterIdx: 1, box, render: RENDER }); + const g = posterWaveGeometry(320, 156); + assert.equal(parts.length, 2); + assert.match(parts[0], /^\[0:a\]aformat=channel_layouts=mono,showwaves=s=\d+x\d+:mode=cline:draw=full:rate=30:colors=#7c5cff\[wave\]$/); + assert.equal(parts[1], `[1:v][wave]overlay=x=${g.x}:y=${g.y}:eof_action=pass[pic]`); +}); + +test("an audio-only src clip builds a poster segment the concat cannot tell from a card's", { skip: !have && "needs ffmpeg, ffprobe and magick" }, async () => { + const { root, manifestPath } = await project(); + try { + const clip = await buildVideo({ manifestPath, only: "p1", opts: { noNetwork: true } }); + assert.equal(clip.failures.length, 0); + const card = await buildVideo({ manifestPath, only: "t0", opts: { noNetwork: true } }); + + const c = probe(clip.out); + assert.ok(c.v, "the segment has a picture"); + assert.equal(c.v.width, 320); + assert.equal(c.v.height, 180); + assert.equal(c.v.r_frame_rate, "30/1"); + assert.equal(c.v.pix_fmt, "yuv420p"); + assert.equal(c.a.channels, 2); + assert.equal(c.a.sample_rate, "48000"); + assert.deepEqual(concatKey(c), concatKey(probe(card.out))); + // About the played window, snapped (start 0.5 → 2.5 of a 3 s file), and the + // picture is no longer than the sound. + assert.ok(c.duration > 1 && c.duration <= 3.1, `duration ${c.duration}`); + assert.ok(Number(c.v.duration) <= Number(c.a.duration) + 1 / 30 + 0.03, `${c.v.duration} vs ${c.a.duration}`); + + // The poster was drawn at the picture box's size: the frame less the header. + const still = probe(path.join(root, "out", "sourced", "cards", "p1.poster.png")); + assert.equal(still.v.width, 320); + assert.equal(still.v.height, 180 - 24); + + // The waveform moves: the frames are not one still. + const md5 = spawnSync("ffmpeg", ["-v", "error", "-i", clip.out, "-map", "0:v", "-f", "framemd5", "-"]); + assert.equal(md5.status, 0, String(md5.stderr)); + const hashes = new Set(String(md5.stdout).split("\n").filter((l) => l && !l.startsWith("#")).map((l) => l.split(",").pop().trim())); + assert.ok(hashes.size > 10, `${hashes.size} distinct frames`); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("under the deck, the poster is framed into the footage box like any footage", { skip: !have && "needs ffmpeg, ffprobe and magick" }, async () => { + const { root, manifestPath } = await project({ + ...RENDER, width: 1920, height: 1080, headerHeight: undefined, + chrome: { engine: "hyperframes", layout: "deck", deck: {} }, + audioPoster: { waveform: false }, + }); + try { + const clip = await buildVideo({ manifestPath, only: "p1", opts: { noNetwork: true } }); + const c = probe(clip.out); + assert.equal(c.v.width, 1920); + assert.equal(c.v.height, 1080); + assert.equal(c.v.r_frame_rate, "30/1"); + const still = probe(path.join(root, "out", "sourced", "cards", "p1.poster.png")); + const box = deckFraming({ ...RENDER, width: 1920, height: 1080, chrome: { engine: "hyperframes", layout: "deck", deck: {} } }).box; + assert.equal(still.v.width, box.width); + assert.equal(still.v.height, box.height); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("--fetch-only on a src clip has nothing to fetch", async () => { + if (!have) return; + const { root, manifestPath } = await project(); + try { + const r = await buildVideo({ manifestPath, fetchOnly: "p1", opts: {} }); + assert.equal(r.out, null); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/umtool/report-to-video/build-video.mjs b/umtool/report-to-video/build-video.mjs @@ -546,8 +546,11 @@ export function audioPosterParts({ posterIdx, box, render }) { const g = posterWaveGeometry(box.width, box.height); return [ `[0:a]aformat=channel_layouts=mono,` + - `showwaves=s=${g.width}x${g.height}:mode=cline:rate=${render.fps}:colors=${render.palette.accent}[wave]`, - `[${posterIdx}:v][wave]overlay=x=${g.x}:y=${g.y}[pic]`, + `showwaves=s=${g.width}x${g.height}:mode=cline:draw=full:rate=${render.fps}:colors=${render.palette.accent}[wave]`, + // `eof_action=pass`: the still runs on bare if the waveform ends a frame + // early. The default (repeat) hands the cut's `fps` a frame stamped far in + // the future, and it fills the gap -- an hour of poster behind 2 s of sound. + `[${posterIdx}:v][wave]overlay=x=${g.x}:y=${g.y}:eof_action=pass[pic]`, ]; } diff --git a/umtool/report-to-video/local-media.test.mjs b/umtool/report-to-video/local-media.test.mjs @@ -0,0 +1,170 @@ +// Tests for local-media.mjs: a clip whose media and cues are files beside the +// manifest (`src`, `cues`) -- the two cue shapes, the path rules, validation, +// and the reader the build and resolve-windows hold. +// +// Run with: pnpm test:scripts +import assert from "node:assert/strict"; +import test from "node:test"; +import path from "node:path"; +import os from "node:os"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; + +import { + clipLabel, createLocalMedia, hasLocalMedia, normaliseCueDoc, resolveManifestPath, validateLocalMedia, +} from "./local-media.mjs"; +import { unresolvable } from "./resolve-windows.mjs"; +import { buildVideo } from "./build-video.mjs"; + +// The same three cues, in each shape. +const CORPUS = { + title: "Episode 12", channel: "Demo Feed", uploadDate: "20260102", duration: 12.5, platform: "podcast", + cues: [ + { start: 4, end: 7.25, text: "and then the second." }, + { start: 0.5, end: 3.9, text: "First thing." }, + { start: 7.5, end: 12, text: "The third one?" }, + ], +}; +const PARAKEET = { + duration_seconds: 12.5, + chunks: 3, + text: "First thing. and then the second. The third one?", + chunk_data: [ + { start_time: 0.5, end_time: 3.9, text: "First thing." }, + { start_time: 4, end_time: 7.25, text: "and then the second." }, + { start_time: 7.5, end_time: 12, text: "The third one?" }, + ], +}; + +async function dir() { + const root = await mkdtemp(path.join(os.tmpdir(), "rtv-local-media-")); + await mkdir(path.join(root, "media"), { recursive: true }); + return root; +} + +test("both cue shapes normalise to the same cues, in time order", () => { + const a = normaliseCueDoc(CORPUS); + const b = normaliseCueDoc(PARAKEET); + assert.deepEqual(a.cues, b.cues); + assert.deepEqual(a.cues.map((c) => c.start), [0.5, 4, 7.5]); + assert.equal(a.duration, 12.5); + assert.equal(b.duration, 12.5); + // The corpus shape carries the record's metadata; the parakeet one has none. + assert.equal(a.title, "Episode 12"); + assert.equal(a.platform, "podcast"); + assert.equal(b.title, undefined); +}); + +test("a cue with no finite start or end is dropped; neither shape is an error", () => { + const got = normaliseCueDoc({ cues: [{ start: 1, end: 2, text: "a" }, { start: "x", end: 3, text: "b" }, { start: 4 }] }); + assert.deepEqual(got.cues, [{ start: 1, end: 2, text: "a" }]); + assert.throws(() => normaliseCueDoc({ segments: [] }, "m/ep.json"), /m\/ep\.json: neither a transcript\.cues\.json/); + assert.throws(() => normaliseCueDoc(null), /not a cue document/); +}); + +test("resolveManifestPath: inside the manifest's directory, or absolute only with leave", () => { + const base = path.resolve("/srv/proj"); + assert.deepEqual(resolveManifestPath(base, "media/ep.mp3"), { file: path.join(base, "media", "ep.mp3") }); + assert.deepEqual(resolveManifestPath(base, "media/../ep.mp3"), { file: path.join(base, "ep.mp3") }); + assert.match(resolveManifestPath(base, "../other/ep.mp3").error, /climbs out of the manifest's directory/); + assert.match(resolveManifestPath(base, "media/../../ep.mp3").error, /climbs out/); + assert.match(resolveManifestPath(base, "..").error, /climbs out/); + // A name that merely starts with two dots is inside. + assert.deepEqual(resolveManifestPath(base, "..ep.mp3"), { file: path.join(base, "..ep.mp3") }); + assert.match(resolveManifestPath(base, "/mnt/feed/ep.mp3").error, /absolute; set render\.allowAbsoluteSrc/); + assert.deepEqual(resolveManifestPath(base, "/mnt/feed/ep.mp3", { allowAbsolute: true }), { file: "/mnt/feed/ep.mp3" }); + assert.match(resolveManifestPath(base, "").error, /non-empty/); +}); + +test("validateLocalMedia: every problem, each naming its entry", async () => { + const root = await dir(); + try { + await writeFile(path.join(root, "media", "ep.mp3"), "x"); + await writeFile(path.join(root, "media", "ep.json"), JSON.stringify(PARAKEET)); + await writeFile(path.join(root, "media", "bad.json"), JSON.stringify({ words: [] })); + const timeline = [ + { id: "ok", type: "clip", src: "media/ep.mp3", cues: "media/ep.json" }, + { id: "nofile", type: "clip", src: "media/missing.mp3" }, + { id: "up", type: "clip", src: "../ep.mp3" }, + { id: "abs", type: "clip", src: path.join(root, "media", "ep.mp3") }, + { id: "both", type: "clip", src: "media/ep.mp3", video: "abc123" }, + { id: "nocues", type: "clip", src: "media/ep.mp3", cues: "media/none.json" }, + { id: "badcues", type: "clip", src: "media/ep.mp3", cues: "media/bad.json" }, + { id: "orphan", type: "clip", video: "abc123", cues: "media/ep.json" }, + // Not this field: an image's `src` is a picture, and a card is no clip. + { id: "img", type: "image", src: "media/missing.png" }, + { id: "card", type: "card", src: "whatever" }, + ]; + const errors = await validateLocalMedia(timeline, root); + assert.equal(errors.length, 7, errors.join("\n")); + assert.match(errors[0], /^timeline\[1\] nofile: no media at .*missing\.mp3 \(src: media\/missing\.mp3\)$/); + assert.match(errors[1], /^timeline\[2\] up: src \.\.\/ep\.mp3 climbs out/); + assert.match(errors[2], /^timeline\[3\] abs: src .* is absolute/); + assert.match(errors[3], /^timeline\[4\] both: a clip takes `src` or `video`, not both$/); + assert.match(errors[4], /^timeline\[5\] nocues: no cue file at .*none\.json/); + assert.match(errors[5], /^timeline\[6\] badcues: media\/bad\.json: neither/); + assert.match(errors[6], /^timeline\[7\] orphan: `cues` belongs to a clip with `src`$/); + // With leave, the absolute one passes. + const abs = await validateLocalMedia([timeline[3]], root, { allowAbsolute: true }); + assert.deepEqual(abs, []); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("createLocalMedia: the file, the cues and the metadata a record would have given", async () => { + const root = await dir(); + try { + await writeFile(path.join(root, "media", "a.json"), JSON.stringify(CORPUS)); + await writeFile(path.join(root, "media", "b.json"), JSON.stringify(PARAKEET)); + const local = createLocalMedia({ baseDir: root }); + const a = { id: "a", type: "clip", src: "media/ep.mp3", cues: "media/a.json" }; + const b = { id: "b", type: "clip", src: "media/ep.mp3", cues: "media/b.json" }; + const bare = { id: "c", type: "clip", src: "media/episode-12.mp3" }; + assert.equal(local.file(a), path.join(root, "media", "ep.mp3")); + assert.deepEqual(await local.cues(a), await local.cues(b)); + assert.deepEqual(await local.cues(bare), []); + assert.deepEqual(await local.meta(a), { + title: "Episode 12", uploadDate: "20260102", webpageUrl: null, duration: 12.5, + channel: "Demo Feed", platform: "podcast", + }); + // No record: the file's own name is the title, and there is no page. + const m = await local.meta(bare); + assert.equal(m.title, "episode-12"); + assert.equal(m.webpageUrl, null); + assert.equal(hasLocalMedia(bare), true); + assert.equal(hasLocalMedia({ id: "v", type: "clip", video: "abc123" }), false); + assert.equal(clipLabel(bare), "media/episode-12.mp3"); + assert.equal(clipLabel({ video: "abc123", src: "x" }), "abc123"); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("resolve-windows leaves a src clip alone without cues or a window", () => { + assert.equal(unresolvable({ type: "clip", video: "abc123", start: 1, end: 2 }), null); + assert.equal(unresolvable({ type: "clip", src: "m.mp3", start: 1, end: 2 }), "no cues file"); + assert.equal(unresolvable({ type: "clip", src: "m.mp3", cues: "m.json" }), "no start/end (the whole file)"); + assert.equal(unresolvable({ type: "clip", src: "m.mp3", cues: "m.json", start: 1, end: 2 }), null); +}); + +test("buildVideo refuses a missing src before anything runs, naming the entry", async () => { + const root = await dir(); + try { + const manifestPath = path.join(root, "video.manifest.json"); + await writeFile(manifestPath, JSON.stringify({ + slug: "demo", + title: "Demo", + render: { width: 320, height: 180, fps: 30 }, + timeline: [ + { id: "t0", type: "card", style: "chapter", heading: "Hi" }, + { id: "p1", type: "clip", src: "media/gone.mp3" }, + ], + })); + await assert.rejects( + buildVideo({ manifestPath, opts: { noNetwork: true } }), + /^Error: manifest: timeline\[1\] p1: no media at .*gone\.mp3 \(src: media\/gone\.mp3\)$/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +});