jm / src /api /controllers /images.ts
ricebug's picture
Upload 45 files
63453d7 verified
import _ from "lodash";
import crypto from "crypto";
import APIException from "@/lib/exceptions/APIException.ts";
import EX from "@/api/consts/exceptions.ts";
import util from "@/lib/util.ts";
import { getCredit, receiveCredit, request } from "./core.ts";
import logger from "@/lib/logger.ts";
const DEFAULT_ASSISTANT_ID = "513695";
export const DEFAULT_MODEL = "jimeng-4.0";
const DRAFT_VERSION = "3.0.2";
const MODEL_MAP = {
"jimeng-4.0": "high_aes_general_v40",
"jimeng-3.1": "high_aes_general_v30l_art_fangzhou:general_v3.0_18b",
"jimeng-3.0": "high_aes_general_v30l:general_v3.0_18b",
"jimeng-2.1": "high_aes_general_v21_L:general_v2.1_L",
"jimeng-2.0-pro": "high_aes_general_v20_L:general_v2.0_L",
"jimeng-2.0": "high_aes_general_v20:general_v2.0",
"jimeng-1.4": "high_aes_general_v14:general_v1.4",
"jimeng-xl-pro": "text2img_xl_sft",
};
export function getModel(model: string) {
return MODEL_MAP[model] || MODEL_MAP[DEFAULT_MODEL];
}
// AWS4-HMAC-SHA256 签名生成函数
function createSignature(
method: string,
url: string,
headers: { [key: string]: string },
accessKeyId: string,
secretAccessKey: string,
sessionToken?: string,
payload: string = ''
) {
const urlObj = new URL(url);
const pathname = urlObj.pathname || '/';
const search = urlObj.search;
// 创建规范请求
const timestamp = headers['x-amz-date'];
const date = timestamp.substr(0, 8);
const region = 'cn-north-1';
const service = 'imagex';
// 规范化查询参数 - 手动处理以确保正确的顺序
const queryParams: Array<[string, string]> = [];
const searchParams = new URLSearchParams(search);
searchParams.forEach((value, key) => {
queryParams.push([key, value]);
});
// 按键名排序 - 大小写敏感,先大写字母,后小写字母
queryParams.sort(([a], [b]) => {
// AWS要求大小写敏感的ASCII排序
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
// 构建规范查询字符串(不进行额外编码,因为URL中已经编码)
const canonicalQueryString = queryParams
.map(([key, value]) => `${key}=${value}`)
.join('&');
// 规范化头部 - 只包含必要的头部
const headersToSign: { [key: string]: string } = {
'x-amz-date': timestamp
};
// 添加 session token
if (sessionToken) {
headersToSign['x-amz-security-token'] = sessionToken;
}
// 如果是POST请求且包含payload,添加content-sha256头
let payloadHash = crypto.createHash('sha256').update('').digest('hex'); // 默认空payload
if (method.toUpperCase() === 'POST' && payload) {
payloadHash = crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
headersToSign['x-amz-content-sha256'] = payloadHash;
}
const signedHeaders = Object.keys(headersToSign)
.map(key => key.toLowerCase())
.sort()
.join(';');
const canonicalHeaders = Object.keys(headersToSign)
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map(key => `${key.toLowerCase()}:${headersToSign[key].trim()}\n`)
.join('');
// 创建规范请求
const canonicalRequest = [
method.toUpperCase(),
pathname,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
payloadHash
].join('\n');
// 调试输出
logger.debug(`规范请求:
Method: ${method.toUpperCase()}
Path: ${pathname}
Query: ${canonicalQueryString}
Headers: ${canonicalHeaders}
SignedHeaders: ${signedHeaders}
PayloadHash: ${payloadHash}
---完整规范请求---
${canonicalRequest}
---结束---`);
// 创建待签名字符串
const credentialScope = `${date}/${region}/${service}/aws4_request`;
const stringToSign = [
'AWS4-HMAC-SHA256',
timestamp,
credentialScope,
crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex')
].join('\n');
logger.debug(`待签名字符串:
${stringToSign}`);
// 生成签名
const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(date).digest();
const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
const kService = crypto.createHmac('sha256', kRegion).update(service).digest();
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex');
return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}
// 计算文件的CRC32值
function calculateCRC32(buffer: ArrayBuffer): string {
const crcTable = [];
for (let i = 0; i < 256; i++) {
let crc = i;
for (let j = 0; j < 8; j++) {
crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
}
crcTable[i] = crc;
}
let crc = 0 ^ (-1);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
}
return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
}
// 图片上传功能:将外部图片URL上传到即梦系统
async function uploadImageFromUrl(imageUrl: string, refreshToken: string): Promise<string> {
try {
logger.info(`开始上传图片: ${imageUrl}`);
// 第一步:获取上传令牌
const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
data: {
scene: 2, // AIGC 图片上传场景
},
});
const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
if (!access_key_id || !secret_access_key || !session_token) {
throw new Error("获取上传令牌失败");
}
// 使用固定的service_id
const actualServiceId = service_id || "tb4s082cfz";
logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
// 下载图片数据
const imageResponse = await fetch(imageUrl);
if (!imageResponse.ok) {
throw new Error(`下载图片失败: ${imageResponse.status}`);
}
const imageBuffer = await imageResponse.arrayBuffer();
const fileSize = imageBuffer.byteLength;
const crc32 = calculateCRC32(imageBuffer);
logger.info(`图片下载完成: 大小=${fileSize}字节, CRC32=${crc32}`);
// 第二步:申请图片上传权限
// 使用UTC时间格式 YYYYMMDD'T'HHMMSS'Z'
const now = new Date();
const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
// 生成随机字符串作为签名参数
const randomStr = Math.random().toString(36).substring(2, 12);
// 保持原始的参数顺序(这是API期望的顺序)
const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`;
logger.debug(`原始URL: ${applyUrl}`);
// 构建AWS签名所需的头部
const requestHeaders = {
'x-amz-date': timestamp,
'x-amz-security-token': session_token
};
// 生成AWS签名
const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
// 调试日志
logger.info(`AWS签名调试信息:
URL: ${applyUrl}
AccessKeyId: ${access_key_id}
SessionToken: ${session_token ? '存在' : '不存在'}
Timestamp: ${timestamp}
Authorization: ${authorization}
`);
const applyResponse = await fetch(applyUrl, {
method: 'GET',
headers: {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9',
'authorization': authorization,
'origin': 'https://jimeng.jianying.com',
'referer': 'https://jimeng.jianying.com/ai-tool/generate',
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
'x-amz-date': timestamp,
'x-amz-security-token': session_token,
},
});
if (!applyResponse.ok) {
const errorText = await applyResponse.text();
throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
}
const applyResult = await applyResponse.json();
// 检查是否有错误
if (applyResult?.ResponseMetadata?.Error) {
throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
}
logger.info(`申请上传权限成功`);
// 解析上传信息
const uploadAddress = applyResult?.Result?.UploadAddress;
if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
}
const storeInfo = uploadAddress.StoreInfos[0];
const uploadHost = uploadAddress.UploadHosts[0];
const auth = storeInfo.Auth;
// 构建上传URL
const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
// 提取图片ID (StoreUri最后一个斜杠后的部分)
const imageId = storeInfo.StoreUri.split('/').pop();
logger.info(`准备上传图片: imageId=${imageId}, uploadUrl=${uploadUrl}`);
// 第三步:上传图片文件
const uploadResponse = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Authorization': auth,
'Connection': 'keep-alive',
'Content-CRC32': crc32,
'Content-Disposition': 'attachment; filename="undefined"',
'Content-Type': 'application/octet-stream',
'Origin': 'https://jimeng.jianying.com',
'Referer': 'https://jimeng.jianying.com/ai-tool/generate',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
'X-Storage-U': '704135154117550', // 用户ID,可以从token或其他地方获取
},
body: imageBuffer,
});
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
}
logger.info(`图片文件上传成功`);
// 第四步:提交上传
const commitUrl = `https://imagex.bytedanceapi.com/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
const commitPayload = JSON.stringify({
SessionKey: uploadAddress.SessionKey,
SuccessActionStatus: "200"
});
// 计算payload的SHA256哈希值
const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
// 构建AWS签名所需的头部
const commitRequestHeaders = {
'x-amz-date': commitTimestamp,
'x-amz-security-token': session_token,
'x-amz-content-sha256': payloadHash
};
// 生成AWS签名
const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
const commitResponse = await fetch(commitUrl, {
method: 'POST',
headers: {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9',
'authorization': commitAuthorization,
'content-type': 'application/json',
'origin': 'https://jimeng.jianying.com',
'referer': 'https://jimeng.jianying.com/ai-tool/generate',
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
'x-amz-date': commitTimestamp,
'x-amz-security-token': session_token,
'x-amz-content-sha256': payloadHash,
},
body: commitPayload,
});
if (!commitResponse.ok) {
const errorText = await commitResponse.text();
throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
}
const commitResult = await commitResponse.json();
// 检查提交结果
if (commitResult?.ResponseMetadata?.Error) {
throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
}
if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
}
const uploadResult = commitResult.Result.Results[0];
if (uploadResult.UriStatus !== 2000) {
throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
}
// 获取完整的URI(包含前缀)
const fullImageUri = uploadResult.Uri; // 如: "tos-cn-i-tb4s082cfz/bab623359bd9410da0c1f07897b16fec"
// 验证图片信息
const pluginResult = commitResult.Result?.PluginResult?.[0];
if (pluginResult) {
logger.info(`图片上传成功详情:`, {
imageUri: pluginResult.ImageUri,
sourceUri: pluginResult.SourceUri,
size: `${pluginResult.ImageWidth}x${pluginResult.ImageHeight}`,
format: pluginResult.ImageFormat,
fileSize: pluginResult.ImageSize,
md5: pluginResult.ImageMd5
});
// 优先使用PluginResult中的ImageUri,因为它可能是最准确的
if (pluginResult.ImageUri) {
logger.info(`图片上传完成: ${pluginResult.ImageUri}`);
return pluginResult.ImageUri; // 返回完整的URI
}
}
logger.info(`图片上传完成: ${fullImageUri}`);
return fullImageUri; // 返回完整的URI
} catch (error) {
logger.error(`图片上传失败: ${error.message}`);
throw error;
}
}
// 图片合成功能:先上传图片,然后进行图生图
export async function generateImageComposition(
_model: string,
prompt: string,
imageUrls: string[],
{
width = 2560,
height = 1440,
sampleStrength = 0.5,
negativePrompt = "",
}: {
width?: number;
height?: number;
sampleStrength?: number;
negativePrompt?: string;
},
refreshToken: string
) {
const model = getModel(_model);
const imageCount = imageUrls.length;
logger.info(`使用模型: ${_model} 映射模型: ${model} 图生图功能 ${imageCount}张图片 ${width}x${height} 精细度: ${sampleStrength}`);
const { totalCredit } = await getCredit(refreshToken);
if (totalCredit <= 0)
await receiveCredit(refreshToken);
// 上传所有输入图片
const uploadedImageIds: string[] = [];
for (let i = 0; i < imageUrls.length; i++) {
try {
const imageId = await uploadImageFromUrl(imageUrls[i], refreshToken);
uploadedImageIds.push(imageId);
logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
} catch (error) {
logger.error(`图片 ${i + 1}/${imageCount} 上传失败: ${error.message}`);
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `图片上传失败: ${error.message}`);
}
}
logger.info(`所有图片上传完成,开始图生图: ${uploadedImageIds.join(', ')}`);
const componentId = util.uuid();
const submitId = util.uuid();
const { aigc_data } = await request(
"post",
"/mweb/v1/aigc_draft/generate",
refreshToken,
{
params: {
babi_param: encodeURIComponent(
JSON.stringify({
scenario: "image_video_generation",
feature_key: "aigc_to_image",
feature_entrance: "to_image",
feature_entrance_detail: "to_image-" + model,
})
),
},
data: {
extend: {
root_model: model,
},
submit_id: submitId,
metrics_extra: JSON.stringify({
promptSource: "custom",
generateCount: 1,
enterFrom: "click",
generateId: submitId,
isRegenerate: false
}),
draft_content: JSON.stringify({
type: "draft",
id: util.uuid(),
min_version: "3.2.9",
min_features: [],
is_from_tsn: true,
version: "3.2.9",
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: "3.0.2",
aigc_mode: "workbench",
metadata: {
type: "",
id: util.uuid(),
created_platform: 3,
created_platform_version: "",
created_time_in_ms: Date.now().toString(),
created_did: "",
},
generate_type: "blend",
abilities: {
type: "",
id: util.uuid(),
blend: {
type: "",
id: util.uuid(),
min_version: "3.2.9",
min_features: [],
core_param: {
type: "",
id: util.uuid(),
model,
prompt: `####${prompt}`,
sample_strength: sampleStrength,
image_ratio: 1,
large_image_info: {
type: "",
id: util.uuid(),
height: 2048,
width: 2048,
resolution_type: "2k"
},
intelligent_ratio: false,
},
ability_list: uploadedImageIds.map((imageId) => ({
type: "",
id: util.uuid(),
name: "byte_edit",
image_uri_list: [imageId],
image_list: [{
type: "image",
id: util.uuid(),
source_from: "upload",
platform_type: 1,
name: "",
image_uri: imageId,
width: 0,
height: 0,
format: "",
uri: imageId
}],
strength: 0.5
})),
prompt_placeholder_info_list: uploadedImageIds.map((_, index) => ({
type: "",
id: util.uuid(),
ability_index: index
})),
postedit_param: {
type: "",
id: util.uuid(),
generate_type: 0
}
},
},
},
],
}),
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
}
);
const historyId = aigc_data?.history_record_id;
if (!historyId)
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
logger.info(`图生图任务已提交,history_id: ${historyId},等待生成完成...`);
let status = 20, failCode, item_list = [];
let pollCount = 0;
const maxPollCount = 600; // 最多轮询10分钟
while (pollCount < maxPollCount) {
await new Promise((resolve) => setTimeout(resolve, 1000));
pollCount++;
if (pollCount % 30 === 0) {
logger.info(`图生图进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length} 张图片...`);
}
const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
data: {
history_ids: [historyId],
image_info: {
width: 2048,
height: 2048,
format: "webp",
image_scene_list: [
{
scene: "smart_crop",
width: 360,
height: 360,
uniq_key: "smart_crop-w:360-h:360",
format: "webp",
},
{
scene: "smart_crop",
width: 480,
height: 480,
uniq_key: "smart_crop-w:480-h:480",
format: "webp",
},
{
scene: "smart_crop",
width: 720,
height: 720,
uniq_key: "smart_crop-w:720-h:720",
format: "webp",
},
{
scene: "smart_crop",
width: 720,
height: 480,
uniq_key: "smart_crop-w:720-h:480",
format: "webp",
},
{
scene: "normal",
width: 2400,
height: 2400,
uniq_key: "2400",
format: "webp",
},
{
scene: "normal",
width: 1080,
height: 1080,
uniq_key: "1080",
format: "webp",
},
{
scene: "normal",
width: 720,
height: 720,
uniq_key: "720",
format: "webp",
},
{
scene: "normal",
width: 480,
height: 480,
uniq_key: "480",
format: "webp",
},
{
scene: "normal",
width: 360,
height: 360,
uniq_key: "360",
format: "webp",
},
],
},
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
});
if (!result[historyId])
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
status = result[historyId].status;
failCode = result[historyId].fail_code;
item_list = result[historyId].item_list || [];
// 检查是否已生成图片
if (item_list.length > 0) {
logger.info(`图生图完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
break;
}
// 记录详细状态
if (pollCount % 60 === 0) {
logger.info(`图生图详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
}
// 如果状态是完成但图片数量为0,记录并继续等待
if (status === 10 && item_list.length === 0 && pollCount % 30 === 0) {
logger.info(`图生图状态已完成但无图片生成: 状态=${status}, 继续等待...`);
}
}
if (pollCount >= maxPollCount) {
logger.warn(`图生图超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
}
if (status === 30) {
if (failCode === '2038')
throw new APIException(EX.API_CONTENT_FILTERED);
else
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `图生图失败,错误代码: ${failCode}`);
}
const resultImageUrls = item_list.map((item) => {
if(!item?.image?.large_images?.[0]?.image_url)
return item?.common_attr?.cover_url || null;
return item.image.large_images[0].image_url;
}).filter(url => url !== null);
logger.info(`图生图结果: 成功生成 ${resultImageUrls.length} 张图片`);
return resultImageUrls;
}
// jimeng-4.0 专用的多图生成函数
async function generateJimeng40MultiImages(
_model: string,
prompt: string,
{
width = 1024,
height = 1024,
sampleStrength = 0.5,
negativePrompt = "",
}: {
width?: number;
height?: number;
sampleStrength?: number;
negativePrompt?: string;
},
refreshToken: string
) {
const model = getModel(_model);
// 从prompt中提取图片数量,默认为4张
const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
logger.info(`使用 jimeng-4.0 多图生成: ${targetImageCount}张图片 ${width}x${height} 精细度: ${sampleStrength}`);
const componentId = util.uuid();
const submitId = util.uuid(); // 生成 submit_id
const { aigc_data } = await request(
"post",
"/mweb/v1/aigc_draft/generate",
refreshToken,
{
params: {
babi_param: encodeURIComponent(
JSON.stringify({
scenario: "image_video_generation",
feature_key: "aigc_to_image",
feature_entrance: "to_image",
feature_entrance_detail: "to_image-" + model,
})
),
},
data: {
extend: {
root_model: model,
template_id: "",
},
submit_id: submitId, // 使用生成的 submit_id
metrics_extra: JSON.stringify({
templateId: "",
generateCount: 1,
promptSource: "custom",
templateSource: "",
lastRequestId: "",
originRequestId: "",
}),
draft_content: JSON.stringify({
type: "draft",
id: util.uuid(),
min_version: DRAFT_VERSION,
is_from_tsn: true,
version: DRAFT_VERSION,
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: DRAFT_VERSION,
generate_type: "generate",
aigc_mode: "workbench",
abilities: {
type: "",
id: util.uuid(),
generate: {
type: "",
id: util.uuid(),
core_param: {
type: "",
id: util.uuid(),
model,
prompt,
negative_prompt: negativePrompt,
seed: Math.floor(Math.random() * 100000000) + 2500000000,
sample_strength: sampleStrength,
image_ratio: 1,
large_image_info: {
type: "",
id: util.uuid(),
height,
width,
},
},
history_option: {
type: "",
id: util.uuid(),
},
},
},
},
],
}),
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
}
);
const historyId = aigc_data?.history_record_id;
if (!historyId)
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
logger.info(`jimeng-4.0 多图生成任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成 ${targetImageCount} 张图片...`);
// 直接使用 history_id 轮询生成结果(增加轮询时间)
let status = 20, failCode, item_list = [];
let pollCount = 0;
const maxPollCount = 600; // 最多轮询10分钟(600次 * 1秒)
while (pollCount < maxPollCount) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // 每1秒轮询一次
pollCount++;
if (pollCount % 30 === 0) {
logger.info(`jimeng-4.0 多图生成进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length}/${targetImageCount} 张图片...`);
}
const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
data: {
history_ids: [historyId],
image_info: {
width: 2048,
height: 2048,
format: "webp",
image_scene_list: [
{
scene: "smart_crop",
width: 360,
height: 360,
uniq_key: "smart_crop-w:360-h:360",
format: "webp",
},
{
scene: "smart_crop",
width: 480,
height: 480,
uniq_key: "smart_crop-w:480-h:480",
format: "webp",
},
{
scene: "smart_crop",
width: 720,
height: 720,
uniq_key: "smart_crop-w:720-h:720",
format: "webp",
},
{
scene: "smart_crop",
width: 720,
height: 480,
uniq_key: "smart_crop-w:720-h:480",
format: "webp",
},
{
scene: "normal",
width: 2400,
height: 2400,
uniq_key: "2400",
format: "webp",
},
{
scene: "normal",
width: 1080,
height: 1080,
uniq_key: "1080",
format: "webp",
},
{
scene: "normal",
width: 720,
height: 720,
uniq_key: "720",
format: "webp",
},
{
scene: "normal",
width: 480,
height: 480,
uniq_key: "480",
format: "webp",
},
{
scene: "normal",
width: 360,
height: 360,
uniq_key: "360",
format: "webp",
},
],
},
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
});
if (!result[historyId])
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
status = result[historyId].status;
failCode = result[historyId].fail_code;
item_list = result[historyId].item_list || [];
// 检查是否已生成足够的图片
if (item_list.length >= targetImageCount) {
logger.info(`jimeng-4.0 多图生成完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
break;
}
// 记录详细状态
if (pollCount % 60 === 0) {
logger.info(`jimeng-4.0 详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
}
// 如果状态是完成但图片数量不够,记录并继续等待
if (status === 10 && item_list.length < targetImageCount && pollCount % 30 === 0) {
logger.info(`jimeng-4.0 状态已完成但图片数量不足: 状态=${status}, 已生成 ${item_list.length}/${targetImageCount} 张图片,继续等待...`);
}
}
if (pollCount >= maxPollCount) {
logger.warn(`jimeng-4.0 多图生成超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
}
if (status === 30) {
if (failCode === '2038')
throw new APIException(EX.API_CONTENT_FILTERED);
else
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误代码: ${failCode}`);
}
const imageUrls = item_list.map((item) => {
if(!item?.image?.large_images?.[0]?.image_url)
return item?.common_attr?.cover_url || null;
return item.image.large_images[0].image_url;
}).filter(url => url !== null);
logger.info(`jimeng-4.0 多图生成结果: 成功生成 ${imageUrls.length} 张图片`);
return imageUrls;
}
export async function generateImages(
_model: string,
prompt: string,
{
width = 1024,
height = 1024,
sampleStrength = 0.5,
negativePrompt = "",
}: {
width?: number;
height?: number;
sampleStrength?: number;
negativePrompt?: string;
},
refreshToken: string
) {
const model = getModel(_model);
logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} 精细度: ${sampleStrength}`);
const { totalCredit } = await getCredit(refreshToken);
if (totalCredit <= 0)
await receiveCredit(refreshToken);
// 检测是否为 jimeng-4.0 的多图生成请求
const isJimeng40MultiImage = _model === "jimeng-4.0" && (
prompt.includes("连续") ||
prompt.includes("绘本") ||
prompt.includes("故事") ||
/\d+张/.test(prompt)
);
// 如果是 jimeng-4.0 的多图请求,使用专门的处理逻辑
if (isJimeng40MultiImage) {
return await generateJimeng40MultiImages(_model, prompt, { width, height, sampleStrength, negativePrompt }, refreshToken);
}
const componentId = util.uuid();
const { aigc_data } = await request(
"post",
"/mweb/v1/aigc_draft/generate",
refreshToken,
{
params: {
babi_param: encodeURIComponent(
JSON.stringify({
scenario: "image_video_generation",
feature_key: "aigc_to_image",
feature_entrance: "to_image",
feature_entrance_detail: "to_image-" + model,
})
),
},
data: {
extend: {
root_model: model,
template_id: "",
},
submit_id: util.uuid(),
metrics_extra: JSON.stringify({
templateId: "",
generateCount: 1,
promptSource: "custom",
templateSource: "",
lastRequestId: "",
originRequestId: "",
}),
draft_content: JSON.stringify({
type: "draft",
id: util.uuid(),
min_version: DRAFT_VERSION,
is_from_tsn: true,
version: DRAFT_VERSION,
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: DRAFT_VERSION,
generate_type: "generate",
aigc_mode: "workbench",
abilities: {
type: "",
id: util.uuid(),
generate: {
type: "",
id: util.uuid(),
core_param: {
type: "",
id: util.uuid(),
model,
prompt,
negative_prompt: negativePrompt,
seed: Math.floor(Math.random() * 100000000) + 2500000000,
sample_strength: sampleStrength,
image_ratio: 1,
large_image_info: {
type: "",
id: util.uuid(),
height,
width,
},
},
history_option: {
type: "",
id: util.uuid(),
},
},
},
},
],
}),
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
}
);
const historyId = aigc_data.history_record_id;
if (!historyId)
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
logger.info(`文生图任务已提交,history_id: ${historyId},等待生成完成...`);
let status = 20, failCode, item_list = [];
let pollCount = 0;
const maxPollCount = 600; // 最多轮询10分钟
while (pollCount < maxPollCount) {
await new Promise((resolve) => setTimeout(resolve, 1000));
pollCount++;
if (pollCount % 30 === 0) {
logger.info(`文生图进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length} 张图片...`);
}
const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
data: {
history_ids: [historyId],
image_info: {
width: 2048,
height: 2048,
format: "webp",
image_scene_list: [
{
scene: "smart_crop",
width: 360,
height: 360,
uniq_key: "smart_crop-w:360-h:360",
format: "webp",
},
{
scene: "smart_crop",
width: 480,
height: 480,
uniq_key: "smart_crop-w:480-h:480",
format: "webp",
},
{
scene: "smart_crop",
width: 720,
height: 720,
uniq_key: "smart_crop-w:720-h:720",
format: "webp",
},
{
scene: "smart_crop",
width: 720,
height: 480,
uniq_key: "smart_crop-w:720-h:480",
format: "webp",
},
{
scene: "smart_crop",
width: 360,
height: 240,
uniq_key: "smart_crop-w:360-h:240",
format: "webp",
},
{
scene: "smart_crop",
width: 240,
height: 320,
uniq_key: "smart_crop-w:240-h:320",
format: "webp",
},
{
scene: "smart_crop",
width: 480,
height: 640,
uniq_key: "smart_crop-w:480-h:640",
format: "webp",
},
{
scene: "normal",
width: 2400,
height: 2400,
uniq_key: "2400",
format: "webp",
},
{
scene: "normal",
width: 1080,
height: 1080,
uniq_key: "1080",
format: "webp",
},
{
scene: "normal",
width: 720,
height: 720,
uniq_key: "720",
format: "webp",
},
{
scene: "normal",
width: 480,
height: 480,
uniq_key: "480",
format: "webp",
},
{
scene: "normal",
width: 360,
height: 360,
uniq_key: "360",
format: "webp",
},
],
},
http_common_info: {
aid: Number(DEFAULT_ASSISTANT_ID),
},
},
});
if (!result[historyId])
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
status = result[historyId].status;
failCode = result[historyId].fail_code;
item_list = result[historyId].item_list || [];
// 检查是否已生成图片
if (item_list.length > 0) {
logger.info(`文生图完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
break;
}
// 记录详细状态
if (pollCount % 60 === 0) {
logger.info(`文生图详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
}
// 如果状态是完成但图片数量为0,记录并继续等待
if (status === 10 && item_list.length === 0 && pollCount % 30 === 0) {
logger.info(`文生图状态已完成但无图片生成: 状态=${status}, 继续等待...`);
}
}
if (pollCount >= maxPollCount) {
logger.warn(`文生图超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
}
if (status === 30) {
if (failCode === '2038')
throw new APIException(EX.API_CONTENT_FILTERED);
else
throw new APIException(EX.API_IMAGE_GENERATION_FAILED);
}
const imageUrls = item_list.map((item) => {
if(!item?.image?.large_images?.[0]?.image_url)
return item?.common_attr?.cover_url || null;
return item.image.large_images[0].image_url;
}).filter(url => url !== null);
logger.info(`文生图结果: 成功生成 ${imageUrls.length} 张图片`);
return imageUrls;
}
export default {
generateImages,
generateImageComposition,
};