| 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]; |
| } |
|
|
|
|
| |
| 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]) => { |
| |
| if (a < b) return -1; |
| if (a > b) return 1; |
| return 0; |
| }); |
| |
| |
| const canonicalQueryString = queryParams |
| .map(([key, value]) => `${key}=${value}`) |
| .join('&'); |
| |
| |
| const headersToSign: { [key: string]: string } = { |
| 'x-amz-date': timestamp |
| }; |
| |
| |
| if (sessionToken) { |
| headersToSign['x-amz-security-token'] = sessionToken; |
| } |
| |
| |
| let payloadHash = crypto.createHash('sha256').update('').digest('hex'); |
| 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}`; |
| } |
|
|
| |
| 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'); |
| } |
|
|
| |
| 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, |
| }, |
| }); |
| |
| const { access_key_id, secret_access_key, session_token, service_id } = tokenResult; |
| if (!access_key_id || !secret_access_key || !session_token) { |
| throw new Error("获取上传令牌失败"); |
| } |
| |
| |
| 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}`); |
| |
| |
| |
| const now = new Date(); |
| const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z'); |
| |
| |
| const randomStr = Math.random().toString(36).substring(2, 12); |
| |
| const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`; |
| |
| logger.debug(`原始URL: ${applyUrl}`); |
| |
| |
| const requestHeaders = { |
| 'x-amz-date': timestamp, |
| 'x-amz-security-token': session_token |
| }; |
| |
| |
| 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; |
| |
| |
| const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.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', |
| }, |
| 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" |
| }); |
| |
| |
| const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex'); |
| |
| |
| const commitRequestHeaders = { |
| 'x-amz-date': commitTimestamp, |
| 'x-amz-security-token': session_token, |
| 'x-amz-content-sha256': payloadHash |
| }; |
| |
| |
| 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}`); |
| } |
| |
| |
| const fullImageUri = uploadResult.Uri; |
| |
| |
| 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 |
| }); |
| |
| |
| if (pluginResult.ImageUri) { |
| logger.info(`图片上传完成: ${pluginResult.ImageUri}`); |
| return pluginResult.ImageUri; |
| } |
| } |
| |
| logger.info(`图片上传完成: ${fullImageUri}`); |
| return fullImageUri; |
| |
| } 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; |
|
|
| 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'}`); |
| } |
| |
| |
| 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; |
| } |
|
|
| |
| 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); |
| |
| |
| 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(); |
| |
| 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, |
| 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} 张图片...`); |
|
|
| |
| let status = 20, failCode, item_list = []; |
| let pollCount = 0; |
| const maxPollCount = 600; |
|
|
| while (pollCount < maxPollCount) { |
| await new Promise((resolve) => setTimeout(resolve, 1000)); |
| 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); |
|
|
| |
| const isJimeng40MultiImage = _model === "jimeng-4.0" && ( |
| prompt.includes("连续") || |
| prompt.includes("绘本") || |
| prompt.includes("故事") || |
| /\d+张/.test(prompt) |
| ); |
|
|
| |
| 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; |
|
|
| 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'}`); |
| } |
|
|
| |
| 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, |
| }; |
|
|