// server.js import express from "express"; import { v4 as uuidv4 } from "uuid"; import fs from "fs"; import path from "path"; import { spawn } from "child_process"; import multer from "multer"; import { fileURLToPath } from "url"; import { dirname } from "path"; // Import our generators import { SceneParser } from "./scripts/scene_parser.js"; import { SceneGraphGenerator } from "./scripts/scene_graph_generator.js"; import { CodeGenerator } from "./scripts/code_generator.js"; import { DebugReportGenerator } from "./scripts/debug_report_generator.js"; import { ThreeJSGLBGenerator } from "./scripts/glb_generator_threejs.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); app.use(express.json({ limit: "10mb" })); const TMP = path.join(process.cwd(), "tmp"); if (!fs.existsSync(TMP)) fs.mkdirSync(TMP, { recursive: true }); // For texture upload (optional) const upload = multer({ dest: path.join(TMP, "uploads/") }); // Cache Three.js availability check let threeJSAvailable = null; let threeJSCheckPromise = null; async function checkThreeJS() { if (threeJSAvailable !== null) { return threeJSAvailable; } if (threeJSCheckPromise) { return threeJSCheckPromise; } threeJSCheckPromise = (async () => { try { await import('three'); threeJSAvailable = true; return true; } catch (error) { threeJSAvailable = false; return false; } finally { threeJSCheckPromise = null; } })(); return threeJSCheckPromise; } // Check Three.js on startup checkThreeJS(); // Default Blender path based on OS const DEFAULT_BLENDER_PATH = process.platform === 'win32' ? "C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe" : "/usr/bin/blender"; // Standard Linux path // Check Blender availability function checkBlender() { const blenderPath = process.env.BLENDER_PATH || DEFAULT_BLENDER_PATH; try { // on Linux, we might check if 'blender' command exists if path is not absolute if (fs.existsSync(blenderPath)) { return { configured: true, path: blenderPath, status: "available" }; } else { return { configured: false, path: blenderPath, status: "not found" }; } } catch (error) { console.error("Error checking blender:", error); return { configured: false, path: blenderPath, status: "error checking" }; } } /** * GET /api/health * Application health and status endpoint */ app.get("/api/health", async (req, res) => { console.log("Health check requested"); try { const threeJSAvail = await checkThreeJS(); const blenderInfo = checkBlender(); const health = { status: "healthy", timestamp: new Date().toISOString(), uptime: process.uptime(), version: "2.0.0", services: { parser: "available", sceneGraph: "available", codeGenerator: "available", debugReport: "available", threeJS: threeJSAvail ? "available" : "not available", blender: blenderInfo.status, blenderPath: blenderInfo.path, blenderConfigured: blenderInfo.configured }, system: { nodeVersion: process.version, platform: process.platform, memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + " MB", total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + " MB" } } }; res.json(health); } catch (error) { console.error("Health check error:", error); res.status(500).json({ status: "error", message: error.message }); } }); /** * GET /generate * Information about the /generate endpoint */ app.get("/generate", (req, res) => { res.json({ message: "This endpoint only accepts POST requests", method: "POST", description: "Generate a GLB file from a scene description", contentType: "application/json", example: { curl: 'curl -X POST http://localhost:3000/generate -H "Content-Type: application/json" -d @example_scene.json -o result.glb', body: { sceneName: "test_scene", objects: [ { type: "cube", location: [1, 0, 0], size: 0.8, color: [1, 0, 0, 1] }, { type: "sphere", location: [-1, 0, 0], radius: 0.6, color: [0.2, 0.7, 0.3, 1] } ], lights: [ { type: "POINT", location: [2, 2, 3], energy: 200 } ], sun_energy: 3.0 } }, multipart: "You can also send multipart/form-data with a 'texture' file and 'scene' JSON field", note: "See example_scene.json for a complete example" }); }); /** * POST /generate * Accepts either: * 1. Text description: { "description": "Create a magical forest..." } * 2. Structured JSON: { "sceneName": "test", "objects": [...], ... } * 3. Multipart form with 'description' text field or 'scene' JSON field * * Returns a ZIP file containing: * - scene.glb * - scene-graph.json * - generation-code.js * - generation-script.py * - debug-report.txt */ // Store active generation statuses const activeGenerations = new Map(); // Helper function to send status update function sendStatusUpdate(id, status, message, progress = null) { if (activeGenerations.has(id)) { activeGenerations.get(id).status = status; activeGenerations.get(id).message = message; activeGenerations.get(id).progress = progress; activeGenerations.get(id).timestamp = new Date().toISOString(); } } app.post("/generate", upload.single("texture"), async (req, res) => { const id = uuidv4(); activeGenerations.set(id, { status: "starting", message: "Initializing generation...", progress: 0, timestamp: new Date().toISOString() }); try { let sceneData = {}; let description = null; sendStatusUpdate(id, "parsing", "Analyzing description...", 10); // Handle multipart form data if (req.body.description) { description = req.body.description; } else if (req.body.scene) { try { sceneData = typeof req.body.scene === 'string' ? JSON.parse(req.body.scene) : req.body.scene; } catch (e) { description = req.body.scene; } } else if (req.body && Object.keys(req.body).length) { if (req.body.description) { description = req.body.description; } else { sceneData = req.body; } } if (req.file) { sceneData.avatar_texture = path.resolve(req.file.path); } if (description) { const parser = new SceneParser(); sceneData = parser.parse(description); } sendStatusUpdate(id, "preparing", "Creating workspace...", 20); const workDir = path.join(TMP, id); if (!fs.existsSync(workDir)) { fs.mkdirSync(workDir, { recursive: true }); } const jsonPath = path.join(workDir, "scene-data.json"); const glbPath = path.join(workDir, "scene.glb"); const graphPath = path.join(workDir, "scene-graph.json"); const codeJSPath = path.join(workDir, "generation-code.js"); const codePyPath = path.join(workDir, "generation-script.py"); const debugPath = path.join(workDir, "debug-report.txt"); sendStatusUpdate(id, "saving", "Saving scene data...", 30); fs.writeFileSync(jsonPath, JSON.stringify(sceneData, null, 2)); console.log(`Scene data saved to: ${jsonPath}`); sendStatusUpdate(id, "graph", "Generating scene graph...", 40); const graphGenerator = new SceneGraphGenerator(); const sceneGraph = graphGenerator.generate(sceneData); fs.writeFileSync(graphPath, JSON.stringify(sceneGraph, null, 2)); sendStatusUpdate(id, "code", "Generating code files...", 50); const codeGenerator = new CodeGenerator(); const nodeJSCode = codeGenerator.generateNodeJS(sceneGraph, sceneData); const pythonCode = codeGenerator.generateBlenderPython(sceneGraph, sceneData); fs.writeFileSync(codeJSPath, nodeJSCode); fs.writeFileSync(codePyPath, pythonCode); sendStatusUpdate(id, "glb", "Generating GLB file...", 60); let glbGenerated = false; let generationLog = []; let warnings = []; let errors = []; let threeJSSuccess = false; // Only try Three.js - no Blender fallback try { sendStatusUpdate(id, "glb", "Attempting Three.js generation...", 65); const threeJSGenerator = new ThreeJSGLBGenerator(); const result = await threeJSGenerator.generate(sceneData, glbPath); if (result.success && fs.existsSync(glbPath)) { const stats = fs.statSync(glbPath); if (stats.size > 0) { glbGenerated = true; threeJSSuccess = true; generationLog.push("GLB generated successfully using Three.js"); sendStatusUpdate(id, "glb", "GLB generated successfully using Three.js", 90); } else { errors.push("GLB file was created but is empty (0 bytes)"); generationLog.push("GLB file is empty"); } } else { errors.push(`Three.js generation failed: ${result.error || 'Unknown error'}`); generationLog.push(`Three.js generation failed: ${result.error || 'Unknown error'}`); } } catch (error) { errors.push(`Three.js generation error: ${error.message}`); generationLog.push(`Three.js error: ${error.message}`); console.error("Three.js generation error:", error); } sendStatusUpdate(id, "debug", "Generating debug report...", 95); const debugGenerator = new DebugReportGenerator(); const debugReport = debugGenerator.generate(sceneData, sceneGraph, generationLog, warnings, errors); fs.writeFileSync(debugPath, debugReport); sendStatusUpdate(id, "complete", "Generation complete!", 100); // Verify which files actually exist const files = { glb: fs.existsSync(glbPath) ? `/download/${id}/scene.glb` : null, sceneGraph: fs.existsSync(graphPath) ? `/download/${id}/scene-graph.json` : null, codeJS: fs.existsSync(codeJSPath) ? `/download/${id}/generation-code.js` : null, codePy: fs.existsSync(codePyPath) ? `/download/${id}/generation-script.py` : null, debug: fs.existsSync(debugPath) ? `/download/${id}/debug-report.txt` : null }; // If GLB wasn't generated, add a more helpful error if (!glbGenerated && !fs.existsSync(glbPath)) { errors.push("GLB file was not generated. Check the debug report for details."); console.error(`GLB file missing at: ${glbPath}`); console.error(`Files in directory:`, fs.existsSync(workDir) ? fs.readdirSync(workDir) : "Directory doesn't exist"); } res.json({ success: glbGenerated && fs.existsSync(glbPath), sceneName: sceneData.sceneName || "generated_scene", id: id, files: files, filesExist: { glb: fs.existsSync(glbPath), sceneGraph: fs.existsSync(graphPath), codeJS: fs.existsSync(codeJSPath), codePy: fs.existsSync(codePyPath), debug: fs.existsSync(debugPath) }, summary: { nodes: sceneGraph.nodes?.length || 0, meshes: sceneGraph.meshes?.length || 0, materials: sceneGraph.materials?.length || 0, lights: sceneGraph.lights?.length || 0, hasAvatar: sceneData.avatar?.present || false, hasAudio: (sceneData.audio?.ambient?.length || 0) + (sceneData.audio?.spatial?.length || 0) > 0, hasEffects: Object.keys(sceneData.effects || {}).length > 0 }, warnings: warnings, errors: errors }); // Clean up after 5 minutes setTimeout(() => { activeGenerations.delete(id); }, 5 * 60 * 1000); } catch (err) { console.error(err); sendStatusUpdate(id, "error", `Error: ${err.message}`, null); res.status(500).json({ error: err.message, stack: err.stack, id: id }); } }); /** * GET /api/status/:id * Get live status of a generation */ app.get("/api/status/:id", (req, res) => { const { id } = req.params; const status = activeGenerations.get(id); if (!status) { return res.status(404).json({ error: "Generation not found" }); } res.json(status); }); /** * GET /download/:id/:filename * Download generated files */ app.get("/download/:id/:filename", (req, res) => { try { const { id, filename } = req.params; const filePath = path.join(TMP, id, filename); console.log(`Download request: ${filename} from ${filePath}`); if (!fs.existsSync(filePath)) { console.error(`File not found: ${filePath}`); console.error(`Directory exists: ${fs.existsSync(path.dirname(filePath))}`); if (fs.existsSync(path.dirname(filePath))) { console.error(`Files in directory:`, fs.readdirSync(path.dirname(filePath))); } return res.status(404).json({ error: "File not found", path: filePath, message: `The file ${filename} was not generated. Check the debug report for details.` }); } const stats = fs.statSync(filePath); if (stats.size === 0) { return res.status(404).json({ error: "File is empty", message: `The file ${filename} exists but is empty (0 bytes). Generation may have failed.` }); } const ext = path.extname(filename).toLowerCase(); const contentTypes = { '.glb': 'model/gltf-binary', '.json': 'application/json', '.js': 'application/javascript', '.py': 'text/x-python', '.txt': 'text/plain' }; res.setHeader("Content-Type", contentTypes[ext] || 'application/octet-stream'); res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); res.setHeader("Content-Length", stats.size); const stream = fs.createReadStream(filePath); stream.on('error', (err) => { console.error(`Stream error for ${filePath}:`, err); if (!res.headersSent) { res.status(500).json({ error: `Failed to read file: ${err.message}` }); } }); stream.pipe(res); } catch (err) { console.error(`Download error:`, err); if (!res.headersSent) { res.status(500).json({ error: err.message, stack: err.stack }); } } }); /** * GET /api/status * Get API status and capabilities */ app.get("/api/status", (req, res) => { res.json({ status: "running", version: "2.0.0", capabilities: { textParsing: true, threeJSGeneration: true, blenderGeneration: checkBlender().configured, sceneGraph: true, codeGeneration: true, debugReports: true }, endpoints: { "POST /generate": "Generate GLB from text description or structured JSON", "GET /download/:id/:filename": "Download generated files", "GET /api/status": "Get API status" } }); }); /** * GET / * Serve the HTML UI */ app.get("/", (req, res) => { const htmlPath = path.join(__dirname, "public", "index.html"); if (fs.existsSync(htmlPath)) { res.sendFile(htmlPath); } else { res.status(404).json({ error: "HTML file not found", message: "Please check your deployment." }); } }); // Static files (after route handlers to avoid conflicts) app.use(express.static("public")); // Global error handler app.use((err, req, res, next) => { console.error("Unhandled error:", err); if (!res.headersSent) { res.status(500).json({ error: "Internal Server Error", message: err.message }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`GLB Generation Engine running on http://localhost:${PORT}`); console.log(`UI available at http://localhost:${PORT}`); console.log(`API documentation at http://localhost:${PORT}/api/status`); });