Archilyzer · Source

archilyzer

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

commit d7f670b2677fbaa04f4b37396cc920e3968cc808
parent 171f0e934bc129c41b5546376bbfbaa83638ecf8
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 03:56:36 -0400

common: a report quote is checked against every transcript of the record, and its moment shows the one that matched (a served en track can be a rewrite)

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

Diffstat:
Mcommon/publish/composeReports.test.ts | 8+++++---
Mcommon/publish/composeReports.ts | 40++++++++++++++++++++++++++++++++++++----
2 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/common/publish/composeReports.test.ts b/common/publish/composeReports.test.ts @@ -245,10 +245,11 @@ async function seedCorpus() { [60, 70, "Far outside any window."], ]), ); - // The served `en` track parses to no cues; the original track has them. + // The served `en` track is a rewrite of what was said; the original track + // has the words. The quote must be checked (and shown) against the original. const def = path.join(paths.channelsDir, VIDEOS, "data", "def456"); writeJson(path.join(def, "metadata.info.json"), meta("def456")); - writeText(path.join(def, "transcript.en.vtt"), vtt([[5, 9, "lost words"]], false)); + writeText(path.join(def, "transcript.en.vtt"), vtt([[5, 9, "Some words a rewrite changed."]])); writeText(path.join(def, "transcript.en-orig.vtt"), vtt([[5, 9, "Words only the original track has."]])); writeJson(path.join(paths.channelsDir, SOCIAL, "config.json"), { @@ -383,10 +384,11 @@ test("verification is computed at compose: a hand-typed block is overwritten, a const view = readJson<View>(pub("reports", REPORT, "page.json")); const v = view.citations.c01.verification!; assert.equal(v.quoteScore, 1); - assert.equal(v.method, QUOTE_CHECK_METHOD); + assert.equal(v.method, `${QUOTE_CHECK_METHOD}; text: transcript.en.vtt`, "the method names the transcript that matched"); assert.ok((v.quoteCheckedAt as string) >= NOW_FLOOR, "checked now, not when the document says"); assert.ok(!("voiceChecked" in v), "a voice is never vouched for by compose"); assert.equal(view.citations.c02.verification?.quoteScore, 1, "checked against the en-orig track"); + assert.match(String(view.citations.c02.verification?.method), /text: transcript\.en-orig\.vtt$/, "a rewritten `en` track loses to the original"); assert.equal(view.citations.p01.verification?.quoteScore, 1, "a post's quote against its text"); assert.equal(view.citations.w01.verification, undefined); assert.equal(view.citations.a01.verification, undefined); diff --git a/common/publish/composeReports.ts b/common/publish/composeReports.ts @@ -67,7 +67,7 @@ import type { Platform } from "../lib/platform"; import { readAllPosts } from "../lib/posts-server"; import type { Post } from "../lib/posts"; import { readJsonFile } from "../lib/jsonFile-server"; -import { momentPath, parseMomentKey, type SpanMoment } from "../lib/citations/moments"; +import { momentKeyOf, momentPath, parseMomentKey, type SpanMoment } from "../lib/citations/moments"; import { CITATIONS_VERSION, type Citation, type PostCitation, type SpanCitation } from "../lib/citations/schema"; import { cueWindowText, quoteDrifted, quoteVerification, QUOTE_DRIFT_THRESHOLD } from "../lib/citations/verify"; import { buildCitedIn } from "../lib/report/citedIn"; @@ -204,6 +204,11 @@ export type ComposedReports = { type CitedRecord = { summary: Pick<TranscriptSummary, "id" | "slug" | "title" | "uploadDate" | "platform" | "webpageUrl" | "channel">; cues: Cue[]; + // Every transcript of the record that has cues, by file name: the + // normalized cues, the whisper output, and each English VTT (`en-orig` + // first). A served `en` track can be a rewrite of what was said, so a + // quote is checked against each and the best match is the one shown. + tracks: { name: string; cues: Cue[] }[]; }; // The English VTT tracks of a video dir, `en-orig` first, then the order @@ -268,7 +273,21 @@ export async function readCitedRecord( if (cues.length > 0) break; } } - return { summary, cues }; + const tracks: CitedRecord["tracks"] = []; + if (fresh.fresh) { + const n = await readNormalizedTranscript(fresh.cuesPath); + if (n?.cues?.length) tracks.push({ name: path.basename(fresh.cuesPath), cues: n.cues }); + } + if (entries.includes(WHISPER_FILENAME)) { + const w = await readCues(path.join(dir, WHISPER_FILENAME), "whisper"); + if (w.length > 0) tracks.push({ name: WHISPER_FILENAME, cues: w }); + } + for (const name of englishVttsByPreference(entries)) { + const v = await readCues(path.join(dir, name), "vtt"); + if (v.length > 0) tracks.push({ name, cues: v }); + } + if (tracks.length === 0 && cues.length > 0) tracks.push({ name: "cues", cues }); + return { summary, cues, tracks }; } const isoDay = (uploadDate: string | undefined): string | undefined => @@ -394,6 +413,8 @@ export async function composeReports(opts: ComposeReportsOptions): Promise<Compo } // Compose works on its own copy: verification is overwritten below. const reports: Report[] = loaded.problems.length > 0 ? [] : structuredClone(loaded.reports); + // moment key → the cues its quote was verified against (first citation wins). + const momentCues = new Map<string, Cue[]>(); const settings = opts.settings ?? getSettings(); const pool = siteChannelSlugs(site); @@ -484,7 +505,16 @@ export async function composeReports(opts: ComposeReportsOptions): Promise<Compo problems.push({ kind: "no-cues", citation: ref, report: report.id, message: `${c.channel}/${c.id} has no transcript cues to check the quote against` }); continue; } - c.verification = quoteVerification(c.quote, cueWindowText(record.cues, c.start, c.end), now); + let best: { name: string; cues: Cue[]; v: ReturnType<typeof quoteVerification> } | null = null; + for (const t of record.tracks) { + const v = quoteVerification(c.quote, cueWindowText(t.cues, c.start, c.end), now); + if (!best || (v.quoteScore ?? 0) > (best.v.quoteScore ?? 0)) best = { name: t.name, cues: t.cues, v }; + } + c.verification = best + ? { ...best.v, method: `${best.v.method}; text: ${best.name}` } + : quoteVerification(c.quote, cueWindowText(record.cues, c.start, c.end), now); + const mk = momentKeyOf(c); + if (best && mk && !momentCues.has(mk)) momentCues.set(mk, best.cues); } if (quoteDrifted(c.verification)) { problems.push({ @@ -657,7 +687,9 @@ export async function composeReports(opts: ComposeReportsOptions): Promise<Compo const m = parseMomentKey(key) as SpanMoment; const info = momentInfo.get(key)!; const span = evidenceSpan({ start: m.start, end: m.end, pad: info.pad }); - const { cues } = records.get(`${c.channel}/${c.id}`)!; + // The transcript the moment's first citation matched best (see the + // verification above), else the record's default cues. + const cues = momentCues.get(key) ?? records.get(`${c.channel}/${c.id}`)!.cues; const from = m.start - MOMENT_CUE_CONTEXT_SECONDS; const to = m.end + MOMENT_CUE_CONTEXT_SECONDS; const lines: CueLineView[] = cues