Archilyzer · Source

archilyzer

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

commit 5af43e79c98a2245bb1f2ea28ce6299ac46fb984
parent 1869615e0c4dc8fa0d59f1e016fd9a13ca7b6d11
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 22:43:40 -0400

report-to-video: every chrome page states pageDuration -- its whole frames floored to 4 decimals -- so HyperFrames' ceil(duration x fps) is the build's frame count; a cut of 434 frames (schedule 14.467 s) rendered 435 deck frames and the build refused it. A duration on the grid (14.4, 7.9, 356.7) is written as before; tests

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

Diffstat:
Mumtool/report-to-video/chrome-deck.mjs | 6+++---
Mumtool/report-to-video/chrome-feed.mjs | 6+++---
Mumtool/report-to-video/chrome-posts.mjs | 6+++---
Mumtool/report-to-video/chrome-teaser.mjs | 6+++---
Mumtool/report-to-video/deck.mjs | 12++++++++++++
Mumtool/report-to-video/deck.test.mjs | 29+++++++++++++++++++++++++++++
6 files changed, 53 insertions(+), 12 deletions(-)

diff --git a/umtool/report-to-video/chrome-deck.mjs b/umtool/report-to-video/chrome-deck.mjs @@ -37,7 +37,7 @@ // second clock or a label. import { fileURLToPath } from "node:url"; -import { deckChoreography, deckLayout, pipSegments, pipXs, resolveDeck } from "./deck.mjs"; +import { deckChoreography, deckLayout, pageDuration, pipSegments, pipXs, resolveDeck } from "./deck.mjs"; /** * GSAP, vendored, for every chrome region. @@ -474,9 +474,9 @@ export function deckHtml(schedule, render, opts = {}) { </style> </head> <body> - <div id="root" data-composition-id="deck" data-start="0" data-duration="${r4(dur)}" + <div id="root" data-composition-id="deck" data-start="0" data-duration="${pageDuration(dur, schedule.fps ?? render.fps ?? 30)}" data-width="${W}" data-height="${H}"> - <div id="deck-clip" class="clip" data-start="0" data-duration="${r4(dur)}" data-track-index="1"> + <div id="deck-clip" class="clip" data-start="0" data-duration="${pageDuration(dur, schedule.fps ?? render.fps ?? 30)}" data-track-index="1"> <div class="panel" data-k="panel"> <div class="ground"></div> ${panel && qr ? `<div class="plate"></div>` : ""} diff --git a/umtool/report-to-video/chrome-feed.mjs b/umtool/report-to-video/chrome-feed.mjs @@ -33,7 +33,7 @@ // fixed name (`embedFn`, docs/quirks.md); the tests call the same function // with heights of their choosing. Every cue is a fromTo whose FROM is stated: // a render is a seek per frame, from parallel workers, in any order. -import { deckChoreography, feedGeometry, resolveDeck } from "./deck.mjs"; +import { deckChoreography, feedGeometry, pageDuration, resolveDeck } from "./deck.mjs"; import { mix, rgba } from "./chrome-deck.mjs"; import { embedFn, PLATFORM_LABEL, postDate, postParagraphs, postWho } from "./chrome-posts.mjs"; @@ -375,9 +375,9 @@ export function feedHtml(schedule, render, opts = {}) { </style> </head> <body> - <div id="root" data-composition-id="feed" data-start="0" data-duration="${dur}" + <div id="root" data-composition-id="feed" data-start="0" data-duration="${pageDuration(dur, schedule.fps ?? render.fps ?? 30)}" data-width="${W}" data-height="${H}"> - <div id="feed-clip" class="clip" data-start="0" data-duration="${dur}" data-track-index="1"> + <div id="feed-clip" class="clip" data-start="0" data-duration="${pageDuration(dur, schedule.fps ?? render.fps ?? 30)}" data-track-index="1"> <div class="col" data-k="col"> <div class="ground"></div> ${panel ? `<div class="edge"></div>` : ""} diff --git a/umtool/report-to-video/chrome-posts.mjs b/umtool/report-to-video/chrome-posts.mjs @@ -32,7 +32,7 @@ // Every cue is a fromTo whose FROM is stated, for the deck's reason: a render // is a seek per frame, from parallel workers, in any order. import { formatDeckDate } from "./attribution.mjs"; -import { postsGeometry, resolveDeck, snapWindow } from "./deck.mjs"; +import { pageDuration, postsGeometry, resolveDeck, snapWindow } from "./deck.mjs"; import { mix, rgba } from "./chrome-deck.mjs"; // The window arithmetic is deck.mjs's (pure, and loaded by the build without @@ -339,9 +339,9 @@ export function postsHtml(schedule, render, window, opts = {}) { </style> </head> <body> - <div id="root" data-composition-id="posts" data-start="0" data-duration="${dur}" + <div id="root" data-composition-id="posts" data-start="0" data-duration="${pageDuration(dur, schedule.fps ?? render.fps ?? 30)}" data-width="${W}" data-height="${H}" data-segment="${esc(window.segment)}"> - <div id="posts-clip" class="clip" data-start="0" data-duration="${dur}" data-track-index="1"> + <div id="posts-clip" class="clip" data-start="0" data-duration="${pageDuration(dur, schedule.fps ?? render.fps ?? 30)}" data-track-index="1"> <div class="stack" data-k="stack"> ${cardHtml} </div> diff --git a/umtool/report-to-video/chrome-teaser.mjs b/umtool/report-to-video/chrome-teaser.mjs @@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url"; import { - DIP_RISE, dipOf, TEASER_MOTION, teaserLead, teaserLines, teaserMotionOf, teaserSeconds, teaserTail, teaserTimes, + DIP_RISE, dipOf, pageDuration, TEASER_MOTION, teaserLead, teaserLines, teaserMotionOf, teaserSeconds, teaserTail, teaserTimes, transitionOf, } from "./deck.mjs"; @@ -368,9 +368,9 @@ export function teaserHtml(entry, render, opts = {}) { </style> </head> <body> - <div id="root" data-composition-id="teaser" data-start="0" data-duration="${r4(seconds)}" + <div id="root" data-composition-id="teaser" data-start="0" data-duration="${pageDuration(seconds, render.fps ?? 30)}" data-width="${W}" data-height="${H}" data-entry="${esc(entry.id)}"> - <div id="teaser-clip" class="clip" data-start="0" data-duration="${r4(seconds)}" data-track-index="1"> + <div id="teaser-clip" class="clip" data-start="0" data-duration="${pageDuration(seconds, render.fps ?? 30)}" data-track-index="1"> <div class="ground"></div> <div class="stage" data-k="stage"> <div class="leak" data-k="leak"></div>${dipped ? '\n <div class="veil" data-k="veil"></div>' : ""} diff --git a/umtool/report-to-video/deck.mjs b/umtool/report-to-video/deck.mjs @@ -1590,6 +1590,18 @@ export function chromeCacheKey({ html, assets = [], fps, frames, version }) { export const frameCount = (total, fps) => Math.round(total * fps); /** + * The `data-duration` a page states for `seconds` at `fps`: its whole frames + * (`frameCount`) back in seconds, FLOORED to 4 decimals. HyperFrames renders + * ceil(duration × fps) frames, so a duration rounded UP past a frame boundary + * -- a cut of 434 frames is 14.4667 s, and its schedule's 14.467 -- rendered + * one frame more than the build expects (435) and the build refused it. A + * duration already on the 4-decimal grid of whole frames (14.4, 7.9, 356.7) is + * written as it always was. + */ +export const pageDuration = (seconds, fps) => + Math.floor((frameCount(seconds, fps) / fps) * 10000 + 1e-6) / 10000; + +/** * How to invoke HyperFrames. `HYPERFRAMES_BIN` (an executable taking the CLI's * own arguments, e.g. an e2e stub) wins; else `npx --yes <HYPERFRAMES_PKG>`, * pinned by default. `version` is what the cache key records. diff --git a/umtool/report-to-video/deck.test.mjs b/umtool/report-to-video/deck.test.mjs @@ -413,3 +413,32 @@ test("posts shift: the footage moves aside from the column while a clip's posts assert.match(validateChrome({ ...CHROME, deck: { posts: { shift: { speed: 1 } } } }, RENDER)[0], /speed is not a deck setting/); assert.match(validateChrome({ ...CHROME, deck: { posts: { hold: 20 } } }, RENDER)[0], /hold/); }); + +import { pageDuration } from "./deck.mjs"; +import { deckHtml } from "./chrome-deck.mjs"; + +test("pageDuration: whole frames, floored to 4 decimals, so HyperFrames' ceil(duration × fps) is the build's frame count", () => { + // On the grid: written as before. + for (const v of [14.4, 7.9, 356.7, 7, 0.5, 14.2]) assert.equal(pageDuration(v, 30), v); + // 434 frames is 14.4667 s, and a schedule writes 14.467: both read as 434 frames, never 435. + for (const v of [14.467, 14.4667, 434 / 30]) { + const d = pageDuration(v, 30); + assert.equal(d, 14.4666); + assert.equal(Math.ceil(d * 30), 434); + assert.equal(frameCount(d, 30), frameCount(v, 30)); + } + assert.equal(pageDuration(14.767, 30), 14.7666); + assert.equal(Math.ceil(pageDuration(14.633, 30) * 30), 439); + assert.equal(pageDuration(10.04, 25), 10.04); +}); + +test("the deck page states pageDuration of the cut, not its millisecond total", () => { + const timeline = [ + { id: "a", type: "clip", video: "v", start: 0, end: 7.0667 }, + { id: "b", type: "clip", video: "v", start: 10, end: 17.9 }, + ]; + const R = { ...RENDER, palette: { bg: "#12101a", fg: "#f4f1ea", muted: "#9a93ad", accent: "#a97bff", amber: "#ffc860" } }; + const s = deckSchedule({ entries: timeline, durs: [212 / 30, 7.9], D: 0.5, render: R, provenance: PROV }); + assert.equal(s.total, 14.467); + assert.match(deckHtml(s, R), /data-composition-id="deck" data-start="0" data-duration="14\.4666"/); +});