113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
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}: 执行中断,租约到期后恢复`);
|
|
}
|
|
}),
|
|
);
|
|
}
|