- Set transcript_source to 'captions' when using VTT captions (caption fast path) - Set transcript_source to 'whisper' when whisper transcription completes via webhook - Add comprehensive test suite for VTT fallback scenarios - Tests verify empty/whitespace captions trigger whisper, non-empty captions skip whisper - Persists transcript_source to database for audit trail Closes story-d5-vtt-fallback-y-d1d6 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
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<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 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 });
|
|
}
|
|
}
|