fix: 在图片上绘制红色标记点让AI识别,识别后直接添加到对话框
This commit is contained in:
parent
7db61d8533
commit
221579abb2
@ -14,7 +14,7 @@ const POSITION_OPTIONS = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
interface PickObjectRequest {
|
interface PickObjectRequest {
|
||||||
imageUrl: string; // 图片 URL
|
imageData: string; // 带红色标记的图片 base64
|
||||||
clickX: number; // 点击位置 X (0-1)
|
clickX: number; // 点击位置 X (0-1)
|
||||||
clickY: number; // 点击位置 Y (0-1)
|
clickY: number; // 点击位置 Y (0-1)
|
||||||
}
|
}
|
||||||
@ -34,15 +34,15 @@ interface PickObjectResult {
|
|||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body: PickObjectRequest = await request.json();
|
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({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: '缺少图片 URL',
|
error: '缺少图片数据',
|
||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,12 +78,14 @@ export async function POST(request: Request) {
|
|||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 prompt(严格限制输出格式)
|
// 构建 prompt - AI 通过红色标记来识别用户选择的物品
|
||||||
const prompt = `
|
const prompt = `
|
||||||
请识别图片中用户点击位置(坐标 ${(clickX * 100).toFixed(0)}%, ${(clickY * 100).toFixed(0)}%)最近的那个具体物品。
|
图片中有一个红色圆形标记(带十字线),请识别这个红色标记所指向的具体物品。
|
||||||
|
|
||||||
【重要】只识别点击位置最近的那个小物品,不要识别大范围的家具或背景。
|
【重要】
|
||||||
例如:用户点击花瓶位置 → 返回"花瓶"而不是"桌子"或"屏风"
|
- 只识别红色标记正中心位置的那个物品
|
||||||
|
- 返回具体的小物品名称,不要返回大范围的家具或背景
|
||||||
|
- 例如:标记在花瓶上 → 返回"花瓶",不是"桌子"或"屏风"
|
||||||
|
|
||||||
【输出要求 - 严格遵守】
|
【输出要求 - 严格遵守】
|
||||||
返回纯 JSON,无其他文字:
|
返回纯 JSON,无其他文字:
|
||||||
@ -94,8 +96,8 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
【name 格式】≤6字,简洁具体
|
【name 格式】≤6字,简洁具体
|
||||||
正确:橙色花瓶、白色杯子、木质椅子、吊灯
|
正确示例:橙色花瓶、白色杯子、木质椅子、吊灯、抱枕、花盆
|
||||||
错误:木质屏风(太大范围)、一个漂亮的花瓶(太长)
|
错误示例:屏风(太大)、一个漂亮的花瓶(太长)
|
||||||
|
|
||||||
【position 格式】固定4字,从以下选择:
|
【position 格式】固定4字,从以下选择:
|
||||||
${POSITION_OPTIONS.join('、')}
|
${POSITION_OPTIONS.join('、')}
|
||||||
@ -103,7 +105,7 @@ ${POSITION_OPTIONS.join('、')}
|
|||||||
|
|
||||||
console.log('[PickObject] 调用 Qwen VL 模型...');
|
console.log('[PickObject] 调用 Qwen VL 模型...');
|
||||||
|
|
||||||
// 调用 Qwen VL 模型(复用现有的 generateText)
|
// 调用 Qwen VL 模型,发送带红色标记的图片
|
||||||
const { text: responseText } = await generateText({
|
const { text: responseText } = await generateText({
|
||||||
model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
@ -112,7 +114,7 @@ ${POSITION_OPTIONS.join('、')}
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
image: imageUrl, // 直接使用图片 URL
|
image: imageData, // 带红色标记的 base64 图片
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@ -8,6 +8,101 @@ import { useCanvasStore, type PickedObject } from "@/store/canvas";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
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() {
|
export function ObjectPickerOverlay() {
|
||||||
const isPickerMode = useCanvasStore((state) => state.isPickerMode);
|
const isPickerMode = useCanvasStore((state) => state.isPickerMode);
|
||||||
@ -41,12 +136,15 @@ export function ObjectPickerOverlay() {
|
|||||||
setPickerLoading(true);
|
setPickerLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 直接使用原始图片 URL 调用 API(无任何标记)
|
// 在图片上绘制红色标记点,让 AI 能看到用户点击的位置
|
||||||
|
const markedImageBase64 = await drawMarkerOnImage(imageUrl, clickX, clickY);
|
||||||
|
|
||||||
|
// 发送带标记的图片给 AI
|
||||||
const response = await fetch('/api/pick-object', {
|
const response = await fetch('/api/pick-object', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
imageUrl, // 原始图片 URL,无标记
|
imageData: markedImageBase64, // 带红色标记的图片
|
||||||
clickX,
|
clickX,
|
||||||
clickY,
|
clickY,
|
||||||
}),
|
}),
|
||||||
@ -59,9 +157,9 @@ export function ObjectPickerOverlay() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.object) {
|
if (result.success && result.object) {
|
||||||
// 构建拾取结果
|
// 构建拾取结果(保存原始图片 URL,不是带标记的)
|
||||||
const picked: PickedObject = {
|
const picked: PickedObject = {
|
||||||
imageUrl,
|
imageUrl, // 原始图片
|
||||||
imageNodeId: pickerTargetNodeId,
|
imageNodeId: pickerTargetNodeId,
|
||||||
clickX,
|
clickX,
|
||||||
clickY,
|
clickY,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user