Dee Ferdinand commited on
Commit
b84a129
·
1 Parent(s): f88cf50

feat: init Dee Video Studio — HyperFrames cloud render for testimonial/teaser/trailer videos

Browse files

- 4 workflows: testimonial (75s), teaser (30s), trailer (60s), community (60s)
- 6 royalty-free Pixabay music tracks auto-selected by workflow
- WebSocket real-time render progress
- Dark-mode Studio UI with drag-drop upload
- HuggingFace Docker Space ready (port 7860)

Files changed (1) hide show
  1. server.js +144 -0
server.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import fileUpload from 'express-fileupload';
3
+ import cors from 'cors';
4
+ import { WebSocketServer } from 'ws';
5
+ import { createServer } from 'http';
6
+ import { v4 as uuid } from 'uuid';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { renderVideo } from './lib/renderer.js';
11
+ import { buildComposition } from './lib/composer.js';
12
+ import { getMusicTrack } from './lib/music.js';
13
+ import { WORKFLOWS } from './workflows/index.js';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const app = express();
17
+ const server = createServer(app);
18
+ const wss = new WebSocketServer({ server });
19
+
20
+ const jobs = new Map();
21
+ const clients = new Map();
22
+
23
+ app.use(cors());
24
+ app.use(express.json());
25
+ app.use(fileUpload({ limits: { fileSize: 500 * 1024 * 1024 }, useTempFiles: true, tempFileDir: '/tmp/' }));
26
+ app.use('/static', express.static(path.join(__dirname, 'static')));
27
+ app.use('/renders', express.static(path.join(__dirname, 'renders')));
28
+ app.use('/', express.static(path.join(__dirname, 'static')));
29
+
30
+ wss.on('connection', (ws, req) => {
31
+ const jobId = new URL(req.url, 'http://x').searchParams.get('job');
32
+ if (jobId) clients.set(jobId, ws);
33
+ ws.on('close', () => { for (const [id, c] of clients) if (c === ws) clients.delete(id); });
34
+ });
35
+
36
+ function push(jobId, data) {
37
+ const ws = clients.get(jobId);
38
+ if (ws?.readyState === 1) ws.send(JSON.stringify(data));
39
+ const job = jobs.get(jobId);
40
+ if (job) Object.assign(job, data);
41
+ }
42
+
43
+ app.get('/api/health', (_, res) => res.json({ status: 'ok', time: new Date().toISOString() }));
44
+
45
+ app.get('/api/workflows', (_, res) => res.json(
46
+ Object.entries(WORKFLOWS).map(([id, w]) => ({ id, ...w.meta }))
47
+ ));
48
+
49
+ app.get('/api/music', (_, res) => {
50
+ const lib = path.join(__dirname, 'music/library');
51
+ const tracks = fs.existsSync(lib)
52
+ ? fs.readdirSync(lib).filter(f => f.endsWith('.mp3') || f.endsWith('.wav'))
53
+ : [];
54
+ const meta = tracks.map(f => {
55
+ const metaFile = path.join(lib, f.replace(/\.(mp3|wav)$/, '.json'));
56
+ const m = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, 'utf8')) : {};
57
+ return { file: f, ...m };
58
+ });
59
+ res.json(meta);
60
+ });
61
+
62
+ app.get('/api/jobs', (_, res) =>
63
+ res.json([...jobs.values()].sort((a, b) => b.createdAt - a.createdAt))
64
+ );
65
+
66
+ app.get('/api/jobs/:id', (req, res) => {
67
+ const job = jobs.get(req.params.id);
68
+ if (!job) return res.status(404).json({ error: 'Not found' });
69
+ res.json(job);
70
+ });
71
+
72
+ app.post('/api/render', async (req, res) => {
73
+ const jobId = uuid();
74
+ const config = {
75
+ workflow: req.body.workflow || 'testimonial',
76
+ projectName: req.body.projectName || req.body.clientName || 'untitled',
77
+ clientName: req.body.clientName || '',
78
+ trainerName: req.body.trainerName || 'Dee Ferdinand',
79
+ tagline: req.body.tagline || 'AI Corporate Trainer',
80
+ website: req.body.website || 'deeferdinand.com',
81
+ musicTrack: req.body.musicTrack || 'auto',
82
+ musicVolume: parseFloat(req.body.musicVolume || '0.3'),
83
+ format: req.body.format || '9:16',
84
+ duration: parseInt(req.body.duration || '75'),
85
+ };
86
+
87
+ const job = { id: jobId, status: 'queued', progress: 0, ...config, createdAt: Date.now(), files: [] };
88
+ jobs.set(jobId, job);
89
+
90
+ const projectDir = path.join(__dirname, 'uploads', jobId);
91
+ fs.mkdirSync(projectDir, { recursive: true });
92
+
93
+ const uploadedFiles = req.files ? Object.values(req.files).flat() : [];
94
+ for (const f of uploadedFiles) {
95
+ const dest = path.join(projectDir, f.name);
96
+ await f.mv(dest);
97
+ job.files.push({ name: f.name, path: dest, mime: f.mimetype, size: f.size });
98
+ }
99
+ config._files = job.files;
100
+
101
+ res.json({ jobId, status: 'queued' });
102
+ setImmediate(() => runJob(jobId, projectDir, config));
103
+ });
104
+
105
+ app.get('/api/download/:jobId', (req, res) => {
106
+ const job = jobs.get(req.params.jobId);
107
+ if (!job?.outputPath || !fs.existsSync(job.outputPath))
108
+ return res.status(404).json({ error: 'Not ready' });
109
+ res.download(job.outputPath);
110
+ });
111
+
112
+ async function runJob(jobId, projectDir, config) {
113
+ try {
114
+ push(jobId, { status: 'composing', progress: 5 });
115
+ const musicPath = await getMusicTrack(config.musicTrack, config.workflow, config.duration,
116
+ (d) => push(jobId, d));
117
+ push(jobId, { progress: 15 });
118
+
119
+ const compDir = path.join(__dirname, 'compositions', 'projects', jobId);
120
+ fs.mkdirSync(compDir, { recursive: true });
121
+
122
+ const workflow = WORKFLOWS[config.workflow];
123
+ if (!workflow) throw new Error(`Unknown workflow: ${config.workflow}`);
124
+
125
+ await buildComposition(compDir, projectDir, musicPath, config, workflow,
126
+ (p) => push(jobId, { status: 'composing', progress: 15 + Math.round(p * 0.4) }));
127
+ push(jobId, { status: 'rendering', progress: 55 });
128
+
129
+ const outDir = path.join(__dirname, 'renders');
130
+ fs.mkdirSync(outDir, { recursive: true });
131
+ const outputFile = path.join(outDir, `${jobId}.mp4`);
132
+
133
+ await renderVideo(compDir, outputFile, config,
134
+ (p) => push(jobId, { status: 'rendering', progress: 55 + Math.round(p * 0.4) }));
135
+
136
+ push(jobId, { status: 'done', progress: 100, outputPath: outputFile, outputUrl: `/renders/${jobId}.mp4` });
137
+ } catch (err) {
138
+ console.error('[job error]', jobId, err);
139
+ push(jobId, { status: 'error', error: err.message });
140
+ }
141
+ }
142
+
143
+ const PORT = process.env.PORT || 7860;
144
+ server.listen(PORT, () => console.log(`✅ Dee Video Studio on port ${PORT}`));