Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/tests/stress/lib/fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export async function ensurePrerequisites(ctx) {
"video",
"generate",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"压测前置短视频:海浪与静态远景,无明显人物。",
"--duration",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/stress/lib/suite-fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export async function generateCombinedFixtures({ suiteRoot, cliPackage }) {
"video",
"generate",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"压测前置短视频:海浪与静态远景,无明显人物。",
"--duration",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/stress/targets/video-i2v.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/stress/targets/video-ref.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/stress/targets/video-t2v.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
16 changes: 14 additions & 2 deletions packages/commands/src/commands/config/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Expand Down Expand Up @@ -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" },
Expand Down
93 changes: 74 additions & 19 deletions packages/commands/src/commands/video/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand All @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
Expand All @@ -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");
Expand All @@ -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,
Expand All @@ -210,13 +249,29 @@ export default defineCommand({

if (settings.dryRun) {
let previewBody = body;
if (isKf2v) {
if (resolvedFileUrl) {
previewBody = {
...body,
input: {
...body.input,
media: [{ type: "file" as const, url: redactDataUri(resolvedFileUrl) }],
},
};
} else if (isKf2v) {
const redactedFirst = redactDataUri(resolvedImageUrl ?? "");
const redactedLast = redactDataUri(resolvedLastFrameUrl ?? "");
previewBody = {
...body,
input: {
...body.input,
first_frame_url: redactDataUri(resolvedImageUrl ?? ""),
last_frame_url: redactDataUri(resolvedLastFrameUrl ?? ""),
...(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) {
Expand All @@ -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,
Expand Down
47 changes: 36 additions & 11 deletions packages/commands/src/commands/video/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand All @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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++) {
Expand All @@ -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);
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/commands/tests/e2e/topic-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
11 changes: 6 additions & 5 deletions packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ 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[].
["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}`);
Expand Down Expand Up @@ -179,7 +180,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"generate",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-i2v",
"wan3.0-video",
"--image",
"https://example.com/placeholder.png",
]);
Expand All @@ -194,7 +195,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
...cliTimeoutPrefix(),
"--dry-run",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"干跑无图",
"--output",
Expand All @@ -208,7 +209,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, [
Expand All @@ -234,7 +235,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
"generate",
...cliTimeoutPrefix(),
"--model",
"happyhorse-1.1-i2v",
"wan3.0-video",
"--image",
imagePath,
"--prompt",
Expand Down
Loading