Archilyzer · Source

archilyzer

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

commit 15e50d6e34e50c9e469e7f3186988087593b96f8
parent f01cc78fc7caea0d3426ce50da47525c7fa39d59
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 20 Sep 2026 17:43:17 -0400

bench: the walk visits the clips that need judgement and are fetched

prev/next -- and so the p/n keys, the two header links and the jump y makes
after it confirms -- are the nearest clip either side that still has no
verdict AND has a cached file holding its window. Walking onto a clip with
nothing to play is a dead end: there is nothing to judge and the only move is
to press n again. Walking back onto one already answered is the round trip a
walk exists to remove.

A URL still opens ANY clip. The bench renders it, says "not fetched yet" when
that is why it is off the walk, and its links point at the neighbours on
either side. Both pages carry "ready N of M needing judgement", and the
project page starts the walk where the walk actually goes.

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

Diffstat:
Mumtool/components/projects/ClipBench.tsx | 27++++++++++++++++++++++++++-
Mumtool/components/projects/ClipBenchPage.tsx | 31+++++++++++++++++++++++++++----
Mumtool/components/projects/ReportProject.tsx | 36++++++++++++++++++++++++++++++------
Mumtool/lib/report/serve.mjs | 5+++++
4 files changed, 88 insertions(+), 11 deletions(-)

