ai-chatbot/lib/video-canvas/production-services.ts

83 lines
4.8 KiB
TypeScript

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<string, { expires: number; pending: Promise<ReturnType<typeof extractPublicCatalogModels>> }>();
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<Record<string, boolean>> | 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;
});
}