fix: preserve auto image ratio for agnes edits
This commit is contained in:
parent
2b96fdb3e7
commit
038730a24b
@ -11,6 +11,8 @@ const filePartSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
url: z.string().url(),
|
||||
assetId: z.string().uuid().optional(),
|
||||
width: z.number().positive().optional(),
|
||||
height: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
const partSchema = z.union([textPartSchema, filePartSchema]);
|
||||
|
||||
@ -1810,13 +1810,23 @@ export function CanvasChatPanel({
|
||||
const getCanvasNodeAttachment = useCallback(
|
||||
(node: any, fallbackName: string): PickedObjectAttachment | null => {
|
||||
if (isCanvasImageUploadPending(node)) return null;
|
||||
const src = (node?.data as any)?.src;
|
||||
const nodeData = (node?.data as any) || {};
|
||||
const src = nodeData.src;
|
||||
if (!src || typeof src !== "string" || !isValidChatHttpUrl(src)) return null;
|
||||
|
||||
const nodeTitle =
|
||||
String((node?.data as any)?.title || "").trim() ||
|
||||
String((node?.data as any)?.prompt || "").trim() ||
|
||||
String(nodeData.title || "").trim() ||
|
||||
String(nodeData.prompt || "").trim() ||
|
||||
fallbackName;
|
||||
const metadata = (nodeData.metadata || {}) as Record<string, unknown>;
|
||||
const width =
|
||||
Number(metadata.originalWidth || 0) ||
|
||||
Number(node?.size?.width || 0) ||
|
||||
0;
|
||||
const height =
|
||||
Number(metadata.originalHeight || 0) ||
|
||||
Number(node?.size?.height || 0) ||
|
||||
0;
|
||||
|
||||
return {
|
||||
url: src,
|
||||
@ -1826,6 +1836,8 @@ export function CanvasChatPanel({
|
||||
typeof (node?.data as any)?.assetId === "string" && (node?.data as any)?.assetId.trim()
|
||||
? String((node?.data as any)?.assetId).trim()
|
||||
: undefined,
|
||||
width: Number.isFinite(width) && width > 0 ? Math.round(width) : undefined,
|
||||
height: Number.isFinite(height) && height > 0 ? Math.round(height) : undefined,
|
||||
};
|
||||
},
|
||||
[]
|
||||
@ -2079,11 +2091,31 @@ export function CanvasChatPanel({
|
||||
currentAttachments.length + i
|
||||
);
|
||||
|
||||
const pickedNode = nodes.find((node) => node.id === picked.imageNodeId);
|
||||
const pickedNodeMetadata =
|
||||
((pickedNode?.data as any)?.metadata || {}) as Record<string, unknown>;
|
||||
const pickedWidth =
|
||||
Number(pickedNodeMetadata.originalWidth || 0) ||
|
||||
Number(pickedNode?.size?.width || 0) ||
|
||||
0;
|
||||
const pickedHeight =
|
||||
Number(pickedNodeMetadata.originalHeight || 0) ||
|
||||
Number(pickedNode?.size?.height || 0) ||
|
||||
0;
|
||||
|
||||
// 添加到附件列表(带物品信息,原图片 URL 会发送给 AI 用于多图融合编辑)
|
||||
currentAttachments.push({
|
||||
url: picked.imageUrl, // 原图片 URL
|
||||
name: `${picked.objectName}-${picked.objectPosition}`, // 物品名称-位置
|
||||
contentType: 'image/jpeg',
|
||||
width:
|
||||
Number.isFinite(pickedWidth) && pickedWidth > 0
|
||||
? Math.round(pickedWidth)
|
||||
: undefined,
|
||||
height:
|
||||
Number.isFinite(pickedHeight) && pickedHeight > 0
|
||||
? Math.round(pickedHeight)
|
||||
: undefined,
|
||||
pickedObject: {
|
||||
objectName: picked.objectName,
|
||||
objectPosition: picked.objectPosition,
|
||||
@ -2105,7 +2137,7 @@ export function CanvasChatPanel({
|
||||
// 清空待添加列表
|
||||
clearPendingPickedObjects();
|
||||
}
|
||||
}, [pendingPickedObjects, attachments, insertPickedObjectTagInEditor, clearPendingPickedObjects]);
|
||||
}, [pendingPickedObjects, attachments, insertPickedObjectTagInEditor, clearPendingPickedObjects, nodes]);
|
||||
|
||||
// 🆕 标签悬停预览 - 使用全局事件监听
|
||||
useEffect(() => {
|
||||
@ -4594,13 +4626,19 @@ export function CanvasChatPanel({
|
||||
return;
|
||||
}
|
||||
|
||||
const parts: any[] = launchAttachments.map((attachment) => ({
|
||||
const parts: any[] = launchAttachments.map((attachment) => {
|
||||
const width = Number(attachment.width);
|
||||
const height = Number(attachment.height);
|
||||
return {
|
||||
type: "file" as const,
|
||||
mediaType: normalizeChatMediaType(attachment.contentType),
|
||||
name: normalizeChatAttachmentName(attachment.name),
|
||||
url: attachment.url,
|
||||
assetId: isValidChatUuid(attachment.assetId) ? attachment.assetId : undefined,
|
||||
}));
|
||||
width: Number.isFinite(width) && width > 0 ? Math.round(width) : undefined,
|
||||
height: Number.isFinite(height) && height > 0 ? Math.round(height) : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
if (textContent) {
|
||||
parts.push({ type: "text" as const, text: textContent });
|
||||
@ -7694,12 +7732,22 @@ export function CanvasChatPanel({
|
||||
|
||||
effectiveAttachments.forEach((a) => {
|
||||
const att = a as PickedObjectAttachment;
|
||||
const attachmentWidth = Number(att.width);
|
||||
const attachmentHeight = Number(att.height);
|
||||
parts.push({
|
||||
type: "file" as const,
|
||||
mediaType: normalizeChatMediaType(a.contentType),
|
||||
name: normalizeChatAttachmentName(a.name),
|
||||
url: a.url,
|
||||
assetId: isValidChatUuid(a.assetId) ? a.assetId : undefined,
|
||||
width:
|
||||
Number.isFinite(attachmentWidth) && attachmentWidth > 0
|
||||
? Math.round(attachmentWidth)
|
||||
: undefined,
|
||||
height:
|
||||
Number.isFinite(attachmentHeight) && attachmentHeight > 0
|
||||
? Math.round(attachmentHeight)
|
||||
: undefined,
|
||||
hasAnnotations: hasAnnotatedImages, // 🆕 标记图片是否有标注
|
||||
pickedObject: att.pickedObject, // 🆕 包含物品信息
|
||||
});
|
||||
@ -7989,7 +8037,14 @@ export function CanvasChatPanel({
|
||||
const uploadImageFileAndAddToCanvas = useCallback(async (
|
||||
file: File,
|
||||
nodeIndex: number
|
||||
): Promise<{ imageUrl: string; name: string; contentType: string; assetId?: string } | null> => {
|
||||
): Promise<{
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
contentType: string;
|
||||
assetId?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
} | null> => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('请选择图片文件');
|
||||
return null;
|
||||
@ -8046,6 +8101,8 @@ export function CanvasChatPanel({
|
||||
name: fileName,
|
||||
contentType: file.type || 'image/jpeg',
|
||||
assetId,
|
||||
width: dimensions.originalWidth || dimensions.width,
|
||||
height: dimensions.originalHeight || dimensions.height,
|
||||
};
|
||||
} catch (_error) {
|
||||
toast.error('图片上传失败');
|
||||
@ -8077,6 +8134,8 @@ export function CanvasChatPanel({
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
assetId: uploaded.assetId,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
};
|
||||
updateAttachmentAtAgentSlot(targetSlotIndex, attachment);
|
||||
currentSlots[targetSlotIndex] = attachment;
|
||||
@ -8117,6 +8176,8 @@ export function CanvasChatPanel({
|
||||
url: uploaded.imageUrl,
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
};
|
||||
updateAttachmentAtVideoSlot(targetSlotIndex, attachment);
|
||||
currentSlots[targetSlotIndex] = attachment;
|
||||
@ -8161,6 +8222,9 @@ export function CanvasChatPanel({
|
||||
url: uploaded.imageUrl,
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
assetId: uploaded.assetId,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -8212,6 +8276,8 @@ export function CanvasChatPanel({
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
assetId: uploaded.assetId,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
});
|
||||
|
||||
currentSlots[targetSlotIndex] = {
|
||||
@ -8219,6 +8285,8 @@ export function CanvasChatPanel({
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
assetId: uploaded.assetId,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
};
|
||||
handledCount += 1;
|
||||
uploadIndex += 1;
|
||||
@ -8270,6 +8338,7 @@ export function CanvasChatPanel({
|
||||
const imageUrl = data.url;
|
||||
const assetId =
|
||||
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
|
||||
const dimensions = await getImageDimensions(imageUrl);
|
||||
|
||||
// 记录第一张图片的 URL(用于自动命名和缩略图)
|
||||
if (!firstImageUrl) {
|
||||
@ -8289,11 +8358,12 @@ export function CanvasChatPanel({
|
||||
name: file.name,
|
||||
contentType: file.type,
|
||||
assetId,
|
||||
width: dimensions.originalWidth || dimensions.width,
|
||||
height: dimensions.originalHeight || dimensions.height,
|
||||
}];
|
||||
});
|
||||
|
||||
// 获取图片原始尺寸后添加到画布
|
||||
const dimensions = await getImageDimensions(imageUrl);
|
||||
// 使用 baseNodeCount + uploadIndex 计算位置,确保多张图片不重叠
|
||||
const nodeIndex = baseNodeCount + uploadIndex;
|
||||
const spacing = 20;
|
||||
|
||||
@ -83,6 +83,10 @@ export function sanitizeChatAttachment(
|
||||
if (!attachment || !isValidChatHttpUrl(attachment.url)) {
|
||||
return null;
|
||||
}
|
||||
const width = Number(attachment.width);
|
||||
const height = Number(attachment.height);
|
||||
const safeWidth = Number.isFinite(width) && width > 0 ? Math.round(width) : undefined;
|
||||
const safeHeight = Number.isFinite(height) && height > 0 ? Math.round(height) : undefined;
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
@ -90,6 +94,8 @@ export function sanitizeChatAttachment(
|
||||
name: normalizeChatAttachmentName(attachment.name),
|
||||
contentType: normalizeChatMediaType(attachment.contentType),
|
||||
assetId: isValidChatUuid(attachment.assetId) ? attachment.assetId : undefined,
|
||||
width: safeWidth,
|
||||
height: safeHeight,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -4505,10 +4505,27 @@ export function ImageNode({
|
||||
displayTitle
|
||||
);
|
||||
await animateAddToChat(attachment.url);
|
||||
const metadata = (data.metadata || {}) as Record<string, unknown>;
|
||||
const sourceWidth =
|
||||
Number(metadata.originalWidth || 0) ||
|
||||
Number(node.size?.width || 0) ||
|
||||
0;
|
||||
const sourceHeight =
|
||||
Number(metadata.originalHeight || 0) ||
|
||||
Number(node.size?.height || 0) ||
|
||||
0;
|
||||
addPendingChatAttachment({
|
||||
url: attachment.url,
|
||||
name: displayTitle,
|
||||
contentType: attachment.contentType,
|
||||
width:
|
||||
Number.isFinite(sourceWidth) && sourceWidth > 0
|
||||
? Math.round(sourceWidth)
|
||||
: undefined,
|
||||
height:
|
||||
Number.isFinite(sourceHeight) && sourceHeight > 0
|
||||
? Math.round(sourceHeight)
|
||||
: undefined,
|
||||
});
|
||||
toast.success("已添加到对话框");
|
||||
} catch (error) {
|
||||
@ -4520,10 +4537,13 @@ export function ImageNode({
|
||||
}, [
|
||||
addPendingChatAttachment,
|
||||
animateAddToChat,
|
||||
data.metadata,
|
||||
data.src,
|
||||
displayTitle,
|
||||
isAddingToChat,
|
||||
isProcessing,
|
||||
node.size?.height,
|
||||
node.size?.width,
|
||||
]);
|
||||
|
||||
const getFloatingViewportBounds = (toolbarHeight: number) => {
|
||||
|
||||
@ -26,6 +26,12 @@ interface UploadedImage {
|
||||
original: string;
|
||||
}
|
||||
|
||||
type InputImageReference = {
|
||||
url: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_IMAGE_SIZE = "2K";
|
||||
const DEFAULT_TIMEOUT_MS = 600000;
|
||||
const LONG_RUNNING_COMFLY_TIMEOUT_MS = 600000;
|
||||
@ -173,6 +179,20 @@ function inferClosestAspectRatio(aspectValue: number) {
|
||||
return closest;
|
||||
}
|
||||
|
||||
function inferAspectRatioFromDimensions(width?: number, height?: number) {
|
||||
const safeWidth = Number(width);
|
||||
const safeHeight = Number(height);
|
||||
if (
|
||||
!Number.isFinite(safeWidth) ||
|
||||
!Number.isFinite(safeHeight) ||
|
||||
safeWidth <= 0 ||
|
||||
safeHeight <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return inferClosestAspectRatio(safeWidth / safeHeight);
|
||||
}
|
||||
|
||||
async function inferAspectRatioFromImageUrl(imageUrl: string): Promise<string | null> {
|
||||
const normalized = String(imageUrl || "").trim();
|
||||
if (!normalized) return null;
|
||||
@ -419,14 +439,20 @@ function extractPromptAndImages(messages: Array<{ role: string; content: any }>)
|
||||
}
|
||||
|
||||
let prompt = "";
|
||||
const imageUrls: string[] = [];
|
||||
const imageRefs: InputImageReference[] = [];
|
||||
|
||||
const pushImageUrl = (value?: string) => {
|
||||
const pushImageUrl = (value?: string, geometry?: { width?: unknown; height?: unknown }) => {
|
||||
if (!value) return;
|
||||
const normalized = String(value).trim();
|
||||
if (!normalized) return;
|
||||
if (!imageUrls.includes(normalized)) {
|
||||
imageUrls.push(normalized);
|
||||
if (!imageRefs.some((item) => item.url === normalized)) {
|
||||
const width = Number(geometry?.width);
|
||||
const height = Number(geometry?.height);
|
||||
imageRefs.push({
|
||||
url: normalized,
|
||||
width: Number.isFinite(width) && width > 0 ? Math.round(width) : undefined,
|
||||
height: Number.isFinite(height) && height > 0 ? Math.round(height) : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -437,11 +463,11 @@ function extractPromptAndImages(messages: Array<{ role: string; content: any }>)
|
||||
if (part?.type === "text" && typeof part.text === "string") {
|
||||
prompt = `${prompt} ${part.text}`.trim();
|
||||
} else if (part?.type === "image_url" && part.image_url?.url) {
|
||||
pushImageUrl(part.image_url.url);
|
||||
pushImageUrl(part.image_url.url, part);
|
||||
} else if (part?.type === "file") {
|
||||
pushImageUrl(part.url || part.data);
|
||||
pushImageUrl(part.url || part.data, part);
|
||||
} else if (part?.type === "image") {
|
||||
pushImageUrl(part.image || part.url);
|
||||
pushImageUrl(part.image || part.url, part);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -450,7 +476,7 @@ function extractPromptAndImages(messages: Array<{ role: string; content: any }>)
|
||||
throw new Error("No prompt found in user message");
|
||||
}
|
||||
|
||||
return { prompt, imageUrls };
|
||||
return { prompt, imageUrls: imageRefs.map((item) => item.url), imageRefs };
|
||||
}
|
||||
|
||||
async function loadInputImage(source: string) {
|
||||
@ -805,14 +831,19 @@ export function createOpenAIImage({ apiKey, baseURL }: OpenAIImageConfig) {
|
||||
imageParams?: OpenAIImageParams;
|
||||
overrideModelId?: string;
|
||||
}) => {
|
||||
const { prompt, imageUrls } = extractPromptAndImages(options.messages);
|
||||
const { prompt, imageUrls, imageRefs } = extractPromptAndImages(options.messages);
|
||||
const activeModelId = options.overrideModelId || modelId;
|
||||
const normalizedAspectRatio = normalizeAspectRatio(options.imageParams?.aspectRatio);
|
||||
const promptAspectRatio = inferAspectRatioFromPrompt(prompt);
|
||||
const referenceAspectRatio =
|
||||
imageUrls.length > 0
|
||||
? await inferAspectRatioFromImageUrl(imageUrls[0] || "")
|
||||
const referenceGeometryAspectRatio =
|
||||
imageRefs.length > 0
|
||||
? inferAspectRatioFromDimensions(imageRefs[0]?.width, imageRefs[0]?.height)
|
||||
: null;
|
||||
const referenceAspectRatio =
|
||||
referenceGeometryAspectRatio ||
|
||||
(imageUrls.length > 0
|
||||
? await inferAspectRatioFromImageUrl(imageUrls[0] || "")
|
||||
: null);
|
||||
const shouldPreferReferenceAspectRatio =
|
||||
imageUrls.length > 0 && isAgnesImageModel(baseURL, activeModelId);
|
||||
const aspectRatio =
|
||||
@ -826,11 +857,22 @@ export function createOpenAIImage({ apiKey, baseURL }: OpenAIImageConfig) {
|
||||
? inferImageSizeFromModelId(activeModelId)
|
||||
: undefined;
|
||||
const imageSize = modelImageSize || requestedImageSize;
|
||||
const size =
|
||||
modelImageSize && aspectRatio
|
||||
? resolveProviderImageSize(baseURL, aspectRatio, imageSize)
|
||||
: options.imageParams?.size ||
|
||||
const shouldOmitAgnesAutoSize =
|
||||
isAgnesImageModel(baseURL, activeModelId) &&
|
||||
imageUrls.length > 0 &&
|
||||
normalizedAspectRatio === "auto" &&
|
||||
(!aspectRatio || aspectRatio === "auto") &&
|
||||
!options.imageParams?.size;
|
||||
let size: string | undefined;
|
||||
if (shouldOmitAgnesAutoSize) {
|
||||
size = undefined;
|
||||
} else if (modelImageSize && aspectRatio) {
|
||||
size = resolveProviderImageSize(baseURL, aspectRatio, imageSize);
|
||||
} else {
|
||||
size =
|
||||
options.imageParams?.size ||
|
||||
(aspectRatio ? resolveProviderImageSize(baseURL, aspectRatio, imageSize) : undefined);
|
||||
}
|
||||
const quality = normalizeQuality(options.imageParams?.quality);
|
||||
const background = normalizeBackground(options.imageParams?.background);
|
||||
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
|
||||
@ -841,8 +883,10 @@ export function createOpenAIImage({ apiKey, baseURL }: OpenAIImageConfig) {
|
||||
size,
|
||||
aspectRatio: aspectRatio || "auto",
|
||||
referenceAspectRatio: referenceAspectRatio || undefined,
|
||||
referenceGeometryAspectRatio: referenceGeometryAspectRatio || undefined,
|
||||
preferReferenceAspectRatio: shouldPreferReferenceAspectRatio,
|
||||
promptAspectRatio: promptAspectRatio || undefined,
|
||||
omittedSizeForAgnesAuto: shouldOmitAgnesAutoSize || undefined,
|
||||
imageSize,
|
||||
requestedImageSize,
|
||||
modelImageSize,
|
||||
|
||||
@ -56,4 +56,6 @@ export type Attachment = {
|
||||
url: string;
|
||||
contentType: string;
|
||||
assetId?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
@ -45,6 +45,8 @@ interface PendingChatAttachment {
|
||||
url: string;
|
||||
name: string;
|
||||
contentType: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// 🆕 拾取的物品信息
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user