"use client"; import { useId, useState } from "react"; import type { Worker } from "yt-dlp-transcript-common/lib/workers"; import type { TranscriptionAppDescriptor } from "yt-dlp-transcript-common/lib/transcriptionApps"; // Its controlled checkboxes and selects sit inside action forms, whose reset they would not // survive alone (forms/Controlled.tsx). import { ControlledCheck, ControlledSelect, } from "../../components/forms/Controlled"; // Editor for the transcription worker list. Manages an ordered array of workers // (list order == priority, top = highest) and serializes it to a hidden JSON // input saveWorkersAction reads (mirrors SocialLinksField/socialLinksJson). The // server re-sanitizes and validates, so this component only needs to produce a // reasonable shape. // // BOTH IMPORTS BELOW ARE `import type` AND MUST STAY SO: workers.ts reaches // transcriptionApps.ts, which reaches paths.ts and node:fs. The tag vocabulary // and the app list are built on the server and passed down as props. type Row = Worker & { _key: number }; let keyCounter = 0; function withKeys(workers: Worker[]): Row[] { return workers.map((w) => ({ ...w, _key: keyCounter++ })); } function blankLocal(appId: string): Row { return { _key: keyCounter++, id: "", name: "", kind: "local", enabled: true, priority: 0, appId, config: {}, }; } function blankRemote(): Row { return { _key: keyCounter++, id: "", name: "", kind: "remote", enabled: true, priority: 0, remote: { baseUrl: "" }, }; } function blankLlm(): Row { return { _key: keyCounter++, id: "", name: "", kind: "llm", enabled: true, priority: 0, llm: { baseUrl: "" }, }; } // Duplicate a worker into a new slot: same config, fresh id, "(copy)" name. function copyOf(row: Row): Row { const { _key, id, name, ...rest } = row; void _key; void id; return { ...rest, _key: keyCounter++, id: "", name: name ? `${name} (copy)` : "", config: row.config ? { ...row.config } : undefined, remote: row.remote ? { ...row.remote } : undefined, llm: row.llm ? { ...row.llm } : undefined, }; } // Strip the React-only _key and set priority from list order before serializing. function serialize(rows: Row[]): string { return JSON.stringify( rows.map(({ _key, ...w }, i) => { void _key; return { ...w, priority: i }; }), ); } type Props = { initial: Worker[]; apps: TranscriptionAppDescriptor[]; name: string; // The known tag vocabulary (operation ids + resource names), built on the // server: the catalog lives in operations.ts, whose import graph must not // reach the client bundle. Used only to WARN about a tag the scheduler will // never match — an unknown tag saves fine. knownTags?: string[]; }; export function WorkersField({ initial, apps, name, knownTags }: Props) { const [rows, setRows] = useState(() => withKeys(initial)); const defaultAppId = apps[0]?.id ?? "whisper-cpp"; function update(next: Row[]) { setRows(next); } function patch(key: number, change: Partial) { update(rows.map((r) => (r._key === key ? { ...r, ...change } : r))); } function patchConfig(key: number, change: Partial>) { update( rows.map((r) => r._key === key ? { ...r, config: { ...r.config, ...change } } : r, ), ); } function patchRemote(key: number, change: Partial>) { update( rows.map((r) => r._key === key ? { ...r, remote: { baseUrl: "", ...r.remote, ...change } } : r, ), ); } function patchLlm(key: number, change: Partial>) { update( rows.map((r) => r._key === key ? { ...r, llm: { baseUrl: "", ...r.llm, ...change } } : r, ), ); } function remove(key: number) { update(rows.filter((r) => r._key !== key)); } function copy(index: number) { const next = [...rows]; next.splice(index + 1, 0, copyOf(rows[index])); update(next); } function move(index: number, dir: -1 | 1) { const j = index + dir; if (j < 0 || j >= rows.length) return; const next = [...rows]; [next[index], next[j]] = [next[j], next[index]]; update(next); } return (
{rows.length === 0 && (

No workers defined — add at least one so transcription can run.

)} {rows.map((row, i) => ( patch(row._key, c)} onPatchConfig={(c) => patchConfig(row._key, c)} onPatchRemote={(c) => patchRemote(row._key, c)} onPatchLlm={(c) => patchLlm(row._key, c)} onRemove={() => remove(row._key)} onCopy={() => copy(i)} onMove={(dir) => move(i, dir)} /> ))}
); } function WorkerCard({ row, index, total, apps, knownTags, onPatch, onPatchConfig, onPatchRemote, onPatchLlm, onRemove, onCopy, onMove, }: { row: Row; index: number; total: number; apps: TranscriptionAppDescriptor[]; knownTags?: string[]; onPatch: (c: Partial) => void; onPatchConfig: (c: Partial>) => void; onPatchRemote: (c: Partial>) => void; onPatchLlm: (c: Partial>) => void; onRemove: () => void; onCopy: () => void; onMove: (dir: -1 | 1) => void; }) { const uid = useId(); const app = apps.find((a) => a.id === row.appId); const cfg = row.config ?? {}; const remote = row.remote ?? { baseUrl: "" }; const llmCfg = row.llm ?? { baseUrl: "" }; return (
#{index + 1} onPatch({ name: e.target.value })} className="flex-1 min-w-40 rounded border border-border bg-card px-2 py-1 text-sm" />
{row.kind === "local" && ( )} {row.kind === "local" ? "Local · one slot" : row.kind === "llm" ? `LLM endpoint · ${llmCfg.slots ?? 1} slot${(llmCfg.slots ?? 1) === 1 ? "" : "s"}` : remote.slots !== undefined ? `Remote · ${remote.slots} slot${remote.slots === 1 ? "" : "s"}` : "Remote · slots probed from the remote"} onPatch({ tags: tags.length > 0 ? tags : undefined }) } />
{row.kind === "llm" ? (
onPatchLlm({ baseUrl: v })} hint="A bare ollama endpoint (e.g. http://macbook.lan:11434) — the box runs nothing but `ollama serve`. It must serve the primary's exact model tag; the scheduler checks and skips it if not." id={`${uid}-llmurl`} /> onPatchLlm({ slots: v.trim() === "" ? undefined : Number(v) }) } hint="Concurrent generations to send this endpoint. Blank = 1." id={`${uid}-llmslots`} />

Takes digest and speaker-attribution model calls only — never a transcription. Tags narrow which of those operations it serves; blank tags mean all of them.

) : row.kind === "local" ? (
onPatchConfig({ bin: v })} hint="Path/name of the executable. Blank = the engine's default." id={`${uid}-bin`} /> {app?.fields.model && ( onPatchConfig({ model: v })} hint="whisper.cpp: {model}. chough: CHOUGH_MODEL. parakeet: .gguf path. Blank = default." id={`${uid}-model`} /> )} {app?.fields.remoteUrl && ( onPatchConfig({ remoteUrl: v })} hint="chough only: transcribe via a chough --server (sets CHOUGH_URL). Point two copies of this worker at the same server for two togglable slots. Blank = run chough locally." id={`${uid}-remoteurl`} /> )} {app?.fields.device && ( onPatchConfig({ device: v })} hint="parakeet compute device (sets PARAKEET_DEVICE). e.g. cpu to force CPU, or CUDA0 / Vulkan1 for a specific GPU. Blank = parakeet-cli's default (first GPU)." id={`${uid}-device`} /> )} {app?.fields.chunkSize && ( onPatchConfig({ chunkSize: v.trim() === "" ? undefined : Number(v), }) } hint="parakeet: per-window seconds. chough: -c. Blank = default." id={`${uid}-chunk`} /> )} {app?.fields.customArgs && (