| 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); |
| |
| 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 }, |
| ); |
| } |
| } |
|
|
|
|