mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-31 22:29:48 +08:00
fix(image-gen): tighten Jimeng provider behavior
This commit is contained in:
@@ -573,11 +573,10 @@ function detectProvider(args: CliArgs): Provider {
|
|||||||
args.provider !== "google" &&
|
args.provider !== "google" &&
|
||||||
args.provider !== "openai" &&
|
args.provider !== "openai" &&
|
||||||
args.provider !== "openrouter" &&
|
args.provider !== "openrouter" &&
|
||||||
args.provider !== "replicate" &&
|
args.provider !== "replicate"
|
||||||
args.provider !== "jimeng"
|
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"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."
|
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider openrouter (OpenRouter multimodal), or --provider replicate."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,9 +595,8 @@ function detectProvider(args: CliArgs): Provider {
|
|||||||
if (hasOpenai) return "openai";
|
if (hasOpenai) return "openai";
|
||||||
if (hasOpenrouter) return "openrouter";
|
if (hasOpenrouter) return "openrouter";
|
||||||
if (hasReplicate) return "replicate";
|
if (hasReplicate) return "replicate";
|
||||||
if (hasJimeng) return "jimeng";
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"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."
|
"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."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { CliArgs } from "../types";
|
import type { CliArgs } from "../types";
|
||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
|
|
||||||
|
type JimengSizePreset = "normal" | "2k" | "4k";
|
||||||
|
|
||||||
export function getDefaultModel(): string {
|
export function getDefaultModel(): string {
|
||||||
return process.env.JIMENG_IMAGE_MODEL || "jimeng_t2i_v40";
|
return process.env.JIMENG_IMAGE_MODEL || "jimeng_t2i_v40";
|
||||||
}
|
}
|
||||||
@@ -17,6 +19,34 @@ function getRegion(): string {
|
|||||||
return process.env.JIMENG_REGION || "cn-north-1";
|
return process.env.JIMENG_REGION || "cn-north-1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBaseUrl(): string {
|
||||||
|
return process.env.JIMENG_BASE_URL || "https://visual.volcengineapi.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEndpoint(query: Record<string, string>): {
|
||||||
|
url: string;
|
||||||
|
host: string;
|
||||||
|
canonicalUri: string;
|
||||||
|
} {
|
||||||
|
let baseUrl: URL;
|
||||||
|
try {
|
||||||
|
baseUrl = new URL(getBaseUrl());
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid JIMENG_BASE_URL: ${getBaseUrl()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrl.search = "";
|
||||||
|
for (const [key, value] of Object.entries(query).sort(([a], [b]) => a.localeCompare(b))) {
|
||||||
|
baseUrl.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: baseUrl.toString(),
|
||||||
|
host: baseUrl.host,
|
||||||
|
canonicalUri: baseUrl.pathname || "/",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Volcengine HMAC-SHA256 signature generation
|
* Volcengine HMAC-SHA256 signature generation
|
||||||
* Following the official documentation at:
|
* Following the official documentation at:
|
||||||
@@ -30,11 +60,10 @@ function generateSignature(
|
|||||||
accessKey: string,
|
accessKey: string,
|
||||||
secretKey: string,
|
secretKey: string,
|
||||||
region: string,
|
region: string,
|
||||||
service: string
|
service: string,
|
||||||
|
canonicalUri: string
|
||||||
): string {
|
): string {
|
||||||
// 1. Create canonical request
|
// 1. Create canonical request
|
||||||
const canonicalUri = "/";
|
|
||||||
|
|
||||||
// Sort query parameters alphabetically
|
// Sort query parameters alphabetically
|
||||||
const sortedQuery = Object.entries(query)
|
const sortedQuery = Object.entries(query)
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
@@ -70,8 +99,10 @@ function generateSignature(
|
|||||||
|
|
||||||
// 2. Create string to sign
|
// 2. Create string to sign
|
||||||
const algorithm = "HMAC-SHA256";
|
const algorithm = "HMAC-SHA256";
|
||||||
const now = new Date();
|
const timestamp = headers["X-Date"] || headers["x-date"];
|
||||||
const timestamp = now.toISOString().replace(/[:\-]|\.\d{3}/g, "");
|
if (!timestamp) {
|
||||||
|
throw new Error("Jimeng signature generation requires an X-Date header.");
|
||||||
|
}
|
||||||
const dateStamp = timestamp.slice(0, 8);
|
const dateStamp = timestamp.slice(0, 8);
|
||||||
|
|
||||||
const credentialScope = `${dateStamp}/${region}/${service}/request`;
|
const credentialScope = `${dateStamp}/${region}/${service}/request`;
|
||||||
@@ -99,9 +130,7 @@ function generateSignature(
|
|||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
// 4. Create authorization header
|
// 4. Create authorization header
|
||||||
const authorization = `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
return `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
||||||
|
|
||||||
return { authorization, timestamp };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,29 +173,22 @@ const SIZE_PRESETS: Record<string, Record<string, string>> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getImageSize(ar: string | null, quality: CliArgs["quality"], imageSize?: string | null): string {
|
function normalizeDimensions(value: string): string | null {
|
||||||
// If explicit size is provided, normalize it (replace * with x)
|
const match = value.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);
|
||||||
if (imageSize) {
|
if (!match) return null;
|
||||||
return imageSize.replace("*", "x");
|
return `${match[1]}x${match[2]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to 2K quality if not specified
|
function getClosestPresetSize(ar: string | null, qualityLevel: JimengSizePreset): string {
|
||||||
const qualityLevel = quality === "normal" ? "normal" : "2k";
|
|
||||||
const presets = SIZE_PRESETS[qualityLevel];
|
const presets = SIZE_PRESETS[qualityLevel];
|
||||||
|
const defaultSize = presets["1:1"]!;
|
||||||
|
|
||||||
// Default size
|
|
||||||
const defaultSize = qualityLevel === "normal" ? "1024x1024" : "2048x2048";
|
|
||||||
|
|
||||||
// If no aspect ratio, return default
|
|
||||||
if (!ar) return defaultSize;
|
if (!ar) return defaultSize;
|
||||||
|
|
||||||
// Parse aspect ratio and find closest match
|
|
||||||
const parsed = parseAspectRatio(ar);
|
const parsed = parseAspectRatio(ar);
|
||||||
if (!parsed) return defaultSize;
|
if (!parsed) return defaultSize;
|
||||||
|
|
||||||
const targetRatio = parsed.width / parsed.height;
|
const targetRatio = parsed.width / parsed.height;
|
||||||
|
|
||||||
// Find closest aspect ratio in presets
|
|
||||||
let bestMatch = defaultSize;
|
let bestMatch = defaultSize;
|
||||||
let bestDiff = Infinity;
|
let bestDiff = Infinity;
|
||||||
|
|
||||||
@@ -183,6 +205,25 @@ function getImageSize(ar: string | null, quality: CliArgs["quality"], imageSize?
|
|||||||
return bestMatch;
|
return bestMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeImageSizePreset(imageSize: string, ar: string | null): string | null {
|
||||||
|
const preset = imageSize.trim().toUpperCase();
|
||||||
|
if (preset === "1K") return getClosestPresetSize(ar, "normal");
|
||||||
|
if (preset === "2K") return getClosestPresetSize(ar, "2k");
|
||||||
|
if (preset === "4K") return getClosestPresetSize(ar, "4k");
|
||||||
|
return normalizeDimensions(imageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageSize(ar: string | null, quality: CliArgs["quality"], imageSize?: string | null): string {
|
||||||
|
if (imageSize) {
|
||||||
|
const normalizedSize = normalizeImageSizePreset(imageSize, ar);
|
||||||
|
if (normalizedSize) return normalizedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to 2K quality if not specified
|
||||||
|
const qualityLevel: JimengSizePreset = quality === "normal" ? "normal" : "2k";
|
||||||
|
return getClosestPresetSize(ar, qualityLevel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 1: Submit async task to Volcengine Jimeng API
|
* Step 1: Submit async task to Volcengine Jimeng API
|
||||||
*/
|
*/
|
||||||
@@ -194,13 +235,12 @@ async function submitTask(
|
|||||||
secretKey: string,
|
secretKey: string,
|
||||||
region: string
|
region: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const baseUrl = "https://visual.volcengineapi.com";
|
|
||||||
|
|
||||||
// Query parameters for submit endpoint
|
// Query parameters for submit endpoint
|
||||||
const query = {
|
const query = {
|
||||||
Action: "CVSync2AsyncSubmitTask",
|
Action: "CVSync2AsyncSubmitTask",
|
||||||
Version: "2022-08-31",
|
Version: "2022-08-31",
|
||||||
};
|
};
|
||||||
|
const endpoint = resolveEndpoint(query);
|
||||||
|
|
||||||
// Request body - Jimeng API expects width/height as separate integers
|
// Request body - Jimeng API expects width/height as separate integers
|
||||||
const [width, height] = size.split("x").map(Number);
|
const [width, height] = size.split("x").map(Number);
|
||||||
@@ -221,11 +261,11 @@ async function submitTask(
|
|||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Date": timestampHeader,
|
"X-Date": timestampHeader,
|
||||||
"Host": "visual.volcengineapi.com",
|
"Host": endpoint.host,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate signature
|
// Generate signature
|
||||||
const { authorization, timestamp } = generateSignature(
|
const authorization = generateSignature(
|
||||||
"POST",
|
"POST",
|
||||||
query,
|
query,
|
||||||
headers,
|
headers,
|
||||||
@@ -233,15 +273,13 @@ async function submitTask(
|
|||||||
accessKey,
|
accessKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
region,
|
region,
|
||||||
"cv"
|
"cv",
|
||||||
|
endpoint.canonicalUri
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build URL with query parameters
|
|
||||||
const url = `${baseUrl}/?Action=${query.Action}&Version=${query.Version}`;
|
|
||||||
|
|
||||||
console.error(`Submitting task to Jimeng (${model})...`, { width, height });
|
console.error(`Submitting task to Jimeng (${model})...`, { width, height });
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(endpoint.url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
@@ -283,7 +321,6 @@ async function pollForResult(
|
|||||||
secretKey: string,
|
secretKey: string,
|
||||||
region: string
|
region: string
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const baseUrl = "https://visual.volcengineapi.com";
|
|
||||||
const maxAttempts = 60;
|
const maxAttempts = 60;
|
||||||
const pollIntervalMs = 2000;
|
const pollIntervalMs = 2000;
|
||||||
|
|
||||||
@@ -293,6 +330,7 @@ async function pollForResult(
|
|||||||
Action: "CVSync2AsyncGetResult",
|
Action: "CVSync2AsyncGetResult",
|
||||||
Version: "2022-08-31",
|
Version: "2022-08-31",
|
||||||
};
|
};
|
||||||
|
const endpoint = resolveEndpoint(query);
|
||||||
|
|
||||||
// Request body - include req_key and task_id
|
// Request body - include req_key and task_id
|
||||||
const bodyObj = {
|
const bodyObj = {
|
||||||
@@ -307,11 +345,11 @@ async function pollForResult(
|
|||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Date": timestampHeader,
|
"X-Date": timestampHeader,
|
||||||
"Host": "visual.volcengineapi.com",
|
"Host": endpoint.host,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate signature
|
// Generate signature
|
||||||
const { authorization } = generateSignature(
|
const authorization = generateSignature(
|
||||||
"POST",
|
"POST",
|
||||||
query,
|
query,
|
||||||
headers,
|
headers,
|
||||||
@@ -319,13 +357,11 @@ async function pollForResult(
|
|||||||
accessKey,
|
accessKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
region,
|
region,
|
||||||
"cv"
|
"cv",
|
||||||
|
endpoint.canonicalUri
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build URL with query parameters
|
const res = await fetch(endpoint.url, {
|
||||||
const url = `${baseUrl}/?Action=${query.Action}&Version=${query.Version}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
@@ -401,6 +437,12 @@ export async function generateImage(
|
|||||||
model: string,
|
model: string,
|
||||||
args: CliArgs
|
args: CliArgs
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
|
if (args.referenceImages.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Jimeng does not support reference images. Use --provider google, openai, openrouter, or replicate."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const accessKey = getAccessKey();
|
const accessKey = getAccessKey();
|
||||||
const secretKey = getSecretKey();
|
const secretKey = getSecretKey();
|
||||||
const region = getRegion();
|
const region = getRegion();
|
||||||
|
|||||||
Reference in New Issue
Block a user