fix: preserve SnapGen reference image order
This commit is contained in:
parent
91a855c041
commit
560491aabb
@ -120,6 +120,7 @@ import {
|
|||||||
shouldBypassCanvasImagePointsPrecheckForAgentControl,
|
shouldBypassCanvasImagePointsPrecheckForAgentControl,
|
||||||
shouldPreserveCanvasAgentSlotOnlyContext,
|
shouldPreserveCanvasAgentSlotOnlyContext,
|
||||||
} from "@/lib/canvas/agent-client-intent";
|
} from "@/lib/canvas/agent-client-intent";
|
||||||
|
import { orderItemsBySelectionIds } from "@/lib/canvas/selection-order";
|
||||||
import { getCanvasSkillIconComponent } from "@/components/canvas/canvas-skill-icons";
|
import { getCanvasSkillIconComponent } from "@/components/canvas/canvas-skill-icons";
|
||||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||||
import { VideoParamsBar, type VideoGenerationParams } from "@/components/video-params-bar";
|
import { VideoParamsBar, type VideoGenerationParams } from "@/components/video-params-bar";
|
||||||
@ -2190,13 +2191,16 @@ export function CanvasChatPanel({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 获取选中的图片节点
|
// 获取选中的图片节点
|
||||||
const selectedImageNodes = useMemo(
|
const selectedImageNodes = useMemo(() => {
|
||||||
() =>
|
const selectedNodeIdSet = new Set(selectedNodeIds);
|
||||||
|
return orderItemsBySelectionIds(
|
||||||
nodes.filter(
|
nodes.filter(
|
||||||
(node) => selectedNodeIds.includes(node.id) && isCanvasImageReadyForAi(node)
|
(node) => selectedNodeIdSet.has(node.id) && isCanvasImageReadyForAi(node)
|
||||||
),
|
),
|
||||||
[nodes, selectedNodeIds]
|
selectedNodeIds,
|
||||||
);
|
(node) => node.id,
|
||||||
|
);
|
||||||
|
}, [nodes, selectedNodeIds]);
|
||||||
const selectedImageNodesRef = useRef<CanvasNode[]>([]);
|
const selectedImageNodesRef = useRef<CanvasNode[]>([]);
|
||||||
const generationAnchorNodesRef = useRef<CanvasNode[]>([]);
|
const generationAnchorNodesRef = useRef<CanvasNode[]>([]);
|
||||||
|
|
||||||
@ -6410,9 +6414,11 @@ export function CanvasChatPanel({
|
|||||||
|
|
||||||
const MAX_CONTEXT_IMAGES = 12;
|
const MAX_CONTEXT_IMAGES = 12;
|
||||||
const MAX_AGENT_CANDIDATE_NODES = 30;
|
const MAX_AGENT_CANDIDATE_NODES = 30;
|
||||||
const selectedCanvasNodes = nodes
|
const selectedCanvasNodes = orderItemsBySelectionIds(
|
||||||
.filter((node) => selectedNodeIdSet.has(node.id))
|
nodes.filter((node) => selectedNodeIdSet.has(node.id)),
|
||||||
.slice(0, 20);
|
effectiveSelectedNodeIds,
|
||||||
|
(node) => node.id,
|
||||||
|
).slice(0, 20);
|
||||||
const selectedContextNodes = selectedImageNodesForContext.slice(0, 20);
|
const selectedContextNodes = selectedImageNodesForContext.slice(0, 20);
|
||||||
generationAnchorNodesRef.current = selectedContextNodes.slice(0, 4);
|
generationAnchorNodesRef.current = selectedContextNodes.slice(0, 4);
|
||||||
const selectedIds = new Set(selectedContextNodes.map((node) => node.id));
|
const selectedIds = new Set(selectedContextNodes.map((node) => node.id));
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { extractImageUrlsFromText, inferOriginalImageUrl } from "@/lib/image-url";
|
import { extractImageUrlsFromText, inferOriginalImageUrl } from "@/lib/image-url";
|
||||||
|
import { orderItemsBySelectionIds } from "@/lib/canvas/selection-order";
|
||||||
import type { DBMessage } from "@/lib/db/schema";
|
import type { DBMessage } from "@/lib/db/schema";
|
||||||
import type {
|
import type {
|
||||||
CanvasImageNodeContext,
|
CanvasImageNodeContext,
|
||||||
@ -228,9 +229,16 @@ export function resolveChatMediaInput(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const canvasImageRefs = toCanvasImageRefs(params.contextPayload?.canvasImageNodes, maxImages);
|
const canvasImageRefs = toCanvasImageRefs(params.contextPayload?.canvasImageNodes, maxImages);
|
||||||
const selectedNodeIds = new Set(params.contextPayload?.selectedNodeIds || []);
|
const selectedNodeIds = (params.contextPayload?.selectedNodeIds || [])
|
||||||
|
.map((id) => String(id || "").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const selectedNodeIdSet = new Set(selectedNodeIds);
|
||||||
const selectedImageRefs = dedupeRefs(
|
const selectedImageRefs = dedupeRefs(
|
||||||
canvasImageRefs.filter((ref) => ref.isSelected || (ref.nodeId && selectedNodeIds.has(ref.nodeId))),
|
orderItemsBySelectionIds(
|
||||||
|
canvasImageRefs.filter((ref) => ref.isSelected || (ref.nodeId && selectedNodeIdSet.has(ref.nodeId))),
|
||||||
|
selectedNodeIds,
|
||||||
|
(ref) => ref.nodeId,
|
||||||
|
),
|
||||||
maxImages,
|
maxImages,
|
||||||
);
|
);
|
||||||
const history = extractHistoryImageRefs({
|
const history = extractHistoryImageRefs({
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import "server-only";
|
|||||||
|
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
import { buildSnapgenReferenceOrderPrompt } from "@/lib/ai/providers/snapgen-reference-order";
|
||||||
import {
|
import {
|
||||||
SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS,
|
SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS,
|
||||||
SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS,
|
SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS,
|
||||||
@ -876,9 +877,10 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) {
|
|||||||
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
|
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
|
||||||
const maxImages = options.imageGenConfig?.maxImages || (apiFormat === "snapgen-grok-image" ? 8 : apiFormat === "snapgen-meta-ai-image" ? 4 : 1);
|
const maxImages = options.imageGenConfig?.maxImages || (apiFormat === "snapgen-grok-image" ? 8 : apiFormat === "snapgen-meta-ai-image" ? 4 : 1);
|
||||||
const imageCount = clampImageCount(options.imageParams?.imageCount, maxImages);
|
const imageCount = clampImageCount(options.imageParams?.imageCount, maxImages);
|
||||||
|
const promptForRequest = buildSnapgenReferenceOrderPrompt(prompt, imageUrls);
|
||||||
const formData = await buildRequestForm({
|
const formData = await buildRequestForm({
|
||||||
apiFormat,
|
apiFormat,
|
||||||
prompt,
|
prompt: promptForRequest,
|
||||||
runtimeModelId,
|
runtimeModelId,
|
||||||
imageUrls,
|
imageUrls,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
@ -908,6 +910,7 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) {
|
|||||||
: apiFormat === "snapgen-generic-image"
|
: apiFormat === "snapgen-generic-image"
|
||||||
? "file_urls"
|
? "file_urls"
|
||||||
: "files",
|
: "files",
|
||||||
|
referenceOrderPrompt: promptForRequest !== prompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createPayload = await requestSnapgenMultipart({
|
const createPayload = await requestSnapgenMultipart({
|
||||||
|
|||||||
14
lib/ai/providers/snapgen-reference-order.ts
Normal file
14
lib/ai/providers/snapgen-reference-order.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export function buildSnapgenReferenceOrderPrompt(prompt: string, imageUrls: string[]) {
|
||||||
|
const normalizedPrompt = String(prompt || "").trim();
|
||||||
|
const referenceCount = imageUrls.filter((url) => String(url || "").trim()).length;
|
||||||
|
if (referenceCount < 2) return normalizedPrompt;
|
||||||
|
|
||||||
|
return [
|
||||||
|
`参考图顺序说明:本次请求包含 ${referenceCount} 张参考图,顺序与上传/选中顺序一致。`,
|
||||||
|
"第 1 张参考图优先作为主参考;如果用户提到“第一张图/图1/主图/原图”,对应第 1 张参考图,除非用户明确另行指定。",
|
||||||
|
"第 2 张及后续参考图仅作为辅助参考,按用户提示使用。",
|
||||||
|
normalizedPrompt,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
40
lib/canvas/selection-order.ts
Normal file
40
lib/canvas/selection-order.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export function orderItemsBySelectionIds<T>(
|
||||||
|
items: T[],
|
||||||
|
selectedIds: string[],
|
||||||
|
getItemId: (item: T) => string | null | undefined,
|
||||||
|
) {
|
||||||
|
const normalizedSelectedIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
selectedIds
|
||||||
|
.map((id) => String(id || "").trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (normalizedSelectedIds.length === 0) return items;
|
||||||
|
|
||||||
|
const itemById = new Map<string, T>();
|
||||||
|
for (const item of items) {
|
||||||
|
const id = String(getItemId(item) || "").trim();
|
||||||
|
if (id && !itemById.has(id)) {
|
||||||
|
itemById.set(id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedItems: T[] = [];
|
||||||
|
const orderedItemSet = new Set<T>();
|
||||||
|
for (const id of normalizedSelectedIds) {
|
||||||
|
const item = itemById.get(id);
|
||||||
|
if (item && !orderedItemSet.has(item)) {
|
||||||
|
orderedItems.push(item);
|
||||||
|
orderedItemSet.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (!orderedItemSet.has(item)) {
|
||||||
|
orderedItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedItems;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user