"use client"; // The bodies for the three speaker operations — what a diarization or an // attribution record actually says about this video. // // Until this existed the speaker layer had the problem the digest layer had // before its panel: sidecars on disk with nowhere for a human to look at them. // diarization.json is produced from audio that the cleanup sweep then deletes, // so the record is frequently the ONLY surviving evidence of what was heard — // and it was invisible per video. // // BOTH ATTRIBUTION OPERATIONS SHARE attribution.json (lib/operations.ts's // two-kinds-one-file rule). So `attribution-diarized` and `attribution-text` // draw the SAME record with their own state(): the pill differs, the body does // not, and `provenance.method` is how a reader tells which lane wrote it. // // TYPE-ONLY imports from the common lib modules named here: lib/diarization.ts // and lib/attribution.ts have no imports at all and are safe for values, but // nothing in this file needs one. import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog"; import type { DiarizationEngine } from "yt-dlp-transcript-common/lib/diarization"; import type { AttributionProvenance } from "yt-dlp-transcript-common/lib/attribution"; import { runOperationForVideoAction } from "../operationActions"; import { cancelJobAction } from "../../../../../jobs/actions"; import type { OperationPanelView } from "./operationState"; // Projected on the server from DiarizationRecord — `turns` as a COUNT, because // a long recording has thousands of them and none is worth a row here. export type DiarizationBodyData = { record: { generatedAt: string; audioSeconds?: number; durationMs?: number; speakers: number; turns: number; engine: DiarizationEngine; windowing?: { windows: number; windowSeconds: number; overlapSeconds: number; }; } | null; }; // Projected from AttributionRecord. `segments` and `warnings` are counts for // the same reason; the speakers ARE the content and are listed. export type AttributionBodyData = { record: { generatedAt: string; speakers: { index: number; label: string; cluster?: number; confidence?: number; seconds?: number; }[]; segments: number; warnings: number; provenance: AttributionProvenance; } | null; }; export function DiarizationBody({ slug, videoId, view, data, }: { slug: string; videoId: string; view: OperationPanelView; data: DiarizationBodyData; }) { // ONE CHILD LIST, WHATEVER IS ON DISK — and that is what keeps the run log // alive. React reconciles a fragment's children BY POSITION, so the old shape // (`[empty state, RunOne]` before a record exists, `[provenance, …, RunOne]` // after) MOVED down the list the moment the record appeared. A moved // child is not re-rendered, it is unmounted and mounted again — taking // StreamActionLog's `log` state with it. So the run finished, its own // router.refresh() landed the new record, and the operator's log vanished at // the instant the summary line reached it. `attribution.spec.ts:346` was the // 1-in-10 witness: it waits for the batch's "1 done" and the panel was gone. // The record body is a SIBLING of the button now, and RunOne is last and keyed // in both branches, so nothing can move it. return ( <> {data.record ? ( ) : (

No speaker turns have been captured for this video.

)} ); } function DiarizationRecordBody({ record, }: { record: NonNullable; }) { const e = record.engine; return ( <> {/* WHICH configuration produced this, for the same reason the digest's provenance block exists: swapping the engine, a model or the clustering threshold changes the answer, and after the audio is gone this record is the only thing that can say which one ran. */}
{e.segmentationModel && ( )} {e.embeddingModel && ( )} {e.model && } {e.version && } {e.threshold !== undefined && ( )} {record.audioSeconds !== undefined && ( )} {record.durationMs !== undefined && ( )} {record.windowing && ( )}

{record.speakers} speakers · {record.turns} turns

); } export function AttributionBody({ slug, videoId, view, data, }: { slug: string; videoId: string; view: OperationPanelView; data: AttributionBodyData; }) { // Same two-child shape as DiarizationBody above, and for the same reason: // must not change position when the record appears, or React // remounts it and StreamActionLog loses the log the run just wrote. return ( <> {data.record ? ( ) : (

No speaker names for this video yet.

)} ); } function AttributionRecordBody({ record, }: { record: NonNullable; }) { const p = record.provenance; return ( <>
{/* The row that says which of the two lanes wrote this file. */} {p.transcriptSource && ( )} {p.diarizationGeneratedAt && ( )} {p.chunks !== undefined && ( )} {p.costUsd !== undefined && ( )}
    {record.speakers.map((s) => (
  • {s.label} {s.cluster !== undefined && ( · cluster {s.cluster} )} {s.confidence !== undefined && ( {" "} · {(s.confidence * 100).toFixed(0)}% confident )} {s.seconds !== undefined && ( {" "} · {clock(s.seconds)} )}
  • ))}

{record.segments} segments · {record.warnings} warnings

); } // One video, on the speaker lane. NOT DRAWN for a switched-off operation: its // panel is shown so the record stays visible, but there is nothing to run — the // action refuses on the same condition, which is the belt behind this brace. function RunOne({ slug, videoId, view, }: { slug: string; videoId: string; view: OperationPanelView; }) { if (!view.enabled) return null; return (
runOperationForVideoAction(slug, videoId, view.id)} cancelAction={cancelJobAction} buttonLabel={`Run ${view.label}`} runningLabel="Running…" // The accessible name the log, the cancel button and the error all // derive from — see StreamActionLog. label={`Run ${view.label}`} />

Runs this one video on the speaker lane, behind nothing — the channel's Speakers stage runs the whole channel.

); } function Row({ label, value }: { label: string; value: string }) { return ( <>
{label}
{value}
); } function clock(seconds: number): string { const s = Math.max(0, Math.round(seconds)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; }