From ad7a7a646d5f55f3a7d9d1933973dff935c3ab46 Mon Sep 17 00:00:00 2001 From: Davidlaizz <1830579068@qq.com> Date: Sat, 6 Jun 2026 04:53:00 +0800 Subject: [PATCH 1/5] feat(baoyu-image-gen): add Agnes AI image generation provider --- .../references/providers/agnes.md | 55 +++++ skills/baoyu-image-gen/scripts/main.ts | 44 +++- .../scripts/providers/agnes.test.ts | 213 ++++++++++++++++++ .../scripts/providers/agnes.ts | 189 ++++++++++++++++ skills/baoyu-image-gen/scripts/types.ts | 7 +- 5 files changed, 499 insertions(+), 9 deletions(-) create mode 100644 skills/baoyu-image-gen/references/providers/agnes.md create mode 100644 skills/baoyu-image-gen/scripts/providers/agnes.test.ts create mode 100644 skills/baoyu-image-gen/scripts/providers/agnes.ts diff --git a/skills/baoyu-image-gen/references/providers/agnes.md b/skills/baoyu-image-gen/references/providers/agnes.md new file mode 100644 index 0000000..cf12942 --- /dev/null +++ b/skills/baoyu-image-gen/references/providers/agnes.md @@ -0,0 +1,55 @@ +# Sapiens AI Agnes Image + +Read when the user picks `--provider agnes` or sets `default_model.agnes`. Default model is `agnes-image-2.1-flash`. + +## Models + +**`agnes-image-2.1-flash`** (only model) + +- Text-to-image and image-to-image (with `--ref`) in a single `/images/generations` endpoint +- Supports reference images as public URLs or Data URI (base64) +- Optimized for high information density, complex layouts, and rich details +- Size rules: both dimensions divisible by 32 (720px exception), long edge ≤ 2048, total pixels ≤ ~4M +- Default size: `1024x1024`; custom `--size` supports arbitrary WxH within the above rules +- `--ar` supported: computed as 2048-based size (long edge ≤ 2048, short edge proportional, both snapped to 32px); `1:1` special-cased to `1024x1024` + +## Response Format + +- The sync API always returns a URL +- Default (`--response-format file`): downloads the image and saves as `.png` +- Pass `--response-format url`: writes the URL string to `.txt` instead + +## `--n` Behavior + +The Agnes API returns a single image per request regardless of the `n` parameter. Passing `--n > 1` triggers a local error from `validateArgs` before any API call is made. + +## Behavior Notes + +- API key required: `AGNES_API_KEY` +- Base URL: `https://apihub.agnes-ai.com/v1` (override with `AGNES_BASE_URL`) +- Model override: `AGNES_IMAGE_MODEL` env +- `response_format` is always embedded in `extra_body` (not at request top level) +- Reference images: local files converted to Data URI base64 inline; remote URLs passed through +- Rate limit defaults: concurrency=3, startIntervalMs=1100 (override via `BAOYU_IMAGE_GEN_AGNES_CONCURRENCY` / `BAOYU_IMAGE_GEN_AGNES_START_INTERVAL_MS`) +- Timeout: 120s per request + +## Size Resolution + +- `--size ` wins over `--ar` +- `--ar` maps to a concrete size using the algorithm: long edge ≤ 2048, short edge proportional, both dimensions snapped to 32px +- `--ar 1:1` is special-cased to `1024x1024` + +### Common `--ar` Results + +| Aspect Ratio | Result | +|--------------|--------| +| `1:1` | `1024x1024` | +| `16:9` | `2048x1152` | +| `4:3` | `2048x1536` | +| `3:2` | `2048x1376` | +| `21:9` | `2048x896` | +| Unlisted ratio | Computed on the fly (portrait mirror swaps width/height) | + +## Official References + +- [Agnes AIGC API Hub](https://apihub.agnes-ai.com) diff --git a/skills/baoyu-image-gen/scripts/main.ts b/skills/baoyu-image-gen/scripts/main.ts index d17e913..ad646f4 100644 --- a/skills/baoyu-image-gen/scripts/main.ts +++ b/skills/baoyu-image-gen/scripts/main.ts @@ -65,6 +65,7 @@ const DEFAULT_PROVIDER_RATE_LIMITS: Record = { seedream: { concurrency: 3, startIntervalMs: 1100 }, azure: { concurrency: 3, startIntervalMs: 1100 }, "codex-cli": { concurrency: 1, startIntervalMs: 2000 }, + agnes: { concurrency: 3, startIntervalMs: 1100 }, }; function printUsage(): void { @@ -79,13 +80,14 @@ Options: --image Output image path (required in single-image mode) --batchfile JSON batch file for multi-image generation --jobs Worker count for batch mode (default: auto, max from config, built-in default 10) - --provider google|openai|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|azure|codex-cli Force provider (auto-detect by default) + --provider google|openai|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|azure|codex-cli|agnes Force provider (auto-detect by default) -m, --model Model ID --ar Aspect ratio (e.g., 16:9, 1:1, 4:3) --size Size (e.g., 1024x1024) --quality normal|2k Quality preset (default: 2k) --imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality) --imageApiDialect OpenAI-compatible image dialect: openai-native|ratio-metadata + --response-format file|url Output mode: file (download image, default) or url (return URL text) --ref Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, Seedream 4.0/4.5/5.0, or DashScope wan2.7-image*) --n Number of images for the current task (default: 1; Replicate currently requires 1) --json JSON output @@ -180,6 +182,7 @@ export function parseArgs(argv: string[]): CliArgs { imageSize: null, imageSizeSource: null, imageApiDialect: null, + responseFormat: null, referenceImages: [], n: 1, batchFile: null, @@ -265,7 +268,8 @@ export function parseArgs(argv: string[]): CliArgs { v !== "jimeng" && v !== "seedream" && v !== "azure" && - v !== "codex-cli" + v !== "codex-cli" && + v !== "agnes" ) { throw new Error(`Invalid provider: ${v}`); } @@ -319,6 +323,13 @@ export function parseArgs(argv: string[]): CliArgs { continue; } + if (a === "--response-format") { + const v = argv[++i]; + if (v !== "file" && v !== "url") throw new Error(`Invalid response-format: ${v}`); + out.responseFormat = v; + continue; + } + if (a === "--ref" || a === "--reference") { const { items, next } = takeMany(i); if (items.length === 0) throw new Error(`Missing files for ${a}`); @@ -438,6 +449,7 @@ export function parseSimpleYaml(yaml: string): Partial { seedream: null, azure: null, "codex-cli": null, + agnes: null, }; currentKey = "default_model"; currentProvider = null; @@ -641,9 +653,10 @@ export function getConfiguredProviderRateLimits( seedream: { ...DEFAULT_PROVIDER_RATE_LIMITS.seedream }, azure: { ...DEFAULT_PROVIDER_RATE_LIMITS.azure }, "codex-cli": { ...DEFAULT_PROVIDER_RATE_LIMITS["codex-cli"] }, + agnes: { ...DEFAULT_PROVIDER_RATE_LIMITS.agnes }, }; - for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure", "codex-cli"] as Provider[]) { + for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure", "codex-cli", "agnes"] as Provider[]) { const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase().replace(/-/g, "_")}`; const extendLimit = extendConfig.batch?.provider_limits?.[provider]; configured[provider] = { @@ -696,6 +709,7 @@ function inferProviderFromModel(model: string | null): Provider | null { if (normalized.includes("seedream") || normalized.includes("seededit")) return "seedream"; if (normalized === "image-01" || normalized === "image-01-live") return "minimax"; if (normalized === "glm-image" || normalized === "cogview-4-250304") return "zai"; + if (normalized.includes("agnes-image")) return "agnes"; return null; } @@ -711,10 +725,11 @@ export function detectProvider(args: CliArgs): Provider { args.provider !== "seedream" && args.provider !== "minimax" && args.provider !== "dashscope" && - args.provider !== "codex-cli" + args.provider !== "codex-cli" && + args.provider !== "agnes" ) { 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 dashscope with a wan2.7 image model, --provider seedream for supported Seedream models, --provider minimax for MiniMax subject-reference workflows, or --provider codex-cli (Codex image_gen with references)." + "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, --provider minimax for MiniMax subject-reference workflows, --provider codex-cli (Codex image_gen with references), or --provider agnes (Agnes Image)." ); } @@ -730,6 +745,7 @@ export function detectProvider(args: CliArgs): Provider { const hasReplicate = !!process.env.REPLICATE_API_TOKEN; const hasJimeng = !!(process.env.JIMENG_ACCESS_KEY_ID && process.env.JIMENG_SECRET_ACCESS_KEY); const hasSeedream = !!process.env.ARK_API_KEY; + const hasAgnes = !!process.env.AGNES_API_KEY; const modelProvider = inferProviderFromModel(args.model); if (modelProvider === "seedream") { @@ -753,6 +769,13 @@ export function detectProvider(args: CliArgs): Provider { return "zai"; } + if (modelProvider === "agnes") { + if (!hasAgnes) { + throw new Error("Model looks like an Agnes image model, but AGNES_API_KEY is not set."); + } + return "agnes"; + } + if (args.referenceImages.length > 0) { if (hasGoogle) return "google"; if (hasOpenai) return "openai"; @@ -761,8 +784,9 @@ export function detectProvider(args: CliArgs): Provider { if (hasReplicate) return "replicate"; if (hasSeedream) return "seedream"; if (hasMinimax) return "minimax"; + if (hasAgnes) return "agnes"; throw new Error( - "Reference images require Google, OpenAI, Azure, OpenRouter, Replicate, supported Seedream models, or MiniMax. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, REPLICATE_API_TOKEN, ARK_API_KEY, or MINIMAX_API_KEY, or remove --ref." + "Reference images require Google, OpenAI, Azure, OpenRouter, Replicate, supported Seedream models, MiniMax, or Agnes. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, REPLICATE_API_TOKEN, ARK_API_KEY, MINIMAX_API_KEY, or AGNES_API_KEY, or remove --ref." ); } @@ -777,13 +801,14 @@ export function detectProvider(args: CliArgs): Provider { hasReplicate && "replicate", hasJimeng && "jimeng", hasSeedream && "seedream", + hasAgnes && "agnes", ].filter(Boolean) as Provider[]; if (available.length === 1) return available[0]!; if (available.length > 1) return available[0]!; throw new Error( - "No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, ZAI_API_KEY, MINIMAX_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, or ARK_API_KEY.\n" + + "No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, ZAI_API_KEY, MINIMAX_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, ARK_API_KEY, or AGNES_API_KEY.\n" + "Create ~/.baoyu-skills/.env or /.baoyu-skills/.env with your keys." ); } @@ -852,6 +877,7 @@ async function loadProviderModule(provider: Provider): Promise { if (provider === "seedream") return (await import("./providers/seedream")) as ProviderModule; if (provider === "azure") return (await import("./providers/azure")) as ProviderModule; if (provider === "codex-cli") return (await import("./providers/codex-cli")) as ProviderModule; + if (provider === "agnes") return (await import("./providers/agnes")) as ProviderModule; return (await import("./providers/openai")) as ProviderModule; } @@ -884,6 +910,7 @@ function getModelForProvider( if (provider === "seedream" && extendConfig.default_model.seedream) return extendConfig.default_model.seedream; if (provider === "azure" && extendConfig.default_model.azure) return extendConfig.default_model.azure; if (provider === "codex-cli" && extendConfig.default_model["codex-cli"]) return extendConfig.default_model["codex-cli"]; + if (provider === "agnes" && extendConfig.default_model.agnes) return extendConfig.default_model.agnes; } return providerModule.getDefaultModel(); } @@ -966,6 +993,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, + responseFormat: task.responseFormat ?? baseArgs.responseFormat ?? null, referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchReferencePath(batchDir, filePath)) : [], n: task.n ?? baseArgs.n, batchFile: null, @@ -1118,7 +1146,7 @@ async function runBatchTasks( const acquireProvider = createProviderGate(providerRateLimits); const workerCount = getWorkerCount(tasks.length, jobs, maxWorkers); console.error(`Batch mode: ${tasks.length} tasks, ${workerCount} workers, parallel mode enabled.`); - for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure", "codex-cli"] as Provider[]) { + for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure", "codex-cli", "agnes"] as Provider[]) { const limit = providerRateLimits[provider]; console.error(`- ${provider}: concurrency=${limit.concurrency}, startIntervalMs=${limit.startIntervalMs}`); } diff --git a/skills/baoyu-image-gen/scripts/providers/agnes.test.ts b/skills/baoyu-image-gen/scripts/providers/agnes.test.ts new file mode 100644 index 0000000..287f7cd --- /dev/null +++ b/skills/baoyu-image-gen/scripts/providers/agnes.test.ts @@ -0,0 +1,213 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test, { type TestContext } from "node:test"; + +import type { CliArgs } from "../types.ts"; +import { + buildRequestBody, + extractImageFromResponse, + parseAspectRatio, + resolveReferenceImages, + resolveSize, + snapDim, + validateArgs, +} from "./agnes.ts"; + +function useEnv( + t: TestContext, + values: Record, +): void { + const previous = new Map(); + for (const [key, value] of Object.entries(values)) { + previous.set(key, process.env[key]); + if (value == null) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + + t.after(() => { + for (const [key, value] of previous.entries()) { + if (value == null) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); +} + +function makeArgs(overrides: Partial = {}): CliArgs { + return { + prompt: null, + promptFiles: [], + imagePath: null, + provider: null, + model: null, + aspectRatio: null, + size: null, + quality: null, + imageSize: null, + imageApiDialect: null, + referenceImages: [], + n: 1, + batchFile: null, + jobs: null, + json: false, + help: false, + responseFormat: null, + ...overrides, + }; +} + +test("snapDim rounds to the nearest multiple of 32", () => { + assert.equal(snapDim(767), 768); + assert.equal(snapDim(1023), 1024); + assert.equal(snapDim(1024), 1024); + assert.equal(snapDim(32), 32); + assert.equal(snapDim(0), 32); + assert.equal(snapDim(16), 32); + assert.equal(snapDim(48), 64); +}); + +test("parseAspectRatio parses valid ratios and rejects invalid inputs", () => { + assert.deepEqual(parseAspectRatio("3:4"), { width: 3, height: 4 }); + assert.deepEqual(parseAspectRatio("16:9"), { width: 16, height: 9 }); + assert.deepEqual(parseAspectRatio("1:1"), { width: 1, height: 1 }); + assert.deepEqual(parseAspectRatio("1.5:1"), { width: 1.5, height: 1 }); + + assert.equal(parseAspectRatio(""), null); + assert.equal(parseAspectRatio("invalid"), null); + assert.equal(parseAspectRatio("3x4"), null); + assert.equal(parseAspectRatio("0:1"), null); + assert.equal(parseAspectRatio("1:0"), null); +}); + +test("resolveSize returns explicit --size directly", () => { + assert.equal(resolveSize({ size: "1024x1024" }), "1024x1024"); + assert.equal(resolveSize({ size: "768x1024", aspectRatio: "16:9" }), "768x1024"); +}); + +test("resolveSize returns default 1024x1024 when no size or ratio given", () => { + assert.equal(resolveSize({}), "1024x1024"); + assert.equal(resolveSize({ size: null, aspectRatio: null }), "1024x1024"); +}); + +test("resolveSize computes 32-aligned size within 2048 max edge", () => { + assert.equal(resolveSize({ aspectRatio: "1:1" }), "1024x1024"); + assert.equal(resolveSize({ aspectRatio: "16:9" }), "2048x1152"); + assert.equal(resolveSize({ aspectRatio: "4:3" }), "2048x1536"); + assert.equal(resolveSize({ aspectRatio: "3:4" }), "1536x2048"); + assert.equal(resolveSize({ aspectRatio: "9:16" }), "1152x2048"); +}); + +test("resolveSize aligns to 32 and respects max edge", () => { + assert.equal(resolveSize({ aspectRatio: "3:1" }), "2048x672"); + assert.equal(resolveSize({ aspectRatio: "1:3" }), "672x2048"); +}); + +test("validateArgs rejects --n > 1", () => { + assert.throws( + () => validateArgs("agnes-image-2.1-flash", makeArgs({ n: 2 })), + /returns a single image per request/, + ); + assert.doesNotThrow(() => + validateArgs("agnes-image-2.1-flash", makeArgs({ n: 1 })), + ); +}); + +test("buildRequestBody maps prompt, model, size, and reference images", () => { + const body = buildRequestBody("a cat", "agnes-image-2.1-flash", { + size: "1024x1024", + aspectRatio: null, + referenceImages: [], + }); + assert.equal(body.model, "agnes-image-2.1-flash"); + assert.equal(body.prompt, "a cat"); + assert.equal(body.size, "1024x1024"); + assert.deepEqual(body.extra_body, { response_format: "url" }); + + const bodyWithRef = buildRequestBody("a cat", "agnes-image-2.1-flash", { + size: null, + aspectRatio: "3:4", + referenceImages: ["https://example.com/ref.jpg"], + }); + assert.equal(bodyWithRef.size, "1536x2048"); + assert.deepEqual(bodyWithRef.image, ["https://example.com/ref.jpg"]); +}); + +test("extractImageFromResponse decodes b64_json payloads", async () => { + const fromBase64 = await extractImageFromResponse({ + data: [{ b64_json: Buffer.from("hello").toString("base64") }], + }); + assert.equal(Buffer.from(fromBase64).toString("utf8"), "hello"); +}); + +test("extractImageFromResponse downloads URL payloads", async (t) => { + const originalFetch = globalThis.fetch; + t.after(() => { + globalThis.fetch = originalFetch; + }); + + globalThis.fetch = async () => + new Response(Uint8Array.from([1, 2, 3]), { + status: 200, + headers: { "Content-Type": "image/png" }, + }); + + const fromUrl = await extractImageFromResponse({ + data: [{ url: "https://example.com/output.png" }], + }); + assert.deepEqual([...fromUrl], [1, 2, 3]); +}); + +test("extractImageFromResponse throws on empty data", async () => { + await assert.rejects( + () => extractImageFromResponse({ data: [] }), + /No image/, + ); + await assert.rejects( + () => extractImageFromResponse({ data: [{}] }), + /No image/, + ); +}); + +test("resolveReferenceImages converts local files to data URIs and passes URLs through", async (t) => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "agnes-ref-")); + t.after(() => fs.rm(dir, { recursive: true, force: true })); + + const localPath = path.join(dir, "ref.png"); + const localBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47]); + await fs.writeFile(localPath, localBytes); + + const jpegPath = path.join(dir, "photo.jpeg"); + await fs.writeFile(jpegPath, Buffer.from([0xff, 0xd8])); + + const results = await resolveReferenceImages([ + localPath, + "https://example.com/remote.jpg", + jpegPath, + ]); + + assert.equal(results.length, 3); + assert.match(results[0]!, /^data:image\/png;base64,/); + assert.match(results[1]!, /^https:\/\/example.com\/remote.jpg$/); + assert.match(results[2]!, /^data:image\/jpeg;base64,/); +}); + +test("resolveReferenceImages detects gif and webp mime types", async (t) => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "agnes-mime-")); + t.after(() => fs.rm(dir, { recursive: true, force: true })); + + const webpPath = path.join(dir, "ref.webp"); + const gifPath = path.join(dir, "ref.gif"); + await fs.writeFile(webpPath, Buffer.from([0x00])); + await fs.writeFile(gifPath, Buffer.from([0x00])); + + const results = await resolveReferenceImages([webpPath, gifPath]); + assert.match(results[0]!, /^data:image\/webp;base64,/); + assert.match(results[1]!, /^data:image\/gif;base64,/); +}); diff --git a/skills/baoyu-image-gen/scripts/providers/agnes.ts b/skills/baoyu-image-gen/scripts/providers/agnes.ts new file mode 100644 index 0000000..0cf6a12 --- /dev/null +++ b/skills/baoyu-image-gen/scripts/providers/agnes.ts @@ -0,0 +1,189 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import type { CliArgs } from "../types"; + +const DEFAULT_MODEL = "agnes-image-2.1-flash"; +const DEFAULT_BASE_URL = "https://apihub.agnes-ai.com/v1"; +const DEFAULT_SIZE = "1024x1024"; + +type AgnesResponse = { + created?: number; + data: Array<{ url?: string; b64_json?: string }>; +}; + +export function getDefaultModel(): string { + return process.env.AGNES_IMAGE_MODEL || DEFAULT_MODEL; +} + +function getApiKey(): string { + const key = process.env.AGNES_API_KEY; + if (!key) { + throw new Error("AGNES_API_KEY is required. Get one from https://apihub.agnes-ai.com."); + } + return key; +} + +function getBaseUrl(): string { + return (process.env.AGNES_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, ""); +} + +export function parseAspectRatio(ar: string): { width: number; height: number } | null { + const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/); + if (!match) return null; + const w = parseFloat(match[1]!); + const h = parseFloat(match[2]!); + if (w <= 0 || h <= 0) return null; + return { width: w, height: h }; +} + +function gcd(a: number, b: number): number { + let x = Math.abs(Math.round(a)); + let y = Math.abs(Math.round(b)); + while (y !== 0) { + const next = x % y; + x = y; + y = next; + } + return x || 1; +} + +export function snapDim(n: number): number { + return Math.max(32, Math.round(n / 32) * 32); +} + +export function resolveSize(args: Pick): string { + if (args.size) return args.size; + + if (args.aspectRatio) { + const parsed = parseAspectRatio(args.aspectRatio); + if (parsed) { + const g = gcd(parsed.width, parsed.height); + const rw = Math.round(parsed.width / g); + const rh = Math.round(parsed.height / g); + if (rw === 1 && rh === 1) return "1024x1024"; + const maxEdge = 2048; + const scale = Math.max(1, Math.floor(maxEdge / Math.max(rw, rh))); + const width = rw * scale; + const height = rh * scale; + return `${snapDim(width)}x${snapDim(height)}`; + } + } + + return DEFAULT_SIZE; +} + +function isRemoteUrl(refPath: string): boolean { + return /^https?:\/\//i.test(refPath); +} + +export async function resolveReferenceImages( + referenceImages: string[] +): Promise { + const result: string[] = []; + for (const refPath of referenceImages) { + if (isRemoteUrl(refPath)) { + result.push(refPath); + continue; + } + const bytes = await readFile(refPath); + const ext = path.extname(refPath).toLowerCase(); + let mime = "image/png"; + if (ext === ".jpg" || ext === ".jpeg") mime = "image/jpeg"; + else if (ext === ".webp") mime = "image/webp"; + else if (ext === ".gif") mime = "image/gif"; + const b64 = Buffer.from(bytes).toString("base64"); + result.push(`data:${mime};base64,${b64}`); + } + return result; +} + +export function validateArgs(_model: string, args: CliArgs): void { + if (args.n > 1) { + throw new Error("Agnes image generation currently returns a single image per request. Set --n 1 or omit --n."); + } +} + +export function getDefaultOutputExtension(_model: string, args: CliArgs): string { + return args.responseFormat === "url" ? ".txt" : ".png"; +} + +export function buildRequestBody( + prompt: string, + model: string, + args: Pick +): Record { + const body: Record = { + model, + prompt, + size: resolveSize(args), + }; + + if (args.referenceImages.length > 0) { + body.image = args.referenceImages; + } + + body.extra_body = { response_format: "url" }; + + return body; +} + +export async function extractImageFromResponse(result: AgnesResponse): Promise { + const img = result.data[0]; + + if (img?.b64_json) { + return Uint8Array.from(Buffer.from(img.b64_json, "base64")); + } + + if (img?.url) { + const imgRes = await fetch(img.url); + if (!imgRes.ok) throw new Error(`Failed to download image from Agnes: ${imgRes.status}`); + return new Uint8Array(await imgRes.arrayBuffer()); + } + + throw new Error("No image in Agnes response"); +} + +export async function generateImage( + prompt: string, + model: string, + args: CliArgs +): Promise { + const apiKey = getApiKey(); + const baseUrl = getBaseUrl(); + + const referenceImages = await resolveReferenceImages(args.referenceImages); + + const body = buildRequestBody(prompt, model, { ...args, referenceImages }); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 120_000); + + try { + const res = await fetch(`${baseUrl}/images/generations`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`Agnes API error (${res.status}): ${err}`); + } + + const result = (await res.json()) as AgnesResponse; + + if (args.responseFormat === "url") { + const url = result.data[0]?.url; + if (!url) throw new Error("No URL in Agnes response"); + return new Uint8Array(Buffer.from(url, "utf-8")); + } + + return extractImageFromResponse(result); + } finally { + clearTimeout(timeout); + } +} diff --git a/skills/baoyu-image-gen/scripts/types.ts b/skills/baoyu-image-gen/scripts/types.ts index b3d1723..2a1cd4c 100644 --- a/skills/baoyu-image-gen/scripts/types.ts +++ b/skills/baoyu-image-gen/scripts/types.ts @@ -9,9 +9,11 @@ export type Provider = | "jimeng" | "seedream" | "azure" - | "codex-cli"; + | "codex-cli" + | "agnes"; export type Quality = "normal" | "2k"; export type OpenAIImageApiDialect = "openai-native" | "ratio-metadata"; +export type ResponseFormat = "file" | "url"; export type CliArgs = { prompt: string | null; @@ -26,6 +28,7 @@ export type CliArgs = { imageSize: string | null; imageSizeSource?: "cli" | "task" | "config" | null; imageApiDialect: OpenAIImageApiDialect | null; + responseFormat: ResponseFormat | null; referenceImages: string[]; n: number; batchFile: string | null; @@ -46,6 +49,7 @@ export type BatchTaskInput = { quality?: Quality | null; imageSize?: "1K" | "2K" | "4K" | null; imageApiDialect?: OpenAIImageApiDialect | null; + responseFormat?: ResponseFormat | null; ref?: string[]; n?: number; }; @@ -76,6 +80,7 @@ export type ExtendConfig = { seedream: string | null; azure: string | null; "codex-cli": string | null; + agnes: string | null; }; batch?: { max_workers?: number | null; From 591614cfa51d5a7cf4b6f6236e47d4a2126ec5e1 Mon Sep 17 00:00:00 2001 From: Davidlaizz <1830579068@qq.com> Date: Sat, 6 Jun 2026 04:56:19 +0800 Subject: [PATCH 2/5] docs(baoyu-image-gen): add Agnes to SKILL.md, README, and config docs --- README.md | 6 +++--- README.zh.md | 6 +++--- skills/baoyu-image-gen/SKILL.md | 16 +++++++++------- .../references/config/first-time-setup.md | 4 ++++ .../references/config/preferences-schema.md | 11 ++++++++++- .../baoyu-image-gen/references/usage-examples.md | 9 +++++++++ 6 files changed, 38 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2999ee5..bc84086 100644 --- a/README.md +++ b/README.md @@ -797,7 +797,7 @@ AI SDK-based image generation using OpenAI GPT Image 2, Azure OpenAI, Google, Op | `--image` | Output image path (required) | | `--batchfile` | JSON batch file for multi-image generation | | `--jobs` | Worker count for batch mode | -| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `zai`, `minimax`, `jimeng`, `seedream`, or `replicate` | +| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `zai`, `minimax`, `jimeng`, `seedream`, `replicate`, or `agnes` | | `--model`, `-m` | Model ID or deployment name. Azure uses deployment name; OpenRouter uses full model IDs; Z.AI uses `glm-image`; MiniMax uses `image-01` / `image-01-live` | | `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) | | `--size` | Size (e.g., `1024x1024`; `gpt-image-2` accepts valid custom sizes up to 3840px max edge) | @@ -871,9 +871,9 @@ AI SDK-based image generation using OpenAI GPT Image 2, Azure OpenAI, Google, Op **Provider Auto-Selection**: 1. If `--provider` is specified → use it -2. If `--ref` is provided and no provider is specified → try Google, then OpenAI, Azure, OpenRouter, Replicate, Seedream, and finally MiniMax +2. If `--ref` is provided and no provider is specified → try Google, then OpenAI, Azure, OpenRouter, Replicate, Seedream, MiniMax, and finally Agnes 3. If only one API key is available → use that provider -4. If multiple providers are available → default to Google, then OpenAI, Azure, OpenRouter, DashScope, Z.AI, MiniMax, Replicate, Jimeng, Seedream +4. If multiple providers are available → default to Google, then OpenAI, Azure, OpenRouter, DashScope, Z.AI, MiniMax, Replicate, Jimeng, Seedream, Agnes #### baoyu-danger-gemini-web diff --git a/README.zh.md b/README.zh.md index 57f2a6a..db7c8c4 100644 --- a/README.zh.md +++ b/README.zh.md @@ -798,7 +798,7 @@ AI 驱动的生成后端。 | `--image` | 输出图片路径(必需) | | `--batchfile` | 多图批量生成的 JSON 文件 | | `--jobs` | 批量模式的并发 worker 数 | -| `--provider` | `google`、`openai`、`azure`、`openrouter`、`dashscope`、`zai`、`minimax`、`jimeng`、`seedream` 或 `replicate` | +| `--provider` | `google`、`openai`、`azure`、`openrouter`、`dashscope`、`zai`、`minimax`、`jimeng`、`seedream`、`replicate` 或 `agnes` | | `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 ID;Z.AI 使用 `glm-image`;MiniMax 使用 `image-01` / `image-01-live` | | `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) | | `--size` | 尺寸(如 `1024x1024`;`gpt-image-2` 支持最长边不超过 3840px 的有效自定义尺寸) | @@ -872,9 +872,9 @@ AI 驱动的生成后端。 **服务商自动选择**: 1. 如果指定了 `--provider` → 使用指定的 -2. 如果传了 `--ref` 且未指定 provider → 依次尝试 Google、OpenAI、Azure、OpenRouter、Replicate、Seedream,最后是 MiniMax +2. 如果传了 `--ref` 且未指定 provider → 依次尝试 Google、OpenAI、Azure、OpenRouter、Replicate、Seedream、MiniMax,最后是 Agnes 3. 如果只有一个 API 密钥 → 使用对应服务商 -4. 如果多个可用 → 默认使用 Google,然后依次为 OpenAI、Azure、OpenRouter、DashScope、Z.AI、MiniMax、Replicate、即梦、豆包 +4. 如果多个可用 → 默认使用 Google,然后依次为 OpenAI、Azure、OpenRouter、DashScope、Z.AI、MiniMax、Replicate、即梦、豆包、Agnes #### baoyu-danger-gemini-web diff --git a/skills/baoyu-image-gen/SKILL.md b/skills/baoyu-image-gen/SKILL.md index 68bae1c..f468e54 100644 --- a/skills/baoyu-image-gen/SKILL.md +++ b/skills/baoyu-image-gen/SKILL.md @@ -1,6 +1,6 @@ --- name: baoyu-image-gen -description: AI image generation with OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope, Z.AI GLM-Image, MiniMax, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images. +description: AI image generation with OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope, Z.AI GLM-Image, MiniMax, Jimeng, Seedream, Replicate and Agnes APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images. version: 2.1.0 metadata: openclaw: @@ -13,7 +13,7 @@ metadata: # Image Generation (AI SDK) -Official API-based image generation. Supports OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Z.AI GLM-Image, MiniMax, Jimeng (即梦), Seedream (豆包) and Replicate. +Official API-based image generation. Supports OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Z.AI GLM-Image, MiniMax, Jimeng (即梦), Seedream (豆包), Replicate and Agnes. ## User Input Tools @@ -111,7 +111,7 @@ When the user wants a person/object preserved from reference images: | `--image ` | Output image path (required in single-image mode) | | `--batchfile ` | JSON batch file for multi-image generation | | `--jobs ` | Worker count for batch mode (default: auto, max from config, built-in default 10) | -| `--provider google\|openai\|azure\|openrouter\|dashscope\|zai\|minimax\|jimeng\|seedream\|replicate\|codex-cli` | Force provider (default: auto-detect; `codex-cli` is never auto-selected — must be pinned via CLI or EXTEND.md) | +| `--provider google\|openai\|azure\|openrouter\|dashscope\|zai\|minimax\|jimeng\|seedream\|replicate\|codex-cli\|agnes` | Force provider (default: auto-detect; `codex-cli` is never auto-selected — must be pinned via CLI or EXTEND.md) | | `--model `, `-m` | Model ID — see provider references for defaults and allowed values | | `--ar ` | Aspect ratio (`16:9`, `1:1`, `4:3`, …) | | `--size ` | Explicit size (e.g., `1024x1024`; for `gpt-image-2`, width/height must be multiples of 16, max edge 3840px, ratio no wider than 3:1) | @@ -136,7 +136,7 @@ When the user wants a person/object preserved from reference images: | `REPLICATE_API_TOKEN` | Replicate API token | | `JIMENG_ACCESS_KEY_ID`, `JIMENG_SECRET_ACCESS_KEY` | Jimeng (即梦) Volcengine credentials | | `ARK_API_KEY` | Seedream (豆包) Volcengine ARK API key | -| `_IMAGE_MODEL` | Per-provider model override (`OPENAI_IMAGE_MODEL`, `GOOGLE_IMAGE_MODEL`, `DASHSCOPE_IMAGE_MODEL`, `ZAI_IMAGE_MODEL`/`BIGMODEL_IMAGE_MODEL`, `MINIMAX_IMAGE_MODEL`, `OPENROUTER_IMAGE_MODEL`, `REPLICATE_IMAGE_MODEL`, `JIMENG_IMAGE_MODEL`, `SEEDREAM_IMAGE_MODEL`) | +| `_IMAGE_MODEL` | Per-provider model override (`OPENAI_IMAGE_MODEL`, `GOOGLE_IMAGE_MODEL`, `DASHSCOPE_IMAGE_MODEL`, `ZAI_IMAGE_MODEL`/`BIGMODEL_IMAGE_MODEL`, `MINIMAX_IMAGE_MODEL`, `OPENROUTER_IMAGE_MODEL`, `REPLICATE_IMAGE_MODEL`, `JIMENG_IMAGE_MODEL`, `SEEDREAM_IMAGE_MODEL`, `AGNES_IMAGE_MODEL`) | | `AZURE_OPENAI_DEPLOYMENT` (alias `AZURE_OPENAI_IMAGE_MODEL`) | Azure default deployment | | `_BASE_URL` | Per-provider endpoint override | | `AZURE_API_VERSION` | Azure image API version (default `2025-04-01-preview`) | @@ -207,13 +207,14 @@ Each provider has its own quirks (model families, size rules, ref support, limit | OpenRouter (multimodal models, `/chat/completions` flow) | `references/providers/openrouter.md` | | Replicate (nano-banana, Seedream, Wan) | `references/providers/replicate.md` | | Codex CLI (wraps bundled `scripts/codex-imagegen/`; Codex login, no `OPENAI_API_KEY`) | `references/providers/codex-cli.md` | +| Agnes (agnes-image-2.1-flash, reference-image support) | `references/providers/agnes.md` | ## Provider Selection -1. `--ref` provided + no `--provider` → auto-select Google → OpenAI → Azure → OpenRouter → Replicate → Seedream → MiniMax (MiniMax's subject reference is more specialized toward character/portrait consistency) -2. `--provider` specified → use it (if `--ref`, must be google/openai/azure/openrouter/replicate/seedream/minimax/codex-cli) +1. `--ref` provided + no `--provider` → auto-select Google → OpenAI → Azure → OpenRouter → Replicate → Seedream → MiniMax → Agnes (MiniMax's subject reference is more specialized toward character/portrait consistency) +2. `--provider` specified → use it (if `--ref`, must be google/openai/azure/openrouter/replicate/seedream/minimax/codex-cli/agnes) 3. Only one API key present → use that provider -4. Multiple keys → default priority: Google → OpenAI → Azure → OpenRouter → DashScope → Z.AI → MiniMax → Replicate → Jimeng → Seedream +4. Multiple keys → default priority: Google → OpenAI → Azure → OpenRouter → DashScope → Z.AI → MiniMax → Replicate → Jimeng → Seedream → Agnes 5. `codex-cli` is **never auto-selected** — set `default_provider: codex-cli` in EXTEND.md or pass `--provider codex-cli`. It spawns `codex exec` via the bundled `scripts/codex-imagegen/main.ts` TS entrypoint (run with `bun`) and uses the user's Codex subscription (no `OPENAI_API_KEY`). Requires `codex` on `PATH` with an active `codex login`. ## Quality Presets @@ -281,6 +282,7 @@ If `--provider openai --model gpt-image-2` fails because `OPENAI_API_KEY` is mis | `references/providers/minimax.md` | MiniMax image-01 + subject reference | | `references/providers/openrouter.md` | OpenRouter multimodal flow | | `references/providers/replicate.md` | Replicate supported families + guardrails | +| `references/providers/agnes.md` | Agnes (agnes-image-2.1-flash) sizing, refs, and limits | | `references/config/preferences-schema.md` | EXTEND.md schema | | `references/config/first-time-setup.md` | First-time setup flow | diff --git a/skills/baoyu-image-gen/references/config/first-time-setup.md b/skills/baoyu-image-gen/references/config/first-time-setup.md index 1f4103a..448be85 100644 --- a/skills/baoyu-image-gen/references/config/first-time-setup.md +++ b/skills/baoyu-image-gen/references/config/first-time-setup.md @@ -59,6 +59,8 @@ options: description: "MiniMax image generation with subject-reference character workflows" - label: "Replicate" description: "Curated Replicate image families - nano-banana-2, Seedream, and Wan image models" + - label: "Agnes" + description: "Sapiens AI Agnes - optimized for high information density, complex layouts, reference-image support" ``` ### Question 2: Default Google Model @@ -187,6 +189,7 @@ default_model: zai: [selected Z.AI model or null] minimax: [selected minimax model or null] replicate: null + agnes: null --- ``` @@ -358,6 +361,7 @@ default_model: zai: [value or null] minimax: [value or null] replicate: [value or null] + agnes: [value or null] ``` Only set the selected provider's model; leave others as their current value or null. diff --git a/skills/baoyu-image-gen/references/config/preferences-schema.md b/skills/baoyu-image-gen/references/config/preferences-schema.md index cd0b3e4..4099ba6 100644 --- a/skills/baoyu-image-gen/references/config/preferences-schema.md +++ b/skills/baoyu-image-gen/references/config/preferences-schema.md @@ -11,7 +11,7 @@ description: EXTEND.md YAML schema for baoyu-image-gen user preferences --- version: 1 -default_provider: null # google|openai|azure|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|codex-cli|null (null = auto-detect; codex-cli is never auto-detected — pin it here or via --provider) +default_provider: null # google|openai|azure|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|codex-cli|agnes|null (null = auto-detect; codex-cli is never auto-detected — pin it here or via --provider) default_quality: null # normal|2k|null (null = use default: 2k) @@ -31,6 +31,7 @@ default_model: minimax: null # e.g., "image-01" replicate: null # e.g., "google/nano-banana-2" codex-cli: null # Logical label only — Codex image_gen has no user-selectable model. Default: "codex-image-gen" + agnes: null # e.g., "agnes-image-2.1-flash" batch: max_workers: 10 @@ -62,6 +63,9 @@ batch: codex-cli: concurrency: 1 start_interval_ms: 2000 + agnes: + concurrency: 3 + start_interval_ms: 1100 --- ``` @@ -84,6 +88,7 @@ batch: | `default_model.minimax` | string\|null | null | MiniMax default model | | `default_model.replicate` | string\|null | null | Replicate default model | | `default_model.codex-cli` | string\|null | null | Codex-CLI logical label (Codex image_gen has no user-selectable model) | +| `default_model.agnes` | string\|null | null | Agnes default model | | `batch.max_workers` | int\|null | 10 | Batch worker cap | | `batch.provider_limits..concurrency` | int\|null | provider default | Max simultaneous requests per provider | | `batch.provider_limits..start_interval_ms` | int\|null | provider default | Minimum gap between request starts per provider | @@ -118,6 +123,7 @@ default_model: zai: "glm-image" minimax: "image-01" replicate: "google/nano-banana-2" + agnes: "agnes-image-2.1-flash" batch: max_workers: 10 provider_limits: @@ -136,5 +142,8 @@ batch: minimax: concurrency: 3 start_interval_ms: 1100 + agnes: + concurrency: 3 + start_interval_ms: 1100 --- ``` diff --git a/skills/baoyu-image-gen/references/usage-examples.md b/skills/baoyu-image-gen/references/usage-examples.md index 1ae0056..904871c 100644 --- a/skills/baoyu-image-gen/references/usage-examples.md +++ b/skills/baoyu-image-gen/references/usage-examples.md @@ -83,6 +83,15 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic portrait" --image out.p # Codex CLI with reference images (style/composition guidance) ${BUN_X} {baseDir}/scripts/main.ts --prompt "Match this color palette" --image out.png --provider codex-cli --ref source.png --ar 1:1 + +# Agnes (default model) +${BUN_X} {baseDir}/scripts/main.ts --prompt "A detailed infographic" --image out.png --provider agnes + +# Agnes with aspect ratio and URL output +${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic scene" --image out.txt --provider agnes --ar 16:9 --response-format url + +# Agnes with reference image +${BUN_X} {baseDir}/scripts/main.ts --prompt "Apply this style" --image out.png --provider agnes --ref source.png ``` Notes on `codex-cli`: From 7ea2692acd77a0b5e3451779581fd9a0365dbbb0 Mon Sep 17 00:00:00 2001 From: Davidlaizz <1830579068@qq.com> Date: Sat, 6 Jun 2026 05:15:26 +0800 Subject: [PATCH 3/5] fix(baoyu-image-gen): add agnes to default_model YAML whitelist and remote ref allowlist --- skills/baoyu-image-gen/scripts/main.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skills/baoyu-image-gen/scripts/main.ts b/skills/baoyu-image-gen/scripts/main.ts index ad646f4..bcc4841 100644 --- a/skills/baoyu-image-gen/scripts/main.ts +++ b/skills/baoyu-image-gen/scripts/main.ts @@ -499,7 +499,8 @@ export function parseSimpleYaml(yaml: string): Partial { key === "jimeng" || key === "seedream" || key === "azure" || - key === "codex-cli" + key === "codex-cli" || + key === "agnes" ) ) { const cleaned = value.replace(/['"]/g, ""); @@ -822,7 +823,7 @@ function isRemoteReferenceImage(refPath: string): boolean { } function shouldAllowRemoteReferenceImages(provider: Provider | null): boolean { - return provider === "dashscope"; + return provider === "dashscope" || provider === "agnes"; } export async function validateReferenceImages( From 3f1120e9036c10ec61058ac217f8d791f6e0b972 Mon Sep 17 00:00:00 2001 From: Davidlaizz <1830579068@qq.com> Date: Sat, 6 Jun 2026 05:18:27 +0800 Subject: [PATCH 4/5] fix(baoyu-image-gen): add agnes to provider_limits YAML whitelist --- skills/baoyu-image-gen/scripts/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skills/baoyu-image-gen/scripts/main.ts b/skills/baoyu-image-gen/scripts/main.ts index bcc4841..f08bd9b 100644 --- a/skills/baoyu-image-gen/scripts/main.ts +++ b/skills/baoyu-image-gen/scripts/main.ts @@ -479,7 +479,8 @@ export function parseSimpleYaml(yaml: string): Partial { key === "jimeng" || key === "seedream" || key === "azure" || - key === "codex-cli" + key === "codex-cli" || + key === "agnes" ) ) { config.batch ??= {}; From 53aa30bbca3439e8d403e25921f6b698a40c6ea3 Mon Sep 17 00:00:00 2001 From: Davidlaizz <1830579068@qq.com> Date: Sat, 6 Jun 2026 05:46:38 +0800 Subject: [PATCH 5/5] fix(baoyu-image-gen): remove gcd from resolveSize to fix decimal aspect ratio distortion --- .../scripts/providers/agnes.ts | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/skills/baoyu-image-gen/scripts/providers/agnes.ts b/skills/baoyu-image-gen/scripts/providers/agnes.ts index 0cf6a12..2a53943 100644 --- a/skills/baoyu-image-gen/scripts/providers/agnes.ts +++ b/skills/baoyu-image-gen/scripts/providers/agnes.ts @@ -36,17 +36,6 @@ export function parseAspectRatio(ar: string): { width: number; height: number } return { width: w, height: h }; } -function gcd(a: number, b: number): number { - let x = Math.abs(Math.round(a)); - let y = Math.abs(Math.round(b)); - while (y !== 0) { - const next = x % y; - x = y; - y = next; - } - return x || 1; -} - export function snapDim(n: number): number { return Math.max(32, Math.round(n / 32) * 32); } @@ -57,14 +46,11 @@ export function resolveSize(args: Pick): string if (args.aspectRatio) { const parsed = parseAspectRatio(args.aspectRatio); if (parsed) { - const g = gcd(parsed.width, parsed.height); - const rw = Math.round(parsed.width / g); - const rh = Math.round(parsed.height / g); - if (rw === 1 && rh === 1) return "1024x1024"; + if (parsed.width === 1 && parsed.height === 1) return "1024x1024"; const maxEdge = 2048; - const scale = Math.max(1, Math.floor(maxEdge / Math.max(rw, rh))); - const width = rw * scale; - const height = rh * scale; + const scale = Math.max(1, Math.floor(maxEdge / Math.max(parsed.width, parsed.height))); + const width = parsed.width * scale; + const height = parsed.height * scale; return `${snapDim(width)}x${snapDim(height)}`; } }