Spaces:
Configuration error
Configuration error
Dee Ferdinand
feat: upload rendered MP4s to HuggingFace Dataset repo instead of local download
3cd9013 | import express from 'express'; | |
| import fileUpload from 'express-fileupload'; | |
| import cors from 'cors'; | |
| import { WebSocketServer } from 'ws'; | |
| import { createServer } from 'http'; | |
| import { v4 as uuid } from 'uuid'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| import { fileURLToPath } from 'url'; | |
| import { renderVideo } from './lib/renderer.js'; | |
| import { buildComposition } from './lib/composer.js'; | |
| import { getMusicTrack } from './lib/music.js'; | |
| import { uploadToHuggingFace } from './lib/uploader.js'; | |
| import { WORKFLOWS } from './workflows/index.js'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const app = express(); | |
| const server = createServer(app); | |
| const wss = new WebSocketServer({ server }); | |
| const jobs = new Map(); | |
| const clients = new Map(); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| app.use(fileUpload({ limits: { fileSize: 500 * 1024 * 1024 }, useTempFiles: true, tempFileDir: '/tmp/' })); | |
| app.use('/static', express.static(path.join(__dirname, 'static'))); | |
| app.use('/renders', express.static(path.join(__dirname, 'renders'))); | |
| app.use('/', express.static(path.join(__dirname, 'static'))); | |
| wss.on('connection', (ws, req) => { | |
| const jobId = new URL(req.url, 'http://x').searchParams.get('job'); | |
| if (jobId) clients.set(jobId, ws); | |
| ws.on('close', () => { for (const [id, c] of clients) if (c === ws) clients.delete(id); }); | |
| }); | |
| function push(jobId, data) { | |
| const ws = clients.get(jobId); | |
| if (ws?.readyState === 1) ws.send(JSON.stringify(data)); | |
| const job = jobs.get(jobId); | |
| if (job) Object.assign(job, data); | |
| } | |
| app.get('/api/health', (_, res) => res.json({ | |
| status: 'ok', | |
| time: new Date().toISOString(), | |
| hf_renders_repo: process.env.HF_RENDERS_REPO || 'AIgoose/video-renders', | |
| hf_token_set: !!process.env.HF_TOKEN, | |
| })); | |
| app.get('/api/workflows', (_, res) => res.json( | |
| Object.entries(WORKFLOWS).map(([id, w]) => ({ id, ...w.meta })) | |
| )); | |
| app.get('/api/music', (_, res) => { | |
| const lib = path.join(__dirname, 'music/library'); | |
| const tracks = fs.existsSync(lib) | |
| ? fs.readdirSync(lib).filter(f => f.endsWith('.mp3') || f.endsWith('.wav')) | |
| : []; | |
| const meta = tracks.map(f => { | |
| const metaFile = path.join(lib, f.replace(/\.(mp3|wav)$/, '.json')); | |
| const m = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, 'utf8')) : {}; | |
| return { file: f, ...m }; | |
| }); | |
| res.json(meta); | |
| }); | |
| app.get('/api/jobs', (_, res) => | |
| res.json([...jobs.values()].sort((a, b) => b.createdAt - a.createdAt)) | |
| ); | |
| app.get('/api/jobs/:id', (req, res) => { | |
| const job = jobs.get(req.params.id); | |
| if (!job) return res.status(404).json({ error: 'Not found' }); | |
| res.json(job); | |
| }); | |
| app.post('/api/render', async (req, res) => { | |
| const jobId = uuid(); | |
| const config = { | |
| workflow: req.body.workflow || 'testimonial', | |
| projectName: req.body.projectName || req.body.clientName || 'untitled', | |
| clientName: req.body.clientName || '', | |
| trainerName: req.body.trainerName || 'Dee Ferdinand', | |
| tagline: req.body.tagline || 'AI Corporate Trainer', | |
| website: req.body.website || 'deeferdinand.com', | |
| musicTrack: req.body.musicTrack || 'auto', | |
| musicVolume: parseFloat(req.body.musicVolume || '0.3'), | |
| format: req.body.format || '9:16', | |
| duration: parseInt(req.body.duration || '75'), | |
| }; | |
| const job = { id: jobId, status: 'queued', progress: 0, ...config, createdAt: Date.now(), files: [] }; | |
| jobs.set(jobId, job); | |
| const projectDir = path.join(__dirname, 'uploads', jobId); | |
| fs.mkdirSync(projectDir, { recursive: true }); | |
| const uploadedFiles = req.files ? Object.values(req.files).flat() : []; | |
| for (const f of uploadedFiles) { | |
| const dest = path.join(projectDir, f.name); | |
| await f.mv(dest); | |
| job.files.push({ name: f.name, path: dest, mime: f.mimetype, size: f.size }); | |
| } | |
| config._files = job.files; | |
| res.json({ jobId, status: 'queued' }); | |
| setImmediate(() => runJob(jobId, projectDir, config)); | |
| }); | |
| // Legacy local download (fallback if HF upload fails) | |
| app.get('/api/download/:jobId', (req, res) => { | |
| const job = jobs.get(req.params.jobId); | |
| if (!job?.outputPath || !fs.existsSync(job.outputPath)) | |
| return res.status(404).json({ error: 'Not ready or already uploaded to HF' }); | |
| res.download(job.outputPath); | |
| }); | |
| async function runJob(jobId, projectDir, config) { | |
| try { | |
| push(jobId, { status: 'composing', progress: 5 }); | |
| const musicPath = await getMusicTrack( | |
| config.musicTrack, config.workflow, config.duration, | |
| (d) => push(jobId, d) | |
| ); | |
| push(jobId, { progress: 15 }); | |
| const compDir = path.join(__dirname, 'compositions', 'projects', jobId); | |
| fs.mkdirSync(compDir, { recursive: true }); | |
| const workflow = WORKFLOWS[config.workflow]; | |
| if (!workflow) throw new Error(`Unknown workflow: ${config.workflow}`); | |
| await buildComposition(compDir, projectDir, musicPath, config, workflow, | |
| (p) => push(jobId, { status: 'composing', progress: 15 + Math.round(p * 0.35) })); | |
| push(jobId, { status: 'rendering', progress: 50 }); | |
| const outDir = path.join(__dirname, 'renders'); | |
| fs.mkdirSync(outDir, { recursive: true }); | |
| const outputFile = path.join(outDir, `${jobId}.mp4`); | |
| await renderVideo(compDir, outputFile, config, | |
| (p) => push(jobId, { status: 'rendering', progress: 50 + Math.round(p * 0.35) })); | |
| push(jobId, { status: 'uploading', progress: 88 }); | |
| // Build a human-readable filename | |
| const slug = (config.clientName || config.projectName || 'video') | |
| .toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40); | |
| const ts = new Date().toISOString().slice(0, 10); | |
| const hfFilename = `${slug}-${config.workflow}-${ts}-${jobId.slice(0, 8)}.mp4`; | |
| let hfResult = null; | |
| try { | |
| hfResult = await uploadToHuggingFace(outputFile, hfFilename); | |
| // Clean up local render to save Space disk | |
| fs.unlinkSync(outputFile); | |
| } catch (uploadErr) { | |
| console.warn('[upload warning] HF upload failed, keeping local file:', uploadErr.message); | |
| } | |
| push(jobId, { | |
| status: 'done', | |
| progress: 100, | |
| outputPath: hfResult ? null : outputFile, | |
| hf_url: hfResult?.hf_url || null, | |
| viewer_url: hfResult?.viewer_url || null, | |
| hf_filename: hfResult?.filename || null, | |
| repo_id: hfResult?.repo_id || null, | |
| outputUrl: hfResult ? hfResult.hf_url : `/renders/${jobId}.mp4`, | |
| }); | |
| } catch (err) { | |
| console.error('[job error]', jobId, err); | |
| push(jobId, { status: 'error', error: err.message }); | |
| } | |
| } | |
| const PORT = process.env.PORT || 7860; | |
| server.listen(PORT, () => console.log(`\u2705 Dee Video Studio on port ${PORT}`)); | |