Archilyzer · Source

archilyzer

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

commit b5c8184f379b40b6e7bfae1f924966cbd51747f8
parent 349854ab07761dc1816481b1fb8783eecd83680a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 31 Aug 2026 12:47:00 -0400

channels: the transcode stage is gone

Every editor reader of the transcode operation, so the common controllers can
go next without breaking tsc mid-series. The Transcode stage panel and its
station, the failed-transcodings side of the attention strip, the bulk clear
and the /cleanup card, the four replay entries and the four actions behind
them, the "Transcoded" dot on the video list and the video page.

The dot was never the operation's fact anyway: it read
transcodeSources.length === 0, true of any audio.* file, so a video already in
the channel's own format showed as not transcoded. The strip is Downloaded ·
Transcribed.

The per-file "Transcode <file> → <fmt>" rows and transcodeAudioAction stay —
the one escape hatch — and so do both audio-format cleanup sweeps, now gated
on the channel having an audioFormat, which is what they compare against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Diffstat:
Meditor/app/channels/[slug]/bulkVideoActions.ts | 20++++++--------------
Meditor/app/channels/[slug]/components/SocialChannelPanel.tsx | 2+-
Meditor/app/channels/[slug]/components/VideoListPane.tsx | 14+++-----------
Meditor/app/channels/[slug]/components/flow/AttentionStrip.tsx | 10----------
Meditor/app/channels/[slug]/components/flow/FlowStation.tsx | 4++--
Meditor/app/channels/[slug]/components/stages/CleanupStage.tsx | 10++++++----
Deditor/app/channels/[slug]/components/stages/TranscodeStage.tsx | 244-------------------------------------------------------------------------------
Meditor/app/channels/[slug]/lib/channelFlow.test.ts | 31-------------------------------
Meditor/app/channels/[slug]/lib/channelFlow.ts | 32++------------------------------
Meditor/app/channels/[slug]/lib/stageOrder.test.ts | 33++++++---------------------------
Meditor/app/channels/[slug]/lib/stageStatus.ts | 66+++++++-----------------------------------------------------------
Meditor/app/channels/[slug]/lib/videoRows.ts | 8++------
Meditor/app/channels/[slug]/lib/videoRowsServer.ts | 10+---------
Meditor/app/channels/[slug]/page.tsx | 36+++++++-----------------------------
Meditor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx | 4----
Meditor/app/channels/[slug]/videos/page.tsx | 6------
Meditor/app/channels/[slug]/whisperActions.ts | 116-------------------------------------------------------------------------------
Meditor/app/cleanup/components/ChannelCleanupCard.tsx | 14+-------------
Meditor/app/cleanup/lib/loadCleanup.ts | 19++++++-------------
Meditor/app/cleanup/page.tsx | 1-
Meditor/app/components/channelWork/sections.test.ts | 2+-
Meditor/app/jobs/jobReplayRegistry.ts | 24------------------------
Meditor/app/operations/[id]/page.tsx | 6+++---
Meditor/e2e/bulk-actions.spec.ts | 20+++++++-------------
Meditor/e2e/helpers.ts | 1-
Meditor/e2e/whisper.spec.ts | 35-----------------------------------
26 files changed, 61 insertions(+), 707 deletions(-)

