// Client for the torch stage worker (~/dockers/torch-worker) — speaker diarization and the // "is this a conversation?" probe. The worker is deliberately dumb: it answers acoustic // questions about audio it is handed and holds no policy. Deciding what to do with a verdict // happens in index.ts. // // Every call passes `timeout: false`: a full diarization runs at roughly half realtime on CPU, // so an hour-long recording legitimately holds this connection for tens of minutes. The 255s // lessons of 2026-08-24 are not getting relearned here. type BunRequestInit = RequestInit & { timeout?: boolean | number }; export type SpeakerTurn = { start: number; end: number; speaker: string }; export type ProbeVerdict = 'solo' | 'conversation' | 'unsure'; export type ProbeResult = { duration: number; analyzed: { start: number; end: number; whole_file: boolean }; speakers_found: number; overlap_ratio: number; verdict: ProbeVerdict; /** Complete diarization when the probe covered the whole file — reuse it, skip /diarize. */ turns: SpeakerTurn[] | null; }; export type DiarizeResult = { duration: number; speakers: string[]; turns: SpeakerTurn[]; }; async function post(url: string, form: FormData): Promise { const res = await fetch(url, { method: 'POST', body: form, timeout: false } as BunRequestInit); if (!res.ok) { const detail = await res.text().catch(() => ''); throw new Error(`worker ${res.status}: ${detail.slice(0, 300)}`); } return (await res.json()) as T; } export async function probeAudio(file: File | Blob, workerUrl: string): Promise { const form = new FormData(); form.append('file', file); return post(`${workerUrl.replace(/\/+$/, '')}/probe`, form); } export async function diarizeAudio(file: File | Blob, workerUrl: string): Promise { const form = new FormData(); form.append('file', file); return post(`${workerUrl.replace(/\/+$/, '')}/diarize`, form); } export type WorkerProbe = { ok: true } | { ok: false; error: string }; /** Reachability check for PUT /config validation and /_health, mirroring probeWhisper. */ export async function probeWorker(workerUrl: string): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); try { const res = await fetch(`${workerUrl.replace(/\/+$/, '')}/health`, { signal: controller.signal }); if (!res.ok) return { ok: false, error: `worker answered ${res.status}` }; return { ok: true }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : 'could not reach the worker' }; } finally { clearTimeout(timer); } }