fix(ai): preserve relational image references
This commit is contained in:
parent
9717196264
commit
6269aefee5
71
lib/ai/context-memory/ordinal-image-references.ts
Normal file
71
lib/ai/context-memory/ordinal-image-references.ts
Normal file
@ -0,0 +1,71 @@
|
||||
function chineseNumberToInt(value: string): number | null {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return null;
|
||||
if (/^\d+$/.test(normalized)) return Number.parseInt(normalized, 10);
|
||||
|
||||
const map: Record<string, number> = {
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
十: 10,
|
||||
};
|
||||
|
||||
if (normalized === "十") return 10;
|
||||
if (normalized.length === 1) return map[normalized] ?? null;
|
||||
if (normalized.startsWith("十")) {
|
||||
return 10 + (map[normalized.slice(1)] ?? 0);
|
||||
}
|
||||
if (normalized.endsWith("十")) {
|
||||
return (map[normalized.slice(0, 1)] ?? 0) * 10;
|
||||
}
|
||||
if (normalized.includes("十")) {
|
||||
const [head, tail] = normalized.split("十");
|
||||
return (map[head] ?? 0) * 10 + (map[tail] ?? 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function collectOrdinalImageIndexes(text: string): number[] {
|
||||
const indexes = new Set<number>();
|
||||
const normalizedText = String(text || "");
|
||||
const chineseOrdinalPattern =
|
||||
/第\s*([一二三四五六七八九十两\d]+)\s*(?:张|个|幅)?\s*(?:图|图片|照片)?/gi;
|
||||
const numberedImagePattern = /(?:图|图片|照片|image|photo|reference)\s*([1-9]\d*)\b/gi;
|
||||
const compactChineseImagePattern =
|
||||
/(?:图片|照片|图)\s*([一二两三四五六七八九十])(?![一二两三四五六七八九十\d维])/g;
|
||||
const englishOrdinals: Array<{ index: number; pattern: RegExp }> = [
|
||||
{ index: 0, pattern: /\b(?:first|1st)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 1, pattern: /\b(?:second|2nd)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 2, pattern: /\b(?:third|3rd)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 3, pattern: /\b(?:fourth|4th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 4, pattern: /\b(?:fifth|5th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 5, pattern: /\b(?:sixth|6th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 6, pattern: /\b(?:seventh|7th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 7, pattern: /\b(?:eighth|8th)\s+(?:image|photo|reference)\b/i },
|
||||
];
|
||||
|
||||
for (const match of normalizedText.matchAll(chineseOrdinalPattern)) {
|
||||
const numeric = chineseNumberToInt(match[1] || "");
|
||||
if (numeric && numeric > 0) indexes.add(numeric - 1);
|
||||
}
|
||||
for (const match of normalizedText.matchAll(numberedImagePattern)) {
|
||||
const numeric = Number.parseInt(match[1] || "", 10);
|
||||
if (Number.isFinite(numeric) && numeric > 0) indexes.add(numeric - 1);
|
||||
}
|
||||
for (const match of normalizedText.matchAll(compactChineseImagePattern)) {
|
||||
const numeric = chineseNumberToInt(match[1] || "");
|
||||
if (numeric && numeric > 0) indexes.add(numeric - 1);
|
||||
}
|
||||
for (const item of englishOrdinals) {
|
||||
if (item.pattern.test(normalizedText)) indexes.add(item.index);
|
||||
}
|
||||
|
||||
return Array.from(indexes).sort((left, right) => left - right);
|
||||
}
|
||||
@ -10,6 +10,7 @@ import {
|
||||
hasCanvasAgentDeicticTarget,
|
||||
resolveCanvasAgentTargetPreference,
|
||||
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||
import { collectOrdinalImageIndexes } from "@/lib/ai/context-memory/ordinal-image-references";
|
||||
|
||||
const ORIGINAL_IMAGE_REGEX =
|
||||
/(原图|上传图|参考图|最初那张|第一张图|素材图|源图|原始图片|原始那张|我发的那张|上传的那张|原来的图)/i;
|
||||
@ -33,40 +34,6 @@ function normalizeStringArray(values?: string[] | null): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function chineseNumberToInt(value: string): number | null {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return null;
|
||||
if (/^\d+$/.test(normalized)) return Number.parseInt(normalized, 10);
|
||||
|
||||
const map: Record<string, number> = {
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
十: 10,
|
||||
};
|
||||
|
||||
if (normalized === "十") return 10;
|
||||
if (normalized.length === 1) return map[normalized] ?? null;
|
||||
if (normalized.startsWith("十")) {
|
||||
return 10 + (map[normalized.slice(1)] ?? 0);
|
||||
}
|
||||
if (normalized.endsWith("十")) {
|
||||
return (map[normalized.slice(0, 1)] ?? 0) * 10;
|
||||
}
|
||||
if (normalized.includes("十")) {
|
||||
const [head, tail] = normalized.split("十");
|
||||
return (map[head] ?? 0) * 10 + (map[tail] ?? 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sortRefsForCanvasOrdering(refs: ResolvedImageRef[]) {
|
||||
return [...refs].sort((left, right) => {
|
||||
const leftY = Number(left.y ?? 0);
|
||||
@ -98,46 +65,8 @@ function getPositionLabel(ref: ResolvedImageRef, refs: ResolvedImageRef[]): stri
|
||||
return `${vertical}${horizontal}`;
|
||||
}
|
||||
|
||||
function collectOrdinalIndexes(text: string): number[] {
|
||||
const indexes = new Set<number>();
|
||||
const normalizedText = String(text || "");
|
||||
const chineseOrdinalPattern =
|
||||
/第\s*([一二三四五六七八九十两\d]+)\s*(?:张|个|幅)?\s*(?:图|图片|照片)?/gi;
|
||||
const numberedImagePattern = /(?:图|图片|照片|image|photo|reference)\s*([1-9]\d*)\b/gi;
|
||||
const compactChineseImagePattern =
|
||||
/(?:图片|照片|图)\s*([一二两三四五六七八九十])(?=\s*(?:的|上|中|里|内|和|与|及|、|,|,|。|;|;|$))/g;
|
||||
const englishOrdinals: Array<{ index: number; pattern: RegExp }> = [
|
||||
{ index: 0, pattern: /\b(?:first|1st)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 1, pattern: /\b(?:second|2nd)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 2, pattern: /\b(?:third|3rd)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 3, pattern: /\b(?:fourth|4th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 4, pattern: /\b(?:fifth|5th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 5, pattern: /\b(?:sixth|6th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 6, pattern: /\b(?:seventh|7th)\s+(?:image|photo|reference)\b/i },
|
||||
{ index: 7, pattern: /\b(?:eighth|8th)\s+(?:image|photo|reference)\b/i },
|
||||
];
|
||||
|
||||
for (const match of normalizedText.matchAll(chineseOrdinalPattern)) {
|
||||
const numeric = chineseNumberToInt(match[1] || "");
|
||||
if (numeric && numeric > 0) indexes.add(numeric - 1);
|
||||
}
|
||||
for (const match of normalizedText.matchAll(numberedImagePattern)) {
|
||||
const numeric = Number.parseInt(match[1] || "", 10);
|
||||
if (Number.isFinite(numeric) && numeric > 0) indexes.add(numeric - 1);
|
||||
}
|
||||
for (const match of normalizedText.matchAll(compactChineseImagePattern)) {
|
||||
const numeric = chineseNumberToInt(match[1] || "");
|
||||
if (numeric && numeric > 0) indexes.add(numeric - 1);
|
||||
}
|
||||
for (const item of englishOrdinals) {
|
||||
if (item.pattern.test(normalizedText)) indexes.add(item.index);
|
||||
}
|
||||
|
||||
return Array.from(indexes).sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
function pickByOrdinal(text: string, refs: ResolvedImageRef[]): ResolvedImageRef[] {
|
||||
const ordinalIndexes = collectOrdinalIndexes(text);
|
||||
const ordinalIndexes = collectOrdinalImageIndexes(text);
|
||||
if (ordinalIndexes.length === 0) return [];
|
||||
const ordered = sortRefsForCanvasOrdering(refs);
|
||||
return ordinalIndexes
|
||||
|
||||
@ -30,6 +30,7 @@ export default defineConfig({
|
||||
/routes\/hicad-manufacturing-extension\.test\.ts/,
|
||||
/routes\/openapi-access-key\.test\.ts/,
|
||||
/routes\/vip98k-reference-routing\.test\.ts/,
|
||||
/routes\/ordinal-image-references\.test\.ts/,
|
||||
],
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
22
tests/routes/ordinal-image-references.test.ts
Normal file
22
tests/routes/ordinal-image-references.test.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { collectOrdinalImageIndexes } from "@/lib/ai/context-memory/ordinal-image-references";
|
||||
|
||||
test.describe("ordinal image references", () => {
|
||||
test("keeps both sides of a Chinese replacement relationship", () => {
|
||||
expect(
|
||||
collectOrdinalImageIndexes("把图二替换到图一的场景中,靠着左边放"),
|
||||
).toEqual([0, 1]);
|
||||
expect(collectOrdinalImageIndexes("用图三替换图一的产品")).toEqual([
|
||||
0, 2,
|
||||
]);
|
||||
});
|
||||
|
||||
test("still resolves a single compact ordinal", () => {
|
||||
expect(collectOrdinalImageIndexes("只调整图二")).toEqual([1]);
|
||||
expect(collectOrdinalImageIndexes("修改第三张图")).toEqual([2]);
|
||||
});
|
||||
|
||||
test("does not treat dimensional wording as an image ordinal", () => {
|
||||
expect(collectOrdinalImageIndexes("保持图二维布局")).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user