83 lines
4.8 KiB
TypeScript
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;
|
|
});
|
|
}
|