import path from "node:path"; import { resolveClip, segmentFor, windowsFor } from "@/lib/report/serve.mjs"; import { manifestToken } from "@/lib/report/manifest.mjs"; import { cuesInWindow, readClipDetail, siblingsOf } from "@/lib/projects/report.mjs"; export const dynamic = "force-dynamic"; // Everything the bench needs to open a clip, in one request. // // The window, the cached source files it can draw from, the cues around it, and // the manifest's mtime as a WRITE TOKEN -- handed out here so a save can be // refused when somebody (a `resolve-windows --write`, an agent, another tab) // has written in between. export async function GET(request: Request) { const url = new URL(request.url); const projectId = url.searchParams.get("project") ?? ""; const clipId = url.searchParams.get("clip") ?? ""; const r = await resolveClip(projectId, clipId); if ("error" in r) return Response.json({ error: r.error }, { status: r.status }); const { project, manifest, clip } = r; const windows = await windowsFor(project, clip); // Re-read after a re-render: the segment lands on the SAME path, so its mtime // is what tells the player it is looking at a different file. const seg = await segmentFor(project, clipId); const detail = await readClipDetail(project.dir, { manifest }); const entry = detail?.entries.find((e: { id: string }) => e.id === clipId) ?? null; // The view is the widest cached file when there is one, else a generous span // around the window so the waveform is not a blank rectangle before a fetch. const widest = windows[0] ?? null; const view = widest ? { from: widest.from, to: widest.to } : { from: Math.max(0, clip.start - 20), to: clip.end + 20 }; const cues = await cuesInWindow(project.dir, clipId, view.from, view.to); return Response.json( { project: project.id, dir: project.dir, clip: { id: clip.id, video: clip.video, channel: entry?.channel ?? null, start: clip.start, end: clip.end, cite: clip.cite ?? null, quote: clip.quote ?? null, note: clip.note ?? null, correction: clip.correction ?? null, title: clip.title ?? null, date: clip.date ?? null, citeUrl: clip.citeUrl ?? null, lock: !!clip.lock, lockStart: !!clip.lockStart, lockEnd: !!clip.lockEnd, cutStart: clip.cutStart ?? null, cutEnd: clip.cutEnd ?? null, lockCut: !!clip.lockCut, muteFrom: clip.muteFrom ?? null, }, view, windows: windows.map((w: { name: string; from: number; to: number }) => ({ name: w.name, from: w.from, to: w.to, })), // What resolve-windows WOULD do, computed in-process because widen() is // pure once the cues are read. "Run the widener and see" stops being a // leap of faith. proposed: entry?.proposed ?? null, endsSentence: entry?.endsSentence ?? null, noPunctuation: entry?.noPunctuation ?? false, sourceDuration: entry?.duration ?? null, segment: seg?.rel ?? entry?.segment ?? null, segmentMtime: seg?.mtimeMs ?? null, sourceTitle: entry?.sourceTitleClean ?? null, uploadDate: entry?.uploadDate ?? null, // Already resolved (override -> record -> provenance -> slug); the bench // feeds it straight back into attributionLine's override slot, so the // 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, outLabel: path.posix.join(project.id, "out"), }, { headers: { "cache-control": "no-store" } }, ); }