// The one-word answer to "what does this video know about who is speaking?", // client-safe so a badge, a snapshot bucket and a stage card can all read it // without pulling a server module in. // // FOUR STATES, and the two middle ones are deliberately not collapsed into a // single "attributed". The lanes are not interchangeable: "diarized" names // acoustic clusters and costs about one model call per video, while "text-only" // reconstructs speaker changes from the transcript at ~30 calls and has to // re-establish identity at every chunk seam. A UI that showed one word for both // would be claiming a confidence the text-only lane has not earned — and PLAN.md // is explicit that this misfires on rapid back-and-forth and has nothing to work // with on auto-caption channels. The filtering that eventually consumes this is // supposed to be able to say "diarized only", which it cannot do against a // status that has already thrown the distinction away. import { isAttributionFresh, type AttributionFreshnessTarget, type AttributionRecord, } from "./attribution"; export type AttributionStatus = "none" | "text-only" | "diarized" | "stale"; // `target` is OPTIONAL, and the two cases are genuinely different questions. // // With a target — a server-side caller that has settings — "stale" is reachable // and means the record exists but is not what we would produce now. Without one // — an exported viewer, which has no settings and no engine — staleness is // unknowable, so the honest answer is the method that IS recorded. Returning // "stale" there would be a guess, and returning "none" would hide real work. export function attributionStatus( record: AttributionRecord | null, target?: AttributionFreshnessTarget, ): AttributionStatus { if (!record) return "none"; const method = record.provenance?.method; if (method !== "text-only" && method !== "diarized") return "none"; if (target && !isAttributionFresh(record, target)) return "stale"; return method; } // UI copy, kept beside the type so a second surface cannot invent its own // wording for the same state. export function attributionStatusLabel(status: AttributionStatus): string { switch (status) { case "diarized": return "Speakers named from audio"; case "text-only": // Hedged on purpose. See the header. return "Speakers guessed from the transcript"; case "stale": return "Speaker names are out of date"; default: return "No speaker names"; } }