File size: 3,714 Bytes
2704918
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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<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.";

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