import type { ImageAnnotationBounds } from "@/lib/api"; export type ImageEditAnnotation = { id: string; label: string; instruction: string; bounds: ImageAnnotationBounds; selectionType?: "box" | "point"; point?: { x: number; y: number }; visibleText?: string; description?: string; }; export type ImageEditorGenerationSettings = { count: number; model: string; quality?: string; resolution?: string; thirdPartyImageGroupId?: string; }; export type ImageEditorAction = ( | { type: "annotations"; annotations: ImageEditAnnotation[]; /** 被编辑图片自身的宽高比(整数格式,如 "2:3"),局部修改时输出应保持该比例 */ imageRatio?: string; } | { type: "expand"; aspectRatio: string; instruction?: string; systemPrompt: string; fillColor: string; referenceImageDataUrl: string; referenceImageWidth: number; referenceImageHeight: number; maskImageDataUrl: string; } ) & ImageEditorGenerationSettings; function percent(value: number) { return `${Math.max(0, Math.min(100, value)).toFixed(1)}%`; } /** 把扩图参考画布的像素宽高整理成 `宽x高` 形式,尺寸非法时返回空字符串。 */ export function toExpansionReferenceSize(width?: number, height?: number) { const parsedWidth = Math.round(Number(width) || 0); const parsedHeight = Math.round(Number(height) || 0); if (parsedWidth <= 0 || parsedHeight <= 0) return ""; return `${parsedWidth}x${parsedHeight}`; } export function buildImageAnnotationEditPrompt(annotations: ImageEditAnnotation[]) { const actionable = annotations.filter((item) => item.instruction.trim()); const items = actionable.map((item, index) => { const centerX = item.bounds.x + item.bounds.width / 2; const centerY = item.bounds.y + item.bounds.height / 2; const pointX = item.point?.x ?? centerX; const pointY = item.point?.y ?? centerY; const details = item.selectionType === "point" ? [ `${index + 1}. 点选目标:${item.label.trim() || "用户点选位置"}`, ` 点选位置:(x: ${percent(pointX)}, y: ${percent(pointY)})`, ` 识别参考范围:(x: ${percent(item.bounds.x)}, y: ${percent(item.bounds.y)}, w: ${percent(item.bounds.width)}, h: ${percent(item.bounds.height)})`, ] : [ `${index + 1}. 框选目标:${item.label.trim() || "用户框选区域"}`, ` 区域中心:(x: ${percent(centerX)}, y: ${percent(centerY)})`, ` 区域范围:(x: ${percent(item.bounds.x)}, y: ${percent(item.bounds.y)}, w: ${percent(item.bounds.width)}, h: ${percent(item.bounds.height)})`, ]; if (item.visibleText?.trim()) { details.push(` 当前可见文字:${item.visibleText.trim()}`); } if (item.description?.trim()) { details.push(` 视觉说明:${item.description.trim()}`); } details.push(` 修改:${item.instruction.trim()}`); return details.join("\n"); }); return [ "请编辑图像1。坐标以原图左上角为 (0%, 0%),右下角为 (100%, 100%)。", "以下每一项都对应用户在原图上明确点选或框选的目标:", "", items.join("\n\n"), "", "严格只修改以上点选目标或框选区域;未标记的商品、人物、文字、构图、颜色、光线和背景保持不变。", "不要在成品中保留编号、点位、选框、坐标或任何标注界面元素。", ].join("\n"); } export const DEFAULT_IMAGE_EXPANSION_FILL_COLOR = "#ffb6d9"; const IMAGE_EXPANSION_COLOR_NAMES: Record = { "#ffffff": "白色", "#000000": "黑色", "#808080": "灰色", "#ff0000": "红色", "#00ff00": "绿色", "#0000ff": "蓝色", [DEFAULT_IMAGE_EXPANSION_FILL_COLOR]: "粉色", }; export function describeImageExpansionFillColor(fillColor: string) { const normalized = fillColor.trim().toLowerCase(); return IMAGE_EXPANSION_COLOR_NAMES[normalized] || `颜色为 ${normalized.toUpperCase()} 的纯色`; } export function buildDefaultImageExpansionPrompt(fillColor = DEFAULT_IMAGE_EXPANSION_FILL_COLOR) { const colorDescription = describeImageExpansionFillColor(fillColor); return `第一张参考图是扩展画布,其中${colorDescription}色块是需要补全的新增区域。透明蒙版可能作为第二张参考图提供,也可能通过 mask 字段提供;蒙版的透明区域与这些色块对应,是唯一允许填充的区域,不透明区域必须保持不变。根据原图内容自然填充新增区域,保持原图的清晰程度、色调和颜色不变,新增区域和原图的衔接要自然`; } export const DEFAULT_IMAGE_EXPANSION_PROMPT = buildDefaultImageExpansionPrompt(); export function buildImageExpansionPrompt( systemPrompt: string, instruction?: string, fillColor = DEFAULT_IMAGE_EXPANSION_FILL_COLOR, aspectRatio?: string, referenceWidth?: number, referenceHeight?: number, ) { const normalizedSystemPrompt = systemPrompt.trim() || buildDefaultImageExpansionPrompt(fillColor); const extraInstruction = instruction?.trim(); const parts = [normalizedSystemPrompt]; const referenceSize = toExpansionReferenceSize(referenceWidth, referenceHeight); if (referenceSize) { // 用「参考画布的实际像素尺寸」约束比例,而不是 16:9 / 404:281 这类比例字符串: // 比例字符串会被后端换算成 1024 长边的标准尺寸,与扩图画布的真实宽高对不上, // 生成结果会被二次裁切,导致扩图内容错位。 parts.push( `输出图片必须严格保持 ${referenceSize} 的像素尺寸与宽高比,不要拉伸、压缩或裁切画面,也不要改变画布比例。`, ); } else if (aspectRatio?.trim()) { parts.push(`输出图片的宽高比必须为 ${aspectRatio.trim()},请严格按照这个尺寸比例生成。`); } if (extraInstruction) { parts.push(`补充要求:${extraInstruction}`); } return parts.join("\n\n"); }