| 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(), |
| }); |
|
|
| |
| const descriptionCache: Record<string, string> = {}; |
|
|
| 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."; |
|
|
| |
| if (descriptionCache[imageUrl]) { |
| return new Response(JSON.stringify({ success: true, description: descriptionCache[imageUrl] }), { |
| status: 200, |
| headers: { "Content-Type": "application/json" }, |
| }); |
| } |
|
|
| |
| 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); |
|
|
| |
| const urlObj = new URL(imageUrl); |
| let ext = path.extname(urlObj.pathname).toLowerCase() || ".jpg"; |
| if (ext === ".ico" || !ext) ext = ".jpg"; |
|
|
| const ts = Date.now(); |
| const tempImagePath = `/tmp/describe_${ts}${ext}`; |
| writeFileSync(tempImagePath, buffer); |
|
|
| |
| 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" }, |
| } |
| ); |
| } |
| }, |
| }, |
| }, |
| }); |
| |