video-flow-toon/data/vendor/volcengine.ts
2026-04-10 11:53:54 +08:00

527 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Toonflow 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)[];
}
interface ImageConfig {
prompt: string;
imageBase64: string[];
size: "1K" | "2K" | "4K";
aspectRatio: `${number}:${number}`;
}
interface VideoConfig {
duration: number;
resolution: string;
aspectRatio: "16:9" | "9:16";
prompt: string;
referenceList?: string[];
audio?: boolean;
mode: VideoMode[];
}
interface TTSConfig {
text: string;
voice: string;
speechRate: number;
pitchRate: number;
volume: number;
}
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) => 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: "volcengine-doubao",
version: "2.0",
author: "Toonflow",
name: "火山引擎(豆包)",
description: "## 火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。",
icon: "",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "火山引擎API Key" },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v3结束示例https://ark.cn-beijing.volces.com/api/v3" },
],
inputValues: {
apiKey: "",
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
},
models: [
// ===================== 文本模型 - 推荐 =====================
{ name: "Doubao-Seed-2.0-Pro", modelName: "doubao-seed-2-0-pro-260215", type: "text", think: true },
{ name: "Doubao-Seed-2.0-Lite", modelName: "doubao-seed-2-0-lite-260215", type: "text", think: true },
{ name: "Doubao-Seed-2.0-Mini", modelName: "doubao-seed-2-0-mini-260215", type: "text", think: true },
{ name: "Doubao-Seed-2.0-Code-Preview", modelName: "doubao-seed-2-0-code-preview-260215", type: "text", think: true },
{ name: "Doubao-Seed-Character", modelName: "doubao-seed-character-251128", type: "text", think: false },
// ===================== 文本模型 - 往期 =====================
{ name: "Doubao-Seed-1.8", modelName: "doubao-seed-1-8-251228", type: "text", think: true },
{ name: "Doubao-Seed-Code-Preview", modelName: "doubao-seed-code-preview-251028", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Lite", modelName: "doubao-seed-1-6-lite-251015", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Flash(0828)", modelName: "doubao-seed-1-6-flash-250828", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Vision", modelName: "doubao-seed-1-6-vision-250815", type: "text", think: true },
{ name: "Doubao-Seed-1.6(1015)", modelName: "doubao-seed-1-6-251015", type: "text", think: true },
{ name: "Doubao-Seed-1.6(0615)", modelName: "doubao-seed-1-6-250615", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Flash(0615)", modelName: "doubao-seed-1-6-flash-250615", type: "text", think: true },
{ name: "Doubao-Seed-Translation", modelName: "doubao-seed-translation-250915", type: "text", think: false },
{ name: "Doubao-1.5-Pro-32K", modelName: "doubao-1-5-pro-32k-250115", type: "text", think: false },
{ name: "Doubao-1.5-Pro-32K-Character(0715)", modelName: "doubao-1-5-pro-32k-character-250715", type: "text", think: false },
{ name: "Doubao-1.5-Pro-32K-Character(0228)", modelName: "doubao-1-5-pro-32k-character-250228", type: "text", think: false },
{ name: "Doubao-1.5-Lite-32K", modelName: "doubao-1-5-lite-32k-250115", type: "text", think: false },
{ name: "Doubao-1.5-Vision-Pro-32K", modelName: "doubao-1-5-vision-pro-32k-250115", type: "text", think: false },
// ===================== 文本模型 - 第三方(火山引擎托管) =====================
{ name: "GLM-4-7", modelName: "glm-4-7-251222", type: "text", think: true },
{ name: "DeepSeek-V3-2", modelName: "deepseek-v3-2-251201", type: "text", think: true },
{ name: "DeepSeek-V3-1-Terminus", modelName: "deepseek-v3-1-terminus", type: "text", think: true },
{ name: "DeepSeek-V3(0324)", modelName: "deepseek-v3-250324", type: "text", think: false },
{ name: "DeepSeek-R1(0528)", modelName: "deepseek-r1-250528", type: "text", think: true },
{ name: "Qwen3-32B", modelName: "qwen3-32b-20250429", type: "text", think: false },
{ name: "Qwen3-14B", modelName: "qwen3-14b-20250429", type: "text", think: false },
{ name: "Qwen3-8B", modelName: "qwen3-8b-20250429", type: "text", think: false },
{ name: "Qwen3-0.6B", modelName: "qwen3-0-6b-20250429", type: "text", think: false },
{ name: "Qwen2.5-72B", modelName: "qwen2-5-72b-20240919", type: "text", think: false },
{ name: "GLM-4.5-Air", modelName: "glm-4-5-air", type: "text", think: false },
// ===================== 图片生成模型 =====================
{
name: "Seedream-5.0",
modelName: "doubao-seedream-5-0-260128",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-5.0-Lite",
modelName: "doubao-seedream-5-0-lite-260128",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-4.5",
modelName: "doubao-seedream-4-5-251128",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-4.0",
modelName: "doubao-seedream-4-0-250828",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-3.0-T2I",
modelName: "doubao-seedream-3-0-t2i-250415",
type: "image",
mode: ["text"],
},
// ===================== 视频生成模型 =====================
// Seedance 2.0: 多模态参考(图0~9+视频0~3+音频0~3) + 首尾帧 + 首帧 + 文生视频
{
name: "Seedance-2.0(音画同生)",
modelName: "doubao-seedance-2-0-260128",
type: "video",
mode: [
"text",
"startFrameOptional",
["imageReference:9", "videoReference:3", "audioReference:3"],
],
audio: "optional",
durationResolutionMap: [
{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p"] },
],
},
{
name: "Seedance-2.0-Fast(音画同生)",
modelName: "doubao-seedance-2-0-fast-260128",
type: "video",
mode: [
"text",
"startFrameOptional",
["imageReference:9", "videoReference:3", "audioReference:3"],
],
audio: "optional",
durationResolutionMap: [
{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p"] },
],
},
// Seedance 1.5 pro: 首尾帧 + 首帧 + 文生视频
{
name: "Seedance-1.5-Pro(音画同生)",
modelName: "doubao-seedance-1-5-pro-251215",
type: "video",
mode: ["text", "startFrameOptional"],
audio: "optional",
durationResolutionMap: [
{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] },
],
},
// Seedance 1.0 pro: 首尾帧 + 首帧 + 文生视频
{
name: "Seedance-1.0-Pro",
modelName: "doubao-seedance-1-0-pro-250528",
type: "video",
mode: ["text", "startFrameOptional"],
audio: false,
durationResolutionMap: [
{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] },
],
},
// Seedance 1.0 pro fast: 首帧 + 文生视频(不支持首尾帧)
{
name: "Seedance-1.0-Pro-Fast",
modelName: "doubao-seedance-1-0-pro-fast-251015",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [
{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] },
],
},
// Seedance 1.0 lite t2v: 仅文生视频
{
name: "Seedance-1.0-Lite-T2V",
modelName: "doubao-seedance-1-0-lite-t2v-250428",
type: "video",
mode: ["text"],
audio: false,
durationResolutionMap: [
{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] },
],
},
// Seedance 1.0 lite i2v: 参考图(1~4) + 首尾帧 + 首帧
{
name: "Seedance-1.0-Lite-I2V",
modelName: "doubao-seedance-1-0-lite-i2v-250428",
type: "video",
mode: ["startFrameOptional", ["imageReference:4"]],
audio: false,
durationResolutionMap: [
{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] },
],
},
],
};
// ============================================================
// 辅助工具
// ============================================================
const getHeaders = () => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
return {
"Content-Type": "application/json",
Authorization: `Bearer ${vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "")}`,
};
};
const getBaseUrl = () => vendor.inputValues.baseUrl.replace(/\/+$/, "");
// ============================================================
// 适配器函数
// ============================================================
/** 文本请求 - 直接使用 createOpenAI */
const textRequest = (model: TextModel) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return createOpenAI({ baseURL: getBaseUrl(), apiKey }).chat(model.modelName);
};
/** 图片生成请求 */
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
const baseUrl = getBaseUrl();
const headers = getHeaders();
// 构建 content
const content: any[] = [];
// 文本提示词
if (config.prompt) {
content.push({ type: "text", text: config.prompt });
}
// 图片输入
if (config.imageBase64 && config.imageBase64.length > 0) {
for (const base64 of config.imageBase64) {
content.push({
type: "image_url",
image_url: { url: `data:image/png;base64,${base64}` },
});
}
}
// 解析宽高比
const [w, h] = config.aspectRatio.split(":").map(Number);
// 解析尺寸到像素
const sizeMap: Record<string, { width: number; height: number }> = {
"1K": { width: 1024, height: Math.round(1024 * (h / w)) },
"2K": { width: 2048, height: Math.round(2048 * (h / w)) },
"4K": { width: 4096, height: Math.round(4096 * (h / w)) },
};
const size = sizeMap[config.size] || sizeMap["1K"];
const body = {
model: model.modelName,
content,
size: `${size.width}x${size.height}`,
response_format: "url",
};
logger(`[图片生成] 请求模型: ${model.modelName}`);
const response = await axios.post(`${baseUrl}/images/generations`, body, { headers });
const data = response.data;
if (data?.data?.[0]?.url) {
return await urlToBase64(data.data[0].url);
}
throw new Error("图片生成失败:未返回有效结果");
};
/** 视频生成请求 */
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
const baseUrl = getBaseUrl();
const headers = getHeaders();
// 构建 content
const content: any[] = [];
// 文本提示词
if (config.prompt) {
content.push({ type: "text", text: config.prompt });
}
// 判断当前使用的 mode
const activeMode = config.mode && config.mode.length > 0 ? config.mode[0] : "text";
if (typeof activeMode === "string") {
switch (activeMode) {
case "singleImage":
// 首帧模式单张图片role 为 first_frame
if (config.imageBase64 && config.imageBase64.length > 0) {
content.push({
type: "image_url",
image_url: { url: `data:image/png;base64,${config.imageBase64[0]}` },
role: "first_frame",
});
}
break;
case "startFrameOptional":
// 首帧 + 可选尾帧模式
if (config.imageBase64 && config.imageBase64.length > 0) {
content.push({
type: "image_url",
image_url: { url: `data:image/png;base64,${config.imageBase64[0]}` },
role: "first_frame",
});
if (config.imageBase64.length > 1) {
content.push({
type: "image_url",
image_url: { url: `data:image/png;base64,${config.imageBase64[1]}` },
role: "last_frame",
});
}
}
break;
case "text":
// 纯文生视频,无需额外处理
break;
}
} else if (Array.isArray(activeMode)) {
// 多模态参考模式
let imageIndex = 0;
for (const ref of activeMode) {
if (typeof ref === "string") {
if (ref.startsWith("imageReference:")) {
// 参考图片
const maxCount = parseInt(ref.split(":")[1], 10);
if (config.imageBase64) {
const images = config.imageBase64.slice(imageIndex, imageIndex + maxCount);
for (const base64 of images) {
content.push({
type: "image_url",
image_url: { url: `data:image/png;base64,${base64}` },
role: "reference_image",
});
}
imageIndex += images.length;
}
}
// videoReference 和 audioReference 需要 URL当前框架暂不支持直接传入
}
}
}
// 映射宽高比
const ratioMap: Record<string, string> = {
"16:9": "16:9",
"9:16": "9:16",
"4:3": "4:3",
"3:4": "3:4",
"1:1": "1:1",
"21:9": "21:9",
};
const ratio = ratioMap[config.aspectRatio] || "16:9";
const body: any = {
model: model.modelName,
content,
ratio,
duration: config.duration,
resolution: config.resolution || "720p",
watermark: false,
};
// 音频控制
if (model.audio === "optional") {
body.generate_audio = config.audio !== false;
} else if (model.audio === true) {
body.generate_audio = true;
} else {
body.generate_audio = false;
}
logger(`[视频生成] 提交任务, 模型: ${model.modelName}, 时长: ${config.duration}s, 分辨率: ${config.resolution}`);
// 提交创建任务
const createResponse = await axios.post(`${baseUrl}/contents/generations/tasks`, body, { headers });
const taskId = createResponse.data?.id;
if (!taskId) {
throw new Error("视频生成任务创建失败未返回任务ID");
}
logger(`[视频生成] 任务已创建, ID: ${taskId}`);
// 轮询查询任务状态
const result = await pollTask(async (): Promise<PollResult> => {
const queryResponse = await axios.get(`${baseUrl}/contents/generations/tasks/${taskId}`, { headers });
const task = queryResponse.data;
logger(`[视频生成] 任务状态: ${task.status}`);
switch (task.status) {
case "succeeded":
if (task.content?.video_url) {
return { completed: true, data: task.content.video_url };
}
return { completed: true, error: "任务成功但未返回视频URL" };
case "failed":
return { completed: true, error: task.error?.message || "视频生成失败" };
case "expired":
return { completed: true, error: "视频生成任务超时" };
case "cancelled":
return { completed: true, error: "视频生成任务已取消" };
default:
// queued / running
return { completed: false };
}
}, 10000, 600000); // 每10秒查询一次最长等待10分钟
if (result.error) {
throw new Error(result.error);
}
return result.data || "";
};
/** TTS请求火山引擎暂无TTS模型配置预留接口 */
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
return "";
};
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
return { hasUpdate: false, latestVersion: "2.0", notice: "" };
};
const updateVendor = async (): Promise<string> => {
return "";
};
// ============================================================
// 导出
// ============================================================
exports.vendor = vendor;
exports.textRequest = textRequest;
exports.imageRequest = imageRequest;
exports.videoRequest = videoRequest;
exports.ttsRequest = ttsRequest;
exports.checkForUpdates = checkForUpdates;
exports.updateVendor = updateVendor;
export {};