jm / src /api /builders /payload-builder.ts
xt8's picture
Upload 50 files
124fc9e verified
import util from "@/lib/util.ts";
import { DRAFT_MIN_VERSION, DRAFT_VERSION, RESOLUTION_OPTIONS, RESOLUTION_OPTIONS_NANOBANANAPRO_4K } from "@/api/consts/common.ts";
import { RegionInfo, getAssistantId } from "@/api/controllers/core.ts";
export type RegionKey = "CN" | "US" | "HK" | "JP" | "SG";
export interface ResolutionResult {
width: number;
height: number;
imageRatio: number;
resolutionType: string;
isForced: boolean;
}
function getRegionKey(regionInfo: RegionInfo): RegionKey {
if (regionInfo.isUS) return "US";
if (regionInfo.isHK) return "HK";
if (regionInfo.isJP) return "JP";
if (regionInfo.isSG) return "SG";
return "CN";
}
function lookupResolution(resolution: string = "2k", ratio: string = "1:1", userModel?: string) {
// nanobananapro 模型使用 4k 时,使用专用配置
if (userModel === "nanobananapro" && resolution === "4k") {
const ratioConfig = RESOLUTION_OPTIONS_NANOBANANAPRO_4K[ratio];
if (!ratioConfig) {
const supportedRatios = Object.keys(RESOLUTION_OPTIONS_NANOBANANAPRO_4K).join(", ");
throw new Error(`nanobananapro 模型在 4k 分辨率下,不支持的比例 "${ratio}"。支持的比例: ${supportedRatios}`);
}
return {
width: ratioConfig.width,
height: ratioConfig.height,
imageRatio: ratioConfig.ratio,
resolutionType: resolution,
};
}
const resolutionGroup = RESOLUTION_OPTIONS[resolution];
if (!resolutionGroup) {
const supportedResolutions = Object.keys(RESOLUTION_OPTIONS).join(", ");
throw new Error(`不支持的分辨率 "${resolution}"。支持的分辨率: ${supportedResolutions}`);
}
const ratioConfig = resolutionGroup[ratio];
if (!ratioConfig) {
const supportedRatios = Object.keys(resolutionGroup).join(", ");
throw new Error(`在 "${resolution}" 分辨率下,不支持的比例 "${ratio}"。支持的比例: ${supportedRatios}`);
}
return {
width: ratioConfig.width,
height: ratioConfig.height,
imageRatio: ratioConfig.ratio,
resolutionType: resolution,
};
}
/**
* 统一分辨率处理逻辑
* - CN 站: 不支持 nano 系列模型 (nanobanana/nanobananapro),抛出异常
* - US 站 nanobanana: 强制 1024x1024 @ 2k,image_ratio=1
* - HK/JP/SG 站 nanobanana: 强制 1k 分辨率,但 ratio 可自定义
* - 所有站点 nanobananapro: resolution 和 ratio 都可自定义
*/
export function resolveResolution(
userModel: string,
regionInfo: RegionInfo,
resolution: string = "2k",
ratio: string = "1:1"
): ResolutionResult {
const regionKey = getRegionKey(regionInfo);
// ⚠️ 国内站不支持nano系列模型
if (regionKey === "CN" && (userModel === "nanobanana" || userModel === "nanobananapro")) {
throw new Error(
`国内站不支持${userModel}模型,请使用jimeng系列模型`
);
}
// ⚠️ nanobanana 模型的站点差异处理
if (userModel === "nanobanana") {
if (regionKey === "US") {
// US 站: 强制 1024x1024@2k, ratio 固定为 1
return {
width: 1024,
height: 1024,
imageRatio: 1,
resolutionType: "2k",
isForced: true,
};
} else if (regionKey === "HK" || regionKey === "JP" || regionKey === "SG") {
// HK/JP/SG 站: 强制 1k 分辨率,但 ratio 可自定义
const params = lookupResolution("1k", ratio, userModel);
return {
width: params.width,
height: params.height,
imageRatio: params.imageRatio,
resolutionType: "1k",
isForced: true,
};
}
}
// 其他所有情况: 使用用户指定的 resolution 和 ratio
const params = lookupResolution(resolution, ratio, userModel);
return {
...params,
isForced: false,
};
}
/**
* benefitCount 规则
* - 生图模式统一返回 4
* - 多图模式: 不加
*/
export function getBenefitCount(
userModel: string,
regionInfo: RegionInfo,
isMultiImage: boolean = false
): number | undefined {
if (isMultiImage) return undefined;
return 4;
}
export type GenerateMode = "text2img" | "img2img";
export interface BuildCoreParamOptions {
userModel: string; // 用户模型名(如 'jimeng-4.0', 'nanobanana')
model: string; // 映射后的内部模型名
prompt: string;
imageCount?: number; // 图生图时的图片数量,用于生成动态 ## 前缀
negativePrompt?: string;
seed?: number;
sampleStrength: number;
resolution: ResolutionResult;
intelligentRatio?: boolean;
mode?: GenerateMode;
}
/**
* 构建 core_param
* - 图生图: image_ratio 始终保留,prompt 前缀为 ## * imageCount
* - 文生图: intelligent_ratio=true 时移除 image_ratio
* - intelligent_ratio 仅对 jimeng-4.0/jimeng-4.1/jimeng-4.5 模型有效,其他模型忽略此参数
*/
export function buildCoreParam(options: BuildCoreParamOptions) {
const {
userModel,
model,
prompt,
imageCount = 0,
negativePrompt,
seed,
sampleStrength,
resolution,
intelligentRatio = false,
mode = "text2img",
} = options;
// ⚠️ intelligent_ratio 仅对 jimeng-4.0/jimeng-4.1/jimeng-4.5/jimeng-4.6/jimeng-5.0 模型有效
const effectiveIntelligentRatio = ['jimeng-4.0', 'jimeng-4.1', 'jimeng-4.5', 'jimeng-4.6', 'jimeng-5.0'].includes(userModel) ? intelligentRatio : false;
// 图生图时,prompt 前缀规则: 每张图片对应 2 个 #
// 1张图 → ##, 2张图 → ####, 3张图 → ######
const promptPrefix = mode === "img2img" ? '#'.repeat(imageCount * 2) : '';
const coreParam: any = {
type: "",
id: util.uuid(),
model,
prompt: `${promptPrefix}${prompt}`,
sample_strength: sampleStrength,
large_image_info: {
type: "",
id: util.uuid(),
min_version: DRAFT_MIN_VERSION,
height: resolution.height,
width: resolution.width,
resolution_type: resolution.resolutionType,
},
intelligent_ratio: effectiveIntelligentRatio,
};
if (mode === "img2img") {
coreParam.image_ratio = resolution.imageRatio;
} else if (!effectiveIntelligentRatio) {
coreParam.image_ratio = resolution.imageRatio;
}
if (negativePrompt !== undefined) {
coreParam.negative_prompt = negativePrompt;
}
if (seed !== undefined) {
coreParam.seed = seed;
}
return coreParam;
}
export type SceneType = "ImageBasicGenerate" | "ImageMultiGenerate";
/**
* metrics_extra 中 abilityList 的能力项
* - source.imageUrl: 前端使用 blob URL (如 blob:https://dreamina.capcut.com/[uuid])
* - 后端实现时需要生成占位符,保持 blob URL 格式
*/
interface Ability {
abilityName: string;
strength: number;
source?: {
imageUrl: string; // 格式: blob:https://dreamina.capcut.com/[uuid]
};
}
export interface BuildMetricsExtraOptions {
userModel: string;
model: string; // 映射后的内部模型名 (如 high_aes_general_v50)
regionInfo: RegionInfo;
submitId: string;
scene: SceneType;
resolutionType: string;
abilityList?: Ability[];
isMultiImage?: boolean;
}
/**
* 构建 metrics_extra,自动处理 benefitCount 站点差异 & 多图禁用
*/
export function buildMetricsExtra({
userModel,
model,
regionInfo,
submitId,
scene,
resolutionType,
abilityList = [],
isMultiImage = false,
}: BuildMetricsExtraOptions): string {
const benefitCount = getBenefitCount(userModel, regionInfo, isMultiImage);
const sceneOption: any = {
type: "image",
scene,
modelReqKey: model,
resolutionType,
abilityList,
reportParams: {
enterSource: "generate",
vipSource: "generate",
extraVipFunctionKey: `${model}-${resolutionType}`,
useVipFunctionDetailsReporterHoc: true,
},
};
if (benefitCount !== undefined) {
sceneOption.benefitCount = benefitCount;
}
const metrics: any = {
promptSource: "custom",
generateCount: 1,
enterFrom: "click",
sceneOptions: JSON.stringify([sceneOption]),
generateId: submitId,
isRegenerate: false,
};
if (isMultiImage) {
Object.assign(metrics, {
templateId: "",
templateSource: "",
lastRequestId: "",
originRequestId: "",
});
}
return JSON.stringify(metrics);
}
export interface BuildDraftContentOptions {
componentId: string;
generateType: "generate" | "blend";
coreParam: any;
abilityList?: any[];
promptPlaceholderInfoList?: any[];
posteditParam?: any;
imageCount?: number; // 图生图时的图片数量
}
export function buildDraftContent({
componentId,
generateType,
coreParam,
abilityList,
promptPlaceholderInfoList,
posteditParam,
imageCount = 0,
}: BuildDraftContentOptions): string {
const abilities: any = {
type: "",
id: util.uuid(),
};
// 图生图时,draft 和 blend 的 min_version 规则:
// - draft.min_version: 始终为 "3.2.9"
// - blend.min_version: 仅当 imageCount >= 2 时添加 "3.2.9"
const isBlend = generateType === "blend";
const draftMinVersion = isBlend ? "3.2.9" : DRAFT_MIN_VERSION;
if (generateType === "generate") {
abilities.generate = {
type: "",
id: util.uuid(),
core_param: coreParam,
gen_option: {
type: "",
id: util.uuid(),
generate_all: false,
},
};
} else {
abilities.blend = {
type: "",
id: util.uuid(),
...(imageCount >= 2 ? { min_version: "3.2.9" } : {}),
min_features: [],
core_param: coreParam,
ability_list: abilityList,
prompt_placeholder_info_list: promptPlaceholderInfoList,
postedit_param: posteditParam,
};
abilities.gen_option = {
type: "",
id: util.uuid(),
generate_all: false,
};
}
const draftContent = {
type: "draft",
id: util.uuid(),
min_version: draftMinVersion,
min_features: [],
is_from_tsn: true,
version: DRAFT_VERSION,
main_component_id: componentId,
component_list: [
{
type: "image_base_component",
id: componentId,
min_version: DRAFT_MIN_VERSION,
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: generateType,
abilities,
},
],
};
return JSON.stringify(draftContent);
}
export interface BuildGenerateRequestOptions {
model: string;
regionInfo: RegionInfo;
submitId: string;
draftContent: string;
metricsExtra: string;
}
export function buildGenerateRequest({
model,
regionInfo,
submitId,
draftContent,
metricsExtra,
}: BuildGenerateRequestOptions) {
return {
extend: {
root_model: model,
},
submit_id: submitId,
metrics_extra: metricsExtra,
draft_content: draftContent,
http_common_info: {
aid: getAssistantId(regionInfo),
},
};
}
export function buildBlendAbilityList(uploadedImageIds: string[], strength: number): any[] {
return 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,
}));
}
export function buildPromptPlaceholderList(count: number): any[] {
return Array.from({ length: count }, (_, index) => ({
type: "",
id: util.uuid(),
ability_index: index,
}));
}