2026-02-05 19:16:49 +08:00

169 lines
4.7 KiB
TypeScript

import "../type";
import axios from "axios";
import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
// 根据分辨率档位和宽高比计算具体尺寸
const getSizeFromConfig = (resolution: string, aspectRatio: string): string => {
const sizeMap: Record<string, Record<string, string>> = {
"480p": {
"16:9": "832*480",
"9:16": "480*832",
"1:1": "624*624",
},
"720p": {
"16:9": "1280*720",
"9:16": "720*1280",
"1:1": "960*960",
"4:3": "1088*832",
"3:4": "832*1088",
},
"1080p": {
"16:9": "1920*1080",
"9:16": "1080*1920",
"1:1": "1440*1440",
"4:3": "1632*1248",
"3:4": "1248*1632",
},
};
const resolutionKey = resolution.toLowerCase();
const size = sizeMap[resolutionKey]?.[aspectRatio];
if (!size) {
throw new Error(`不支持的分辨率(${resolution})和宽高比(${aspectRatio})组合`);
}
return size;
};
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
const { owned, images, hasStartEndType, hasTextType } = validateVideoConfig(input, config);
// 解析URL配置
const baseUrl = "https://dashscope.aliyuncs.com/api/v1";
const [
i2vUrl = baseUrl + "/services/aigc/video-generation/video-synthesis",
kf2vUrl = baseUrl + "/services/aigc/image2video/video-synthesis",
queryUrl = baseUrl + "/tasks",
] = (config.baseURL || "").split("|");
const types = owned.type;
const authorization = `Bearer ${config.apiKey}`;
// 确定端点和构建请求体
let submitUrl: string;
let body: Record<string, any>;
if (hasTextType && images.length === 0) {
// 文本生视频
submitUrl = i2vUrl;
body = {
model: config.model,
input: {
prompt: input.prompt,
},
parameters: {
size: getSizeFromConfig(input.resolution, input.aspectRatio),
duration: input.duration,
},
};
} else if (types.includes("singleImage")) {
// 图生视频
submitUrl = i2vUrl;
body = {
model: config.model,
input: {
prompt: input.prompt,
img_url: images[0],
},
parameters: {
resolution: input.resolution.toUpperCase(),
duration: input.duration,
},
};
// audio参数仅部分模型支持
if (owned.audio && input.audio !== undefined) {
body.parameters.audio = input.audio;
}
} else if (hasStartEndType) {
// 首尾帧
submitUrl = kf2vUrl;
const inputObj: Record<string, any> = {
prompt: input.prompt,
first_frame_url: images[0],
};
// 尾帧处理
if (types.includes("startEndRequired")) {
inputObj.last_frame_url = images[1];
} else if ((types.includes("endFrameOptional") || types.includes("startFrameOptional")) && images.length >= 2) {
inputObj.last_frame_url = images[1];
}
body = {
model: config.model,
input: inputObj,
parameters: {
resolution: input.resolution.toUpperCase(),
duration: input.duration,
},
};
} else {
throw new Error(`不支持的视频生成类型: ${types.join(", ")}`);
}
// 提交任务
const submitResponse = await axios.post(submitUrl, body, {
headers: {
"Content-Type": "application/json",
Authorization: authorization,
"X-DashScope-Async": "enable",
},
});
const submitData = submitResponse.data;
if (submitData.code) {
throw new Error(`任务提交失败: [${submitData.code}] ${submitData.message}`);
}
const taskId = submitData.output?.task_id;
if (!taskId) {
throw new Error("任务提交失败: 未返回task_id");
}
// 轮询任务状态
return await pollTask(async () => {
const response = await axios.get(`${queryUrl}/${taskId}`, {
headers: { Authorization: authorization },
});
const data = response.data;
// 顶层错误
if (data.code) {
return { completed: false, error: `[${data.code}] ${data.message}` };
}
const taskStatus = data.output?.task_status;
switch (taskStatus) {
case "SUCCEEDED":
return { completed: true, url: data.output?.video_url };
case "FAILED":
return {
completed: false,
error: `任务失败: [${data.output?.code || "UNKNOWN"}] ${data.output?.message || "未知错误"}`,
};
case "CANCELED":
return { completed: false, error: "任务已取消" };
case "UNKNOWN":
return { completed: false, error: "任务不存在或状态未知" };
case "PENDING":
case "RUNNING":
return { completed: false };
default:
return { completed: false, error: `未知状态: ${taskStatus}` };
}
});
};