import { createFileRoute } from "@tanstack/react-router"; import { z } from "zod"; import { writeFileSync, unlinkSync } from "fs"; import { execSync } from "child_process"; import path from "path"; const QuerySchema = z.object({ imageUrl: z.string().url(), prompt: z.string().optional(), }); // Cache descriptions to avoid running expensive CPU inference repeatedly const descriptionCache: Record = {}; export const Route = createFileRoute("/api/describe-image")({ server: { handlers: { POST: async ({ request }) => { try { const body = QuerySchema.parse(await request.json()); const imageUrl = body.imageUrl; const prompt = body.prompt || "Describe this image in detail."; // Check cache if (descriptionCache[imageUrl]) { return new Response(JSON.stringify({ success: true, description: descriptionCache[imageUrl] }), { status: 200, headers: { "Content-Type": "application/json" }, }); } // Fetch original image const response = await fetch(imageUrl, { headers: { "User-Agent": "LovableGrabber/1.0" }, }); if (!response.ok) { throw new Error(`Failed to fetch image: HTTP ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // Get file extension from URL const urlObj = new URL(imageUrl); let ext = path.extname(urlObj.pathname).toLowerCase() || ".jpg"; if (ext === ".ico" || !ext) ext = ".jpg"; // fallback to standard jpg for llama.cpp const ts = Date.now(); const tempImagePath = `/tmp/describe_${ts}${ext}`; writeFileSync(tempImagePath, buffer); // Spawn python describer script const scriptPath = path.join(process.cwd(), "src", "lib", "grabber", "describe.py"); let description = ""; try { console.log(`Spawning description job for ${imageUrl}...`); const stdout = execSync( `python3 "${scriptPath}" "${tempImagePath}" "${prompt.replace(/"/g, '\\"')}"`, { encoding: "utf-8" } ); // Parse result from python logs const startMarker = "---RESULT_START---"; const endMarker = "---RESULT_END---"; const startIdx = stdout.indexOf(startMarker); const endIdx = stdout.indexOf(endMarker); if (startIdx !== -1 && endIdx !== -1) { description = stdout.substring(startIdx + startMarker.length, endIdx).trim(); } else { console.error("Python output lacked start/end markers. Full output:", stdout); throw new Error("Failed to extract description from output"); } } finally { // Clean up temp image try { unlinkSync(tempImagePath); } catch { // ignore } } if (description) { descriptionCache[imageUrl] = description; } return new Response(JSON.stringify({ success: true, description }), { status: 200, headers: { "Content-Type": "application/json" }, }); } catch (err) { return new Response( JSON.stringify({ error: err instanceof Error ? err.message : "Failed to describe image" }), { status: 400, headers: { "Content-Type": "application/json" }, } ); } }, }, }, });