Dee Ferdinand
feat: upload rendered MP4s to HuggingFace Dataset repo instead of local download
3cd9013
Raw
History Blame Contribute Delete
6.6 kB
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}`));