commit ae2c0f42c3c2e240af6d0efbc3605a8416595b52
parent 6cb327e2121ce66203f5b156824a011aacb1aa9a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 29 Jul 2026 11:20:16 -0400
Wire the duplicate-cluster confirm path — it was built and never called
`updateDuplicateOverride` has existed since duplicate review was built, with the
`confirmed` flag, the read-modify-write, the atomic rename, and the careful rule
that an empty patch clears a decision without deleting a confirmation. It had
**zero callers anywhere in the repo**.
That matters because `clusterMaySharePartial` fails CLOSED: a `needsReview`
cluster — title and near-identical runtime, but nothing compared the content —
shares no derived work and reaches no built site until a human records
`confirmed: true`. With no caller there was no way for a human to record it, so
the review queue asked a question that could not be answered. Corpus-wide that
is ~166 clusters permanently stuck.
/actionable now reads the overrides alongside the report (so a decided cluster
stops asking), each card carries Confirm / Not a duplicate / Undo, and the
badges distinguish "needs review" from "confirmed" and "not a duplicate".
Confirming also runs the share immediately, via the equally-uncalled
`shareClusterFromCanonical`. The point of confirming is to let derived work
flow; making the operator wait for the canonical member's next sweep to find out
whether it would have makes the button feel inert. It is a no-op when the
canonical has no digest yet, which today is almost always.
The buttons are disabled until mounted. A server-rendered button has no handler
until React hydrates, so a click before then fires nothing at all — no request,
no job, no error. That is the recorded root cause of the digest pilot's
"un-created job", and /actionable renders a page heavy enough to hit the same
window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat:
4 files changed, 241 insertions(+), 9 deletions(-)
diff --git a/editor/app/actionable/actions.ts b/editor/app/actionable/actions.ts
@@ -10,7 +10,12 @@ import {
import { getRegistry } from "yt-dlp-transcript-common/jobs/registry";
import { runManagedFunction } from "yt-dlp-transcript-common/jobs/streamCommand";
import { drainStream } from "yt-dlp-transcript-common/jobs/drainStream";
-import { detectDuplicateShorts } from "yt-dlp-transcript-common/controller/duplicateShorts";
+import {
+ detectDuplicateShorts,
+ readDuplicateReport,
+ updateDuplicateOverride,
+} from "yt-dlp-transcript-common/controller/duplicateShorts";
+import { shareClusterFromCanonical } from "yt-dlp-transcript-common/controller/digestSharing";
import {
clearIncompleteTranscriptsAction,
enableAutoRunners,
@@ -130,6 +135,69 @@ export async function runDuplicateDetectionAction(
return { ok: true, clusters, videosInClusters };
}
+// What a human can say about a cluster the detector could not decide.
+// confirmed — "I looked; these really are the same video." Unblocks
+// sharing AND publication for a title+duration suspect.
+// not-duplicate — "They are not." Suppresses the cluster entirely.
+// clear — undo, back to awaiting review.
+export type DuplicateClusterDecision = "confirmed" | "not-duplicate" | "clear";
+
+export type ReviewDuplicateClusterResult =
+ | { ok: true; shared: number; misaligned: number }
+ | { ok: false; error: string };
+
+// Record a review decision for one cluster.
+//
+// `updateDuplicateOverride` has existed — with the `confirmed` flag, the
+// read-modify-write, the atomic rename and the "an empty patch clears the
+// decision" rule — since duplicate review was built, and until now **nothing in
+// the repo called it**. `clusterMaySharePartial` fails closed, so every
+// needsReview cluster shared nothing and there was no way for a human to change
+// that. This is that missing caller.
+//
+// Confirming also attempts the share immediately, via the equally-uncalled
+// `shareClusterFromCanonical`: the point of confirming is to let derived work
+// flow, and making the operator wait for the canonical member's next sweep to
+// find out whether it would have would make the button feel inert. It is a
+// no-op when the canonical has no digest yet, which today is almost always.
+export async function reviewDuplicateClusterAction(
+ clusterId: string,
+ decision: DuplicateClusterDecision,
+): Promise<ReviewDuplicateClusterResult> {
+ const paths = getPaths();
+ try {
+ const overrides = await updateDuplicateOverride(
+ paths,
+ clusterId,
+ decision === "confirmed"
+ ? { confirmed: true, notDuplicate: false }
+ : decision === "not-duplicate"
+ ? { notDuplicate: true, confirmed: false }
+ : { confirmed: false, notDuplicate: false },
+ );
+
+ let shared = 0;
+ let misaligned = 0;
+ if (decision === "confirmed") {
+ const report = await readDuplicateReport(paths);
+ const cluster = report?.clusters.find((c) => c.clusterId === clusterId);
+ if (cluster) {
+ for (const outcome of await shareClusterFromCanonical(paths, cluster, {
+ overrides,
+ })) {
+ if (outcome.status === "shared") shared++;
+ else if (outcome.status === "misaligned") misaligned++;
+ }
+ }
+ }
+
+ revalidatePath("/actionable");
+ return { ok: true, shared, misaligned };
+ } catch (e) {
+ return { ok: false, error: (e as Error)?.message ?? String(e) };
+ }
+}
+
export type GlobalIncompleteResult =
| { ok: true; channels: number; affected: number }
| { ok: false; error: string };
diff --git a/editor/app/actionable/components/DuplicateClusterReview.tsx b/editor/app/actionable/components/DuplicateClusterReview.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import {
+ reviewDuplicateClusterAction,
+ type DuplicateClusterDecision,
+ type ReviewDuplicateClusterResult,
+} from "../actions";
+import type { DuplicateClusterOverride } from "yt-dlp-transcript-common/lib/duplicates";
+
+type Status =
+ | { kind: "idle" }
+ | { kind: "running"; decision: DuplicateClusterDecision }
+ | { kind: "done"; result: ReviewDuplicateClusterResult }
+ | { kind: "error"; message: string };
+
+export function DuplicateClusterReview({
+ clusterId,
+ override,
+}: {
+ clusterId: string;
+ override: DuplicateClusterOverride | undefined;
+}) {
+ const [status, setStatus] = useState<Status>({ kind: "idle" });
+ // Disabled until mounted, deliberately. A server-rendered button has no
+ // handler until React hydrates, so a click before then fires NOTHING — no
+ // request, no error, nothing to debug. That is the exact failure that made
+ // the digest pilot's job look "un-created", and /actionable renders a page
+ // heavy enough to hit the same window. See StreamActionLog.
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
+
+ async function decide(decision: DuplicateClusterDecision) {
+ setStatus({ kind: "running", decision });
+ try {
+ setStatus({
+ kind: "done",
+ result: await reviewDuplicateClusterAction(clusterId, decision),
+ });
+ } catch (e) {
+ setStatus({ kind: "error", message: (e as Error).message });
+ }
+ }
+
+ const busy = status.kind === "running";
+ const disabled = busy || !mounted;
+ const confirmed = override?.confirmed === true;
+ const rejected = override?.notDuplicate === true;
+
+ return (
+ <div className="flex items-center gap-2 flex-wrap">
+ <button
+ type="button"
+ onClick={() => decide("confirmed")}
+ disabled={disabled || confirmed}
+ aria-label={`confirm duplicate cluster ${clusterId}`}
+ className="px-2 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted disabled:opacity-50"
+ >
+ {busy && status.decision === "confirmed" ? "Confirming…" : "Confirm"}
+ </button>
+ <button
+ type="button"
+ onClick={() => decide("not-duplicate")}
+ disabled={disabled || rejected}
+ aria-label={`reject duplicate cluster ${clusterId}`}
+ className="px-2 py-1 rounded-md border border-border text-xs font-medium hover:bg-muted disabled:opacity-50"
+ >
+ {busy && status.decision === "not-duplicate" ? "Rejecting…" : "Not a duplicate"}
+ </button>
+ {(confirmed || rejected) && (
+ <button
+ type="button"
+ onClick={() => decide("clear")}
+ disabled={disabled}
+ aria-label={`clear duplicate cluster decision ${clusterId}`}
+ className="px-2 py-1 rounded-md text-xs text-muted-foreground underline hover:text-foreground disabled:opacity-50"
+ >
+ {busy && status.decision === "clear" ? "Clearing…" : "Undo"}
+ </button>
+ )}
+ {status.kind === "done" && status.result.ok && (
+ <span
+ aria-label={`duplicate cluster review result ${clusterId}`}
+ className="text-xs text-muted-foreground"
+ >
+ {status.result.shared > 0 || status.result.misaligned > 0
+ ? `${status.result.shared} digest(s) shared` +
+ (status.result.misaligned > 0
+ ? `, ${status.result.misaligned} misaligned`
+ : "")
+ : "Saved."}
+ </span>
+ )}
+ {status.kind === "done" && !status.result.ok && (
+ <span
+ role="alert"
+ aria-label={`duplicate cluster review error ${clusterId}`}
+ className="text-xs text-destructive"
+ >
+ {status.result.error}
+ </span>
+ )}
+ {status.kind === "error" && (
+ <span
+ role="alert"
+ aria-label={`duplicate cluster review error ${clusterId}`}
+ className="text-xs text-destructive"
+ >
+ {status.message}
+ </span>
+ )}
+ </div>
+ );
+}
diff --git a/editor/app/actionable/lib/loadActionable.ts b/editor/app/actionable/lib/loadActionable.ts
@@ -8,8 +8,14 @@ import {
readChannelSnapshot,
type ChannelSnapshot,
} from "yt-dlp-transcript-common/controller/channelSnapshot";
-import { readDuplicateReport } from "yt-dlp-transcript-common/controller/duplicateShorts";
-import type { DuplicateReport } from "yt-dlp-transcript-common/lib/duplicates";
+import {
+ readDuplicateOverrides,
+ readDuplicateReport,
+} from "yt-dlp-transcript-common/controller/duplicateShorts";
+import type {
+ DuplicateOverrides,
+ DuplicateReport,
+} from "yt-dlp-transcript-common/lib/duplicates";
export type ActionableRow = {
channel: ChannelStat;
@@ -26,6 +32,12 @@ export type ActionableSummary = {
cleanExtraFormats: ActionableRow[];
staleOrMissing: ActionableRow[];
duplicates: DuplicateReport | null;
+ // The human decisions kept alongside the report — a cluster's canonical
+ // choice, "not a duplicate", and the `confirmed` flag that is the only thing
+ // letting a needsReview cluster share derived work. Loaded here because the
+ // review UI cannot show what has already been decided without it, and a
+ // review queue that forgets its own answers re-asks every question.
+ duplicateOverrides: DuplicateOverrides;
};
export function isStaleOrMissing(row: ActionableRow): boolean {
@@ -100,7 +112,7 @@ export async function loadActionableSummary(
paths: Paths,
): Promise<ActionableSummary> {
const channels = await listChannels(paths);
- const [rows, duplicates] = await Promise.all([
+ const [rows, duplicates, duplicateOverrides] = await Promise.all([
Promise.all(
channels.map(async (channel) => ({
channel,
@@ -108,6 +120,7 @@ export async function loadActionableSummary(
})),
),
readDuplicateReport(paths),
+ readDuplicateOverrides(paths),
]);
const undownloaded = rows
@@ -163,5 +176,6 @@ export async function loadActionableSummary(
cleanExtraFormats,
staleOrMissing,
duplicates,
+ duplicateOverrides,
};
}
diff --git a/editor/app/actionable/page.tsx b/editor/app/actionable/page.tsx
@@ -18,8 +18,10 @@ import { InlineActionButton } from "./components/InlineActionButton";
import { FixAllIncompleteButton } from "./components/FixAllIncompleteButton";
import { RefreshAllReportsButton } from "./components/RefreshAllReportsButton";
import { RunDuplicateDetectionButton } from "./components/RunDuplicateDetectionButton";
+import { DuplicateClusterReview } from "./components/DuplicateClusterReview";
import type {
DuplicateCluster,
+ DuplicateOverrides,
DuplicateReport,
} from "yt-dlp-transcript-common/lib/duplicates";
@@ -232,12 +234,21 @@ export default async function ActionablePage() {
<Section key={config.id} config={config} rows={rows} />
))
)}
- <DuplicatesSection report={summary.duplicates} />
+ <DuplicatesSection
+ report={summary.duplicates}
+ overrides={summary.duplicateOverrides}
+ />
</div>
);
}
-function DuplicatesSection({ report }: { report: DuplicateReport | null }) {
+function DuplicatesSection({
+ report,
+ overrides,
+}: {
+ report: DuplicateReport | null;
+ overrides: DuplicateOverrides;
+}) {
const clusters = report?.clusters ?? [];
return (
<section aria-label="duplicate-shorts" className="flex flex-col gap-2">
@@ -275,7 +286,11 @@ function DuplicatesSection({ report }: { report: DuplicateReport | null }) {
) : (
<ul className="flex flex-col gap-3">
{clusters.map((cluster) => (
- <DuplicateClusterCard key={cluster.clusterId} cluster={cluster} />
+ <DuplicateClusterCard
+ key={cluster.clusterId}
+ cluster={cluster}
+ override={overrides.clusters[cluster.clusterId]}
+ />
))}
</ul>
)}
@@ -283,7 +298,13 @@ function DuplicatesSection({ report }: { report: DuplicateReport | null }) {
);
}
-function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) {
+function DuplicateClusterCard({
+ cluster,
+ override,
+}: {
+ cluster: DuplicateCluster;
+ override: DuplicateOverrides["clusters"][string] | undefined;
+}) {
const matchLabel: Record<DuplicateCluster["matchKind"], string> = {
"transcript-exact": "exact transcript",
"transcript-near": "near transcript",
@@ -299,7 +320,11 @@ function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) {
{/* Nothing compared these videos' content — one side has no transcript.
The cluster is a suspect for a human, stays out of the built site,
and shares no derived work until someone confirms it. */}
- {cluster.needsReview && <Badge>needs review</Badge>}
+ {cluster.needsReview && !override?.confirmed && !override?.notDuplicate && (
+ <Badge>needs review</Badge>
+ )}
+ {override?.confirmed && <Badge>confirmed</Badge>}
+ {override?.notDuplicate && <Badge>not a duplicate</Badge>}
{cluster.score !== null && (
<Badge>score {cluster.score.toFixed(2)}</Badge>
)}
@@ -309,6 +334,17 @@ function DuplicateClusterCard({ cluster }: { cluster: DuplicateCluster }) {
<span className="text-muted-foreground">
{cluster.videoRefs.length} videos · ~{cluster.durationBucket}s
</span>
+ {/* The confirm path exists precisely for `needsReview` clusters:
+ clusterMaySharePartial fails closed, so until a human says
+ "confirmed" these share no digest and never reach a built site.
+ Content-confirmed clusters need no confirmation — but they can still
+ be rejected, which is the only way to un-assert a wrong one. */}
+ <span className="ml-auto">
+ <DuplicateClusterReview
+ clusterId={cluster.clusterId}
+ override={override}
+ />
+ </span>
</div>
<ul className="flex flex-col gap-1">
{cluster.videoRefs.map((ref) => (