fix(image): resolve size and prompt_extend by model profile

Stop inferring size/prompt_extend from sync vs async; use per-family sizeProfile. wanx*-imageedit uses function+base_image_url; bare qwen-image uses the fixed resolution table.
This commit is contained in:
qcq01083097
2026-07-27 10:17:09 +08:00
parent 9a13700390
commit c4f5bb09c6
12 changed files with 604 additions and 164 deletions
+60 -29
View File
@@ -31,12 +31,6 @@ import { resolveImageSize } from "bailian-cli-runtime";
import { join } from "path";
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
function enablesPromptExtendByDefault(model: string): boolean {
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
}
const EDIT_FLAGS = {
image: {
type: "array",
@@ -71,6 +65,12 @@ const EDIT_FLAGS = {
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
},
function: {
type: "string",
valueHint: "<name>",
description:
"wanx*-imageedit function (default: description_edit). Examples: stylization_all, description_edit",
},
promptExtend: {
type: "boolean",
valueHint: "<bool>",
@@ -109,6 +109,7 @@ export default defineCommand({
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
'--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview',
'--image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all',
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
],
async run(ctx) {
@@ -133,14 +134,14 @@ export default defineCommand({
const promptExtend = resolveBooleanFlag(
flags.promptExtend,
enablesPromptExtendByDefault(model) ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark);
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(flags.size, route.useSync),
size: resolveImageSize(flags.size, route.sizeProfile),
n,
seed: flags.seed,
prompt_extend: promptExtend,
@@ -148,7 +149,24 @@ export default defineCommand({
};
let body: DashScopeImageRequest;
if (route.inputStyle === "prompt-images") {
if (route.inputStyle === "function-base-image") {
const baseImageUrl = resolvedImages[0];
if (!baseImageUrl) {
throw new BailianError(
"wanx*-imageedit requires at least one --image as base_image_url.",
ExitCode.USAGE,
);
}
body = {
model,
input: {
function: flags.function || "description_edit",
prompt,
base_image_url: baseImageUrl,
},
parameters,
};
} else if (route.inputStyle === "prompt-images") {
body = {
model,
input: {
@@ -186,26 +204,39 @@ export default defineCommand({
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
const previewBody =
"messages" in body.input
? {
...body,
input: {
messages: body.input.messages.map((message) => ({
...message,
content: message.content.map((item) =>
item.image ? { ...item, image: redactDataUri(item.image) } : item,
),
})),
},
}
: {
...body,
input: {
...body.input,
images: body.input.images?.map((imageUrl) => redactDataUri(imageUrl)),
},
};
let previewBody: DashScopeImageRequest = body;
if ("messages" in body.input) {
previewBody = {
...body,
input: {
messages: body.input.messages.map((message) => ({
...message,
content: message.content.map((item) =>
item.image ? { ...item, image: redactDataUri(item.image) } : item,
),
})),
},
};
} else if ("images" in body.input) {
previewBody = {
...body,
input: {
...body.input,
images: body.input.images?.map((imageUrl) => redactDataUri(imageUrl)),
},
};
} else if ("base_image_url" in body.input) {
previewBody = {
...body,
input: {
...body.input,
base_image_url: redactDataUri(body.input.base_image_url),
mask_image_url: body.input.mask_image_url
? redactDataUri(body.input.mask_image_url)
: undefined,
},
};
}
emitResult(
{ request: previewBody, mode: route.useSync ? "sync" : "async", path: route.path },
format,
@@ -30,12 +30,6 @@ import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bai
import { join } from "path";
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
function enablesPromptExtendByDefault(model: string): boolean {
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
}
const GENERATE_FLAGS = {
prompt: { type: "string", valueHint: "<text>", description: "Image description", required: true },
model: {
@@ -115,13 +109,13 @@ export default defineCommand({
const route = resolveImageGenerateApi(model);
const defaultSize = "1:1";
const sizeInput = flags.size || defaultSize;
const size = resolveImageSize(sizeInput, route.useSync);
const size = resolveImageSize(sizeInput, route.sizeProfile);
const n = flags.n ?? 1;
const concurrent = getConcurrency(flags);
const promptExtend = resolveBooleanFlag(
flags.promptExtend,
enablesPromptExtendByDefault(model) ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
@@ -96,6 +96,78 @@ describe("e2e: image edit", () => {
"data:image/png;base64,<omitted>",
);
});
test("wan2.5-i2i-preview dry-run 走 prompt+images", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"edit",
"--model",
"wan2.5-i2i-preview",
"--image",
"https://example.com/source.png",
"--prompt",
"Place on a table",
"--size",
"1:1",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
mode?: string;
path?: string;
request?: {
input?: { prompt?: string; images?: string[]; messages?: unknown };
parameters?: { size?: string };
};
}>(stdout);
expect(data.mode).toBe("async");
expect(data.path).toBe("/api/v1/services/aigc/image2image/image-synthesis");
expect(data.request?.input?.prompt).toBe("Place on a table");
expect(data.request?.input?.images).toEqual(["https://example.com/source.png"]);
expect(data.request?.input?.messages).toBeUndefined();
expect(data.request?.parameters?.size).toBe("1280*1280");
});
test("wanx2.1-imageedit dry-run 走 function + base_image_url", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"edit",
"--model",
"wanx2.1-imageedit",
"--image",
"https://example.com/source.png",
"--prompt",
"转换成绘本风格",
"--function",
"stylization_all",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
mode?: string;
path?: string;
request?: {
input?: {
function?: string;
prompt?: string;
base_image_url?: string;
images?: unknown;
messages?: unknown;
};
};
}>(stdout);
expect(data.mode).toBe("async");
expect(data.path).toBe("/api/v1/services/aigc/image2image/image-synthesis");
expect(data.request?.input?.function).toBe("stylization_all");
expect(data.request?.input?.prompt).toBe("转换成绘本风格");
expect(data.request?.input?.base_image_url).toBe("https://example.com/source.png");
expect(data.request?.input?.images).toBeUndefined();
expect(data.request?.input?.messages).toBeUndefined();
});
});
describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: image edit", () => {
@@ -107,6 +107,65 @@ describe("e2e: image generate", () => {
expect(data.request?.model).toBe("qwen-image-plus");
});
test("qwen-image-plus 默认 1:1 映射为 1328*1328", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"qwen-image-plus",
"--prompt",
"一只猫",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { size?: string; prompt_extend?: boolean } };
}>(stdout);
expect(data.request?.parameters?.size).toBe("1328*1328");
});
test("z-image-turbo 默认 prompt_extend 为 false", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"z-image-turbo",
"--prompt",
"一只猫",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { size?: string; prompt_extend?: boolean } };
}>(stdout);
expect(data.request?.parameters?.prompt_extend).toBe(false);
expect(data.request?.parameters?.size).toBe("1024*1024");
});
test("wanx-v1 默认 1:1 映射为 1024*1024", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
"generate",
"--model",
"wanx-v1",
"--prompt",
"一只猫",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { parameters?: { size?: string }; input?: { prompt?: string } };
}>(stdout);
expect(data.request?.parameters?.size).toBe("1024*1024");
expect(data.request?.input?.prompt).toBe("一只猫");
});
test("wanx2.0-t2i-turbo dry-run 走 text2image prompt 路径", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [
"image",
+150 -49
View File
@@ -10,8 +10,10 @@ import { image2ImagePath, imagePath, imageSyncPath, imageText2ImagePath } from "
* - async text2image + prompt: wan2.5/2.2/2.1-t2i*, wanx*-t2i*
*
* Edit (I2I):
* - sync multimodal + messages(+images): qwen-image-*, wan2.6-image*, wan2.7-image*, z-image*
* - async image2image + prompt/images: wan2.5-i2i*, *imageedit*
* - sync multimodal + messages(+images): qwen-image-2.0*, qwen-image-edit*, wan2.6-image*, wan2.7-image*
* (pure T2I models such as z-image / qwen-image-plus / qwen-image-max are NOT edit models)
* - async image2image + prompt/images: wan2.5-i2i*
* - async image2image + function/base_image_url: *imageedit* (e.g. wanx2.1-imageedit)
* - async image-generation + messages(+images): other async fallbacks
*/
@@ -21,33 +23,56 @@ export type ImageApiKind =
| "async-text2image"
| "async-image2image";
/** Model-family size presets — not inferred from sync/async. */
export type ImageSizeProfile =
| "qwen-image-2.0"
| "qwen-image-fixed"
| "wan27"
| "z-image"
| "wan26"
| "wan-legacy"
| "wanx-v1"
| "wan25-i2i";
export type ImageInputStyle = "messages" | "prompt" | "prompt-images" | "function-base-image";
export interface ImageApiRoute {
kind: ImageApiKind;
path: string;
/** True when the call is synchronous (no X-DashScope-Async / task poll). */
useSync: boolean;
/** How to shape `input` in the request body. */
inputStyle: "messages" | "prompt" | "prompt-images";
inputStyle: ImageInputStyle;
/** Ratio → pixel map family for `--size`. */
sizeProfile: ImageSizeProfile;
/**
* CLI default when `--prompt-extend` is omitted.
* `undefined` means omit the parameter (leave to DashScope default).
*/
promptExtendDefault?: boolean;
}
/** Models that accept text-only sync multimodal for generate. */
const SYNC_GENERATE_PREFIXES = ["qwen-image", "wan2.7-image", "z-image"] as const;
/**
* Extra models that use sync multimodal only for edit (messages must include images).
* wan2.6-image generate is async image-generation instead.
* Models that support sync multimodal edit (messages must include images).
* Pure T2I models (z-image / qwen-image-plus / qwen-image-max) are excluded.
*/
const SYNC_EDIT_ONLY_PREFIXES = ["wan2.6-image"] as const;
const SYNC_EDIT_PREFIXES = [
"qwen-image-2.0",
"qwen-image-edit",
"wan2.7-image",
"wan2.6-image",
] as const;
function startsWithAny(model: string, prefixes: readonly string[]): boolean {
return prefixes.some((prefix) => model.startsWith(prefix));
}
/** True when the model family can use sync multimodal (generate and/or edit). */
/** True when the model family can use sync multimodal for generate. */
export function isSyncMultimodalImageModel(model: string): boolean {
return (
startsWithAny(model, SYNC_GENERATE_PREFIXES) || startsWithAny(model, SYNC_EDIT_ONLY_PREFIXES)
);
return startsWithAny(model, SYNC_GENERATE_PREFIXES) || model.startsWith("wan2.6-image");
}
function isSyncGenerateModel(model: string): boolean {
@@ -55,7 +80,7 @@ function isSyncGenerateModel(model: string): boolean {
}
function isSyncEditModel(model: string): boolean {
return isSyncMultimodalImageModel(model);
return startsWithAny(model, SYNC_EDIT_PREFIXES);
}
/** wan2.5 / wan2.2 / wan2.1 / wanx text-to-image models use the legacy prompt API. */
@@ -68,58 +93,134 @@ export function isLegacyText2ImageModel(model: string): boolean {
return false;
}
/** wan2.5-i2i / *imageedit* use the legacy image2image prompt+images API. */
/** wan2.5-i2i uses the legacy image2image prompt+images API. */
export function isLegacyImage2ImageModel(model: string): boolean {
return /wan2\.5-i2i/i.test(model) || /imageedit/i.test(model);
return /wan2\.5-i2i/i.test(model);
}
/** wanx*-imageedit uses function + base_image_url (not prompt+images). */
export function isWanxFunctionImageEditModel(model: string): boolean {
return /imageedit/i.test(model);
}
export function resolveImageSizeProfile(model: string): ImageSizeProfile {
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-edit")) {
return "qwen-image-2.0";
}
// Remaining qwen-image* (plus / max / bare qwen-image) share the fixed table.
if (model.startsWith("qwen-image")) {
return "qwen-image-fixed";
}
if (model.startsWith("wan2.7-image")) return "wan27";
if (model.startsWith("z-image")) return "z-image";
if (
model.startsWith("wan2.6-t2i") ||
model.startsWith("wan2.6-image") ||
model.startsWith("wan2.5-t2i")
) {
return "wan26";
}
if (/^wanx-v1$/i.test(model)) return "wanx-v1";
if (/wan2\.5-i2i/i.test(model)) return "wan25-i2i";
if (isLegacyText2ImageModel(model)) return "wan-legacy";
return "wan26";
}
/** Official / CLI defaults for prompt_extend when the flag is omitted. */
export function resolvePromptExtendDefault(model: string): boolean | undefined {
if (model.startsWith("qwen-image-2.0") || model.startsWith("qwen-image-max")) return true;
// Z-Image docs default prompt_extend to false.
if (model.startsWith("z-image")) return false;
return undefined;
}
function buildRoute(
partial: Omit<ImageApiRoute, "sizeProfile" | "promptExtendDefault">,
model: string,
): ImageApiRoute {
return {
...partial,
sizeProfile: resolveImageSizeProfile(model),
promptExtendDefault: resolvePromptExtendDefault(model),
};
}
export function resolveImageGenerateApi(model: string): ImageApiRoute {
if (isSyncGenerateModel(model)) {
return {
kind: "sync-multimodal",
path: imageSyncPath(),
useSync: true,
inputStyle: "messages",
};
return buildRoute(
{
kind: "sync-multimodal",
path: imageSyncPath(),
useSync: true,
inputStyle: "messages",
},
model,
);
}
if (isLegacyText2ImageModel(model)) {
return {
kind: "async-text2image",
path: imageText2ImagePath(),
useSync: false,
inputStyle: "prompt",
};
return buildRoute(
{
kind: "async-text2image",
path: imageText2ImagePath(),
useSync: false,
inputStyle: "prompt",
},
model,
);
}
// Includes wan2.6-t2i* and wan2.6-image* (text-only generate).
return {
kind: "async-image-generation",
path: imagePath(),
useSync: false,
inputStyle: "messages",
};
return buildRoute(
{
kind: "async-image-generation",
path: imagePath(),
useSync: false,
inputStyle: "messages",
},
model,
);
}
export function resolveImageEditApi(model: string): ImageApiRoute {
if (isSyncEditModel(model)) {
return {
kind: "sync-multimodal",
path: imageSyncPath(),
useSync: true,
inputStyle: "messages",
};
return buildRoute(
{
kind: "sync-multimodal",
path: imageSyncPath(),
useSync: true,
inputStyle: "messages",
},
model,
);
}
if (isWanxFunctionImageEditModel(model)) {
return buildRoute(
{
kind: "async-image2image",
path: image2ImagePath(),
useSync: false,
inputStyle: "function-base-image",
},
model,
);
}
if (isLegacyImage2ImageModel(model)) {
return {
kind: "async-image2image",
path: image2ImagePath(),
useSync: false,
inputStyle: "prompt-images",
};
return buildRoute(
{
kind: "async-image2image",
path: image2ImagePath(),
useSync: false,
inputStyle: "prompt-images",
},
model,
);
}
return {
kind: "async-image-generation",
path: imagePath(),
useSync: false,
inputStyle: "messages",
};
return buildRoute(
{
kind: "async-image-generation",
path: imagePath(),
useSync: false,
inputStyle: "messages",
},
model,
);
}
+5
View File
@@ -24,10 +24,15 @@ export {
isLegacyImage2ImageModel,
isLegacyText2ImageModel,
isSyncMultimodalImageModel,
isWanxFunctionImageEditModel,
resolveImageEditApi,
resolveImageGenerateApi,
resolveImageSizeProfile,
resolvePromptExtendDefault,
type ImageApiKind,
type ImageApiRoute,
type ImageInputStyle,
type ImageSizeProfile,
} from "./image-routes.ts";
export { CHANNEL, SOURCE_CONFIG, TAGS, trackingHeaders } from "./headers.ts";
export type { RequestOpts } from "./http.ts";
+8
View File
@@ -124,6 +124,13 @@ export interface DashScopeImageRequest {
/** Required by image2image models such as wan2.5-i2i-preview. */
images?: string[];
negative_prompt?: string;
}
| {
/** Required by wanx*-imageedit models. */
function: string;
prompt: string;
base_image_url: string;
mask_image_url?: string;
};
parameters?: {
size?: string;
@@ -132,6 +139,7 @@ export interface DashScopeImageRequest {
prompt_extend?: boolean;
watermark?: boolean;
negative_prompt?: string;
strength?: number;
};
}
+74 -39
View File
@@ -3,8 +3,11 @@ import {
isLegacyImage2ImageModel,
isLegacyText2ImageModel,
isSyncMultimodalImageModel,
isWanxFunctionImageEditModel,
resolveImageEditApi,
resolveImageGenerateApi,
resolveImageSizeProfile,
resolvePromptExtendDefault,
} from "../src/client/image-routes.ts";
test("sync multimodal family covers qwen-image, wan2.6/2.7 image, and z-image", () => {
@@ -23,79 +26,111 @@ test("legacy text2image covers wan2.5/2.2/2.1 t2i and wanx but not wan2.6-t2i/im
expect(isLegacyText2ImageModel("wan2.5-t2i-preview")).toBe(true);
expect(isLegacyText2ImageModel("wan2.1-t2i-turbo")).toBe(true);
expect(isLegacyText2ImageModel("wanx2.0-t2i-turbo")).toBe(true);
expect(isLegacyText2ImageModel("wanx-v1")).toBe(true);
expect(isLegacyText2ImageModel("wan2.6-t2i")).toBe(false);
expect(isLegacyText2ImageModel("wan2.6-image")).toBe(false);
expect(isLegacyText2ImageModel("wan2.7-image")).toBe(false);
});
test("legacy image2image covers wan2.5-i2i and imageedit models", () => {
test("legacy image2image is wan2.5-i2i only; wanx imageedit uses function protocol", () => {
expect(isLegacyImage2ImageModel("wan2.5-i2i-preview")).toBe(true);
expect(isLegacyImage2ImageModel("wanx2.1-imageedit")).toBe(true);
expect(isLegacyImage2ImageModel("wan2.6-image")).toBe(false);
expect(isLegacyImage2ImageModel("wanx2.1-imageedit")).toBe(false);
expect(isWanxFunctionImageEditModel("wanx2.1-imageedit")).toBe(true);
expect(isWanxFunctionImageEditModel("wan2.5-i2i-preview")).toBe(false);
});
test("resolveImageGenerateApi picks path and input style by model family", () => {
test("size profiles are model-specific, not sync/async", () => {
expect(resolveImageSizeProfile("qwen-image-2.0")).toBe("qwen-image-2.0");
expect(resolveImageSizeProfile("qwen-image")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("qwen-image-plus")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("qwen-image-max")).toBe("qwen-image-fixed");
expect(resolveImageSizeProfile("wan2.7-image")).toBe("wan27");
expect(resolveImageSizeProfile("z-image-turbo")).toBe("z-image");
expect(resolveImageSizeProfile("wan2.6-t2i")).toBe("wan26");
expect(resolveImageSizeProfile("wanx-v1")).toBe("wanx-v1");
expect(resolveImageSizeProfile("wan2.5-i2i-preview")).toBe("wan25-i2i");
expect(resolveImageSizeProfile("wan2.2-t2i-plus")).toBe("wan-legacy");
});
test("prompt_extend defaults follow model docs", () => {
expect(resolvePromptExtendDefault("qwen-image-2.0")).toBe(true);
expect(resolvePromptExtendDefault("qwen-image-max")).toBe(true);
expect(resolvePromptExtendDefault("z-image-turbo")).toBe(false);
expect(resolvePromptExtendDefault("wan2.7-image")).toBeUndefined();
expect(resolvePromptExtendDefault("qwen-image-plus")).toBeUndefined();
});
test("resolveImageGenerateApi picks path, input style, and size profile", () => {
expect(resolveImageGenerateApi("wanx2.0-t2i-turbo")).toMatchObject({
kind: "async-text2image",
path: "/api/v1/services/aigc/text2image/image-synthesis",
inputStyle: "prompt",
useSync: false,
sizeProfile: "wan-legacy",
});
expect(resolveImageGenerateApi("wan2.2-t2i-plus")).toMatchObject({
kind: "async-text2image",
path: "/api/v1/services/aigc/text2image/image-synthesis",
expect(resolveImageGenerateApi("wanx-v1")).toMatchObject({
sizeProfile: "wanx-v1",
inputStyle: "prompt",
useSync: false,
});
expect(resolveImageGenerateApi("wan2.6-t2i")).toMatchObject({
kind: "async-image-generation",
path: "/api/v1/services/aigc/image-generation/generation",
inputStyle: "messages",
useSync: false,
});
expect(resolveImageGenerateApi("wan2.6-image")).toMatchObject({
kind: "async-image-generation",
path: "/api/v1/services/aigc/image-generation/generation",
inputStyle: "messages",
useSync: false,
sizeProfile: "wan26",
});
expect(resolveImageGenerateApi("qwen-image-2.0")).toMatchObject({
kind: "sync-multimodal",
path: "/api/v1/services/aigc/multimodal-generation/generation",
inputStyle: "messages",
useSync: true,
});
expect(resolveImageGenerateApi("z-image-turbo")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
sizeProfile: "qwen-image-2.0",
promptExtendDefault: true,
});
expect(resolveImageGenerateApi("qwen-image-plus")).toMatchObject({
kind: "sync-multimodal",
path: "/api/v1/services/aigc/multimodal-generation/generation",
inputStyle: "messages",
useSync: true,
sizeProfile: "qwen-image-fixed",
});
expect(resolveImageGenerateApi("wan2.7-image")).toMatchObject({
expect(resolveImageGenerateApi("qwen-image")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
sizeProfile: "qwen-image-fixed",
});
expect(resolveImageGenerateApi("z-image-turbo")).toMatchObject({
kind: "sync-multimodal",
sizeProfile: "z-image",
promptExtendDefault: false,
});
});
test("resolveImageEditApi picks path and input style by model family", () => {
expect(resolveImageEditApi("wan2.5-i2i-preview")).toMatchObject({
kind: "async-image2image",
path: "/api/v1/services/aigc/image2image/image-synthesis",
inputStyle: "prompt-images",
useSync: false,
});
expect(resolveImageEditApi("wan2.6-image")).toMatchObject({
test("resolveImageEditApi excludes pure T2I models from sync edit", () => {
expect(resolveImageEditApi("wan2.7-image")).toMatchObject({
kind: "sync-multimodal",
path: "/api/v1/services/aigc/multimodal-generation/generation",
inputStyle: "messages",
useSync: true,
});
expect(resolveImageEditApi("wan2.7-image")).toMatchObject({
expect(resolveImageEditApi("wan2.6-image")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
});
expect(resolveImageEditApi("qwen-image-2.0")).toMatchObject({
kind: "sync-multimodal",
useSync: true,
});
// Pure T2I models fall through to async image-generation, not sync edit.
expect(resolveImageEditApi("z-image-turbo")).toMatchObject({
kind: "async-image-generation",
useSync: false,
});
expect(resolveImageEditApi("qwen-image-plus")).toMatchObject({
kind: "async-image-generation",
useSync: false,
});
expect(resolveImageEditApi("qwen-image-max")).toMatchObject({
kind: "async-image-generation",
useSync: false,
});
expect(resolveImageEditApi("wan2.5-i2i-preview")).toMatchObject({
kind: "async-image2image",
inputStyle: "prompt-images",
sizeProfile: "wan25-i2i",
});
expect(resolveImageEditApi("wanx2.1-imageedit")).toMatchObject({
kind: "async-image2image",
inputStyle: "function-base-image",
path: "/api/v1/services/aigc/image2image/image-synthesis",
});
});
+24 -5
View File
@@ -177,12 +177,12 @@ export async function imageGenerate(
const promptExtend = resolveBooleanFlag(
input["prompt-extend"],
route.useSync ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(input.size, route.useSync),
size: resolveImageSize(input.size, route.sizeProfile),
n,
seed: input.seed,
prompt_extend: promptExtend,
@@ -252,6 +252,7 @@ export interface ImageEditInput {
"negative-prompt"?: string;
"prompt-extend"?: boolean | string;
watermark?: boolean | string;
function?: string;
"out-dir"?: string;
"out-prefix"?: string;
}
@@ -274,7 +275,7 @@ export async function imageEdit(
const promptExtend = resolveBooleanFlag(
input["prompt-extend"],
route.useSync ? true : undefined,
route.promptExtendDefault,
"prompt-extend",
);
@@ -288,7 +289,7 @@ export async function imageEdit(
}
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(input.size, route.useSync),
size: resolveImageSize(input.size, route.sizeProfile),
n,
seed: input.seed,
prompt_extend: promptExtend,
@@ -296,7 +297,25 @@ export async function imageEdit(
};
let body: DashScopeImageRequest;
if (route.inputStyle === "prompt-images") {
if (route.inputStyle === "function-base-image") {
const baseImageUrl = resolvedImages[0];
if (!baseImageUrl) {
throw new PipelineError(
"missing_input",
"image/edit with wanx*-imageedit requires at least one image",
{ step: "image/edit" },
);
}
body = {
model,
input: {
function: input.function || "description_edit",
prompt: input.prompt,
base_image_url: baseImageUrl,
},
parameters,
};
} else if (route.inputStyle === "prompt-images") {
body = {
model,
input: {
+99 -16
View File
@@ -1,19 +1,15 @@
import type { ImageSizeProfile } from "bailian-cli-core";
/**
* Resolve image `size` flag for image generate/edit.
* Resolve image `--size` for generate/edit by model-family profile.
*
* Users may pass either a ratio (e.g. "1:1", "3:4", "16:9") or a pixel size
* (e.g. "2048*2048"). The DashScope API only accepts the pixel format, so we
* map known ratios to the recommended pixel size for each model family.
*
* Sync models (qwen-image-2.0 / qwen-image-max / qwen-image-edit-2.0):
* higher-resolution presets.
*
* Async models (wanx2.x): smaller presets.
*
* Pixel-format input is passed through unchanged.
* Users may pass a ratio (e.g. "1:1") or pixels (e.g. "2048*2048").
* Pixel input is passed through; ratios are mapped per model profile.
* Do not infer size from sync/async — that mismatches model constraints.
*/
export const SYNC_RATIO_MAP: Record<string, string> = {
/** qwen-image-2.0 / qwen-image-edit recommended high-res presets. */
export const QWEN_IMAGE_20_RATIO_MAP: Record<string, string> = {
"16:9": "2688*1536",
"9:16": "1536*2688",
"1:1": "2048*2048",
@@ -21,7 +17,8 @@ export const SYNC_RATIO_MAP: Record<string, string> = {
"3:4": "1728*2368",
};
export const ASYNC_RATIO_MAP: Record<string, string> = {
/** qwen-image-plus / qwen-image-max fixed resolution presets. */
export const QWEN_IMAGE_FIXED_RATIO_MAP: Record<string, string> = {
"16:9": "1664*928",
"4:3": "1472*1104",
"1:1": "1328*1328",
@@ -29,11 +26,97 @@ export const ASYNC_RATIO_MAP: Record<string, string> = {
"9:16": "928*1664",
};
/** Resolve `--size` value: accept ratio (3:4) or pixel (W*H) format. */
/** wan2.7-image* — default 2K square for 1:1. */
export const WAN27_RATIO_MAP: Record<string, string> = {
"16:9": "2688*1536",
"9:16": "1536*2688",
"1:1": "2048*2048",
"4:3": "2368*1728",
"3:4": "1728*2368",
};
/** z-image* recommended ~1K presets. */
export const Z_IMAGE_RATIO_MAP: Record<string, string> = {
"1:1": "1024*1024",
"16:9": "1280*720",
"9:16": "720*1280",
"4:3": "1152*864",
"3:4": "864*1152",
"3:2": "1248*832",
"2:3": "832*1248",
};
/** wan2.6-t2i / wan2.6-image / wan2.5-t2i — ~1280 class. */
export const WAN26_RATIO_MAP: Record<string, string> = {
"1:1": "1280*1280",
"16:9": "1280*720",
"9:16": "720*1280",
"4:3": "1472*1104",
"3:4": "1104*1472",
};
/** wan2.2 and earlier t2i / wanx*-t2i — 1024 class. */
export const WAN_LEGACY_RATIO_MAP: Record<string, string> = {
"1:1": "1024*1024",
"16:9": "1280*720",
"9:16": "720*1280",
"3:4": "768*1152",
"4:3": "1152*768",
};
/** wanx-v1 only documents these discrete sizes. */
export const WANX_V1_RATIO_MAP: Record<string, string> = {
"1:1": "1024*1024",
"9:16": "720*1280",
"3:4": "768*1152",
"16:9": "1280*720",
};
/** wan2.5-i2i — total pixels up to ~1280*1280. */
export const WAN25_I2I_RATIO_MAP: Record<string, string> = {
"1:1": "1280*1280",
"16:9": "1280*720",
"9:16": "720*1280",
"4:3": "1152*864",
"3:4": "864*1152",
};
/** @deprecated Prefer profile maps; kept for callers that still think in sync/async. */
export const SYNC_RATIO_MAP = QWEN_IMAGE_20_RATIO_MAP;
/** @deprecated Prefer profile maps; kept as qwen-image-plus/max fixed presets. */
export const ASYNC_RATIO_MAP = QWEN_IMAGE_FIXED_RATIO_MAP;
const PROFILE_RATIO_MAPS: Record<ImageSizeProfile, Record<string, string>> = {
"qwen-image-2.0": QWEN_IMAGE_20_RATIO_MAP,
"qwen-image-fixed": QWEN_IMAGE_FIXED_RATIO_MAP,
wan27: WAN27_RATIO_MAP,
"z-image": Z_IMAGE_RATIO_MAP,
wan26: WAN26_RATIO_MAP,
"wan-legacy": WAN_LEGACY_RATIO_MAP,
"wanx-v1": WANX_V1_RATIO_MAP,
"wan25-i2i": WAN25_I2I_RATIO_MAP,
};
/** Resolve `--size` with an explicit model size profile. */
export function resolveImageSize(input: string, sizeProfile: ImageSizeProfile): string;
export function resolveImageSize(
input: string | undefined,
sizeProfile: ImageSizeProfile,
): string | undefined;
/** @deprecated Prefer `ImageSizeProfile`; boolean maps sync→qwen-image-2.0 / async→qwen-image-fixed. */
export function resolveImageSize(input: string, useSync: boolean): string;
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined;
export function resolveImageSize(input: string | undefined, useSync: boolean): string | undefined {
export function resolveImageSize(
input: string | undefined,
sizeProfileOrUseSync: ImageSizeProfile | boolean,
): string | undefined {
if (!input) return undefined;
const map = useSync ? SYNC_RATIO_MAP : ASYNC_RATIO_MAP;
const profile: ImageSizeProfile =
typeof sizeProfileOrUseSync === "boolean"
? sizeProfileOrUseSync
? "qwen-image-2.0"
: "qwen-image-fixed"
: sizeProfileOrUseSync;
const map = PROFILE_RATIO_MAPS[profile];
return map[input] ?? input;
}
+28
View File
@@ -0,0 +1,28 @@
import { expect, test } from "vite-plus/test";
import { resolveImageSize } from "../src/utils/image-size.ts";
test("qwen-image-plus fixed profile maps 1:1 to 1328*1328", () => {
expect(resolveImageSize("1:1", "qwen-image-fixed")).toBe("1328*1328");
expect(resolveImageSize("16:9", "qwen-image-fixed")).toBe("1664*928");
});
test("qwen-image-2.0 profile maps 1:1 to 2048*2048", () => {
expect(resolveImageSize("1:1", "qwen-image-2.0")).toBe("2048*2048");
});
test("wanx-v1 profile maps 1:1 to 1024*1024", () => {
expect(resolveImageSize("1:1", "wanx-v1")).toBe("1024*1024");
expect(resolveImageSize("16:9", "wanx-v1")).toBe("1280*720");
});
test("wan2.5-i2i profile maps 1:1 to 1280*1280", () => {
expect(resolveImageSize("1:1", "wan25-i2i")).toBe("1280*1280");
});
test("z-image profile maps 1:1 to 1024*1024", () => {
expect(resolveImageSize("1:1", "z-image")).toBe("1024*1024");
});
test("pixel sizes pass through unchanged", () => {
expect(resolveImageSize("1024*1024", "qwen-image-fixed")).toBe("1024*1024");
});
+23 -18
View File
@@ -24,24 +24,25 @@ Index: [index.md](index.md)
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | ----------------------------------------------------------------------- |
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
| `--prompt <text>` | string | yes | Edit instruction text |
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
| `--seed <n>` | number | no | Random seed for reproducible results |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--out-dir <dir>` | string | no | Download images to directory |
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
| `--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: 3) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------- |
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
| `--prompt <text>` | string | yes | Edit instruction text |
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
| `--seed <n>` | number | no | Random seed for reproducible results |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--function <name>` | string | no | wanx\*-imageedit function (default: description_edit). Examples: stylization_all, description_edit |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--out-dir <dir>` | string | no | Download images to directory |
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
| `--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: 3) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -69,6 +70,10 @@ bl image edit --image ./photo.png --prompt "Change the style" --model wan2.7-ima
bl image edit --image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview
```
```bash
bl image edit --image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all
```
```bash
bl image edit --image ./photo.png --prompt "Replace the background with a beach" --watermark false
```