hoangquocviet's picture
aaa
a79e363
/**
* 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}`);
});