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/jimeng.ts
1import type { CliArgs } from "../types";2import * as crypto from "node:crypto";34type JimengSizePreset = "normal" | "2k" | "4k";56export function getDefaultModel(): string {7return process.env.JIMENG_IMAGE_MODEL || "jimeng_t2i_v40";8}910function getAccessKey(): string | null {11return process.env.JIMENG_ACCESS_KEY_ID || null;12}1314function getSecretKey(): string | null {15return process.env.JIMENG_SECRET_ACCESS_KEY || null;16}1718function getRegion(): string {19return process.env.JIMENG_REGION || "cn-north-1";20}2122function getBaseUrl(): string {23return process.env.JIMENG_BASE_URL || "https://visual.volcengineapi.com";24}2526function resolveEndpoint(query: Record<string, string>): {27url: string;28host: string;29canonicalUri: string;30} {31let baseUrl: URL;32try {33baseUrl = new URL(getBaseUrl());34} catch {35throw new Error(`Invalid JIMENG_BASE_URL: ${getBaseUrl()}`);36}3738baseUrl.search = "";39for (const [key, value] of Object.entries(query).sort(([a], [b]) => a.localeCompare(b))) {40baseUrl.searchParams.set(key, value);41}4243return {44url: baseUrl.toString(),45host: baseUrl.host,46canonicalUri: baseUrl.pathname || "/",47};48}4950/**51* Volcengine HMAC-SHA256 signature generation52* Following the official documentation at:53* https://www.volcengine.com/docs/85621/181704554*/55function generateSignature(56method: string,57query: Record<string, string>,58headers: Record<string, string>,59body: string,60accessKey: string,61secretKey: string,62region: string,63service: string,64canonicalUri: string65): string {66// 1. Create canonical request67// Sort query parameters alphabetically68const sortedQuery = Object.entries(query)69.sort(([a], [b]) => a.localeCompare(b))70.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)71.join("&");7273// Sort headers alphabetically and create canonical headers74const sortedHeaders = Object.entries(headers)75.sort(([a], [b]) => a.localeCompare(b))76.map(([k, v]) => `${k.toLowerCase()}:${v.trim()}\n`)77.join("");7879const signedHeaders = Object.keys(headers)80.sort()81.map(k => k.toLowerCase())82.join(";");8384const hashedPayload = crypto.createHash("sha256").update(body, "utf8").digest("hex");8586const canonicalRequest = [87method,88canonicalUri,89sortedQuery,90sortedHeaders,91signedHeaders,92hashedPayload,93].join("\n");9495const hashedCanonicalRequest = crypto96.createHash("sha256")97.update(canonicalRequest, "utf8")98.digest("hex");99100// 2. Create string to sign101const algorithm = "HMAC-SHA256";102const timestamp = headers["X-Date"] || headers["x-date"];103if (!timestamp) {104throw new Error("Jimeng signature generation requires an X-Date header.");105}106const dateStamp = timestamp.slice(0, 8);107108const credentialScope = `${dateStamp}/${region}/${service}/request`;109110const stringToSign = [111algorithm,112timestamp,113credentialScope,114hashedCanonicalRequest,115].join("\n");116117// 3. Calculate signature118const kDate = crypto119.createHmac("sha256", secretKey)120.update(dateStamp)121.digest();122123const kRegion = crypto.createHmac("sha256", kDate).update(region).digest();124const kService = crypto.createHmac("sha256", kRegion).update(service).digest();125const kSigning = crypto.createHmac("sha256", kService).update("request").digest();126127const signature = crypto128.createHmac("sha256", kSigning)129.update(stringToSign)130.digest("hex");131132// 4. Create authorization header133return `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;134}135136/**137* Parse aspect ratio string like "16:9", "1:1", "4:3" into width and height138*/139function parseAspectRatio(ar: string): { width: number; height: number } | null {140const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);141if (!match) return null;142const w = parseFloat(match[1]!);143const h = parseFloat(match[2]!);144if (w <= 0 || h <= 0) return null;145return { width: w, height: h };146}147148/**149* Supported size presets for different quality levels150* Based on Volcengine Jimeng documentation151*/152const SIZE_PRESETS: Record<string, Record<string, string>> = {153normal: {154"1:1": "1024x1024",155"4:3": "1360x1020",156"16:9": "1536x864",157"3:2": "1440x960",158"21:9": "1920x824",159},160"2k": {161"1:1": "2048x2048",162"4:3": "2304x1728",163"16:9": "2560x1440",164"3:2": "2496x1664",165"21:9": "3024x1296",166},167"4k": {168"1:1": "4096x4096",169"4:3": "4694x3520",170"16:9": "5404x3040",171"3:2": "4992x3328",172"21:9": "6198x2656",173},174};175176function normalizeDimensions(value: string): string | null {177const match = value.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);178if (!match) return null;179return `${match[1]}x${match[2]}`;180}181182function getClosestPresetSize(ar: string | null, qualityLevel: JimengSizePreset): string {183const presets = SIZE_PRESETS[qualityLevel];184const defaultSize = presets["1:1"]!;185186if (!ar) return defaultSize;187188const parsed = parseAspectRatio(ar);189if (!parsed) return defaultSize;190191const targetRatio = parsed.width / parsed.height;192let bestMatch = defaultSize;193let bestDiff = Infinity;194195for (const [ratio, size] of Object.entries(presets)) {196const [w, h] = ratio.split(":").map(Number);197const presetRatio = w / h;198const diff = Math.abs(presetRatio - targetRatio);199if (diff < bestDiff) {200bestDiff = diff;201bestMatch = size;202}203}204205return bestMatch;206}207208function normalizeImageSizePreset(imageSize: string, ar: string | null): string | null {209const preset = imageSize.trim().toUpperCase();210if (preset === "1K") return getClosestPresetSize(ar, "normal");211if (preset === "2K") return getClosestPresetSize(ar, "2k");212if (preset === "4K") return getClosestPresetSize(ar, "4k");213return normalizeDimensions(imageSize);214}215216function getImageSize(ar: string | null, quality: CliArgs["quality"], imageSize?: string | null): string {217if (imageSize) {218const normalizedSize = normalizeImageSizePreset(imageSize, ar);219if (normalizedSize) return normalizedSize;220}221222// Default to 2K quality if not specified223const qualityLevel: JimengSizePreset = quality === "normal" ? "normal" : "2k";224return getClosestPresetSize(ar, qualityLevel);225}226227/**228* Step 1: Submit async task to Volcengine Jimeng API229*/230async function submitTask(231prompt: string,232model: string,233size: string,234accessKey: string,235secretKey: string,236region: string237): Promise<string> {238// Query parameters for submit endpoint239const query = {240Action: "CVSync2AsyncSubmitTask",241Version: "2022-08-31",242};243const endpoint = resolveEndpoint(query);244245// Request body - Jimeng API expects width/height as separate integers246const [width, height] = size.split("x").map(Number);247const bodyObj = {248req_key: model,249prompt,250// Use separate width and height parameters instead of size string251width: width,252height: height,253// Optional: seed for reproducibility254// seed: Math.floor(Math.random() * 999999),255};256257const body = JSON.stringify(bodyObj);258259// Headers260const timestampHeader = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, "");261const headers = {262"Content-Type": "application/json",263"X-Date": timestampHeader,264"Host": endpoint.host,265};266267// Generate signature268const authorization = generateSignature(269"POST",270query,271headers,272body,273accessKey,274secretKey,275region,276"cv",277endpoint.canonicalUri278);279280console.error(`Submitting task to Jimeng (${model})...`, { width, height });281282const res = await fetch(endpoint.url, {283method: "POST",284headers: {285...headers,286"Authorization": authorization,287},288body,289});290291if (!res.ok) {292const err = await res.text();293throw new Error(`Jimeng API submit error (${res.status}): ${err}`);294}295296const result = (await res.json()) as {297code?: number;298message?: string;299data?: {300task_id?: string;301};302};303304// Volcengine API returns code 10000 for success305if (result.code !== 10000 || !result.data?.task_id) {306console.error("Submit response:", JSON.stringify(result, null, 2));307throw new Error(`Failed to submit task: ${result.message || "Unknown error"}`);308}309310return result.data.task_id;311}312313/**314* Step 2: Poll for task result315* Returns image data directly as Uint8Array316*/317async function pollForResult(318taskId: string,319model: string,320accessKey: string,321secretKey: string,322region: string323): Promise<Uint8Array> {324const maxAttempts = 60;325const pollIntervalMs = 2000;326327for (let attempt = 0; attempt < maxAttempts; attempt++) {328// Query parameters for result endpoint329const query = {330Action: "CVSync2AsyncGetResult",331Version: "2022-08-31",332};333const endpoint = resolveEndpoint(query);334335// Request body - include req_key and task_id336const bodyObj = {337req_key: model,338task_id: taskId,339};340341const body = JSON.stringify(bodyObj);342343// Headers344const timestampHeader = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, "");345const headers = {346"Content-Type": "application/json",347"X-Date": timestampHeader,348"Host": endpoint.host,349};350351// Generate signature352const authorization = generateSignature(353"POST",354query,355headers,356body,357accessKey,358secretKey,359region,360"cv",361endpoint.canonicalUri362);363364const res = await fetch(endpoint.url, {365method: "POST",366headers: {367...headers,368"Authorization": authorization,369},370body,371});372373if (!res.ok) {374const err = await res.text();375throw new Error(`Jimeng API poll error (${res.status}): ${err}`);376}377378const result = (await res.json()) as {379code?: number;380message?: string;381data?: {382status?: string;383image_urls?: string[];384binary_data_base64?: string[];385};386};387388// Volcengine API returns code 10000 for success389if (result.code === 10000 && result.data) {390const { status, image_urls, binary_data_base64 } = result.data;391392// Check for base64 image data (preferred by Jimeng)393if (binary_data_base64 && binary_data_base64.length > 0) {394console.error("Image received as base64 data");395const base64Data = binary_data_base64[0]!;396// Convert base64 to Uint8Array397const binaryString = Buffer.from(base64Data, "base64").toString("binary");398const bytes = new Uint8Array(binaryString.length);399for (let i = 0; i < binaryString.length; i++) {400bytes[i] = binaryString.charCodeAt(i);401}402return bytes;403}404405// Fallback to URL format406if (status === "done" && image_urls && image_urls.length > 0) {407// Download from URL408console.error(`Downloading image from ${image_urls[0]}...`);409const imgRes = await fetch(image_urls[0]!);410if (!imgRes.ok) {411throw new Error(`Failed to download image from ${image_urls[0]}`);412}413const buffer = await imgRes.arrayBuffer();414return new Uint8Array(buffer);415}416417if (status === "in_queue" || status === "generating") {418console.error(`Task status: ${status} (${attempt + 1}/${maxAttempts})`);419await new Promise(resolve => setTimeout(resolve, pollIntervalMs));420continue;421}422423if (status === "fail") {424throw new Error(`Jimeng task failed: ${result.message || "Generation failed"}`);425}426}427428console.error("Poll response:", JSON.stringify(result, null, 2));429throw new Error(`Unexpected response during polling: ${result.message || "Unknown error"}`);430}431432throw new Error("Task timeout: image generation took too long");433}434435export async function generateImage(436prompt: string,437model: string,438args: CliArgs439): Promise<Uint8Array> {440if (args.referenceImages.length > 0) {441throw new Error(442"Jimeng does not support reference images. Use --provider google, openai, openrouter, or replicate."443);444}445446const accessKey = getAccessKey();447const secretKey = getSecretKey();448const region = getRegion();449450if (!accessKey || !secretKey) {451throw new Error(452"JIMENG_ACCESS_KEY_ID and JIMENG_SECRET_ACCESS_KEY are required. " +453"Get your credentials from https://console.volcengine.com/iam/keymanage"454);455}456457const size = getImageSize(args.aspectRatio, args.quality, args.imageSize);458459// Step 1: Submit task460const taskId = await submitTask(prompt, model, size, accessKey, secretKey, region);461462// Step 2: Poll for result (returns image data directly)463const imageData = await pollForResult(taskId, model, accessKey, secretKey, region);464465console.error("Image generation complete!");466return imageData;467}468