完成重构AI模块
This commit is contained in:
parent
6ecc5dda99
commit
709c0cbd5a
561
src/utils/ai.ts
561
src/utils/ai.ts
@ -1,561 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import u from "@/utils";
|
|
||||||
import FormData from "form-data";
|
|
||||||
import axiosRetry from "axios-retry";
|
|
||||||
import { OpenAIChatModel, type OpenAIChatModelOptions } from "@aigne/openai";
|
|
||||||
import sharp from "sharp";
|
|
||||||
|
|
||||||
axiosRetry(axios, { retries: 3, retryDelay: () => 200 });
|
|
||||||
|
|
||||||
export const text = async (config: OpenAIChatModelOptions = {}) => {
|
|
||||||
const { model, apiKey, baseURL } = await u.getConfig("text");
|
|
||||||
return new OpenAIChatModel({
|
|
||||||
apiKey: apiKey ?? "",
|
|
||||||
baseURL: baseURL ?? "",
|
|
||||||
model: model ?? "gpt-4.1",
|
|
||||||
modelOptions: { temperature: 0.7 },
|
|
||||||
...config,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ImageConfig {
|
|
||||||
systemPrompt?: string;
|
|
||||||
prompt: string;
|
|
||||||
imageBase64: string[];
|
|
||||||
size: "1K" | "2K" | "4K";
|
|
||||||
aspectRatio: string;
|
|
||||||
resType?: "url" | "b64";
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlToBase64 = async (url: string): Promise<string> => {
|
|
||||||
const res = await axios.get(url, { responseType: "arraybuffer" });
|
|
||||||
const base64 = Buffer.from(res.data).toString("base64");
|
|
||||||
const mimeType = res.headers["content-type"] || "image/png";
|
|
||||||
return `data:${mimeType};base64,${base64}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
const pollTask = async (
|
|
||||||
queryFn: () => Promise<{ completed: boolean; imageUrl?: string; error?: string }>,
|
|
||||||
maxAttempts = 500,
|
|
||||||
interval = 2000,
|
|
||||||
): Promise<string> => {
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
await sleep(interval);
|
|
||||||
const { completed, imageUrl, error } = await queryFn();
|
|
||||||
if (error) throw new Error(error);
|
|
||||||
if (completed && imageUrl) return imageUrl;
|
|
||||||
}
|
|
||||||
throw new Error(`任务轮询超时,已尝试 ${maxAttempts} 次`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传 base64 图片到 runninghub
|
|
||||||
const uploadBase64ToRunninghub = async (base64Image: string, apiKey: string, baseURL: string): Promise<string> => {
|
|
||||||
try {
|
|
||||||
apiKey = apiKey.replace("Bearer ", "");
|
|
||||||
// 移除 base64 前缀
|
|
||||||
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
|
||||||
let buffer = Buffer.from(base64Data, "base64");
|
|
||||||
|
|
||||||
// 压缩图片到 5MB 以下
|
|
||||||
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
||||||
if (buffer.length > MAX_SIZE) {
|
|
||||||
let quality = 90;
|
|
||||||
|
|
||||||
while (buffer.length > MAX_SIZE && quality > 10) {
|
|
||||||
const compressed = await sharp(buffer).jpeg({ quality, mozjpeg: true }).toBuffer();
|
|
||||||
buffer = Buffer.from(compressed);
|
|
||||||
quality -= 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果仍然超过限制,进一步调整尺寸
|
|
||||||
if (buffer.length > MAX_SIZE) {
|
|
||||||
const metadata = await sharp(buffer).metadata();
|
|
||||||
const scale = Math.sqrt(MAX_SIZE / buffer.length);
|
|
||||||
|
|
||||||
const resized = await sharp(buffer)
|
|
||||||
.resize({
|
|
||||||
width: Math.floor((metadata.width || 1920) * scale),
|
|
||||||
height: Math.floor((metadata.height || 1080) * scale),
|
|
||||||
fit: "inside",
|
|
||||||
})
|
|
||||||
.jpeg({ quality: 80, mozjpeg: true })
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
buffer = Buffer.from(resized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 FormData
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", buffer, {
|
|
||||||
filename: "image.jpg",
|
|
||||||
contentType: "image/jpeg",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 上传图片
|
|
||||||
const uploadRes = await axios.post(`https://www.runninghub.cn/openapi/v2/media/upload/binary`, formData, {
|
|
||||||
headers: { Authorization: `Bearer ${apiKey}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uploadRes.data.code !== 0 || !uploadRes.data.data?.download_url) {
|
|
||||||
throw new Error(`图片上传失败: ${JSON.stringify(uploadRes.data)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return uploadRes.data.data.download_url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("上传图片时发生错误:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const generators = {
|
|
||||||
volcengine: async (config: ImageConfig, apiKey: string, baseURL: string, model: string) => {
|
|
||||||
if (config.size == "1K") config.size = "2K";
|
|
||||||
apiKey = apiKey.replace("Bearer ", "");
|
|
||||||
const body: Record<string, any> = {
|
|
||||||
model,
|
|
||||||
prompt: config.prompt,
|
|
||||||
size: config.size,
|
|
||||||
response_format: "url",
|
|
||||||
sequential_image_generation: "disabled",
|
|
||||||
stream: false,
|
|
||||||
watermark: false,
|
|
||||||
};
|
|
||||||
// 图生图:存在图片时添加 image 字段
|
|
||||||
if (config.imageBase64) {
|
|
||||||
body.image = config.imageBase64;
|
|
||||||
}
|
|
||||||
const res = await axios.post(`https://ark.cn-beijing.volces.com/api/v3/images/generations`, body, {
|
|
||||||
headers: { Authorization: `Bearer ${apiKey}` },
|
|
||||||
});
|
|
||||||
return res.data.data[0].url;
|
|
||||||
},
|
|
||||||
|
|
||||||
gemini: async (config: ImageConfig, apiKey: string, baseURL: string, model: string) => {
|
|
||||||
apiKey = apiKey.replace("Bearer ", "");
|
|
||||||
const messages = [
|
|
||||||
...(config.systemPrompt ? [{ role: "system", content: config.systemPrompt }] : []),
|
|
||||||
{ role: "user", content: config.prompt },
|
|
||||||
...config.imageBase64.map((img) => ({ role: "user", content: { image: img } })),
|
|
||||||
];
|
|
||||||
const res = await axios.post(
|
|
||||||
`${baseURL}/chat/completions`,
|
|
||||||
{ model, stream: false, messages, extra_body: { google: { image_config: { aspect_ratio: config.aspectRatio, image_size: config.size } } } },
|
|
||||||
{ headers: { Authorization: "Bearer " + apiKey } },
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.data.choices[0].message.content;
|
|
||||||
},
|
|
||||||
|
|
||||||
runninghub: async (config: ImageConfig, apiKey: string, baseURL: string) => {
|
|
||||||
apiKey = apiKey.replace("Bearer ", "");
|
|
||||||
const imageUrls = await Promise.all(config.imageBase64.map((base64Image) => uploadBase64ToRunninghub(base64Image, apiKey, baseURL)));
|
|
||||||
|
|
||||||
const endpoint = config.imageBase64.length === 0 ? "/openapi/v2/rhart-image-n-pro/text-to-image" : "/openapi/v2/rhart-image-n-pro/edit";
|
|
||||||
const taskRes = await axios.post(
|
|
||||||
`https://www.runninghub.cn${endpoint}`,
|
|
||||||
{ prompt: config.prompt, resolution: config.size, aspectRatio: config.aspectRatio, ...(imageUrls.length > 0 && { imageUrls }) },
|
|
||||||
{ headers: { Authorization: "Bearer " + apiKey } },
|
|
||||||
);
|
|
||||||
const taskId = taskRes.data.taskId;
|
|
||||||
if (!taskId) throw new Error(`任务创建失败,${JSON.stringify(taskRes.data)}`);
|
|
||||||
|
|
||||||
return pollTask(async () => {
|
|
||||||
const res = await axios.post(`https://www.runninghub.cn/task/openapi/outputs`, { taskId, apiKey: apiKey });
|
|
||||||
const { code, msg, data } = res.data;
|
|
||||||
if (code === 0 && msg === "success") return { completed: true, imageUrl: data?.[0]?.fileUrl };
|
|
||||||
if (code === 804 || code === 813) return { completed: false };
|
|
||||||
if (code === 805) return { completed: false, error: `任务失败: ${data?.[0]?.failedReason?.exception_message || "未知原因"}` };
|
|
||||||
return { completed: false, error: `未知状态: code=${code}, msg=${msg}` };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
apimart: async (config: ImageConfig, apiKey: string, baseURL: string, model: string) => {
|
|
||||||
apiKey = apiKey.replace("Bearer ", "");
|
|
||||||
const taskRes = await axios.post(
|
|
||||||
`https://api.apimart.ai/v1/images/generations`,
|
|
||||||
{ model: "gemini-3-pro-image-preview", prompt: config.prompt, size: config.aspectRatio, n: 1, resolution: config.size },
|
|
||||||
{ headers: { Authorization: apiKey } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (taskRes.data.code !== 200 || !taskRes.data.data?.[0]?.task_id) throw new Error("任务创建失败: " + JSON.stringify(taskRes.data));
|
|
||||||
|
|
||||||
const taskId = taskRes.data.data[0].task_id;
|
|
||||||
return pollTask(async () => {
|
|
||||||
const res = await axios.get(`https://api.apimart.ai/v1/tasks/${taskId}`, { headers: { Authorization: apiKey }, params: { language: "en" } });
|
|
||||||
if (res.data.code !== 200) return { completed: false, error: `查询失败: ${JSON.stringify(res.data)}` };
|
|
||||||
const { status, result } = res.data.data;
|
|
||||||
if (status === "completed") return { completed: true, imageUrl: result?.images?.[0]?.url?.[0] };
|
|
||||||
if (status === "failed" || status === "cancelled") return { completed: false, error: `任务${status}` };
|
|
||||||
return { completed: false };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateImage = async (config: ImageConfig, replaceConfig?: Awaited<ReturnType<typeof u.getConfig<"image">>>): Promise<string> => {
|
|
||||||
let { model, apiKey, baseURL, manufacturer } = await u.getConfig("image");
|
|
||||||
if (replaceConfig) {
|
|
||||||
model = replaceConfig.model || model;
|
|
||||||
apiKey = replaceConfig.apiKey || apiKey;
|
|
||||||
baseURL = replaceConfig.baseURL || baseURL;
|
|
||||||
manufacturer = replaceConfig.manufacturer || manufacturer;
|
|
||||||
}
|
|
||||||
const generator = generators[manufacturer as keyof typeof generators];
|
|
||||||
if (!generator) throw new Error(`不支持的厂商: ${manufacturer}`);
|
|
||||||
|
|
||||||
let imageUrl = await generator(config, apiKey ?? "", baseURL ?? "", model ?? "");
|
|
||||||
if (!config.resType) config.resType = "b64";
|
|
||||||
if (config.resType === "b64" && imageUrl.startsWith("http")) imageUrl = await urlToBase64(imageUrl);
|
|
||||||
return imageUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
type VideoAspectRatio = "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "21:9" | "adaptive";
|
|
||||||
interface BaseVideoConfig {
|
|
||||||
prompt: string;
|
|
||||||
savePath: string;
|
|
||||||
imageBase64?: string[]; // 单张参考图片 base64
|
|
||||||
}
|
|
||||||
interface DoubaoVideoConfig extends BaseVideoConfig {
|
|
||||||
duration: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; // 支持 2~12 秒
|
|
||||||
aspectRatio: VideoAspectRatio;
|
|
||||||
audio?: boolean;
|
|
||||||
}
|
|
||||||
interface RunninghubVideoConfig extends BaseVideoConfig {
|
|
||||||
duration: 10 | 15; // 仅支持 10 或 15 秒
|
|
||||||
aspectRatio: "16:9" | "9:16" | "1:1"; // 仅支持这三种比例
|
|
||||||
}
|
|
||||||
interface OpenAIVideoConfig extends BaseVideoConfig {
|
|
||||||
duration: 10 | 15; // 仅支持 10 或 15 秒
|
|
||||||
aspectRatio: Exclude<VideoAspectRatio, "adaptive">; // 不支持 adaptive
|
|
||||||
}
|
|
||||||
type VideoConfig = DoubaoVideoConfig | RunninghubVideoConfig | OpenAIVideoConfig;
|
|
||||||
const generateVideoWithConfig = async (config: VideoConfig, configItem: { model: string; apiKey: string; baseURL: string; manufacturer: string }) => {
|
|
||||||
const { apiKey, baseURL, manufacturer, model } = configItem;
|
|
||||||
const imageArrPath = [];
|
|
||||||
for (const imageVal of config?.imageBase64!) {
|
|
||||||
// 判断是否为base64串
|
|
||||||
const isBase64 = typeof imageVal === "string" && /^data:image\/[a-zA-Z0-9\+\-\.]+;base64,[\s\S]+$/.test(imageVal.trim());
|
|
||||||
if (isBase64) {
|
|
||||||
imageArrPath.push(imageVal);
|
|
||||||
} else {
|
|
||||||
const base64 = await urlToBase64(imageVal);
|
|
||||||
imageArrPath.push(base64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.imageBase64 = imageArrPath;
|
|
||||||
let videoUrl: string | null = null;
|
|
||||||
if (manufacturer === "volcengine") {
|
|
||||||
const doubaoConfig = config as DoubaoVideoConfig;
|
|
||||||
const createRes = await axios.post(
|
|
||||||
baseURL ?? "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks",
|
|
||||||
{
|
|
||||||
model: "doubao-seedance-1-5-pro-251215",
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: config.prompt },
|
|
||||||
...(doubaoConfig.imageBase64
|
|
||||||
? doubaoConfig.imageBase64.map((base64, i) => ({
|
|
||||||
type: "image_url",
|
|
||||||
image_url: { url: base64 },
|
|
||||||
role: i === 0 ? "first_frame" : "last_frame",
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
generate_audio: doubaoConfig.audio ?? false,
|
|
||||||
duration: doubaoConfig.duration,
|
|
||||||
resolution: doubaoConfig.aspectRatio,
|
|
||||||
watermark: false,
|
|
||||||
},
|
|
||||||
{ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` } },
|
|
||||||
);
|
|
||||||
const taskId = createRes.data.id;
|
|
||||||
if (!taskId) throw new Error("视频任务创建失败");
|
|
||||||
videoUrl = await pollTask(async () => {
|
|
||||||
const res = await axios.get(`${baseURL ?? "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks"}/${taskId}`, {
|
|
||||||
headers: { Authorization: `Bearer ${apiKey}` },
|
|
||||||
});
|
|
||||||
const { status, content } = res.data;
|
|
||||||
if (status === "succeeded") return { completed: true, imageUrl: content?.video_url };
|
|
||||||
if (["failed", "cancelled", "expired"].includes(status)) return { completed: false, error: `任务${status}` };
|
|
||||||
if (["queued", "running"].includes(status)) return { completed: false };
|
|
||||||
return { completed: false, error: `未知状态: ${status}` };
|
|
||||||
});
|
|
||||||
} else if (manufacturer === "runninghub") {
|
|
||||||
const runninghubConfig = config as RunninghubVideoConfig;
|
|
||||||
// 如果有图片,先上传
|
|
||||||
let uploadedImageUrl: string | undefined;
|
|
||||||
if (runninghubConfig.imageBase64 && runninghubConfig.imageBase64.length > 0) {
|
|
||||||
uploadedImageUrl = await uploadBase64ToRunninghub(runninghubConfig.imageBase64[0]!, apiKey ?? "", "https://www.runninghub.cn");
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = uploadedImageUrl ? "/openapi/v2/rhart-video-s/image-to-video" : "/openapi/v2/rhart-video-s/text-to-video";
|
|
||||||
const requestBody = uploadedImageUrl
|
|
||||||
? {
|
|
||||||
prompt: config.prompt,
|
|
||||||
imageUrl: uploadedImageUrl,
|
|
||||||
duration: String(runninghubConfig.duration) as "10" | "15",
|
|
||||||
aspectRatio: runninghubConfig.aspectRatio,
|
|
||||||
}
|
|
||||||
: { prompt: config.prompt, model };
|
|
||||||
const createRes = await axios.post(`https://www.runninghub.cn${endpoint}`, requestBody, {
|
|
||||||
headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { taskId, status: initialStatus, errorMessage } = createRes.data;
|
|
||||||
if (!taskId) throw new Error(`视频任务创建失败: ${errorMessage || "未知错误"}`);
|
|
||||||
if (initialStatus === "FAILED") throw new Error(`任务创建失败: ${errorMessage}`);
|
|
||||||
videoUrl = await pollTask(async () => {
|
|
||||||
const res = await axios.post(
|
|
||||||
`https://www.runninghub.cn/task/openapi/outputs`,
|
|
||||||
{ apiKey: apiKey?.replace("Bearer ", ""), taskId },
|
|
||||||
{ headers: { Authorization: "Bearer " + apiKey } },
|
|
||||||
);
|
|
||||||
const { code, msg, data } = res.data;
|
|
||||||
|
|
||||||
// 成功完成
|
|
||||||
if (code === 0 && msg === "success" && data?.[0]?.fileUrl) {
|
|
||||||
return { completed: true, imageUrl: data[0].fileUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进行中
|
|
||||||
if (code === 804 || code === 813) {
|
|
||||||
return { completed: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 失败
|
|
||||||
if (code === 805) {
|
|
||||||
const failedReason = data?.[0]?.failedReason;
|
|
||||||
let errorMsg = "未知原因";
|
|
||||||
|
|
||||||
if (failedReason) {
|
|
||||||
// 尝试多种可能的错误信息字段
|
|
||||||
errorMsg =
|
|
||||||
failedReason.exception_message ||
|
|
||||||
failedReason.exceptionMessage ||
|
|
||||||
failedReason.message ||
|
|
||||||
failedReason.reason ||
|
|
||||||
JSON.stringify(failedReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
completed: false,
|
|
||||||
error: `任务失败: ${errorMsg}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他未知状态
|
|
||||||
return {
|
|
||||||
completed: false,
|
|
||||||
error: `未知状态: code=${code}, msg=${msg}, data=${JSON.stringify(data)}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (manufacturer === "openAi") {
|
|
||||||
const openaiConfig = config as OpenAIVideoConfig;
|
|
||||||
// 如果有图片,先上传
|
|
||||||
let uploadedImageUrl: string | undefined;
|
|
||||||
if (openaiConfig.imageBase64 && openaiConfig.imageBase64.length) {
|
|
||||||
const base64Data = openaiConfig.imageBase64[0]!.replace(/^data:image\/\w+;base64,/, "");
|
|
||||||
const buffer = Buffer.from(base64Data, "base64");
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", buffer, { filename: "image.jpg", contentType: "image/jpeg" });
|
|
||||||
const uploadRes = await axios.post(`${baseURL}/videos`, formData, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
...formData.getHeaders(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
uploadedImageUrl = uploadRes.data?.id || uploadRes.data?.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建视频生成任务
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("model", model);
|
|
||||||
formData.append("prompt", config.prompt);
|
|
||||||
formData.append("seconds", String(openaiConfig.duration));
|
|
||||||
|
|
||||||
// 根据 aspectRatio 设置 size
|
|
||||||
const sizeMap: Record<string, string> = {
|
|
||||||
"16:9": "1920x1080",
|
|
||||||
"9:16": "1080x1920",
|
|
||||||
"1:1": "1080x1080",
|
|
||||||
"4:3": "1440x1080",
|
|
||||||
"3:4": "1080x1440",
|
|
||||||
"21:9": "2560x1080",
|
|
||||||
};
|
|
||||||
formData.append("size", sizeMap[openaiConfig.aspectRatio] || "1920x1080");
|
|
||||||
if (uploadedImageUrl) {
|
|
||||||
formData.append("input_reference", uploadedImageUrl);
|
|
||||||
}
|
|
||||||
const createRes = await axios.post(`${baseURL}/videos`, formData, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
...formData.getHeaders(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const taskId = createRes.data?.id;
|
|
||||||
|
|
||||||
if (!taskId) throw new Error("视频任务创建失败");
|
|
||||||
// 轮询任务状态
|
|
||||||
videoUrl = await pollTask(async () => {
|
|
||||||
const res = await axios.get(`${baseURL}/videos/${taskId}`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { status, imageUrl, failReason } = res.data;
|
|
||||||
if (status === "SUCCESS") return { completed: true, imageUrl };
|
|
||||||
if (status === "FAILURE" || status === "CANCEL") {
|
|
||||||
return { completed: false, error: `任务${status}: ${failReason || "未知原因"}` };
|
|
||||||
}
|
|
||||||
if (["NOT_START", "SUBMITTED", "IN_PROGRESS", "MODAL"].includes(status)) {
|
|
||||||
return { completed: false };
|
|
||||||
}
|
|
||||||
return { completed: false, error: `未知状态: ${status}` };
|
|
||||||
});
|
|
||||||
} else if (manufacturer === "apimart") {
|
|
||||||
// apimart 视频生成
|
|
||||||
const apimartConfig = config as OpenAIVideoConfig;
|
|
||||||
const apimartBaseURL = "https://api.apimart.ai";
|
|
||||||
|
|
||||||
// 上传图片到 apimart 图床
|
|
||||||
let imageUrls: string[] = [];
|
|
||||||
if (apimartConfig.imageBase64 && apimartConfig.imageBase64.length > 0) {
|
|
||||||
for (const base64Image of apimartConfig.imageBase64) {
|
|
||||||
// 如果已经是 URL,直接使用
|
|
||||||
if (base64Image.startsWith("http")) {
|
|
||||||
imageUrls.push(base64Image);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取预签名 URL
|
|
||||||
const presignRes = await axios.post(
|
|
||||||
"https://apimart.ai/api/upload/presign",
|
|
||||||
{ contentType: "image/jpeg", fileExtension: "jpeg", permanent: false },
|
|
||||||
{ headers: { "Content-Type": "application/json" } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!presignRes.data.success || !presignRes.data.presignedUrl || !presignRes.data.cdnUrl) {
|
|
||||||
throw new Error(`获取预签名 URL 失败: ${JSON.stringify(presignRes.data)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { presignedUrl, cdnUrl } = presignRes.data;
|
|
||||||
|
|
||||||
// 移除 base64 前缀并转为 buffer
|
|
||||||
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
|
||||||
const buffer = Buffer.from(base64Data, "base64");
|
|
||||||
|
|
||||||
// 上传图片到预签名 URL
|
|
||||||
await axios.put(presignedUrl, buffer, {
|
|
||||||
headers: { "Content-Type": "image/jpeg" },
|
|
||||||
});
|
|
||||||
|
|
||||||
imageUrls.push(cdnUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建视频生成任务
|
|
||||||
const requestBody: {
|
|
||||||
model: string;
|
|
||||||
prompt: string;
|
|
||||||
duration: number;
|
|
||||||
aspect_ratio: string;
|
|
||||||
image_urls?: string[];
|
|
||||||
} = {
|
|
||||||
model: model || "sora-2",
|
|
||||||
prompt: config.prompt,
|
|
||||||
duration: apimartConfig.duration,
|
|
||||||
aspect_ratio: apimartConfig.aspectRatio,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (imageUrls.length > 0) {
|
|
||||||
requestBody.image_urls = imageUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createRes = await axios.post(`${apimartBaseURL}/v1/videos/generations`, requestBody, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (createRes.data.code !== 200 || !createRes.data.data?.[0]?.task_id) {
|
|
||||||
const errorMsg = createRes.data.error?.message || JSON.stringify(createRes.data);
|
|
||||||
throw new Error(`视频任务创建失败: ${errorMsg}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskId = createRes.data.data[0].task_id;
|
|
||||||
|
|
||||||
// 轮询任务状态
|
|
||||||
videoUrl = await pollTask(async () => {
|
|
||||||
const res = await axios.get(`${apimartBaseURL}/v1/tasks/${taskId}`, {
|
|
||||||
headers: { Authorization: `Bearer ${apiKey}` },
|
|
||||||
params: { language: "en" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否有错误
|
|
||||||
if (res.data.error) {
|
|
||||||
return {
|
|
||||||
completed: false,
|
|
||||||
error: `查询失败: ${res.data.error.message || JSON.stringify(res.data.error)}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.data.code !== 200) {
|
|
||||||
return { completed: false, error: `查询失败: ${JSON.stringify(res.data)}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, result } = res.data.data;
|
|
||||||
|
|
||||||
if (status === "completed") {
|
|
||||||
// 获取视频 URL
|
|
||||||
const videoUrlResult = result?.videos?.[0]?.url?.[0];
|
|
||||||
return { completed: true, imageUrl: videoUrlResult };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "failed" || status === "cancelled") {
|
|
||||||
return { completed: false, error: `任务${status}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他状态(submitted, processing 等)继续轮询
|
|
||||||
return { completed: false };
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(`不支持的厂商: ${manufacturer}`);
|
|
||||||
}
|
|
||||||
return videoUrl;
|
|
||||||
};
|
|
||||||
export const generateVideo = async (config: VideoConfig, manufacturer: string) => {
|
|
||||||
if (!config.imageBase64 || config.imageBase64.length <= 0) throw new Error("未传图片");
|
|
||||||
const configList = await u.getConfig("video", manufacturer);
|
|
||||||
console.log("%c Line:533 🥔 configList", "background:#ea7e5c", configList);
|
|
||||||
if (!configList || configList.length === 0) {
|
|
||||||
throw new Error("未找到任何视频配置");
|
|
||||||
}
|
|
||||||
let lastError: Error | null = null;
|
|
||||||
for (const configItem of configList) {
|
|
||||||
// 每个配置项重试1次,共2次尝试
|
|
||||||
for (let attempt = 0; attempt < 2; attempt++) {
|
|
||||||
try {
|
|
||||||
const videoUrl = await generateVideoWithConfig(config, configItem);
|
|
||||||
if (videoUrl) {
|
|
||||||
const response = await axios.get(videoUrl, { responseType: "stream" });
|
|
||||||
await u.oss.writeFile(config.savePath, response.data);
|
|
||||||
return config.savePath;
|
|
||||||
}
|
|
||||||
return videoUrl;
|
|
||||||
} catch (error: any) {
|
|
||||||
lastError = error as Error;
|
|
||||||
console.warn(`配置 ${configItem.model} 第 ${attempt + 1} 次尝试失败:`, error?.response?.data || error.message);
|
|
||||||
// 如果是第一次尝试失败,继续重试
|
|
||||||
if (attempt === 0) continue;
|
|
||||||
// 第二次也失败了,跳到下一个配置项
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 所有配置都失败了
|
|
||||||
throw new Error(`所有视频配置都失败了。最后一次错误: ${lastError?.message || "未知错误"}`);
|
|
||||||
};
|
|
||||||
@ -24,7 +24,7 @@ const modelInstance = {
|
|||||||
kling: kling,
|
kling: kling,
|
||||||
vidu: vidu,
|
vidu: vidu,
|
||||||
runninghub: runninghub,
|
runninghub: runninghub,
|
||||||
apimart: apimart,
|
// apimart: apimart,
|
||||||
other,
|
other,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import vidu from "./owned/vidu";
|
|||||||
import wan from "./owned/wan";
|
import wan from "./owned/wan";
|
||||||
import runninghub from "./owned/runninghub";
|
import runninghub from "./owned/runninghub";
|
||||||
import gemini from "./owned/gemini";
|
import gemini from "./owned/gemini";
|
||||||
|
import apimart from "./owned/apimart";
|
||||||
|
|
||||||
const modelInstance = {
|
const modelInstance = {
|
||||||
volcengine: volcengine,
|
volcengine: volcengine,
|
||||||
@ -17,7 +18,7 @@ const modelInstance = {
|
|||||||
wan: wan,
|
wan: wan,
|
||||||
gemini: gemini,
|
gemini: gemini,
|
||||||
runninghub: runninghub,
|
runninghub: runninghub,
|
||||||
apimart: null,
|
apimart: apimart,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default async (input: VideoConfig, config?: AIConfig) => {
|
export default async (input: VideoConfig, config?: AIConfig) => {
|
||||||
|
|||||||
@ -450,7 +450,7 @@ const modelList: Owned[] = [
|
|||||||
// sora
|
// sora
|
||||||
{
|
{
|
||||||
manufacturer: "runninghub",
|
manufacturer: "runninghub",
|
||||||
model: "sora",
|
model: "sora-2",
|
||||||
durationResolutionMap: [{ duration: [10, 15], resolution: [] }],
|
durationResolutionMap: [{ duration: [10, 15], resolution: [] }],
|
||||||
aspectRatio: ["16:9", "9:16"],
|
aspectRatio: ["16:9", "9:16"],
|
||||||
type: ["singleImage", "text"],
|
type: ["singleImage", "text"],
|
||||||
@ -459,7 +459,26 @@ const modelList: Owned[] = [
|
|||||||
// sora 2
|
// sora 2
|
||||||
{
|
{
|
||||||
manufacturer: "runninghub",
|
manufacturer: "runninghub",
|
||||||
|
model: "sora-2-pro",
|
||||||
|
durationResolutionMap: [{ duration: [15, 25], resolution: [] }],
|
||||||
|
aspectRatio: ["16:9", "9:16"],
|
||||||
|
type: ["singleImage", "text"],
|
||||||
|
audio: false,
|
||||||
|
},
|
||||||
|
// ================== Apimart 系列 ==================
|
||||||
|
// sora
|
||||||
|
{
|
||||||
|
manufacturer: "apimart",
|
||||||
model: "sora-2",
|
model: "sora-2",
|
||||||
|
durationResolutionMap: [{ duration: [10, 15], resolution: [] }],
|
||||||
|
aspectRatio: ["16:9", "9:16"],
|
||||||
|
type: ["singleImage", "text"],
|
||||||
|
audio: false,
|
||||||
|
},
|
||||||
|
// sora 2
|
||||||
|
{
|
||||||
|
manufacturer: "apimart",
|
||||||
|
model: "sora-2-pro",
|
||||||
durationResolutionMap: [{ duration: [15, 25], resolution: [] }],
|
durationResolutionMap: [{ duration: [15, 25], resolution: [] }],
|
||||||
aspectRatio: ["16:9", "9:16"],
|
aspectRatio: ["16:9", "9:16"],
|
||||||
type: ["singleImage", "text"],
|
type: ["singleImage", "text"],
|
||||||
|
|||||||
115
src/utils/ai/video/owned/apimart.ts
Normal file
115
src/utils/ai/video/owned/apimart.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import "../type";
|
||||||
|
import axios from "axios";
|
||||||
|
import { pollTask } from "@/utils/ai/utils";
|
||||||
|
import modelList from "../modelList";
|
||||||
|
|
||||||
|
// 上传图片到 apimart 图床
|
||||||
|
async function uploadImageToApimart(base64Image: string): Promise<string> {
|
||||||
|
if (base64Image.startsWith("http")) {
|
||||||
|
return base64Image;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presignRes = await axios.post(
|
||||||
|
"https://apimart.ai/api/upload/presign",
|
||||||
|
{ contentType: "image/jpeg", fileExtension: "jpeg", permanent: false },
|
||||||
|
{ headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!presignRes.data.success || !presignRes.data.presignedUrl || !presignRes.data.cdnUrl) {
|
||||||
|
throw new Error(`获取预签名 URL 失败: ${JSON.stringify(presignRes.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { presignedUrl, cdnUrl } = presignRes.data;
|
||||||
|
|
||||||
|
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
||||||
|
const buffer = Buffer.from(base64Data, "base64");
|
||||||
|
|
||||||
|
await axios.put(presignedUrl, buffer, {
|
||||||
|
headers: { "Content-Type": "image/jpeg" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return cdnUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (input: VideoConfig, config: AIConfig) => {
|
||||||
|
if (!config.model) throw new Error("缺少 Model 名称");
|
||||||
|
if (!config.apiKey) throw new Error("缺少 API Key");
|
||||||
|
|
||||||
|
const owned = modelList.find((m) => m.model === config.model);
|
||||||
|
if (!owned) throw new Error(`未找到模型: ${config.model}`);
|
||||||
|
|
||||||
|
// 默认 baseURL 配置
|
||||||
|
const defaultBaseUrl = "https://api.apimart.ai/v1/videos/generations|https://api.apimart.ai/v1/tasks/{taskId}";
|
||||||
|
const [generateUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
||||||
|
|
||||||
|
const authorization = `Bearer ${config.apiKey}`;
|
||||||
|
|
||||||
|
// 上传图片到图床
|
||||||
|
let imageUrls: string[] = [];
|
||||||
|
if (input.imageBase64 && input.imageBase64.length > 0) {
|
||||||
|
for (const base64Image of input.imageBase64) {
|
||||||
|
const imageUrl = await uploadImageToApimart(base64Image);
|
||||||
|
imageUrls.push(imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
const requestBody: Record<string, unknown> = {
|
||||||
|
model: config.model,
|
||||||
|
prompt: input.prompt,
|
||||||
|
duration: input.duration,
|
||||||
|
aspect_ratio: input.aspectRatio,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (imageUrls.length > 0) {
|
||||||
|
requestBody.image_urls = imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建任务
|
||||||
|
const createRes = await axios.post(generateUrl, requestBody, {
|
||||||
|
headers: {
|
||||||
|
Authorization: authorization,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createRes.data.code !== 200 || !createRes.data.data?.[0]?.task_id) {
|
||||||
|
throw new Error(`创建任务失败: ${JSON.stringify(createRes.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = createRes.data.data[0].task_id;
|
||||||
|
const actualQueryUrl = queryUrl.replace("{taskId}", taskId);
|
||||||
|
|
||||||
|
// 轮询任务状态
|
||||||
|
return await pollTask(async () => {
|
||||||
|
const queryRes = await axios.get(actualQueryUrl, {
|
||||||
|
headers: { Authorization: authorization },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, data } = queryRes.data;
|
||||||
|
|
||||||
|
if (code !== 200 || !data) {
|
||||||
|
return { completed: false, error: `查询失败: ${JSON.stringify(queryRes.data)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, result, error } = data;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "completed":
|
||||||
|
const videoUrl = result?.videos?.[0]?.url?.[0];
|
||||||
|
if (!videoUrl) {
|
||||||
|
return { completed: false, error: "未获取到视频 URL" };
|
||||||
|
}
|
||||||
|
return { completed: true, url: videoUrl };
|
||||||
|
case "failed":
|
||||||
|
return { completed: false, error: error?.message || "任务失败" };
|
||||||
|
case "cancelled":
|
||||||
|
return { completed: false, error: "任务已取消" };
|
||||||
|
case "pending":
|
||||||
|
case "processing":
|
||||||
|
return { completed: false };
|
||||||
|
default:
|
||||||
|
return { completed: false, error: `未知状态: ${status}` };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -11,7 +11,13 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
if (!config.apiKey) throw new Error("缺少API Key");
|
if (!config.apiKey) throw new Error("缺少API Key");
|
||||||
|
|
||||||
const { owned, images, hasStartEndType } = validateVideoConfig(input, config);
|
const { owned, images, hasStartEndType } = validateVideoConfig(input, config);
|
||||||
const baseUrl = (config.baseURL || "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
|
|
||||||
|
const defaultBaseUrl = [
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/models/{model}:predictLongRunning",
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/{name}",
|
||||||
|
].join("|");
|
||||||
|
|
||||||
|
const [submitUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
||||||
const headers = { "x-goog-api-key": config.apiKey };
|
const headers = { "x-goog-api-key": config.apiKey };
|
||||||
|
|
||||||
const instance: Record<string, any> = { prompt: input.prompt };
|
const instance: Record<string, any> = { prompt: input.prompt };
|
||||||
@ -36,7 +42,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.post(
|
const { data } = await axios.post(
|
||||||
`${baseUrl}/v1beta/models/${config.model}:predictLongRunning`,
|
submitUrl.replace("{model}", config.model),
|
||||||
{ instances: [instance], parameters },
|
{ instances: [instance], parameters },
|
||||||
{ headers: { ...headers, "Content-Type": "application/json" } },
|
{ headers: { ...headers, "Content-Type": "application/json" } },
|
||||||
);
|
);
|
||||||
@ -44,7 +50,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
if (!data.name) throw new Error("未获取到操作名称");
|
if (!data.name) throw new Error("未获取到操作名称");
|
||||||
|
|
||||||
return pollTask(async () => {
|
return pollTask(async () => {
|
||||||
const { data: status } = await axios.get(`${baseUrl}/v1beta/${data.name}`, { headers });
|
const { data: status } = await axios.get(queryUrl.replace("{name}", data.name), { headers });
|
||||||
const { done, response, error } = status;
|
const { done, response, error } = status;
|
||||||
|
|
||||||
if (!done) return { completed: false };
|
if (!done) return { completed: false };
|
||||||
@ -59,4 +65,4 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
|
|
||||||
return { completed: true, url: savePath };
|
return { completed: true, url: savePath };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -9,12 +9,9 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
const { images } = validateVideoConfig(input, config);
|
const { images } = validateVideoConfig(input, config);
|
||||||
|
|
||||||
// 解析URL配置:图生视频|文生视频|查询地址
|
// 解析URL配置:图生视频|文生视频|查询地址
|
||||||
const baseUrl = "https://api-beijing.klingai.com";
|
const defaultBaseUrl =
|
||||||
const [
|
"https://api-beijing.klingai.com/v1/videos/image2video|https://api-beijing.klingai.com/v1/videos/text2video|https://api-beijing.klingai.com/v1/videos/text2video/{taskId}";
|
||||||
image2videoUrl = baseUrl + "/v1/videos/image2video",
|
const [image2videoUrl, text2videoUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
||||||
text2videoUrl = baseUrl + "/v1/videos/text2video",
|
|
||||||
queryUrl = baseUrl + "/v1/videos/text2video/{id}",
|
|
||||||
] = config.baseURL.split("|");
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Bearer ${config.apiKey}`,
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
@ -64,7 +61,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
|
|
||||||
// 轮询任务状态
|
// 轮询任务状态
|
||||||
return await pollTask(async () => {
|
return await pollTask(async () => {
|
||||||
const queryResponse = await axios.get(`${queryUrl.replace("{id}", taskId)}`, { headers });
|
const queryResponse = await axios.get(`${queryUrl.replace("{taskId}", taskId)}`, { headers });
|
||||||
const queryData = queryResponse.data;
|
const queryData = queryResponse.data;
|
||||||
if (queryData.code !== 0) {
|
if (queryData.code !== 0) {
|
||||||
return { completed: false, error: `查询失败: ${queryData.message || "未知错误"}` };
|
return { completed: false, error: `查询失败: ${queryData.message || "未知错误"}` };
|
||||||
|
|||||||
@ -9,13 +9,18 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
|
|
||||||
const { owned, images, hasTextType } = validateVideoConfig(input, config);
|
const { owned, images, hasTextType } = validateVideoConfig(input, config);
|
||||||
|
|
||||||
const baseUrl = "https://www.runninghub.cn";
|
const defaultBaseUrl = [
|
||||||
const parts = (config.baseURL || "").split("|");
|
"https://www.runninghub.cn/openapi/v2/rhart-video-s/image-to-video",
|
||||||
const suffix = owned.model === "sora-2" ? "-pro" : "";
|
"https://www.runninghub.cn/openapi/v2/rhart-video-s/image-to-video-pro",
|
||||||
|
"https://www.runninghub.cn/openapi/v2/rhart-video-s/text-to-video",
|
||||||
|
"https://www.runninghub.cn/openapi/v2/rhart-video-s/text-to-video-pro",
|
||||||
|
"https://www.runninghub.cn/openapi/v2/rhart-video-s/{taskId}",
|
||||||
|
"https://www.runninghub.cn/openapi/v2/media/upload/binary",
|
||||||
|
].join("|");
|
||||||
|
|
||||||
const image2videoUrl = parts[0] || `${baseUrl}/openapi/v2/rhart-video-s/image-to-video${suffix}`;
|
const [image2videoUrl, image2videoProUrl, text2videoUrl, text2videoProUrl, queryUrl, uploadUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
||||||
const text2videoUrl = parts[1] || `${baseUrl}/openapi/v2/rhart-video-s/text-to-video${suffix}`;
|
|
||||||
const queryUrl = parts[2] || `${baseUrl}/openapi/v2/rhart-video-s/{id}`;
|
const isPro = owned.model === "sora-2-pro";
|
||||||
const authorization = `Bearer ${config.apiKey}`;
|
const authorization = `Bearer ${config.apiKey}`;
|
||||||
|
|
||||||
// 上传 base64 图片
|
// 上传 base64 图片
|
||||||
@ -41,7 +46,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", buffer, { filename: "image.jpg", contentType: "image/jpeg" });
|
formData.append("file", buffer, { filename: "image.jpg", contentType: "image/jpeg" });
|
||||||
|
|
||||||
const { data } = await axios.post(`${baseUrl}/openapi/v2/media/upload/binary`, formData, {
|
const { data } = await axios.post(uploadUrl, formData, {
|
||||||
headers: { Authorization: authorization },
|
headers: { Authorization: authorization },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,11 +62,12 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
headers: { "Content-Type": "application/json", Authorization: authorization },
|
headers: { "Content-Type": "application/json", Authorization: authorization },
|
||||||
});
|
});
|
||||||
if (data.status === "FAILED") throw new Error(`任务提交失败: ${data.errorMessage || "未知错误"}`);
|
if (data.status === "FAILED") throw new Error(`任务提交失败: ${data.errorMessage || "未知错误"}`);
|
||||||
return { taskId: data.taskId, status: data.status, videoUrl: data.results?.[0]?.url };
|
return { taskId: data.taskId, status: data.status, url: data.results?.[0]?.url };
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTextToVideo = images.length === 0 && hasTextType;
|
const isTextToVideo = images.length === 0 && hasTextType;
|
||||||
const submitUrl = isTextToVideo ? text2videoUrl : image2videoUrl;
|
const submitUrl = isTextToVideo ? (isPro ? text2videoProUrl : text2videoUrl) : isPro ? image2videoProUrl : image2videoUrl;
|
||||||
|
|
||||||
const requestBody: Record<string, unknown> = {
|
const requestBody: Record<string, unknown> = {
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
duration: String(input.duration),
|
duration: String(input.duration),
|
||||||
@ -69,15 +75,14 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
...(isTextToVideo ? {} : { imageUrl: await uploadImage(images[0]) }),
|
...(isTextToVideo ? {} : { imageUrl: await uploadImage(images[0]) }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { taskId, status, videoUrl } = await submitTask(submitUrl, requestBody);
|
const { taskId } = await submitTask(submitUrl, requestBody);
|
||||||
if (status === "SUCCESS" && videoUrl) return { completed: true, videoUrl };
|
|
||||||
|
|
||||||
return await pollTask(async () => {
|
return await pollTask(async () => {
|
||||||
const { data } = await axios.get(queryUrl.replace("{id}", taskId), {
|
const { data } = await axios.get(queryUrl.replace("{taskId}", taskId), {
|
||||||
headers: { Authorization: authorization },
|
headers: { Authorization: authorization },
|
||||||
});
|
});
|
||||||
if (data.status === "SUCCESS") {
|
if (data.status === "SUCCESS") {
|
||||||
return data.results?.length ? { completed: true, videoUrl: data.results[0].url } : { completed: false, error: "任务成功但未返回视频链接" };
|
return data.results?.length ? { completed: true, url: data.results[0].url } : { completed: false, error: "任务成功但未返回视频链接" };
|
||||||
}
|
}
|
||||||
if (data.status === "FAILED") return { completed: false, error: `任务失败: ${data.errorMessage || "未知错误"}` };
|
if (data.status === "FAILED") return { completed: false, error: `任务失败: ${data.errorMessage || "未知错误"}` };
|
||||||
if (data.status === "QUEUED" || data.status === "RUNNING") return { completed: false };
|
if (data.status === "QUEUED" || data.status === "RUNNING") return { completed: false };
|
||||||
|
|||||||
@ -10,9 +10,11 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
throw new Error("至少需要提供prompt或图片");
|
throw new Error("至少需要提供prompt或图片");
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = "https://api.vidu.cn/ent/v2";
|
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 [image2videoUrl = baseUrl + "/text2video", text2videoUrl = baseUrl + "/img2video", queryUrl = baseUrl + "/tasks"] =
|
"|",
|
||||||
config.baseURL!.split("|");
|
);
|
||||||
|
|
||||||
|
const [text2videoUrl, image2videoUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
||||||
|
|
||||||
const authorization = `Token ${config.apiKey}`;
|
const authorization = `Token ${config.apiKey}`;
|
||||||
const hasImages = input.imageBase64 && input.imageBase64.length > 0;
|
const hasImages = input.imageBase64 && input.imageBase64.length > 0;
|
||||||
|
|||||||
@ -41,13 +41,13 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
|
|
||||||
const { owned, images, hasStartEndType, hasTextType } = validateVideoConfig(input, config);
|
const { owned, images, hasStartEndType, hasTextType } = validateVideoConfig(input, config);
|
||||||
|
|
||||||
// 解析URL配置
|
const defaultBaseUrl = [
|
||||||
const baseUrl = "https://dashscope.aliyuncs.com/api/v1";
|
"https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
|
||||||
const [
|
"https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis",
|
||||||
i2vUrl = baseUrl + "/services/aigc/video-generation/video-synthesis",
|
"https://dashscope.aliyuncs.com/api/v1/tasks/{taskId}",
|
||||||
kf2vUrl = baseUrl + "/services/aigc/image2video/video-synthesis",
|
].join("|");
|
||||||
queryUrl = baseUrl + "/tasks",
|
|
||||||
] = (config.baseURL || "").split("|");
|
const [i2vUrl, kf2vUrl, queryUrl] = (config.baseURL || defaultBaseUrl).split("|");
|
||||||
|
|
||||||
const types = owned.type;
|
const types = owned.type;
|
||||||
const authorization = `Bearer ${config.apiKey}`;
|
const authorization = `Bearer ${config.apiKey}`;
|
||||||
@ -133,7 +133,7 @@ export default async (input: VideoConfig, config: AIConfig) => {
|
|||||||
|
|
||||||
// 轮询任务状态
|
// 轮询任务状态
|
||||||
return await pollTask(async () => {
|
return await pollTask(async () => {
|
||||||
const response = await axios.get(`${queryUrl}/${taskId}`, {
|
const response = await axios.get(queryUrl.replace("{taskId}", taskId), {
|
||||||
headers: { Authorization: authorization },
|
headers: { Authorization: authorization },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user