import "server-only"; import { execFile } from "node:child_process"; import path from "node:path"; import { promisify } from "node:util"; import { getEnabledAiModelsWithProvider } from "@/lib/db/queries"; import { isCanvasVisionModel } from "./model-matching"; import { remoteRenderConfig } from "./smart-edit/remote-render"; import { remoteAudioConfig } from "./smart-edit/remote-audio"; import { upscaleConfig } from "./video-upscale-remote"; import { assertCanvasProductionServices } from "./production-preflight"; import type { CanvasTaskRequest } from "./task-contract"; import { extractPublicCatalogModels, normalizeVip98kBaseUrl } from "@/lib/ai/vip98k-models"; import { mapVip98kVideoParameters } from "@/lib/ai/vip98k-video-parameters"; const videoContracts = new Map> }>(); async function publishedVideoContracts(base: string) { const key = normalizeVip98kBaseUrl(base); const cached = videoContracts.get(key); if (cached && cached.expires > Date.now()) return cached.pending; const pending = fetch(key + "/api/v2/catalog/models", { cache: "no-store", signal: AbortSignal.timeout(5000), redirect: "error" }) .then(async response => { if (!response.ok) throw Error("无法核验供应商视频线路"); return extractPublicCatalogModels(await response.json()); }); videoContracts.set(key, { expires: Date.now() + 60_000, pending }); try { return await pending; } catch (error) { videoContracts.delete(key); throw error; } } const execute = promisify(execFile); export async function preflightCanvasProductionRequests(requests: CanvasTaskRequest[]) { const semanticRequests = requests.filter(request => request.kind === "smart-edit" && request.params.semantic === true); const videoRequests = requests.filter(request => request.kind === "video"); const enabledModels = semanticRequests.length || videoRequests.length ? await getEnabledAiModelsWithProvider(false) : []; for (const request of videoRequests) { const ref = request.params.preferredModelRef || request.params.model; const selected = enabledModels.find(item => item.model.id === ref || item.model.modelId === ref); if (selected?.provider.type !== "vip98k") continue; try { const catalog = await publishedVideoContracts(selected.provider.baseUrl); const current = catalog.find(item => item.id === selected.model.modelId); if (!current?.invocation?.input_schema) throw Error("当前视频线路没有可核验的规格"); mapVip98kVideoParameters({ aspect_ratio: request.params.ratio, resolution: request.params.resolution, duration_seconds: request.params.duration }, current.invocation.input_schema); } catch (error) { throw Object.assign(Error(`视频模型线路检查未通过:${error instanceof Error ? error.message : "规格不可用"};本次未启动生成,请同步模型配置或选择其他模型`), { status: 503, code: "CANVAS_SERVICE_UNAVAILABLE" }); } } if (semanticRequests.length) { const models = enabledModels.filter(item => isCanvasVisionModel({ ...item.model, providerType: item.provider.type })); for (const request of semanticRequests) { const ref = request.params.preferredModelRef; if (!models.length || (ref && ref !== "auto" && !models.some(item => item.model.id === ref || item.model.modelId === ref))) throw Object.assign(Error("智能选片所需的视觉模型不可用;本次未启动生成任务,请先配置或选择可用模型"), { status: 503, code: "CANVAS_SERVICE_UNAVAILABLE" }); } } let localAudio: Promise> | undefined; await assertCanvasProductionServices(requests, async service => { const config = service === "render" ? (process.env.SMART_EDIT_RENDER_FORCE_LOCAL === "1" ? null : remoteRenderConfig()) : service === "upscale" ? upscaleConfig() : remoteAudioConfig(); if (config) { const response = await fetch(config.base + "/health", { headers: { authorization: "Bearer " + config.token }, signal: AbortSignal.timeout(5000), redirect: "error", cache: "no-store", }); if (!response.ok) return false; const data = await response.json(); return data.status === "ok" && (service === "asr" || service === "separation" ? data.capabilities?.[service] === true : true); } if (service === "upscale") return false; if (service === "render") { const { stdout } = await execute("ffmpeg", ["-hide_banner", "-filters"], { timeout: 5000, maxBuffer: 262144 }); return /\bass\s+V->V/.test(stdout); } localAudio ??= execute(process.env.SMART_EDIT_PYTHON || "python3", [path.resolve("services/smart-edit/analyze.py"), "--check"], { timeout: 10000, maxBuffer: 65536 }).then(({ stdout }) => JSON.parse(stdout)); return (await localAudio)[service] === true; }); }