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/dashscope.ts
1import type { CliArgs, Quality } from "../types";23type DashScopeModelFamily = "qwen2" | "qwenFixed" | "legacy";45type DashScopeModelSpec = {6family: DashScopeModelFamily;7defaultSize: string;8};910const DEFAULT_MODEL = "qwen-image-2.0-pro";11const MIN_QWEN_2_TOTAL_PIXELS = 512 * 512;12const MAX_QWEN_2_TOTAL_PIXELS = 2048 * 2048;13const SIZE_STEP = 16;14const QWEN_NEGATIVE_PROMPT =15"低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感,构图混乱,文字模糊,扭曲";1617const QWEN_2_TARGET_PIXELS: Record<Quality, number> = {18normal: 1024 * 1024,19"2k": 1536 * 1536,20};2122const QWEN_2_RECOMMENDED: Record<string, Record<Quality, string>> = {23"1:1": { normal: "1024*1024", "2k": "1536*1536" },24"2:3": { normal: "768*1152", "2k": "1024*1536" },25"3:2": { normal: "1152*768", "2k": "1536*1024" },26"3:4": { normal: "960*1280", "2k": "1080*1440" },27"4:3": { normal: "1280*960", "2k": "1440*1080" },28"9:16": { normal: "720*1280", "2k": "1080*1920" },29"16:9": { normal: "1280*720", "2k": "1920*1080" },30"21:9": { normal: "1344*576", "2k": "2048*872" },31};3233const QWEN_FIXED_SIZES_BY_RATIO: Record<string, string> = {34"16:9": "1664*928",35"4:3": "1472*1104",36"1:1": "1328*1328",37"3:4": "1104*1472",38"9:16": "928*1664",39};4041const QWEN_FIXED_SIZES = Object.values(QWEN_FIXED_SIZES_BY_RATIO);4243const LEGACY_STANDARD_SIZES: [number, number][] = [44[1024, 1024],45[1280, 720],46[720, 1280],47[1024, 768],48[768, 1024],49[1536, 1024],50[1024, 1536],51[1536, 864],52[864, 1536],53];5455const LEGACY_STANDARD_SIZES_2K: [number, number][] = [56[1536, 1536],57[2048, 1152],58[1152, 2048],59[1536, 1024],60[1024, 1536],61[1536, 864],62[864, 1536],63[2048, 2048],64];6566const QWEN_2_SPEC: DashScopeModelSpec = {67family: "qwen2",68defaultSize: "1024*1024",69};7071const QWEN_FIXED_SPEC: DashScopeModelSpec = {72family: "qwenFixed",73defaultSize: QWEN_FIXED_SIZES_BY_RATIO["16:9"],74};7576const LEGACY_SPEC: DashScopeModelSpec = {77family: "legacy",78defaultSize: "1536*1536",79};8081const MODEL_SPEC_ALIASES: Record<string, DashScopeModelSpec> = {82"qwen-image-2.0-pro": QWEN_2_SPEC,83"qwen-image-2.0-pro-2026-03-03": QWEN_2_SPEC,84"qwen-image-2.0": QWEN_2_SPEC,85"qwen-image-2.0-2026-03-03": QWEN_2_SPEC,86"qwen-image-max": QWEN_FIXED_SPEC,87"qwen-image-max-2025-12-30": QWEN_FIXED_SPEC,88"qwen-image-plus": QWEN_FIXED_SPEC,89"qwen-image-plus-2026-01-09": QWEN_FIXED_SPEC,90"qwen-image": QWEN_FIXED_SPEC,91};9293export function getDefaultModel(): string {94return process.env.DASHSCOPE_IMAGE_MODEL || DEFAULT_MODEL;95}9697function getApiKey(): string | null {98return process.env.DASHSCOPE_API_KEY || null;99}100101function getBaseUrl(): string {102const base = process.env.DASHSCOPE_BASE_URL || "https://dashscope.aliyuncs.com";103return base.replace(/\/+$/g, "");104}105106function getModelSpec(model: string): DashScopeModelSpec {107return MODEL_SPEC_ALIASES[model.trim().toLowerCase()] || LEGACY_SPEC;108}109110export function getModelFamily(model: string): DashScopeModelFamily {111return getModelSpec(model).family;112}113114function normalizeQuality(quality: CliArgs["quality"]): Quality {115return quality === "normal" ? "normal" : "2k";116}117118export function parseAspectRatio(ar: string): { width: number; height: number } | null {119const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);120if (!match) return null;121const w = parseFloat(match[1]!);122const h = parseFloat(match[2]!);123if (w <= 0 || h <= 0) return null;124return { width: w, height: h };125}126127export function normalizeSize(size: string): string {128return size.replace("x", "*");129}130131export function parseSize(size: string): { width: number; height: number } | null {132const match = normalizeSize(size).match(/^(\d+)\*(\d+)$/);133if (!match) return null;134const width = Number(match[1]);135const height = Number(match[2]);136if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {137return null;138}139return { width, height };140}141142function formatSize(width: number, height: number): string {143return `${width}*${height}`;144}145146function getRatioValue(ar: string): number | null {147const parsed = parseAspectRatio(ar);148if (!parsed) return null;149return parsed.width / parsed.height;150}151152function findKnownRatioKey(ar: string, candidates: string[], tolerance = 0.02): string | null {153const targetRatio = getRatioValue(ar);154if (targetRatio == null) return null;155156let bestKey: string | null = null;157let bestDiff = Infinity;158159for (const candidate of candidates) {160const candidateRatio = getRatioValue(candidate);161if (candidateRatio == null) continue;162const diff = Math.abs(candidateRatio - targetRatio);163if (diff < bestDiff) {164bestDiff = diff;165bestKey = candidate;166}167}168169return bestDiff <= tolerance ? bestKey : null;170}171172function roundToStep(value: number): number {173return Math.max(SIZE_STEP, Math.round(value / SIZE_STEP) * SIZE_STEP);174}175176function fitToPixelBudget(177width: number,178height: number,179minPixels: number,180maxPixels: number,181): { width: number; height: number } {182let nextWidth = width;183let nextHeight = height;184let pixels = nextWidth * nextHeight;185186if (pixels > maxPixels) {187const scale = Math.sqrt(maxPixels / pixels);188nextWidth *= scale;189nextHeight *= scale;190} else if (pixels < minPixels) {191const scale = Math.sqrt(minPixels / pixels);192nextWidth *= scale;193nextHeight *= scale;194}195196let roundedWidth = roundToStep(nextWidth);197let roundedHeight = roundToStep(nextHeight);198pixels = roundedWidth * roundedHeight;199200while (pixels > maxPixels && (roundedWidth > SIZE_STEP || roundedHeight > SIZE_STEP)) {201if (roundedWidth >= roundedHeight && roundedWidth > SIZE_STEP) {202roundedWidth -= SIZE_STEP;203} else if (roundedHeight > SIZE_STEP) {204roundedHeight -= SIZE_STEP;205} else {206break;207}208pixels = roundedWidth * roundedHeight;209}210211while (pixels < minPixels) {212if (roundedWidth <= roundedHeight) {213roundedWidth += SIZE_STEP;214} else {215roundedHeight += SIZE_STEP;216}217pixels = roundedWidth * roundedHeight;218}219220return { width: roundedWidth, height: roundedHeight };221}222223export function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {224const normalizedQuality = normalizeQuality(quality);225const sizes = normalizedQuality === "2k" ? LEGACY_STANDARD_SIZES_2K : LEGACY_STANDARD_SIZES;226const defaultSize = normalizedQuality === "2k" ? "1536*1536" : "1024*1024";227228if (!ar) return defaultSize;229230const parsed = parseAspectRatio(ar);231if (!parsed) return defaultSize;232233const targetRatio = parsed.width / parsed.height;234let best = defaultSize;235let bestDiff = Infinity;236237for (const [width, height] of sizes) {238const diff = Math.abs(width / height - targetRatio);239if (diff < bestDiff) {240bestDiff = diff;241best = formatSize(width, height);242}243}244245return best;246}247248export function getQwen2SizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {249const normalizedQuality = normalizeQuality(quality);250251if (!ar) {252return QWEN_2_RECOMMENDED["1:1"][normalizedQuality];253}254255const recommendedRatio = findKnownRatioKey(ar, Object.keys(QWEN_2_RECOMMENDED));256if (recommendedRatio) {257return QWEN_2_RECOMMENDED[recommendedRatio][normalizedQuality];258}259260const parsed = parseAspectRatio(ar);261if (!parsed) {262return QWEN_2_RECOMMENDED["1:1"][normalizedQuality];263}264265const targetRatio = parsed.width / parsed.height;266const targetPixels = QWEN_2_TARGET_PIXELS[normalizedQuality];267const rawWidth = Math.sqrt(targetPixels * targetRatio);268const rawHeight = Math.sqrt(targetPixels / targetRatio);269const fitted = fitToPixelBudget(270rawWidth,271rawHeight,272MIN_QWEN_2_TOTAL_PIXELS,273MAX_QWEN_2_TOTAL_PIXELS,274);275276return formatSize(fitted.width, fitted.height);277}278279function getQwenFixedSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {280if (quality === "normal") {281console.warn(282"DashScope qwen-image-max/plus/image models use fixed output sizes; --quality normal does not change the generated resolution."283);284}285286if (!ar) return QWEN_FIXED_SPEC.defaultSize;287288const ratioKey = findKnownRatioKey(ar, Object.keys(QWEN_FIXED_SIZES_BY_RATIO));289if (!ratioKey) {290throw new Error(291`DashScope model supports only fixed ratios ${Object.keys(QWEN_FIXED_SIZES_BY_RATIO).join(", ")}. ` +292`For custom ratios like "${ar}", use --model qwen-image-2.0-pro.`293);294}295296return QWEN_FIXED_SIZES_BY_RATIO[ratioKey]!;297}298299function validateSizeFormat(size: string): { width: number; height: number } {300const parsed = parseSize(size);301if (!parsed) {302throw new Error(`Invalid DashScope size "${size}". Expected <width>x<height> or <width>*<height>.`);303}304return parsed;305}306307function validateQwen2Size(size: string): string {308const normalized = normalizeSize(size);309const parsed = validateSizeFormat(normalized);310const totalPixels = parsed.width * parsed.height;311if (totalPixels < MIN_QWEN_2_TOTAL_PIXELS || totalPixels > MAX_QWEN_2_TOTAL_PIXELS) {312throw new Error(313`DashScope qwen-image-2.0* models require total pixels between ${MIN_QWEN_2_TOTAL_PIXELS} ` +314`and ${MAX_QWEN_2_TOTAL_PIXELS}. Received ${normalized} (${totalPixels} pixels).`315);316}317return normalized;318}319320function validateQwenFixedSize(size: string): string {321const normalized = normalizeSize(size);322validateSizeFormat(normalized);323if (!QWEN_FIXED_SIZES.includes(normalized)) {324throw new Error(325`DashScope qwen-image-max/plus/image models support only these sizes: ${QWEN_FIXED_SIZES.join(", ")}. ` +326`Received ${normalized}.`327);328}329return normalized;330}331332export function resolveSizeForModel(333model: string,334args: Pick<CliArgs, "size" | "aspectRatio" | "quality">,335): string {336const spec = getModelSpec(model);337338if (args.size) {339if (spec.family === "qwen2") return validateQwen2Size(args.size);340if (spec.family === "qwenFixed") return validateQwenFixedSize(args.size);341validateSizeFormat(args.size);342return normalizeSize(args.size);343}344345if (spec.family === "qwen2") {346return getQwen2SizeFromAspectRatio(args.aspectRatio, args.quality);347}348349if (spec.family === "qwenFixed") {350return getQwenFixedSizeFromAspectRatio(args.aspectRatio, args.quality);351}352353return getSizeFromAspectRatio(args.aspectRatio, args.quality);354}355356function buildParameters(357family: DashScopeModelFamily,358size: string,359): Record<string, unknown> {360const parameters: Record<string, unknown> = {361prompt_extend: false,362size,363};364365if (family === "qwen2" || family === "qwenFixed") {366parameters.watermark = false;367parameters.negative_prompt = QWEN_NEGATIVE_PROMPT;368}369370return parameters;371}372373type DashScopeResponse = {374output?: {375result_image?: string;376choices?: Array<{377message?: {378content?: Array<{ image?: string }>;379};380}>;381};382};383384async function extractImageFromResponse(result: DashScopeResponse): Promise<Uint8Array> {385let imageData: string | null = null;386387if (result.output?.result_image) {388imageData = result.output.result_image;389} else if (result.output?.choices?.[0]?.message?.content) {390const content = result.output.choices[0].message.content;391for (const item of content) {392if (item.image) {393imageData = item.image;394break;395}396}397}398399if (!imageData) {400console.error("Response:", JSON.stringify(result, null, 2));401throw new Error("No image in response");402}403404if (imageData.startsWith("http://") || imageData.startsWith("https://")) {405const imgRes = await fetch(imageData);406if (!imgRes.ok) throw new Error("Failed to download image");407const buf = await imgRes.arrayBuffer();408return new Uint8Array(buf);409}410411return Uint8Array.from(Buffer.from(imageData, "base64"));412}413414export async function generateImage(415prompt: string,416model: string,417args: CliArgs418): Promise<Uint8Array> {419const apiKey = getApiKey();420if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");421422if (args.referenceImages.length > 0) {423throw new Error(424"Reference images are not supported with DashScope provider in baoyu-image-gen. Use --provider google with a Gemini multimodal model."425);426}427428const spec = getModelSpec(model);429const size = resolveSizeForModel(model, args);430const url = `${getBaseUrl()}/api/v1/services/aigc/multimodal-generation/generation`;431432const body = {433model,434input: {435messages: [436{437role: "user",438content: [{ text: prompt }],439},440],441},442parameters: buildParameters(spec.family, size),443};444445console.log(`Generating image with DashScope (${model})...`, { family: spec.family, size });446447const res = await fetch(url, {448method: "POST",449headers: {450"Content-Type": "application/json",451Authorization: `Bearer ${apiKey}`,452},453body: JSON.stringify(body),454});455456if (!res.ok) {457const err = await res.text();458throw new Error(`DashScope API error (${res.status}): ${err}`);459}460461const result = await res.json() as DashScopeResponse;462return extractImageFromResponse(result);463}464