fix: 在图片上绘制红色标记点让AI识别,识别后直接添加到对话框

This commit is contained in:
Your Name 2026-02-02 12:14:00 +08:00
parent 7db61d8533
commit 221579abb2
2 changed files with 117 additions and 17 deletions

View File

@ -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',

View File

@ -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<string> {
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,