From 221579abb299cd93e45233a00cb6e34b4cb4867e Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Feb 2026 12:14:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9C=A8=E5=9B=BE=E7=89=87=E4=B8=8A?= =?UTF-8?q?=E7=BB=98=E5=88=B6=E7=BA=A2=E8=89=B2=E6=A0=87=E8=AE=B0=E7=82=B9?= =?UTF-8?q?=E8=AE=A9AI=E8=AF=86=E5=88=AB=EF=BC=8C=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E5=90=8E=E7=9B=B4=E6=8E=A5=E6=B7=BB=E5=8A=A0=E5=88=B0=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/pick-object/route.ts | 28 +++--- components/canvas/ObjectPickerOverlay.tsx | 106 +++++++++++++++++++++- 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/app/api/pick-object/route.ts b/app/api/pick-object/route.ts index e18f8a4..5bf1bf2 100644 --- a/app/api/pick-object/route.ts +++ b/app/api/pick-object/route.ts @@ -14,7 +14,7 @@ const POSITION_OPTIONS = [ ] as const; interface PickObjectRequest { - imageUrl: string; // 图片 URL + imageData: string; // 带红色标记的图片 base64 clickX: number; // 点击位置 X (0-1) clickY: number; // 点击位置 Y (0-1) } @@ -34,15 +34,15 @@ interface PickObjectResult { export async function POST(request: Request) { try { const body: PickObjectRequest = await request.json(); - const { imageUrl, clickX, clickY } = body; + const { imageData, clickX, clickY } = body; - console.log('[PickObject] 收到请求:', { imageUrl: imageUrl?.substring(0, 50), clickX, clickY }); + console.log('[PickObject] 收到请求:', { imageDataLength: imageData?.length, clickX, clickY }); // 验证参数 - if (!imageUrl) { + if (!imageData) { return NextResponse.json({ success: false, - error: '缺少图片 URL', + error: '缺少图片数据', }, { status: 400 }); } @@ -78,12 +78,14 @@ export async function POST(request: Request) { }, { status: 500 }); } - // 构建 prompt(严格限制输出格式) + // 构建 prompt - AI 通过红色标记来识别用户选择的物品 const prompt = ` -请识别图片中用户点击位置(坐标 ${(clickX * 100).toFixed(0)}%, ${(clickY * 100).toFixed(0)}%)最近的那个具体物品。 +图片中有一个红色圆形标记(带十字线),请识别这个红色标记所指向的具体物品。 -【重要】只识别点击位置最近的那个小物品,不要识别大范围的家具或背景。 -例如:用户点击花瓶位置 → 返回"花瓶"而不是"桌子"或"屏风" +【重要】 +- 只识别红色标记正中心位置的那个物品 +- 返回具体的小物品名称,不要返回大范围的家具或背景 +- 例如:标记在花瓶上 → 返回"花瓶",不是"桌子"或"屏风" 【输出要求 - 严格遵守】 返回纯 JSON,无其他文字: @@ -94,8 +96,8 @@ export async function POST(request: Request) { } 【name 格式】≤6字,简洁具体 -正确:橙色花瓶、白色杯子、木质椅子、吊灯 -错误:木质屏风(太大范围)、一个漂亮的花瓶(太长) +正确示例:橙色花瓶、白色杯子、木质椅子、吊灯、抱枕、花盆 +错误示例:屏风(太大)、一个漂亮的花瓶(太长) 【position 格式】固定4字,从以下选择: ${POSITION_OPTIONS.join('、')} @@ -103,7 +105,7 @@ ${POSITION_OPTIONS.join('、')} console.log('[PickObject] 调用 Qwen VL 模型...'); - // 调用 Qwen VL 模型(复用现有的 generateText) + // 调用 Qwen VL 模型,发送带红色标记的图片 const { text: responseText } = await generateText({ model, messages: [ @@ -112,7 +114,7 @@ ${POSITION_OPTIONS.join('、')} content: [ { type: 'image', - image: imageUrl, // 直接使用图片 URL + image: imageData, // 带红色标记的 base64 图片 }, { type: 'text', diff --git a/components/canvas/ObjectPickerOverlay.tsx b/components/canvas/ObjectPickerOverlay.tsx index 0ed736c..f6b96ce 100644 --- a/components/canvas/ObjectPickerOverlay.tsx +++ b/components/canvas/ObjectPickerOverlay.tsx @@ -8,6 +8,101 @@ import { useCanvasStore, type PickedObject } from "@/store/canvas"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; +/** + * 在图片上绘制标记点,返回带标记的图片 base64 + * AI 通过看到这个红色标记来识别用户点击的位置 + */ +async function drawMarkerOnImage( + imageUrl: string, + clickX: number, + clickY: number +): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + // 创建 canvas + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('无法创建 canvas')); + return; + } + + // 压缩图片尺寸(最大 800px),加快 AI 分析速度 + const maxSize = 800; + let width = img.width; + let height = img.height; + + if (width > maxSize || height > maxSize) { + if (width > height) { + height = (height / width) * maxSize; + width = maxSize; + } else { + width = (width / height) * maxSize; + height = maxSize; + } + } + + canvas.width = width; + canvas.height = height; + + // 绘制图片 + ctx.drawImage(img, 0, 0, width, height); + + // 计算标记位置 + const markerX = clickX * width; + const markerY = clickY * height; + const markerRadius = Math.max(12, Math.min(width, height) * 0.025); // 标记大小 + + // 绘制红色标记圆圈(带白色边框,更醒目) + ctx.beginPath(); + ctx.arc(markerX, markerY, markerRadius + 3, 0, Math.PI * 2); + ctx.fillStyle = 'white'; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(markerX, markerY, markerRadius, 0, Math.PI * 2); + ctx.fillStyle = '#FF0000'; + ctx.fill(); + + // 绘制十字线(帮助 AI 定位) + ctx.strokeStyle = '#FF0000'; + ctx.lineWidth = 2; + + // 水平线 + ctx.beginPath(); + ctx.moveTo(markerX - markerRadius * 2, markerY); + ctx.lineTo(markerX - markerRadius - 2, markerY); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(markerX + markerRadius + 2, markerY); + ctx.lineTo(markerX + markerRadius * 2, markerY); + ctx.stroke(); + + // 垂直线 + ctx.beginPath(); + ctx.moveTo(markerX, markerY - markerRadius * 2); + ctx.lineTo(markerX, markerY - markerRadius - 2); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(markerX, markerY + markerRadius + 2); + ctx.lineTo(markerX, markerY + markerRadius * 2); + ctx.stroke(); + + // 转为 base64 + const markedImageBase64 = canvas.toDataURL('image/jpeg', 0.85); + resolve(markedImageBase64); + }; + + img.onerror = () => { + reject(new Error('图片加载失败')); + }; + + img.src = imageUrl; + }); +} export function ObjectPickerOverlay() { const isPickerMode = useCanvasStore((state) => state.isPickerMode); @@ -41,12 +136,15 @@ export function ObjectPickerOverlay() { setPickerLoading(true); try { - // 直接使用原始图片 URL 调用 API(无任何标记) + // 在图片上绘制红色标记点,让 AI 能看到用户点击的位置 + const markedImageBase64 = await drawMarkerOnImage(imageUrl, clickX, clickY); + + // 发送带标记的图片给 AI const response = await fetch('/api/pick-object', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - imageUrl, // 原始图片 URL,无标记 + imageData: markedImageBase64, // 带红色标记的图片 clickX, clickY, }), @@ -59,9 +157,9 @@ export function ObjectPickerOverlay() { const result = await response.json(); if (result.success && result.object) { - // 构建拾取结果 + // 构建拾取结果(保存原始图片 URL,不是带标记的) const picked: PickedObject = { - imageUrl, + imageUrl, // 原始图片 imageNodeId: pickerTargetNodeId, clickX, clickY,