133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
import "../type";
|
|
import axios from "axios";
|
|
import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
|
|
import modelList from "../modelList";
|
|
|
|
export default async (input: VideoConfig, config: AIConfig) => {
|
|
if (!config.model) throw new Error("缺少Model名称");
|
|
if (!config.apiKey) throw new Error("缺少API Key");
|
|
if (!input.prompt && (!input.imageBase64 || input.imageBase64.length === 0)) {
|
|
throw new Error("至少需要提供prompt或图片");
|
|
}
|
|
|
|
const defaultBaseUrl = ["https://api.vidu.cn/ent/v2/text2video", "https://api.vidu.cn/ent/v2/img2video", "https://api.vidu.cn/ent/v2/tasks"].join(
|
|
"|",
|
|
);
|
|
|
|
const [text2videoUrl, image2videoUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
|
|
|
const authorization = `Token ${config.apiKey}`;
|
|
const hasImages = input.imageBase64 && input.imageBase64.length > 0;
|
|
|
|
// 根据是否有图片,查找匹配的模型配置
|
|
const customOwned = modelList.find((m) => {
|
|
if (m.manufacturer !== "vidu") return false;
|
|
if (m.model !== config.model) return false;
|
|
if (hasImages) {
|
|
return m.type.some((t) => t !== "text");
|
|
} else {
|
|
return m.type.includes("text");
|
|
}
|
|
});
|
|
|
|
if (!customOwned) {
|
|
throw new Error(`未找到匹配的模型配置: ${config.model}`);
|
|
}
|
|
|
|
// 使用统一校验函数
|
|
const { owned, images } = validateVideoConfig(input, config, customOwned);
|
|
|
|
// 判断生成类型
|
|
const genType: "text" | "image" = images.length === 0 ? "text" : "image";
|
|
|
|
// 校验宽高比(仅文生视频需要)
|
|
if (genType === "text" && owned.aspectRatio.length > 0 && !owned.aspectRatio.includes(input.aspectRatio as `${number}:${number}`)) {
|
|
throw new Error(`模型 ${owned.model} 不支持宽高比 ${input.aspectRatio},支持的宽高比:${owned.aspectRatio.join("、")}`);
|
|
}
|
|
|
|
// 创建任务
|
|
let taskId: string;
|
|
|
|
if (genType === "text") {
|
|
// 文生视频
|
|
const requestBody: Record<string, unknown> = {
|
|
model: owned.model,
|
|
prompt: input.prompt,
|
|
duration: input.duration,
|
|
resolution: input.resolution,
|
|
aspect_ratio: input.aspectRatio,
|
|
};
|
|
if (owned.audio && input.audio !== undefined) {
|
|
requestBody.audio = input.audio;
|
|
}
|
|
|
|
const response = await axios.post(text2videoUrl, requestBody, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: authorization,
|
|
},
|
|
});
|
|
taskId = response.data.task_id;
|
|
} else {
|
|
// 图生视频
|
|
const requestBody: Record<string, unknown> = {
|
|
model: owned.model,
|
|
images: images,
|
|
duration: input.duration,
|
|
resolution: input.resolution,
|
|
};
|
|
if (input.prompt) {
|
|
requestBody.prompt = input.prompt;
|
|
}
|
|
if (owned.audio && input.audio !== undefined) {
|
|
requestBody.audio = input.audio;
|
|
}
|
|
|
|
const response = await axios.post(image2videoUrl, requestBody, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: authorization,
|
|
},
|
|
});
|
|
taskId = response.data.task_id;
|
|
}
|
|
|
|
// 轮询任务状态
|
|
return await pollTask(async () => {
|
|
const response = await axios.get(queryUrl, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: authorization,
|
|
},
|
|
params: {
|
|
task_ids: [taskId],
|
|
},
|
|
});
|
|
|
|
const tasks = response.data.tasks;
|
|
if (!tasks || tasks.length === 0) {
|
|
return { completed: false, error: "任务不存在" };
|
|
}
|
|
|
|
const task = tasks[0];
|
|
|
|
switch (task.state) {
|
|
case "success": {
|
|
const creation = task.creations?.[0];
|
|
return {
|
|
completed: true,
|
|
url: creation?.url,
|
|
};
|
|
}
|
|
case "failed":
|
|
return { completed: false, error: "任务生成失败" };
|
|
case "created":
|
|
case "queueing":
|
|
case "processing":
|
|
return { completed: false };
|
|
default:
|
|
return { completed: false, error: `未知状态: ${task.state}` };
|
|
}
|
|
});
|
|
};
|