// ONE read of out/clips-raw, answering for every clip in a project. // // Its own module, and deliberately a LEAF: lib/projects/report.mjs reads it, and // so does lib/report/serve.mjs, which report.mjs is itself imported by. Putting // it in serve.mjs closed that circle and broke module initialisation outright // (kinds.mjs reads a const of report.mjs at top level, and in the cycle that // const is still in its temporal dead zone). Nothing here imports anything of // ours but the predicate. import path from "node:path"; import { listRawNames, tightestContaining, windowsFromNames, } from "umtool-report-to-video/build-video"; /** * The cache. The directory is keyed by VIDEO and a page asks about it per clip * -- the bench page did two readdirs per clip, and a forty-clip cut paid eighty * of them for one screen. The listing is read once, the per-video parse is * memoised, and every question below is then arithmetic. * * `isFetched(clip)` is the walk's question and the one a follow-up importer * wants: is there a cached file holding this clip's OWN window, end to end. * Not the PADDED window the build would fetch (that is `containing()` with the * pad applied, and it calls three of four fixture clips unfetched), and not * mere overlap -- a half-covered clip cannot be watched through, so it is not * ready to judge. * * @param {string} projectDir * @param {{ extraWindows?: { video: string, windows: { name: string, path: string, from: number, to: number }[] }[] }} [opts] * Windows OUTSIDE the project that also hold a given video -- the corpus's * clip windows and a saved whole source, as `clipWindowDirs` lists them * through the render's own sources.mjs, so the bench and the build agree on * what is local. A whole container is the window [0, duration]: a full * source contains every window of its video, which is exactly what the * predicate needs to know. */ export async function rawCacheOf(projectDir, { extraWindows = [] } = {}) { const rawDir = path.join(projectDir, "out", "clips-raw"); const names = await listRawNames(rawDir); const extra = new Map(); for (const e of extraWindows) { if (!e?.video || !e?.windows?.length) continue; extra.set(e.video, [...(extra.get(e.video) ?? []), ...e.windows]); } const byVideo = new Map(); const windows = (video) => { if (!byVideo.has(video)) { byVideo.set(video, [ ...windowsFromNames(names, rawDir, video), ...(extra.get(video) ?? []), ]); } return byVideo.get(video); }; return { rawDir, windows, containing: (video, from, to) => tightestContaining(windows(video), from, to), isFetched: (clip) => { const { start, end, video } = clip ?? {}; if (!Number.isFinite(start) || !Number.isFinite(end)) return false; return !!tightestContaining(windows(video), start, end); }, }; }