diff --git a/editor/app/channels/[slug]/bulkVideoActions.ts b/editor/app/channels/[slug]/bulkVideoActions.ts @@ -8,10 +8,6 @@ import { failedTranscriptionsFile, pruneFailedTranscriptions, } from "yt-dlp-transcript-common/controller/failedTranscriptions"; -import { - failedTranscodingsFile, - pruneFailedTranscodings, -} from "yt-dlp-transcript-common/controller/failedTranscodings"; import { readChannelConfig } from "yt-dlp-transcript-common/controller/channels"; import { transcribeBucketAction } from "./whisperActions"; import { retryBucketAction } from "./pipelineActions"; @@ -110,7 +106,7 @@ export async function bulkMarkUntranscribableAction( } // Deleting a video directory is a pure filesystem op (no queued job), so it -// reports a per-id summary and never triggers a transcode — the whole point of +// reports a per-id summary and never queues a job — the whole point of // the bulk delete button. export async function bulkDeleteVideoDirsAction( slug: string, @@ -135,7 +131,7 @@ export async function bulkDeleteVideoDirsAction( // Removing audio files is a pure filesystem op (no queued job): finalized // audio.<ext> is deleted, .part partials/transcripts/metadata are kept. Reports -// a per-id summary; never triggers a transcode. +// a per-id summary; never queues a job. export async function bulkRemoveAudioAction( slug: string, videoIds: string[], @@ -199,9 +195,9 @@ export async function bulkRemoveWrongFormatAudioAction( }; } -// Clearing failure markers is a set operation over the two failure-list files, -// so `succeeded` reports how many ids were actually removed from either list -// (some selected ids may not have been marked failed). +// Clearing failure markers is a set operation over the failed-transcriptions +// file, so `succeeded` reports how many ids were actually removed (some +// selected ids may not have been marked failed). export async function bulkClearFailedMarkersAction( slug: string, videoIds: string[], @@ -212,16 +208,12 @@ export async function bulkClearFailedMarkersAction( failedTranscriptionsFile(paths, slug), idSet, ); - const trcd = await pruneFailedTranscodings( - failedTranscodingsFile(paths, slug), - idSet, - ); revalidatePath(`/channels/${slug}`); requestChannelSnapshot(paths, slug); return { ok: true, attempted: videoIds.length, - succeeded: trxn.pruned + trcd.pruned, + succeeded: trxn.pruned, failures: [], }; } diff --git a/editor/app/channels/[slug]/components/SocialChannelPanel.tsx b/editor/app/channels/[slug]/components/SocialChannelPanel.tsx @@ -219,7 +219,7 @@ export function SocialChannelPanel({ <p className="text-xs text-muted-foreground"> Posts are indexed into the search corpus by the normal build — there is - no per-post download, transcode or transcription stage. + no per-post download or transcription stage. </p> </div> ); diff --git a/editor/app/channels/[slug]/components/VideoListPane.tsx b/editor/app/channels/[slug]/components/VideoListPane.tsx @@ -250,7 +250,7 @@ export function VideoListPane({ } // Mark-untranscribable / clear-markers / delete are synchronous metadata or - // filesystem ops with a per-id summary — no queued job, so no transcode. + // filesystem ops with a per-id summary — no queued job. function doSummaryBulk( summaryAction: (slug: string, ids: string[]) => Promise<BulkActionSummary>, onOk?: () => void, @@ -276,7 +276,7 @@ export function VideoListPane({ setSelected((prev) => { const next = new Set(prev); for (const r of rows) { - if (r.failedTranscription || r.failedTranscoding) next.add(r.id); + if (r.failedTranscription) next.add(r.id); } return next; }); @@ -723,12 +723,6 @@ function StatusGlyphs({ row }: { row: VideoRow }) { : row.downloaded ? "bg-success" : "bg-muted"; - const tcColor = - row.transcoded === null - ? "bg-transparent border border-border" - : row.transcoded - ? "bg-success" - : "bg-muted"; const trColor = row.untranscribable ? "bg-muted-foreground" : row.incompleteTranscript @@ -736,8 +730,7 @@ function StatusGlyphs({ row }: { row: VideoRow }) { : row.transcribed ? "bg-success" : "bg-muted"; - const failureColor = - row.failedTranscription || row.failedTranscoding ? "bg-destructive" : null; + const failureColor = row.failedTranscription ? "bg-destructive" : null; return ( <span aria-label={`status ${row.status}`} @@ -753,7 +746,6 @@ function StatusGlyphs({ row }: { row: VideoRow }) { } className={`w-2 h-2 rounded-full ${dlColor}`} /> - <span title="transcoded" className={`w-2 h-2 rounded-full ${tcColor}`} /> <span title={ row.incompleteTranscript diff --git a/editor/app/channels/[slug]/components/flow/AttentionStrip.tsx b/editor/app/channels/[slug]/components/flow/AttentionStrip.tsx @@ -34,8 +34,6 @@ export type AttentionInput = { buckets: SnapshotBuckets; digestWarnings: string[]; failedVideoIds: string[]; - failedTranscodingIds: string[]; - transcodeApplies: boolean; generatedAt: string; lastSyncedAt?: string; }; @@ -60,14 +58,6 @@ export function collectAttention(input: AttentionInput): Chip[] { }; add("failed transcriptions", input.failedVideoIds.length, "transcribe", "destructive"); - if (input.transcodeApplies) { - add( - "failed transcodings", - input.failedTranscodingIds.length, - "transcode", - "destructive", - ); - } add("need cookies", buckets.needsCookies.length, "download"); add("partial downloads", buckets.partialDownloads.length, "download"); add("corrupt sources", buckets.corruptSource.length, "download", "destructive"); diff --git a/editor/app/channels/[slug]/components/flow/FlowStation.tsx b/editor/app/channels/[slug]/components/flow/FlowStation.tsx @@ -97,8 +97,8 @@ export function StationFoot({ station }: { station: Station }) { /> )) ) : station.coverage != null ? ( - // The two stations that are not registry operations — playlist and - // transcode — keep the plain meter. + // The one station that is not a registry operation — playlist — keeps + // the plain meter. // // aria-hidden, and no label: the <p> above states the same ratio in // words, so the bar is decorative. Naming it "<station> coverage meter" diff --git a/editor/app/channels/[slug]/components/stages/CleanupStage.tsx b/editor/app/channels/[slug]/components/stages/CleanupStage.tsx @@ -25,7 +25,9 @@ type Props = { // Videos where our transcript won and the superseded YouTube ASR VTT is still // on disk as a backup. Never cleaned automatically — only by the purge below. supersededAutoSubsIds: string[]; - transcodeApplies: boolean; + // Both audio-format sweeps compare against the channel's audioFormat; a + // channel with none has no "wrong" format and neither section applies. + wrongFormatApplies: boolean; // Estimated bytes each cleanup would reclaim, as of the last report. transcribedAudioBytes: number; extraFormatsBytes: number; @@ -51,7 +53,7 @@ export function CleanupStage({ multipleAudioFormatIds, foreignAudioIds, supersededAutoSubsIds, - transcodeApplies, + wrongFormatApplies, transcribedAudioBytes, extraFormatsBytes, foreignAudioBytes, @@ -133,7 +135,7 @@ export function CleanupStage({ defaultQueueKey={defaultQueueKey} /> )} - {transcodeApplies && ( + {wrongFormatApplies && ( <div className="flex flex-col gap-2"> <Heading title="Clean extra audio formats" @@ -148,7 +150,7 @@ export function CleanupStage({ /> </div> )} - {transcodeApplies && ( + {wrongFormatApplies && ( <div className="flex flex-col gap-2"> <Heading title="Remove wrong-format audio" diff --git a/editor/app/channels/[slug]/components/stages/TranscodeStage.tsx b/editor/app/channels/[slug]/components/stages/TranscodeStage.tsx @@ -1,244 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog"; -import { - AUDIO_FORMAT_VALUES, - type AudioFormat, -} from "yt-dlp-transcript-common/lib/channelConfig"; -import { QueueControl } from "../../../../components/QueueControl"; -import { cancelJobAction } from "../../../../jobs/actions"; -import { - clearFailedTranscodingsAction, - removeFailedTranscodingsAction, - transcodeFailuresAction, - transcodeUntranscodedAction, -} from "../../whisperActions"; -import { VideoIdList } from "../VideoIdList"; - -type Props = { - slug: string; - existingQueues: string[]; - failedTranscodingIds: string[]; - untranscodedIds: string[]; - transcodeDefaultQueueKey: string; -}; - -export function TranscodeStage({ - slug, - existingQueues, - failedTranscodingIds, - untranscodedIds, - transcodeDefaultQueueKey, -}: Props) { - // Filesystem-only ops (remove) stay on the channel queue; transcoding itself - // is local-resource-bound and shares the transcription queue. - const removeDefaultQueueKey = `channel:${slug}`; - const [transcodeFormat, setTranscodeFormat] = useState<AudioFormat>("mp3"); - const [transcodeQueue, setTranscodeQueue] = useState(transcodeDefaultQueueKey); - return ( - <div className="flex flex-col gap-6"> - <div className="flex flex-col gap-2"> - <Heading - title="Transcode failures" - desc="Re-encode the audio for every video in the failure list to a different format. Useful when a codec is incompatible with whisper. Existing audio files are kept; the new file is written as audio.<format>." - /> - <label className="flex items-center gap-2 text-sm"> - Target format - <select - value={transcodeFormat} - onChange={(e) => - setTranscodeFormat(e.target.value as AudioFormat) - } - aria-label="transcode failures target format" - className="rounded border border-border bg-card px-2 py-1 text-sm" - > - {AUDIO_FORMAT_VALUES.map((f) => ( - <option key={f} value={f}> - {f} - </option> - ))} - </select> - </label> - <StreamActionLog - trigger={() => - transcodeFailuresAction(slug, transcodeFormat, transcodeQueue) - } - cancelAction={cancelJobAction} - buttonLabel={`Transcode failures → ${transcodeFormat}`} - runningLabel="Transcoding…" - label="Transcode failures" - extraControls={ - <QueueControl - value={transcodeQueue} - onChange={setTranscodeQueue} - defaultQueueKey={transcodeDefaultQueueKey} - existingQueues={existingQueues} - actionLabel="Transcode failures" - /> - } - /> - </div> - <UntranscodedList - slug={slug} - ids={untranscodedIds} - existingQueues={existingQueues} - defaultQueueKey={transcodeDefaultQueueKey} - /> - <FailedTranscodingsSection - slug={slug} - ids={failedTranscodingIds} - existingQueues={existingQueues} - defaultQueueKey={removeDefaultQueueKey} - /> - </div> - ); -} - -function UntranscodedList({ - slug, - ids, - existingQueues, - defaultQueueKey, -}: { - slug: string; - ids: string[]; - existingQueues: string[]; - defaultQueueKey: string; -}) { - const [queue, setQueue] = useState(defaultQueueKey); - if (ids.length === 0) return null; - return ( - <div className="flex flex-col gap-2 rounded border border-border p-3"> - <div> - <h4 className="text-sm font-semibold"> - Has audio but missing target format ({ids.length}) - </h4> - <p className="text-xs text-muted-foreground"> - Audio file present in a different format than the channel&apos;s - configured audioFormat — re-run the transcoder to produce - audio.&lt;format&gt;. Failures land in the failed-transcodings list - below. - </p> - </div> - <VideoIdList - slug={slug} - ids={ids} - ariaLabel="untranscoded list" - emptyAriaLabel="untranscoded empty" - emptyMessage="None" - itemAriaLabel={(id) => `untranscoded ${id}`} - /> - <StreamActionLog - trigger={() => transcodeUntranscodedAction(slug, queue)} - cancelAction={cancelJobAction} - buttonLabel={`Transcode all (${ids.length}) → channel audioFormat`} - runningLabel="Transcoding…" - label="Transcode untranscoded" - extraControls={ - <QueueControl - value={queue} - onChange={setQueue} - defaultQueueKey={defaultQueueKey} - existingQueues={existingQueues} - actionLabel="Transcode untranscoded" - /> - } - /> - </div> - ); -} - -function FailedTranscodingsSection({ - slug, - ids, - existingQueues, - defaultQueueKey, -}: { - slug: string; - ids: string[]; - existingQueues: string[]; - defaultQueueKey: string; -}) { - const [removeQueue, setRemoveQueue] = useState(defaultQueueKey); - const [confirmText, setConfirmText] = useState(""); - const armed = confirmText === "remove"; - return ( - <div - aria-label="failed transcodings section" - className="flex flex-col gap-2 rounded border border-border p-3" - > - <div> - <h4 className="text-sm font-semibold"> - Failed transcodings ({ids.length}) - </h4> - <p className="text-xs text-muted-foreground"> - Videos that ffmpeg refused to transcode. Source files are likely - corrupt — the button below deletes their video directories on disk - and clears this list. - </p> - </div> - <VideoIdList - slug={slug} - ids={ids} - ariaLabel="failed transcodings list" - emptyAriaLabel="failed transcodings empty" - emptyMessage="No failed transcodings." - itemAriaLabel={(id) => `failed transcoding ${id}`} - /> - {ids.length > 0 ? ( - <div className="flex flex-col gap-2"> - <StreamActionLog - trigger={() => clearFailedTranscodingsAction(slug, removeQueue)} - cancelAction={cancelJobAction} - buttonLabel={`Clear failed-transcodings list (${ids.length})`} - runningLabel="Clearing…" - label="Clear failed-transcodings list" - /> - <p className="text-xs text-muted-foreground"> - Clears the list only — video directories stay on disk. Use this when - ffmpeg has been fixed or the listing is stale. - </p> - <label className="flex items-center gap-2 text-xs text-muted-foreground"> - Type - <span className="font-mono">remove</span> - to confirm - <input - type="text" - value={confirmText} - onChange={(e) => setConfirmText(e.target.value)} - aria-label="confirm remove failed transcodings" - className="rounded border border-border bg-card px-2 py-0.5 font-mono" - /> - </label> - <StreamActionLog - trigger={() => removeFailedTranscodingsAction(slug, removeQueue)} - cancelAction={cancelJobAction} - buttonLabel={`Remove ${ids.length} failed-transcoding video dir(s)`} - runningLabel="Removing…" - label="Remove failed transcodings" - disabled={!armed} - extraControls={ - <QueueControl - value={removeQueue} - onChange={setRemoveQueue} - defaultQueueKey={defaultQueueKey} - existingQueues={existingQueues} - actionLabel="Remove failed transcodings" - /> - } - /> - </div> - ) : null} - </div> - ); -} - -function Heading({ title, desc }: { title: string; desc: string }) { - return ( - <div> - <h3 className="text-base font-semibold">{title}</h3> - <p className="text-sm text-muted-foreground">{desc}</p> - </div> - ); -} diff --git a/editor/app/channels/[slug]/lib/channelFlow.test.ts b/editor/app/channels/[slug]/lib/channelFlow.test.ts @@ -43,26 +43,20 @@ function flowOf( opts: { laneOperations?: Operation[]; playlistCount?: number | null; - transcodeApplies?: boolean; failedVideoIds?: string[]; - failedTranscodingIds?: string[]; } = {}, ) { const failedVideoIds = opts.failedVideoIds ?? []; - const failedTranscodingIds = opts.failedTranscodingIds ?? []; return computeChannelFlow({ snapshot, stages: computeStageStatuses({ snapshot, failedVideoIds, - failedTranscodingIds, config: CONFIG, runningJobs: [], }), config: CONFIG, failedVideoIds, - failedTranscodingIds, - transcodeApplies: opts.transcodeApplies ?? false, laneOperations: opts.laneOperations ?? [kind({ id: "diarization" })], playlistCount: opts.playlistCount ?? null, }); @@ -283,31 +277,6 @@ test("the digest station reads the registry entry", () => { assert.equal(station(flow, "digest").through, null); }); -test("the transcode station appears only when the channel transcodes", () => { - const withOut = flowOf(snapshotOf()); - assert.equal( - withOut.stations.some((s) => s.id === "transcode"), - false, - ); - assert.deepEqual( - withOut.gaps.map((g) => g.to), - ["download", "transcribe", "digest", "speakers"], - ); - - const withIt = flowOf( - snapshotOf({ - buckets: { ...normalizeBuckets(undefined), untranscoded: ["a", "b"] }, - }), - { transcodeApplies: true }, - ); - assert.deepEqual( - withIt.gaps.map((g) => g.to), - ["download", "transcode", "transcribe", "digest", "speakers"], - ); - assert.equal(withIt.gaps.find((g) => g.to === "transcode")?.reachable, 2); - assert.equal(station(withIt, "transcode").through, 58); -}); - test("the bottleneck is the biggest gap; the next action is the furthest upstream one", () => { const flow = flowOf( snapshotOf({ diff --git a/editor/app/channels/[slug]/lib/channelFlow.ts b/editor/app/channels/[slug]/lib/channelFlow.ts @@ -63,7 +63,6 @@ import { export type FlowStationId = | "playlist" | "download" - | "transcode" | "transcribe" | "digest" // Renamed with StageId — see stageStatus.ts. The station and the stage card it @@ -104,8 +103,8 @@ export type FlowStation = { // Which stage panel this station opens (?stage=). stage: StageId; // The pipelines that run at this station, in dependency order. Empty for - // playlist and transcode, which are not operations the registry dispatches or - // counts — they keep the plain coverage meter. + // playlist, which is not an operation the registry dispatches or counts — it + // keeps the plain coverage meter. operations: StationOperation[]; }; @@ -152,8 +151,6 @@ export type ComputeChannelFlowInput = { stages: Record<StageId, StageStatus>; config: ChannelConfig; failedVideoIds: string[]; - failedTranscodingIds: string[]; - transcodeApplies: boolean; // Enabled lane kinds. EMPTY MEANS THE LANE IS OFF, which is not the same as // finished — see the tone rule at the bottom of this file. laneOperations: Operation[]; @@ -227,8 +224,6 @@ export function computeChannelFlow( snapshot, stages, failedVideoIds, - failedTranscodingIds, - transcodeApplies, laneOperations, playlistCount, } = input; @@ -315,23 +310,6 @@ export function computeChannelFlow( stage: "download", operations: opsFor("download"), }, - transcode: { - id: "transcode", - label: "Transcode", - // No `totals.transcoded` field exists, but the population is derivable - // exactly: everything downloaded that is not in the untranscoded bucket is - // already in the channel's target format. - through: Math.max(0, totals.downloaded - buckets.untranscoded.length), - denominator: totals.downloaded, - coverage: ratio( - Math.max(0, totals.downloaded - buckets.untranscoded.length), - totals.downloaded, - ), - running: stages.transcode.running, - tone: stages.transcode.tone, - stage: "transcode", - operations: [], - }, transcribe: { id: "transcribe", label: "Transcribe", @@ -384,7 +362,6 @@ export function computeChannelFlow( const order: FlowStationId[] = [ "playlist", "download", - ...(transcodeApplies ? (["transcode"] as const) : []), "transcribe", "digest", "speakers", @@ -432,9 +409,6 @@ export function computeChannelFlow( "Download completed but the audio stayed malformed. File kept for inspection; re-downloading is futile.", ), ], - transcode: transcodeApplies - ? siding("failed transcodings", failedTranscodingIds.length, "transcode") - : [], transcribe: [ ...siding("failed", failedVideoIds.length, "transcribe"), ...siding( @@ -505,7 +479,6 @@ export function computeChannelFlow( const reachableInto: Record<FlowStationId, number> = { playlist: 0, download: undownloadedIds.length, - transcode: transcodeApplies ? buckets.untranscoded.length : 0, transcribe: actionableDownloadedNoTranscript.length, digest: digestWork.reachable, speakers: laneReachable, @@ -514,7 +487,6 @@ export function computeChannelFlow( const GAP_LABEL: Record<FlowStationId, string> = { playlist: "", download: "to download", - transcode: "to transcode", transcribe: "to transcribe", digest: "to digest", // A VALUE tsc keys but does not spell, so it is hand-checked: diff --git a/editor/app/channels/[slug]/lib/stageOrder.test.ts b/editor/app/channels/[slug]/lib/stageOrder.test.ts @@ -4,8 +4,7 @@ // page.tsx builds its stage list from the registry rather than from a literal: // // ["configure", "playlist", -// ...OPERATION_GROUP_ORDER.flatMap((g) => GROUP_STAGES[g]) -// .filter((id) => id !== "transcode" || transcodeApplies), +// ...OPERATION_GROUP_ORDER.flatMap((g) => GROUP_STAGES[g]), // "cleanup", "diagnostics", "danger"] // // That array is what `?stage=` is resolved against and what the switcher renders @@ -14,7 +13,7 @@ // but not ORDER and not CARDINALITY — a group that gained a second stage, or an // OPERATION_GROUP_ORDER someone resorted, would compile and silently reorder the // page. This pins both against the literal list that shipped before the -// derivation, for each value of the one per-channel input. +// derivation. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -25,39 +24,19 @@ import { GROUP_STAGES, type StageId } from "./stageStatus"; // imported because page.tsx is a server component that reads the filesystem at // module scope; what is worth pinning is the SHAPE, and a copy that drifted from // the page would fail this test by construction on the next edit to either. -function stageOrder(transcodeApplies: boolean): StageId[] { +function stageOrder(): StageId[] { return [ "configure", "playlist", - ...OPERATION_GROUP_ORDER.flatMap((g) => GROUP_STAGES[g]).filter( - (id) => id !== "transcode" || transcodeApplies, - ), + ...OPERATION_GROUP_ORDER.flatMap((g) => GROUP_STAGES[g]), "cleanup", "diagnostics", "danger", ]; } -test("the derived stage order is the list that shipped, transcode included", () => { - assert.deepEqual(stageOrder(true), [ - "configure", - "playlist", - "download", - "transcode", - "transcribe", - "digest", - "speakers", - "cleanup", - "diagnostics", - "danger", - ]); -}); - -test("…and the same list without transcode where it does not apply", () => { - // Per-CHANNEL, not per-install: whether a platform needs a transcode pass is a - // fact about the channel, which is why it is filtered here rather than left - // out of GROUP_STAGES. `media` owns both stages either way. - assert.deepEqual(stageOrder(false), [ +test("the derived stage order is the list that shipped", () => { + assert.deepEqual(stageOrder(), [ "configure", "playlist", "download", diff --git a/editor/app/channels/[slug]/lib/stageStatus.ts b/editor/app/channels/[slug]/lib/stageStatus.ts @@ -6,7 +6,6 @@ import { } from "yt-dlp-transcript-common/controller/channelSnapshot"; import { backfillLaneEntriesOf, - operationApplies, operationsGroupLabel, reachableOperationWork, type OperationGroup, @@ -48,7 +47,6 @@ export type StageId = | "configure" | "playlist" | "download" - | "transcode" | "transcribe" | "digest" // THE OPERATIONS, NOT THE QUEUE. This card was called "backfill" — a queue key @@ -71,18 +69,19 @@ export type StageId = // a card only if someone remembered to add one — and the failure mode was a // silent absence, not an error. // -// NOT 1:1 with the groups, and no honest derivation makes it so: `media` owns -// two stages (download and transcode). So this is a Record of ARRAYS, spread in -// OPERATION_GROUP_ORDER — which yields exactly today's order and today's -// cardinality. It is a compile-time membership check, not a re-shaping of the -// page. +// NOT 1:1 with the groups, and no honest derivation makes it so: `sync` owns +// NO stage (below), and a group may own more than one — `media` did while the +// transcode stage existed (retired 2026-08-30). So this is a Record of ARRAYS, +// spread in OPERATION_GROUP_ORDER — which yields exactly today's order and +// today's cardinality. It is a compile-time membership check, not a re-shaping +// of the page. // // DELIBERATELY ONLY THE MIDDLE. configure/playlist and cleanup/diagnostics/ // danger are channel CHORES, not operations — nothing registers them and no // group owns them — so they stay hand-listed at the call site. Do not "finish" // this derivation by inventing groups for them. export const GROUP_STAGES: Record<OperationGroup, readonly StageId[]> = { - media: ["download", "transcode"], + media: ["download"], transcript: ["transcribe"], digest: ["digest"], speakers: ["speakers"], @@ -112,11 +111,6 @@ const JOB_KIND_TO_STAGE: Record<string, StageId> = { sync: "playlist", "download-from-playlist": "download", "download-missing": "download", - "transcode-failures": "transcode", - "transcode-untranscoded": "transcode", - "remove-failed-transcodings": "transcode", - "clear-failed-transcodings": "transcode", - "transcode-audio": "transcode", "whisper-all": "transcribe", "whisper-retry": "transcribe", "transcribe-one": "transcribe", @@ -159,7 +153,6 @@ function pickTone(args: { export type ComputeStageStatusesInput = { snapshot: ChannelSnapshot; failedVideoIds: string[]; - failedTranscodingIds: string[]; config: ChannelConfig; // Only `status` and `kind` are read (which stage has work in flight), so this // takes the SHAPE rather than the record: the channel page now gets its rows @@ -187,7 +180,6 @@ export function computeStageStatuses( const { snapshot, failedVideoIds, - failedTranscodingIds, config, runningJobs, backfillEnabled = true, @@ -211,14 +203,10 @@ export function computeStageStatuses( if (stage) runningByStage.add(stage); } - const transcodeApplies = operationApplies("transcode", config); - const downloadPending = undownloadedIds.length + actionableNoTranscript.length + buckets.partialDownloads.length; - const transcodePending = transcodeApplies ? buckets.untranscoded.length : 0; - const transcodeFailed = transcodeApplies ? failedTranscodingIds.length : 0; const transcribePending = actionableDownloadedNoTranscript.length; const transcribeFailed = failedVideoIds.length; const cleanupPending = buckets.multipleAudioFormats.length; @@ -322,45 +310,6 @@ export function computeStageStatuses( }), }; - const transcodeRunning = runningByStage.has("transcode"); - const transcodeParts: string[] = []; - if (transcodeApplies && buckets.untranscoded.length > 0) { - transcodeParts.push( - pluralize( - buckets.untranscoded.length, - "video missing target format", - "videos missing target format", - ), - ); - } - if (transcodeApplies && failedTranscodingIds.length > 0) { - transcodeParts.push( - pluralize(failedTranscodingIds.length, "failed transcoding"), - ); - } - const transcode: StageStatus = { - id: "transcode", - title: "Transcode", - pending: transcodePending, - failed: transcodeFailed, - running: transcodeRunning, - defaultOpen: transcodeApplies, - summary: !transcodeApplies - ? "Not applicable for this channel." - : transcodeRunning - ? "Running…" - : transcodeParts.length > 0 - ? transcodeParts.join(" · ") - : "Audio formats are aligned.", - tone: !transcodeApplies - ? "neutral" - : pickTone({ - running: transcodeRunning, - pending: transcodePending, - failed: transcodeFailed, - }), - }; - const transcribeRunning = runningByStage.has("transcribe"); const transcribeParts: string[] = []; if (actionableDownloadedNoTranscript.length > 0) { @@ -627,7 +576,6 @@ export function computeStageStatuses( configure, playlist, download, - transcode, transcribe, digest, speakers, diff --git a/editor/app/channels/[slug]/lib/videoRows.ts b/editor/app/channels/[slug]/lib/videoRows.ts @@ -13,8 +13,6 @@ export type VideoRow = { id: string; downloaded: boolean; transcribed: boolean; - // null when the channel doesn't apply transcoding. - transcoded: boolean | null; untranscribable: boolean; partial: boolean; // The audio-check pipeline gave up on a malformed source (download-outcome @@ -27,7 +25,6 @@ export type VideoRow = { // from corruptSource (which never finished) and from a failed transcription. corruptFullSource: boolean; failedTranscription: boolean; - failedTranscoding: boolean; // Has at least one finalized audio file not in the channel's target format — // the failed-extract orphans (audio.m4a with no audio.mp3) plus dirs with the // target alongside extra formats. Drives the "Select wrong-format" quick-select @@ -95,11 +92,10 @@ function matchesFilter(r: VideoRow, filter: VideoFilter): boolean { (r.status !== "transcribed" && r.status !== "untranscribable" && !r.excluded) || - r.failedTranscription || - r.failedTranscoding + r.failedTranscription ); case "failed": - return r.failedTranscription || r.failedTranscoding; + return r.failedTranscription; case "no_audio": return ( r.status === "no_audio" || diff --git a/editor/app/channels/[slug]/lib/videoRowsServer.ts b/editor/app/channels/[slug]/lib/videoRowsServer.ts @@ -24,8 +24,6 @@ export type ComputeRowsInput = { channelDataDirIds: string[]; snapshot: ChannelSnapshot; failedTranscriptionIds: string[]; - failedTranscodingIds: string[]; - transcodeApplies: boolean; runningJobs: JobRecord[]; excludedIds: Set<string>; }; @@ -46,7 +44,6 @@ export function computeVideoRows(input: ComputeRowsInput): VideoRow[] { const corruptSourceSet = new Set(buckets.corruptSource); const corruptFullSourceSet = new Set(buckets.corruptFullSource); const failedTranscription = new Set(input.failedTranscriptionIds); - const failedTranscoding = new Set(input.failedTranscodingIds); const runningIds = new Set<string>(); for (const j of input.runningJobs) { @@ -70,10 +67,8 @@ export function computeVideoRows(input: ComputeRowsInput): VideoRow[] { // corrupt-full-source (it has no transcript and isn't a transcription error). const isFailedT = failedTranscription.has(id) && !isCorruptSource && !isCorruptFullSource; - const isFailedX = failedTranscoding.has(id); const inNoTranscript = noTranscript.has(id); const inDownloadedNoTranscript = downloadedNoTranscript.has(id); - const inUntranscoded = untranscoded.has(id); const downloaded = isOnDisk && !inNoTranscript; // A kept corrupt-full-source has a raw container on disk but no usable @@ -84,11 +79,10 @@ export function computeVideoRows(input: ComputeRowsInput): VideoRow[] { !inNoTranscript && !inDownloadedNoTranscript && !isCorruptFullSource; - const transcoded = input.transcodeApplies ? !inUntranscoded : null; const excluded = input.excludedIds.has(id); let status: VideoRowStatus; - if (isFailedT || isFailedX) status = "failed"; + if (isFailedT) status = "failed"; else if (isCorruptSource) status = "corrupt_source"; else if (isCorruptFullSource) status = "corrupt_full_source"; else if (isPartial) status = "partial_download"; @@ -102,13 +96,11 @@ export function computeVideoRows(input: ComputeRowsInput): VideoRow[] { id, downloaded, transcribed, - transcoded, untranscribable: isUntranscribable, partial: isPartial, corruptSource: isCorruptSource, corruptFullSource: isCorruptFullSource, failedTranscription: isFailedT, - failedTranscoding: isFailedX, wrongFormatAudio: untranscoded.has(id) || multipleAudioFormats.has(id), excluded, diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx @@ -29,7 +29,6 @@ import { } from "yt-dlp-transcript-common/controller/channelSnapshot"; import { countPlaylist } from "yt-dlp-transcript-common/controller/channels"; import { loadFailedTranscriptions } from "yt-dlp-transcript-common/controller/failedTranscriptions"; -import { loadFailedTranscodings } from "yt-dlp-transcript-common/controller/failedTranscodings"; import { savedVideoTotals } from "yt-dlp-transcript-common/controller/savedVideoInventory"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { @@ -64,14 +63,12 @@ import { CleanupStage } from "./components/stages/CleanupStage"; import { DiagnosticsStage } from "./components/stages/DiagnosticsStage"; import { DownloadStage } from "./components/stages/DownloadStage"; import { PlaylistStage } from "./components/stages/PlaylistStage"; -import { TranscodeStage } from "./components/stages/TranscodeStage"; import { TranscribeStage } from "./components/stages/TranscribeStage"; import { DigestStage } from "./components/stages/DigestStage"; import { SpeakersStage } from "./components/stages/SpeakersStage"; import { getOperation, backfillLaneOperations, - operationApplies, operationsActionLabel, operationsGroupLabel, OPERATION_GROUP_ORDER, @@ -199,7 +196,6 @@ export default async function ChannelDetailPage({ // read them fresh on every render — the snapshot bucket only reflects state // at refresh time. const rawFailedVideoIds = await loadFailedTranscriptions(paths, slug); - const failedTranscodingIds = await loadFailedTranscodings(paths, slug); // ONE small readFile — the first station's denominator. Explicitly not the // corpus walk that noCorpusWalkInRenderPaths.test.ts bans. const playlistCount = await countPlaylist( @@ -224,7 +220,6 @@ export default async function ChannelDetailPage({ const actionableDownloadedNoTranscriptIds = buckets.downloadedNoTranscript.filter( (id) => !excludedDownloadIds.has(id), ); - const transcodeApplies = operationApplies("transcode", config); // Enabled lane backfills. A settings read, no I/O. // // backfillLaneOperations, still: this is the BACKFILL lane's list, and the snapshot @@ -234,7 +229,6 @@ export default async function ChannelDetailPage({ const stages = computeStageStatuses({ snapshot, failedVideoIds, - failedTranscodingIds, config, runningJobs, backfillEnabled: laneOperations.length > 0, @@ -250,15 +244,13 @@ export default async function ChannelDetailPage({ // The ends stay hand-listed. configure/playlist and cleanup/diagnostics/danger // are channel chores, not operations — see GROUP_STAGES. // - // `transcode` is filtered rather than omitted from the Record, because whether - // it applies is a per-CHANNEL fact (does this platform need a transcode pass) - // and the Record is a per-INSTALL one. + // `media`, `transcript`, `digest`, `speakers` each spread their stages; there + // is no per-channel filter — every stage in the Record applies to every + // channel. const stageOrder: StageId[] = [ "configure", "playlist", - ...OPERATION_GROUP_ORDER.flatMap((g) => GROUP_STAGES[g]).filter( - (id) => id !== "transcode" || transcodeApplies, - ), + ...OPERATION_GROUP_ORDER.flatMap((g) => GROUP_STAGES[g]), "cleanup", "diagnostics", "danger", @@ -267,8 +259,8 @@ export default async function ChannelDetailPage({ // `?stage=` replaces the old `#stage-` hash: the selection is server-rendered, // shareable, and survives the global AutoRefresh's router.refresh(). An // unknown or absent value lands on the overview rather than 404ing — a saved - // link to a stage that no longer applies (e.g. transcode) should still open - // the channel. + // link to a stage that no longer exists (`?stage=transcode`, retired + // 2026-08-30) should still open the channel. const rawStage = typeof sp.stage === "string" ? sp.stage : undefined; // RETIRED STAGE IDS, so a bookmark does not silently land somewhere else. // An unknown `?stage=` resolves to the OVERVIEW at 200 (pinned by @@ -286,8 +278,6 @@ export default async function ChannelDetailPage({ stages, config, failedVideoIds, - failedTranscodingIds, - transcodeApplies, laneOperations, playlistCount, }); @@ -375,16 +365,6 @@ export default async function ChannelDetailPage({ missingShard={await shard("download-missing")} /> ); - case "transcode": - return ( - <TranscodeStage - slug={slug} - existingQueues={existingQueues} - failedTranscodingIds={failedTranscodingIds} - untranscodedIds={buckets.untranscoded} - transcodeDefaultQueueKey={TRANSCRIPTION_QUEUE} - /> - ); case "transcribe": return ( <TranscribeStage @@ -479,7 +459,7 @@ export default async function ChannelDetailPage({ ...buckets.multipleAudioFormats, ]), ].sort()} - transcodeApplies={transcodeApplies} + wrongFormatApplies={!!config!.audioFormat} transcribedAudioBytes={ snapshot.cleanupBytes?.transcribedWithAudio ?? 0 } @@ -557,8 +537,6 @@ export default async function ChannelDetailPage({ buckets={buckets} digestWarnings={snapshot.buckets?.digestWarnings ?? []} failedVideoIds={failedVideoIds} - failedTranscodingIds={failedTranscodingIds} - transcodeApplies={transcodeApplies} generatedAt={snapshot.generatedAt} lastSyncedAt={config.lastSyncedAt} /> diff --git a/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx b/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx @@ -230,7 +230,6 @@ export function VideoPanel({ )} <PipelineStatusStrip downloaded={!noAudio} - transcoded={transcodeSources.length === 0} transcribed={hasTranscript} downloadFailed={downloadFailed} /> @@ -532,12 +531,10 @@ function VideoNavStrip({ function PipelineStatusStrip({ downloaded, - transcoded, transcribed, downloadFailed, }: { downloaded: boolean; - transcoded: boolean; transcribed: boolean; downloadFailed: boolean; }) { @@ -552,7 +549,6 @@ function PipelineStatusStrip({ label="Downloaded" tone={downloadFailed && !downloaded ? "danger" : undefined} /> - <StatusDot on={transcoded} label="Transcoded" /> <StatusDot on={transcribed} label="Transcribed" /> </div> ); diff --git a/editor/app/channels/[slug]/videos/page.tsx b/editor/app/channels/[slug]/videos/page.tsx @@ -4,14 +4,12 @@ import type { Dirent } from "node:fs"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { isSocialChannel } from "yt-dlp-transcript-common/lib/channelConfig"; -import { operationApplies } from "yt-dlp-transcript-common/lib/operations"; import type { ChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; import { excludedDownloadIdSet, readChannelSnapshot, } from "yt-dlp-transcript-common/controller/channelSnapshot"; import { loadFailedTranscriptions } from "yt-dlp-transcript-common/controller/failedTranscriptions"; -import { loadFailedTranscodings } from "yt-dlp-transcript-common/controller/failedTranscodings"; import { loadDownloadOutcome } from "yt-dlp-transcript-common/lib/downloadOutcome-server"; import { loadAvailability } from "yt-dlp-transcript-common/lib/availability-server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; @@ -131,8 +129,6 @@ export default async function ChannelVideosPage({ const failedVideoIds = (await loadFailedTranscriptions(paths, slug)).filter( (id) => !excludedDownloadIds.has(id), ); - const failedTranscodingIds = await loadFailedTranscodings(paths, slug); - const transcodeApplies = operationApplies("transcode", config); const channelDataDir = path.join(paths.channelsDir, slug, "data"); const channelDataDirIds = await readDataDirVideoIds(channelDataDir); @@ -140,8 +136,6 @@ export default async function ChannelVideosPage({ channelDataDirIds, snapshot, failedTranscriptionIds: failedVideoIds, - failedTranscodingIds, - transcodeApplies, runningJobs, excludedIds: excludedDownloadIds, }); diff --git a/editor/app/channels/[slug]/whisperActions.ts b/editor/app/channels/[slug]/whisperActions.ts @@ -16,13 +16,6 @@ import { readChannelStat, } from "yt-dlp-transcript-common/controller/channels"; import { runWhisperBatch } from "yt-dlp-transcript-common/controller/whisperBatch"; -import { runTranscodeFailures } from "yt-dlp-transcript-common/controller/transcodeFailures"; -import { runTranscodeUntranscoded } from "yt-dlp-transcript-common/controller/transcodeUntranscoded"; -import { removeFailedTranscodings } from "yt-dlp-transcript-common/controller/removeFailedTranscodings"; -import { - clearFailedTranscodings, - loadFailedTranscodings, -} from "yt-dlp-transcript-common/controller/failedTranscodings"; import { clearFailedTranscriptions } from "yt-dlp-transcript-common/controller/failedTranscriptions"; import { cleanAudioFromTranscribed } from "yt-dlp-transcript-common/controller/cleanAudioFromTranscribed"; import { diarizeAll } from "yt-dlp-transcript-common/controller/diarizeAll"; @@ -236,115 +229,6 @@ export async function clearFailedTranscriptionsAction( }); } -export async function transcodeFailuresAction( - slug: string, - targetFormat: AudioFormat, - queueKey?: string, -): Promise<StreamActionResult> { - if (!AUDIO_FORMAT_VALUES.includes(targetFormat)) { - return { ok: false, error: `Unsupported target format: ${targetFormat}` }; - } - const paths = getPaths(); - return runManagedFunction({ - kind: "transcode-failures", - queueKey: resolveQueueKey(TRANSCRIPTION_QUEUE, queueKey), - paths, - channelSlug: slug, - spec: { - kind: "transcode-failures", - slug, - params: { queueKey, targetFormat }, - }, - fn: async (onLog, signal) => { - const result = await runTranscodeFailures({ - channelSlug: slug, - paths, - targetFormat, - onLog, - signal, - }); - onLog( - `Transcode failures: ${result.succeeded} succeeded, ${result.failed} failed, ${result.skipped} skipped, ${result.attempted} attempted.`, - ); - revalidatePath(`/channels/${slug}`); - }, - }); -} - -export async function transcodeUntranscodedAction( - slug: string, - queueKey?: string, -): Promise<StreamActionResult> { - const paths = getPaths(); - return runManagedFunction({ - kind: "transcode-untranscoded", - queueKey: resolveQueueKey(TRANSCRIPTION_QUEUE, queueKey), - paths, - channelSlug: slug, - spec: { kind: "transcode-untranscoded", slug, params: { queueKey } }, - fn: async (onLog, signal) => { - const result = await runTranscodeUntranscoded({ - channelSlug: slug, - paths, - onLog, - signal, - }); - onLog( - `Transcode untranscoded: ${result.succeeded} succeeded, ${result.failed} failed, ${result.skipped} skipped, ${result.attempted} attempted.`, - ); - revalidatePath(`/channels/${slug}`); - }, - }); -} - -export async function removeFailedTranscodingsAction( - slug: string, - queueKey?: string, -): Promise<StreamActionResult> { - const paths = getPaths(); - return runManagedFunction({ - kind: "remove-failed-transcodings", - queueKey: resolveQueueKey(channelQueueKey(slug), queueKey), - paths, - channelSlug: slug, - spec: { kind: "remove-failed-transcodings", slug, params: { queueKey } }, - fn: async (onLog, signal) => { - const result = await removeFailedTranscodings({ - channelSlug: slug, - paths, - onLog, - signal, - }); - onLog( - `Remove failed-transcodings: ${result.removed} removed, ${result.missing} already gone.`, - ); - revalidatePath(`/channels/${slug}`); - }, - }); -} - -export async function clearFailedTranscodingsAction( - slug: string, - queueKey?: string, -): Promise<StreamActionResult> { - const paths = getPaths(); - return runManagedFunction({ - kind: "clear-failed-transcodings", - queueKey: resolveQueueKey(channelQueueKey(slug), queueKey), - paths, - channelSlug: slug, - spec: { kind: "clear-failed-transcodings", slug, params: { queueKey } }, - fn: async (onLog) => { - const ids = await loadFailedTranscodings(paths, slug); - await clearFailedTranscodings(paths, slug); - onLog( - `Cleared ${ids.length} entr${ids.length === 1 ? "y" : "ies"} from failed-transcodings; video dirs left intact.`, - ); - revalidatePath(`/channels/${slug}`); - }, - }); -} - export async function cleanExtraAudioFormatsAction( slug: string, queueKey?: string, diff --git a/editor/app/cleanup/components/ChannelCleanupCard.tsx b/editor/app/cleanup/components/ChannelCleanupCard.tsx @@ -11,7 +11,6 @@ import { cleanExtraAudioFormatsAction, removeWrongFormatAudioAction, clearFailedTranscriptionsAction, - clearFailedTranscodingsAction, } from "../../channels/[slug]/whisperActions"; import { ChannelCleanupToggle } from "./ChannelCleanupToggle"; @@ -24,7 +23,6 @@ export type ChannelCleanupCardProps = { foreignBytes: number; hasTargetFormat: boolean; failedTranscriptions: number; - failedTranscodings: number; }; export function ChannelCleanupCard({ @@ -36,13 +34,12 @@ export function ChannelCleanupCard({ foreignBytes, hasTargetFormat, failedTranscriptions, - failedTranscodings, }: ChannelCleanupCardProps) { const queueKey = `channel:${slug}`; const [armed, setArmed] = useState(false); const nothingToReclaim = transcribedBytes <= 0 && extraFormatsBytes <= 0 && foreignBytes <= 0; - const noHousekeeping = failedTranscriptions === 0 && failedTranscodings === 0; + const noHousekeeping = failedTranscriptions === 0; return ( <section @@ -149,15 +146,6 @@ export function ChannelCleanupCard({ label={`Clear failed transcriptions ${slug}`} /> )} - {failedTranscodings > 0 && ( - <StreamActionLog - trigger={() => clearFailedTranscodingsAction(slug, queueKey)} - cancelAction={cancelJobAction} - buttonLabel={`Clear failed transcodings (${failedTranscodings})`} - runningLabel="Clearing…" - label={`Clear failed transcodings ${slug}`} - /> - )} </div> )} </div> diff --git a/editor/app/cleanup/lib/loadCleanup.ts b/editor/app/cleanup/lib/loadCleanup.ts @@ -7,7 +7,6 @@ import { type HeldAudio, } from "yt-dlp-transcript-common/controller/channelSnapshot"; import { loadFailedTranscriptions } from "yt-dlp-transcript-common/controller/failedTranscriptions"; -import { loadFailedTranscodings } from "yt-dlp-transcript-common/controller/failedTranscodings"; // One channel's row in the cleanup ledger. The three reclaim estimates come // straight from the snapshot's precomputed `cleanupBytes` (already net of @@ -23,9 +22,8 @@ export type CleanupRow = { foreignBytes: number; // Gating for the extra-format / wrong-format sweeps (need a target format). hasTargetFormat: boolean; - // Housekeeping list sizes (failed transcriptions / transcodings). + // Housekeeping list size (failed transcriptions). failedTranscriptions: number; - failedTranscodings: number; // --- What is holding the audio this row can't reclaim -------------------- // Straight off the snapshot the brief already carries (CleanupRow.snapshot has // been loaded and unread since this file was written) — no new disk reads. A @@ -87,7 +85,6 @@ function rowOf( channel: ChannelBrief, snapshot: ChannelSnapshot | null, failedTranscriptions: number, - failedTranscodings: number, ): CleanupRow { return { channel, @@ -98,7 +95,6 @@ function rowOf( foreignBytes: snapshot?.cleanupBytes?.foreignAudio ?? 0, hasTargetFormat: Boolean(channel.config.audioFormat), failedTranscriptions, - failedTranscodings, measured: snapshot?.heldAudioBytes !== undefined, totalAudioBytes: snapshot?.totalAudioBytes ?? 0, heldBytes: snapshot?.heldAudioBytes ?? emptyHeldAudio(), @@ -134,13 +130,10 @@ export async function loadCleanupSummary( const channels = await getChannelBriefs(paths); const rows = await Promise.all( channels.map(async (channel) => { - // The brief already carries the snapshot; only the two failed-lists are - // still per-channel reads, and they run concurrently. - const [failedT, failedX] = await Promise.all([ - loadFailedTranscriptions(paths, channel.slug), - loadFailedTranscodings(paths, channel.slug), - ]); - return rowOf(channel, channel.snapshot, failedT.length, failedX.length); + // The brief already carries the snapshot; only the failed-transcriptions + // list is still a per-channel read. + const failedT = await loadFailedTranscriptions(paths, channel.slug); + return rowOf(channel, channel.snapshot, failedT.length); }), ); @@ -215,7 +208,7 @@ export type CleanableChannelRow = { // Lean per-channel breakdown for the sidebar badge and the widget API — the // primary "clean audio" reclaim over channels NOT excluded from the cleanup // total. Reads only the small snapshot JSONs (unlike loadCleanupSummary, which -// also reads both failed-lists per channel), so it's cheap on the AutoRefresh +// also reads the failed-transcriptions list per channel), so it's cheap on the AutoRefresh // cadence / per widget poll. Sorted by reclaim, descending. // // The comment above was true of the snapshot reads and false of the line that diff --git a/editor/app/cleanup/page.tsx b/editor/app/cleanup/page.tsx @@ -139,7 +139,6 @@ export default async function CleanupPage() { foreignBytes={r.foreignBytes} hasTargetFormat={r.hasTargetFormat} failedTranscriptions={r.failedTranscriptions} - failedTranscodings={r.failedTranscodings} /> ))} </section> diff --git a/editor/app/components/channelWork/sections.test.ts b/editor/app/components/channelWork/sections.test.ts @@ -76,7 +76,7 @@ test("sectionsFor puts work before attention", () => { test("an operation with no sections of its own gets none", () => { // Every runner/sweep page calls this with its own operation id; the ones - // with no work sections (diarization, attribution-*, transcode) must get an + // with no work sections (diarization, attribution-*) must get an // empty list rather than throwing, which is what makes the cast at the // /operations/[id] call site safe. assert.deepEqual( diff --git a/editor/app/jobs/jobReplayRegistry.ts b/editor/app/jobs/jobReplayRegistry.ts @@ -25,12 +25,8 @@ import { cleanAudioAction, cleanExtraAudioFormatsAction, diarizeChannelAction, - clearFailedTranscodingsAction, clearFailedTranscriptionsAction, - removeFailedTranscodingsAction, removeWrongFormatAudioAction, - transcodeFailuresAction, - transcodeUntranscodedAction, purgeSupersededAutoSubsAction, transcribeAutoSubsBucketAction, transcribeBucketAction, @@ -213,26 +209,6 @@ export const JOB_REPLAY_HANDLERS: Record<string, ReplayHandler> = { const { queueKey } = params(spec); return clearFailedTranscriptionsAction(spec.slug, queueKey); }, - "transcode-failures": (spec) => { - const { p, queueKey } = params(spec); - return transcodeFailuresAction( - spec.slug, - str(p.targetFormat) as AudioFormat, - queueKey, - ); - }, - "transcode-untranscoded": (spec) => { - const { queueKey } = params(spec); - return transcodeUntranscodedAction(spec.slug, queueKey); - }, - "remove-failed-transcodings": (spec) => { - const { queueKey } = params(spec); - return removeFailedTranscodingsAction(spec.slug, queueKey); - }, - "clear-failed-transcodings": (spec) => { - const { queueKey } = params(spec); - return clearFailedTranscodingsAction(spec.slug, queueKey); - }, "clean-extra-audio-formats": (spec) => { const { queueKey } = params(spec); return cleanExtraAudioFormatsAction(spec.slug, queueKey); diff --git a/editor/app/operations/[id]/page.tsx b/editor/app/operations/[id]/page.tsx @@ -222,9 +222,9 @@ export default async function OperationPage({ : await liveJobRows((j) => laneJobKinds.includes(j.kind)); // THE CHANNELS WITH THIS OPERATION'S WORK. The cast is safe only because an - // id the module does not know returns [] — diarization, both attribution - // operations and transcode get no table at all, and the summary is not even - // loaded for them. + // id the module does not know returns [] — diarization and both attribution + // operations get no table at all, and the summary is not even loaded for + // them. const sections = sectionsFor(op.id as SectionConfig["operation"]); const channelWork = sections.length > 0 ? ( diff --git a/editor/e2e/bulk-actions.spec.ts b/editor/e2e/bulk-actions.spec.ts @@ -125,8 +125,6 @@ test("bulk retry download submits a single retry-bucket job on the platform queu const dataDir = "test-transcripts/channels/test-transcribe/data"; const failedTranscriptionsFile = "test-transcripts/channels/test-transcribe/failed-transcriptions"; -const failedTranscodingsFile = - "test-transcripts/channels/test-transcribe/failed-transcodings"; // Delete directories and Clear failed markers are synchronous, summary-style // actions (no queued job) — the point of bulk delete is to clear failed @@ -173,36 +171,32 @@ test("Select failed + Delete directories removes the dirs and queues no job", as ).toHaveCount(0); }); -test("Clear failed markers prunes both failure files and clears the glyphs", async ({ +test("Clear failed markers prunes the failure file and clears the glyph", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); - // vidA failed transcription, vidB failed transcoding. + // vidA failed transcription. await writeFile(resolvePath(failedTranscriptionsFile), "vidA\n"); - await writeFile(resolvePath(failedTranscodingsFile), "vidB\n"); await generateReport(page, "test-transcribe"); await page.goto(channelVideos("test-transcribe")); - // Two failure glyphs are shown before clearing. - await expect(page.getByTitle("failure recorded")).toHaveCount(2); + // One failure glyph is shown before clearing. + await expect(page.getByTitle("failure recorded")).toHaveCount(1); await page.getByRole("button", { name: "Select failed", exact: true }).click(); - await expect(page.getByText("2 selected", { exact: true })).toBeVisible(); + await expect(page.getByText("1 selected", { exact: true })).toBeVisible(); await chooseAction(page, "Clear failed markers"); await clickApply(page); await expectSubmitted(page); - // The failure files no longer list the cleared ids (fresh-loaded each render, - // so the glyphs clear immediately too). + // The failure file no longer lists the cleared id (fresh-loaded each render, + // so the glyph clears immediately too). await expect(page.getByTitle("failure recorded")).toHaveCount(0); expect(await readFile(resolvePath(failedTranscriptionsFile), "utf8")).not.toContain( "vidA", ); - expect(await readFile(resolvePath(failedTranscodingsFile), "utf8")).not.toContain( - "vidB", - ); }); // Remove audio files / Remove wrong-format audio are synchronous, summary-style diff --git a/editor/e2e/helpers.ts b/editor/e2e/helpers.ts @@ -337,7 +337,6 @@ export type ChannelStage = | "configure" | "playlist" | "download" - | "transcode" | "transcribe" | "digest" | "speakers" diff --git a/editor/e2e/whisper.spec.ts b/editor/e2e/whisper.spec.ts @@ -224,41 +224,6 @@ test("clearing the failure list lets the next Transcribe missing retry them", as ); }); -test("Transcode failures re-encodes each listed video to the chosen format", async ({ - page, -}) => { - await resetData("one-transcribe-channel-with-audio"); - await writeFile( - resolvePath( - "test-transcripts/channels/test-transcribe/failed-transcriptions", - ), - "vidA\nvidB\n", - ); - await generateReport(page, "test-transcribe"); - await page.goto(channelStage("test-transcribe", "transcode")); - // Default format on the new dropdown is mp3. - await page - .getByRole("button", { name: /^Transcode failures → mp3$/ }) - .click(); - await expect(page.getByLabel("Transcode failures output")).toContainText( - "2 succeeded", - { timeout: 30_000 }, - ); - for (const id of ["vidA", "vidB"]) { - expect( - await pathExists( - `test-transcripts/channels/test-transcribe/data/${id}/audio.mp3`, - ), - ).toBe(true); - // Source m4a is preserved. - expect( - await pathExists( - `test-transcripts/channels/test-transcribe/data/${id}/audio.m4a`, - ), - ).toBe(true); - } -}); - test("Clean audio removes audio files only from transcribed videos", async ({ page, }) => {