venkat_glb_creation / server.js
Antigravity AI
Reorder routes to fix API 404/500 issues
30d674d
Raw
History Blame Contribute Delete
15.6 kB
// 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`);
});