212 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextResponse } from "next/server";
import { generateText } from "ai";
import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
export const runtime = "nodejs";
export const maxDuration = 30;
// 位置词库
const POSITION_OPTIONS = [
'左上区域', '中央顶部', '右上区域',
'左侧中部', '中央区域', '右侧中部',
'左下区域', '中央底部', '右下区域'
] as const;
interface PickObjectRequest {
imageUrl: string; // 图片 URL
clickX: number; // 点击位置 X (0-1)
clickY: number; // 点击位置 Y (0-1)
}
interface PickObjectResult {
name: string;
position: string;
confidence: number;
boundingBox: {
x: number;
y: number;
width: number;
height: number;
};
}
export async function POST(request: Request) {
try {
const body: PickObjectRequest = await request.json();
const { imageUrl, clickX, clickY } = body;
console.log('[PickObject] 收到请求:', { imageUrl: imageUrl?.substring(0, 50), clickX, clickY });
// 验证参数
if (!imageUrl) {
return NextResponse.json({
success: false,
error: '缺少图片 URL',
}, { status: 400 });
}
if (typeof clickX !== 'number' || typeof clickY !== 'number') {
return NextResponse.json({
success: false,
error: '缺少点击坐标',
}, { status: 400 });
}
// 获取 Qwen VL 模型(复用现有逻辑)
const allModels = await getEnabledAiModelsWithProvider();
const qwenModel = allModels.find(
(m) => m.provider.type === "alibaba" && m.model.supportsVision
);
if (!qwenModel) {
console.log("[PickObject] No Qwen VL model found");
return NextResponse.json({
success: false,
error: 'Qwen VL 模型未配置',
}, { status: 500 });
}
console.log('[PickObject] 使用模型:', qwenModel.model.modelId, qwenModel.model.displayName);
const model = await getLanguageModelById(qwenModel.model.id);
if (!model) {
console.log("[PickObject] Failed to get language model");
return NextResponse.json({
success: false,
error: '无法加载模型',
}, { status: 500 });
}
// 构建 prompt(严格限制输出格式)
const prompt = `
请识别图片中用户点击位置(坐标 ${(clickX * 100).toFixed(0)}%, ${(clickY * 100).toFixed(0)}%)最近的那个具体物品。
【重要】只识别点击位置最近的那个小物品,不要识别大范围的家具或背景。
例如:用户点击花瓶位置 → 返回"花瓶"而不是"桌子"或"屏风"
【输出要求 - 严格遵守】
返回纯 JSON,无其他文字:
{
"name": "物品名称",
"position": "位置描述",
"confidence": 0.95
}
【name 格式】≤6字,简洁具体
正确:橙色花瓶、白色杯子、木质椅子、吊灯
错误:木质屏风(太大范围)、一个漂亮的花瓶(太长)
【position 格式】固定4字,从以下选择:
${POSITION_OPTIONS.join('、')}
`.trim();
console.log('[PickObject] 调用 Qwen VL 模型...');
// 调用 Qwen VL 模型(复用现有的 generateText)
const { text: responseText } = await generateText({
model,
messages: [
{
role: 'user',
content: [
{
type: 'image',
image: imageUrl, // 直接使用图片 URL
},
{
type: 'text',
text: prompt,
},
],
},
],
temperature: 0.1,
maxTokens: 300,
} as any);
console.log('[PickObject] Qwen 返回内容:', responseText?.substring(0, 200));
if (!responseText) {
return NextResponse.json({
success: false,
error: 'AI 未返回结果',
}, { status: 500 });
}
// 解析 JSON 结果
let result: PickObjectResult;
try {
// 尝试从返回内容中提取 JSON
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('未找到 JSON');
}
const parsed = JSON.parse(jsonMatch[0]);
// 验证和修正名称
let name = parsed.name || '物品';
if (name.length > 8) {
name = name.substring(0, 6);
}
// 验证位置是否在词库中
let position = parsed.position;
if (!POSITION_OPTIONS.includes(position as any)) {
// 根据点击位置推算
const posX = clickX < 0.33 ? '左' : clickX > 0.66 ? '右' : '中央';
const posY = clickY < 0.33 ? '上' : clickY > 0.66 ? '下' : '中';
if (posX === '中央') {
position = posY === '中' ? '中央区域' : posY === '上' ? '中央顶部' : '中央底部';
} else {
position = posY === '中' ? `${posX}侧中部` : `${posX}${posY}区域`;
}
}
// 确保 confidence 有效
let confidence = parsed.confidence;
if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) {
confidence = 0.85;
}
result = {
name,
position,
confidence,
boundingBox: { x: clickX, y: clickY, width: 0, height: 0 }, // 不再使用,保持兼容
};
} catch (parseError) {
console.error('[PickObject] JSON parse error:', parseError, 'Content:', responseText);
// 返回默认结果
const posX = clickX < 0.33 ? '左' : clickX > 0.66 ? '右' : '中央';
const posY = clickY < 0.33 ? '上' : clickY > 0.66 ? '下' : '中';
const position = posX === '中央'
? (posY === '中' ? '中央区域' : posY === '上' ? '中央顶部' : '中央底部')
: (posY === '中' ? `${posX}侧中部` : `${posX}${posY}区域`);
result = {
name: '物品',
position,
confidence: 0.5,
boundingBox: { x: clickX, y: clickY, width: 0, height: 0 },
};
}
console.log('[PickObject] 识别结果:', result);
return NextResponse.json({
success: true,
object: result,
});
} catch (error) {
console.error('[PickObject] Error:', error);
return NextResponse.json({
success: false,
error: `识别失败: ${error instanceof Error ? error.message : '未知错误'}`,
}, { status: 500 });
}
}