Spaces:
Runtime error
Runtime error
| // 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`); | |
| }); | |