2026-02-02 12:06:57 +08:00

219 lines
6.5 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,
"boundingBox": { "x": 0.1, "y": 0.2, "width": 0.3, "height": 0.4 }
}
【name 格式】≤8字,格式:[颜色/材质]+[物品]
正确:黑色沙发、白色花瓶、木质书架、玻璃茶几
错误:一个黑色的皮质沙发(太长)
【position 格式】固定4字,从以下选择:
${POSITION_OPTIONS.join('、')}
【boundingBox】相对坐标 0-1,精确到小数点后2位
`.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');
}
result = JSON.parse(jsonMatch[0]);
// 验证和修正结果
if (!result.name || result.name.length > 12) {
result.name = result.name?.substring(0, 8) || '未知物品';
}
// 验证位置是否在词库中
if (!POSITION_OPTIONS.includes(result.position as any)) {
// 根据点击位置推算
const posX = clickX < 0.33 ? '左' : clickX > 0.66 ? '右' : '中央';
const posY = clickY < 0.33 ? '上' : clickY > 0.66 ? '下' : '中';
if (posX === '中央') {
result.position = posY === '中' ? '中央区域' : posY === '上' ? '中央顶部' : '中央底部';
} else {
result.position = posY === '中' ? `${posX}侧中部` : `${posX}${posY}区域`;
}
}
// 确保 boundingBox 有效
if (!result.boundingBox ||
typeof result.boundingBox.x !== 'number' ||
typeof result.boundingBox.y !== 'number') {
result.boundingBox = {
x: Math.max(0, clickX - 0.15),
y: Math.max(0, clickY - 0.15),
width: 0.3,
height: 0.3,
};
}
// 确保 confidence 有效
if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {
result.confidence = 0.85;
}
} 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: Math.max(0, clickX - 0.15),
y: Math.max(0, clickY - 0.15),
width: 0.3,
height: 0.3,
},
};
}
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 });
}
}