study / web /src /app /image /components /image-annotation.ts
xiaolongmr
添加编辑
638cad0
Raw History Blame Contribute Delete
6.05 kB
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<string, string> = {
"#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");
}