Files
douyin-archive/lib/stt/worker.ts
T

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