Your Name 740860fde7
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: hardcode canvas vision to qwen3-vl and downscale pick-object input
2026-02-20 22:33:51 +08:00

149 lines
3.8 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 { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
export const runtime = "nodejs";
export const maxDuration = 60;
const requestSchema = z.object({
imageUrl: z.string().min(1, "图片地址不能为空"),
});
interface OcrItem {
id: string;
text: string;
confidence?: number;
}
function parseJsonText(raw: string): any | null {
if (!raw) return null;
const trimmed = raw.trim();
try {
return JSON.parse(trimmed);
} catch {
// continue
}
const markdownJsonMatch = trimmed.match(/```json\s*([\s\S]*?)\s*```/i);
if (markdownJsonMatch?.[1]) {
try {
return JSON.parse(markdownJsonMatch[1]);
} catch {
// continue
}
}
const objectMatch = trimmed.match(/\{[\s\S]*\}/);
if (objectMatch?.[0]) {
try {
return JSON.parse(objectMatch[0]);
} catch {
// continue
}
}
return null;
}
function sanitizeItems(items: unknown[]): OcrItem[] {
const result: OcrItem[] = [];
for (let index = 0; index < items.length; index += 1) {
const item = items[index] as Record<string, unknown>;
const rawText = typeof item?.text === "string" ? item.text : "";
const text = rawText.replace(/\s+/g, " ").trim();
if (!text) continue;
const confidenceValue =
typeof item?.confidence === "number" && Number.isFinite(item.confidence)
? Math.min(Math.max(item.confidence, 0), 1)
: undefined;
result.push({
id: String(item?.id || index + 1),
text,
confidence: confidenceValue,
});
}
return result.slice(0, 50);
}
function fallbackItemsFromRaw(raw: string): OcrItem[] {
const lines = raw
.split("\n")
.map((line) => line.replace(/^[-*\d.\s]+/, "").trim())
.filter(Boolean)
.slice(0, 20);
return lines.map((text, index) => ({
id: String(index + 1),
text,
}));
}
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const body = await request.json();
const parsed = requestSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ success: false, error: parsed.error.errors[0]?.message || "参数错误" }, { status: 400 });
}
const imageUrl = parsed.data.imageUrl.trim();
if (!/^https?:\/\//i.test(imageUrl)) {
return NextResponse.json({ success: false, error: "仅支持 URL 图片,不允许 base64 传输" }, { status: 400 });
}
console.log("[Canvas Text Detect] 使用硬编码模型: qwen3-vl");
const prompt = [
"你是图片文字识别助手。",
"请识别图片中可见文字并按阅读顺序输出 JSON。",
"只返回 JSON,不要额外说明。",
"格式:{\"items\":[{\"id\":\"1\",\"text\":\"示例\",\"confidence\":0.95}]}",
"如果没有识别到文字,返回 {\"items\":[]}。",
].join("\n");
const { text, model } = await callQwen3Vl({
imageUrl,
prompt,
temperature: 0.1,
maxTokens: 800,
});
const parsedJson = parseJsonText(text || "");
let items: OcrItem[] = [];
if (parsedJson && Array.isArray(parsedJson.items)) {
items = sanitizeItems(parsedJson.items);
}
if (items.length === 0 && text) {
items = fallbackItemsFromRaw(text);
}
return NextResponse.json({
success: true,
data: {
items,
model,
},
});
} catch (error) {
console.error("[Canvas Text Detect] Error:", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "文字识别失败",
},
{ status: 500 },
);
}
}