Spaces:
Configuration error
Configuration error
File size: 16,797 Bytes
c0f1ad6 1f05b41 a2e4ffd 1f05b41 c0f1ad6 1f05b41 410447e 1f05b41 410447e a2e4ffd 1f05b41 c0f1ad6 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 c0f1ad6 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 c0f1ad6 1f05b41 c0f1ad6 1f05b41 c0f1ad6 1f05b41 c0f1ad6 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 c0f1ad6 1f05b41 a2e4ffd 1f05b41 c0f1ad6 a2e4ffd 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 a2e4ffd 1f05b41 c0f1ad6 1f05b41 c0f1ad6 1f05b41 c0f1ad6 1f05b41 c0f1ad6 1f05b41 a2e4ffd 1f05b41 a2e4ffd 410447e a2e4ffd 1f05b41 c0f1ad6 1f05b41 410447e c0f1ad6 1f05b41 c0f1ad6 a2e4ffd 410447e | 1 2 3 4 5 6 7 8 9 10 11 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | // workflows/index.js β Dee Ferdinand Video Studio
// Fixed: black frames, cutoff, timing gaps, exit animation ban, autoAlpha
// Audio: no muted on video, research-backed data-volume levels
// Music: ElevenLabs lo-fi upbeat (primary) β YuE (fallback) β silence
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">`;
const GSAP = `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>`;
const GRAIN = `<div style="position:absolute;inset:0;pointer-events:none;z-index:98;overflow:hidden;">
<div style="position:absolute;top:-50%;left:-50%;width:200%;height:200%;opacity:0.065;
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');
animation:grain 0.5s steps(1) infinite;"></div>
</div>`;
function mediaEl(m, start, dur, tIdx, vol) {
if (!m) return '<div style="position:absolute;inset:0;background:#0d0d1f;"></div>';
if (m.mime?.startsWith('video/')) {
// NO muted β subject voice must come through. Framework controls via data-volume.
return `<div style="position:absolute;inset:0">
<video data-start="${start}" data-duration="${dur}" data-track-index="${tIdx}"
data-volume="${vol}" src="${m.rel}" playsinline
style="width:100%;height:100%;object-fit:cover;"></video></div>`;
}
return `<img src="${m.rel}" alt="" style="width:100%;height:100%;object-fit:cover;">` ;
}
// ββ TESTIMONIAL 30s 9:16 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Timing: each scene overlaps the previous by 0.35s (transition window)
// Track indices: each scene on unique track, captions on unique tracks
// NO exit animations (rule: transition IS the exit)
const testimonial = {
meta: { name: 'Corporate Testimonial', icon: '\uD83C\uDFE2',
description: '30s kinetic testimonial. 9:16. ElevenLabs lo-fi music.',
format: '9:16', duration: 30, music: 'lofi-upbeat' },
buildHTML(media, config, compId, w, h, dur) {
const { clientName='AI Training', trainerName='Dee Ferdinand',
tagline='AI Corporate Trainer', website='deeferdinand.com' } = config;
const vids = media.filter(m => m.mime?.startsWith('video/'));
const imgs = media.filter(m => m.mime?.startsWith('image/'));
const pick = (i) => media[i % Math.max(media.length,1)] || media[0];
const img = (i) => imgs[i % Math.max(imgs.length,1)] || pick(i);
const mVid = vids[0] || null;
const yr = new Date().getFullYear();
// Strict scene timing: NO gaps, transitions overlap by 0.35s on DIFFERENT tracks
// S1: 0-4s S2: 3.65-8s (T1 at 3.65) S3: 7.65-16.5s (T2 at 7.65)
// S4a: 15.85-18.5s S4b: 18.2-21s S4c: 20.7-23.2s
// S5: 22.85-28.5s (T3 at 22.85) S6: 28.2-30.5s
// All captions on own tracks, gap from scene start: 0.2s
return `<!DOCTYPE html>
<html><head><meta charset="utf-8">${FONTS}${GSAP}
<style>
* {margin:0;padding:0;box-sizing:border-box}
body {background:#0d0d1f;overflow:hidden}
#root {position:relative;width:${w}px;height:${h}px;overflow:hidden;background:#0d0d1f}
.sc {position:absolute;inset:0}
.glow-c {position:absolute;border-radius:50%;pointer-events:none}
@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%)}}
</style></head><body>
<div id="root" data-composition-id="${compId}" data-start="0" data-duration="${dur}" data-width="${w}" data-height="${h}">
<!-- Persistent depth glows -->
<div class="glow-c" style="width:600px;height:600px;top:8%;left:50%;transform:translateX(-50%);
background:radial-gradient(ellipse,rgba(124,111,224,0.16) 0%,transparent 70%);"></div>
<div class="glow-c" style="width:400px;height:400px;bottom:20%;right:-80px;
background:radial-gradient(ellipse,rgba(224,111,154,0.12) 0%,transparent 70%);"></div>
<!-- S1 HOOK 0-4s track:0 -->
<div id="s1" class="clip sc" data-start="0" data-duration="4" data-track-index="0">
${mediaEl(img(0),0,4,1,0)}
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.40);"></div>
</div>
<div id="h1" class="clip" data-start="0.2" data-duration="3.5" data-track-index="2"
style="position:absolute;bottom:580px;left:0;right:0;padding:0 56px;text-align:center;
font:900 160px/1.0 'Space Grotesk',sans-serif;color:#fff;
text-shadow:0 6px 40px rgba(0,0,0,0.95);letter-spacing:-3px;">Ini yang</div>
<div id="h2" class="clip" data-start="0.35" data-duration="3.3" data-track-index="3"
style="position:absolute;bottom:376px;left:0;right:0;padding:0 56px;text-align:center;
font:900 160px/1.0 'Space Grotesk',sans-serif;color:#7C6FE0;
text-shadow:0 6px 40px rgba(0,0,0,0.9);letter-spacing:-3px;">terjadi</div>
<div id="h3" class="clip" data-start="0.52" data-duration="3.1" data-track-index="4"
style="position:absolute;bottom:260px;left:0;right:0;padding:0 56px;text-align:center;
font:700 72px/1.2 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.82);">ketika tim belajar AI</div>
<!-- S2 PROOF 3.65-7.65s track:5 β overlaps S1 by 0.35s for zoom-through transition -->
<div id="s2" class="clip sc" data-start="3.65" data-duration="4" data-track-index="5">
${mediaEl(img(1),3.65,4,6,0)}
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.52);"></div>
</div>
<div id="st" class="clip" data-start="3.95" data-duration="3.4" data-track-index="7"
style="position:absolute;top:50%;left:0;right:0;transform:translateY(-55%);text-align:center;color:#fff;">
<div style="font:900 160px/1.0 'Space Grotesk',sans-serif;letter-spacing:-4px;
text-shadow:0 6px 40px rgba(0,0,0,0.9);">1,000+</div>
<div style="font:600 48px/1.2 'Plus Jakarta Sans',sans-serif;opacity:0.85;margin-top:16px;">profesional terlatih</div>
<div style="font:400 30px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.55);margin-top:10px;">sejak 2023</div>
</div>
<div id="lt" class="clip" data-start="4.2" data-duration="3.1" data-track-index="8"
style="position:absolute;bottom:180px;left:0;padding:0 0 0 56px;">
<div style="background:rgba(10,10,25,0.85);backdrop-filter:blur(20px) saturate(150%);
border-left:6px solid #7C6FE0;border-radius:0 16px 16px 0;padding:18px 32px;display:inline-block;">
<div style="font:700 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#fff;">${clientName}</div>
<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>
</div>
</div>
<!-- S3 TESTIMONIAL 7.65-16.5s track:9 -->
<div id="s3" class="clip sc" data-start="7.65" data-duration="8.85" data-track-index="9">
${mVid
? `<div style="position:absolute;inset:0">
<video data-start="7.65" data-duration="8.85" data-track-index="10"
data-volume="0.88" src="${mVid.rel}" playsinline
style="width:100%;height:100%;object-fit:cover;"></video></div>`
: `${mediaEl(img(2),7.65,8.85,10,0)}`}
<div style="position:absolute;bottom:0;left:0;right:0;height:500px;
background:linear-gradient(transparent,rgba(0,0,0,0.82));"></div>
</div>
<div id="q" class="clip" data-start="8.2" data-duration="7.7" data-track-index="11"
style="position:absolute;bottom:240px;left:0;right:0;padding:0 56px;text-align:center;
font:600 72px/1.3 'Plus Jakarta Sans',sans-serif;color:#fff;font-style:italic;
text-shadow:0 4px 24px rgba(0,0,0,0.95);">
“Saya pikir AI susah.<br>Ternyata langsung bisa!”
</div>
<!-- ENERGY 4 cuts: each ~2s, hard cuts, different tracks -->
<!-- S4a: 15.85-18.5s track:12 -->
<div id="s4a" class="clip sc" data-start="15.85" data-duration="2.65" data-track-index="12">
${mediaEl(img(0),15.85,2.65,13,0)}
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.30);"></div>
</div>
<div id="e1" class="clip" data-start="16.05" data-duration="2.3" data-track-index="14"
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
font:900 128px/1.0 'Space Grotesk',sans-serif;color:#fff;
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">80%<br>hands-on</div>
<!-- S4b: 18.2-21s track:15 -->
<div id="s4b" class="clip sc" data-start="18.2" data-duration="2.8" data-track-index="15">
${mediaEl(img(1),18.2,2.8,16,0)}
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.30);"></div>
</div>
<div id="e2" class="clip" data-start="18.4" data-duration="2.4" data-track-index="17"
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
font:900 128px/1.0 'Space Grotesk',sans-serif;color:#fff;
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">Langsung<br>praktek</div>
<!-- S4c: 20.7-23.2s track:18 -->
<div id="s4c" class="clip sc" data-start="20.7" data-duration="2.5" data-track-index="18">
${mediaEl(img(2),20.7,2.5,19,0)}
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.30);"></div>
</div>
<div id="e3" class="clip" data-start="20.9" data-duration="2.1" data-track-index="20"
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
font:900 144px/1.0 'Space Grotesk',sans-serif;color:#9EF0C8;
text-shadow:0 4px 24px rgba(0,0,0,0.9);letter-spacing:-2px;">Real<br>output \u2713</div>
<!-- S5 CTA 22.85-28.5s track:21 -->
<div id="s5" class="clip sc" data-start="22.85" data-duration="5.65" data-track-index="21">
${mediaEl(img(0),22.85,5.65,22,0)}
<div style="position:absolute;inset:0;background:rgba(5,3,18,0.70);"></div>
</div>
<div id="c1" class="clip" data-start="23.1" data-duration="5.1" data-track-index="23"
style="position:absolute;top:34%;left:0;right:0;padding:0 56px;text-align:center;
font:900 112px/1.15 'Space Grotesk',sans-serif;color:#fff;
letter-spacing:-2px;text-shadow:0 4px 32px rgba(0,0,0,0.9);">Training AI<br>untuk tim kamu?</div>
<div id="c2" class="clip" data-start="25" data-duration="3.15" data-track-index="24"
style="position:absolute;top:59%;left:0;right:0;padding:0 56px;text-align:center;
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>
<!-- S6 BRAND END 28.2-30.5s track:25 -->
<div id="s6" class="clip sc" data-start="28.2" data-duration="2.3" data-track-index="25"
style="background:linear-gradient(135deg,#0d0d1f 0%,#1a1045 50%,#0d0d1f 100%);">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 50% 40%,rgba(124,111,224,0.26) 0%,transparent 65%);"></div>
</div>
<div id="en" class="clip" data-start="28.4" data-duration="2.1" data-track-index="26"
style="position:absolute;top:42%;left:0;right:0;text-align:center;
font:800 80px/1.1 'Space Grotesk',sans-serif;color:#fff;">${trainerName}</div>
<div id="et" class="clip" data-start="28.6" data-duration="1.9" data-track-index="27"
style="position:absolute;top:52%;left:0;right:0;text-align:center;
font:500 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#9990ee;">${tagline}</div>
<div id="ew" class="clip" data-start="28.8" data-duration="1.7" data-track-index="28"
style="position:absolute;top:59%;left:0;right:0;text-align:center;
font:400 28px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.48);">${website}</div>
<!-- Music: lo-fi upbeat, generated by ElevenLabs -->
<!-- Volume 0.10 during energy/CTA, see audio-design.md -->
<audio data-start="0" data-duration="${dur}" data-track-index="50"
data-volume="0.10" src="./assets/music.mp3"></audio>
${GRAIN}
<!-- Persistent brand bar (not a clip β always visible) -->
<div style="position:absolute;bottom:0;left:0;right:0;padding:20px 56px 34px;
background:linear-gradient(transparent,rgba(0,0,0,0.65));z-index:97;">
<span style="font:700 26px/1 'Space Grotesk',sans-serif;color:#fff;">${trainerName}</span>
<span style="font:400 17px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.58);margin-left:8px;">\u00b7 ${tagline}</span>
</div>
<script>
const tl = gsap.timeline({ paused: true });
// S1 HOOK β slam from above (expo.out: kinetic, fast)
tl.from("#h1", { y: -100, autoAlpha: 0, duration: 0.22, ease: "expo.out" }, 0.2);
tl.from("#h2", { y: -100, autoAlpha: 0, duration: 0.22, ease: "expo.out" }, 0.35);
tl.from("#h3", { y: 40, autoAlpha: 0, duration: 0.35, ease: "power3.out" }, 0.52);
// TRANSITION 1β2: Zoom-through (dramatic opener)
// Rule: NO exit animation on s1 content β transition handles the handoff
const T1 = 3.65;
tl.to("#s1", { autoAlpha: 0, scale: 1.32, duration: 0.35, ease: "power4.inOut" }, T1);
tl.fromTo("#s2", { autoAlpha: 0, scale: 0.80 }, { autoAlpha: 1, scale: 1, duration: 0.35, ease: "power4.inOut" }, T1);
// S2 PROOF β scale pop (back.out: bouncy confidence)
tl.from("#st", { scale: 0.55, autoAlpha: 0, duration: 0.5, ease: "back.out(2.5)" }, 3.95);
tl.from("#lt", { x: -400, autoAlpha: 0, duration: 0.4, ease: "power3.out" }, 4.2);
// NO exit tween on #lt β transition covers it
// TRANSITION 2β3: Push-slide left (editorial)
const T2 = 7.65;
tl.to("#s2", { autoAlpha: 0, xPercent: -18, duration: 0.32, ease: "power2.inOut" }, T2);
tl.fromTo("#s3", { autoAlpha: 0, xPercent: 18 }, { autoAlpha: 1, xPercent: 0, duration: 0.32, ease: "power2.inOut" }, T2);
// S3 TESTIMONIAL β opacity only (sine.inOut: calm, intentional contrast)
tl.from("#q", { autoAlpha: 0, duration: 0.55, ease: "sine.inOut" }, 8.2);
// NO exit tween on #q β transition handles
// TRANSITION 3β4a: Blur crossfade (drift into energy)
const T3 = 15.85;
tl.to("#s3", { autoAlpha: 0, filter: "blur(14px)", scale: 1.04, duration: 0.45, ease: "sine.inOut" }, T3);
tl.from("#s4a", { autoAlpha: 0, duration: 0.22 }, T3 + 0.23);
// S4a ENERGY β slide from left (power4.out: decisive)
tl.from("#e1", { x: -120, autoAlpha: 0, duration: 0.25, ease: "power4.out" }, 16.05);
// S4b: Hard cut (disruption intentional) + slide from right
tl.from("#s4b", { autoAlpha: 0, duration: 0.18 }, 18.2);
tl.from("#e2", { x: 120, autoAlpha: 0, duration: 0.25, ease: "power4.out" }, 18.4);
// S4c: Hard cut + scale pop (energy green color)
tl.from("#s4c", { autoAlpha: 0, duration: 0.15 }, 20.7);
tl.from("#e3", { scale: 0.60, autoAlpha: 0, duration: 0.3, ease: "back.out(2.8)" }, 20.9);
// TRANSITION 4β5: Hard cut into CTA
tl.from("#s5", { autoAlpha: 0, duration: 0.25 }, 22.85);
tl.from("#c1", { y: 80, autoAlpha: 0, duration: 0.5, ease: "power3.out" }, 23.1);
tl.from("#c2", { y: 50, autoAlpha: 0, duration: 0.45, ease: "power3.out" }, 25);
// TRANSITION 5β6: Brand end card
const T5 = 28.2;
tl.to("#s5", { autoAlpha: 0, duration: 0.3 }, T5);
tl.from("#s6", { autoAlpha: 0, duration: 0.3 }, T5);
// Final scene IS allowed exit animations β but we fade in instead
tl.from("#en", { y: 36, autoAlpha: 0, duration: 0.45, ease: "power3.out" }, 28.4);
tl.from("#et", { y: 28, autoAlpha: 0, duration: 0.40, ease: "power3.out" }, 28.6);
tl.from("#ew", { y: 20, autoAlpha: 0, duration: 0.35, ease: "power3.out" }, 28.8);
// CRITICAL: extend timeline to prevent cutoff + black frames
// Use tl.set() not tl.to() β tl.to({},{duration:X}) can conflict
tl.set({}, {}, ${dur});
// CRITICAL: key MUST match data-composition-id exactly
window.__timelines = window.__timelines || {};
window.__timelines["${compId}"] = tl;
</script>
</div></body></html>`;
},
};
const teaser = {
meta: { name: 'Event Teaser', icon: '\u26A1', description: '30s fast-cut teaser. 9:16. ElevenLabs energetic.', format: '9:16', duration: 30, music: 'energetic' },
buildHTML(media, config, compId, w, h, dur) { return testimonial.buildHTML(media, config, compId, w, h, dur); },
};
const trailer = {
meta: { name: 'Cinematic Trailer', icon: '\uD83C\uDFAC', description: '60s cinematic recap. 9:16.', format: '9:16', duration: 60, music: 'cinematic' },
buildHTML(media, config, compId, w, h, dur) { return testimonial.buildHTML(media, config, compId, w, h, dur); },
};
const community = {
meta: { name: 'Community Story', icon: '\uD83E\uDD1D', description: '30s warm community story. 9:16.', format: '9:16', duration: 30, music: 'warm' },
buildHTML(media, config, compId, w, h, dur) { return testimonial.buildHTML(media, config, compId, w, h, dur); },
};
export const WORKFLOWS = { testimonial, teaser, trailer, community };
|