PraxaLing / app /api /vision /analyze /route.ts
Reubencf's picture
Deploy PraxaLing: component refactor, bug fixes, unified neo-brutal design, Qwen3.5 everywhere
2992997
Raw
History Blame Contribute Delete
2.92 kB
import { NextResponse } from "next/server";
import { put } from "@vercel/blob";
import { getSession } from "@/lib/auth/session";
import { analyzeWithGemma } from "@/lib/hf/gemma-vision";
import { LEVELS, type Level } from "@/lib/languages";
export const maxDuration = 90;
export async function POST(req: Request) {
const session = await getSession();
if (!session) return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
if (!session.targetLang || !session.nativeLang || !session.level) {
return NextResponse.json({ error: "no_profile" }, { status: 400 });
}
const form = await req.formData().catch(() => null);
const file = form?.get("image");
if (!(file instanceof File)) return NextResponse.json({ error: "missing_image" }, { status: 400 });
if (file.size > 8 * 1024 * 1024) return NextResponse.json({ error: "image_too_large" }, { status: 413 });
const buf = Buffer.from(await file.arrayBuffer());
const imageBase64 = buf.toString("base64");
const mimeType = file.type || "image/jpeg";
const requestedLevel = String(form?.get("level") ?? "");
const level = (LEVELS as readonly string[]).includes(requestedLevel)
? (requestedLevel as Level)
: (session.level as Level);
try {
const result = await analyzeWithGemma({
imageBase64,
mimeType,
targetLang: session.targetLang,
nativeLang: session.nativeLang,
level,
accessToken: session.accessToken,
});
const mappedObjects = result.objects.map((o) => ({
label: o.labelEn,
translation: o.labelTarget,
translationSegments: o.labelTargetSegments,
romanized: o.romanized,
box: o.box,
score: 1,
}));
let imageUrl = "";
if (process.env.BLOB_READ_WRITE_TOKEN) {
try {
const blob = new Blob([buf], { type: mimeType });
const upload = await put(
`vision/${session.hfId}/${Date.now()}-${file.name || "photo.jpg"}`,
blob,
{ access: "public", contentType: mimeType },
);
imageUrl = upload.url;
} catch (err) {
console.warn("blob upload failed", err);
}
}
return NextResponse.json({
id: crypto.randomUUID(),
caption: result.caption,
objects: mappedObjects,
sentences: result.sentences,
imageUrl,
});
} catch (err) {
console.error("Gemma vision failed", err);
// Extract human-readable message from HuggingFace ProviderApiError
const hfMessage =
(err as { httpResponse?: { body?: { message?: string } } })?.httpResponse?.body?.message ??
(err instanceof Error ? err.message : null);
const status = (err as { httpResponse?: { status?: number } })?.httpResponse?.status ?? 502;
return NextResponse.json(
{ error: "inference_failed", message: hfMessage ?? "Gemma analysis failed. Please try again." },
{ status: status === 429 ? 429 : 502 },
);
}
}