Archilyzer · Source

archilyzer

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

commit 273ac43ad6dbffda870eb02a83453067cbd3da9c
parent fca8cd098bf185b185eaada4f3e68cb4b8bd36e0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 19 May 2026 00:19:47 -0400

transcode failure handling

Diffstat:
Mcommon/controller/transcodeFailures.ts | 64+++++++++++++++++++++++++++++++++++++++++++++++++---------------
Acommon/controller/transcodeUntranscoded.ts | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/channels/[slug]/components/stages/TranscodeStage.tsx | 40+++++++++++++++++++++++++++++++++++++---
Meditor/app/channels/[slug]/lib/stageStatus.ts | 1+
Meditor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx | 11++++++++++-
Meditor/app/channels/[slug]/videos/[id]/page.tsx | 1+
Meditor/app/channels/[slug]/whisperActions.ts | 26++++++++++++++++++++++++++
7 files changed, 178 insertions(+), 19 deletions(-)

diff --git a/common/controller/transcodeFailures.ts b/common/controller/transcodeFailures.ts @@ -35,7 +35,15 @@ function pickSourceAudio( return [...candidates].sort()[0]; } -export type TranscodeFailuresOptions = { +export type TranscodeBatchResult = { + attempted: number; + succeeded: number; + failed: number; + skipped: number; +}; + +export type TranscodeIdsOptions = { + ids: string[]; channelSlug: string; paths: Paths; targetFormat: AudioFormat; @@ -44,33 +52,22 @@ export type TranscodeFailuresOptions = { signal?: AbortSignal; }; -export type TranscodeFailuresResult = { - attempted: number; - succeeded: number; - failed: number; - skipped: number; -}; - -export async function runTranscodeFailures({ +export async function transcodeIdsToFormat({ + ids, channelSlug, paths, targetFormat, concurrency, onLog, signal, -}: TranscodeFailuresOptions): Promise<TranscodeFailuresResult> { +}: TranscodeIdsOptions): Promise<TranscodeBatchResult> { const log = onLog ?? ((m: string) => console.log(m)); const channelDir = path.join(paths.channelsDir, channelSlug); const dataDir = path.join(channelDir, "data"); - const failureListFile = path.join(channelDir, "failed-transcriptions"); const transcodeFailureFile = failedTranscodingsFile(paths, channelSlug); const limit = pLimit(concurrency ?? 1); - await ensureFile(failureListFile); await ensureFile(transcodeFailureFile); - const ids = (await readFile(failureListFile, "utf-8")) - .split("\n") - .filter(Boolean); const previousTranscodeFailures = new Set( (await readFile(transcodeFailureFile, "utf-8")).split("\n").filter(Boolean), ); @@ -148,3 +145,40 @@ export async function runTranscodeFailures({ return { attempted, succeeded, failed, skipped }; } + +export type TranscodeFailuresOptions = { + channelSlug: string; + paths: Paths; + targetFormat: AudioFormat; + concurrency?: number; + onLog?: (msg: string) => void; + signal?: AbortSignal; +}; + +export type TranscodeFailuresResult = TranscodeBatchResult; + +export async function runTranscodeFailures({ + channelSlug, + paths, + targetFormat, + concurrency, + onLog, + signal, +}: TranscodeFailuresOptions): Promise<TranscodeFailuresResult> { + const channelDir = path.join(paths.channelsDir, channelSlug); + const failureListFile = path.join(channelDir, "failed-transcriptions"); + await ensureFile(failureListFile); + const ids = (await readFile(failureListFile, "utf-8")) + .split("\n") + .filter(Boolean); + + return transcodeIdsToFormat({ + ids, + channelSlug, + paths, + targetFormat, + concurrency, + onLog, + signal, + }); +} diff --git a/common/controller/transcodeUntranscoded.ts b/common/controller/transcodeUntranscoded.ts @@ -0,0 +1,54 @@ +import type { Paths } from "../lib/paths"; +import { readChannelConfig } from "./channels"; +import { readChannelSnapshot } from "./channelSnapshot"; +import { + transcodeIdsToFormat, + type TranscodeBatchResult, +} from "./transcodeFailures"; + +export type TranscodeUntranscodedOptions = { + channelSlug: string; + paths: Paths; + concurrency?: number; + onLog?: (msg: string) => void; + signal?: AbortSignal; +}; + +export async function runTranscodeUntranscoded({ + channelSlug, + paths, + concurrency, + onLog, + signal, +}: TranscodeUntranscodedOptions): Promise<TranscodeBatchResult> { + const log = onLog ?? ((m: string) => console.log(m)); + const config = await readChannelConfig(paths, channelSlug); + if (!config?.audioFormat) { + throw new Error( + `Channel ${channelSlug} has no audioFormat configured; set one before running this action.`, + ); + } + const snapshot = await readChannelSnapshot(paths, channelSlug); + const ids = snapshot?.buckets?.untranscoded ?? []; + if (!snapshot) { + log( + "No snapshot on disk — regenerate the channel snapshot first so the untranscoded bucket is populated.", + ); + } + if (ids.length === 0) { + log("Nothing to transcode: untranscoded bucket is empty."); + return { attempted: 0, succeeded: 0, failed: 0, skipped: 0 }; + } + log( + `Transcoding ${ids.length} untranscoded video(s) → audio.${config.audioFormat}`, + ); + return transcodeIdsToFormat({ + ids, + channelSlug, + paths, + targetFormat: config.audioFormat, + concurrency, + onLog: log, + signal, + }); +} diff --git a/editor/app/channels/[slug]/components/stages/TranscodeStage.tsx b/editor/app/channels/[slug]/components/stages/TranscodeStage.tsx @@ -12,6 +12,7 @@ import { clearFailedTranscodingsAction, removeFailedTranscodingsAction, transcodeFailuresAction, + transcodeUntranscodedAction, } from "../../whisperActions"; import { VideoIdList } from "../VideoIdList"; @@ -78,7 +79,12 @@ export function TranscodeStage({ } /> </div> - <UntranscodedList slug={slug} ids={untranscodedIds} /> + <UntranscodedList + slug={slug} + ids={untranscodedIds} + existingQueues={existingQueues} + defaultQueueKey={transcodeDefaultQueueKey} + /> <FailedTranscodingsSection slug={slug} ids={failedTranscodingIds} @@ -89,7 +95,18 @@ export function TranscodeStage({ ); } -function UntranscodedList({ slug, ids }: { slug: string; ids: string[] }) { +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-zinc-200 dark:border-zinc-800 p-3"> @@ -100,7 +117,8 @@ function UntranscodedList({ slug, ids }: { slug: string; ids: string[] }) { <p className="text-xs text-zinc-500"> Audio file present in a different format than the channel&apos;s configured audioFormat — re-run the transcoder to produce - audio.&lt;format&gt;. + audio.&lt;format&gt;. Failures land in the failed-transcodings list + below. </p> </div> <VideoIdList @@ -111,6 +129,22 @@ function UntranscodedList({ slug, ids }: { slug: string; ids: string[] }) { 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> ); } diff --git a/editor/app/channels/[slug]/lib/stageStatus.ts b/editor/app/channels/[slug]/lib/stageStatus.ts @@ -55,6 +55,7 @@ const JOB_KIND_TO_STAGE: Record<string, StageId> = { "download-from-playlist": "download", "download-missing": "download", "transcode-failures": "transcode", + "transcode-untranscoded": "transcode", "remove-failed-transcodings": "transcode", "clear-failed-transcodings": "transcode", "transcode-audio": "transcode", diff --git a/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx b/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx @@ -38,6 +38,7 @@ type Props = { defaultQueueKey: string; existingQueues: string[]; downloadOutcome: DownloadOutcomeRecord | null; + channelAudioFormat?: AudioFormat; }; const AUDIO_EXTS = new Set([ @@ -114,6 +115,7 @@ export function VideoPanel({ defaultQueueKey, existingQueues, downloadOutcome, + channelAudioFormat, }: Props) { const audioFiles = files.filter((f) => isAudioFile(f.name)); const transcodeSources = files.filter((f) => isTranscodeSource(f.name)); @@ -173,6 +175,7 @@ export function VideoPanel({ videoId={videoId} file={f} existingQueues={existingQueues} + channelAudioFormat={channelAudioFormat} /> ))} </div> @@ -377,15 +380,21 @@ function PerFileTranscodeRow({ videoId, file, existingQueues, + channelAudioFormat, }: { slug: string; videoId: string; file: VideoFile; existingQueues: string[]; + channelAudioFormat?: AudioFormat; }) { const ext = audioExt(file.name); const transcodeTargets = AUDIO_FORMAT_VALUES.filter((f) => f !== ext); - const [target, setTarget] = useState<AudioFormat>(transcodeTargets[0] ?? "opus"); + const preferredTarget = + channelAudioFormat && transcodeTargets.includes(channelAudioFormat) + ? channelAudioFormat + : transcodeTargets[0] ?? "opus"; + const [target, setTarget] = useState<AudioFormat>(preferredTarget); const [transcodeQueue, setTranscodeQueue] = useState(""); if (transcodeTargets.length === 0) return null; diff --git a/editor/app/channels/[slug]/videos/[id]/page.tsx b/editor/app/channels/[slug]/videos/[id]/page.tsx @@ -170,6 +170,7 @@ export default async function VideoDetailPage({ defaultQueueKey={defaultQueueKey} existingQueues={existingQueues} downloadOutcome={downloadOutcome} + channelAudioFormat={config.audioFormat} /> </div> ); diff --git a/editor/app/channels/[slug]/whisperActions.ts b/editor/app/channels/[slug]/whisperActions.ts @@ -9,6 +9,7 @@ import { import { TRANSCRIPTION_QUEUE } from "yt-dlp-transcript-common/lib/platform"; 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, @@ -127,6 +128,31 @@ export async function transcodeFailuresAction( }); } +export async function transcodeUntranscodedAction( + slug: string, + queueKey?: string, +): Promise<StreamActionResult> { + const paths = getPaths(); + return runManagedFunction({ + kind: "transcode-untranscoded", + queueKey: queueKey === undefined ? TRANSCRIPTION_QUEUE : queueKey.trim(), + paths, + channelSlug: slug, + 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,