diff --git a/packages/cli/tests/stress/lib/fixtures.mjs b/packages/cli/tests/stress/lib/fixtures.mjs index 40380c001..e0152af0e 100644 --- a/packages/cli/tests/stress/lib/fixtures.mjs +++ b/packages/cli/tests/stress/lib/fixtures.mjs @@ -178,7 +178,7 @@ export async function ensurePrerequisites(ctx) { "video", "generate", "--model", - "happyhorse-1.1-t2v", + "wan3.0-video", "--prompt", "压测前置短视频:海浪与静态远景,无明显人物。", "--duration", diff --git a/packages/cli/tests/stress/lib/suite-fixtures.mjs b/packages/cli/tests/stress/lib/suite-fixtures.mjs index 589b2ea6e..147184cf9 100644 --- a/packages/cli/tests/stress/lib/suite-fixtures.mjs +++ b/packages/cli/tests/stress/lib/suite-fixtures.mjs @@ -130,7 +130,7 @@ export async function generateCombinedFixtures({ suiteRoot, cliPackage }) { "video", "generate", "--model", - "happyhorse-1.1-t2v", + "wan3.0-video", "--prompt", "压测前置短视频:海浪与静态远景,无明显人物。", "--duration", diff --git a/packages/cli/tests/stress/targets/video-i2v.mjs b/packages/cli/tests/stress/targets/video-i2v.mjs index ba8769a3b..d72b78f2b 100644 --- a/packages/cli/tests/stress/targets/video-i2v.mjs +++ b/packages/cli/tests/stress/targets/video-i2v.mjs @@ -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", diff --git a/packages/cli/tests/stress/targets/video-ref.mjs b/packages/cli/tests/stress/targets/video-ref.mjs index 913f79470..e2354e117 100644 --- a/packages/cli/tests/stress/targets/video-ref.mjs +++ b/packages/cli/tests/stress/targets/video-ref.mjs @@ -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", diff --git a/packages/cli/tests/stress/targets/video-t2v.mjs b/packages/cli/tests/stress/targets/video-t2v.mjs index 1f015d964..cc321650f 100644 --- a/packages/cli/tests/stress/targets/video-t2v.mjs +++ b/packages/cli/tests/stress/targets/video-t2v.mjs @@ -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`, diff --git a/packages/commands/src/commands/config/shared.ts b/packages/commands/src/commands/config/shared.ts index 8cd5446f7..d849901ed 100644 --- a/packages/commands/src/commands/config/shared.ts +++ b/packages/commands/src/commands/config/shared.ts @@ -69,7 +69,9 @@ export const UI_BOOLEAN_KEYS = new Set(["telemetry"]); export const UI_MODEL_DEFAULTS: Record = { 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 = { { 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" }, diff --git a/packages/commands/src/commands/video/generate.ts b/packages/commands/src/commands/video/generate.ts index 62276fde3..a2aab9aff 100644 --- a/packages/commands/src/commands/video/generate.ts +++ b/packages/commands/src/commands/video/generate.ts @@ -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 [--image ] [flags]", @@ -35,8 +34,8 @@ export default defineCommand({ type: "string", valueHint: "", 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: "", + 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, + 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) { @@ -243,7 +298,7 @@ export default defineCommand({ settings, () => ctx.client.requestJson({ - path: isKf2v ? image2videoPath() : videoGeneratePath(), + path: isKf2v && !isWan30 ? image2videoPath() : videoGeneratePath(), method: "POST", body, async: true, diff --git a/packages/commands/src/commands/video/ref.ts b/packages/commands/src/commands/video/ref.ts index 4d75de56c..65359072a 100644 --- a/packages/commands/src/commands/video/ref.ts +++ b/packages/commands/src/commands/video/ref.ts @@ -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 --image ... [--ref-video ...] [flags]", @@ -34,8 +35,8 @@ export default defineCommand({ type: "string", valueHint: "", 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: "", 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: "", 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); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index fe249e8d7..2c3167746 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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 = { diff --git a/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts index 3b686571b..0cc5a5536 100644 --- a/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts @@ -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}`); @@ -179,7 +180,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "generate", ...cliTimeoutPrefix(), "--model", - "happyhorse-1.1-i2v", + "wan3.0-video", "--image", "https://example.com/placeholder.png", ]); @@ -194,7 +195,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( ...cliTimeoutPrefix(), "--dry-run", "--model", - "happyhorse-1.1-t2v", + "wan3.0-video", "--prompt", "干跑无图", "--output", @@ -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, [ @@ -234,7 +235,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "generate", ...cliTimeoutPrefix(), "--model", - "happyhorse-1.1-i2v", + "wan3.0-video", "--image", imagePath, "--prompt", diff --git a/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts index e1efdfb44..9151548b1 100644 --- a/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts @@ -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", diff --git a/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts index 6d4856866..559bfbce6 100644 --- a/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts @@ -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); }, ); diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index fd3186d42..c6b053144 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -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; }>; diff --git a/packages/runtime/src/pipeline/steps/bl-api.ts b/packages/runtime/src/pipeline/steps/bl-api.ts index 8206068fd..3bbaa05bf 100644 --- a/packages/runtime/src/pipeline/steps/bl-api.ts +++ b/packages/runtime/src/pipeline/steps/bl-api.ts @@ -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) { diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 24f99cb76..4b7b78057 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -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 | diff --git a/skills/bailian-gen/reference/index.md b/skills/bailian-gen/reference/index.md index 85cab64b6..a85ecb331 100644 --- a/skills/bailian-gen/reference/index.md +++ b/skills/bailian-gen/reference/index.md @@ -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 diff --git a/skills/bailian-gen/reference/video.md b/skills/bailian-gen/reference/video.md index 527887330..8e6a21ddf 100644 --- a/skills/bailian-gen/reference/video.md +++ b/skills/bailian-gen/reference/video.md @@ -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 [--image ] [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 [--image ] [flags]` | #### Flags -| Flag | Type | Required | Description | -| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- | -| `--model ` | string | no | Model ID (default: happyhorse-1.1-t2v, or happyhorse-1.1-i2v with --image) | -| `--prompt ` | string | yes | Video description | -| `--image ` | string | no | Input image URL for image-to-video generation | -| `--last-frame ` | string | no | Last frame image URL (with --image, enables kf2v first+last frame mode) | -| `--negative-prompt ` | string | no | Negative prompt to exclude unwanted content | -| `--resolution ` | string | no | Resolution: 720P or 1080P (default: 1080P) | -| `--ratio ` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) | -| `--duration ` | number | no | Video duration in seconds (default: 5) | -| `--prompt-extend ` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). | -| `--watermark ` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). | -| `--seed ` | number | no | Random seed for reproducible generation | -| `--download ` | string | no | Save video to file on completion | -| `--async` | switch | no | Return async task id without waiting | -| `--concurrent ` | number | no | Run N parallel requests (default: 1) | -| `--poll-interval ` | number | no | Polling interval when waiting (default: 5) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- | +| `--model ` | string | no | Model ID (default: wan3.0-video) | +| `--prompt ` | string | yes | Video description | +| `--image ` | string | no | Input image URL for image-to-video generation | +| `--last-frame ` | string | no | Last frame image URL (with --image, enables kf2v first+last frame mode) | +| `--negative-prompt ` | string | no | Negative prompt to exclude unwanted content | +| `--resolution ` | string | no | Resolution: 720P or 1080P (default: 1080P) | +| `--ratio ` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) | +| `--duration ` | number | no | Video duration in seconds (default: 5) | +| `--prompt-extend ` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). | +| `--watermark ` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). | +| `--seed ` | number | no | Random seed for reproducible generation | +| `--download ` | string | no | Save video to file on completion | +| `--file ` | 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 ` | number | no | Run N parallel requests (default: 1) | +| `--poll-interval ` | number | no | Polling interval when waiting (default: 5) | +| `--api-key ` | string | no | API key | +| `--base-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 --image ... [--ref-video ...] [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 --image ... [--ref-video ...] [flags]` | #### Flags -| Flag | Type | Required | Description | -| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- | -| `--model ` | string | no | Model ID (default: happyhorse-1.1-r2v) | -| `--prompt ` | string | yes | Video description with reference markers (image1, video1, etc.) | -| `--image ` | array | no | Reference image URL or local file (repeatable for multiple subjects) | -| `--ref-video ` | array | no | Reference video URL or local file (repeatable) | -| `--image-voice ` | array | no | Voice URL for corresponding image (pairs by position) | -| `--video-voice ` | array | no | Voice URL for corresponding ref-video (pairs by position) | -| `--resolution ` | string | no | Resolution: 720P or 1080P (default: 1080P) | -| `--ratio ` | string | no | Aspect ratio (16:9, 9:16, 1:1) | -| `--duration ` | number | no | Video duration in seconds (default: 5) | -| `--prompt-extend ` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). | -| `--watermark ` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). | -| `--seed ` | number | no | Random seed for reproducible generation | -| `--download ` | string | no | Save video to file on completion | -| `--async` | switch | no | Return async task id without waiting | -| `--concurrent ` | number | no | Run N parallel requests (default: 1) | -| `--poll-interval ` | number | no | Polling interval when waiting (default: 15) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `--model ` | string | no | Model ID (default: wan3.0-video) | +| `--prompt ` | string | yes | Video description with reference markers (image1, video1, etc.) | +| `--image ` | array | no | Reference image URL or local file (repeatable for multiple subjects) | +| `--ref-video ` | array | no | Reference video URL or local file (repeatable) | +| `--image-voice ` | 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 ` | 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 ` | string | no | Resolution: 720P or 1080P (default: 1080P) | +| `--ratio ` | string | no | Aspect ratio (16:9, 9:16, 1:1) | +| `--duration ` | number | no | Video duration in seconds (default: 5) | +| `--prompt-extend ` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). | +| `--watermark ` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). | +| `--seed ` | number | no | Random seed for reproducible generation | +| `--download ` | string | no | Save video to file on completion | +| `--async` | switch | no | Return async task id without waiting | +| `--concurrent ` | number | no | Run N parallel requests (default: 1) | +| `--poll-interval ` | number | no | Polling interval when waiting (default: 15) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Examples