feat: 将视频转写重构为纯文字异步语音识别
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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}: 执行中断,租约到期后恢复`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user