import type { TranscriptionTask } from "@prisma/client"; import { prepareAudio } from "./audio"; import { AsrError, downloadTranscript, getAsrConfig, queryTranscription, submitTranscription, } from "./dashscope"; import { claimTask, completeTask, updateOwnedTask } from "./tasks"; export const defaultServices = { prepareAudio, submitTranscription, queryTranscription, downloadTranscript, }; export async function processTask( task: TranscriptionTask, services = defaultServices, ) { let submitting = false; const heartbeat = setInterval(() => { void updateOwnedTask(task, { leaseUntil: new Date(Date.now() + 180_000), }).catch(() => {}); }, 30_000); try { getAsrConfig(); if (!task.providerTaskId) { if (task.submittingAt) throw new AsrError( "上次提交结果不确定,请核对百炼任务后手动重试,避免重复计费", ); const url = await services.prepareAudio(task.videoId); if (url === null) { await completeTask(task, []); return; } await updateOwnedTask(task, { submittingAt: new Date() }); submitting = true; const providerTaskId = await services.submitTranscription(url); await updateOwnedTask(task, { providerTaskId, submittedAt: new Date(), nextPollAt: new Date(Date.now() + 10_000), leaseToken: null, leaseUntil: null, lastError: null, }); return; } // Provider result URLs expire after 24 hours; do not poll forever. if ( task.submittedAt && Date.now() - task.submittedAt.getTime() > 24 * 60 * 60 * 1000 ) { throw new AsrError("ASR 任务超过 24 小时,请手动重试"); } const result = await services.queryTranscription(task.providerTaskId); if (result.status === "SUCCEEDED") { const transcript = await services.downloadTranscript(result.resultURL); await completeTask(task, transcript); } else { await updateOwnedTask(task, { nextPollAt: new Date(Date.now() + 10_000), leaseToken: null, leaseUntil: null, lastError: null, attempts: 0, }); } } catch (error) { // Unknown exceptions (e.g. database outage) leave the lease and submitting marker // intact. Recovery can query a saved ID, but must never blindly submit again. if (!(error instanceof AsrError)) throw error; const retry = !submitting && !!task.providerTaskId && error.retryable; await updateOwnedTask(task, { status: retry ? "PROCESSING" : "FAILED", lastError: submitting && !error.definitiveRejection ? `${error.message};提交结果不确定,请核对百炼任务后手动重试` : error.message, attempts: { increment: 1 }, leaseToken: null, leaseUntil: null, nextPollAt: new Date( Date.now() + Math.min(300_000, 10_000 * 2 ** Math.min(task.attempts, 5)), ), }); console.warn(`[ASR] ${task.videoId}: ${retry ? "等待重试" : "失败"}`); } finally { clearInterval(heartbeat); } } export async function workerTick(services = defaultServices) { const tasks: TranscriptionTask[] = []; for (let i = 0; i < 2; i++) { const task = await claimTask(); if (task) tasks.push(task); } await Promise.all( tasks.map(async (task) => { try { await processTask(task, services); } catch { console.error(`[ASR] ${task.videoId}: 执行中断,租约到期后恢复`); } }), ); }