fix: 在图片上绘制红色标记点让AI识别,识别后直接添加到对话框
This commit is contained in:
parent
7db61d8533
commit
221579abb2
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user