feat: add manual video tagging
This commit is contained in:
+36
-1
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user