mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-21 09:49:48 +08:00
feat(baoyu-imagine): add DashScope Wan 2.7 image model support (#141)
* feat(baoyu-imagine): add DashScope Wan 2.7 image model support Closes #139. Adds the new `wan2.7-image-pro` and `wan2.7-image` model family to the DashScope provider so users can call Wan 2.7 directly through the official Aliyun (Bailian) API instead of going through Replicate. - Register `wan2.7-image-pro` and `wan2.7-image` as a new `wan27` family in the DashScope provider with their own size resolution rules: pixel range `[768*768, 4096*4096]` for `wan2.7-image-pro` text-to-image, `[768*768, 2048*2048]` for `wan2.7-image-pro` with refs and for the base `wan2.7-image` model in any mode, with aspect ratios validated against the documented `[1:8, 8:1]` band. - Allow up to 9 reference images per request (image editing / multi-image fusion). Local files are inlined as base64 data URLs; `http(s)://` paths are forwarded as-is. Other DashScope models still reject `--ref` with a hint to switch to a wan2.7 model or another provider. - Drop `prompt_extend` from the request body for the Wan 2.7 family (not part of the Wan 2.7 API surface) and skip the Qwen-only negative prompt for this family. - Allow `--provider dashscope --ref ...` in `detectProvider` so users can opt into Wan 2.7 reference workflows, while keeping Wan 2.7 out of the auto-detect ref priority list. - Add provider, reference, and usage-example documentation, plus unit tests covering family routing, size derivation across the three pixel-budget modes, ratio rejection, explicit-size validation, and the new `--provider dashscope` ref opt-in path. Made-with: Cursor * fix(baoyu-imagine): force n=1 for DashScope wan2.7 to avoid silent multi-image billing Cross-checked the implementation against the official Wan 2.7 image generation & editing API reference and found that the API defaults `parameters.n` to 4 in non-collage mode (1-4 range, billed per image). baoyu-imagine has single-image save semantics — only the first image in the response is kept — so without an explicit `n: 1` users would silently pay for 3 discarded images per request. - Always send `parameters.n: 1` in the wan2.7 request body - Reject `--n > 1` for wan2.7 with a clear error pointing at the single-image save semantics - Add tests asserting the request body shape (n=1, no prompt_extend, no negative_prompt) and the --n>1 rejection - Document the defaults-vs-skill mismatch in the dashscope reference Made-with: Cursor * Fix DashScope Wan 2.7 review feedback
This commit is contained in:
@@ -2,15 +2,42 @@ import assert from "node:assert/strict";
|
||||
import test, { type TestContext } from "node:test";
|
||||
|
||||
import {
|
||||
generateImage,
|
||||
getDefaultModel,
|
||||
getModelFamily,
|
||||
getQwen2SizeFromAspectRatio,
|
||||
getSizeFromAspectRatio,
|
||||
getWan27SizeFromAspectRatio,
|
||||
normalizeSize,
|
||||
parseAspectRatio,
|
||||
parseSize,
|
||||
resolveSizeForModel,
|
||||
} from "./dashscope.ts";
|
||||
import type { CliArgs } from "../types.ts";
|
||||
|
||||
function makeCliArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
prompt: null,
|
||||
promptFiles: [],
|
||||
imagePath: null,
|
||||
provider: "dashscope",
|
||||
model: null,
|
||||
aspectRatio: null,
|
||||
aspectRatioSource: null,
|
||||
size: null,
|
||||
quality: "2k",
|
||||
imageSize: null,
|
||||
imageSizeSource: null,
|
||||
imageApiDialect: null,
|
||||
referenceImages: [],
|
||||
n: 1,
|
||||
batchFile: null,
|
||||
jobs: null,
|
||||
json: false,
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function useEnv(
|
||||
t: TestContext,
|
||||
@@ -51,9 +78,11 @@ test("DashScope aspect-ratio parsing accepts numeric ratios only", () => {
|
||||
assert.equal(parseAspectRatio("-1:2"), null);
|
||||
});
|
||||
|
||||
test("DashScope model family routing distinguishes qwen-2.0, fixed-size qwen, and legacy models", () => {
|
||||
test("DashScope model family routing distinguishes qwen-2.0, fixed-size qwen, wan2.7, and legacy models", () => {
|
||||
assert.equal(getModelFamily("qwen-image-2.0-pro"), "qwen2");
|
||||
assert.equal(getModelFamily("qwen-image"), "qwenFixed");
|
||||
assert.equal(getModelFamily("wan2.7-image"), "wan27");
|
||||
assert.equal(getModelFamily("wan2.7-image-pro"), "wan27");
|
||||
assert.equal(getModelFamily("z-image-turbo"), "legacy");
|
||||
assert.equal(getModelFamily("wanx-v1"), "legacy");
|
||||
});
|
||||
@@ -146,3 +175,218 @@ test("DashScope size normalization converts WxH into provider format", () => {
|
||||
assert.equal(normalizeSize("1024x1024"), "1024*1024");
|
||||
assert.equal(normalizeSize("2048*1152"), "2048*1152");
|
||||
});
|
||||
|
||||
test("Wan 2.7 derives sizes that match the requested ratio at the chosen pixel budget", () => {
|
||||
const square2k = getWan27SizeFromAspectRatio(null, "2k", 2048 * 2048);
|
||||
const parsedSquare = parseSize(square2k);
|
||||
assert.ok(parsedSquare);
|
||||
assert.equal(parsedSquare.width, parsedSquare.height);
|
||||
assert.ok(parsedSquare.width * parsedSquare.height <= 2048 * 2048);
|
||||
|
||||
const widescreen = getWan27SizeFromAspectRatio("16:9", "2k", 2048 * 2048);
|
||||
const parsedWide = parseSize(widescreen);
|
||||
assert.ok(parsedWide);
|
||||
assert.ok(Math.abs(parsedWide.width / parsedWide.height - 16 / 9) < 0.05);
|
||||
assert.ok(parsedWide.width * parsedWide.height <= 2048 * 2048);
|
||||
|
||||
const pro4k = getWan27SizeFromAspectRatio("16:9", "2k", 4096 * 4096);
|
||||
const parsed4k = parseSize(pro4k);
|
||||
assert.ok(parsed4k);
|
||||
assert.ok(parsed4k.width * parsed4k.height > 2048 * 2048);
|
||||
assert.ok(parsed4k.width * parsed4k.height <= 4096 * 4096);
|
||||
});
|
||||
|
||||
test("Wan 2.7 rejects aspect ratios outside the [1:8, 8:1] range", () => {
|
||||
assert.throws(
|
||||
() => getWan27SizeFromAspectRatio("9:1", "2k", 2048 * 2048),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
assert.throws(
|
||||
() => getWan27SizeFromAspectRatio("1:9", "normal", 2048 * 2048),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Wan 2.7 derived sizes stay inside the boundary ratio limits after rounding", () => {
|
||||
for (const ar of ["8:1", "1:8"]) {
|
||||
const size = getWan27SizeFromAspectRatio(ar, "2k", 2048 * 2048);
|
||||
const parsed = parseSize(size);
|
||||
assert.ok(parsed);
|
||||
const ratio = parsed.width / parsed.height;
|
||||
assert.ok(ratio >= 1 / 8);
|
||||
assert.ok(ratio <= 8);
|
||||
assert.ok(parsed.width * parsed.height <= 2048 * 2048);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveSizeForModel routes wan2.7-image to the 2K-capped derivation", () => {
|
||||
const size = resolveSizeForModel("wan2.7-image", {
|
||||
size: null,
|
||||
aspectRatio: "16:9",
|
||||
quality: "2k",
|
||||
});
|
||||
const parsed = parseSize(size);
|
||||
assert.ok(parsed);
|
||||
assert.ok(parsed.width * parsed.height <= 2048 * 2048);
|
||||
assert.ok(Math.abs(parsed.width / parsed.height - 16 / 9) < 0.05);
|
||||
});
|
||||
|
||||
test("resolveSizeForModel allows wan2.7-image-pro 4K only when there are no reference images", () => {
|
||||
assert.equal(
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "4096*4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
"4096*4096",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "4096*4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
const proWithRef = resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: null,
|
||||
aspectRatio: "1:1",
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
});
|
||||
const parsedRef = parseSize(proWithRef);
|
||||
assert.ok(parsedRef);
|
||||
assert.ok(parsedRef.width * parsedRef.height <= 2048 * 2048);
|
||||
});
|
||||
|
||||
test("Wan 2.7 request body forces n=1 and omits prompt_extend / negative_prompt", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedBody: any = null;
|
||||
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
||||
capturedBody = JSON.parse(String(init?.body));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: [{ image: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
await generateImage("hello", "wan2.7-image-pro", makeCliArgs({ aspectRatio: "1:1" }));
|
||||
|
||||
assert.equal(capturedBody.model, "wan2.7-image-pro");
|
||||
assert.deepEqual(Object.keys(capturedBody.parameters).sort(), ["n", "size", "watermark"]);
|
||||
assert.equal(capturedBody.parameters.n, 1);
|
||||
assert.equal(capturedBody.parameters.watermark, false);
|
||||
assert.equal(typeof capturedBody.parameters.size, "string");
|
||||
assert.ok(!("prompt_extend" in capturedBody.parameters));
|
||||
assert.ok(!("negative_prompt" in capturedBody.parameters));
|
||||
|
||||
assert.deepEqual(capturedBody.input.messages[0].content, [{ text: "hello" }]);
|
||||
});
|
||||
|
||||
test("Wan 2.7 request body forwards remote reference image URLs", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedBody: any = null;
|
||||
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
||||
capturedBody = JSON.parse(String(init?.body));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: [{ image: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
await generateImage(
|
||||
"combine these",
|
||||
"wan2.7-image-pro",
|
||||
makeCliArgs({ referenceImages: ["https://example.com/ref.png"] }),
|
||||
);
|
||||
|
||||
assert.deepEqual(capturedBody.input.messages[0].content, [
|
||||
{ image: "https://example.com/ref.png" },
|
||||
{ text: "combine these" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Wan 2.7 rejects --n > 1 to prevent silent multi-image billing", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
await assert.rejects(
|
||||
() => generateImage("hi", "wan2.7-image-pro", makeCliArgs({ n: 2 })),
|
||||
/support exactly one output image/,
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSizeForModel validates explicit wan2.7 sizes by pixel budget and ratio", () => {
|
||||
assert.equal(
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3840x2160",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
"3840*2160",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3840x2160",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image", {
|
||||
size: "4096x4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3072*256",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import path from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type { CliArgs, Quality } from "../types";
|
||||
|
||||
type DashScopeModelFamily = "qwen2" | "qwenFixed" | "legacy";
|
||||
type DashScopeModelFamily = "qwen2" | "qwenFixed" | "wan27" | "legacy";
|
||||
|
||||
type DashScopeModelSpec = {
|
||||
family: DashScopeModelFamily;
|
||||
@@ -19,6 +21,16 @@ const QWEN_2_TARGET_PIXELS: Record<Quality, number> = {
|
||||
"2k": 1536 * 1536,
|
||||
};
|
||||
|
||||
const MIN_WAN27_TOTAL_PIXELS = 768 * 768;
|
||||
const MAX_WAN27_PRO_T2I_PIXELS = 4096 * 4096;
|
||||
const MAX_WAN27_GENERAL_PIXELS = 2048 * 2048;
|
||||
const WAN27_MAX_REFERENCE_IMAGES = 9;
|
||||
|
||||
const WAN27_TARGET_PIXELS: Record<Quality, number> = {
|
||||
normal: 1024 * 1024,
|
||||
"2k": 2048 * 2048,
|
||||
};
|
||||
|
||||
const QWEN_2_RECOMMENDED: Record<string, Record<Quality, string>> = {
|
||||
"1:1": { normal: "1024*1024", "2k": "1536*1536" },
|
||||
"2:3": { normal: "768*1152", "2k": "1024*1536" },
|
||||
@@ -73,6 +85,11 @@ const QWEN_FIXED_SPEC: DashScopeModelSpec = {
|
||||
defaultSize: QWEN_FIXED_SIZES_BY_RATIO["16:9"],
|
||||
};
|
||||
|
||||
const WAN27_SPEC: DashScopeModelSpec = {
|
||||
family: "wan27",
|
||||
defaultSize: "2048*2048",
|
||||
};
|
||||
|
||||
const LEGACY_SPEC: DashScopeModelSpec = {
|
||||
family: "legacy",
|
||||
defaultSize: "1536*1536",
|
||||
@@ -88,12 +105,31 @@ const MODEL_SPEC_ALIASES: Record<string, DashScopeModelSpec> = {
|
||||
"qwen-image-plus": QWEN_FIXED_SPEC,
|
||||
"qwen-image-plus-2026-01-09": QWEN_FIXED_SPEC,
|
||||
"qwen-image": QWEN_FIXED_SPEC,
|
||||
"wan2.7-image-pro": WAN27_SPEC,
|
||||
"wan2.7-image": WAN27_SPEC,
|
||||
};
|
||||
|
||||
export function getDefaultModel(): string {
|
||||
return process.env.DASHSCOPE_IMAGE_MODEL || DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
function getReferenceImageMime(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
||||
if (ext === ".webp") return "image/webp";
|
||||
if (ext === ".bmp") return "image/bmp";
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
async function loadReferenceImage(refPath: string): Promise<string> {
|
||||
if (/^https?:\/\//i.test(refPath)) {
|
||||
return refPath;
|
||||
}
|
||||
const fullPath = path.resolve(refPath);
|
||||
const bytes = await readFile(fullPath);
|
||||
return `data:${getReferenceImageMime(fullPath)};base64,${bytes.toString("base64")}`;
|
||||
}
|
||||
|
||||
function getApiKey(): string | null {
|
||||
return process.env.DASHSCOPE_API_KEY || null;
|
||||
}
|
||||
@@ -173,6 +209,10 @@ function roundToStep(value: number): number {
|
||||
return Math.max(SIZE_STEP, Math.round(value / SIZE_STEP) * SIZE_STEP);
|
||||
}
|
||||
|
||||
function floorToStep(value: number): number {
|
||||
return Math.max(SIZE_STEP, Math.floor(value / SIZE_STEP) * SIZE_STEP);
|
||||
}
|
||||
|
||||
function fitToPixelBudget(
|
||||
width: number,
|
||||
height: number,
|
||||
@@ -220,6 +260,21 @@ function fitToPixelBudget(
|
||||
return { width: roundedWidth, height: roundedHeight };
|
||||
}
|
||||
|
||||
function clampWan27DerivedSizeToRatioBounds(
|
||||
size: { width: number; height: number },
|
||||
): { width: number; height: number } {
|
||||
let { width, height } = size;
|
||||
const ratio = width / height;
|
||||
|
||||
if (ratio > 8) {
|
||||
width = floorToStep(height * 8);
|
||||
} else if (ratio < 1 / 8) {
|
||||
height = floorToStep(width * 8);
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
export function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
|
||||
const normalizedQuality = normalizeQuality(quality);
|
||||
const sizes = normalizedQuality === "2k" ? LEGACY_STANDARD_SIZES_2K : LEGACY_STANDARD_SIZES;
|
||||
@@ -276,6 +331,77 @@ export function getQwen2SizeFromAspectRatio(ar: string | null, quality: CliArgs[
|
||||
return formatSize(fitted.width, fitted.height);
|
||||
}
|
||||
|
||||
function isWan27ProModel(model: string): boolean {
|
||||
return model.trim().toLowerCase() === "wan2.7-image-pro";
|
||||
}
|
||||
|
||||
function getWan27MaxPixels(model: string, hasReferenceImages: boolean): number {
|
||||
if (isWan27ProModel(model) && !hasReferenceImages) {
|
||||
return MAX_WAN27_PRO_T2I_PIXELS;
|
||||
}
|
||||
return MAX_WAN27_GENERAL_PIXELS;
|
||||
}
|
||||
|
||||
export function getWan27SizeFromAspectRatio(
|
||||
ar: string | null,
|
||||
quality: CliArgs["quality"],
|
||||
maxPixels: number,
|
||||
): string {
|
||||
const normalizedQuality = normalizeQuality(quality);
|
||||
const targetPixels = Math.min(WAN27_TARGET_PIXELS[normalizedQuality], maxPixels);
|
||||
|
||||
if (!ar) {
|
||||
const side = roundToStep(Math.sqrt(targetPixels));
|
||||
return formatSize(side, side);
|
||||
}
|
||||
|
||||
const parsed = parseAspectRatio(ar);
|
||||
if (!parsed) {
|
||||
const side = roundToStep(Math.sqrt(targetPixels));
|
||||
return formatSize(side, side);
|
||||
}
|
||||
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (ratio < 1 / 8 || ratio > 8) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. Received "${ar}".`
|
||||
);
|
||||
}
|
||||
|
||||
const rawWidth = Math.sqrt(targetPixels * ratio);
|
||||
const rawHeight = Math.sqrt(targetPixels / ratio);
|
||||
const fitted = fitToPixelBudget(
|
||||
rawWidth,
|
||||
rawHeight,
|
||||
MIN_WAN27_TOTAL_PIXELS,
|
||||
maxPixels,
|
||||
);
|
||||
const bounded = clampWan27DerivedSizeToRatioBounds(fitted);
|
||||
|
||||
return formatSize(bounded.width, bounded.height);
|
||||
}
|
||||
|
||||
function validateWan27Size(size: string, maxPixels: number, model: string): string {
|
||||
const normalized = normalizeSize(size);
|
||||
const parsed = validateSizeFormat(normalized);
|
||||
const totalPixels = parsed.width * parsed.height;
|
||||
if (totalPixels < MIN_WAN27_TOTAL_PIXELS || totalPixels > maxPixels) {
|
||||
const limit = maxPixels === MAX_WAN27_PRO_T2I_PIXELS ? "4096*4096" : "2048*2048";
|
||||
throw new Error(
|
||||
`DashScope ${model} requires total pixels between 768*768 and ${limit} ` +
|
||||
`for the current request. Received ${normalized} (${totalPixels} pixels).`
|
||||
);
|
||||
}
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (ratio < 1 / 8 || ratio > 8) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. ` +
|
||||
`Received ${normalized} (ratio ${ratio.toFixed(3)}).`
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getQwenFixedSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
|
||||
if (quality === "normal") {
|
||||
console.warn(
|
||||
@@ -331,9 +457,16 @@ function validateQwenFixedSize(size: string): string {
|
||||
|
||||
export function resolveSizeForModel(
|
||||
model: string,
|
||||
args: Pick<CliArgs, "size" | "aspectRatio" | "quality">,
|
||||
args: Pick<CliArgs, "size" | "aspectRatio" | "quality"> & { referenceImages?: string[] },
|
||||
): string {
|
||||
const spec = getModelSpec(model);
|
||||
const referenceCount = args.referenceImages?.length ?? 0;
|
||||
|
||||
if (spec.family === "wan27") {
|
||||
const maxPixels = getWan27MaxPixels(model, referenceCount > 0);
|
||||
if (args.size) return validateWan27Size(args.size, maxPixels, model);
|
||||
return getWan27SizeFromAspectRatio(args.aspectRatio, args.quality, maxPixels);
|
||||
}
|
||||
|
||||
if (args.size) {
|
||||
if (spec.family === "qwen2") return validateQwen2Size(args.size);
|
||||
@@ -357,6 +490,14 @@ function buildParameters(
|
||||
family: DashScopeModelFamily,
|
||||
size: string,
|
||||
): Record<string, unknown> {
|
||||
if (family === "wan27") {
|
||||
return {
|
||||
size,
|
||||
n: 1,
|
||||
watermark: false,
|
||||
};
|
||||
}
|
||||
|
||||
const parameters: Record<string, unknown> = {
|
||||
prompt_extend: false,
|
||||
size,
|
||||
@@ -419,23 +560,44 @@ export async function generateImage(
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
|
||||
|
||||
if (args.referenceImages.length > 0) {
|
||||
const spec = getModelSpec(model);
|
||||
|
||||
if (args.referenceImages.length > 0 && spec.family !== "wan27") {
|
||||
throw new Error(
|
||||
"Reference images are not supported with DashScope provider in baoyu-imagine. Use --provider google with a Gemini multimodal model."
|
||||
"Reference images are not supported with this DashScope model. Use a wan2.7 image model (--model wan2.7-image-pro or wan2.7-image), or switch to --provider google with a Gemini multimodal model."
|
||||
);
|
||||
}
|
||||
|
||||
if (args.referenceImages.length > WAN27_MAX_REFERENCE_IMAGES) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models accept at most ${WAN27_MAX_REFERENCE_IMAGES} reference images. Received ${args.referenceImages.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (spec.family === "wan27" && args.n !== 1) {
|
||||
throw new Error(
|
||||
"DashScope wan2.7 image models in baoyu-imagine support exactly one output image per request (extra images would be billed but discarded). Remove --n or use --n 1."
|
||||
);
|
||||
}
|
||||
|
||||
const spec = getModelSpec(model);
|
||||
const size = resolveSizeForModel(model, args);
|
||||
const url = `${getBaseUrl()}/api/v1/services/aigc/multimodal-generation/generation`;
|
||||
|
||||
const content: Array<Record<string, unknown>> = [];
|
||||
if (spec.family === "wan27" && args.referenceImages.length > 0) {
|
||||
for (const refPath of args.referenceImages) {
|
||||
content.push({ image: await loadReferenceImage(refPath) });
|
||||
}
|
||||
}
|
||||
content.push({ text: prompt });
|
||||
|
||||
const body = {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ text: prompt }],
|
||||
content,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user