diff --git a/umtool/components/projects/ClipBench.tsx b/umtool/components/projects/ClipBench.tsx @@ -183,7 +183,13 @@ export type ClipBenchData = { uploadDate: string | null; /** Who the header will name, already resolved server-side. Heads the line. */ sourceChannel: string | null; - /** Neighbours in the cut, computed server-side. Null at either end. */ + /** + * Neighbours ON THE WALK, computed server-side: the nearest clip either side + * that still needs judgement and is fetched. Null when there is none that + * way, which is what "first" / "last" say. Not the neighbours in the cut -- + * a clip with nothing to play is a dead end, and one already answered is the + * round trip the walk exists to remove. + */ prev: string | null; next: string | null; /** Where this clip sits in the cut, 1-based, and how long the cut is. */ @@ -195,6 +201,15 @@ export type ClipBenchData = { * -- and without this component having to guess at the rest of the cut. */ reviewedOthers: number; + /** + * The walk's own reading: `ready` clips are fetched AND still need judgement, + * out of `needing` that need it at all. The gap between the two numbers is + * what is waiting on a download rather than on you. + */ + ready: number; + needing: number; + /** Is there a cached file holding this clip's window, end to end? */ + fetched: boolean; cues: Cue[]; token: string | null; fetchPad: number; @@ -1061,6 +1076,16 @@ export default function ClipBench({ data }: { data: ClipBenchData }) { <span className="micro" data-review-progress=""> {reviewed} reviewed </span> + {/* What the WALK has left, which is not the same number: a clip nobody + has fetched still needs judgement and cannot get it today. */} + <span className="micro" data-ready-count=""> + ready {data.ready} of {data.needing} needing judgement + </span> + {!data.fetched && ( + <span className={badgeVariants({ variant: "open", size: "sm" })} data-clip-unfetched=""> + not fetched yet + </span> + )} {data.prev ? ( <Link data-clip-nav="prev" diff --git a/umtool/components/projects/ClipBenchPage.tsx b/umtool/components/projects/ClipBenchPage.tsx @@ -9,8 +9,9 @@ import { readClipDetail, readManifest, siblingsOf, + walkReadiness, } from "@/lib/projects/report.mjs"; -import { segmentFor, windowsFor } from "@/lib/report/serve.mjs"; +import { rawCacheOf, segmentFor, windowsFor } from "@/lib/report/serve.mjs"; import { FETCH_MAX_PAD } from "@/lib/report/driver.mjs"; import type { ProjectRef } from "@/lib/project-types"; @@ -38,7 +39,24 @@ export default async function ClipBenchPage({ const clips = detail.entries.filter((e: { kind: string }) => e.kind === "clip"); const i = clips.findIndex((e: { id: string }) => e.id === clipId); - const windows = await windowsFor(project, entry); + // ---- where `p` and `n` go ------------------------------------------------ + // + // The walk visits the clips that still NEED JUDGEMENT and are FETCHED, in + // timeline order. A clip nobody can play is a dead end -- there is nothing to + // judge and the only move is to press `n` again -- and one already answered is + // the round trip a walk exists to remove. A URL still opens ANY clip: this + // page renders it, says what it has (or has not) got, and points its links at + // the fetched, unjudged neighbours on either side. + const readiness = walkReadiness(clips); + const inWalk = new Set<string>(readiness.readyIds as string[]); + const onWalk = (e: { id: string }) => e.id !== clipId && inWalk.has(e.id); + const prev = + [...clips.slice(0, Math.max(0, i))].reverse().find(onWalk)?.id ?? null; + const next = clips.slice(i + 1).find(onWalk)?.id ?? null; + + // One listing of clips-raw for the page: readClipDetail already built one to + // answer `fetched` for every clip, and this is the same question for this one. + const windows = await windowsFor(project, entry, await rawCacheOf(project.dir)); // The mtime rides along so a re-render busts the browser's cache: the segment // is written back to the SAME path, and without it the player would keep // showing the cut from before the edit. @@ -91,12 +109,17 @@ export default async function ClipBenchPage({ sourceTitle: entry.sourceTitleClean ?? null, uploadDate: entry.uploadDate ?? null, sourceChannel: entry.sourceChannel ?? null, - prev: i > 0 ? String(clips[i - 1].id) : null, - next: i >= 0 && i < clips.length - 1 ? String(clips[i + 1].id) : null, + prev: prev, + next: next, index: i + 1, total: clips.length, // The OTHERS, so the bench can add this clip's own answer to the count // without a reload and without knowing anything about the rest of the cut. + /** The walk's own reading: fetched clips that still need an answer. */ + ready: readiness.ready, + needing: readiness.needing, + /** Is there a cached file holding THIS clip's window, end to end? */ + fetched: !!entry.fetched, reviewedOthers: clips.filter( (e: { id: string }) => e.id !== clipId && clipVerdict(e) !== "unreviewed", ).length, diff --git a/umtool/components/projects/ReportProject.tsx b/umtool/components/projects/ReportProject.tsx @@ -6,7 +6,13 @@ import CopyButton from "@/components/CopyButton"; import CheckSourcesButton from "@/components/dashboard/CheckSourcesButton"; import { fmtAgo, fmtBytes } from "@/lib/format"; import { Markdown } from "@/lib/markdown"; -import { correctionsOf, readClipDetail, reviewOf, sourcesOf } from "@/lib/projects/report.mjs"; +import { + correctionsOf, + readClipDetail, + reviewOf, + sourcesOf, + walkReadiness, +} from "@/lib/projects/report.mjs"; import { EXPORT_FORMATS, exportableVariants } from "@/lib/report/export.mjs"; import { diffManifests, formatChange } from "@/lib/report/manifest-diff.mjs"; import { listSnapshots, readSnapshot } from "@/lib/report/snapshots.mjs"; @@ -85,6 +91,11 @@ export default async function ReportProject({ // How much of the cut has been walked. One definition, shared with the bench // header and `umtool corrections`. const review = reviewOf(m); + // And how much of it can be walked TODAY: the walk visits the clips that + // still need judgement and have something to play, so the gap between these + // two numbers is what is waiting on a download rather than on somebody. + const readiness = walkReadiness(clips); + const walkStart = (readiness.readyIds[0] as string | undefined) ?? clips[0]?.id; const nonClips = entries.filter((e) => e.kind !== "clip"); const runtime = clips.reduce((n, e) => n + Math.max(0, e.end - e.start), 0); const showAll = search.all === "1"; @@ -184,16 +195,24 @@ export default async function ReportProject({ sources and the build chain. */} {clips.length > 0 && ( <div data-walk-cut="" className="flex flex-col items-start gap-1"> + {/* Where the walk STARTS is where the walk goes: the first clip + that still needs an answer and can be played. A cut whose + first clip is already judged (or not fetched) would otherwise + open on a clip `n` immediately leaves. */} <Link - href={`/browse/${project.id}/clip/${clips[0].id}`} - data-walk-start={clips[0].id} + href={`/browse/${project.id}/clip/${walkStart}`} + data-walk-start={walkStart} className={buttonVariants({ variant: "primary", size: "lg" })} > - Walk the cut → start at {clips[0].id} + Walk the cut → start at {walkStart} </Link> <span className="text-[11px] text-[var(--color-dim)]" data-walk-progress=""> <kbd>p</kbd> / <kbd>n</kbd> move between clips · {review.reviewed} of {review.total}{" "} reviewed{review.incorrect > 0 ? ` · ${review.incorrect} incorrect` : ""} + {" · "} + <span data-ready-count=""> + ready {readiness.ready} of {readiness.needing} needing judgement + </span> </span> </div> )} @@ -412,6 +431,7 @@ export default async function ReportProject({ data-entry={e.id} data-kind="clip" data-cached={e.cached ? "1" : "0"} + data-fetched={e.fetched ? "1" : "0"} data-mid-sentence={midSentence ? "1" : "0"} className="rounded border border-[var(--color-line)] bg-[var(--color-panel)] px-3 py-1.5" > @@ -423,11 +443,15 @@ export default async function ReportProject({ {e.lock && <Pill>locked</Pill>} {!e.lock && e.lockStart && <Pill>start pinned</Pill>} {!e.lock && e.lockEnd && <Pill>end pinned</Pill>} - {e.cached ? ( + {/* The PLAYER's question, and the walk's: is this clip + watchable end to end right now. `data-cached` keeps the + BUILD's -- the padded window it would fetch -- because + the build chain reads it and they are different. */} + {e.fetched ? ( <Pill>cached</Pill> ) : ( <Pill tone="open"> - not fetched + not fetched yet </Pill> )} {e.segment && <Pill>segment built</Pill>} diff --git a/umtool/lib/report/serve.mjs b/umtool/lib/report/serve.mjs @@ -43,6 +43,11 @@ export { rawCacheOf } from "./raw-cache.mjs"; * into a 44-second file, i.e. 1146 s past its end, and plays nothing or the * wrong seconds. A caller with no window (a ledger claim asking for the raw * files of its video) still gets every file, widest first. + * + * @param {{dir: string}} project + * @param {{video: string, start?: number, end?: number}} clip + * @param {Awaited<ReturnType<typeof rawCacheOf>> | null} [cache] a clips-raw + * listing already read this request; one is built when none is passed. */ export async function windowsFor(project, clip, cache = null) { // The cache is optional and passed in by callers that already have one (the