/** * VEditor Render Server * * Standalone Express API for video rendering using Remotion. * Designed for deployment on Hugging Face Spaces (port 7860). */ import express from 'express'; import cors from 'cors'; import path from 'path'; import fs from 'fs'; import { v4 as uuidv4 } from 'uuid'; import { renderVideo } from './render'; const app = express(); const PORT = process.env.PORT || 7860; const VIDEOS_DIR = path.join(process.cwd(), 'videos'); const JOBS_DIR = path.join(process.cwd(), 'jobs'); // Ensure directories exist [VIDEOS_DIR, JOBS_DIR].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // Middleware app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use('/videos', express.static(VIDEOS_DIR)); // Health check app.get('/', (req, res) => { res.json({ status: 'ok', service: 'VEditor Render Server', version: '1.0.0' }); }); // Start render job app.post('/render', async (req, res) => { try { const { design, options } = req.body; if (!design) { return res.status(400).json({ error: 'Design is required' }); } const jobId = `job_${Date.now()}_${uuidv4().substring(0, 8)}`; const jobFile = path.join(JOBS_DIR, `${jobId}.json`); const statusFile = path.join(JOBS_DIR, `${jobId}.status.json`); // Save job data fs.writeFileSync(jobFile, JSON.stringify({ design, options }, null, 2)); fs.writeFileSync(statusFile, JSON.stringify({ status: 'pending', progress: 0 })); console.log(`[Render] Starting job: ${jobId}`); // Start render in background renderVideo(jobId, design, options || {}) .then((firebaseUrl) => { // Recalculate path to ensure correct file is updated const completedStatusFile = path.join(JOBS_DIR, `${jobId}.status.json`); const completedData = JSON.stringify({ status: 'completed', progress: 100, outputUrl: firebaseUrl }); try { fs.writeFileSync(completedStatusFile, completedData); console.log(`[Render] Status file updated: ${completedStatusFile}`); console.log(`[Render] Status data: ${completedData}`); console.log(`[Render] Completed: ${jobId} -> ${firebaseUrl}`); } catch (writeError) { console.error(`[Render] Failed to write status file:`, writeError); } }) .catch((error) => { console.error(`[Render] Failed: ${jobId}`, error); const failedStatusFile = path.join(JOBS_DIR, `${jobId}.status.json`); try { fs.writeFileSync(failedStatusFile, JSON.stringify({ status: 'failed', progress: 0, error: error.message || 'Unknown error' })); } catch (writeError) { console.error(`[Render] Failed to write error status:`, writeError); } }); res.status(202).json({ jobId, status: 'pending', statusUrl: `/render/${jobId}` }); } catch (error) { console.error('[Render] Error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Check render status app.get('/render/:jobId', (req, res) => { const { jobId } = req.params; const statusFile = path.join(JOBS_DIR, `${jobId}.status.json`); if (!fs.existsSync(statusFile)) { return res.status(404).json({ error: 'Job not found' }); } const status = JSON.parse(fs.readFileSync(statusFile, 'utf-8')); res.json({ jobId, ...status }); }); // List all jobs app.get('/jobs', (req, res) => { const files = fs.readdirSync(JOBS_DIR).filter(f => f.endsWith('.status.json')); const jobs = files.map(f => { const jobId = f.replace('.status.json', ''); const status = JSON.parse(fs.readFileSync(path.join(JOBS_DIR, f), 'utf-8')); return { jobId, ...status }; }); res.json(jobs); }); // Start server app.listen(PORT, () => { console.log(`🎬 VEditor Render Server running on port ${PORT}`); console.log(`📁 Videos directory: ${VIDEOS_DIR}`); console.log(`📋 Jobs directory: ${JOBS_DIR}`); });