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:
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