227 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { generateText } from "ai";
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import { getCompatibleTextGenerationTemperature } from "@/lib/ai/model-compat";
import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
import { getAiModelAgentCodingModelId } from "@/lib/db/ai-model-flags";
import {
getAgentAiModelWithProvider,
getEnabledAiModelsWithProvider,
getSystemSetting,
} from "@/lib/db/queries";
import { createLogger } from "@/lib/observability/logger";
import {
VIDEO_CANVAS_FUNCTION_CONFIG_KEY,
defaultVideoCanvasFunctionConfig,
normalizeVideoCanvasFunctionConfig,
} from "@/lib/video-canvas/config";
import {
buildVideoPromptStructureInstructions,
injectVideoPromptStructure,
resolveVideoPromptStructure,
} from "@/lib/video-canvas/prompt-structure";
export const runtime = "nodejs";
export const maxDuration = 60;
const logger = createLogger("video-canvas-prompt-tools");
const requestSchema = z.object({
marks: z
.array(
z.object({
id: z.string().trim().min(1).max(100),
label: z.string().trim().min(1).max(100),
})
)
.max(12)
.default([]),
mode: z.enum(["optimize", "translate"]),
prompt: z.string().trim().min(1, "请输入提示词").max(50_000, "提示词过长"),
references: z
.array(
z.object({
id: z.string().trim().min(1).max(100),
index: z.number().int().min(1).max(10),
label: z.string().trim().min(1).max(100),
mediaType: z.enum(["audio", "image", "video"]),
offset: z.number().int().min(0).max(50_000),
})
)
.max(20)
.default([]),
});
type ModelCandidate = Awaited<
ReturnType<typeof getEnabledAiModelsWithProvider>
>[number];
function isTextModelCandidate(candidate: ModelCandidate) {
const descriptor = {
capabilities: candidate.model.capabilities,
modelId: candidate.model.modelId,
providerType: candidate.provider.type,
supportsImageGen: candidate.model.supportsImageGen,
};
return (
candidate.model.isEnabled &&
candidate.provider.isEnabled &&
!candidate.model.isSubModel &&
candidate.model.healthStatus !== "auto_disabled" &&
!isImageGenerationModelDescriptor(descriptor) &&
!isVideoModelDescriptor(descriptor)
);
}
function normalizeModelRef(value: unknown) {
const normalized = String(value || "").trim();
return normalized && normalized !== "auto" ? normalized : "";
}
function normalizeGeneratedText(value: unknown) {
return String(value || "")
.trim()
.replace(/^```(?:text|markdown|md)?\s*/i, "")
.replace(/\s*```$/i, "")
.replace(/^(?:优化后的提示词|译文|translation)\s*[::]\s*/i, "")
.trim();
}
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const parsed = requestSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ success: false, error: parsed.error.issues[0]?.message || "请求参数无效" },
{ status: 400 },
);
}
const setting = await getSystemSetting({ key: VIDEO_CANVAS_FUNCTION_CONFIG_KEY }).catch(
() => null,
);
const config = setting?.value
? normalizeVideoCanvasFunctionConfig(setting.value)
: defaultVideoCanvasFunctionConfig;
const functionKey =
parsed.data.mode === "optimize"
? "video-prompt-optimize"
: "video-prompt-translate";
const definition = config.functions[functionKey];
if (!definition?.enabled) {
return NextResponse.json(
{ success: false, error: `${definition?.label || "当前功能"}已停用` },
{ status: 403 },
);
}
const [allModels, agentModel] = await Promise.all([
getEnabledAiModelsWithProvider(false),
getAgentAiModelWithProvider(),
]);
const candidates = allModels.filter(isTextModelCandidate);
const preferredModelRef =
normalizeModelRef(definition.model) || normalizeModelRef(config.defaultMultimodalModel);
const preferredModel = preferredModelRef
? candidates.find(
(candidate) =>
candidate.model.id === preferredModelRef ||
candidate.model.modelId === preferredModelRef,
)
: null;
const orderedCandidates = [
preferredModel,
agentModel && isTextModelCandidate(agentModel) ? agentModel : null,
...candidates,
].filter(
(candidate, index, items): candidate is ModelCandidate =>
Boolean(candidate) &&
items.findIndex((item) => item?.model.id === candidate?.model.id) === index,
);
if (orderedCandidates.length === 0) {
return NextResponse.json(
{ success: false, error: "当前站点没有可用的提示词处理模型" },
{ status: 500 },
);
}
for (const candidate of orderedCandidates.slice(0, 3)) {
try {
const codingModelId = getAiModelAgentCodingModelId(candidate.model);
const model = await getLanguageModelById(candidate.model.id, codingModelId);
if (!model) continue;
const structuredPrompt = injectVideoPromptStructure(
parsed.data.prompt,
parsed.data.references,
parsed.data.marks,
);
const structureInstructions = buildVideoPromptStructureInstructions(
parsed.data.references,
parsed.data.marks,
);
const { text } = await generateText({
abortSignal: AbortSignal.timeout(18_000),
maxTokens: parsed.data.mode === "optimize" ? 2200 : 1600,
model,
prompt:
parsed.data.mode === "optimize"
? `原始视频提示词:\n${structuredPrompt}\n\n请按照系统规则输出优化后的提示词正文。`
: `待翻译的视频提示词:\n${structuredPrompt}\n\n请按照系统规则只输出译文。`,
system: [definition.prompt, structureInstructions].filter(Boolean).join("\n\n"),
temperature: getCompatibleTextGenerationTemperature(
candidate,
parsed.data.mode === "optimize" ? 0.65 : 0.25,
),
} as any);
const generatedText = normalizeGeneratedText(text);
if (!generatedText) continue;
const result = resolveVideoPromptStructure(
generatedText,
parsed.data.references,
parsed.data.marks,
{ flatten: parsed.data.mode === "optimize" },
);
if (!result.text) continue;
return NextResponse.json({
success: true,
data: {
mentionOffsets: result.mentionOffsets,
model: candidate.model.displayName,
preservedMarkIds: result.preservedMarkIds,
text: result.text,
},
});
} catch (error) {
logger.warn("prompt model candidate failed", {
error,
modelId: candidate.model.id,
});
}
}
return NextResponse.json(
{ success: false, error: "提示词处理模型暂时不可用,请稍后重试" },
{ status: 502 },
);
} catch (error) {
logger.error("prompt tool failed", { error });
const message = error instanceof Error ? error.message : "提示词处理失败";
return NextResponse.json({ success: false, error: message }, { status: 500 });
}
}