From 32003da694b82d75e01c8cf4585f2ea0ad25797c Mon Sep 17 00:00:00 2001 From: ldf <1305366530@qq.com> Date: Sat, 14 Mar 2026 00:37:46 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90=E5=8D=B3=E6=A2=A6=E7=94=9F?= =?UTF-8?q?=E5=9B=BE=E5=92=8C=E8=B1=86=E5=8C=85=E7=94=9F=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/baoyu-image-gen/scripts/main.ts | 49 +- .../scripts/providers/jimeng.ts | 425 ++++++++++++++++++ .../scripts/providers/seedream.ts | 111 +++++ skills/baoyu-image-gen/scripts/types.ts | 4 +- 4 files changed, 578 insertions(+), 11 deletions(-) create mode 100644 skills/baoyu-image-gen/scripts/providers/jimeng.ts create mode 100644 skills/baoyu-image-gen/scripts/providers/seedream.ts diff --git a/skills/baoyu-image-gen/scripts/main.ts b/skills/baoyu-image-gen/scripts/main.ts index 504489b..0d78c0e 100644 --- a/skills/baoyu-image-gen/scripts/main.ts +++ b/skills/baoyu-image-gen/scripts/main.ts @@ -55,6 +55,8 @@ const DEFAULT_PROVIDER_RATE_LIMITS: Record = { openai: { concurrency: 3, startIntervalMs: 1100 }, openrouter: { concurrency: 3, startIntervalMs: 1100 }, dashscope: { concurrency: 3, startIntervalMs: 1100 }, + jimeng: { concurrency: 3, startIntervalMs: 1100 }, + seedream: { concurrency: 3, startIntervalMs: 1100 }, }; function printUsage(): void { @@ -69,7 +71,7 @@ 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|replicate Force provider (auto-detect by default) + --provider google|openai|openrouter|dashscope|replicate|jimeng|seedream 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) @@ -107,11 +109,16 @@ Environment variables: GEMINI_API_KEY Gemini API key (alias for GOOGLE_API_KEY) DASHSCOPE_API_KEY DashScope API key REPLICATE_API_TOKEN Replicate API token + JIMENG_ACCESS_KEY_ID Jimeng Access Key ID + JIMENG_SECRET_ACCESS_KEY Jimeng Secret Access Key + ARK_API_KEY Seedream/Ark API key OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5) OPENROUTER_IMAGE_MODEL Default OpenRouter model (google/gemini-3.1-flash-image-preview) GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview) DASHSCOPE_IMAGE_MODEL Default DashScope model (z-image-turbo) REPLICATE_IMAGE_MODEL Default Replicate model (google/nano-banana-pro) + JIMENG_IMAGE_MODEL Default Jimeng model (jimeng_t2i_v40) + SEEDREAM_IMAGE_MODEL Default Seedream model (doubao-seedream-5-0-260128) OPENAI_BASE_URL Custom OpenAI endpoint OPENAI_IMAGE_USE_CHAT Use /chat/completions instead of /images/generations (true|false) OPENROUTER_BASE_URL Custom OpenRouter endpoint @@ -120,6 +127,8 @@ Environment variables: GOOGLE_BASE_URL Custom Google endpoint DASHSCOPE_BASE_URL Custom DashScope endpoint REPLICATE_BASE_URL Custom Replicate endpoint + JIMENG_BASE_URL Custom Jimeng endpoint + SEEDREAM_BASE_URL Custom Seedream endpoint BAOYU_IMAGE_GEN_MAX_WORKERS Override batch worker cap BAOYU_IMAGE_GEN__CONCURRENCY Override provider concurrency BAOYU_IMAGE_GEN__START_INTERVAL_MS Override provider start gap in ms @@ -217,7 +226,9 @@ function parseArgs(argv: string[]): CliArgs { v !== "openai" && v !== "openrouter" && v !== "dashscope" && - v !== "replicate" + v !== "replicate" && + v !== "jimeng" && + v !== "seedream" ) { throw new Error(`Invalid provider: ${v}`); } @@ -370,6 +381,8 @@ function parseSimpleYaml(yaml: string): Partial { openrouter: null, dashscope: null, replicate: null, + jimeng: null, + seedream: null, }; currentKey = "default_model"; currentProvider = null; @@ -393,7 +406,9 @@ function parseSimpleYaml(yaml: string): Partial { key === "openai" || key === "openrouter" || key === "dashscope" || - key === "replicate" + key === "replicate" || + key === "jimeng" || + key === "seedream" ) ) { config.batch ??= {}; @@ -407,7 +422,9 @@ function parseSimpleYaml(yaml: string): Partial { key === "openai" || key === "openrouter" || key === "dashscope" || - key === "replicate" + key === "replicate" || + key === "jimeng" || + key === "seedream" ) ) { const cleaned = value.replace(/['"]/g, ""); @@ -498,9 +515,11 @@ function getConfiguredProviderRateLimits( openai: { ...DEFAULT_PROVIDER_RATE_LIMITS.openai }, openrouter: { ...DEFAULT_PROVIDER_RATE_LIMITS.openrouter }, dashscope: { ...DEFAULT_PROVIDER_RATE_LIMITS.dashscope }, + jimeng: { ...DEFAULT_PROVIDER_RATE_LIMITS.jimeng }, + seedream: { ...DEFAULT_PROVIDER_RATE_LIMITS.seedream }, }; - for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope"] as Provider[]) { + for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream"] as Provider[]) { const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase()}`; const extendLimit = extendConfig.batch?.provider_limits?.[provider]; configured[provider] = { @@ -554,10 +573,11 @@ function detectProvider(args: CliArgs): Provider { args.provider !== "google" && args.provider !== "openai" && args.provider !== "openrouter" && - args.provider !== "replicate" + args.provider !== "replicate" && + args.provider !== "jimeng" ) { throw new Error( - "Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider openrouter (OpenRouter multimodal), or --provider replicate." + "Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider openrouter (OpenRouter multimodal), --provider replicate, or --provider jimeng." ); } @@ -568,14 +588,17 @@ function detectProvider(args: CliArgs): Provider { const hasOpenrouter = !!process.env.OPENROUTER_API_KEY; const hasDashscope = !!process.env.DASHSCOPE_API_KEY; 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; if (args.referenceImages.length > 0) { if (hasGoogle) return "google"; if (hasOpenai) return "openai"; if (hasOpenrouter) return "openrouter"; if (hasReplicate) return "replicate"; + if (hasJimeng) return "jimeng"; throw new Error( - "Reference images require Google, OpenAI, OpenRouter or Replicate. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, or REPLICATE_API_TOKEN, or remove --ref." + "Reference images require Google, OpenAI, OpenRouter, Replicate or Jimeng. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, REPLICATE_API_TOKEN, or Jimeng keys, or remove --ref." ); } @@ -585,13 +608,15 @@ function detectProvider(args: CliArgs): Provider { hasOpenrouter && "openrouter", hasDashscope && "dashscope", hasReplicate && "replicate", + hasJimeng && "jimeng", + hasSeedream && "seedream", ].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, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, or REPLICATE_API_TOKEN.\n" + + "No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, or ARK_API_KEY.\n" + "Create ~/.baoyu-skills/.env or /.baoyu-skills/.env with your keys." ); } @@ -632,6 +657,8 @@ async function loadProviderModule(provider: Provider): Promise { if (provider === "dashscope") return (await import("./providers/dashscope")) as ProviderModule; if (provider === "replicate") return (await import("./providers/replicate")) as ProviderModule; if (provider === "openrouter") return (await import("./providers/openrouter")) as ProviderModule; + if (provider === "jimeng") return (await import("./providers/jimeng")) as ProviderModule; + if (provider === "seedream") return (await import("./providers/seedream")) as ProviderModule; return (await import("./providers/openai")) as ProviderModule; } @@ -658,6 +685,8 @@ function getModelForProvider( } if (provider === "dashscope" && extendConfig.default_model.dashscope) return extendConfig.default_model.dashscope; if (provider === "replicate" && extendConfig.default_model.replicate) return extendConfig.default_model.replicate; + if (provider === "jimeng" && extendConfig.default_model.jimeng) return extendConfig.default_model.jimeng; + if (provider === "seedream" && extendConfig.default_model.seedream) return extendConfig.default_model.seedream; } return providerModule.getDefaultModel(); } @@ -873,7 +902,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", "dashscope"] as Provider[]) { + for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream"] as Provider[]) { const limit = providerRateLimits[provider]; console.error(`- ${provider}: concurrency=${limit.concurrency}, startIntervalMs=${limit.startIntervalMs}`); } diff --git a/skills/baoyu-image-gen/scripts/providers/jimeng.ts b/skills/baoyu-image-gen/scripts/providers/jimeng.ts new file mode 100644 index 0000000..106a9b5 --- /dev/null +++ b/skills/baoyu-image-gen/scripts/providers/jimeng.ts @@ -0,0 +1,425 @@ +import type { CliArgs } from "../types"; +import * as crypto from "node:crypto"; + +export function getDefaultModel(): string { + return process.env.JIMENG_IMAGE_MODEL || "jimeng_t2i_v40"; +} + +function getAccessKey(): string | null { + return process.env.JIMENG_ACCESS_KEY_ID || null; +} + +function getSecretKey(): string | null { + return process.env.JIMENG_SECRET_ACCESS_KEY || null; +} + +function getRegion(): string { + return process.env.JIMENG_REGION || "cn-north-1"; +} + +/** + * Volcengine HMAC-SHA256 signature generation + * Following the official documentation at: + * https://www.volcengine.com/docs/85621/1817045 + */ +function generateSignature( + method: string, + query: Record, + headers: Record, + body: string, + accessKey: string, + secretKey: string, + region: string, + service: string +): string { + // 1. Create canonical request + const canonicalUri = "/"; + + // Sort query parameters alphabetically + const sortedQuery = Object.entries(query) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join("&"); + + // Sort headers alphabetically and create canonical headers + const sortedHeaders = Object.entries(headers) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}\n`) + .join(""); + + const signedHeaders = Object.keys(headers) + .sort() + .map(k => k.toLowerCase()) + .join(";"); + + const hashedPayload = crypto.createHash("sha256").update(body, "utf8").digest("hex"); + + const canonicalRequest = [ + method, + canonicalUri, + sortedQuery, + sortedHeaders, + signedHeaders, + hashedPayload, + ].join("\n"); + + const hashedCanonicalRequest = crypto + .createHash("sha256") + .update(canonicalRequest, "utf8") + .digest("hex"); + + // 2. Create string to sign + const algorithm = "HMAC-SHA256"; + const now = new Date(); + const timestamp = now.toISOString().replace(/[:\-]|\.\d{3}/g, ""); + const dateStamp = timestamp.slice(0, 8); + + const credentialScope = `${dateStamp}/${region}/${service}/request`; + + const stringToSign = [ + algorithm, + timestamp, + credentialScope, + hashedCanonicalRequest, + ].join("\n"); + + // 3. Calculate signature + const kDate = crypto + .createHmac("sha256", secretKey) + .update(dateStamp) + .digest(); + + const kRegion = crypto.createHmac("sha256", kDate).update(region).digest(); + const kService = crypto.createHmac("sha256", kRegion).update(service).digest(); + const kSigning = crypto.createHmac("sha256", kService).update("request").digest(); + + const signature = crypto + .createHmac("sha256", kSigning) + .update(stringToSign) + .digest("hex"); + + // 4. Create authorization header + const authorization = `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; + + return { authorization, timestamp }; +} + +/** + * Parse aspect ratio string like "16:9", "1:1", "4:3" into width and height + */ +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 }; +} + +/** + * Supported size presets for different quality levels + * Based on Volcengine Jimeng documentation + */ +const SIZE_PRESETS: Record> = { + normal: { + "1:1": "1024x1024", + "4:3": "1360x1020", + "16:9": "1536x864", + "3:2": "1440x960", + "21:9": "1920x824", + }, + "2k": { + "1:1": "2048x2048", + "4:3": "2304x1728", + "16:9": "2560x1440", + "3:2": "2496x1664", + "21:9": "3024x1296", + }, + "4k": { + "1:1": "4096x4096", + "4:3": "4694x3520", + "16:9": "5404x3040", + "3:2": "4992x3328", + "21:9": "6198x2656", + }, +}; + +function getImageSize(ar: string | null, quality: CliArgs["quality"], imageSize?: string | null): string { + // If explicit size is provided, normalize it (replace * with x) + if (imageSize) { + return imageSize.replace("*", "x"); + } + + // Default to 2K quality if not specified + const qualityLevel = quality === "normal" ? "normal" : "2k"; + const presets = SIZE_PRESETS[qualityLevel]; + + // Default size + const defaultSize = qualityLevel === "normal" ? "1024x1024" : "2048x2048"; + + // If no aspect ratio, return default + if (!ar) return defaultSize; + + // Parse aspect ratio and find closest match + const parsed = parseAspectRatio(ar); + if (!parsed) return defaultSize; + + const targetRatio = parsed.width / parsed.height; + + // Find closest aspect ratio in presets + let bestMatch = defaultSize; + let bestDiff = Infinity; + + for (const [ratio, size] of Object.entries(presets)) { + const [w, h] = ratio.split(":").map(Number); + const presetRatio = w / h; + const diff = Math.abs(presetRatio - targetRatio); + if (diff < bestDiff) { + bestDiff = diff; + bestMatch = size; + } + } + + return bestMatch; +} + +/** + * Step 1: Submit async task to Volcengine Jimeng API + */ +async function submitTask( + prompt: string, + model: string, + size: string, + accessKey: string, + secretKey: string, + region: string +): Promise { + const baseUrl = "https://visual.volcengineapi.com"; + + // Query parameters for submit endpoint + const query = { + Action: "CVSync2AsyncSubmitTask", + Version: "2022-08-31", + }; + + // Request body - Jimeng API expects width/height as separate integers + const [width, height] = size.split("x").map(Number); + const bodyObj = { + req_key: model, + prompt_text: prompt, + // Use separate width and height parameters instead of size string + width: width, + height: height, + // Optional: seed for reproducibility + // seed: Math.floor(Math.random() * 999999), + }; + + const body = JSON.stringify(bodyObj); + + // Headers + const timestampHeader = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, ""); + const headers = { + "Content-Type": "application/json", + "X-Date": timestampHeader, + "Host": "visual.volcengineapi.com", + }; + + // Generate signature + const { authorization, timestamp } = generateSignature( + "POST", + query, + headers, + body, + accessKey, + secretKey, + region, + "cv" + ); + + // Build URL with query parameters + const url = `${baseUrl}/?Action=${query.Action}&Version=${query.Version}`; + + console.error(`Submitting task to Jimeng (${model})...`, { width, height }); + + const res = await fetch(url, { + method: "POST", + headers: { + ...headers, + "Authorization": authorization, + }, + body, + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`Jimeng API submit error (${res.status}): ${err}`); + } + + const result = (await res.json()) as { + code?: number; + message?: string; + data?: { + task_id?: string; + }; + }; + + // Volcengine API returns code 10000 for success + if (result.code !== 10000 || !result.data?.task_id) { + console.error("Submit response:", JSON.stringify(result, null, 2)); + throw new Error(`Failed to submit task: ${result.message || "Unknown error"}`); + } + + return result.data.task_id; +} + +/** + * Step 2: Poll for task result + * Returns image data directly as Uint8Array + */ +async function pollForResult( + taskId: string, + model: string, + accessKey: string, + secretKey: string, + region: string +): Promise { + const baseUrl = "https://visual.volcengineapi.com"; + const maxAttempts = 60; + const pollIntervalMs = 2000; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + // Query parameters for result endpoint + const query = { + Action: "CVSync2AsyncGetResult", + Version: "2022-08-31", + }; + + // Request body - include req_key and task_id + const bodyObj = { + req_key: model, + task_id: taskId, + }; + + const body = JSON.stringify(bodyObj); + + // Headers + const timestampHeader = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, ""); + const headers = { + "Content-Type": "application/json", + "X-Date": timestampHeader, + "Host": "visual.volcengineapi.com", + }; + + // Generate signature + const { authorization } = generateSignature( + "POST", + query, + headers, + body, + accessKey, + secretKey, + region, + "cv" + ); + + // Build URL with query parameters + const url = `${baseUrl}/?Action=${query.Action}&Version=${query.Version}`; + + const res = await fetch(url, { + method: "POST", + headers: { + ...headers, + "Authorization": authorization, + }, + body, + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`Jimeng API poll error (${res.status}): ${err}`); + } + + const result = (await res.json()) as { + code?: number; + message?: string; + data?: { + status?: string; + image_urls?: string[]; + binary_data_base64?: string[]; + }; + }; + + // Volcengine API returns code 10000 for success + if (result.code === 10000 && result.data) { + const { status, image_urls, binary_data_base64 } = result.data; + + // Check for base64 image data (preferred by Jimeng) + if (binary_data_base64 && binary_data_base64.length > 0) { + console.error("Image received as base64 data"); + const base64Data = binary_data_base64[0]!; + // Convert base64 to Uint8Array + const binaryString = Buffer.from(base64Data, "base64").toString("binary"); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + // Fallback to URL format + if (status === "done" && image_urls && image_urls.length > 0) { + // Download from URL + console.error(`Downloading image from ${image_urls[0]}...`); + const imgRes = await fetch(image_urls[0]!); + if (!imgRes.ok) { + throw new Error(`Failed to download image from ${image_urls[0]}`); + } + const buffer = await imgRes.arrayBuffer(); + return new Uint8Array(buffer); + } + + if (status === "in_queue" || status === "generating") { + console.error(`Task status: ${status} (${attempt + 1}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + continue; + } + + if (status === "fail") { + throw new Error(`Jimeng task failed: ${result.message || "Generation failed"}`); + } + } + + console.error("Poll response:", JSON.stringify(result, null, 2)); + throw new Error(`Unexpected response during polling: ${result.message || "Unknown error"}`); + } + + throw new Error("Task timeout: image generation took too long"); +} + +export async function generateImage( + prompt: string, + model: string, + args: CliArgs +): Promise { + const accessKey = getAccessKey(); + const secretKey = getSecretKey(); + const region = getRegion(); + + if (!accessKey || !secretKey) { + throw new Error( + "JIMENG_ACCESS_KEY_ID and JIMENG_SECRET_ACCESS_KEY are required. " + + "Get your credentials from https://console.volcengine.com/iam/keymanage" + ); + } + + const size = getImageSize(args.aspectRatio, args.quality, args.imageSize); + + // Step 1: Submit task + const taskId = await submitTask(prompt, model, size, accessKey, secretKey, region); + + // Step 2: Poll for result (returns image data directly) + const imageData = await pollForResult(taskId, model, accessKey, secretKey, region); + + console.error("Image generation complete!"); + return imageData; +} diff --git a/skills/baoyu-image-gen/scripts/providers/seedream.ts b/skills/baoyu-image-gen/scripts/providers/seedream.ts new file mode 100644 index 0000000..778f1fe --- /dev/null +++ b/skills/baoyu-image-gen/scripts/providers/seedream.ts @@ -0,0 +1,111 @@ +import type { CliArgs } from "../types"; + +export function getDefaultModel(): string { + return process.env.SEEDREAM_IMAGE_MODEL || "doubao-seedream-5-0-260128"; +} + +function getApiKey(): string | null { + return process.env.ARK_API_KEY || null; +} + +function getBaseUrl(): string { + return process.env.SEEDREAM_BASE_URL || "https://ark.cn-beijing.volces.com/api/v3"; +} + +/** + * Convert aspect ratio to Seedream size format + * Seedream API accepts: "2k" (default), "3k", or WIDTHxHEIGHT format + * Note: API uses lowercase "2k"/"3k", not "2K"/"3K" + */ +function getSeedreamSize(ar: string | null, quality: CliArgs["quality"], imageSize?: string | null): string { + // If explicit size is provided + if (imageSize) { + const upper = imageSize.toUpperCase(); + if (upper === "2K" || upper === "3K") { + return upper.toLowerCase(); // API expects "2k" or "3k" + } + // For widthxheight format, pass through as-is + if (imageSize.includes("x")) { + return imageSize; + } + } + + // Default to 2k (smallest option supported by API) + return "2k"; +} + +type SeedreamImageResponse = { + model: string; + created: number; + data: Array<{ + url: string; + size: string; + }>; + usage: { + generated_images: number; + output_tokens: number; + total_tokens: number; + }; +}; + +export async function generateImage( + prompt: string, + model: string, + args: CliArgs +): Promise { + const apiKey = getApiKey(); + if (!apiKey) { + throw new Error( + "ARK_API_KEY is required. " + + "Get your API key from https://console.volcengine.com/ark" + ); + } + + const baseUrl = getBaseUrl(); + const size = getSeedreamSize(args.aspectRatio, args.quality, args.imageSize); + + console.error(`Calling Seedream API (${model}) with size: ${size}`); + + const requestBody = { + model, + prompt, + size, + output_format: "png", + watermark: false, + }; + + const response = await fetch(`${baseUrl}/images/generations`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const err = await response.text(); + throw new Error(`Seedream API error (${response.status}): ${err}`); + } + + const result = (await response.json()) as SeedreamImageResponse; + + if (!result.data || result.data.length === 0) { + throw new Error("No image data in Seedream response"); + } + + const imageUrl = result.data[0].url; + if (!imageUrl) { + throw new Error("No image URL in Seedream response"); + } + + // Download image from URL + console.error(`Downloading image from ${imageUrl}...`); + const imgResponse = await fetch(imageUrl); + if (!imgResponse.ok) { + throw new Error(`Failed to download image from ${imageUrl}`); + } + + const buffer = await imgResponse.arrayBuffer(); + return new Uint8Array(buffer); +} diff --git a/skills/baoyu-image-gen/scripts/types.ts b/skills/baoyu-image-gen/scripts/types.ts index e41f842..e84421a 100644 --- a/skills/baoyu-image-gen/scripts/types.ts +++ b/skills/baoyu-image-gen/scripts/types.ts @@ -1,4 +1,4 @@ -export type Provider = "google" | "openai" | "openrouter" | "dashscope" | "replicate"; +export type Provider = "google" | "openai" | "openrouter" | "dashscope" | "replicate" | "jimeng" | "seedream"; export type Quality = "normal" | "2k"; export type CliArgs = { @@ -53,6 +53,8 @@ export type ExtendConfig = { openrouter: string | null; dashscope: string | null; replicate: string | null; + jimeng: string | null; + seedream: string | null; }; batch?: { max_workers?: number | null;