fix(whisper): handle model warmup events
Build & Push Docker Image / test (push) Successful in 12s
Build & Push Docker Image / build-and-push (push) Successful in 52s

- Ignore backend model lifecycle webhooks so model warmup does not
  mark jobs done early
- Parse batched SSE messages and relay model load states during
  submit retries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-15 00:08:32 +02:00
co-authored by Copilot
parent f70cefc5e9
commit 1072679360
4 changed files with 249 additions and 87 deletions
+78 -55
View File
@@ -6,62 +6,85 @@ import { cleanupJobTmp } 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<WhisperJob['status']>([
'queued',
'running',
'done',
'failed',
'cancelled'
]);
function isWhisperJobWebhook(payload: unknown): payload is WhisperJob {
if (!payload || typeof payload !== 'object') return false;
const candidate = payload as Record<string, unknown>;
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 jobId = params.jobId;
const job = getJob(jobId);
if (!job) throw error(404, 'Job not found');
const whisperJob = (await request.json()) as WhisperJob;
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
});
emitProgress(jobId, { type: 'done', status: 'done' });
await sendNotification(jobId, '✅ Transcript ready', job.title);
await cleanupJobTmp(jobId);
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 });
}
// 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
});
emitProgress(jobId, { type: 'done', status: 'done' });
await sendNotification(jobId, '✅ Transcript ready', job.title);
await cleanupJobTmp(jobId);
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 });
}
}