import { json, error } from '@sveltejs/kit'; import { getJob, updateJob, setJobStatus } from '$lib/server/db.js'; import { writeOutputs } from '$lib/server/formatter.js'; import { sendNotification } from '$lib/server/push.js'; import { cleanupJobTmp, cleanupUploadDir } from '$lib/server/downloader.js'; import { emitProgress } from '$lib/server/pipeline.js'; import type { Segment, WhisperJob } from '$lib/types.js'; const WHISPER_JOB_STATUSES = new Set([ 'queued', 'running', 'done', 'failed', 'cancelled' ]); function isWhisperJobWebhook(payload: unknown): payload is WhisperJob { if (!payload || typeof payload !== 'object') return false; const candidate = payload as Record; return ( typeof candidate.id === 'string' && typeof candidate.status === 'string' && WHISPER_JOB_STATUSES.has(candidate.status as WhisperJob['status']) ); } export async function POST({ params, request }) { const jobId = params.jobId; const job = getJob(jobId); if (!job) throw error(404, 'Job not found'); const payload = (await request.json()) as unknown; if (!isWhisperJobWebhook(payload)) { // whisper-rtx2080 also fires model lifecycle events to registered job webhooks. return json({ ok: true, ignored: 'not_a_job_event' }); } const whisperJob = payload; // Discard the result if the job was cancelled locally while whisper was running if (job.status === 'cancelled') { return json({ ok: true }); } // Ignore stale callbacks from a previous whisper job after a local retry/reset. if (job.whisperJobId && whisperJob.id !== job.whisperJobId) { return json({ ok: true, ignored: 'stale_whisper_job' }); } // Ignore replayed success callbacks after the transcript is already persisted. if (job.status === 'done' && job.segmentsJson) { return json({ ok: true, ignored: 'duplicate_webhook' }); } if (whisperJob.status === 'failed' || whisperJob.status === 'cancelled') { const msg = whisperJob.error ?? `Whisper job ${whisperJob.status}`; updateJob({ id: jobId, status: 'failed', error: msg }); emitProgress(jobId, { type: 'error', message: msg }); return json({ ok: true }); } try { setJobStatus(jobId, 'processing', 90); emitProgress(jobId, { type: 'status', status: 'processing', progress: 90 }); const segments = (whisperJob.segments ?? []) as Segment[]; const paths = await writeOutputs(segments, job.title, jobId); const outputDir = paths.srt.replace(/\/[^/]+$/, ''); updateJob({ id: jobId, status: 'done', progress: 100, segmentsJson: JSON.stringify(segments), outputDir, transcriptSource: 'whisper' }); emitProgress(jobId, { type: 'done', status: 'done' }); await sendNotification(jobId, '✅ Transcript ready', job.title); await cleanupJobTmp(jobId); await cleanupUploadDir(jobId).catch(() => {}); return json({ ok: true }); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); updateJob({ id: jobId, status: 'failed', error: message }); emitProgress(jobId, { type: 'error', message }); return json({ ok: false, error: message }, { status: 500 }); } }