File size: 4,922 Bytes
160d275
 
 
 
 
 
 
 
ce5356c
160d275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce5356c
160d275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import fs from "fs";
import path from "path";
import express from "express";
import axios from "axios";
import sharp from "sharp";
import cors from "cors";
import FormData from "form-data";
import dotenv from "dotenv";
import os from "os";

dotenv.config({ path: ".env.local" });

const app = express();
const PORT = process.env.PORT || 5000;

const ACCOUNT_ID = process.env.ACCOUNT_ID;
const API_TOKEN = process.env.API_TOKEN;

console.log("ACCOUNT_ID:", process.env.ACCOUNT_ID);
console.log("API_TOKEN exists:", !!process.env.API_TOKEN);
console.log("HF_TOKEN exists:", !!process.env.HF_TOKEN);

app.use(cors({ origin: "*" }));
app.use(express.json());

function normalizePrompt(userPrompt) {
    return `Create exactly this object and no other object: "${userPrompt}". Center it as one complete product-shaped object on a plain white background. Use a clean solid dark silhouette suitable for 3D reconstruction. Do not generate a bottle unless the requested object is a bottle. No text, no labels, no scene, no shadows, no extra objects.`;
}

app.post("/api/generate-heightmap", async (req, res) => {
    const prompt = String(req.body?.prompt || "").trim();

    if (!prompt) {
        return res.status(400).json({ error: "Prompt is required" });
    }

    const normalizedPrompt = normalizePrompt(prompt);
    res.set("Cache-Control", "no-store");

    try {
        console.log(`[HOP 1] βœ… Request received from frontend. Prompt: "${prompt}"`);

        // 1. Generate image (Cloudflare)
        console.log('⏳ [STEP 1] Calling Cloudflare...');
        const response = await axios.post(
            `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/black-forest-labs/flux-1-schnell`,
            { prompt: normalizedPrompt },
            { headers: { Authorization: `Bearer ${API_TOKEN}` } }
        );
        console.log(`[HOP 2] βœ… Cloudflare responded. Status: ${response.status}`);

        if (!response.data?.result?.image) {
            return res.status(500).json({ error: "Cloudflare did not return an image" });
        }

        const base64Image = response.data.result.image;
        const inputBuffer = Buffer.from(base64Image, "base64");

        // 2. Send image to Python service for background removal (Explicitly targeting 8080)
        console.log('⏳ [STEP 2] Calling Python Worker...');
        const formData = new FormData();
        formData.append('file', inputBuffer, {
            filename: 'image.png',
            contentType: 'image/png',
        });

        const pythonWorkerUrl = process.env.PYTHON_WORKER_URL || "http://localhost:8080";
        const pythonServiceResponse = await axios.post(
            `${pythonWorkerUrl.replace(/\/$/, "")}/remove-bg`,
            formData,
            {
                headers: { ...formData.getHeaders() },
                responseType: 'arraybuffer',
                timeout: 10000 // 10s absolute network cutoff guard
            }
        );
        console.log(`[HOP 3] βœ… Python service responded. Status: ${pythonServiceResponse.status}`);

        const noBgBuffer = Buffer.from(pythonServiceResponse.data);

        // Safety verification check of incoming image data bounds
        if (!noBgBuffer || noBgBuffer.length < 100) {
            throw new Error("Received an invalid or empty image buffer from the Python background removal worker.");
        }

        // 3. Clean image for heightmap with explicit promise wrapping protection
        console.log(`[HOP 4] ⏳ Processing with Sharp...`);

        const processed = await Promise.race([
            sharp(noBgBuffer)
                .ensureAlpha()
                .flatten({ background: { r: 255, g: 255, b: 255 } })
                .resize(128, 128)
                .grayscale()
                .threshold(240)
                .negate({ alpha: false })
                .png()
                .toBuffer(),
            new Promise((_, reject) => setTimeout(() => reject(new Error("Sharp pipeline timed out")), 5000))
        ]);

        console.log(`[HOP 4] βœ… Sharp processing complete. Output size: ${processed.length} bytes`);

        // --- DEBUG LAYER: SAVE TO DISK ---
        const debugPath = path.join(os.tmpdir(), "debug_output.jpg");
        fs.writeFileSync(debugPath, processed);
        console.log(`[DEBUG] πŸ’Ύ Image saved locally to: ${debugPath}`);
        // ---------------------------------

        const finalBase64 = processed.toString("base64");
        return res.json({ image: `data:image/png;base64,${finalBase64}`, prompt });

    } catch (error) {
        console.error(`[ERROR] ❌ Pipeline failed at some stage:`);
        console.error(`[ERROR] Message: ${error.message}`);
        return res.status(500).json({
            error: "generation failed",
            details: error.message
        });
    }
});

app.listen(PORT, () => {
    console.log(`πŸš€ 3D PROXY ENGINE RUNNING ON port ${PORT}`);
});