mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-30 13: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:
@@ -85,7 +85,7 @@ Options:
|
||||
--quality normal|2k Quality preset (default: 2k)
|
||||
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
|
||||
--imageApiDialect <id> OpenAI-compatible image dialect: openai-native|ratio-metadata
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, or Seedream 4.0/4.5/5.0)
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, Seedream 4.0/4.5/5.0, or DashScope wan2.7-image*)
|
||||
--n <count> Number of images for the current task (default: 1; Replicate currently requires 1)
|
||||
--json JSON output
|
||||
-h, --help Show help
|
||||
@@ -698,10 +698,11 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
args.provider !== "openrouter" &&
|
||||
args.provider !== "replicate" &&
|
||||
args.provider !== "seedream" &&
|
||||
args.provider !== "minimax"
|
||||
args.provider !== "minimax" &&
|
||||
args.provider !== "dashscope"
|
||||
) {
|
||||
throw new Error(
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider dashscope with a wan2.7 image model, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -775,8 +776,24 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateReferenceImages(referenceImages: string[]): Promise<void> {
|
||||
export type ReferenceImageValidationOptions = {
|
||||
allowRemoteUrls?: boolean;
|
||||
};
|
||||
|
||||
function isRemoteReferenceImage(refPath: string): boolean {
|
||||
return /^https?:\/\//i.test(refPath);
|
||||
}
|
||||
|
||||
function shouldAllowRemoteReferenceImages(provider: Provider | null): boolean {
|
||||
return provider === "dashscope";
|
||||
}
|
||||
|
||||
export async function validateReferenceImages(
|
||||
referenceImages: string[],
|
||||
options: ReferenceImageValidationOptions = {},
|
||||
): Promise<void> {
|
||||
for (const refPath of referenceImages) {
|
||||
if (options.allowRemoteUrls && isRemoteReferenceImage(refPath)) continue;
|
||||
const fullPath = path.resolve(refPath);
|
||||
try {
|
||||
await access(fullPath);
|
||||
@@ -803,6 +820,11 @@ export function isRetryableGenerationError(error: unknown): boolean {
|
||||
"API error (404)",
|
||||
"temporarily disabled",
|
||||
"supports saving exactly one image",
|
||||
"supports only",
|
||||
"support exactly one output image",
|
||||
"support aspect ratios in",
|
||||
"requires total pixels between",
|
||||
"accept at most",
|
||||
];
|
||||
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
|
||||
}
|
||||
@@ -858,7 +880,11 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConf
|
||||
const prompt = (await loadPromptForArgs(args)) ?? (await readPromptFromStdin());
|
||||
if (!prompt) throw new Error("Prompt is required");
|
||||
if (!args.imagePath) throw new Error("--image is required");
|
||||
if (args.referenceImages.length > 0) await validateReferenceImages(args.referenceImages);
|
||||
if (args.referenceImages.length > 0) {
|
||||
await validateReferenceImages(args.referenceImages, {
|
||||
allowRemoteUrls: shouldAllowRemoteReferenceImages(args.provider),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = detectProvider(args);
|
||||
const providerModule = await loadProviderModule(provider);
|
||||
@@ -907,6 +933,10 @@ export function resolveBatchPath(batchDir: string, filePath: string): string {
|
||||
return path.isAbsolute(filePath) ? filePath : path.resolve(batchDir, filePath);
|
||||
}
|
||||
|
||||
function resolveBatchReferencePath(batchDir: string, filePath: string): string {
|
||||
return isRemoteReferenceImage(filePath) ? filePath : resolveBatchPath(batchDir, filePath);
|
||||
}
|
||||
|
||||
export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir: string): CliArgs {
|
||||
return {
|
||||
...baseArgs,
|
||||
@@ -922,7 +952,7 @@ export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir
|
||||
imageSize: task.imageSize ?? baseArgs.imageSize ?? null,
|
||||
imageSizeSource: task.imageSize != null ? "task" : (baseArgs.imageSizeSource ?? null),
|
||||
imageApiDialect: task.imageApiDialect ?? baseArgs.imageApiDialect ?? null,
|
||||
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
|
||||
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchReferencePath(batchDir, filePath)) : [],
|
||||
n: task.n ?? baseArgs.n,
|
||||
batchFile: null,
|
||||
jobs: baseArgs.jobs,
|
||||
@@ -946,7 +976,11 @@ async function prepareBatchTasks(
|
||||
const prompt = await loadPromptForArgs(taskArgs);
|
||||
if (!prompt) throw new Error(`Task ${i + 1} is missing prompt or promptFiles.`);
|
||||
if (!taskArgs.imagePath) throw new Error(`Task ${i + 1} is missing image output path.`);
|
||||
if (taskArgs.referenceImages.length > 0) await validateReferenceImages(taskArgs.referenceImages);
|
||||
if (taskArgs.referenceImages.length > 0) {
|
||||
await validateReferenceImages(taskArgs.referenceImages, {
|
||||
allowRemoteUrls: shouldAllowRemoteReferenceImages(taskArgs.provider),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = detectProvider(taskArgs);
|
||||
const providerModule = await loadProviderModule(provider);
|
||||
|
||||
Reference in New Issue
Block a user