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 >[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 }); } }