import path from "node:path"; import { readFile } from "node:fs/promises"; import type { CliArgs } from "../types"; export type SeedreamModelFamily = | "seedream5" | "seedream45" | "seedream40" | "seedream30" | "unknown"; type SeedreamRequestImage = string | string[]; type SeedreamRequestBody = { model: string; prompt: string; size: string; response_format: "url"; watermark: boolean; image?: SeedreamRequestImage; output_format?: "png"; }; type SeedreamImageResponse = { model?: string; created?: number; data?: Array<{ url?: string; b64_json?: string; size?: string; error?: { code?: string; message?: string; }; }>; usage?: { generated_images: number; output_tokens: number; total_tokens: number; }; error?: { code?: string; message?: string; }; }; 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"; } function parsePixelSize(value: string): { width: number; height: number } | null { const match = value.trim().match(/^(\d+)\s*[xX]\s*(\d+)$/); if (!match) return null; const width = parseInt(match[1]!, 10); const height = parseInt(match[2]!, 10); if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { return null; } return { width, height }; } function normalizePixelSize(value: string): string | null { const parsed = parsePixelSize(value); if (!parsed) return null; return `${parsed.width}x${parsed.height}`; } function normalizeSizePreset(value: string): string | null { const upper = value.trim().toUpperCase(); if (upper === "ADAPTIVE") return "adaptive"; if (upper === "1K" || upper === "2K" || upper === "3K" || upper === "4K") return upper; return null; } function normalizeSizeValue(value: string): string | null { return normalizeSizePreset(value) ?? normalizePixelSize(value); } function getMimeType(filename: string): string { const ext = path.extname(filename).toLowerCase(); if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg"; if (ext === ".webp") return "image/webp"; if (ext === ".gif") return "image/gif"; if (ext === ".bmp") return "image/bmp"; if (ext === ".tiff" || ext === ".tif") return "image/tiff"; return "image/png"; } async function readImageAsDataUrl(filePath: string): Promise { const bytes = await readFile(filePath); return `data:${getMimeType(filePath)};base64,${bytes.toString("base64")}`; } export function getModelFamily(model: string): SeedreamModelFamily { const normalized = model.trim(); if (/^doubao-seedream-5-0(?:-lite)?-\d+$/.test(normalized)) return "seedream5"; if (/^doubao-seedream-4-5-\d+$/.test(normalized)) return "seedream45"; if (/^doubao-seedream-4-0-\d+$/.test(normalized)) return "seedream40"; if (/^doubao-seedream-3-0-t2i-\d+$/.test(normalized)) return "seedream30"; return "unknown"; } function isRemovedSeededitModel(model: string): boolean { return /^doubao-seededit-3-0-i2i-\d+$/.test(model.trim()); } function assertSupportedModel(model: string): void { if (isRemovedSeededitModel(model)) { throw new Error( `${model} is no longer supported. SeedEdit 3.0 support has been removed from this tool; use Seedream 5.0/4.5/4.0/3.0 instead.` ); } } export function supportsReferenceImages(model: string): boolean { const family = getModelFamily(model); return family === "seedream5" || family === "seedream45" || family === "seedream40"; } function supportsOutputFormat(model: string): boolean { return getModelFamily(model) === "seedream5"; } export function getDefaultOutputExtension(model: string): ".png" | ".jpg" { assertSupportedModel(model); return supportsOutputFormat(model) ? ".png" : ".jpg"; } export function getDefaultSeedreamSize(model: string, args: CliArgs): string { assertSupportedModel(model); const family = getModelFamily(model); if (family === "seedream5") return "2K"; if (family === "seedream45") return "2K"; if (family === "seedream40") return args.quality === "normal" ? "1K" : "2K"; if (family === "seedream30") return args.quality === "2k" ? "2048x2048" : "1024x1024"; return "2K"; } export function resolveSeedreamSize(model: string, args: CliArgs): string { assertSupportedModel(model); const family = getModelFamily(model); const requested = args.size || args.imageSize || null; const normalized = requested ? normalizeSizeValue(requested) : null; if (!normalized) { return getDefaultSeedreamSize(model, args); } if (family === "seedream30") { const pixelSize = normalizePixelSize(normalized); if (!pixelSize) { throw new Error("Seedream 3.0 only supports explicit WxH sizes such as 1024x1024."); } return pixelSize; } if (family === "seedream5") { if (normalized === "4K" || normalized === "1K" || normalized === "adaptive") { throw new Error("Seedream 5.0 only supports 2K, 3K, or explicit WxH sizes."); } return normalized; } if (family === "seedream45") { if (normalized === "1K" || normalized === "3K" || normalized === "adaptive") { throw new Error("Seedream 4.5 only supports 2K, 4K, or explicit WxH sizes."); } return normalized; } if (family === "seedream40") { if (normalized === "3K" || normalized === "adaptive") { throw new Error("Seedream 4.0 only supports 1K, 2K, 4K, or explicit WxH sizes."); } return normalized; } if (normalized === "adaptive") { throw new Error("Adaptive size is not supported by Seedream image generation."); } if (normalized === "1K" || normalized === "3K" || normalized === "4K") { throw new Error( "Unknown Seedream model ID. Use a documented model ID or pass an explicit WxH size instead of preset imageSize." ); } return normalized; } export function validateArgs(model: string, args: CliArgs): void { assertSupportedModel(model); const family = getModelFamily(model); const refCount = args.referenceImages.length; if (refCount === 0) { resolveSeedreamSize(model, args); return; } if (family === "unknown") { throw new Error( "Reference images with Seedream require a known model ID. Use Seedream 5.0/4.5/4.0 model IDs instead of an endpoint ID." ); } if (!supportsReferenceImages(model)) { throw new Error(`${model} does not support reference images.`); } if ((family === "seedream5" || family === "seedream45" || family === "seedream40") && refCount > 14) { throw new Error(`${model} supports at most 14 reference images.`); } resolveSeedreamSize(model, args); } export async function buildImageInput( model: string, referenceImages: string[], ): Promise { if (referenceImages.length === 0) return undefined; assertSupportedModel(model); const encoded = await Promise.all(referenceImages.map((refPath) => readImageAsDataUrl(refPath))); return encoded.length === 1 ? encoded[0]! : encoded; } export function buildRequestBody( prompt: string, model: string, args: CliArgs, imageInput?: SeedreamRequestImage, ): SeedreamRequestBody { validateArgs(model, args); const requestBody: SeedreamRequestBody = { model, prompt, size: resolveSeedreamSize(model, args), response_format: "url", watermark: false, }; if (imageInput) { requestBody.image = imageInput; } if (supportsOutputFormat(model)) { requestBody.output_format = "png"; } return requestBody; } async function downloadImage(url: string): Promise { const imgResponse = await fetch(url); if (!imgResponse.ok) { throw new Error(`Failed to download image from ${url}`); } const buffer = await imgResponse.arrayBuffer(); return new Uint8Array(buffer); } export async function extractImageFromResponse(result: SeedreamImageResponse): Promise { const first = result.data?.find((item) => item.url || item.b64_json || item.error); if (!first) { throw new Error("No image data in Seedream response"); } if (first.error) { throw new Error(first.error.message || "Seedream returned an image generation error"); } if (first.b64_json) { return Uint8Array.from(Buffer.from(first.b64_json, "base64")); } if (first.url) { console.error(`Downloading image from ${first.url}...`); return downloadImage(first.url); } throw new Error("No image URL or base64 data in Seedream response"); } 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" ); } validateArgs(model, args); const imageInput = await buildImageInput(model, args.referenceImages); const requestBody = buildRequestBody(prompt, model, args, imageInput); console.error(`Calling Seedream API (${model}) with size: ${requestBody.size}`); const response = await fetch(`${getBaseUrl()}/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.error) { throw new Error(result.error.message || "Seedream API returned an error"); } return extractImageFromResponse(result); }