169 lines
4.7 KiB
TypeScript
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}` };
|
|
}
|
|
});
|
|
};
|