Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate images via OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream, and Replicate APIs with batch support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/providers/seedream.ts
1import path from "node:path";2import { readFile } from "node:fs/promises";34import type { CliArgs } from "../types";56export type SeedreamModelFamily =7| "seedream5"8| "seedream45"9| "seedream40"10| "seedream30"11| "unknown";1213type SeedreamRequestImage = string | string[];1415type SeedreamRequestBody = {16model: string;17prompt: string;18size: string;19response_format: "url";20watermark: boolean;21image?: SeedreamRequestImage;22output_format?: "png";23};2425type SeedreamImageResponse = {26model?: string;27created?: number;28data?: Array<{29url?: string;30b64_json?: string;31size?: string;32error?: {33code?: string;34message?: string;35};36}>;37usage?: {38generated_images: number;39output_tokens: number;40total_tokens: number;41};42error?: {43code?: string;44message?: string;45};46};4748export function getDefaultModel(): string {49return process.env.SEEDREAM_IMAGE_MODEL || "doubao-seedream-5-0-260128";50}5152function getApiKey(): string | null {53return process.env.ARK_API_KEY || null;54}5556function getBaseUrl(): string {57return process.env.SEEDREAM_BASE_URL || "https://ark.cn-beijing.volces.com/api/v3";58}5960function parsePixelSize(value: string): { width: number; height: number } | null {61const match = value.trim().match(/^(\d+)\s*[xX]\s*(\d+)$/);62if (!match) return null;6364const width = parseInt(match[1]!, 10);65const height = parseInt(match[2]!, 10);66if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {67return null;68}6970return { width, height };71}7273function normalizePixelSize(value: string): string | null {74const parsed = parsePixelSize(value);75if (!parsed) return null;76return `${parsed.width}x${parsed.height}`;77}7879function normalizeSizePreset(value: string): string | null {80const upper = value.trim().toUpperCase();81if (upper === "ADAPTIVE") return "adaptive";82if (upper === "1K" || upper === "2K" || upper === "3K" || upper === "4K") return upper;83return null;84}8586function normalizeSizeValue(value: string): string | null {87return normalizeSizePreset(value) ?? normalizePixelSize(value);88}8990function getMimeType(filename: string): string {91const ext = path.extname(filename).toLowerCase();92if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";93if (ext === ".webp") return "image/webp";94if (ext === ".gif") return "image/gif";95if (ext === ".bmp") return "image/bmp";96if (ext === ".tiff" || ext === ".tif") return "image/tiff";97return "image/png";98}99100async function readImageAsDataUrl(filePath: string): Promise<string> {101const bytes = await readFile(filePath);102return `data:${getMimeType(filePath)};base64,${bytes.toString("base64")}`;103}104105export function getModelFamily(model: string): SeedreamModelFamily {106const normalized = model.trim();107if (/^doubao-seedream-5-0(?:-lite)?-\d+$/.test(normalized)) return "seedream5";108if (/^doubao-seedream-4-5-\d+$/.test(normalized)) return "seedream45";109if (/^doubao-seedream-4-0-\d+$/.test(normalized)) return "seedream40";110if (/^doubao-seedream-3-0-t2i-\d+$/.test(normalized)) return "seedream30";111return "unknown";112}113114function isRemovedSeededitModel(model: string): boolean {115return /^doubao-seededit-3-0-i2i-\d+$/.test(model.trim());116}117118function assertSupportedModel(model: string): void {119if (isRemovedSeededitModel(model)) {120throw new Error(121`${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.`122);123}124}125126export function supportsReferenceImages(model: string): boolean {127const family = getModelFamily(model);128return family === "seedream5" || family === "seedream45" || family === "seedream40";129}130131function supportsOutputFormat(model: string): boolean {132return getModelFamily(model) === "seedream5";133}134135export function getDefaultOutputExtension(model: string): ".png" | ".jpg" {136assertSupportedModel(model);137return supportsOutputFormat(model) ? ".png" : ".jpg";138}139140export function getDefaultSeedreamSize(model: string, args: CliArgs): string {141assertSupportedModel(model);142const family = getModelFamily(model);143144if (family === "seedream5") return "2K";145if (family === "seedream45") return "2K";146if (family === "seedream40") return args.quality === "normal" ? "1K" : "2K";147if (family === "seedream30") return args.quality === "2k" ? "2048x2048" : "1024x1024";148return "2K";149}150151export function resolveSeedreamSize(model: string, args: CliArgs): string {152assertSupportedModel(model);153const family = getModelFamily(model);154const requested = args.size || args.imageSize || null;155const normalized = requested ? normalizeSizeValue(requested) : null;156157if (!normalized) {158return getDefaultSeedreamSize(model, args);159}160161if (family === "seedream30") {162const pixelSize = normalizePixelSize(normalized);163if (!pixelSize) {164throw new Error("Seedream 3.0 only supports explicit WxH sizes such as 1024x1024.");165}166return pixelSize;167}168169if (family === "seedream5") {170if (normalized === "4K" || normalized === "1K" || normalized === "adaptive") {171throw new Error("Seedream 5.0 only supports 2K, 3K, or explicit WxH sizes.");172}173return normalized;174}175176if (family === "seedream45") {177if (normalized === "1K" || normalized === "3K" || normalized === "adaptive") {178throw new Error("Seedream 4.5 only supports 2K, 4K, or explicit WxH sizes.");179}180return normalized;181}182183if (family === "seedream40") {184if (normalized === "3K" || normalized === "adaptive") {185throw new Error("Seedream 4.0 only supports 1K, 2K, 4K, or explicit WxH sizes.");186}187return normalized;188}189190if (normalized === "adaptive") {191throw new Error("Adaptive size is not supported by Seedream image generation.");192}193194if (normalized === "1K" || normalized === "3K" || normalized === "4K") {195throw new Error(196"Unknown Seedream model ID. Use a documented model ID or pass an explicit WxH size instead of preset imageSize."197);198}199200return normalized;201}202203export function validateArgs(model: string, args: CliArgs): void {204assertSupportedModel(model);205const family = getModelFamily(model);206const refCount = args.referenceImages.length;207208if (refCount === 0) {209resolveSeedreamSize(model, args);210return;211}212213if (family === "unknown") {214throw new Error(215"Reference images with Seedream require a known model ID. Use Seedream 5.0/4.5/4.0 model IDs instead of an endpoint ID."216);217}218219if (!supportsReferenceImages(model)) {220throw new Error(`${model} does not support reference images.`);221}222223if ((family === "seedream5" || family === "seedream45" || family === "seedream40") && refCount > 14) {224throw new Error(`${model} supports at most 14 reference images.`);225}226227resolveSeedreamSize(model, args);228}229230export async function buildImageInput(231model: string,232referenceImages: string[],233): Promise<SeedreamRequestImage | undefined> {234if (referenceImages.length === 0) return undefined;235assertSupportedModel(model);236237const encoded = await Promise.all(referenceImages.map((refPath) => readImageAsDataUrl(refPath)));238239return encoded.length === 1 ? encoded[0]! : encoded;240}241242export function buildRequestBody(243prompt: string,244model: string,245args: CliArgs,246imageInput?: SeedreamRequestImage,247): SeedreamRequestBody {248validateArgs(model, args);249250const requestBody: SeedreamRequestBody = {251model,252prompt,253size: resolveSeedreamSize(model, args),254response_format: "url",255watermark: false,256};257258if (imageInput) {259requestBody.image = imageInput;260}261262if (supportsOutputFormat(model)) {263requestBody.output_format = "png";264}265266return requestBody;267}268269async function downloadImage(url: string): Promise<Uint8Array> {270const imgResponse = await fetch(url);271if (!imgResponse.ok) {272throw new Error(`Failed to download image from ${url}`);273}274275const buffer = await imgResponse.arrayBuffer();276return new Uint8Array(buffer);277}278279export async function extractImageFromResponse(result: SeedreamImageResponse): Promise<Uint8Array> {280const first = result.data?.find((item) => item.url || item.b64_json || item.error);281282if (!first) {283throw new Error("No image data in Seedream response");284}285286if (first.error) {287throw new Error(first.error.message || "Seedream returned an image generation error");288}289290if (first.b64_json) {291return Uint8Array.from(Buffer.from(first.b64_json, "base64"));292}293294if (first.url) {295console.error(`Downloading image from ${first.url}...`);296return downloadImage(first.url);297}298299throw new Error("No image URL or base64 data in Seedream response");300}301302export async function generateImage(303prompt: string,304model: string,305args: CliArgs,306): Promise<Uint8Array> {307const apiKey = getApiKey();308if (!apiKey) {309throw new Error(310"ARK_API_KEY is required. " +311"Get your API key from https://console.volcengine.com/ark"312);313}314315validateArgs(model, args);316const imageInput = await buildImageInput(model, args.referenceImages);317const requestBody = buildRequestBody(prompt, model, args, imageInput);318319console.error(`Calling Seedream API (${model}) with size: ${requestBody.size}`);320321const response = await fetch(`${getBaseUrl()}/images/generations`, {322method: "POST",323headers: {324"Content-Type": "application/json",325Authorization: `Bearer ${apiKey}`,326},327body: JSON.stringify(requestBody),328});329330if (!response.ok) {331const err = await response.text();332throw new Error(`Seedream API error (${response.status}): ${err}`);333}334335const result = (await response.json()) as SeedreamImageResponse;336if (result.error) {337throw new Error(result.error.message || "Seedream API returned an error");338}339340return extractImageFromResponse(result);341}342