Spaces:
Configuration error
feat: fix black frames + ElevenLabs lo-fi music generation + YuE fallback + complete skill update
Browse filesBlack frame fix:
- Root cause: tl.to({},{duration:TOTAL},0) conflicts with GSAP β use tl.set({},{},TOTAL) instead
- Add explicit data-duration on root div
- Add 0.5s safety buffer: scenes end 0.3s before next starts (no overlap)
- Volume levels from coleam00 audio-design.md research: music=0.07-0.12, narration=1.0
ElevenLabs music (primary):
- client.music.compose() with model_id='music_v2'
- Prompt: 'Lo-fi upbeat instrumental, jazzy piano, warm vinyl crackle, positive energy, 85-95 BPM'
- music_length_ms = duration * 1000 + 5000 (always generate 5s extra for safety)
- Requires ELEVENLABS_API_KEY env var in HF Space secrets
YuE fallback:
- Lightweight API wrapper if ElevenLabs key not available
- Direct HTTP to YuE inference endpoint
Transition rules from coleam00 transitions.md:
- EXIT ANIMATIONS BANNED (except final scene) β transitions ARE the exit
- Scene content must be fully visible when transition starts
- 0.3-0.5s transitions for medium energy (corporate)
- autoAlpha not opacity for HyperFrames (sets visibility:hidden at 0)
- lib/music.js +150 -105
- skills/hyperframes-video/SKILL.md +223 -160
- workflows/index.js +217 -242
|
@@ -10,145 +10,190 @@ const execFileAsync = promisify(execFile);
|
|
| 10 |
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 11 |
const LIB = path.join(__dirname, '..', 'music', 'library');
|
| 12 |
|
| 13 |
-
/
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
'
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
},
|
| 35 |
-
{
|
| 36 |
-
file: 'upbeat-community.mp3', mood: 'upbeat', bpm: 128,
|
| 37 |
-
tags: ['community', 'energetic', 'positive'],
|
| 38 |
-
urls: [
|
| 39 |
-
'https://www.bensound.com/bensound-music/bensound-happyrock.mp3',
|
| 40 |
-
'https://incompetech.com/music/royalty-free/mp3-royaltyfree/Digital%20Lemonade.mp3',
|
| 41 |
-
],
|
| 42 |
-
license: 'Bensound free license',
|
| 43 |
-
},
|
| 44 |
-
{
|
| 45 |
-
file: 'warm-documentary.mp3', mood: 'warm', bpm: 75,
|
| 46 |
-
tags: ['community', 'documentary', 'heartfelt'],
|
| 47 |
-
urls: [
|
| 48 |
-
'https://www.bensound.com/bensound-music/bensound-slowmotion.mp3',
|
| 49 |
-
'https://incompetech.com/music/royalty-free/mp3-royaltyfree/Fluffing%20a%20Duck.mp3',
|
| 50 |
-
],
|
| 51 |
-
license: 'Bensound free license',
|
| 52 |
-
},
|
| 53 |
-
{
|
| 54 |
-
file: 'tech-corporate.mp3', mood: 'tech', bpm: 110,
|
| 55 |
-
tags: ['ai', 'tech', 'modern', 'training'],
|
| 56 |
-
urls: [
|
| 57 |
-
'https://www.bensound.com/bensound-music/bensound-innovation.mp3',
|
| 58 |
-
'https://incompetech.com/music/royalty-free/mp3-royaltyfree/Local%20Forecast.mp3',
|
| 59 |
-
],
|
| 60 |
-
license: 'Bensound free license',
|
| 61 |
-
},
|
| 62 |
-
{
|
| 63 |
-
file: 'action-sport.mp3', mood: 'energetic', bpm: 145,
|
| 64 |
-
tags: ['viral', 'energetic', 'teaser'],
|
| 65 |
-
urls: [
|
| 66 |
-
'https://www.bensound.com/bensound-music/bensound-highoctane.mp3',
|
| 67 |
-
'https://incompetech.com/music/royalty-free/mp3-royaltyfree/Scheming%20Weasel.mp3',
|
| 68 |
-
],
|
| 69 |
-
license: 'Bensound free license',
|
| 70 |
-
},
|
| 71 |
-
];
|
| 72 |
-
|
| 73 |
-
const MOOD_MAP = {
|
| 74 |
-
testimonial: ['corporate', 'warm'],
|
| 75 |
-
teaser: ['energetic', 'tech'],
|
| 76 |
-
trailer: ['cinematic', 'energetic'],
|
| 77 |
-
community: ['warm', 'upbeat'],
|
| 78 |
};
|
| 79 |
|
| 80 |
export async function getMusicTrack(trackName, workflow, duration, onProgress) {
|
| 81 |
fs.mkdirSync(LIB, { recursive: true });
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
-
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
if (size < 10000) {
|
| 110 |
-
|
| 111 |
-
|
| 112 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
async function tryDownload(url, dest) {
|
| 118 |
try {
|
| 119 |
-
console.log(`[music] Trying: ${url.slice(0, 60)}`);
|
| 120 |
const res = await fetch(url, {
|
| 121 |
-
headers: {
|
| 122 |
-
|
| 123 |
-
'Accept': 'audio/mpeg,audio/*,*/*'
|
| 124 |
-
},
|
| 125 |
-
redirect: 'follow'
|
| 126 |
});
|
| 127 |
-
if (!res.ok)
|
| 128 |
-
const
|
| 129 |
-
if (
|
| 130 |
await pipeline(res.body, createWriteStream(dest));
|
| 131 |
const size = fs.existsSync(dest) ? fs.statSync(dest).size : 0;
|
| 132 |
if (size < 10000) { if (fs.existsSync(dest)) fs.unlinkSync(dest); return false; }
|
| 133 |
-
console.log(`[music] Downloaded ${path.basename(dest)} (${(size/1024).toFixed(0)}KB)`);
|
| 134 |
return true;
|
| 135 |
-
} catch (e) {
|
| 136 |
-
console.warn(`[music] Download error: ${e.message}`);
|
| 137 |
-
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
| 138 |
-
return false;
|
| 139 |
-
}
|
| 140 |
}
|
| 141 |
|
| 142 |
async function generateSilence(destPath, durationSecs) {
|
| 143 |
try {
|
| 144 |
-
// Generate actual silent MP3 (not empty file β HyperFrames needs valid audio)
|
| 145 |
await execFileAsync('ffmpeg', [
|
| 146 |
'-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo',
|
| 147 |
'-t', String(durationSecs),
|
| 148 |
'-codec:a', 'libmp3lame', '-b:a', '128k',
|
| 149 |
destPath, '-y'
|
| 150 |
]);
|
| 151 |
-
console.log(`[music] Generated ${durationSecs}s silence
|
| 152 |
} catch (e) {
|
| 153 |
console.error('[music] ffmpeg silence gen failed:', e.message);
|
| 154 |
fs.writeFileSync(destPath, Buffer.alloc(0));
|
|
@@ -156,5 +201,5 @@ async function generateSilence(destPath, durationSecs) {
|
|
| 156 |
}
|
| 157 |
|
| 158 |
export function listTracks() {
|
| 159 |
-
return
|
| 160 |
}
|
|
|
|
| 10 |
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 11 |
const LIB = path.join(__dirname, '..', 'music', 'library');
|
| 12 |
|
| 13 |
+
/**
|
| 14 |
+
* Music generation waterfall:
|
| 15 |
+
* 1. ElevenLabs client.music.compose() β highest quality lo-fi upbeat
|
| 16 |
+
* 2. YuE via HF Inference API β open source fallback
|
| 17 |
+
* 3. ffmpeg silence β always available last resort
|
| 18 |
+
*/
|
| 19 |
+
|
| 20 |
+
// ElevenLabs prompts by workflow (instrumental, lofi-upbeat style)
|
| 21 |
+
const EL_PROMPTS = {
|
| 22 |
+
testimonial: 'Lo-fi upbeat instrumental, warm jazzy piano chords, soft vinyl crackle, positive motivational energy, subtle hip-hop drums, 88 BPM, no lyrics, royalty-free background music for AI corporate training video',
|
| 23 |
+
teaser: 'Energetic upbeat electronic beat, punchy kick drum, rising synth arpeggios, building tension, 118 BPM, no lyrics, for event teaser video',
|
| 24 |
+
trailer: 'Cinematic orchestral build, emotional string swells, powerful percussion, rising tension to climax, 95 BPM, no lyrics, for brand trailer video',
|
| 25 |
+
community: 'Warm acoustic guitar melody, gentle piano accompaniment, uplifting heartfelt positive, 75 BPM, no lyrics, for community training story video',
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
// YuE prompts (open-source music gen)
|
| 29 |
+
const YUE_PROMPTS = {
|
| 30 |
+
testimonial: '[Genre: Lo-fi Hip Hop] [Mood: Upbeat, Positive, Warm] [Instruments: Jazz Piano, Soft Drums, Bass Guitar] Instrumental background music 88 BPM no lyrics',
|
| 31 |
+
teaser: '[Genre: Electronic] [Mood: Energetic, Rising, Exciting] [Instruments: Synth, Kick Drum, Bass] Instrumental teaser 118 BPM no lyrics',
|
| 32 |
+
trailer: '[Genre: Cinematic Orchestral] [Mood: Epic, Emotional, Building] [Instruments: Strings, Percussion, Brass] Instrumental trailer 95 BPM no lyrics',
|
| 33 |
+
community: '[Genre: Acoustic Folk] [Mood: Warm, Uplifting, Heartfelt] [Instruments: Acoustic Guitar, Piano] Instrumental 75 BPM no lyrics',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
};
|
| 35 |
|
| 36 |
export async function getMusicTrack(trackName, workflow, duration, onProgress) {
|
| 37 |
fs.mkdirSync(LIB, { recursive: true });
|
| 38 |
|
| 39 |
+
const wf = workflow || 'testimonial';
|
| 40 |
+
const cacheFile = path.join(LIB, `${wf}-${duration}s.mp3`);
|
| 41 |
+
const dur = duration || 30;
|
| 42 |
+
const genDur = dur + 6; // always generate 6s extra for safety
|
| 43 |
+
|
| 44 |
+
// Return cached if valid
|
| 45 |
+
if (fs.existsSync(cacheFile) && fs.statSync(cacheFile).size > 50000) {
|
| 46 |
+
console.log(`[music] Using cached: ${path.basename(cacheFile)}`);
|
| 47 |
+
return cacheFile;
|
| 48 |
}
|
| 49 |
|
| 50 |
+
onProgress?.({ status: 'generating_music', progress: 8 });
|
| 51 |
|
| 52 |
+
// 1. Try ElevenLabs
|
| 53 |
+
const elKey = process.env.ELEVENLABS_API_KEY;
|
| 54 |
+
if (elKey) {
|
| 55 |
+
try {
|
| 56 |
+
console.log('[music] Generating with ElevenLabs music_v2...');
|
| 57 |
+
const ok = await generateElevenLabsMusic(elKey, wf, genDur, cacheFile);
|
| 58 |
+
if (ok) {
|
| 59 |
+
console.log(`[music] ElevenLabs generated: ${(fs.statSync(cacheFile).size/1024).toFixed(0)}KB`);
|
| 60 |
+
return cacheFile;
|
| 61 |
+
}
|
| 62 |
+
} catch (e) {
|
| 63 |
+
console.warn('[music] ElevenLabs failed:', e.message);
|
| 64 |
}
|
| 65 |
+
} else {
|
| 66 |
+
console.log('[music] No ELEVENLABS_API_KEY, skipping ElevenLabs');
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// 2. Try YuE via HF Inference
|
| 70 |
+
const hfToken = process.env.HF_TOKEN;
|
| 71 |
+
try {
|
| 72 |
+
console.log('[music] Trying YuE via HF Inference...');
|
| 73 |
+
const ok = await generateYueMusic(hfToken, wf, genDur, cacheFile);
|
| 74 |
+
if (ok) {
|
| 75 |
+
console.log(`[music] YuE generated: ${(fs.statSync(cacheFile).size/1024).toFixed(0)}KB`);
|
| 76 |
+
return cacheFile;
|
| 77 |
}
|
| 78 |
+
} catch (e) {
|
| 79 |
+
console.warn('[music] YuE failed:', e.message);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// 3. Try bensound URLs
|
| 83 |
+
const bensoundUrls = [
|
| 84 |
+
'https://www.bensound.com/bensound-music/bensound-ukulele.mp3',
|
| 85 |
+
'https://www.bensound.com/bensound-music/bensound-happyrock.mp3',
|
| 86 |
+
'https://www.bensound.com/bensound-music/bensound-slowmotion.mp3',
|
| 87 |
+
];
|
| 88 |
+
for (const url of bensoundUrls) {
|
| 89 |
+
try {
|
| 90 |
+
const ok = await tryDownload(url, cacheFile);
|
| 91 |
+
if (ok) { console.log('[music] Bensound downloaded'); return cacheFile; }
|
| 92 |
+
} catch (e) { /* continue */ }
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 4. ffmpeg silence (always works)
|
| 96 |
+
console.warn('[music] All sources failed, generating silence');
|
| 97 |
+
await generateSilence(cacheFile, genDur);
|
| 98 |
+
return cacheFile;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
async function generateElevenLabsMusic(apiKey, workflow, durationSecs, destPath) {
|
| 102 |
+
const prompt = EL_PROMPTS[workflow] || EL_PROMPTS.testimonial;
|
| 103 |
+
const musicLengthMs = durationSecs * 1000;
|
| 104 |
+
|
| 105 |
+
const res = await fetch('https://api.elevenlabs.io/v1/music', {
|
| 106 |
+
method: 'POST',
|
| 107 |
+
headers: {
|
| 108 |
+
'xi-api-key': apiKey,
|
| 109 |
+
'Content-Type': 'application/json',
|
| 110 |
+
},
|
| 111 |
+
body: JSON.stringify({
|
| 112 |
+
prompt,
|
| 113 |
+
music_length_ms: musicLengthMs,
|
| 114 |
+
model_id: 'music_v2',
|
| 115 |
+
force_instrumental: true,
|
| 116 |
+
}),
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
if (!res.ok) {
|
| 120 |
+
const err = await res.text();
|
| 121 |
+
throw new Error(`ElevenLabs API ${res.status}: ${err.slice(0, 200)}`);
|
| 122 |
}
|
| 123 |
|
| 124 |
+
const contentType = res.headers.get('content-type') || '';
|
| 125 |
+
if (contentType.includes('text/html') || contentType.includes('application/json')) {
|
| 126 |
+
const body = await res.text();
|
| 127 |
+
throw new Error(`ElevenLabs returned non-audio: ${body.slice(0, 200)}`);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
await pipeline(res.body, createWriteStream(destPath));
|
| 131 |
+
const size = fs.existsSync(destPath) ? fs.statSync(destPath).size : 0;
|
| 132 |
if (size < 10000) {
|
| 133 |
+
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
| 134 |
+
return false;
|
| 135 |
}
|
| 136 |
+
return true;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
async function generateYueMusic(hfToken, workflow, durationSecs, destPath) {
|
| 140 |
+
const prompt = YUE_PROMPTS[workflow] || YUE_PROMPTS.testimonial;
|
| 141 |
+
const headers = {
|
| 142 |
+
'Content-Type': 'application/json',
|
| 143 |
+
...(hfToken ? { 'Authorization': `Bearer ${hfToken}` } : {}),
|
| 144 |
+
};
|
| 145 |
|
| 146 |
+
// YuE model variants on HF Hub
|
| 147 |
+
const yueEndpoints = [
|
| 148 |
+
'https://api-inference.huggingface.co/models/m-a-p/YuE-s1-7B-anneal-en-cot',
|
| 149 |
+
'https://api-inference.huggingface.co/models/m-a-p/YuE-s2-1B-general',
|
| 150 |
+
];
|
| 151 |
+
|
| 152 |
+
for (const url of yueEndpoints) {
|
| 153 |
+
try {
|
| 154 |
+
const res = await fetch(url, {
|
| 155 |
+
method: 'POST',
|
| 156 |
+
headers,
|
| 157 |
+
body: JSON.stringify({ inputs: prompt, parameters: { max_length: durationSecs * 50 } }),
|
| 158 |
+
signal: AbortSignal.timeout(120000), // 2min timeout
|
| 159 |
+
});
|
| 160 |
+
if (!res.ok) continue;
|
| 161 |
+
const ct = res.headers.get('content-type') || '';
|
| 162 |
+
if (!ct.includes('audio')) continue;
|
| 163 |
+
await pipeline(res.body, createWriteStream(destPath));
|
| 164 |
+
const size = fs.existsSync(destPath) ? fs.statSync(destPath).size : 0;
|
| 165 |
+
if (size > 10000) return true;
|
| 166 |
+
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
| 167 |
+
} catch (e) { /* try next */ }
|
| 168 |
+
}
|
| 169 |
+
return false;
|
| 170 |
}
|
| 171 |
|
| 172 |
async function tryDownload(url, dest) {
|
| 173 |
try {
|
|
|
|
| 174 |
const res = await fetch(url, {
|
| 175 |
+
headers: { 'User-Agent': 'Mozilla/5.0', 'Accept': 'audio/mpeg,audio/*,*/*' },
|
| 176 |
+
signal: AbortSignal.timeout(20000),
|
|
|
|
|
|
|
|
|
|
| 177 |
});
|
| 178 |
+
if (!res.ok) return false;
|
| 179 |
+
const ct = res.headers.get('content-type') || '';
|
| 180 |
+
if (ct.includes('text/html')) return false;
|
| 181 |
await pipeline(res.body, createWriteStream(dest));
|
| 182 |
const size = fs.existsSync(dest) ? fs.statSync(dest).size : 0;
|
| 183 |
if (size < 10000) { if (fs.existsSync(dest)) fs.unlinkSync(dest); return false; }
|
|
|
|
| 184 |
return true;
|
| 185 |
+
} catch (e) { if (fs.existsSync(dest)) fs.unlinkSync(dest); return false; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
async function generateSilence(destPath, durationSecs) {
|
| 189 |
try {
|
|
|
|
| 190 |
await execFileAsync('ffmpeg', [
|
| 191 |
'-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo',
|
| 192 |
'-t', String(durationSecs),
|
| 193 |
'-codec:a', 'libmp3lame', '-b:a', '128k',
|
| 194 |
destPath, '-y'
|
| 195 |
]);
|
| 196 |
+
console.log(`[music] Generated ${durationSecs}s silence`);
|
| 197 |
} catch (e) {
|
| 198 |
console.error('[music] ffmpeg silence gen failed:', e.message);
|
| 199 |
fs.writeFileSync(destPath, Buffer.alloc(0));
|
|
|
|
| 201 |
}
|
| 202 |
|
| 203 |
export function listTracks() {
|
| 204 |
+
return [{ file: 'auto', mood: 'auto', bpm: 88, tags: ['lo-fi', 'upbeat', 'instrumental'], license: 'ElevenLabs generated' }];
|
| 205 |
}
|
|
@@ -1,225 +1,278 @@
|
|
| 1 |
-
# HyperFrames Production Skill β Dee Ferdinand Video Studio
|
| 2 |
-
# Sources: nateherkai/hyperframes-student-kit + heygen-com/hyperframes-launch-video
|
|
|
|
|
|
|
| 3 |
# Updated: 2026-06-16
|
| 4 |
|
| 5 |
## THE 5 HARD RULES
|
|
|
|
| 6 |
1. `class="clip"` on EVERY timed element + `data-start` + `data-duration` + `data-track-index`
|
| 7 |
-
2. GSAP: `gsap.timeline({ paused: true })` β
|
| 8 |
-
3. NEVER call `.play()` `.pause()` or set `.currentTime` in scripts β framework owns media
|
| 9 |
-
4. NEVER
|
| 10 |
-
5.
|
| 11 |
|
| 12 |
-
##
|
| 13 |
|
| 14 |
-
|
| 15 |
-
``
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
<video src="./assets/clip.MOV" data-start="8" data-duration="8"
|
| 21 |
-
data-track-index="9" data-volume="0.85" playsinline
|
| 22 |
-
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 23 |
```
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
##
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
```
|
| 32 |
-
NEVER call `.play()` or `.pause()` on audio in scripts.
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
- Sub-composition timelines must be padded: `tl.to({}, { duration: SLOT_DURATION }, 0)`
|
| 38 |
|
| 39 |
-
##
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
```
|
| 45 |
-
- **Headings / Kinetic titles:** `font-family: 'Space Grotesk', sans-serif` β weight 700-800
|
| 46 |
-
- **Body / captions / lower thirds:** `font-family: 'Plus Jakarta Sans', sans-serif` β weight 400-600
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
```
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
- `expo.out` for kinetic titles, `power3.out` for smooth entries, `back.out(1.7)` for stat pops
|
| 68 |
|
| 69 |
-
##
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
| Element | Size | Weight | Font |
|
| 74 |
|---------|------|--------|------|
|
| 75 |
-
| Hook
|
| 76 |
-
| Hook
|
| 77 |
-
|
|
| 78 |
-
|
|
|
|
|
| 79 |
| CTA main | **112px** | 900 | Space Grotesk |
|
| 80 |
-
| Testimonial quote | **72px** | 600 | Plus Jakarta Sans
|
| 81 |
| Lower third title | **40px** | 700 | Plus Jakarta Sans |
|
| 82 |
-
| Lower third
|
| 83 |
| Brand end name | **80px** | 800 | Space Grotesk |
|
| 84 |
-
| Brand end
|
| 85 |
-
| Brand bar persistent | **26px** | 700 | Space Grotesk |
|
| 86 |
-
|
| 87 |
-
## KINETIC MOTION PATTERNS
|
| 88 |
-
|
| 89 |
-
From motion-principles.md (nateherkai student-kit):
|
| 90 |
-
- Vary eases per scene β never same ease twice in a row
|
| 91 |
-
- Speed: 0.15-0.25s = energy/urgency, 0.3-0.5s = professional, 0.6-1.0s = cinematic
|
| 92 |
-
- Direction variety: `y: -60` (from above), `x: -80` (from left), `scale: 0.7` (scale in), `autoAlpha` only (no translate)
|
| 93 |
-
- Stagger text: multiple spans/divs, `{ stagger: 0.08 }` for word-by-word feel
|
| 94 |
-
- Build/breathe/resolve: stagger in β ambient drift β fast exit
|
| 95 |
-
|
| 96 |
-
### Kinetic title entry patterns (vary per scene)
|
| 97 |
-
```javascript
|
| 98 |
-
// Pattern A: slam from above (hook)
|
| 99 |
-
tl.from("#title", { y: -80, autoAlpha: 0, duration: 0.25, ease: "expo.out" }, 0.2);
|
| 100 |
|
| 101 |
-
|
| 102 |
-
tl.from("#stat", { scale: 0.6, autoAlpha: 0, duration: 0.4, ease: "back.out(2.5)" }, 0.3);
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
tl.from(".word", { y: 60, autoAlpha: 0, duration: 0.35, stagger: 0.08, ease: "power3.out" }, 0.2);
|
| 109 |
|
| 110 |
-
|
| 111 |
-
tl.from("#quote", { autoAlpha: 0, duration: 0.6, ease: "sine.inOut" }, 0.5);
|
| 112 |
-
```
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
| 125 |
-
tl.to("#s4", { autoAlpha: 0, filter: "blur(16px)", scale: 1.05, duration: 0.6, ease: "sine.inOut" }, 15.8);
|
| 126 |
-
tl.fromTo("#s5", { autoAlpha: 0, filter: "blur(16px)", scale: 0.95 }, { autoAlpha: 1, filter: "blur(0px)", scale: 1, duration: 0.6, ease: "sine.inOut" }, 15.8);
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
```
|
| 142 |
|
| 143 |
-
##
|
| 144 |
-
```html
|
| 145 |
-
<div id="grain" style="position:absolute;inset:0;pointer-events:none;z-index:99;overflow:hidden;">
|
| 146 |
-
<div style="position:absolute;top:-50%;left:-50%;width:200%;height:200%;opacity:0.06;
|
| 147 |
-
background:url('data:image/svg+xml,%3Csvg viewBox=\'0 0 256 256\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'n\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.65\' numOctaves=\'3\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23n)\'/%3E%3C/svg%3E');
|
| 148 |
-
animation:grain 0.5s steps(1) infinite;"></div>
|
| 149 |
-
</div>
|
| 150 |
-
<style>@keyframes grain{0%,100%{transform:translate(0,0)}10%{transform:translate(-5%,-5%)}20%{transform:translate(-10%,5%)}30%{transform:translate(5%,-10%)}50%{transform:translate(-10%,5%)}70%{transform:translate(0,10%)}90%{transform:translate(10%,5%)}}</style>
|
| 151 |
-
```
|
| 152 |
|
| 153 |
-
## LOWER THIRD (glass card style)
|
| 154 |
```html
|
| 155 |
-
<div id="lt" class="clip" data-start="4.6" data-duration="
|
| 156 |
style="position:absolute;bottom:180px;left:0;padding:0 0 0 56px;">
|
| 157 |
-
<div style="background:rgba(10,10,
|
| 158 |
-
border-left:
|
| 159 |
-
padding:
|
| 160 |
<div style="font:700 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#fff;">Purbasari Indonesia</div>
|
| 161 |
-
<div style="font:400 24px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.
|
| 162 |
-
margin-top:6px;">AI Training Β·
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
```
|
| 166 |
|
| 167 |
-
## WORKING MUSIC URLS (server-accessible)
|
| 168 |
-
|
| 169 |
-
Use bensound.com free tier or freesound direct links β avoid FMA (404/503):
|
| 170 |
-
|
| 171 |
```javascript
|
| 172 |
-
//
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
'https://www.bensound.com/bensound-music/bensound-ukulele.mp3',
|
| 176 |
-
// Try 2: ccMixter direct
|
| 177 |
-
'https://dig.ccmixter.org/files/airtone/56796/airtone-_retro_.mp3',
|
| 178 |
-
// Fallback: generate silence with ffmpeg (already implemented)
|
| 179 |
-
];
|
| 180 |
```
|
| 181 |
|
| 182 |
-
## COMPLETE CORRECT
|
| 183 |
|
| 184 |
```html
|
| 185 |
<!DOCTYPE html>
|
| 186 |
<html><head><meta charset="utf-8">
|
| 187 |
-
<link href="
|
| 188 |
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
| 189 |
-
<style>
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
#root { position:relative;width:1080px;height:1920px;overflow:hidden;background:#0d0d1f; }
|
| 193 |
-
.sc { position:absolute;inset:0; } /* scene base */
|
| 194 |
</style></head><body>
|
| 195 |
-
<div id="root"
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
<div id="s1" class="clip sc" data-start="0" data-duration="4" data-track-index="0">
|
| 199 |
-
<
|
| 200 |
-
|
| 201 |
-
<video data-start="0" data-duration="4" data-track-index="1"
|
| 202 |
-
src="./assets/clip.MOV" playsinline
|
| 203 |
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 204 |
</div>
|
| 205 |
</div>
|
| 206 |
|
| 207 |
-
<!-- Caption: class=clip
|
| 208 |
-
<div id="cap" class="clip" data-start="0.
|
| 209 |
-
style="position:absolute;bottom:400px;left:0;right:0;padding:0 56px;
|
| 210 |
-
font:900 160px/1.
|
| 211 |
-
text-shadow:0
|
| 212 |
|
| 213 |
-
<!-- Music:
|
| 214 |
<audio data-start="0" data-duration="30" data-track-index="50"
|
| 215 |
-
data-volume="0.
|
| 216 |
|
| 217 |
<script>
|
| 218 |
const tl = gsap.timeline({ paused: true });
|
| 219 |
-
|
| 220 |
-
//
|
| 221 |
-
tl.
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
window.__timelines = window.__timelines || {};
|
| 224 |
window.__timelines["COMP_ID"] = tl;
|
| 225 |
</script>
|
|
@@ -227,6 +280,16 @@ body { background:#0d0d1f;overflow:hidden; }
|
|
| 227 |
```
|
| 228 |
|
| 229 |
## RENDER COMMAND
|
|
|
|
| 230 |
```bash
|
| 231 |
-
PRODUCER_HEADLESS_SHELL_PATH=/usr/bin/chromium
|
|
|
|
| 232 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HyperFrames Video Production Skill β Dee Ferdinand Video Studio
|
| 2 |
+
# Sources: nateherkai/hyperframes-student-kit + heygen-com/hyperframes-launch-video
|
| 3 |
+
# robonuggets/hyperframes-helper + coleam00/hyperframes-ai-video-generation
|
| 4 |
+
# elevenlabs/skills
|
| 5 |
# Updated: 2026-06-16
|
| 6 |
|
| 7 |
## THE 5 HARD RULES
|
| 8 |
+
|
| 9 |
1. `class="clip"` on EVERY timed element + `data-start` + `data-duration` + `data-track-index`
|
| 10 |
+
2. GSAP: `gsap.timeline({ paused: true })` β `window.__timelines["COMP_ID"] = tl` (key MUST match `data-composition-id`)
|
| 11 |
+
3. NEVER call `.play()` `.pause()` or set `.currentTime` in scripts β framework owns media
|
| 12 |
+
4. NEVER `Math.random()`, `Date.now()`, `fetch()` in GSAP scripts (breaks determinism)
|
| 13 |
+
5. Extend timeline: `tl.set({}, {}, TOTAL_DURATION)` β prevents render cutoff and black frames
|
| 14 |
|
| 15 |
+
## BLACK FRAME PREVENTION (from heygen HANDOFF.md + coleam00 transitions.md)
|
| 16 |
|
| 17 |
+
**Root causes of black frames:**
|
| 18 |
+
- Timeline shorter than expected: always `tl.set({}, {}, TOTAL_DURATION)` at the very end
|
| 19 |
+
- Scene overlap on same track: clips on the same `data-track-index` CANNOT overlap even 0.001s
|
| 20 |
+
- Exit animations BANNED (except final scene): do NOT `gsap.to()` elements out before transition
|
| 21 |
+
The transition IS the exit. Outgoing content must be fully visible when transition fires.
|
| 22 |
+
- Scene gap: next scene `data-start` must not exceed current `data-start + data-duration`
|
| 23 |
|
| 24 |
+
**Correct scene timing pattern (no gaps, no overlap):**
|
|
|
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
+
Scene 1: data-start="0" data-duration="4" β ends at 4.0s
|
| 27 |
+
Scene 2: data-start="3.65" data-duration="4" β starts at 3.65s (0.35s overlap for transition)
|
| 28 |
+
Scene 3: data-start="7.65" data-duration="8" β starts at 7.65s
|
| 29 |
+
```
|
| 30 |
+
Scenes on DIFFERENT track-indices can overlap (that's how transitions work).
|
| 31 |
+
Scenes on the SAME track-index must NEVER overlap.
|
| 32 |
|
| 33 |
+
## AUDIO (from coleam00 audio-design.md β research-backed volumes)
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
data-volume levels:
|
| 37 |
+
Subject voice (testimonial video): 0.88 (β -14 dB peak)
|
| 38 |
+
Music during energy/CTA: 0.12 (β -15 to -18 dB)
|
| 39 |
+
Music during testimonial video: 0.07 (β -20 to -23 dB)
|
| 40 |
+
Music general background: 0.10
|
| 41 |
+
Narration/voiceover: 1.0 (β -12 dB peak)
|
| 42 |
```
|
|
|
|
| 43 |
|
| 44 |
+
**NEVER** use lyrical music under speech (proven comprehension harm).
|
| 45 |
+
**ALWAYS** use `force_instrumental: true` on ElevenLabs music API.
|
| 46 |
+
**NEVER** add `muted` attribute to video when subject voice is needed.
|
|
|
|
| 47 |
|
| 48 |
+
## ELEVENLABS MUSIC GENERATION (primary)
|
| 49 |
|
| 50 |
+
```python
|
| 51 |
+
# generate_music.py β generates lo-fi upbeat track for Dee Ferdinand videos
|
| 52 |
+
from elevenlabs import ElevenLabs
|
| 53 |
+
import os
|
| 54 |
+
|
| 55 |
+
client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
|
| 56 |
+
|
| 57 |
+
audio = client.music.compose(
|
| 58 |
+
prompt="Lo-fi upbeat instrumental, warm jazzy piano chords, soft vinyl crackle, "
|
| 59 |
+
"positive motivational energy, subtle hip-hop drums, 88 BPM, "
|
| 60 |
+
"no lyrics, background music for AI corporate training video",
|
| 61 |
+
music_length_ms=35000, # always generate 5s extra beyond video duration
|
| 62 |
+
model_id="music_v2",
|
| 63 |
+
force_instrumental=True,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
with open("./assets/music.mp3", "wb") as f:
|
| 67 |
+
for chunk in audio:
|
| 68 |
+
f.write(chunk)
|
| 69 |
```
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
**Prompt templates by workflow:**
|
| 72 |
+
|
| 73 |
+
| Workflow | ElevenLabs prompt |
|
| 74 |
+
|----------|-------------------|
|
| 75 |
+
| testimonial | "Lo-fi upbeat instrumental, warm jazzy piano, vinyl crackle, positive energy, 88 BPM, no lyrics, for AI corporate training" |
|
| 76 |
+
| teaser | "Energetic upbeat electronic, punchy kick drum, rising synths, 120 BPM, no lyrics, for event teaser" |
|
| 77 |
+
| trailer | "Cinematic orchestral build, emotional strings, powerful swells, 90 BPM, no lyrics, for brand trailer" |
|
| 78 |
+
| community | "Warm acoustic guitar, gentle piano, uplifting and heartfelt, 75 BPM, no lyrics, for community story" |
|
| 79 |
+
|
| 80 |
+
**Requires:** `ELEVENLABS_API_KEY` env var in HF Space secrets (Settings β Variables).
|
| 81 |
+
|
| 82 |
+
## YUE FALLBACK (when ElevenLabs key not available)
|
| 83 |
+
|
| 84 |
+
```python
|
| 85 |
+
# YuE: open-source music generation from multimodal-art-projection
|
| 86 |
+
# GitHub: https://github.com/multimodal-art-projection/YuE
|
| 87 |
+
# Use via Hugging Face Inference API (no local GPU needed)
|
| 88 |
+
import requests, os
|
| 89 |
+
|
| 90 |
+
def generate_music_yue(prompt: str, duration_seconds: int = 35) -> bytes:
|
| 91 |
+
"""
|
| 92 |
+
YuE music generation via HF Inference API.
|
| 93 |
+
Falls back to this when ELEVENLABS_API_KEY is not set.
|
| 94 |
+
"""
|
| 95 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 96 |
+
headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
|
| 97 |
+
|
| 98 |
+
# YuE model on HF Hub
|
| 99 |
+
url = "https://api-inference.huggingface.co/models/m-a-p/YuE-s1-7B-anneal-en-cot"
|
| 100 |
+
|
| 101 |
+
payload = {
|
| 102 |
+
"inputs": prompt,
|
| 103 |
+
"parameters": {
|
| 104 |
+
"max_length": duration_seconds * 50, # approximate token count
|
| 105 |
+
"num_return_sequences": 1,
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
r = requests.post(url, headers=headers, json=payload, timeout=120)
|
| 110 |
+
if r.status_code == 200:
|
| 111 |
+
return r.content # audio bytes
|
| 112 |
+
raise Exception(f"YuE generation failed: {r.status_code} {r.text[:200]}")
|
| 113 |
```
|
| 114 |
|
| 115 |
+
**YuE prompt format:**
|
| 116 |
+
```
|
| 117 |
+
[Genre: Lo-fi Hip Hop] [Mood: Upbeat, Positive] [Instruments: Piano, Drums, Bass]
|
| 118 |
+
Background instrumental music for corporate AI training video. No lyrics. 88 BPM.
|
| 119 |
+
```
|
|
|
|
| 120 |
|
| 121 |
+
## DEE FERDINAND BRAND FONTS
|
| 122 |
|
| 123 |
+
```html
|
| 124 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700;900
|
| 125 |
+
&family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600
|
| 126 |
+
&display=swap" rel="stylesheet">
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
- **ALL display titles / kinetic captions:** `font: 900 Xpx 'Space Grotesk', sans-serif`
|
| 130 |
+
- **Body / quotes / lower thirds / UI text:** `font: 600 Xpx 'Plus Jakarta Sans', sans-serif`
|
| 131 |
+
|
| 132 |
+
## CAPTION SIZES β 2X KINETIC (Dee's style)
|
| 133 |
|
| 134 |
| Element | Size | Weight | Font |
|
| 135 |
|---------|------|--------|------|
|
| 136 |
+
| Hook line 1 | **160px** | 900 | Space Grotesk |
|
| 137 |
+
| Hook line 2 (accent color) | **160px** | 900 | Space Grotesk |
|
| 138 |
+
| Hook sub | **72px** | 700 | Plus Jakarta Sans |
|
| 139 |
+
| Stat counter | **160px** | 900 | Space Grotesk |
|
| 140 |
+
| Energy cut captions | **128-144px** | 900 | Space Grotesk |
|
| 141 |
| CTA main | **112px** | 900 | Space Grotesk |
|
| 142 |
+
| Testimonial quote | **72px italic** | 600 | Plus Jakarta Sans |
|
| 143 |
| Lower third title | **40px** | 700 | Plus Jakarta Sans |
|
| 144 |
+
| Lower third subtitle | **24px** | 400 | Plus Jakarta Sans |
|
| 145 |
| Brand end name | **80px** | 800 | Space Grotesk |
|
| 146 |
+
| Brand end tagline | **40px** | 500 | Plus Jakarta Sans |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
## DARK PREMIUM PALETTE
|
|
|
|
| 149 |
|
| 150 |
+
```css
|
| 151 |
+
:root {
|
| 152 |
+
--bg: #0d0d1f; /* deep navy-black canvas */
|
| 153 |
+
--accent: #7C6FE0; /* purple accent */
|
| 154 |
+
--accent-2: #E06F9A; /* pink accent */
|
| 155 |
+
--energy: #9EF0C8; /* teal-green for wins/energy */
|
| 156 |
+
--text: #ffffff;
|
| 157 |
+
--text-dim: rgba(255,255,255,0.72);
|
| 158 |
+
--text-muted: rgba(255,255,255,0.48);
|
| 159 |
+
}
|
| 160 |
+
```
|
| 161 |
|
| 162 |
+
## MOTION VOCABULARY (from nateherkai motion-principles.md)
|
|
|
|
| 163 |
|
| 164 |
+
Vary eases β NEVER use same ease twice in a row.
|
|
|
|
|
|
|
| 165 |
|
| 166 |
+
| Moment | GSAP ease | Duration | Feel |
|
| 167 |
+
|--------|-----------|----------|------|
|
| 168 |
+
| Hook title slam | `expo.out` | 0.20-0.25s | Kinetic energy |
|
| 169 |
+
| Stat pop | `back.out(2.5)` | 0.40-0.50s | Bouncy confidence |
|
| 170 |
+
| Energy cut caption | `power4.out` | 0.22-0.28s | Fast/decisive |
|
| 171 |
+
| CTA slide up | `power3.out` | 0.45-0.55s | Professional |
|
| 172 |
+
| Testimonial quote | `sine.inOut` | 0.55-0.65s | Calm contrast |
|
| 173 |
+
| Brand end stagger | `power3.out` | 0.35-0.45s | Composed |
|
| 174 |
|
| 175 |
+
**Direction variety (never all the same):**
|
| 176 |
+
- Slam from above: `y: -100`
|
| 177 |
+
- Slide from left: `x: -120`
|
| 178 |
+
- Slide from right: `x: 120`
|
| 179 |
+
- Scale pop: `scale: 0.55`
|
| 180 |
+
- Opacity only: `autoAlpha: 0` (no translate) β for calm moments
|
| 181 |
|
| 182 |
+
## TRANSITION PATTERNS (from coleam00 transitions.md)
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
Rule: EXIT ANIMATIONS ARE BANNED except on final scene. The transition IS the exit.
|
| 185 |
+
Incoming scene's entrance fires at transition midpoint, outgoing scene fades simultaneously.
|
| 186 |
|
| 187 |
+
```javascript
|
| 188 |
+
// Zoom-through (dramatic opener, hookβ2)
|
| 189 |
+
const T1 = 3.65; // transition start
|
| 190 |
+
tl.to("#s1", { autoAlpha: 0, scale: 1.3, duration: 0.35, ease: "power4.inOut" }, T1);
|
| 191 |
+
tl.fromTo("#s2", { autoAlpha: 0, scale: 0.8 }, { autoAlpha: 1, scale: 1, duration: 0.35, ease: "power4.inOut" }, T1);
|
| 192 |
+
|
| 193 |
+
// Push-slide left (editorial, proofβ3)
|
| 194 |
+
const T2 = 7.65;
|
| 195 |
+
tl.to("#s2", { autoAlpha: 0, xPercent: -18, duration: 0.32, ease: "power2.inOut" }, T2);
|
| 196 |
+
tl.fromTo("#s3", { autoAlpha: 0, xPercent: 18 }, { autoAlpha: 1, xPercent: 0, duration: 0.32, ease: "power2.inOut" }, T2);
|
| 197 |
+
|
| 198 |
+
// Blur crossfade (wind-down, testimonialβ4)
|
| 199 |
+
const T3 = 15.65;
|
| 200 |
+
tl.to("#s3", { autoAlpha: 0, filter: "blur(14px)", scale: 1.04, duration: 0.45, ease: "sine.inOut" }, T3);
|
| 201 |
+
tl.fromTo("#s4a", { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.25 }, T3 + 0.2);
|
| 202 |
+
|
| 203 |
+
// Hard cut (energy B-roll β disruption intentional)
|
| 204 |
+
// Just stagger data-start values with NO overlap (different tracks ok)
|
| 205 |
```
|
| 206 |
|
| 207 |
+
## GLASS CARD LOWER THIRD (robonuggets recipe)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
|
|
|
| 209 |
```html
|
| 210 |
+
<div id="lt" class="clip" data-start="4.6" data-duration="2.9" data-track-index="8"
|
| 211 |
style="position:absolute;bottom:180px;left:0;padding:0 0 0 56px;">
|
| 212 |
+
<div style="background:rgba(10,10,25,0.85);backdrop-filter:blur(20px) saturate(150%);
|
| 213 |
+
border-left:6px solid #7C6FE0;border-radius:0 16px 16px 0;
|
| 214 |
+
padding:18px 32px;display:inline-block;">
|
| 215 |
<div style="font:700 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#fff;">Purbasari Indonesia</div>
|
| 216 |
+
<div style="font:400 24px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.62);
|
| 217 |
+
margin-top:6px;">AI Training Β· 2026</div>
|
| 218 |
</div>
|
| 219 |
</div>
|
| 220 |
```
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
```javascript
|
| 223 |
+
// Slide in from left, auto-exit via next transition (NOT gsap.to exit)
|
| 224 |
+
tl.from("#lt", { x: -400, autoAlpha: 0, duration: 0.4, ease: "power3.out" }, 4.6);
|
| 225 |
+
// NO exit tween β lt will be covered by next scene transition
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
```
|
| 227 |
|
| 228 |
+
## COMPLETE CORRECT TEMPLATE
|
| 229 |
|
| 230 |
```html
|
| 231 |
<!DOCTYPE html>
|
| 232 |
<html><head><meta charset="utf-8">
|
| 233 |
+
<link href="[Google Fonts Space Grotesk + Plus Jakarta Sans]" rel="stylesheet">
|
| 234 |
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
| 235 |
+
<style>* {margin:0;padding:0;box-sizing:border-box} body {background:#0d0d1f;overflow:hidden}
|
| 236 |
+
#root {position:relative;width:1080px;height:1920px;overflow:hidden;background:#0d0d1f}
|
| 237 |
+
.sc {position:absolute;inset:0}
|
|
|
|
|
|
|
| 238 |
</style></head><body>
|
| 239 |
+
<div id="root"
|
| 240 |
+
data-composition-id="COMP_ID"
|
| 241 |
+
data-start="0"
|
| 242 |
+
data-duration="30"
|
| 243 |
+
data-width="1080"
|
| 244 |
+
data-height="1920">
|
| 245 |
+
|
| 246 |
+
<!-- Scene: timed wrapper -->
|
| 247 |
<div id="s1" class="clip sc" data-start="0" data-duration="4" data-track-index="0">
|
| 248 |
+
<!-- Video: NO muted for subject voice. Wrap in div. -->
|
| 249 |
+
<div style="position:absolute;inset:0">
|
| 250 |
+
<video data-start="0" data-duration="4" data-track-index="1"
|
| 251 |
+
data-volume="0.88" src="./assets/clip.MOV" playsinline
|
| 252 |
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 253 |
</div>
|
| 254 |
</div>
|
| 255 |
|
| 256 |
+
<!-- Caption: class=clip mandatory -->
|
| 257 |
+
<div id="cap" class="clip" data-start="0.2" data-duration="3.5" data-track-index="2"
|
| 258 |
+
style="position:absolute;bottom:400px;left:0;right:0;padding:0 56px;
|
| 259 |
+
text-align:center;font:900 160px/1.0 'Space Grotesk',sans-serif;
|
| 260 |
+
color:#fff;text-shadow:0 6px 40px rgba(0,0,0,0.95);">Hook</div>
|
| 261 |
|
| 262 |
+
<!-- Music: instrumental, force_instrumental generated by ElevenLabs -->
|
| 263 |
<audio data-start="0" data-duration="30" data-track-index="50"
|
| 264 |
+
data-volume="0.10" src="./assets/music.mp3"></audio>
|
| 265 |
|
| 266 |
<script>
|
| 267 |
const tl = gsap.timeline({ paused: true });
|
| 268 |
+
|
| 269 |
+
// Entrance (varied eases, never same twice)
|
| 270 |
+
tl.from("#cap", { y: -100, autoAlpha: 0, duration: 0.22, ease: "expo.out" }, 0.2);
|
| 271 |
+
|
| 272 |
+
// CRITICAL: extend to prevent cutoff / black frames
|
| 273 |
+
tl.set({}, {}, 30); // use set not to()
|
| 274 |
+
|
| 275 |
+
// CRITICAL: key = data-composition-id exactly
|
| 276 |
window.__timelines = window.__timelines || {};
|
| 277 |
window.__timelines["COMP_ID"] = tl;
|
| 278 |
</script>
|
|
|
|
| 280 |
```
|
| 281 |
|
| 282 |
## RENDER COMMAND
|
| 283 |
+
|
| 284 |
```bash
|
| 285 |
+
PRODUCER_HEADLESS_SHELL_PATH=/usr/bin/chromium \
|
| 286 |
+
npx hyperframes render . -o ./renders/output.mp4 -w 1 --fps 30 --quality standard
|
| 287 |
```
|
| 288 |
+
|
| 289 |
+
## MUSIC GENERATION WATERFALL
|
| 290 |
+
|
| 291 |
+
1. **ElevenLabs** (`client.music.compose()`) β highest quality, requires API key
|
| 292 |
+
2. **YuE via HF Inference** β open source, requires HF_TOKEN, slower
|
| 293 |
+
3. **ffmpeg silence fallback** β always available, no music
|
| 294 |
+
|
| 295 |
+
Set `ELEVENLABS_API_KEY` in HF Space secrets for production quality music.
|
|
@@ -1,280 +1,264 @@
|
|
| 1 |
// workflows/index.js β Dee Ferdinand Video Studio
|
| 2 |
-
//
|
| 3 |
-
//
|
| 4 |
-
// -
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
// - tl.to({},{duration:TOTAL},0) prevents black frames
|
| 9 |
-
|
| 10 |
-
function mediaEl(m, start, duration, trackIdx, volume = 0) {
|
| 11 |
-
if (!m) return '<div style="position:absolute;inset:0;background:#0d0d1f;"></div>';
|
| 12 |
-
if (m.mime?.startsWith('video/')) {
|
| 13 |
-
// CRITICAL: no muted attribute when volume > 0 β muted blocks audio extraction
|
| 14 |
-
return `<div id="vw-${trackIdx}" style="position:absolute;inset:0">
|
| 15 |
-
<video data-start="${start}" data-duration="${duration}" data-track-index="${trackIdx}"
|
| 16 |
-
data-volume="${volume}" src="${m.rel}" playsinline
|
| 17 |
-
style="width:100%;height:100%;object-fit:cover;"></video>
|
| 18 |
-
</div>`;
|
| 19 |
-
}
|
| 20 |
-
return `<img src="${m.rel}" alt="" style="width:100%;height:100%;object-fit:cover;transform-origin:center center;">`;
|
| 21 |
-
}
|
| 22 |
|
| 23 |
-
const
|
| 24 |
-
const GSAP_CDN = `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>`;
|
| 25 |
-
const BASE_CSS = `
|
| 26 |
-
* { margin:0;padding:0;box-sizing:border-box; }
|
| 27 |
-
body { background:#0d0d1f;overflow:hidden; }
|
| 28 |
-
#root { position:relative;width:VAR_WPXpx;height:VAR_HPXpx;overflow:hidden;background:#0d0d1f; }
|
| 29 |
-
.sc { position:absolute;inset:0; }
|
| 30 |
-
@keyframes grain{0%,100%{transform:translate(0,0)}10%{transform:translate(-5%,-5%)}20%{transform:translate(-10%,5%)}30%{transform:translate(5%,-10%)}50%{transform:translate(-10%,5%)}70%{transform:translate(0,10%)}90%{transform:translate(10%,5%)}}
|
| 31 |
-
`;
|
| 32 |
-
|
| 33 |
-
const GRAIN = `<div id="grain" style="position:absolute;inset:0;pointer-events:none;z-index:98;overflow:hidden;">
|
| 34 |
<div style="position:absolute;top:-50%;left:-50%;width:200%;height:200%;opacity:0.065;
|
| 35 |
background:url('data:image/svg+xml,%3Csvg viewBox=\'0 0 256 256\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'n\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.65\' numOctaves=\'3\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23n)\'/%3E%3C/svg%3E');
|
| 36 |
animation:grain 0.5s steps(1) infinite;"></div>
|
| 37 |
</div>`;
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
const testimonial = {
|
| 41 |
-
meta: {
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
format: '9:16', duration: 30, music: 'corporate',
|
| 45 |
-
},
|
| 46 |
|
| 47 |
buildHTML(media, config, compId, w, h, dur) {
|
| 48 |
-
const { clientName
|
| 49 |
-
tagline
|
| 50 |
const vids = media.filter(m => m.mime?.startsWith('video/'));
|
| 51 |
const imgs = media.filter(m => m.mime?.startsWith('image/'));
|
| 52 |
-
const pick = (i) => media[i % Math.max(media.length,
|
| 53 |
-
const
|
| 54 |
-
const
|
| 55 |
-
const
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
return `<!DOCTYPE html>
|
| 59 |
-
<html><head><meta charset="utf-8">${FONTS}${
|
| 60 |
-
<style>
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
.glow-
|
| 66 |
-
|
| 67 |
-
bottom:15%;right:-100px;pointer-events:none;z-index:0; }
|
| 68 |
</style></head><body>
|
| 69 |
-
<div id="root" data-composition-id="${compId}" data-start="0" data-width="${w}" data-height="${h}">
|
| 70 |
|
| 71 |
-
<!-- Persistent
|
| 72 |
-
<div class="glow-
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
<!--
|
| 76 |
<div id="s1" class="clip sc" data-start="0" data-duration="4" data-track-index="0">
|
| 77 |
-
${mediaEl(
|
| 78 |
-
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.
|
| 79 |
</div>
|
| 80 |
-
<
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
<div
|
| 96 |
-
${mediaEl(mImg(1), 4, 4, 6, 0)}
|
| 97 |
-
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.55);"></div>
|
| 98 |
</div>
|
| 99 |
-
<
|
| 100 |
-
|
| 101 |
-
style="position:absolute;top:50%;left:0;right:0;transform:translateY(-55%);
|
| 102 |
-
text-align:center;color:#fff;">
|
| 103 |
<div style="font:900 160px/1.0 'Space Grotesk',sans-serif;letter-spacing:-4px;
|
| 104 |
-
|
| 105 |
-
<div style="font:600 48px/1.2 'Plus Jakarta Sans',sans-serif;opacity:0.85;
|
| 106 |
-
|
| 107 |
-
<div style="font:400 30px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.55);
|
| 108 |
-
margin-top:10px;">sejak 2023</div>
|
| 109 |
</div>
|
| 110 |
-
<
|
| 111 |
-
|
| 112 |
-
style="position:absolute;bottom:180px;left:0;padding:0 0 0 56px;">
|
| 113 |
<div style="background:rgba(10,10,25,0.85);backdrop-filter:blur(20px) saturate(150%);
|
| 114 |
-
|
| 115 |
-
padding:18px 32px;display:inline-block;">
|
| 116 |
<div style="font:700 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#fff;">${clientName}</div>
|
| 117 |
-
<div style="font:400 24px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.62);
|
| 118 |
-
margin-top:6px;">AI Training \u00b7 ${year}</div>
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
|
| 122 |
-
<!--
|
| 123 |
-
<div id="s3" class="clip sc" data-start="
|
| 124 |
${mVid
|
| 125 |
-
? `<div style="position:absolute;inset:0
|
| 126 |
-
<
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
</div>`
|
| 131 |
-
: `${mediaEl(mImg(2), 8, 8, 10, 0)}`
|
| 132 |
-
}
|
| 133 |
-
<!-- Gradient for caption readability -->
|
| 134 |
<div style="position:absolute;bottom:0;left:0;right:0;height:500px;
|
| 135 |
-
|
| 136 |
</div>
|
| 137 |
-
<
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
color:#fff;text-shadow:0 4px 24px rgba(0,0,0,0.95);
|
| 142 |
-
font-style:italic;">
|
| 143 |
“Saya pikir AI susah.<br>Ternyata langsung bisa!”
|
| 144 |
</div>
|
| 145 |
|
| 146 |
-
<!--
|
| 147 |
-
<
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
</div>
|
| 151 |
-
<div id="cap-e1" class="clip" data-start="16.2" data-duration="2.1" data-track-index="14"
|
| 152 |
-
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 153 |
-
font:900 128px/1.0 'Space Grotesk',sans-serif;color:#fff;
|
| 154 |
-
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">80% hands-on</div>
|
| 155 |
-
|
| 156 |
-
<!-- βββ SCENE 4b: ENERGY CUT 2 β 18.5β21s βββββββββββββββββ -->
|
| 157 |
-
<div id="s4b" class="clip sc" data-start="18.5" data-duration="2.5" data-track-index="15">
|
| 158 |
-
${mediaEl(mImg(1), 18.5, 2.5, 16, 0)}
|
| 159 |
-
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.32);"></div>
|
| 160 |
</div>
|
| 161 |
-
<div id="
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
<!--
|
| 167 |
-
<div id="
|
| 168 |
-
${mediaEl(
|
| 169 |
-
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.
|
| 170 |
</div>
|
| 171 |
-
<div id="
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
<!--
|
| 177 |
-
<div id="
|
| 178 |
-
${mediaEl(
|
| 179 |
-
<div style="position:absolute;inset:0;background:rgba(
|
| 180 |
</div>
|
| 181 |
-
<div id="
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</div>
|
| 187 |
-
<div id="
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
| 196 |
</div>
|
| 197 |
-
<div id="en" class="clip" data-start="28.
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
<div id="et" class="clip" data-start="28.
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
<div id="ew" class="clip" data-start="28.
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
<!-- Music -->
|
|
|
|
| 208 |
<audio data-start="0" data-duration="${dur}" data-track-index="50"
|
| 209 |
-
data-volume="0.
|
| 210 |
|
| 211 |
-
<!-- Grain overlay -->
|
| 212 |
${GRAIN}
|
| 213 |
|
| 214 |
-
<!-- Persistent brand bar -->
|
| 215 |
<div style="position:absolute;bottom:0;left:0;right:0;padding:20px 56px 34px;
|
| 216 |
-
|
| 217 |
<span style="font:700 26px/1 'Space Grotesk',sans-serif;color:#fff;">${trainerName}</span>
|
| 218 |
-
<span style="font:400 17px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.58);
|
| 219 |
-
margin-left:8px;">\u00b7 ${tagline}</span>
|
| 220 |
</div>
|
| 221 |
|
| 222 |
<script>
|
| 223 |
const tl = gsap.timeline({ paused: true });
|
| 224 |
|
| 225 |
-
// S1 HOOK β slam from above (expo.out
|
| 226 |
-
tl.from("#
|
| 227 |
-
tl.from("#
|
| 228 |
-
tl.from("#
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
tl.
|
| 234 |
-
tl.
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
tl.
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
//
|
| 242 |
-
|
| 243 |
-
tl.to("#
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
tl.from("#
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
tl.from("#
|
| 260 |
-
tl.
|
| 261 |
-
|
| 262 |
-
//
|
| 263 |
-
tl.
|
| 264 |
-
tl.from("#
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
tl.from("#
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
window.__timelines = window.__timelines || {};
|
| 279 |
window.__timelines["${compId}"] = tl;
|
| 280 |
</script>
|
|
@@ -282,26 +266,17 @@ ${GRAIN}
|
|
| 282 |
},
|
| 283 |
};
|
| 284 |
|
| 285 |
-
// Teaser: 30s fast-cut
|
| 286 |
const teaser = {
|
| 287 |
-
meta: { name: 'Event Teaser', icon: '\u26A1', description: '30s fast-cut teaser. 9:16.', format: '9:16', duration: 30, music: 'energetic' },
|
| 288 |
-
buildHTML(media, config, compId, w, h, dur) {
|
| 289 |
-
return testimonial.buildHTML(media, { ...config, clientName: config.clientName || 'COMING SOON' }, compId, w, h, dur);
|
| 290 |
-
},
|
| 291 |
};
|
| 292 |
-
|
| 293 |
const trailer = {
|
| 294 |
meta: { name: 'Cinematic Trailer', icon: '\uD83C\uDFAC', description: '60s cinematic recap. 9:16.', format: '9:16', duration: 60, music: 'cinematic' },
|
| 295 |
-
buildHTML(media, config, compId, w, h, dur) {
|
| 296 |
-
return testimonial.buildHTML(media, config, compId, w, h, dur);
|
| 297 |
-
},
|
| 298 |
};
|
| 299 |
-
|
| 300 |
const community = {
|
| 301 |
-
meta: { name: 'Community Story', icon: '\uD83E\uDD1D', description: '30s warm community. 9:16.', format: '9:16', duration: 30, music: 'warm' },
|
| 302 |
-
buildHTML(media, config, compId, w, h, dur) {
|
| 303 |
-
return testimonial.buildHTML(media, config, compId, w, h, dur);
|
| 304 |
-
},
|
| 305 |
};
|
| 306 |
|
| 307 |
export const WORKFLOWS = { testimonial, teaser, trailer, community };
|
|
|
|
| 1 |
// workflows/index.js β Dee Ferdinand Video Studio
|
| 2 |
+
// Fixed: black frames, cutoff, timing gaps, exit animation ban, autoAlpha
|
| 3 |
+
// Audio: no muted on video, research-backed data-volume levels
|
| 4 |
+
// Music: ElevenLabs lo-fi upbeat (primary) β YuE (fallback) β silence
|
| 5 |
+
|
| 6 |
+
const FONTS = `<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700;900&family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600&display=swap" rel="stylesheet">`;
|
| 7 |
+
const GSAP = `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
const GRAIN = `<div style="position:absolute;inset:0;pointer-events:none;z-index:98;overflow:hidden;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
<div style="position:absolute;top:-50%;left:-50%;width:200%;height:200%;opacity:0.065;
|
| 11 |
background:url('data:image/svg+xml,%3Csvg viewBox=\'0 0 256 256\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'n\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.65\' numOctaves=\'3\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23n)\'/%3E%3C/svg%3E');
|
| 12 |
animation:grain 0.5s steps(1) infinite;"></div>
|
| 13 |
</div>`;
|
| 14 |
|
| 15 |
+
function mediaEl(m, start, dur, tIdx, vol) {
|
| 16 |
+
if (!m) return '<div style="position:absolute;inset:0;background:#0d0d1f;"></div>';
|
| 17 |
+
if (m.mime?.startsWith('video/')) {
|
| 18 |
+
// NO muted β subject voice must come through. Framework controls via data-volume.
|
| 19 |
+
return `<div style="position:absolute;inset:0">
|
| 20 |
+
<video data-start="${start}" data-duration="${dur}" data-track-index="${tIdx}"
|
| 21 |
+
data-volume="${vol}" src="${m.rel}" playsinline
|
| 22 |
+
style="width:100%;height:100%;object-fit:cover;"></video></div>`;
|
| 23 |
+
}
|
| 24 |
+
return `<img src="${m.rel}" alt="" style="width:100%;height:100%;object-fit:cover;">` ;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// ββ TESTIMONIAL 30s 9:16 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
// Timing: each scene overlaps the previous by 0.35s (transition window)
|
| 29 |
+
// Track indices: each scene on unique track, captions on unique tracks
|
| 30 |
+
// NO exit animations (rule: transition IS the exit)
|
| 31 |
const testimonial = {
|
| 32 |
+
meta: { name: 'Corporate Testimonial', icon: '\uD83C\uDFE2',
|
| 33 |
+
description: '30s kinetic testimonial. 9:16. ElevenLabs lo-fi music.',
|
| 34 |
+
format: '9:16', duration: 30, music: 'lofi-upbeat' },
|
|
|
|
|
|
|
| 35 |
|
| 36 |
buildHTML(media, config, compId, w, h, dur) {
|
| 37 |
+
const { clientName='AI Training', trainerName='Dee Ferdinand',
|
| 38 |
+
tagline='AI Corporate Trainer', website='deeferdinand.com' } = config;
|
| 39 |
const vids = media.filter(m => m.mime?.startsWith('video/'));
|
| 40 |
const imgs = media.filter(m => m.mime?.startsWith('image/'));
|
| 41 |
+
const pick = (i) => media[i % Math.max(media.length,1)] || media[0];
|
| 42 |
+
const img = (i) => imgs[i % Math.max(imgs.length,1)] || pick(i);
|
| 43 |
+
const mVid = vids[0] || null;
|
| 44 |
+
const yr = new Date().getFullYear();
|
| 45 |
+
|
| 46 |
+
// Strict scene timing: NO gaps, transitions overlap by 0.35s on DIFFERENT tracks
|
| 47 |
+
// S1: 0-4s S2: 3.65-8s (T1 at 3.65) S3: 7.65-16.5s (T2 at 7.65)
|
| 48 |
+
// S4a: 15.85-18.5s S4b: 18.2-21s S4c: 20.7-23.2s
|
| 49 |
+
// S5: 22.85-28.5s (T3 at 22.85) S6: 28.2-30.5s
|
| 50 |
+
// All captions on own tracks, gap from scene start: 0.2s
|
| 51 |
|
| 52 |
return `<!DOCTYPE html>
|
| 53 |
+
<html><head><meta charset="utf-8">${FONTS}${GSAP}
|
| 54 |
+
<style>
|
| 55 |
+
* {margin:0;padding:0;box-sizing:border-box}
|
| 56 |
+
body {background:#0d0d1f;overflow:hidden}
|
| 57 |
+
#root {position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#0d0d1f}
|
| 58 |
+
.sc {position:absolute;inset:0}
|
| 59 |
+
.glow-c {position:absolute;border-radius:50%;pointer-events:none}
|
| 60 |
+
@keyframes grain{0%,100%{transform:translate(0,0)}10%{transform:translate(-5%,-5%)}30%{transform:translate(5%,-10%)}50%{transform:translate(-10%,5%)}70%{transform:translate(0,10%)}90%{transform:translate(10%,5%)}}
|
|
|
|
| 61 |
</style></head><body>
|
| 62 |
+
<div id="root" data-composition-id="${compId}" data-start="0" data-duration="${dur}" data-width="${w}" data-height="${h}">
|
| 63 |
|
| 64 |
+
<!-- Persistent depth glows -->
|
| 65 |
+
<div class="glow-c" style="width:600px;height:600px;top:8%;left:50%;transform:translateX(-50%);
|
| 66 |
+
background:radial-gradient(ellipse,rgba(124,111,224,0.16) 0%,transparent 70%);"></div>
|
| 67 |
+
<div class="glow-c" style="width:400px;height:400px;bottom:20%;right:-80px;
|
| 68 |
+
background:radial-gradient(ellipse,rgba(224,111,154,0.12) 0%,transparent 70%);"></div>
|
| 69 |
|
| 70 |
+
<!-- S1 HOOK 0-4s track:0 -->
|
| 71 |
<div id="s1" class="clip sc" data-start="0" data-duration="4" data-track-index="0">
|
| 72 |
+
${mediaEl(img(0),0,4,1,0)}
|
| 73 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.40);"></div>
|
| 74 |
</div>
|
| 75 |
+
<div id="h1" class="clip" data-start="0.2" data-duration="3.5" data-track-index="2"
|
| 76 |
+
style="position:absolute;bottom:580px;left:0;right:0;padding:0 56px;text-align:center;
|
| 77 |
+
font:900 160px/1.0 'Space Grotesk',sans-serif;color:#fff;
|
| 78 |
+
text-shadow:0 6px 40px rgba(0,0,0,0.95);letter-spacing:-3px;">Ini yang</div>
|
| 79 |
+
<div id="h2" class="clip" data-start="0.35" data-duration="3.3" data-track-index="3"
|
| 80 |
+
style="position:absolute;bottom:376px;left:0;right:0;padding:0 56px;text-align:center;
|
| 81 |
+
font:900 160px/1.0 'Space Grotesk',sans-serif;color:#7C6FE0;
|
| 82 |
+
text-shadow:0 6px 40px rgba(0,0,0,0.9);letter-spacing:-3px;">terjadi</div>
|
| 83 |
+
<div id="h3" class="clip" data-start="0.52" data-duration="3.1" data-track-index="4"
|
| 84 |
+
style="position:absolute;bottom:260px;left:0;right:0;padding:0 56px;text-align:center;
|
| 85 |
+
font:700 72px/1.2 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.82);">ketika tim belajar AI</div>
|
| 86 |
+
|
| 87 |
+
<!-- S2 PROOF 3.65-7.65s track:5 β overlaps S1 by 0.35s for zoom-through transition -->
|
| 88 |
+
<div id="s2" class="clip sc" data-start="3.65" data-duration="4" data-track-index="5">
|
| 89 |
+
${mediaEl(img(1),3.65,4,6,0)}
|
| 90 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.52);"></div>
|
|
|
|
|
|
|
| 91 |
</div>
|
| 92 |
+
<div id="st" class="clip" data-start="3.95" data-duration="3.4" data-track-index="7"
|
| 93 |
+
style="position:absolute;top:50%;left:0;right:0;transform:translateY(-55%);text-align:center;color:#fff;">
|
|
|
|
|
|
|
| 94 |
<div style="font:900 160px/1.0 'Space Grotesk',sans-serif;letter-spacing:-4px;
|
| 95 |
+
text-shadow:0 6px 40px rgba(0,0,0,0.9);">1,000+</div>
|
| 96 |
+
<div style="font:600 48px/1.2 'Plus Jakarta Sans',sans-serif;opacity:0.85;margin-top:16px;">profesional terlatih</div>
|
| 97 |
+
<div style="font:400 30px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.55);margin-top:10px;">sejak 2023</div>
|
|
|
|
|
|
|
| 98 |
</div>
|
| 99 |
+
<div id="lt" class="clip" data-start="4.2" data-duration="3.1" data-track-index="8"
|
| 100 |
+
style="position:absolute;bottom:180px;left:0;padding:0 0 0 56px;">
|
|
|
|
| 101 |
<div style="background:rgba(10,10,25,0.85);backdrop-filter:blur(20px) saturate(150%);
|
| 102 |
+
border-left:6px solid #7C6FE0;border-radius:0 16px 16px 0;padding:18px 32px;display:inline-block;">
|
|
|
|
| 103 |
<div style="font:700 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#fff;">${clientName}</div>
|
| 104 |
+
<div style="font:400 24px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.62);margin-top:6px;">AI Training \u00b7 ${yr}</div>
|
|
|
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
+
<!-- S3 TESTIMONIAL 7.65-16.5s track:9 -->
|
| 109 |
+
<div id="s3" class="clip sc" data-start="7.65" data-duration="8.85" data-track-index="9">
|
| 110 |
${mVid
|
| 111 |
+
? `<div style="position:absolute;inset:0">
|
| 112 |
+
<video data-start="7.65" data-duration="8.85" data-track-index="10"
|
| 113 |
+
data-volume="0.88" src="${mVid.rel}" playsinline
|
| 114 |
+
style="width:100%;height:100%;object-fit:cover;"></video></div>`
|
| 115 |
+
: `${mediaEl(img(2),7.65,8.85,10,0)}`}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
<div style="position:absolute;bottom:0;left:0;right:0;height:500px;
|
| 117 |
+
background:linear-gradient(transparent,rgba(0,0,0,0.82));"></div>
|
| 118 |
</div>
|
| 119 |
+
<div id="q" class="clip" data-start="8.2" data-duration="7.7" data-track-index="11"
|
| 120 |
+
style="position:absolute;bottom:240px;left:0;right:0;padding:0 56px;text-align:center;
|
| 121 |
+
font:600 72px/1.3 'Plus Jakarta Sans',sans-serif;color:#fff;font-style:italic;
|
| 122 |
+
text-shadow:0 4px 24px rgba(0,0,0,0.95);">
|
|
|
|
|
|
|
| 123 |
“Saya pikir AI susah.<br>Ternyata langsung bisa!”
|
| 124 |
</div>
|
| 125 |
|
| 126 |
+
<!-- ENERGY 4 cuts: each ~2s, hard cuts, different tracks -->
|
| 127 |
+
<!-- S4a: 15.85-18.5s track:12 -->
|
| 128 |
+
<div id="s4a" class="clip sc" data-start="15.85" data-duration="2.65" data-track-index="12">
|
| 129 |
+
${mediaEl(img(0),15.85,2.65,13,0)}
|
| 130 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.30);"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
</div>
|
| 132 |
+
<div id="e1" class="clip" data-start="16.05" data-duration="2.3" data-track-index="14"
|
| 133 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 134 |
+
font:900 128px/1.0 'Space Grotesk',sans-serif;color:#fff;
|
| 135 |
+
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">80%<br>hands-on</div>
|
| 136 |
+
|
| 137 |
+
<!-- S4b: 18.2-21s track:15 -->
|
| 138 |
+
<div id="s4b" class="clip sc" data-start="18.2" data-duration="2.8" data-track-index="15">
|
| 139 |
+
${mediaEl(img(1),18.2,2.8,16,0)}
|
| 140 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.30);"></div>
|
| 141 |
</div>
|
| 142 |
+
<div id="e2" class="clip" data-start="18.4" data-duration="2.4" data-track-index="17"
|
| 143 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 144 |
+
font:900 128px/1.0 'Space Grotesk',sans-serif;color:#fff;
|
| 145 |
+
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">Langsung<br>praktek</div>
|
| 146 |
+
|
| 147 |
+
<!-- S4c: 20.7-23.2s track:18 -->
|
| 148 |
+
<div id="s4c" class="clip sc" data-start="20.7" data-duration="2.5" data-track-index="18">
|
| 149 |
+
${mediaEl(img(2),20.7,2.5,19,0)}
|
| 150 |
+
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.30);"></div>
|
| 151 |
</div>
|
| 152 |
+
<div id="e3" class="clip" data-start="20.9" data-duration="2.1" data-track-index="20"
|
| 153 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 154 |
+
font:900 144px/1.0 'Space Grotesk',sans-serif;color:#9EF0C8;
|
| 155 |
+
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">Real<br>output \u2713</div>
|
| 156 |
+
|
| 157 |
+
<!-- S5 CTA 22.85-28.5s track:21 -->
|
| 158 |
+
<div id="s5" class="clip sc" data-start="22.85" data-duration="5.65" data-track-index="21">
|
| 159 |
+
${mediaEl(img(0),22.85,5.65,22,0)}
|
| 160 |
+
<div style="position:absolute;inset:0;background:rgba(5,3,18,0.70);"></div>
|
| 161 |
</div>
|
| 162 |
+
<div id="c1" class="clip" data-start="23.1" data-duration="5.1" data-track-index="23"
|
| 163 |
+
style="position:absolute;top:34%;left:0;right:0;padding:0 56px;text-align:center;
|
| 164 |
+
font:900 112px/1.15 'Space Grotesk',sans-serif;color:#fff;
|
| 165 |
+
letter-spacing:-2px;text-shadow:0 4px 32px rgba(0,0,0,0.9);">Training AI<br>untuk tim kamu?</div>
|
| 166 |
+
<div id="c2" class="clip" data-start="25" data-duration="3.15" data-track-index="24"
|
| 167 |
+
style="position:absolute;top:59%;left:0;right:0;padding:0 56px;text-align:center;
|
| 168 |
+
font:500 44px/1.2 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.78);">DM \u00b7 atau klik link di bio</div>
|
| 169 |
+
|
| 170 |
+
<!-- S6 BRAND END 28.2-30.5s track:25 -->
|
| 171 |
+
<div id="s6" class="clip sc" data-start="28.2" data-duration="2.3" data-track-index="25"
|
| 172 |
+
style="background:linear-gradient(135deg,#0d0d1f 0%,#1a1045 50%,#0d0d1f 100%);">
|
| 173 |
+
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 50% 40%,rgba(124,111,224,0.26) 0%,transparent 65%);"></div>
|
| 174 |
</div>
|
| 175 |
+
<div id="en" class="clip" data-start="28.4" data-duration="2.1" data-track-index="26"
|
| 176 |
+
style="position:absolute;top:42%;left:0;right:0;text-align:center;
|
| 177 |
+
font:800 80px/1.1 'Space Grotesk',sans-serif;color:#fff;">${trainerName}</div>
|
| 178 |
+
<div id="et" class="clip" data-start="28.6" data-duration="1.9" data-track-index="27"
|
| 179 |
+
style="position:absolute;top:52%;left:0;right:0;text-align:center;
|
| 180 |
+
font:500 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#9990ee;">${tagline}</div>
|
| 181 |
+
<div id="ew" class="clip" data-start="28.8" data-duration="1.7" data-track-index="28"
|
| 182 |
+
style="position:absolute;top:59%;left:0;right:0;text-align:center;
|
| 183 |
+
font:400 28px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.48);">${website}</div>
|
| 184 |
+
|
| 185 |
+
<!-- Music: lo-fi upbeat, generated by ElevenLabs -->
|
| 186 |
+
<!-- Volume 0.10 during energy/CTA, see audio-design.md -->
|
| 187 |
<audio data-start="0" data-duration="${dur}" data-track-index="50"
|
| 188 |
+
data-volume="0.10" src="./assets/music.mp3"></audio>
|
| 189 |
|
|
|
|
| 190 |
${GRAIN}
|
| 191 |
|
| 192 |
+
<!-- Persistent brand bar (not a clip β always visible) -->
|
| 193 |
<div style="position:absolute;bottom:0;left:0;right:0;padding:20px 56px 34px;
|
| 194 |
+
background:linear-gradient(transparent,rgba(0,0,0,0.65));z-index:97;">
|
| 195 |
<span style="font:700 26px/1 'Space Grotesk',sans-serif;color:#fff;">${trainerName}</span>
|
| 196 |
+
<span style="font:400 17px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.58);margin-left:8px;">\u00b7 ${tagline}</span>
|
|
|
|
| 197 |
</div>
|
| 198 |
|
| 199 |
<script>
|
| 200 |
const tl = gsap.timeline({ paused: true });
|
| 201 |
|
| 202 |
+
// S1 HOOK β slam from above (expo.out: kinetic, fast)
|
| 203 |
+
tl.from("#h1", { y: -100, autoAlpha: 0, duration: 0.22, ease: "expo.out" }, 0.2);
|
| 204 |
+
tl.from("#h2", { y: -100, autoAlpha: 0, duration: 0.22, ease: "expo.out" }, 0.35);
|
| 205 |
+
tl.from("#h3", { y: 40, autoAlpha: 0, duration: 0.35, ease: "power3.out" }, 0.52);
|
| 206 |
+
|
| 207 |
+
// TRANSITION 1β2: Zoom-through (dramatic opener)
|
| 208 |
+
// Rule: NO exit animation on s1 content β transition handles the handoff
|
| 209 |
+
const T1 = 3.65;
|
| 210 |
+
tl.to("#s1", { autoAlpha: 0, scale: 1.32, duration: 0.35, ease: "power4.inOut" }, T1);
|
| 211 |
+
tl.fromTo("#s2", { autoAlpha: 0, scale: 0.80 }, { autoAlpha: 1, scale: 1, duration: 0.35, ease: "power4.inOut" }, T1);
|
| 212 |
+
|
| 213 |
+
// S2 PROOF β scale pop (back.out: bouncy confidence)
|
| 214 |
+
tl.from("#st", { scale: 0.55, autoAlpha: 0, duration: 0.5, ease: "back.out(2.5)" }, 3.95);
|
| 215 |
+
tl.from("#lt", { x: -400, autoAlpha: 0, duration: 0.4, ease: "power3.out" }, 4.2);
|
| 216 |
+
// NO exit tween on #lt β transition covers it
|
| 217 |
+
|
| 218 |
+
// TRANSITION 2β3: Push-slide left (editorial)
|
| 219 |
+
const T2 = 7.65;
|
| 220 |
+
tl.to("#s2", { autoAlpha: 0, xPercent: -18, duration: 0.32, ease: "power2.inOut" }, T2);
|
| 221 |
+
tl.fromTo("#s3", { autoAlpha: 0, xPercent: 18 }, { autoAlpha: 1, xPercent: 0, duration: 0.32, ease: "power2.inOut" }, T2);
|
| 222 |
+
|
| 223 |
+
// S3 TESTIMONIAL β opacity only (sine.inOut: calm, intentional contrast)
|
| 224 |
+
tl.from("#q", { autoAlpha: 0, duration: 0.55, ease: "sine.inOut" }, 8.2);
|
| 225 |
+
// NO exit tween on #q β transition handles
|
| 226 |
+
|
| 227 |
+
// TRANSITION 3β4a: Blur crossfade (drift into energy)
|
| 228 |
+
const T3 = 15.85;
|
| 229 |
+
tl.to("#s3", { autoAlpha: 0, filter: "blur(14px)", scale: 1.04, duration: 0.45, ease: "sine.inOut" }, T3);
|
| 230 |
+
tl.from("#s4a", { autoAlpha: 0, duration: 0.22 }, T3 + 0.23);
|
| 231 |
+
|
| 232 |
+
// S4a ENERGY β slide from left (power4.out: decisive)
|
| 233 |
+
tl.from("#e1", { x: -120, autoAlpha: 0, duration: 0.25, ease: "power4.out" }, 16.05);
|
| 234 |
+
|
| 235 |
+
// S4b: Hard cut (disruption intentional) + slide from right
|
| 236 |
+
tl.from("#s4b", { autoAlpha: 0, duration: 0.18 }, 18.2);
|
| 237 |
+
tl.from("#e2", { x: 120, autoAlpha: 0, duration: 0.25, ease: "power4.out" }, 18.4);
|
| 238 |
+
|
| 239 |
+
// S4c: Hard cut + scale pop (energy green color)
|
| 240 |
+
tl.from("#s4c", { autoAlpha: 0, duration: 0.15 }, 20.7);
|
| 241 |
+
tl.from("#e3", { scale: 0.60, autoAlpha: 0, duration: 0.3, ease: "back.out(2.8)" }, 20.9);
|
| 242 |
+
|
| 243 |
+
// TRANSITION 4β5: Hard cut into CTA
|
| 244 |
+
tl.from("#s5", { autoAlpha: 0, duration: 0.25 }, 22.85);
|
| 245 |
+
tl.from("#c1", { y: 80, autoAlpha: 0, duration: 0.5, ease: "power3.out" }, 23.1);
|
| 246 |
+
tl.from("#c2", { y: 50, autoAlpha: 0, duration: 0.45, ease: "power3.out" }, 25);
|
| 247 |
+
|
| 248 |
+
// TRANSITION 5β6: Brand end card
|
| 249 |
+
const T5 = 28.2;
|
| 250 |
+
tl.to("#s5", { autoAlpha: 0, duration: 0.3 }, T5);
|
| 251 |
+
tl.from("#s6", { autoAlpha: 0, duration: 0.3 }, T5);
|
| 252 |
+
// Final scene IS allowed exit animations β but we fade in instead
|
| 253 |
+
tl.from("#en", { y: 36, autoAlpha: 0, duration: 0.45, ease: "power3.out" }, 28.4);
|
| 254 |
+
tl.from("#et", { y: 28, autoAlpha: 0, duration: 0.40, ease: "power3.out" }, 28.6);
|
| 255 |
+
tl.from("#ew", { y: 20, autoAlpha: 0, duration: 0.35, ease: "power3.out" }, 28.8);
|
| 256 |
+
|
| 257 |
+
// CRITICAL: extend timeline to prevent cutoff + black frames
|
| 258 |
+
// Use tl.set() not tl.to() β tl.to({},{duration:X}) can conflict
|
| 259 |
+
tl.set({}, {}, ${dur});
|
| 260 |
+
|
| 261 |
+
// CRITICAL: key MUST match data-composition-id exactly
|
| 262 |
window.__timelines = window.__timelines || {};
|
| 263 |
window.__timelines["${compId}"] = tl;
|
| 264 |
</script>
|
|
|
|
| 266 |
},
|
| 267 |
};
|
| 268 |
|
|
|
|
| 269 |
const teaser = {
|
| 270 |
+
meta: { name: 'Event Teaser', icon: '\u26A1', description: '30s fast-cut teaser. 9:16. ElevenLabs energetic.', format: '9:16', duration: 30, music: 'energetic' },
|
| 271 |
+
buildHTML(media, config, compId, w, h, dur) { return testimonial.buildHTML(media, config, compId, w, h, dur); },
|
|
|
|
|
|
|
| 272 |
};
|
|
|
|
| 273 |
const trailer = {
|
| 274 |
meta: { name: 'Cinematic Trailer', icon: '\uD83C\uDFAC', description: '60s cinematic recap. 9:16.', format: '9:16', duration: 60, music: 'cinematic' },
|
| 275 |
+
buildHTML(media, config, compId, w, h, dur) { return testimonial.buildHTML(media, config, compId, w, h, dur); },
|
|
|
|
|
|
|
| 276 |
};
|
|
|
|
| 277 |
const community = {
|
| 278 |
+
meta: { name: 'Community Story', icon: '\uD83E\uDD1D', description: '30s warm community story. 9:16.', format: '9:16', duration: 30, music: 'warm' },
|
| 279 |
+
buildHTML(media, config, compId, w, h, dur) { return testimonial.buildHTML(media, config, compId, w, h, dur); },
|
|
|
|
|
|
|
| 280 |
};
|
| 281 |
|
| 282 |
export const WORKFLOWS = { testimonial, teaser, trailer, community };
|