feat: add manual video tagging

This commit is contained in:
2026-04-29 21:20:21 +08:00
parent 02d37f3953
commit eac6ad2343
17 changed files with 1066 additions and 418 deletions
+36 -1
View File
@@ -3,6 +3,30 @@ export const runtime = "nodejs";
import { type NextRequest, NextResponse } from "next/server";
import { transcriptAweme } from ".";
const runningJobs = new Map<string, Promise<void>>();
function startBackgroundAsr(awemeId: string) {
if (runningJobs.has(awemeId)) return false;
const job = transcriptAweme(awemeId, {
log: (message) => console.log(`[ASR ${awemeId}] ${message}`),
})
.then((result) => {
console.log(
`[ASR ${awemeId}] done tags=${result.content_tags.join("、")}`,
);
})
.catch((error) => {
console.error(`[ASR ${awemeId}] failed:`, error);
})
.finally(() => {
runningJobs.delete(awemeId);
});
runningJobs.set(awemeId, job);
return true;
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const awemeId = searchParams.get("awemeId");
@@ -12,8 +36,19 @@ export async function GET(req: NextRequest) {
{ status: 400 },
);
}
const asyncMode =
searchParams.get("async") === "1" || searchParams.get("mode") === "async";
if (asyncMode) {
const started = startBackgroundAsr(awemeId);
return NextResponse.json(
{ awemeId, status: started ? "started" : "running" },
{ status: 202 },
);
}
try {
const result = await transcriptAweme(awemeId, /* {log: (msg) => console.log(`[ASR] ${msg}`)} */);
const result = await transcriptAweme(awemeId);
return NextResponse.json(result);
} catch (error) {
console.error("ASR failed:", error);
+127
View File
@@ -0,0 +1,127 @@
export const runtime = "nodejs";
import { type NextRequest, NextResponse } from "next/server";
import { isAsrContentTag } from "@/lib/asr-tags";
import { prisma } from "@/lib/prisma";
type UpdateMode = "set" | "add";
function uniqueValidTags(tags: unknown[]) {
const validTags: string[] = [];
for (const tag of tags) {
if (typeof tag !== "string" || !isAsrContentTag(tag)) {
throw new Error("Invalid tag");
}
if (!validTags.includes(tag)) validTags.push(tag);
}
return validTags;
}
function resolveContentTags(manualTags: string[], asrTags: string[]) {
return manualTags.length > 0 ? manualTags : asrTags;
}
export async function POST(req: NextRequest) {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
if (!body || typeof body !== "object") {
return NextResponse.json(
{ error: "Invalid request body" },
{ status: 400 },
);
}
const raw = body as {
awemeId?: unknown;
awemeIds?: unknown;
tag?: unknown;
contentTags?: unknown;
mode?: unknown;
};
const awemeIds = Array.isArray(raw.awemeIds)
? raw.awemeIds
: typeof raw.awemeId === "string"
? [raw.awemeId]
: [];
const normalizedAwemeIds = [...new Set(awemeIds)]
.filter(
(id): id is string => typeof id === "string" && id.trim().length > 0,
)
.map((id) => id.trim());
if (normalizedAwemeIds.length === 0 || normalizedAwemeIds.length > 200) {
return NextResponse.json(
{ error: "awemeIds must contain 1 to 200 ids" },
{ status: 400 },
);
}
const mode: UpdateMode = raw.mode === "add" ? "add" : "set";
const rawTags = Array.isArray(raw.contentTags)
? raw.contentTags
: typeof raw.tag === "string"
? [raw.tag]
: [];
let contentTags: string[];
try {
contentTags = uniqueValidTags(rawTags);
} catch {
return NextResponse.json({ error: "Invalid tag" }, { status: 400 });
}
if (mode === "add" && contentTags.length === 0) {
return NextResponse.json(
{ error: "add mode requires at least one tag" },
{ status: 400 },
);
}
const videos = await prisma.video.findMany({
where: { aweme_id: { in: normalizedAwemeIds } },
select: {
aweme_id: true,
manual_content_tags: true,
transcript: { select: { content_tags: true } },
},
});
if (videos.length === 0) {
return NextResponse.json({ error: "No videos found" }, { status: 404 });
}
const updates = videos.map((video) => {
const asrTags = video.transcript?.content_tags ?? [];
const baseTags = resolveContentTags(video.manual_content_tags, asrTags);
const manualTags =
mode === "add"
? [...new Set([...baseTags, ...contentTags])]
: contentTags;
return { awemeId: video.aweme_id, manualTags, asrTags };
});
await prisma.$transaction(
updates.map((update) =>
prisma.video.update({
where: { aweme_id: update.awemeId },
data: { manual_content_tags: update.manualTags },
}),
),
);
return NextResponse.json({
updated: updates.map((update) => ({
aweme_id: update.awemeId,
manual_content_tags: update.manualTags,
content_tags: resolveContentTags(update.manualTags, update.asrTags),
})),
missing: normalizedAwemeIds.filter(
(id) => !videos.some((video) => video.aweme_id === id),
),
});
}