Archilyzer · Source

archilyzer

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

commit 42dbdd30808fd0a85229a70584fab6e22cc48be7
parent 546b88aef02c4f1dde3499f2efc675a955c35c99
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 19 Sep 2026 02:58:53 -0400

clip bench: the other clips cut from this same recording

"Should this clip be wider, or is what it is missing already in the cut?" is
the one question a single clip cannot answer, and the two cases look identical
from inside it: twenty seconds nothing picks up is a hole, twenty seconds the
next entry already covers is a duplicate.

siblingsOf() reads it off the manifest -- every other clip with the same video,
and the same channel when the entries carry one, because two uploads of the
same event are two clocks. Array order is the cut, so position is the distance
in it ("next in cut", "3 later") and the gap is measured against this clip's
own edges, which is what somebody dragging one is actually asking about.

Three places, because the question arrives three ways. A list under the
description, saying it plainly when the nearest sibling is within 15 s ("the
next 12 s are not in the cut; c03 starts at 17:32"), or that nothing else uses
this video. Bands on the cue rail, labelled, and HATCHED where the selection
overlaps one -- a window widened into the next clip is a duplicate and that is
precisely what widening hides. And the peek tooltips, where a dimmed cue
inside another clip's window now reads "already in the cut as c03", so reading
ahead answers "is this missing" rather than only "what is out there".

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

Diffstat:
Mumtool/app/api/report/clip/route.ts | 5++++-
Mumtool/components/projects/ClipBench.tsx | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mumtool/components/projects/ClipBenchPage.tsx | 9++++++++-
Mumtool/docs/clip-bench.md | 25+++++++++++++++++++++++++
Mumtool/lib/projects/report.mjs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 246 insertions(+), 5 deletions(-)

diff --git a/umtool/app/api/report/clip/route.ts b/umtool/app/api/report/clip/route.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { resolveClip, segmentFor, windowsFor } from "@/lib/report/serve.mjs"; import { manifestToken } from "@/lib/report/manifest.mjs"; -import { cuesInWindow, readClipDetail } from "@/lib/projects/report.mjs"; +import { cuesInWindow, readClipDetail, siblingsOf } from "@/lib/projects/report.mjs"; export const dynamic = "force-dynamic"; @@ -79,6 +79,9 @@ export async function GET(request: Request) { // preview and the renderer cannot disagree about who is speaking. sourceChannel: entry?.sourceChannel ?? null, cues: cues?.cues ?? [], + // The other clips cut from this same recording: is the context this clip + // is missing already in the cut, or is it a hole? + siblings: siblingsOf(manifest, clipId), punctuationRate: cues?.punctuationRate ?? null, token: await manifestToken(project.dir), fetchPad: manifest.render?.fetchPad ?? 3, diff --git a/umtool/components/projects/ClipBench.tsx b/umtool/components/projects/ClipBench.tsx @@ -36,6 +36,28 @@ import { attributionLine } from "umtool-report-to-video/attribution"; // --------------------------------------------------------------------------- type Cue = { start: number; end: number; text: string; endsSentence: boolean }; + +/** + * Another clip in the cut from the SAME recording. + * + * The question this answers is the one you cannot answer from inside a single + * clip: should this window be wider, or is what it is missing already in the + * cut a few entries later? Computed server-side from the manifest -- see + * siblingsOf() -- because array order IS the cut and nothing here should be + * re-deriving that. + */ +type Sibling = { + id: string; + start: number; + end: number; + cite: number | null; + quote: string; + note: string | null; + steps: number; + where: string; + side: "after" | "before" | "overlap"; + gap: number | null; +}; type Win = { name: string; from: number; to: number }; type Clip = { @@ -165,6 +187,8 @@ export type ClipBenchData = { maxPad: number; /** A /mix deep link for this clip's cached window, or null. Server-built. */ mixHref: string | null; + /** Every other clip in the cut from this same recording, by start. */ + siblings: Sibling[]; }; const hms = (t: number) => { @@ -213,6 +237,22 @@ const EDGE_AUDITION = 4; /** How far past the cached window the cue rail reads ahead, per step. */ const PEEK_STEP = 60; +/** + * Close enough that the gap is worth a sentence rather than a number. + * + * Under this, "the next 12 s are not in the cut" is a decision waiting to be + * made -- widen this clip, or leave the hole because the next clip picks it + * up. Over it the two clips are simply elsewhere in the same stream. + */ +const NEAR_SIBLING = 15; + +/** m:ss, for a list of positions rather than a pair of edges. */ +const clock = (t: number) => { + const s = Math.max(0, Math.floor(t)); + const m = Math.floor(s / 60); + return `${Math.floor(m / 60) > 0 ? `${Math.floor(m / 60)}:${String(m % 60).padStart(2, "0")}` : m}:${String(s % 60).padStart(2, "0")}`; +}; + export default function ClipBench({ data }: { data: ClipBenchData }) { const [clip, setClip] = useState<Clip>(data.clip); const [windows, setWindows] = useState<Win[]>(data.windows); @@ -825,6 +865,17 @@ export default function ClipBench({ data }: { data: ClipBenchData }) { const nextPad = Math.min(data.maxPad, Math.round(reach) + 20); const atMaxPad = reach >= data.maxPad - 0.5; + // ---- the other clips from this recording ---------------------------------- + const siblings = data.siblings; + /** The sibling whose window holds this cue, if any. */ + const siblingAt = (c: Cue) => + siblings.find((sb) => c.end > sb.start + 0.02 && c.start < sb.end - 0.02) ?? null; + // The one that is about to matter: nearest by gap, and close enough that the + // seconds between the two are a decision rather than trivia. + const nearest = siblings + .filter((sb) => sb.gap != null && sb.gap <= NEAR_SIBLING) + .sort((a, b) => (a.gap ?? 0) - (b.gap ?? 0))[0]; + /** The pad that would put this cue (plus a breath) inside the cache. */ const padForCue = (c: Cue) => Math.min( @@ -1088,8 +1139,38 @@ export default function ClipBench({ data }: { data: ClipBenchData }) { ))} </> )} + {/* ---- what the CUT already covers ---- + A band per other clip from this recording, and a hatch where + one of them overlaps the selection: a window widened into the + next clip is a duplicate, and that is invisible from inside a + single clip. */} + {siblings.map((sb) => { + if (sb.end <= rail.from || sb.start >= rail.to) return null; + const from = Math.max(sb.start, rail.from); + const to = Math.min(sb.end, rail.to); + const clash = Math.min(to, sel.to) - Math.max(from, sel.from) > 0.02; + return ( + <div + key={`sib-${sb.id}`} + data-sibling-band={sb.id} + data-sibling-clash={clash ? "1" : "0"} + title={`${sb.id} · ${sb.where} · ${clock(sb.start)}–${clock(sb.end)}${ + clash ? " — this selection overlaps it" : "" + }`} + className={`absolute bottom-0 h-3 overflow-hidden whitespace-nowrap rounded-sm border px-1 text-[8px] leading-3 ${ + clash + ? "border-[var(--color-dirty)] text-[var(--color-dirty)] [background:repeating-linear-gradient(45deg,color-mix(in_srgb,var(--color-dirty)_26%,transparent)_0_4px,transparent_4px_8px)]" + : "border-[var(--color-sel)] bg-[color-mix(in_srgb,var(--color-sel)_16%,transparent)] text-[var(--color-sel)]" + }`} + style={{ left: rpct(from), width: `calc(${rpct(to)} - ${rpct(from)})` }} + > + {sb.id} · {sb.where} + </div> + ); + })} {railCues.map((c) => { const inSel = c.end > sel.from && c.start < sel.to; + const owner = siblingAt(c); // Outside the cached file: readable, dimmed, and a click offers // to go and get it rather than pretending it is already here. const outside = c.start < view.from - 0.02 || c.end > view.to + 0.02; @@ -1100,10 +1181,17 @@ export default function ClipBench({ data }: { data: ClipBenchData }) { data-cue={c.start} data-ends-sentence={c.endsSentence ? "1" : "0"} data-peek={outside ? "1" : "0"} + data-in-sibling={owner ? owner.id : ""} title={ - outside - ? `${c.text}\n\nnot fetched — click to fetch to ±${padForCue(c)} s` - : c.text + [ + c.text, + // The whole point of reading ahead: these words may not + // be missing at all, they may be the next clip. + owner ? `already in the cut as ${owner.id} (${owner.where})` : null, + outside ? `not fetched — click to fetch to ±${padForCue(c)} s` : null, + ] + .filter(Boolean) + .join("\n\n") } onClick={() => { if (outside) { @@ -1307,6 +1395,60 @@ export default function ClipBench({ data }: { data: ClipBenchData }) { </p> </section> + {/* ---- what else the cut takes from this recording ---- + "Should this clip be wider, or is what it is missing already in + the cut?" is the question you cannot answer from inside one clip, + and it decides whether a widened window is context or a + duplicate. */} + <section + data-siblings={siblings.length} + className="space-y-1 rounded border border-[var(--color-line)] px-2 py-1.5 text-[11px]" + > + <div className="micro">also from this video</div> + {siblings.length === 0 ? ( + <p className="text-[var(--color-dim)]">no other clip in the cut uses this video</p> + ) : ( + <> + {nearest && ( + <p data-sibling-near={nearest.id} className="text-[var(--color-dirty)]"> + {nearest.side === "after" + ? `the next ${nearest.gap!.toFixed(0)} s are not in the cut; ${nearest.id} starts at ${clock(nearest.start)}` + : `the ${nearest.gap!.toFixed(0)} s before this are not in the cut; ${nearest.id} ends at ${clock(nearest.end)}`} + </p> + )} + <ul className="space-y-0.5"> + {siblings.map((sb) => ( + <li key={sb.id} data-sibling={sb.id} className="leading-snug"> + <Link + href={`/browse/${data.project}/clip/${sb.id}`} + className="font-mono text-[var(--color-sel)] hover:underline" + > + {sb.id} + </Link>{" "} + <span className="num text-[var(--color-dim)]"> + {clock(sb.start)}–{clock(sb.end)} + </span>{" "} + <span className="text-[var(--color-dim)]"> + ·{" "} + {sb.side === "overlap" + ? "overlaps this clip" + : sb.side === "after" + ? `${sb.gap!.toFixed(0)} s after this clip ends` + : `${sb.gap!.toFixed(0)} s before this clip starts`}{" "} + · {sb.where} + </span> + {sb.quote && ( + <span className="block text-[var(--color-dim)]"> + &ldquo;{sb.quote}&rdquo; + </span> + )} + </li> + ))} + </ul> + </> + )} + </section> + <div className="flex flex-wrap items-center gap-2"> <span className="text-[var(--color-text)]">Is this accurate?</span> <button diff --git a/umtool/components/projects/ClipBenchPage.tsx b/umtool/components/projects/ClipBenchPage.tsx @@ -3,7 +3,13 @@ import { notFound } from "next/navigation"; import BrowseHeader from "@/components/BrowseHeader"; import ClipBench, { type ClipBenchData } from "./ClipBench"; import { manifestToken } from "@/lib/report/manifest.mjs"; -import { clipVerdict, cuesInWindow, readClipDetail, readManifest } from "@/lib/projects/report.mjs"; +import { + clipVerdict, + cuesInWindow, + readClipDetail, + readManifest, + siblingsOf, +} from "@/lib/projects/report.mjs"; import { segmentFor, windowsFor } from "@/lib/report/serve.mjs"; import { FETCH_MAX_PAD } from "@/lib/report/driver.mjs"; import type { ProjectRef } from "@/lib/project-types"; @@ -92,6 +98,7 @@ export default async function ClipBenchPage({ (e: { id: string }) => e.id !== clipId && clipVerdict(e) !== "unreviewed", ).length, cues: cues?.cues ?? [], + siblings: siblingsOf(manifest, clipId), token: await manifestToken(project.dir), fetchPad: manifest.render?.fetchPad ?? 3, // The widest the fetch route will go. The BUTTON has to know it: it names diff --git a/umtool/docs/clip-bench.md b/umtool/docs/clip-bench.md @@ -187,6 +187,31 @@ reviewed. See [report-video.md](report-video.md#verdict--what-the-walk-said) for what is stored and what the writer refuses. +## What else the cut takes from this recording + +"Should this clip be wider, or is what it is missing already in the cut?" is +the one question a single clip cannot answer, and the two cases look identical +from inside it: twenty seconds of context that nothing picks up is a hole, and +twenty seconds the next entry already covers is a duplicate. + +`siblingsOf()` reads it off the manifest — every other `type: "clip"` with the +same `video` (and the same `channel` when the entries carry one, because two +uploads of the same event are two clocks). The bench shows them three ways: + +- **Under the description**, as a list: id (a link to its bench page), its + position on the source clock, the gap to this clip's edges, where it sits in + the *cut* ("next in cut", "3 later") and its quote. When the nearest one is + within 15 s it says so in words — *"the next 12 s are not in the cut; c03 + starts at 17:32"* — because that is a decision waiting to be made rather than + a number to notice. With none: *"no other clip in the cut uses this video"*. +- **As bands on the cue rail**, labelled `c03 · next in cut`, so where the cut + already reaches is visible against the window you are dragging. A band the + selection overlaps is **hatched**: a widened window that runs into the next + clip is a duplicate, and that is exactly what somebody widening cannot see. +- **In the peek tooltips**: a dimmed cue inside another clip's window reads + *"already in the cut as c03"*, so reading ahead answers "is this missing?" + rather than only "what is out there?". + ## The cue rail Every cue in view, positioned by time. Inside the selection in full contrast, diff --git a/umtool/lib/projects/report.mjs b/umtool/lib/projects/report.mjs @@ -183,6 +183,70 @@ export function reviewOf(m) { }; } +/** + * The OTHER clips in the cut that come from this same recording. + * + * "Does this clip need more context, or is the context already coming up as + * another clip?" is a question the manifest can answer and nothing in the + * bench could: a widened window that runs into the next clip from the same + * stream is a duplicate, and twenty seconds of missing context that the cut + * never picks up is a hole. Both look identical from inside one clip. + * + * Matched on the VIDEO, and on the channel when the entries carry one -- two + * recordings of the same event on different channels are different clocks, and + * saying "c07 covers this" about a different upload would be worse than saying + * nothing. Position is the distance in the CUT (array order is the cut), and + * the gap is measured against this clip's own edges, which is what somebody + * dragging one is actually asking about. + * + * @param {any} m manifest + * @param {string} clipId + */ +export function siblingsOf(m, clipId) { + const clips = clipsOf(m); + const i = clips.findIndex((e) => e.id === clipId); + if (i < 0) return []; + const me = clips[i]; + + const where = (steps) => { + if (steps === 1) return "next in cut"; + if (steps === -1) return "previous in cut"; + return steps > 0 ? `${steps} later` : `${-steps} earlier`; + }; + + return clips + .map((e, j) => ({ e, j })) + .filter( + ({ e, j }) => + j !== i && e.video === me.video && (e.channel ?? null) === (me.channel ?? null), + ) + .map(({ e, j }) => { + const after = e.start >= me.end; + const before = e.end <= me.start; + const quote = String(e.quote ?? "").trim(); + return { + id: e.id, + start: e.start, + end: e.end, + cite: e.cite ?? null, + quote: quote.length > 80 ? `${quote.slice(0, 80)}…` : quote, + note: e.note ?? null, + /** Distance in the cut: +1 is the next clip, -2 is two clips back. */ + steps: j - i, + where: where(j - i), + /** Where it sits on the SOURCE clock relative to this clip's window. */ + side: after ? "after" : before ? "before" : "overlap", + /** Seconds of source between the two windows; null when they overlap. */ + gap: after + ? Number((e.start - me.end).toFixed(2)) + : before + ? Number((me.start - e.end).toFixed(2)) + : null, + }; + }) + .sort((a, b) => a.start - b.start); +} + /** The recorded preflight, and when it ran. Never a live probe. */ export async function readAvailability(dir) { const file = path.join(dir, "out", "availability.json");