638 lines
22 KiB
TypeScript
638 lines
22 KiB
TypeScript
/**
|
||
* Toonflow AI供应商模板 - 可灵AI
|
||
* @version 2.0
|
||
*/
|
||
|
||
// ============================================================
|
||
// 类型定义
|
||
// ============================================================
|
||
|
||
type VideoMode =
|
||
| "singleImage"
|
||
| "startEndRequired"
|
||
| "endFrameOptional"
|
||
| "startFrameOptional"
|
||
| "text"
|
||
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
|
||
|
||
interface TextModel {
|
||
name: string;
|
||
modelName: string;
|
||
type: "text";
|
||
think: boolean;
|
||
}
|
||
|
||
interface ImageModel {
|
||
name: string;
|
||
modelName: string;
|
||
type: "image";
|
||
mode: ("text" | "singleImage" | "multiReference")[];
|
||
associationSkills?: string;
|
||
}
|
||
|
||
interface VideoModel {
|
||
name: string;
|
||
modelName: string;
|
||
type: "video";
|
||
mode: VideoMode[];
|
||
associationSkills?: string;
|
||
audio: "optional" | false | true;
|
||
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
||
}
|
||
|
||
interface TTSModel {
|
||
name: string;
|
||
modelName: string;
|
||
type: "tts";
|
||
voices: { title: string; voice: string }[];
|
||
}
|
||
|
||
interface VendorConfig {
|
||
id: string;
|
||
version: string;
|
||
name: string;
|
||
author: string;
|
||
description?: string;
|
||
icon?: string;
|
||
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
|
||
inputValues: Record<string, string>;
|
||
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
||
}
|
||
|
||
type ReferenceList =
|
||
| { type: "image"; sourceType: "base64"; base64: string }
|
||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||
| { type: "video"; sourceType: "base64"; base64: string };
|
||
|
||
interface ImageConfig {
|
||
prompt: string;
|
||
referenceList?: Extract<ReferenceList, { type: "image" }>[];
|
||
size: "1K" | "2K" | "4K";
|
||
aspectRatio: `${number}:${number}`;
|
||
}
|
||
|
||
interface VideoConfig {
|
||
duration: number;
|
||
resolution: string;
|
||
aspectRatio: "16:9" | "9:16";
|
||
prompt: string;
|
||
referenceList?: ReferenceList[];
|
||
audio?: boolean;
|
||
mode: VideoMode[];
|
||
}
|
||
|
||
interface TTSConfig {
|
||
text: string;
|
||
voice: string;
|
||
speechRate: number;
|
||
pitchRate: number;
|
||
volume: number;
|
||
referenceList?: Extract<ReferenceList, { type: "audio" }>[];
|
||
}
|
||
|
||
interface PollResult {
|
||
completed: boolean;
|
||
data?: string;
|
||
error?: string;
|
||
}
|
||
|
||
// ============================================================
|
||
// 全局声明
|
||
// ============================================================
|
||
|
||
declare const axios: any;
|
||
declare const logger: (msg: string) => void;
|
||
declare const jsonwebtoken: any;
|
||
declare const zipImage: (base64: string, size: number) => Promise<string>;
|
||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
|
||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
|
||
declare const urlToBase64: (url: string) => Promise<string>;
|
||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
|
||
declare const createOpenAI: any;
|
||
declare const createDeepSeek: any;
|
||
declare const createZhipu: any;
|
||
declare const createQwen: any;
|
||
declare const createAnthropic: any;
|
||
declare const createOpenAICompatible: any;
|
||
declare const createXai: any;
|
||
declare const createMinimax: any;
|
||
declare const createGoogleGenerativeAI: any;
|
||
declare const exports: {
|
||
vendor: VendorConfig;
|
||
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;
|
||
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;
|
||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
|
||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
|
||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
|
||
updateVendor?: () => Promise<string>;
|
||
};
|
||
|
||
// ============================================================
|
||
// 供应商配置
|
||
// ============================================================
|
||
|
||
const vendor: VendorConfig = {
|
||
id: "klingai",
|
||
version: "2.0",
|
||
author: "Toonflow",
|
||
name: "可灵AI",
|
||
description:
|
||
"可灵AI视频生成\n\n支持可灵全系列视频模型,包括 kling-video-o1、kling-v3-omni、kling-v3、kling-v2-6、kling-v2-5-turbo、kling-v2-1、kling-v2-master、kling-v1-6、kling-v1-5、kling-v1 等。\n\n需要在[可灵AI开放平台](https://klingai.com)\n\n获取 Access Key 和 Secret Key。",
|
||
inputs: [
|
||
{ key: "accessKey", label: "Access Key", type: "password", required: true, placeholder: "请输入可灵AI的Access Key" },
|
||
{ key: "secretKey", label: "Secret Key", type: "password", required: true, placeholder: "请输入可灵AI的Secret Key" },
|
||
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "默认:https://api-beijing.klingai.com" },
|
||
],
|
||
inputValues: { accessKey: "", secretKey: "", baseUrl: "https://api-beijing.klingai.com" },
|
||
models: [
|
||
// kling-video-o1 (Omni)
|
||
{
|
||
name: "kling-video-o1 标准",
|
||
modelName: "kling-video-o1:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-video-o1 专家",
|
||
modelName: "kling-video-o1:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
// kling-v3-omni (Omni)
|
||
{
|
||
name: "kling-v3-omni 标准",
|
||
modelName: "kling-v3-omni:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v3-omni 专家",
|
||
modelName: "kling-v3-omni:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
|
||
},
|
||
// kling-v3
|
||
{
|
||
name: "kling-v3 标准",
|
||
modelName: "kling-v3:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v3 专家",
|
||
modelName: "kling-v3:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
|
||
},
|
||
// kling-v2-6
|
||
{
|
||
name: "kling-v2-6 标准",
|
||
modelName: "kling-v2-6:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v2-6 专家",
|
||
modelName: "kling-v2-6:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired"],
|
||
audio: "optional",
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
// kling-v2-5-turbo
|
||
{
|
||
name: "kling-v2-5-turbo 标准",
|
||
modelName: "kling-v2-5-turbo:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
{
|
||
name: "kling-v2-5-turbo 专家",
|
||
modelName: "kling-v2-5-turbo:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
// kling-v2-1
|
||
{
|
||
name: "kling-v2-1 标准",
|
||
modelName: "kling-v2-1:std",
|
||
type: "video",
|
||
mode: ["singleImage"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v2-1 专家",
|
||
modelName: "kling-v2-1:pro",
|
||
type: "video",
|
||
mode: ["singleImage", "startEndRequired"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
// kling-v2-1-master
|
||
{
|
||
name: "kling-v2-1 Master",
|
||
modelName: "kling-v2-1-master:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
// kling-v2-master
|
||
{
|
||
name: "kling-v2 Master",
|
||
modelName: "kling-v2-master:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
// kling-v1-6
|
||
{
|
||
name: "kling-v1-6 标准",
|
||
modelName: "kling-v1-6:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage", ["imageReference:4"]],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v1-6 专家",
|
||
modelName: "kling-v1-6:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "endFrameOptional", ["imageReference:4"]],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
// kling-v1-5
|
||
{
|
||
name: "kling-v1-5 标准",
|
||
modelName: "kling-v1-5:std",
|
||
type: "video",
|
||
mode: ["singleImage"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v1-5 专家",
|
||
modelName: "kling-v1-5:pro",
|
||
type: "video",
|
||
mode: ["singleImage", "endFrameOptional"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
|
||
},
|
||
// kling-v1
|
||
{
|
||
name: "kling-v1 标准",
|
||
modelName: "kling-v1:std",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
{
|
||
name: "kling-v1 专家",
|
||
modelName: "kling-v1:pro",
|
||
type: "video",
|
||
mode: ["text", "singleImage", "startEndRequired"],
|
||
audio: false,
|
||
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
|
||
},
|
||
],
|
||
};
|
||
|
||
// ============================================================
|
||
// 辅助工具
|
||
// ============================================================
|
||
|
||
/**
|
||
* 生成可灵AI的JWT鉴权Token
|
||
*/
|
||
const generateAuthToken = (): string => {
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const payload = {
|
||
iss: vendor.inputValues.accessKey,
|
||
exp: now + 1800,
|
||
nbf: now - 5,
|
||
};
|
||
return jsonwebtoken.sign(payload, vendor.inputValues.secretKey, {
|
||
algorithm: "HS256",
|
||
header: { alg: "HS256", typ: "JWT" },
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 获取基础请求地址
|
||
*/
|
||
const getBaseUrl = (): string => {
|
||
return vendor.inputValues.baseUrl || "https://api-beijing.klingai.com";
|
||
};
|
||
|
||
/**
|
||
* 从 ReferenceList 条目中提取可用的数据字符串
|
||
* 对于 url 类型返回 url,对于 base64 类型返回纯 base64(去掉 data: 前缀)
|
||
*/
|
||
const extractRawBase64 = (ref: ReferenceList): string => {
|
||
return ref.base64.replace(/^data:[^;]+;base64,/, "");
|
||
};
|
||
|
||
/**
|
||
* 从 ReferenceList 条目中提取带头的 base64 或 url
|
||
* 用于 omni-video 接口,该接口的 image_url 支持带前缀的 base64 和 url
|
||
*/
|
||
const extractImageUrl = (ref: ReferenceList): string => {
|
||
return ref.base64.startsWith("data:") ? ref.base64 : `data:image/jpeg;base64,${ref.base64}`;
|
||
};
|
||
|
||
/**
|
||
* 提交任务并轮询获取结果的通用函数
|
||
*/
|
||
const submitAndPoll = async (submitUrl: string, queryUrlBase: string, requestBody: any): Promise<string> => {
|
||
const token = generateAuthToken();
|
||
|
||
logger(`开始提交可灵AI视频生成任务: ${submitUrl}`);
|
||
logger(
|
||
`请求参数: ${JSON.stringify({
|
||
...requestBody,
|
||
image: requestBody.image ? "[BASE64]" : undefined,
|
||
image_tail: requestBody.image_tail ? "[BASE64]" : undefined,
|
||
image_list: requestBody.image_list ? "[IMAGES]" : undefined,
|
||
})}`,
|
||
);
|
||
|
||
const submitResp = await axios.post(submitUrl, requestBody, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
if (submitResp.data.code !== 0) {
|
||
throw new Error(`提交任务失败: ${submitResp.data.message || JSON.stringify(submitResp.data)}`);
|
||
}
|
||
|
||
const taskId = submitResp.data.data.task_id;
|
||
logger(`任务已提交,任务ID: ${taskId}`);
|
||
|
||
const result = await pollTask(
|
||
async () => {
|
||
const freshToken = generateAuthToken();
|
||
const queryResp = await axios.get(`${queryUrlBase}/${taskId}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${freshToken}`,
|
||
},
|
||
});
|
||
|
||
if (queryResp.data.code !== 0) {
|
||
return { completed: true, error: `查询任务失败: ${queryResp.data.message}` };
|
||
}
|
||
|
||
const taskData = queryResp.data.data;
|
||
const status = taskData.task_status;
|
||
logger(`轮询中... 任务状态: ${status}`);
|
||
|
||
if (status === "succeed") {
|
||
const videoUrl = taskData.task_result?.videos?.[0]?.url;
|
||
if (!videoUrl) {
|
||
return { completed: true, error: "任务完成但未获取到视频URL" };
|
||
}
|
||
return { completed: true, data: videoUrl };
|
||
}
|
||
|
||
if (status === "failed") {
|
||
return { completed: true, error: `视频生成失败: ${taskData.task_status_msg || "未知错误"}` };
|
||
}
|
||
|
||
return { completed: false };
|
||
},
|
||
5000,
|
||
600000,
|
||
);
|
||
|
||
if (result.error) throw new Error(result.error);
|
||
logger(`视频生成完成,正在转换为Base64...`);
|
||
return await urlToBase64(result.data!);
|
||
};
|
||
|
||
// ============================================================
|
||
// 适配器函数
|
||
// ============================================================
|
||
|
||
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
|
||
throw new Error("可灵AI不支持文本模型");
|
||
};
|
||
|
||
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
|
||
throw new Error("可灵AI不支持图片模型");
|
||
};
|
||
|
||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||
if (!vendor.inputValues.accessKey) throw new Error("缺少Access Key");
|
||
if (!vendor.inputValues.secretKey) throw new Error("缺少Secret Key");
|
||
|
||
const baseUrl = getBaseUrl();
|
||
|
||
// 解析 modelName,格式:kling-video-o1:pro => modelName=kling-video-o1, mode=pro
|
||
const colonIdx = model.modelName.indexOf(":");
|
||
const modelName = colonIdx > -1 ? model.modelName.substring(0, colonIdx) : model.modelName;
|
||
const mode = colonIdx > -1 ? model.modelName.substring(colonIdx + 1) : "pro";
|
||
|
||
// 判断是否为 Omni 模型
|
||
const isOmniModel = modelName === "kling-video-o1" || modelName === "kling-v3-omni";
|
||
|
||
// 判断当前选中的视频生成模式
|
||
const currentMode = config.mode;
|
||
const isText = currentMode.includes("text");
|
||
const isSingleImage = currentMode.includes("singleImage");
|
||
const isStartEndRequired = currentMode.includes("startEndRequired");
|
||
const isEndFrameOptional = currentMode.includes("endFrameOptional");
|
||
const isStartFrameOptional = currentMode.includes("startFrameOptional");
|
||
const hasMultiRef = currentMode.some((m) => Array.isArray(m));
|
||
|
||
// 提取不同类型的引用
|
||
const imageRefs = (config.referenceList || []).filter((r) => r.type === "image");
|
||
const videoRefs = (config.referenceList || []).filter((r) => r.type === "video");
|
||
|
||
// =====================================================
|
||
// Omni 模型 —— 使用 /v1/videos/omni-video 接口
|
||
// =====================================================
|
||
if (isOmniModel) {
|
||
const requestBody: any = {
|
||
model_name: modelName,
|
||
mode: mode,
|
||
duration: String(config.duration),
|
||
sound: config.audio === true ? "on" : "off",
|
||
};
|
||
|
||
if (config.prompt) {
|
||
requestBody.prompt = config.prompt;
|
||
}
|
||
|
||
if (isSingleImage && imageRefs.length > 0) {
|
||
const imageUrl = extractImageUrl(imageRefs[0]);
|
||
requestBody.image_list = [{ image_url: imageUrl, type: "first_frame" }];
|
||
if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频";
|
||
} else if (isStartEndRequired && imageRefs.length >= 2) {
|
||
const firstUrl = extractImageUrl(imageRefs[0]);
|
||
const endUrl = extractImageUrl(imageRefs[1]);
|
||
requestBody.image_list = [
|
||
{ image_url: firstUrl, type: "first_frame" },
|
||
{ image_url: endUrl, type: "end_frame" },
|
||
];
|
||
if (!requestBody.prompt) requestBody.prompt = "根据首尾帧图片生成过渡视频";
|
||
} else if (isEndFrameOptional && imageRefs.length >= 1) {
|
||
const firstUrl = extractImageUrl(imageRefs[0]);
|
||
requestBody.image_list = [{ image_url: firstUrl, type: "first_frame" }];
|
||
if (imageRefs.length >= 2) {
|
||
const endUrl = extractImageUrl(imageRefs[1]);
|
||
requestBody.image_list.push({ image_url: endUrl, type: "end_frame" });
|
||
}
|
||
if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频";
|
||
} else if (isStartFrameOptional && imageRefs.length >= 1) {
|
||
if (imageRefs.length >= 2) {
|
||
const firstUrl = extractImageUrl(imageRefs[0]);
|
||
const endUrl = extractImageUrl(imageRefs[1]);
|
||
requestBody.image_list = [
|
||
{ image_url: firstUrl, type: "first_frame" },
|
||
{ image_url: endUrl, type: "end_frame" },
|
||
];
|
||
} else {
|
||
const endUrl = extractImageUrl(imageRefs[0]);
|
||
requestBody.image_list = [{ image_url: endUrl, type: "end_frame" }];
|
||
}
|
||
if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频";
|
||
} else if (hasMultiRef && (imageRefs.length > 0 || videoRefs.length > 0)) {
|
||
requestBody.image_list = [];
|
||
for (let i = 0; i < imageRefs.length; i++) {
|
||
const imageUrl = extractImageUrl(imageRefs[i]);
|
||
requestBody.image_list.push({ image_url: imageUrl });
|
||
}
|
||
if (!requestBody.prompt) {
|
||
const refs = imageRefs.map((_, idx) => `<<<image_${idx + 1}>>>`).join("、");
|
||
requestBody.prompt = `参考${refs}生成视频`;
|
||
}
|
||
}
|
||
|
||
// 文生视频或无图片输入时需要设置宽高比
|
||
const hasImageInput = requestBody.image_list && requestBody.image_list.length > 0;
|
||
if (!hasImageInput) {
|
||
requestBody.aspect_ratio = config.aspectRatio || "16:9";
|
||
if (!requestBody.prompt) throw new Error("文生视频模式需要提供提示词");
|
||
}
|
||
|
||
const apiPath = "/v1/videos/omni-video";
|
||
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
|
||
}
|
||
|
||
// =====================================================
|
||
// 非 Omni 模型 —— 根据模式选择不同接口
|
||
// =====================================================
|
||
|
||
// 多图参考模式 —— 使用 /v1/videos/multi-image2video 接口(仅 kling-v1-6 支持)
|
||
if (hasMultiRef && imageRefs.length > 0) {
|
||
const imageList = [];
|
||
for (let i = 0; i < imageRefs.length; i++) {
|
||
const rawBase64 = extractRawBase64(imageRefs[i]);
|
||
imageList.push({ image: rawBase64 });
|
||
}
|
||
|
||
const requestBody: any = {
|
||
model_name: modelName,
|
||
image_list: imageList,
|
||
prompt: config.prompt || "根据参考图片生成视频",
|
||
mode: mode,
|
||
duration: String(config.duration),
|
||
aspect_ratio: config.aspectRatio || "16:9",
|
||
};
|
||
|
||
const apiPath = "/v1/videos/multi-image2video";
|
||
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
|
||
}
|
||
|
||
// 文生视频模式 —— 使用 /v1/videos/text2video 接口
|
||
if (isText) {
|
||
if (!config.prompt) throw new Error("文生视频模式需要提供提示词");
|
||
|
||
const requestBody: any = {
|
||
model_name: modelName,
|
||
prompt: config.prompt,
|
||
mode: mode,
|
||
duration: String(config.duration),
|
||
aspect_ratio: config.aspectRatio || "16:9",
|
||
sound: config.audio === true ? "on" : "off",
|
||
};
|
||
|
||
const apiPath = "/v1/videos/text2video";
|
||
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
|
||
}
|
||
|
||
// 图生视频模式(单图 / 首尾帧 / 尾帧可选等)—— 使用 /v1/videos/image2video 接口
|
||
if ((isSingleImage || isStartEndRequired || isEndFrameOptional || isStartFrameOptional) && imageRefs.length > 0) {
|
||
const requestBody: any = {
|
||
model_name: modelName,
|
||
prompt: config.prompt || "根据图片生成视频",
|
||
mode: mode,
|
||
duration: String(config.duration),
|
||
sound: config.audio === true ? "on" : "off",
|
||
};
|
||
|
||
if (isSingleImage) {
|
||
requestBody.image = extractRawBase64(imageRefs[0]);
|
||
} else if (isStartEndRequired && imageRefs.length >= 2) {
|
||
requestBody.image = extractRawBase64(imageRefs[0]);
|
||
requestBody.image_tail = extractRawBase64(imageRefs[1]);
|
||
} else if (isEndFrameOptional) {
|
||
requestBody.image = extractRawBase64(imageRefs[0]);
|
||
if (imageRefs.length >= 2) {
|
||
requestBody.image_tail = extractRawBase64(imageRefs[1]);
|
||
}
|
||
} else if (isStartFrameOptional) {
|
||
if (imageRefs.length >= 2) {
|
||
requestBody.image = extractRawBase64(imageRefs[0]);
|
||
requestBody.image_tail = extractRawBase64(imageRefs[1]);
|
||
} else {
|
||
requestBody.image = extractRawBase64(imageRefs[0]);
|
||
}
|
||
}
|
||
|
||
const apiPath = "/v1/videos/image2video";
|
||
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
|
||
}
|
||
|
||
throw new Error("不支持的视频生成模式或缺少必要的输入参数");
|
||
};
|
||
|
||
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
||
return "";
|
||
};
|
||
|
||
// ============================================================
|
||
// 导出
|
||
// ============================================================
|
||
|
||
exports.vendor = vendor;
|
||
exports.textRequest = textRequest;
|
||
exports.imageRequest = imageRequest;
|
||
exports.videoRequest = videoRequest;
|
||
exports.ttsRequest = ttsRequest;
|
||
|
||
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
|
||
export {}; |