feat: 将视频转写重构为纯文字异步语音识别
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { Prisma, type TranscriptionTask } from "@prisma/client";
|
||||
import { prisma } from "../prisma";
|
||||
import { getAsrConfig } from "./dashscope";
|
||||
import { isActive, transcriptionState } from "./types";
|
||||
|
||||
function configurationError(): string | null {
|
||||
try {
|
||||
getAsrConfig();
|
||||
return null;
|
||||
} catch (error) {
|
||||
return (error as Error).message;
|
||||
}
|
||||
}
|
||||
export async function enqueueTranscription(videoId: string, force = false) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const videos = await tx.$queryRaw<
|
||||
{ aweme_id: string }[]
|
||||
>`SELECT aweme_id FROM "Video" WHERE aweme_id = ${videoId} FOR UPDATE`;
|
||||
if (!videos.length) return null;
|
||||
const [transcript, task] = await Promise.all([
|
||||
tx.videoTranscript.findUnique({ where: { videoId } }),
|
||||
tx.transcriptionTask.findUnique({ where: { videoId } }),
|
||||
]);
|
||||
if ((task && isActive(task.status)) || (!force && transcript))
|
||||
return transcriptionState(transcript, task);
|
||||
const lastError = configurationError();
|
||||
const data = {
|
||||
status: lastError ? "FAILED" : "QUEUED",
|
||||
lastError,
|
||||
providerTaskId: null,
|
||||
submittingAt: null,
|
||||
submittedAt: null,
|
||||
leaseUntil: null,
|
||||
leaseToken: null,
|
||||
attempts: 0,
|
||||
nextPollAt: new Date(),
|
||||
};
|
||||
const saved = await tx.transcriptionTask.upsert({
|
||||
where: { videoId },
|
||||
create: { videoId, ...data },
|
||||
update: data,
|
||||
});
|
||||
return transcriptionState(transcript, saved);
|
||||
});
|
||||
}
|
||||
export async function enqueueMissingTranscriptions() {
|
||||
const error = configurationError();
|
||||
if (error) throw new Error(error);
|
||||
// One durable, atomic batch. Completed historical transcripts are never requeued.
|
||||
return prisma.$executeRaw`
|
||||
INSERT INTO "TranscriptionTask" ("videoId", status, "updatedAt")
|
||||
SELECT v.aweme_id, 'QUEUED', (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') FROM "Video" v
|
||||
WHERE NOT EXISTS (SELECT 1 FROM "VideoTranscript" t WHERE t."videoId" = v.aweme_id)
|
||||
ON CONFLICT ("videoId") DO UPDATE SET status = 'QUEUED', "providerTaskId" = NULL,
|
||||
"submittingAt" = NULL, "submittedAt" = NULL, "leaseUntil" = NULL, "leaseToken" = NULL,
|
||||
attempts = 0, "lastError" = NULL, "nextPollAt" = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), "updatedAt" = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
|
||||
WHERE "TranscriptionTask".status = 'FAILED'
|
||||
`;
|
||||
}
|
||||
export async function readTranscription(videoId: string) {
|
||||
const video = await prisma.video.findUnique({
|
||||
where: { aweme_id: videoId },
|
||||
include: { transcript: true, transcriptionTask: true },
|
||||
});
|
||||
return video
|
||||
? transcriptionState(video.transcript, video.transcriptionTask)
|
||||
: null;
|
||||
}
|
||||
|
||||
export async function claimTask(): Promise<TranscriptionTask | null> {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
// Serialize admission across all worker processes, including PM2 restarts.
|
||||
await tx.$queryRaw`SELECT pg_advisory_xact_lock(74196321)::text`;
|
||||
const running = await tx.transcriptionTask.count({
|
||||
where: { status: "PROCESSING" },
|
||||
});
|
||||
const eligible =
|
||||
running < 2
|
||||
? Prisma.sql`status IN ('QUEUED', 'PROCESSING')`
|
||||
: Prisma.sql`status = 'PROCESSING'`;
|
||||
const rows = await tx.$queryRaw<TranscriptionTask[]>`
|
||||
SELECT * FROM "TranscriptionTask" WHERE ${eligible}
|
||||
AND "nextPollAt" <= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') AND ("leaseUntil" IS NULL OR "leaseUntil" < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'))
|
||||
ORDER BY CASE WHEN status = 'PROCESSING' THEN 0 ELSE 1 END, "nextPollAt", "createdAt"
|
||||
LIMIT 1 FOR UPDATE SKIP LOCKED
|
||||
`;
|
||||
if (!rows.length) return null;
|
||||
return tx.transcriptionTask.update({
|
||||
where: { videoId: rows[0].videoId },
|
||||
data: {
|
||||
status: "PROCESSING",
|
||||
leaseToken: randomUUID(),
|
||||
leaseUntil: new Date(Date.now() + 180_000),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
export async function updateOwnedTask(
|
||||
task: TranscriptionTask,
|
||||
data: Prisma.TranscriptionTaskUpdateManyMutationInput,
|
||||
) {
|
||||
const result = await prisma.transcriptionTask.updateMany({
|
||||
where: {
|
||||
videoId: task.videoId,
|
||||
leaseToken: task.leaseToken,
|
||||
status: "PROCESSING",
|
||||
leaseUntil: { gt: new Date() },
|
||||
},
|
||||
data,
|
||||
});
|
||||
if (!result.count) throw new Error("转写任务租约已失效");
|
||||
}
|
||||
export async function completeTask(
|
||||
task: TranscriptionTask,
|
||||
transcript: string[],
|
||||
) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const result = await tx.transcriptionTask.updateMany({
|
||||
where: {
|
||||
videoId: task.videoId,
|
||||
leaseToken: task.leaseToken,
|
||||
status: "PROCESSING",
|
||||
leaseUntil: { gt: new Date() },
|
||||
},
|
||||
data: {
|
||||
status: "SUCCEEDED",
|
||||
lastError: null,
|
||||
leaseToken: null,
|
||||
leaseUntil: null,
|
||||
attempts: 0,
|
||||
},
|
||||
});
|
||||
if (!result.count) throw new Error("转写任务租约已失效");
|
||||
await tx.videoTranscript.upsert({
|
||||
where: { videoId: task.videoId },
|
||||
create: { videoId: task.videoId, transcript },
|
||||
update: { transcript },
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user