Dee Ferdinand commited on
Commit
345fbd8
·
1 Parent(s): a60741e

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. lib/composer.js +107 -0
lib/composer.js ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ function pickMedia(mediaList, preferVideo = true, index = 0) {
5
+ const videos = mediaList.filter(m => m.mime?.startsWith('video/'));
6
+ const images = mediaList.filter(m => m.mime?.startsWith('image/'));
7
+ if (preferVideo && videos.length > index) return videos[index];
8
+ if (images.length > index) return images[index];
9
+ return mediaList[index % Math.max(mediaList.length, 1)] || null;
10
+ }
11
+
12
+ function captionOverlay(text, startSec, durationSec, style = 'default') {
13
+ const styles = {
14
+ default: 'font-size:42px;font-weight:700;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,0.8);text-align:center;padding:0 40px;',
15
+ badge: 'font-size:22px;font-weight:600;color:#fff;background:rgba(0,0,0,0.6);padding:8px 20px;border-radius:8px;',
16
+ hook: 'font-size:56px;font-weight:900;color:#fff;text-shadow:0 4px 16px rgba(0,0,0,0.9);line-height:1.1;text-align:center;padding:0 32px;',
17
+ stat: 'font-size:68px;font-weight:900;color:#fff;text-shadow:0 4px 20px rgba(0,0,0,0.85);text-align:center;',
18
+ brand: 'font-size:28px;font-weight:500;color:#fff;letter-spacing:0.04em;text-align:center;',
19
+ };
20
+ return `
21
+ <div class="clip caption-overlay"
22
+ data-start="${startSec}"
23
+ data-duration="${durationSec}"
24
+ data-track-index="10"
25
+ style="position:absolute;bottom:140px;left:0;right:0;display:flex;align-items:flex-end;justify-content:center;pointer-events:none;">
26
+ <span style="${styles[style] || styles.default}">${text}</span>
27
+ </div>`;
28
+ }
29
+
30
+ function clipBlock(srcRel, startSec, durationSec, trackIdx) {
31
+ return `
32
+ <video class="clip" data-start="${startSec}" data-duration="${durationSec}" data-track-index="${trackIdx}"
33
+ src="${srcRel}" muted playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>`;
34
+ }
35
+
36
+ function imageBlock(srcRel, startSec, durationSec, trackIdx) {
37
+ return `
38
+ <img class="clip" data-start="${startSec}" data-duration="${durationSec}" data-track-index="${trackIdx}"
39
+ src="${srcRel}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;">`;
40
+ }
41
+
42
+ export async function buildComposition(compDir, projectDir, musicPath, config, workflow, onProgress) {
43
+ const files = config._files || [];
44
+ const { trainerName, tagline, website, clientName, format, duration: totalDur } = config;
45
+
46
+ const assetsDir = path.join(compDir, 'assets');
47
+ fs.mkdirSync(assetsDir, { recursive: true });
48
+
49
+ const linkedMedia = [];
50
+ for (const f of files) {
51
+ const dest = path.join(assetsDir, path.basename(f.path));
52
+ if (!fs.existsSync(dest)) fs.copyFileSync(f.path, dest);
53
+ linkedMedia.push({ ...f, localPath: dest, rel: `assets/${path.basename(f.path)}` });
54
+ }
55
+
56
+ const musicDest = path.join(assetsDir, path.basename(musicPath));
57
+ if (!fs.existsSync(musicDest)) fs.copyFileSync(musicPath, musicDest);
58
+ const musicRel = `assets/${path.basename(musicPath)}`;
59
+
60
+ onProgress?.(0.2);
61
+ const cuts = workflow.generateCuts(linkedMedia, config);
62
+ onProgress?.(0.5);
63
+
64
+ const [w, h] = format === '9:16' ? [1080, 1920] : format === '1:1' ? [1080, 1080] : [1920, 1080];
65
+
66
+ let clipBlocks = '';
67
+ let captionBlocks = '';
68
+ let trackIdx = 0;
69
+
70
+ for (const cut of cuts) {
71
+ if (cut.mediaFile) {
72
+ if (cut.mediaFile.mime?.startsWith('video/')) clipBlocks += clipBlock(cut.mediaFile.rel, cut.start, cut.duration, trackIdx++);
73
+ else clipBlocks += imageBlock(cut.mediaFile.rel, cut.start, cut.duration, trackIdx++);
74
+ }
75
+ if (cut.caption) captionBlocks += captionOverlay(cut.caption, cut.start + 0.3, cut.duration - 0.5, cut.captionStyle || 'default');
76
+ }
77
+
78
+ const html = `<!DOCTYPE html>
79
+ <html><head><meta charset="utf-8">
80
+ <style>
81
+ * { margin:0;padding:0;box-sizing:border-box; }
82
+ body { background:#000;overflow:hidden; }
83
+ #stage { position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#000; }
84
+ .caption-overlay { animation:fadeIn 0.3s ease-in; }
85
+ @keyframes fadeIn { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)} }
86
+ </style></head><body>
87
+ <div id="stage" data-composition-id="dee-video" data-start="0" data-width="${w}" data-height="${h}">
88
+ ${clipBlocks}
89
+ <audio data-start="0" data-duration="${totalDur}" data-track-index="20" data-volume="${config.musicVolume || 0.3}" src="${musicRel}"></audio>
90
+ ${captionBlocks}
91
+ <div class="clip" data-start="0" data-duration="999" data-track-index="11"
92
+ style="position:absolute;bottom:0;left:0;right:0;padding:12px 24px;background:linear-gradient(transparent,rgba(0,0,0,0.65));">
93
+ <span style="font-size:18px;font-weight:700;color:#fff;">${trainerName || 'Dee Ferdinand'}</span>
94
+ <span style="font-size:14px;color:rgba(255,255,255,0.7);margin-left:8px;">· ${tagline || 'AI Corporate Trainer'}</span>
95
+ </div>
96
+ <div class="clip" data-start="${totalDur - 5}" data-duration="5" data-track-index="30"
97
+ style="position:absolute;inset:0;background:rgba(0,0,0,0.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;">
98
+ <div style="font-size:44px;font-weight:800;color:#fff;text-align:center;">${trainerName || 'Dee Ferdinand'}</div>
99
+ <div style="font-size:22px;color:rgba(255,255,255,0.8);text-align:center;">${tagline || 'AI Corporate Trainer'}</div>
100
+ <div style="font-size:18px;color:rgba(255,255,255,0.55);margin-top:6px;">${website || 'deeferdinand.com'}</div>
101
+ ${clientName ? `<div style="font-size:14px;color:rgba(255,255,255,0.4);margin-top:4px;">${clientName} · ${new Date().getFullYear()}</div>` : ''}
102
+ </div>
103
+ </div></body></html>`;
104
+
105
+ fs.writeFileSync(path.join(compDir, 'index.html'), html);
106
+ onProgress?.(1.0);
107
+ }