Merge pull request #174 from modelstudioai/feat/update-def-video-model

Switch default video model to wan3.0-video
This commit is contained in:
Gong Shiqi
2026-08-26 18:00:38 +08:00
committed by GitHub
17 changed files with 535 additions and 162 deletions
+1 -1
View File
@@ -178,7 +178,7 @@ export async function ensurePrerequisites(ctx) {
"video",
"generate",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"压测前置短视频:海浪与静态远景,无明显人物。",
"--duration",
@@ -130,7 +130,7 @@ export async function generateCombinedFixtures({ suiteRoot, cliPackage }) {
"video",
"generate",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"压测前置短视频:海浪与静态远景,无明显人物。",
"--duration",
@@ -16,7 +16,7 @@ const motions = [
export const runStress = defineStressTarget({
canonical: "video-i2v",
defaultModel: "happyhorse-1.1-i2v",
defaultModel: "wan3.0-video",
batchDirPrefix: "video-i2v-batch",
helpText: "pnpm run test:stress -- video-i2v [--reuse-fixtures] -- --count 5 -c 2",
@@ -16,7 +16,7 @@ const prompts = [
export const runStress = defineStressTarget({
canonical: "video-ref",
defaultModel: "happyhorse-1.1-r2v",
defaultModel: "wan3.0-video",
batchDirPrefix: "video-ref-batch",
helpText: "pnpm run test:stress -- video-ref [--reuse-fixtures] -- --count 5 -c 2",
@@ -45,7 +45,7 @@ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
export const runStress = defineStressTarget({
canonical: "video-t2v",
defaultModel: "happyhorse-1.1-t2v",
defaultModel: "wan3.0-video",
batchDirPrefix: "video-t2v-batch",
helpText: `用法:pnpm run test:stress -- video-t2v -- --concurrency 1 --count 3
详见 docs/agents/stress-batch-tests.md`,
@@ -69,7 +69,9 @@ export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry"]);
export const UI_MODEL_DEFAULTS: Record<string, string> = {
default_text_model: "qwen3.8-max",
default_image_model: "qwen-image-3.0",
default_video_model: "happyhorse-1.1-t2v",
default_video_model: "wan3.0-video",
default_image_to_video_model: "wan3.0-video",
default_reference_to_video_model: "wan3.0-video",
default_speech_model: "cosyvoice-v3-flash",
default_omni_model: "qwen3.5-omni-plus",
};
@@ -100,8 +102,18 @@ export const UI_MODEL_CATALOG: Record<string, ModelOption[]> = {
{ id: "wanx2.x", role: "image/generate · async series" },
],
default_video_model: [
{ id: "happyhorse-1.1-t2v", role: "video/generate default · text-to-video" },
{ id: "wan3.0-video", role: "video/generate · t2v / i2v / r2v default" },
{ id: "happyhorse-1.1-t2v", role: "video/generate · text-to-video" },
{ id: "happyhorse-1.1-i2v", role: "video/generate · image-to-video" },
{ id: "happyhorse-1.1-r2v", role: "video/ref · reference-to-video" },
],
default_image_to_video_model: [
{ id: "wan3.0-video", role: "video/generate default · image-to-video" },
{ id: "happyhorse-1.1-i2v", role: "video/generate · image-to-video" },
],
default_reference_to_video_model: [
{ id: "wan3.0-video", role: "video/ref default · reference-to-video" },
{ id: "happyhorse-1.1-r2v", role: "video/ref · reference-to-video" },
],
default_speech_model: [
{ id: "cosyvoice-v3-flash", role: "speech/synthesize (TTS) default" },
@@ -24,9 +24,8 @@ import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailia
export default defineCommand({
description: {
"en-US":
"Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v)",
"zh-CN": "根据文本或图片生成视频(happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v)",
"en-US": "Generate a video from text or image (wan3.0-video / wan2.6-t2v / happyhorse-1.1-i2v)",
"zh-CN": "根据文本或图片生成视频(wan3.0-video / wan2.6-t2v / happyhorse-1.1-i2v)",
},
auth: "apiKey",
usageArgs: "--prompt <text> [--image <url>] [flags]",
@@ -35,8 +34,8 @@ export default defineCommand({
type: "string",
valueHint: "<model>",
description: {
"en-US": "Model ID (default: happyhorse-1.1-t2v, or happyhorse-1.1-i2v with --image)",
"zh-CN": "模型 ID(默认:happyhorse-1.1-t2v;使用 --image 时为 happyhorse-1.1-i2v)",
"en-US": "Model ID (default: wan3.0-video)",
"zh-CN": "模型 ID(默认:wan3.0-video)",
},
},
prompt: {
@@ -119,6 +118,16 @@ export default defineCommand({
"zh-CN": "完成后将视频保存到文件",
},
},
file: {
type: "string",
valueHint: "<url-or-path>",
description: {
"en-US":
"Reference file URL or local path for file-to-video (wan3.0-video only; mutually exclusive with --image/--last-frame)",
"zh-CN":
"参考文件 URL 或本地路径,用于文件生视频(仅 wan3.0-video;与 --image/--last-frame 互斥)",
},
},
...ASYNC_FLAG,
...CONCURRENT_FLAG,
pollInterval: {
@@ -159,12 +168,20 @@ export default defineCommand({
const model =
flags.model ||
(flags.image
? settings.defaultImageToVideoModel || "happyhorse-1.1-i2v"
: settings.defaultVideoModel || "happyhorse-1.1-t2v");
? settings.defaultImageToVideoModel || "wan3.0-video"
: settings.defaultVideoModel || "wan3.0-video");
const format = detectOutputFormat(settings.output);
const imageUrl = flags.image;
const lastFrameUrl = flags.lastFrame as string | undefined;
const fileUrl = flags.file as string | undefined;
if (fileUrl && (imageUrl || lastFrameUrl)) {
throw new BailianError(
"--file is mutually exclusive with --image/--last-frame.",
ExitCode.USAGE,
);
}
// Auto-upload local image file for i2v
let resolvedImageUrl: string | undefined;
@@ -178,6 +195,18 @@ export default defineCommand({
// kf2v mode: both --image and --last-frame provided.
const isKf2v = Boolean(resolvedImageUrl && resolvedLastFrameUrl);
// 万相 3.x(All-in-One)首尾帧走 media[];旧 kf2v 仍走 image2video 平铺字段。
// 与 video/ref.ts 的 useReferenceAudio 保持同一判定:忽略大小写、覆盖 wan3.x 系列。
const isWan30 = /^wan3\./i.test(model);
if (fileUrl && !isWan30) {
throw new BailianError("--file is only supported by wan3.0-video.", ExitCode.USAGE);
}
let resolvedFileUrl: string | undefined;
if (fileUrl) {
resolvedFileUrl = await ctx.client.uploadFile(fileUrl, model);
}
const watermark = resolveWatermark(flags.watermark);
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
@@ -187,16 +216,26 @@ export default defineCommand({
input: {
prompt: prompt,
negative_prompt: flags.negativePrompt || undefined,
// kf2v: first+last frame flat fields via image2video endpoint.
// wan3.0 kf2v: media[first_frame, last_frame] via video-generation endpoint.
// legacy kf2v: first_frame_url/last_frame_url via image2video endpoint.
// wan2.1~2.6 i2v: flat img_url via video-generation endpoint.
// wan2.7+ / happyhorse i2v: media[] via video-generation endpoint.
...(isKf2v
? { first_frame_url: resolvedImageUrl, last_frame_url: resolvedLastFrameUrl }
: resolvedImageUrl
? /wan[x]?2\.[1-6]/i.test(model)
? { img_url: resolvedImageUrl }
: { media: [{ type: "first_frame" as const, url: resolvedImageUrl }] }
: {}),
...(resolvedFileUrl
? { media: [{ type: "file" as const, url: resolvedFileUrl }] }
: isKf2v
? isWan30
? {
media: [
{ type: "first_frame" as const, url: resolvedImageUrl! },
{ type: "last_frame" as const, url: resolvedLastFrameUrl! },
],
}
: { first_frame_url: resolvedImageUrl, last_frame_url: resolvedLastFrameUrl }
: resolvedImageUrl
? /wan[x]?2\.[1-6]/i.test(model)
? { img_url: resolvedImageUrl }
: { media: [{ type: "first_frame" as const, url: resolvedImageUrl }] }
: {}),
},
parameters: {
resolution: flags.resolution || undefined,
@@ -210,13 +249,29 @@ export default defineCommand({
if (settings.dryRun) {
let previewBody = body;
if (isKf2v) {
if (resolvedFileUrl) {
previewBody = {
...body,
input: {
...body.input,
first_frame_url: redactDataUri(resolvedImageUrl ?? ""),
last_frame_url: redactDataUri(resolvedLastFrameUrl ?? ""),
media: [{ type: "file" as const, url: redactDataUri(resolvedFileUrl) }],
},
};
} else if (isKf2v) {
const redactedFirst = redactDataUri(resolvedImageUrl ?? "");
const redactedLast = redactDataUri(resolvedLastFrameUrl ?? "");
previewBody = {
...body,
input: {
...body.input,
...(isWan30
? {
media: [
{ type: "first_frame" as const, url: redactedFirst },
{ type: "last_frame" as const, url: redactedLast },
],
}
: { first_frame_url: redactedFirst, last_frame_url: redactedLast }),
},
};
} else if (resolvedImageUrl) {
@@ -243,7 +298,7 @@ export default defineCommand({
settings,
() =>
ctx.client.requestJson<DashScopeAsyncResponse>({
path: isKf2v ? image2videoPath() : videoGeneratePath(),
path: isKf2v && !isWan30 ? image2videoPath() : videoGeneratePath(),
method: "POST",
body,
async: true,
+36 -11
View File
@@ -24,8 +24,9 @@ import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailia
export default defineCommand({
description: {
"en-US":
"Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice",
"zh-CN": "参考生视频(happyhorse-1.1-r2v / wan2.6-r2v):支持多主体、多镜头和语音",
"Reference-to-video generation (wan3.0-video / happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice",
"zh-CN":
"参考生视频(wan3.0-video / happyhorse-1.1-r2v / wan2.6-r2v):支持多主体、多镜头和语音",
},
auth: "apiKey",
usageArgs: "--prompt <text> --image <url>... [--ref-video <url>...] [flags]",
@@ -34,8 +35,8 @@ export default defineCommand({
type: "string",
valueHint: "<model>",
description: {
"en-US": "Model ID (default: happyhorse-1.1-r2v)",
"zh-CN": "模型 ID(默认:happyhorse-1.1-r2v)",
"en-US": "Model ID (default: wan3.0-video)",
"zh-CN": "模型 ID(默认:wan3.0-video)",
},
},
prompt: {
@@ -67,16 +68,20 @@ export default defineCommand({
type: "array",
valueHint: "<url>",
description: {
"en-US": "Voice URL for corresponding image (pairs by position)",
"zh-CN": "对应图片的语音 URL(按位置配对)",
"en-US":
"Voice URL for corresponding image (pairs by position). On wan3.0-video emitted as reference_audio (refer to Audio 1, Audio 2 in prompt)",
"zh-CN":
"对应图片的语音 URL(按位置配对)。在 wan3.0-video 上作为 reference_audio 条目发送(prompt 中用「音频1」「音频2」引用)",
},
},
videoVoice: {
type: "array",
valueHint: "<url>",
description: {
"en-US": "Voice URL for corresponding ref-video (pairs by position)",
"zh-CN": "对应参考视频的语音 URL(按位置配对)",
"en-US":
"Voice URL for corresponding ref-video (pairs by position). On wan3.0-video emitted as reference_audio (refer to Audio 1, Audio 2 in prompt)",
"zh-CN":
"对应参考视频的语音 URL(按位置配对)。在 wan3.0-video 上作为 reference_audio 条目发送(prompt 中用「音频1」「音频2」引用)",
},
},
resolution: {
@@ -182,11 +187,16 @@ export default defineCommand({
const imageVoices = flags.imageVoice || [];
const videoVoices = flags.videoVoice || [];
const model = flags.model || settings.defaultReferenceToVideoModel || "happyhorse-1.1-r2v";
const model = flags.model || settings.defaultReferenceToVideoModel || "wan3.0-video";
const format = detectOutputFormat(settings.output);
// wan3.0-video emits voice as standalone reference_audio entries (referenced as Audio 1, Audio 2 in prompt);
// other models (wan2.7-r2v / happyhorse-1.1-r2v, etc.) keep the legacy reference_voice field attached to each asset.
const useReferenceAudio = /^wan3\./i.test(model);
// --- Resolve file URLs (auto-upload local files) ---
const media: DashScopeVideoRefRequest["input"]["media"] = [];
const audioEntries: Array<{ type: "reference_audio"; url: string }> = [];
// Add reference images
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
@@ -199,7 +209,11 @@ export default defineCommand({
// Pair voice by position
if (imageVoices[imageIndex]) {
const resolvedVoice = await ctx.client.uploadFile(imageVoices[imageIndex]!, model);
entry.reference_voice = resolvedVoice;
if (useReferenceAudio) {
audioEntries.push({ type: "reference_audio", url: resolvedVoice });
} else {
entry.reference_voice = resolvedVoice;
}
}
media.push(entry);
@@ -216,12 +230,23 @@ export default defineCommand({
// Pair voice by position
if (videoVoices[videoIndex]) {
const resolvedVoice = await ctx.client.uploadFile(videoVoices[videoIndex]!, model);
entry.reference_voice = resolvedVoice;
if (useReferenceAudio) {
audioEntries.push({ type: "reference_audio", url: resolvedVoice });
} else {
entry.reference_voice = resolvedVoice;
}
}
media.push(entry);
}
// wan3.0: append reference voices as standalone media entries, ordered image-voice then video-voice (Audio 1, Audio 2, ...)
if (useReferenceAudio) {
for (const audioEntry of audioEntries) {
media.push(audioEntry);
}
}
// --- Build request body ---
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
@@ -62,6 +62,7 @@ export const VIDEO_ROUTES: E2eRouteExports = {
"video ref": "videoRef",
"video task get": "videoTaskGet",
"video download": "videoDownload",
"speech synthesize": "speechSynthesize",
};
export const VISION_ROUTES: E2eRouteExports = {
@@ -114,46 +114,27 @@ describe("e2e: video generate (i2v)", () => {
});
test.each([
// wan2.1~2.6 (legacy) use flat img_url; wan2.7+ and happyhorse use media[].
// wan2.1~2.6 (legacy) use flat img_url; wan2.7+ / wan3.0 / happyhorse use media[].
// 用普通 dry-run(不绑 token-plan):TP 不支持 wan3.0,绑 TP 会形成错误测试契约。
["wan2.5-i2v-preview", "img_url"],
["wan2.6-i2v", "img_url"],
["wan2.7-i2v", "media"],
["wan3.0-video", "media"],
["happyhorse-1.1-i2v", "media"],
])("video generate --dry-run %s 首帧走 %s 字段", async (model, field) => {
const configDir = makeE2eOutputDir(`video-i2v-input-shape-${model}`);
writeFileSync(
join(configDir, "config.json"),
JSON.stringify({
"token-plan": {
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
},
}),
);
const { stdout, stderr, exitCode } = await runCommandE2e(
VIDEO_ROUTES,
[
"video",
"generate",
"--config",
"token-plan",
"--dry-run",
"--model",
model,
"--image",
"https://example.com/placeholder.png",
"--prompt",
"干跑校验",
"--output",
"json",
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
"--dry-run",
"--model",
model,
"--image",
"https://example.com/placeholder.png",
"--prompt",
"干跑校验",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: {
@@ -168,6 +149,98 @@ describe("e2e: video generate (i2v)", () => {
expect(data.request?.input?.img_url).toBeUndefined();
}
});
test("video generate --dry-run --file 走 media file 字段", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
"--dry-run",
"--model",
"wan3.0-video",
"--file",
"https://example.com/reference.pdf",
"--prompt",
"文件生视频干跑",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: {
model?: string;
input?: {
media?: Array<{ type?: string; url?: string }>;
img_url?: string;
first_frame_url?: string;
};
};
}>(stdout);
expect(data.request?.model).toBe("wan3.0-video");
expect(data.request?.input?.media?.[0]?.type).toBe("file");
expect(data.request?.input?.media?.[0]?.url).toBe("https://example.com/reference.pdf");
expect(data.request?.input?.img_url).toBeUndefined();
expect(data.request?.input?.first_frame_url).toBeUndefined();
});
test("--file 与 --image 互斥时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
"--dry-run",
"--model",
"wan3.0-video",
"--file",
"https://example.com/reference.pdf",
"--image",
"https://example.com/placeholder.png",
"--prompt",
"互斥校验",
"--output",
"json",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--file.*mutually exclusive|--file.*互斥|mutually exclusive/i);
});
test("--file 与 --last-frame 互斥时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
"--dry-run",
"--model",
"wan3.0-video",
"--file",
"https://example.com/reference.pdf",
"--last-frame",
"https://example.com/last-frame.png",
"--prompt",
"互斥校验",
"--output",
"json",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--file.*mutually exclusive|--file.*互斥|mutually exclusive/i);
});
test("非 wan3.0 模型使用 --file 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
"--dry-run",
"--model",
"wan2.6-i2v",
"--file",
"https://example.com/reference.pdf",
"--prompt",
"模型限制校验",
"--output",
"json",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(
/--file.*only supported by wan3\.0-video|--file.*仅.*wan3\.0-video|only supported by wan3\.0-video/i,
);
});
});
describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
@@ -179,7 +252,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"generate",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-i2v",
"wan3.0-video",
"--image",
"https://example.com/placeholder.png",
]);
@@ -194,7 +267,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
...cliTimeoutPrefix(),
"--dry-run",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"干跑无图",
"--output",
@@ -208,7 +281,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
expect(data.request?.input?.media).toBeUndefined();
});
test("【happyhorse-1.1-i2v】图片生成视频", async () => {
test("【wan3.0-video】图片生成视频", async () => {
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
const png = join(outDir, "e2e-gen.png");
const gen = await runCommandE2e(VIDEO_ROUTES, [
@@ -234,7 +307,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"generate",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-i2v",
"wan3.0-video",
"--image",
imagePath,
"--prompt",
@@ -32,7 +32,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"generate",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--prompt|Usage:/i);
@@ -44,8 +44,6 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"generate",
"--dry-run",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-t2v",
"--prompt",
"干跑校验",
"--output",
@@ -55,18 +53,18 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
const data = parseStdoutJson<{ request?: { model?: string; input?: { prompt?: string } } }>(
stdout,
);
expect(data.request?.model).toBe("happyhorse-1.1-t2v");
expect(data.request?.model).toBe("wan3.0-video");
expect(data.request?.input?.prompt).toBe("干跑校验");
});
test("【happyhorse-1.1-t2v】文本生成视频", async () => {
test("【wan3.0-video】文本生成视频", async () => {
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"夕阳下海面波光,远景静态镜头",
"--download",
@@ -45,6 +45,87 @@ describe("e2e: video ref (r2v)", () => {
expect(data.request?.input?.media?.[0]?.url).toBe("https://example.com/person.png");
});
test("video ref --dry-run 在 wan3.0-video 上将 --image-voice 转为独立 reference_audio 条目", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"ref",
"--dry-run",
"--model",
"wan3.0-video",
"--prompt",
"Image 1 speaks with Audio 1",
"--image",
"https://example.com/a.jpg",
"--ref-video",
"https://example.com/b.mp4",
"--image-voice",
"https://example.com/va.mp3",
"--video-voice",
"https://example.com/vb.mp3",
"--resolution",
"720P",
"--ratio",
"16:9",
"--duration",
"5",
"--prompt-extend",
"true",
"--watermark",
"false",
"--seed",
"42",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: {
input?: {
media?: Array<{ type?: string; url?: string; reference_voice?: string }>;
};
parameters?: { prompt_extend?: boolean; watermark?: boolean; seed?: number };
};
}>(stdout);
const media = data.request?.input?.media ?? [];
// wan3.0: image/video 条目不挂 reference_voice
const imageEntry = media.find((m) => m.type === "reference_image");
expect(imageEntry?.reference_voice).toBeUndefined();
// 音色作为独立 reference_audio 条目,按 image-voice 再 video-voice 顺序追加
const audioEntries = media.filter((m) => m.type === "reference_audio");
expect(audioEntries).toHaveLength(2);
expect(audioEntries[0]?.url).toBe("https://example.com/va.mp3");
expect(audioEntries[1]?.url).toBe("https://example.com/vb.mp3");
// 全部生成参数进入 parameters
expect(data.request?.parameters?.prompt_extend).toBe(true);
expect(data.request?.parameters?.watermark).toBe(false);
expect(data.request?.parameters?.seed).toBe(42);
});
test("video ref --dry-run 在 wan2.7-r2v 上沿用 reference_voice 字段(既有逻辑)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"ref",
"--dry-run",
"--model",
"wan2.7-r2v",
"--prompt",
"Image 1 speaks",
"--image",
"https://example.com/a.jpg",
"--image-voice",
"https://example.com/va.mp3",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { input?: { media?: Array<{ type?: string; reference_voice?: string }> } };
}>(stdout);
const media = data.request?.input?.media ?? [];
expect(media).toHaveLength(1);
expect(media[0]?.reference_voice).toBe("https://example.com/va.mp3");
});
test("Token Plan 使用独立的参考生视频默认模型", async () => {
const configDir = makeE2eOutputDir("video-r2v-token-plan-default");
writeFileSync(
@@ -141,7 +222,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"ref",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-r2v",
"wan3.0-video",
"--image",
"https://example.com/x.png",
]);
@@ -155,7 +236,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"ref",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-r2v",
"wan3.0-video",
"--prompt",
"仅有描述无素材",
]);
@@ -163,7 +244,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
expect(stderr).toMatch(/--image|ref-video|At least one|required/i);
});
test("【happyhorse-1.1-r2v】视频参考生成", async () => {
test("【wan3.0-video】视频参考生成", async () => {
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
const gen = await runCommandE2e(VIDEO_ROUTES, [
"image",
@@ -189,7 +270,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"ref",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-r2v",
"wan3.0-video",
"--prompt",
"图1在画面中心轻微晃动",
"--image",
@@ -204,5 +285,125 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
expect(data.status).toBe("SUCCEEDED");
expect(data.video_url?.startsWith("https://")).toBe(true);
}, 3_600_000);
test("【wan3.0-video】全参数参考生视频(图+视频+双音色+全部生成参数)", async () => {
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
// 1. 参考图
const gen = await runCommandE2e(VIDEO_ROUTES, [
"image",
"generate",
"--model",
"qwen-image-3.0",
"--prompt",
"一片绿色的树叶,白底",
"--out-dir",
outDir,
"--out-prefix",
"e2e-ref-img",
"--output",
"json",
]);
expect(gen.exitCode, gen.stderr).toBe(0);
const imagePath = parseStdoutJson<{ saved?: string[] }>(gen.stdout).saved?.[0];
expect(imagePath).toBeTruthy();
// 2. 参考视频(t2v 生成一段素材)
const vid = await runCommandE2e(VIDEO_ROUTES, [
"video",
"generate",
...cliTimeoutPrefix(),
"--model",
"wan3.0-video",
"--prompt",
"海面波光粼粼,远景静态镜头",
"--download",
join(outDir, "e2e-ref-video.mp4"),
"--output",
"json",
]);
expect(vid.exitCode, vid.stderr).toBe(0);
const refVideoPath = parseStdoutJson<{ saved?: string }>(vid.stdout).saved;
expect(refVideoPath).toBeTruthy();
// 3. 两个参考音色(文本足够长确保 ≥1s,合计 ≤15s)
const vaOut = join(outDir, "e2e-va.mp3");
const va = await runCommandE2e(VIDEO_ROUTES, [
"speech",
"synthesize",
"--model",
"cosyvoice-v3-flash",
"--voice",
"longwan_v3",
"--text",
"你好,很高兴见到你,今天我们一起聊聊吧",
"--out",
vaOut,
"--output",
"json",
]);
expect(va.exitCode, va.stderr).toBe(0);
expect(parseStdoutJson<{ saved?: string }>(va.stdout).saved).toBe(vaOut);
const vbOut = join(outDir, "e2e-vb.mp3");
const vb = await runCommandE2e(VIDEO_ROUTES, [
"speech",
"synthesize",
"--model",
"cosyvoice-v3-flash",
"--voice",
"longxiaochun_v3",
"--text",
"你好呀,今天天气真不错,我们出去走走吧",
"--out",
vbOut,
"--output",
"json",
]);
expect(vb.exitCode, vb.stderr).toBe(0);
expect(parseStdoutJson<{ saved?: string }>(vb.stdout).saved).toBe(vbOut);
// 4. 全参数 video ref:图1/视频1/音频1/音频2 + 全部生成参数
const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
"video",
"ref",
...cliTimeoutPrefix(),
"--model",
"wan3.0-video",
"--prompt",
"图1中的人物看向视频1中的角色,并用音频1的音色说:你好,视频1中的角色用音频2的音色回应:你好呀",
"--image",
imagePath!,
"--ref-video",
refVideoPath!,
"--image-voice",
vaOut,
"--video-voice",
vbOut,
"--resolution",
"720P",
"--ratio",
"16:9",
"--duration",
"5",
"--prompt-extend",
"true",
"--watermark",
"false",
"--seed",
"42",
"--poll-interval",
"10",
"--download",
join(outDir, "e2e-video-ref-full.mp4"),
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ status?: string; video_url?: string; saved?: string }>(stdout);
expect(data.status).toBe("SUCCEEDED");
expect(data.video_url?.startsWith("https://")).toBe(true);
expect(data.saved).toBeTruthy();
}, 3_600_000);
},
);
+9 -2
View File
@@ -209,7 +209,14 @@ export interface DashScopeVideoRequest {
first_frame_url?: string;
last_frame_url?: string;
media?: Array<{
type: "image" | "video" | "first_frame" | "last_frame" | "driving_audio" | "first_clip";
type:
| "image"
| "video"
| "first_frame"
| "last_frame"
| "driving_audio"
| "first_clip"
| "file";
url: string;
}>;
};
@@ -228,7 +235,7 @@ export interface DashScopeVideoRefRequest {
input: {
prompt: string;
media: Array<{
type: "reference_image" | "reference_video";
type: "reference_image" | "reference_video" | "reference_audio";
url: string;
reference_voice?: string;
}>;
@@ -433,7 +433,7 @@ export async function videoGenerate(
});
}
const model = input.model || (input.image ? "happyhorse-1.1-i2v" : "happyhorse-1.1-t2v");
const model = input.model || "wan3.0-video";
let resolvedImageUrl: string | undefined;
if (input.image) {
+2 -2
View File
@@ -33,9 +33,9 @@ description: >-
| ---------------------------------------- | ----------------------------------------- | --------------------------------------------------- |
| Text-to-image | `bl image generate` | `qwen-image-3.0` |
| Image edit / multi-image merge | `bl image edit` (repeat `--image`) | `qwen-image-3.0` |
| Text-to-video / image-to-video | `bl video generate` | `happyhorse-1.1-t2v` / `-i2v` (with `--image`) |
| Text-to-video / image-to-video | `bl video generate` | `wan3.0-video` |
| Video edit / style transfer | `bl video edit` | `happyhorse-1.0-video-edit` |
| Reference-to-video + voice | `bl video ref` | `happyhorse-1.1-r2v` |
| Reference-to-video + voice | `bl video ref` | `wan3.0-video` |
| Speech synthesis (TTS / voiceover) | `bl speech synthesize` | `cosyvoice-v3-flash` |
| Speech recognition (ASR / transcription) | `bl speech recognize` | `fun-asr` |
| Image describe | `bl vision describe` | `qwen3-vl-plus`;宿主能做且未点名 → host-first |
+13 -13
View File
@@ -9,19 +9,19 @@ Use this index for the skill-scoped quick index and global flags.
## Quick index
| Command | Authentication | Description | Detail |
| ---------------------- | -------------- | ----------------------------------------------------------------------------------------------------- | ---------------------- |
| `bl image edit` | API Key | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) | [image.md](image.md) |
| `bl image generate` | API Key | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
| `bl omni` | API Key | Multimodal chat with text + audio output (Qwen-Omni) | [omni.md](omni.md) |
| `bl speech recognize` | API Key | Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash) | [speech.md](speech.md) |
| `bl speech synthesize` | API Key | Synthesize speech from text (CosyVoice TTS) | [speech.md](speech.md) |
| `bl video download` | API Key | Download a completed video by task ID | [video.md](video.md) |
| `bl video edit` | API Key | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) | [video.md](video.md) |
| `bl video generate` | API Key | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) | [video.md](video.md) |
| `bl video ref` | API Key | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice | [video.md](video.md) |
| `bl video task get` | API Key | Query async task status | [video.md](video.md) |
| `bl vision describe` | API Key | Describe an image or video using Qwen-VL | [vision.md](vision.md) |
| Command | Authentication | Description | Detail |
| ---------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `bl image edit` | API Key | Edit an existing image with text instructions (Qwen-Image / Wan 2.7) | [image.md](image.md) |
| `bl image generate` | API Key | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
| `bl omni` | API Key | Multimodal chat with text + audio output (Qwen-Omni) | [omni.md](omni.md) |
| `bl speech recognize` | API Key | Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash) | [speech.md](speech.md) |
| `bl speech synthesize` | API Key | Synthesize speech from text (CosyVoice TTS) | [speech.md](speech.md) |
| `bl video download` | API Key | Download a completed video by task ID | [video.md](video.md) |
| `bl video edit` | API Key | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) | [video.md](video.md) |
| `bl video generate` | API Key | Generate a video from text or image (wan3.0-video / wan2.6-t2v / happyhorse-1.1-i2v) | [video.md](video.md) |
| `bl video ref` | API Key | Reference-to-video generation (wan3.0-video / happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice | [video.md](video.md) |
| `bl video task get` | API Key | Query async task status | [video.md](video.md) |
| `bl vision describe` | API Key | Describe an image or video using Qwen-VL | [vision.md](vision.md) |
## By group
+59 -58
View File
@@ -7,13 +7,13 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Authentication | Description |
| ------------------- | -------------- | ----------------------------------------------------------------------------------------------------- |
| `bl video download` | API Key | Download a completed video by task ID |
| `bl video edit` | API Key | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) |
| `bl video generate` | API Key | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) |
| `bl video ref` | API Key | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice |
| `bl video task get` | API Key | Query async task status |
| Command | Authentication | Description |
| ------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- |
| `bl video download` | API Key | Download a completed video by task ID |
| `bl video edit` | API Key | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) |
| `bl video generate` | API Key | Generate a video from text or image (wan3.0-video / wan2.6-t2v / happyhorse-1.1-i2v) |
| `bl video ref` | API Key | Reference-to-video generation (wan3.0-video / happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice |
| `bl video task get` | API Key | Query async task status |
## Command details
@@ -97,34 +97,35 @@ bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the
### `bl video generate`
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------------------ |
| **Name** | `video generate` |
| **Description** | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) |
| **Authentication** | API Key |
| **Usage** | `bl video generate --prompt <text> [--image <url>] [flags]` |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------------ |
| **Name** | `video generate` |
| **Description** | Generate a video from text or image (wan3.0-video / wan2.6-t2v / happyhorse-1.1-i2v) |
| **Authentication** | API Key |
| **Usage** | `bl video generate --prompt <text> [--image <url>] [flags]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: happyhorse-1.1-t2v, or happyhorse-1.1-i2v with --image) |
| `--prompt <text>` | string | yes | Video description |
| `--image <url>` | string | no | Input image URL for image-to-video generation |
| `--last-frame <url>` | string | no | Last frame image URL (with --image, enables kf2v first+last frame mode) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) |
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 5) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: wan3.0-video) |
| `--prompt <text>` | string | yes | Video description |
| `--image <url>` | string | no | Input image URL for image-to-video generation |
| `--last-frame <url>` | string | no | Last frame image URL (with --image, enables kf2v first+last frame mode) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) |
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--file <url-or-path>` | string | no | Reference file URL or local path for file-to-video (wan3.0-video only; mutually exclusive with --image/--last-frame) |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 5) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -150,35 +151,35 @@ bl video generate --prompt "A cat playing with a ball" --watermark false
### `bl video ref`
| Field | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| **Name** | `video ref` |
| **Description** | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice |
| **Authentication** | API Key |
| **Usage** | `bl video ref --prompt <text> --image <url>... [--ref-video <url>...] [flags]` |
| Field | Value |
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
| **Name** | `video ref` |
| **Description** | Reference-to-video generation (wan3.0-video / happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice |
| **Authentication** | API Key |
| **Usage** | `bl video ref --prompt <text> --image <url>... [--ref-video <url>...] [flags]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: happyhorse-1.1-r2v) |
| `--prompt <text>` | string | yes | Video description with reference markers (image1, video1, etc.) |
| `--image <url>` | array | no | Reference image URL or local file (repeatable for multiple subjects) |
| `--ref-video <url>` | array | no | Reference video URL or local file (repeatable) |
| `--image-voice <url>` | array | no | Voice URL for corresponding image (pairs by position) |
| `--video-voice <url>` | array | no | Voice URL for corresponding ref-video (pairs by position) |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1) |
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 15) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: wan3.0-video) |
| `--prompt <text>` | string | yes | Video description with reference markers (image1, video1, etc.) |
| `--image <url>` | array | no | Reference image URL or local file (repeatable for multiple subjects) |
| `--ref-video <url>` | array | no | Reference video URL or local file (repeatable) |
| `--image-voice <url>` | array | no | Voice URL for corresponding image (pairs by position). On wan3.0-video emitted as reference_audio (refer to Audio 1, Audio 2 in prompt) |
| `--video-voice <url>` | array | no | Voice URL for corresponding ref-video (pairs by position). On wan3.0-video emitted as reference_audio (refer to Audio 1, Audio 2 in prompt) |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1) |
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--async` | switch | no | Return async task id without waiting |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 15) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples