Archilyzer · Source

archilyzer

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

commit 24caf1ef46902b2810e47859060f7072f1c9b055
parent 918934a32e67a428de3dff59f8e78bb4c940e032
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 18 Sep 2026 17:59:13 -0400

report-to-video: a still's header line, in the same module as a clip's

An image entry has no moment inside it to cite, so the clock a clip carries
would point at nothing. imageAttributionLine() is the same ` · ` shape without
it, and the empty string when there is nothing to say -- which the renderer
will read as "draw no header", because an empty textfile is the one thing
drawtext refuses outright.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mumtool/report-to-video/attribution.mjs | 24++++++++++++++++++++++++
Aumtool/report-to-video/attribution.test.mjs | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 107 insertions(+), 0 deletions(-)

diff --git a/umtool/report-to-video/attribution.mjs b/umtool/report-to-video/attribution.mjs @@ -71,3 +71,27 @@ export function attributionLine(entry, meta) { const date = entry.date ?? uploadDateToIso(meta.uploadDate); return `${title} · ${date} @ ${hms(entry.cite ?? entry.start ?? 0)}`; } + +/** + * The same header line, for a still IMAGE entry. + * + * An image has no clock: there is no moment inside it to cite, so the ` @ h:mm:ss` + * a clip carries would be a number pointing at nothing. What is left is the + * author's own caption and, when the picture is dated, its date -- in the same + * ` · ` shape, so a still sitting between two clips reads as one more line of the + * same header rather than as a different program's output. + * + * The title is NOT run through `cleanTitle`: a clip's title comes from an + * archive record full of emoji and !commands, but an image's is typed by the + * author for this cut and is already the line they meant. + * + * Both fields absent yields "", which the renderer reads as "draw no header" -- + * an empty textfile is the one thing drawtext refuses outright. + * + * @param {{ title?: string|null, date?: string|null }} entry + */ +export function imageAttributionLine(entry) { + const title = String(entry?.title ?? "").trim(); + const date = String(entry?.date ?? "").trim(); + return [title, date].filter(Boolean).join(" · "); +} diff --git a/umtool/report-to-video/attribution.test.mjs b/umtool/report-to-video/attribution.test.mjs @@ -0,0 +1,83 @@ +// Tests for attribution.mjs — the one definition of the burned-in header line. +// +// Three programs draw or promise this string (the renderer, the clip bench's +// preview, the manifest writer's validation), so it is worth pinning to the +// character. +// +// Run with: pnpm test:scripts +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + attributionLine, + cleanTitle, + hms, + imageAttributionLine, + isCalendarDate, + uploadDateToIso, +} from "./attribution.mjs"; + +test("hms: hours only when there are any", () => { + assert.equal(hms(0), "0:00"); + assert.equal(hms(61), "1:01"); + assert.equal(hms(3599), "59:59"); + assert.equal(hms(3600), "1:00:00"); + assert.equal(hms(32989.9), "9:09:49"); +}); + +test("attributionLine: a clip carries a clock", () => { + assert.equal( + attributionLine({ title: "Monday Mail #11", date: "2016-09-12", cite: 32989 }, {}), + "Monday Mail #11 · 2016-09-12 @ 9:09:49", + ); + assert.equal( + attributionLine({ start: 10 }, { title: "Stream 🙂 !socials", uploadDate: "20250101" }), + "Stream · 2025-01-01 @ 0:10", + ); +}); + +test("imageAttributionLine: title and date", () => { + assert.equal( + imageAttributionLine({ + title: "Bx (@bx_on_x) on X, September 2026 — 1/4", + date: "2026-08-19", + }), + "Bx (@bx_on_x) on X, September 2026 — 1/4 · 2026-08-19", + ); +}); + +test("imageAttributionLine: no date, no separator", () => { + assert.equal( + imageAttributionLine({ title: "1st pic — Substack profile" }), + "1st pic — Substack profile", + ); + // A still has no moment to cite, so there is never a clock on it. + assert.doesNotMatch(imageAttributionLine({ title: "x", date: "2026-08-19" }), /@/); +}); + +test("imageAttributionLine: a date alone still reads", () => { + assert.equal(imageAttributionLine({ date: "2026-08-19" }), "2026-08-19"); +}); + +test("imageAttributionLine: nothing to say is the empty string", () => { + // The renderer reads "" as "draw no header": an empty textfile is the one + // thing drawtext refuses outright. + assert.equal(imageAttributionLine({}), ""); + assert.equal(imageAttributionLine({ title: " ", date: null }), ""); + assert.equal(imageAttributionLine(undefined), ""); +}); + +test("imageAttributionLine: the author's title is not cleaned", () => { + // An @handle is the caption's subject here, not a chat !command to strip. + const line = imageAttributionLine({ title: "Bx (@bx_on_x) on X" }); + assert.equal(line, "Bx (@bx_on_x) on X"); + assert.notEqual(line, cleanTitle("Bx (@bx_on_x) on X")); +}); + +test("isCalendarDate / uploadDateToIso", () => { + assert.equal(isCalendarDate("2026-08-19"), true); + assert.equal(isCalendarDate("2025-02-31"), false); + assert.equal(isCalendarDate("20250101"), false); + assert.equal(uploadDateToIso("20250101"), "2025-01-01"); + assert.equal(uploadDateToIso("2025-01-01"), "2025-01-01"); +});