Archilyzer · Source

archilyzer

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

commit de476ce02f9103b49af12f2487f0f479e7a5ab5c
parent e4b6c190b4b2c03c97d3cda97cbdfb952b775a9c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 03:21:56 -0400

umtool: endCard null reads as off, so a twice-resolved render keeps it off; the header mark is cached per drawing (a hash of the kit SVG in its name), not per size; doctor lists rsvg-convert as needed by the brand preset (S4 review)

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

Diffstat:
Mumtool/lib/tools.mjs | 2+-
Mumtool/report-to-video/brand-cards.mjs | 14+++++++++++---
Mumtool/report-to-video/brand.mjs | 6++++--
Mumtool/report-to-video/brand.test.mjs | 21++++++++++++++++++++-
4 files changed, 36 insertions(+), 7 deletions(-)

diff --git a/umtool/lib/tools.mjs b/umtool/lib/tools.mjs @@ -38,7 +38,7 @@ export const TOOLS = () => [ { id: "yt-dlp", bin: process.env.YTDLP_BIN ?? "yt-dlp", args: ["--version"], neededBy: ["report-video fetch", "check-sources"], required: true }, { id: "qrencode", bin: process.env.QRENCODE_BIN ?? "qrencode", args: ["--version"], neededBy: ["report-video QR"], required: true }, { id: "imagemagick", bin: "magick", args: ["-version"], fallback: "convert", neededBy: ["report-video cards, rail, chrome"], required: false }, - { id: "rsvg-convert", bin: process.env.RSVG_BIN ?? "rsvg-convert", args: ["--version"], neededBy: ["report-video chart"], required: false }, + { id: "rsvg-convert", bin: process.env.RSVG_BIN ?? "rsvg-convert", args: ["--version"], neededBy: ["report-video chart", "brand preset"], required: false }, { id: "python", bin: facedetPython(), args: ["--version"], neededBy: ["faces (facecrop.py)"], required: false }, { id: "facecrop.py", file: facecropPy(), neededBy: ["faces"], required: false }, ]; diff --git a/umtool/report-to-video/brand-cards.mjs b/umtool/report-to-video/brand-cards.mjs @@ -16,6 +16,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { access, mkdir, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; import path from "node:path"; import { brandFaces, brandKit, childOpts } from "./brand.mjs"; @@ -81,12 +82,19 @@ export async function lockupPng(render, px, dir) { }; } -/** Mark M2 (`any`: rounded square) as a `size` px PNG, once per directory. */ +/** + * Mark M2 (`any`: rounded square) as a `size` px PNG. Rendered once per + * directory per DRAWING: the name carries a hash of the kit's SVG, so a kit + * regenerated by `brand-media.ts --video-kit` is never answered by a stale + * PNG left in out/<variant>/cards. + */ export async function markPng(render, size, dir) { - const file = path.join(dir, `_mark-${size}.png`); + const svg = brandKit(render.brand).mark.any; + const key = createHash("sha256").update(svg).digest("hex").slice(0, 12); + const file = path.join(dir, `_mark-${size}-${key}.png`); if (!(await exists(file))) { await mkdir(dir, { recursive: true }); - await svgPng(render, brandKit(render.brand).mark.any, path.join(dir, `_mark-${size}.svg`), file, size / 512); + await svgPng(render, svg, path.join(dir, `_mark-${size}-${key}.svg`), file, size / 512); } return file; } diff --git a/umtool/report-to-video/brand.mjs b/umtool/report-to-video/brand.mjs @@ -75,11 +75,13 @@ export function brandKit(id) { /** * `render.endCard` as the preset reads it: `false` turns the card off, a number - * or `{ seconds, url }` configures it, absent is the default 20 s. + * or `{ seconds, url }` configures it, absent is the default 20 s. `null` is + * OFF too: it is what this function returns for `false`, so resolving a render + * block twice leaves an ended-off card off. * @returns {{ seconds: number, url: string } | null} */ export function endCardConfig(endCard) { - if (endCard === false) return null; + if (endCard === false || endCard === null) return null; const o = typeof endCard === "number" ? { seconds: endCard } : (endCard ?? {}); const seconds = o.seconds ?? END_CARD_DEFAULT_SECONDS; if (!Number.isFinite(seconds) || seconds <= 0) { diff --git a/umtool/report-to-video/brand.test.mjs b/umtool/report-to-video/brand.test.mjs @@ -20,7 +20,7 @@ import { } from "./brand.mjs"; import { headerFilters, selectVariant } from "./build-video.mjs"; import { renderCard } from "./render-cards.mjs"; -import { renderThumbnail, THUMB } from "./brand-cards.mjs"; +import { markPng, renderThumbnail, THUMB } from "./brand-cards.mjs"; const PLAIN_RENDER = { width: 1920, height: 1080, fps: 30, headerHeight: 56, @@ -82,6 +82,19 @@ test("endCardConfig: default 20 s, a number, an object, or false", () => { assert.throws(() => endCardConfig({ seconds: 0 }), /positive/); }); +test("endCard false stays off when a render block is resolved twice", () => { + assert.equal(endCardConfig(null), null); + const branded = { ...PLAIN_RENDER, brand: "archilyzer-media" }; + const once = resolveBrandRender({ ...branded, endCard: false }); + assert.equal(once.endCard, null); + const twice = resolveBrandRender(once); + assert.equal(twice.endCard, null); + assert.equal(brandManifest(manifest(twice)).timeline.length, 1); + // A configured card resolves to itself. + const on = resolveBrandRender({ ...branded, endCard: { seconds: 12 } }); + assert.deepEqual(resolveBrandRender(on).endCard, on.endCard); +}); + test("brandManifest appends one end card, clear of the rail, unless told otherwise", () => { const branded = { ...PLAIN_RENDER, brand: "archilyzer-media" }; const m = brandManifest(manifest(branded)); @@ -170,6 +183,12 @@ test("rendered: title and end cards at the frame size, the thumbnail at 1280 x 7 "magick", [out, "-crop", "110x50+1170+670", "+repage", "-format", "%k", "info:"], { encoding: "utf8" }, ); assert.equal(corner, "1"); + // The header mark is cached per DRAWING, not per size: its name carries + // the kit SVG's hash, so a regenerated kit cannot be answered by a stale PNG. + const mark = await markPng(render, 34, dir); + assert.match(path.basename(mark), /^_mark-34-[0-9a-f]{12}\.png$/); + assert.equal(size(mark), "34 34"); + assert.equal(await markPng(render, 34, dir), mark); } finally { rmSync(dir, { recursive: true, force: true }); }