Spaces:
Configuration error
Configuration error
AIGoose commited on
Commit ·
0624752
1
Parent(s): 96ae60b
feat: add HyperFrames testimonial pipeline — composition, workflow, patcher, README
Browse files- .github/workflows/render-testimonial.yml +127 -0
- README.md +36 -74
- compositions/testimonial.html +149 -0
- scripts/patch_composition.py +30 -0
.github/workflows/render-testimonial.yml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Render Testimonial Video
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
gdrive_file_id:
|
| 7 |
+
description: 'Google Drive file ID of testimonial MOV'
|
| 8 |
+
required: true
|
| 9 |
+
default: '1f1qlgyLJSCQgpjrWflaqldsNE2fZ-_1X'
|
| 10 |
+
participant_name:
|
| 11 |
+
description: 'Participant name (lower-third)'
|
| 12 |
+
required: false
|
| 13 |
+
default: 'Peserta Training'
|
| 14 |
+
participant_role:
|
| 15 |
+
description: 'Participant role / org'
|
| 16 |
+
required: false
|
| 17 |
+
default: 'AI Transformation Series - Batch 2026'
|
| 18 |
+
pull_quote:
|
| 19 |
+
description: 'Pull quote text (Indonesian)'
|
| 20 |
+
required: false
|
| 21 |
+
default: 'Setelah training ini, tim kami langsung bisa praktek dan hasilnya nyata.'
|
| 22 |
+
|
| 23 |
+
jobs:
|
| 24 |
+
render:
|
| 25 |
+
runs-on: ubuntu-latest
|
| 26 |
+
timeout-minutes: 30
|
| 27 |
+
|
| 28 |
+
steps:
|
| 29 |
+
- name: Checkout repo
|
| 30 |
+
uses: actions/checkout@v4
|
| 31 |
+
|
| 32 |
+
- name: Install system dependencies
|
| 33 |
+
run: |
|
| 34 |
+
sudo apt-get update -qq
|
| 35 |
+
sudo apt-get install -y ffmpeg python3-pip
|
| 36 |
+
pip3 install gdown huggingface_hub --quiet
|
| 37 |
+
|
| 38 |
+
- name: Setup Node.js 22
|
| 39 |
+
uses: actions/setup-node@v4
|
| 40 |
+
with:
|
| 41 |
+
node-version: '22'
|
| 42 |
+
|
| 43 |
+
- name: Install HyperFrames CLI
|
| 44 |
+
run: npm install -g hyperframes
|
| 45 |
+
|
| 46 |
+
- name: Install Chromium
|
| 47 |
+
run: sudo apt-get install -y chromium-browser || true
|
| 48 |
+
|
| 49 |
+
- name: Download testimonial MOV from Google Drive
|
| 50 |
+
run: |
|
| 51 |
+
mkdir -p assets
|
| 52 |
+
python3 - <<'EOF'
|
| 53 |
+
import gdown, os, sys
|
| 54 |
+
fid = "${{ github.event.inputs.gdrive_file_id }}"
|
| 55 |
+
out = "assets/testimoni_raw.MOV"
|
| 56 |
+
gdown.download(f"https://drive.google.com/uc?id={fid}", out, quiet=False)
|
| 57 |
+
if not os.path.exists(out):
|
| 58 |
+
sys.exit(1)
|
| 59 |
+
print(f"Downloaded {os.path.getsize(out)/1024/1024:.1f} MB")
|
| 60 |
+
EOF
|
| 61 |
+
|
| 62 |
+
- name: Convert MOV to MP4
|
| 63 |
+
run: |
|
| 64 |
+
ffmpeg -y -i assets/testimoni_raw.MOV \
|
| 65 |
+
-c:v libx264 -preset fast -crf 22 \
|
| 66 |
+
-c:a aac -b:a 192k -movflags +faststart \
|
| 67 |
+
assets/testimoni.mp4
|
| 68 |
+
echo "Size: $(du -sh assets/testimoni.mp4)"
|
| 69 |
+
|
| 70 |
+
- name: Get video duration
|
| 71 |
+
run: |
|
| 72 |
+
DUR=$(ffprobe -v quiet -show_entries format=duration \
|
| 73 |
+
-of default=noprint_wrappers=1:nokey=1 assets/testimoni.mp4 | cut -d. -f1)
|
| 74 |
+
echo "VIDEO_DURATION=$DUR" >> $GITHUB_ENV
|
| 75 |
+
echo "Duration: ${DUR}s"
|
| 76 |
+
|
| 77 |
+
- name: Patch composition with participant data
|
| 78 |
+
run: |
|
| 79 |
+
python3 scripts/patch_composition.py \
|
| 80 |
+
--input compositions/testimonial.html \
|
| 81 |
+
--output compositions/testimonial_render.html \
|
| 82 |
+
--name "${{ github.event.inputs.participant_name }}" \
|
| 83 |
+
--role "${{ github.event.inputs.participant_role }}" \
|
| 84 |
+
--quote "${{ github.event.inputs.pull_quote }}"
|
| 85 |
+
sed -i "s/data-duration=\"90\"/data-duration=\"${VIDEO_DURATION}\"/" \
|
| 86 |
+
compositions/testimonial_render.html
|
| 87 |
+
|
| 88 |
+
- name: Lint composition
|
| 89 |
+
run: npx hyperframes lint compositions/testimonial_render.html || true
|
| 90 |
+
|
| 91 |
+
- name: Render to MP4
|
| 92 |
+
run: |
|
| 93 |
+
mkdir -p output
|
| 94 |
+
TS=$(date +%Y%m%d-%H%M)
|
| 95 |
+
OUT="output/dee-testimonial-${TS}.mp4"
|
| 96 |
+
echo "OUTPUT_FILE=$OUT" >> $GITHUB_ENV
|
| 97 |
+
npx hyperframes render compositions/testimonial_render.html \
|
| 98 |
+
--output "$OUT" --width 1080 --height 1920 --fps 30
|
| 99 |
+
echo "Rendered: $OUT ($(du -sh $OUT | cut -f1))"
|
| 100 |
+
|
| 101 |
+
- name: Upload to Hugging Face dataset
|
| 102 |
+
env:
|
| 103 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 104 |
+
run: |
|
| 105 |
+
python3 - <<'EOF'
|
| 106 |
+
import os
|
| 107 |
+
from huggingface_hub import HfApi
|
| 108 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 109 |
+
out = os.environ["OUTPUT_FILE"]
|
| 110 |
+
fname = os.path.basename(out)
|
| 111 |
+
api.upload_file(
|
| 112 |
+
path_or_fileobj=out,
|
| 113 |
+
path_in_repo=f"renders/{fname}",
|
| 114 |
+
repo_id="AIgoose/video-renders",
|
| 115 |
+
repo_type="dataset",
|
| 116 |
+
)
|
| 117 |
+
url = f"https://huggingface.co/datasets/AIgoose/video-renders/resolve/main/renders/{fname}"
|
| 118 |
+
print(f"Video ready: {url}")
|
| 119 |
+
print(f"::notice title=Video Ready::{url}")
|
| 120 |
+
EOF
|
| 121 |
+
|
| 122 |
+
- name: Upload artifact (backup)
|
| 123 |
+
uses: actions/upload-artifact@v4
|
| 124 |
+
with:
|
| 125 |
+
name: testimonial-mp4
|
| 126 |
+
path: output/*.mp4
|
| 127 |
+
retention-days: 7
|
README.md
CHANGED
|
@@ -1,96 +1,58 @@
|
|
| 1 |
-
--
|
| 2 |
-
title: Hyperframes Video Studio
|
| 3 |
-
emoji: 🎬
|
| 4 |
-
colorFrom: purple
|
| 5 |
-
colorTo: pink
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
pinned: true
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
# 🎬 Hyperframes Video Studio
|
| 12 |
-
|
| 13 |
-
**Cloud video production studio powered by HyperFrames — Testimonial · Teaser · Trailer generator**
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
|
| 19 |
---
|
| 20 |
|
| 21 |
-
##
|
| 22 |
-
|
| 23 |
-
| Workflow | Duration | Format | Best For |
|
| 24 |
-
|---|---|---|---|
|
| 25 |
-
| 🏢 Corporate Testimonial | 75s | 9:16 | Corporate AI training social proof |
|
| 26 |
-
| ⚡ Event Teaser | 30s | 9:16 | Upcoming event announcement |
|
| 27 |
-
| 🎬 Cinematic Trailer | 60s | 9:16/16:9 | Program recap, brand video |
|
| 28 |
-
| 🤝 Community Story | 60s | 9:16 | Community/church/NGO sessions |
|
| 29 |
-
|
| 30 |
-
---
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
|
| 41 |
---
|
| 42 |
|
| 43 |
-
##
|
| 44 |
|
| 45 |
-
|
| 46 |
-
- **[FFmpeg](https://github.com/FFmpeg/FFmpeg)** — video encoding
|
| 47 |
-
- **Chromium** — headless browser for frame rendering
|
| 48 |
-
- **Node.js + Express** — REST API server
|
| 49 |
-
- **WebSocket** — real-time render progress
|
| 50 |
-
- **Pixabay Music** — 6 royalty-free tracks (free commercial use)
|
| 51 |
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
| [heygen-com/hyperframes](https://github.com/heygen-com/hyperframes) | Core HTML→MP4 engine |
|
| 57 |
-
| [heygen-com/hyperframes-launches](https://github.com/heygen-com/hyperframes-launches) | Production composition examples |
|
| 58 |
-
| [calesthio/OpenMontage](https://github.com/calesthio/OpenMontage) | Agentic video pipeline patterns |
|
| 59 |
-
| [mifi/editly](https://github.com/mifi/editly) | Declarative clip assembly |
|
| 60 |
-
| [wzk1015/video-bgm-generation](https://github.com/wzk1015/video-bgm-generation) | AI video→music matching |
|
| 61 |
|
| 62 |
---
|
| 63 |
|
| 64 |
-
##
|
| 65 |
-
|
| 66 |
-
| Track | Mood | BPM | Auto-assigned to |
|
| 67 |
-
|---|---|---|---|
|
| 68 |
-
| corporate-motivation.mp3 | Corporate | 120 | Testimonial |
|
| 69 |
-
| inspiring-cinematic.mp3 | Cinematic | 90 | Trailer |
|
| 70 |
-
| upbeat-community.mp3 | Upbeat | 128 | Community |
|
| 71 |
-
| warm-documentary.mp3 | Warm | 75 | Community Story |
|
| 72 |
-
| tech-corporate.mp3 | Tech | 110 | Teaser |
|
| 73 |
-
| action-sport.mp3 | Energetic | 145 | Event Teaser |
|
| 74 |
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
---
|
| 78 |
|
| 79 |
-
##
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
WebSocket: `wss://aigoose-hyperframes-video-studio.hf.space?job=JOB_ID`
|
| 91 |
|
| 92 |
---
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
<!-- rebuild-trigger: 1781584816 -->
|
|
|
|
| 1 |
+
# dee-video-studio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
**HyperFrames x GitHub Actions x Hugging Face**
|
| 4 |
+
Testimonial video pipeline for Dee Ferdinand AI / MAIN AI.
|
| 5 |
+
No HeyGen subscription. No paid API. 100% open-source.
|
| 6 |
|
| 7 |
---
|
| 8 |
|
| 9 |
+
## Pipeline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
```
|
| 12 |
+
Google Drive MOV
|
| 13 |
+
→ gdown download (GitHub Actions)
|
| 14 |
+
→ ffmpeg MOV→MP4
|
| 15 |
+
→ patch_composition.py (inject name/role/quote)
|
| 16 |
+
→ npx hyperframes render (headless Chrome + ffmpeg)
|
| 17 |
+
→ HuggingFace dataset: AIgoose/video-renders
|
| 18 |
+
```
|
| 19 |
|
| 20 |
---
|
| 21 |
|
| 22 |
+
## Run a render
|
| 23 |
|
| 24 |
+
**Actions → Render Testimonial Video → Run workflow**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
| Input | What to fill |
|
| 27 |
+
|-------|-------------|
|
| 28 |
+
| `gdrive_file_id` | The ID from the Drive share URL |
|
| 29 |
+
| `participant_name` | For the lower-third (e.g. Budi Santoso) |
|
| 30 |
+
| `participant_role` | Role line (e.g. Head of Digital · PT Maju) |
|
| 31 |
+
| `pull_quote` | Strongest spoken line in Indonesian |
|
| 32 |
|
| 33 |
+
Output lands at:
|
| 34 |
+
`https://huggingface.co/datasets/AIgoose/video-renders/resolve/main/renders/<filename>.mp4`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
---
|
| 37 |
|
| 38 |
+
## One-time setup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
1. **GitHub Secret**: Settings → Secrets → `HF_TOKEN` (your HF write token from hf.co/settings/tokens)
|
| 41 |
+
2. **Drive permission**: Set the MOV to "Anyone with the link" → Viewer
|
| 42 |
|
| 43 |
---
|
| 44 |
|
| 45 |
+
## Composition overlay map
|
| 46 |
|
| 47 |
+
| Overlay | Timing | Description |
|
| 48 |
+
|---------|--------|-------------|
|
| 49 |
+
| Name card | 0–4s | Program name + brand |
|
| 50 |
+
| Lower-third | 8–18s | Participant name + role |
|
| 51 |
+
| Pull quote | 30–48s | Strongest spoken line |
|
| 52 |
+
| Stat callout | 52–64s | 1,000+ professionals trained |
|
| 53 |
+
| Feature chips | 66–78s | 80% hands-on, langsung praktek, real output |
|
| 54 |
+
| Brand outro | 83–90s | Dee Ferdinand AI CTA |
|
|
|
|
|
|
|
| 55 |
|
| 56 |
---
|
| 57 |
|
| 58 |
+
HyperFrames: https://github.com/heygen-com/hyperframes
|
|
|
|
|
|
compositions/testimonial.html
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="id">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<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">
|
| 6 |
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
| 7 |
+
<style>
|
| 8 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
body { background: #0d0d1f; overflow: hidden; }
|
| 10 |
+
#root { position: relative; width: 1080px; height: 1920px; overflow: hidden; background: #0d0d1f; }
|
| 11 |
+
.clip { position: absolute; }
|
| 12 |
+
.sc { inset: 0; }
|
| 13 |
+
</style>
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<div id="root"
|
| 17 |
+
data-composition-id="dee-testimonial"
|
| 18 |
+
data-start="0"
|
| 19 |
+
data-duration="90"
|
| 20 |
+
data-width="1080"
|
| 21 |
+
data-height="1920">
|
| 22 |
+
|
| 23 |
+
<!-- A-ROLL: original footage (muted video + separate audio element) -->
|
| 24 |
+
<video id="aroll-video" class="clip sc" src="../assets/testimoni.mp4"
|
| 25 |
+
data-start="0" data-duration="90" data-track-index="0"
|
| 26 |
+
muted playsinline style="object-fit:cover;width:100%;height:100%;"></video>
|
| 27 |
+
|
| 28 |
+
<audio id="aroll-audio" src="../assets/testimoni.mp4"
|
| 29 |
+
data-start="0" data-duration="90" data-track-index="10" data-volume="1.0"></audio>
|
| 30 |
+
|
| 31 |
+
<!-- Bottom vignette (persistent) -->
|
| 32 |
+
<div style="position:absolute;inset:0;pointer-events:none;
|
| 33 |
+
background:linear-gradient(to top,rgba(0,0,0,0.75) 0%,transparent 45%);z-index:5;"></div>
|
| 34 |
+
|
| 35 |
+
<!-- SCENE 1: NAME CARD 0-4s -->
|
| 36 |
+
<div id="s1-bg" class="clip sc" data-start="0" data-duration="4" data-track-index="1"
|
| 37 |
+
style="background:linear-gradient(180deg,rgba(13,13,31,0.72) 0%,transparent 40%);z-index:6;"></div>
|
| 38 |
+
<div id="s1-rule" class="clip" data-start="0.3" data-duration="3.5" data-track-index="2"
|
| 39 |
+
style="position:absolute;top:80px;left:56px;width:56px;height:3px;background:#7C6FE0;z-index:7;"></div>
|
| 40 |
+
<div id="s1-name" class="clip" data-start="0.4" data-duration="3.4" data-track-index="2"
|
| 41 |
+
style="position:absolute;top:96px;left:56px;right:56px;
|
| 42 |
+
font:700 72px/1.1 'Space Grotesk',sans-serif;color:#fff;letter-spacing:-1px;z-index:7;">
|
| 43 |
+
AI Transformation<br>Series</div>
|
| 44 |
+
<div id="s1-tag" class="clip" data-start="0.6" data-duration="3.2" data-track-index="2"
|
| 45 |
+
style="position:absolute;top:238px;left:56px;
|
| 46 |
+
font:500 32px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.65);z-index:7;">
|
| 47 |
+
Dee Ferdinand AI · MAIN AI</div>
|
| 48 |
+
|
| 49 |
+
<!-- SCENE 2: LOWER-THIRD BAR 8-18s -->
|
| 50 |
+
<div id="s2-card" class="clip" data-start="8" data-duration="10" data-track-index="3"
|
| 51 |
+
style="position:absolute;bottom:180px;left:0;background:rgba(10,10,25,0.88);
|
| 52 |
+
border-left:6px solid #7C6FE0;border-radius:0 16px 16px 0;padding:20px 36px 20px 50px;z-index:8;">
|
| 53 |
+
<div style="font:700 40px/1.2 'Plus Jakarta Sans',sans-serif;color:#fff;">Peserta Training</div>
|
| 54 |
+
<div style="font:400 24px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.60);margin-top:6px;">
|
| 55 |
+
AI Transformation Series · Batch 2026</div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<!-- SCENE 3: PULL QUOTE 30-48s -->
|
| 59 |
+
<div id="s3-quote-bg" class="clip" data-start="30" data-duration="18" data-track-index="4"
|
| 60 |
+
style="position:absolute;bottom:220px;left:40px;right:40px;background:rgba(10,10,25,0.90);
|
| 61 |
+
border-radius:20px;padding:40px 44px;z-index:9;">
|
| 62 |
+
<div style="font:600 56px/1.3 'Plus Jakarta Sans',sans-serif;color:#fff;font-style:italic;">
|
| 63 |
+
“Setelah training ini, tim kami langsung bisa praktek dan hasilnya nyata.”</div>
|
| 64 |
+
<div style="margin-top:24px;font:400 28px/1 'Plus Jakarta Sans',sans-serif;color:#9EF0C8;">
|
| 65 |
+
— Peserta · AI Transformation Series</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<!-- SCENE 4: STAT CALLOUT 52-64s -->
|
| 69 |
+
<div id="s4-stat" class="clip" data-start="52" data-duration="12" data-track-index="5"
|
| 70 |
+
style="position:absolute;top:80px;right:40px;background:rgba(124,111,224,0.18);
|
| 71 |
+
border:1.5px solid rgba(124,111,224,0.5);border-radius:16px;padding:28px 36px;
|
| 72 |
+
text-align:center;z-index:8;">
|
| 73 |
+
<div style="font:900 88px/1 'Space Grotesk',sans-serif;color:#7C6FE0;">1,000+</div>
|
| 74 |
+
<div style="font:500 28px/1.3 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.75);margin-top:10px;">
|
| 75 |
+
profesional terlatih<br>sejak 2023</div>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<!-- SCENE 5: FEATURE CHIPS 66-78s -->
|
| 79 |
+
<div id="s5-chip1" class="clip" data-start="66" data-duration="12" data-track-index="6"
|
| 80 |
+
style="position:absolute;bottom:380px;left:40px;background:#7C6FE0;border-radius:40px;padding:16px 32px;z-index:8;">
|
| 81 |
+
<span style="font:700 30px/1 'Plus Jakarta Sans',sans-serif;color:#fff;">80% hands-on</span></div>
|
| 82 |
+
<div id="s5-chip2" class="clip" data-start="67" data-duration="11" data-track-index="6"
|
| 83 |
+
style="position:absolute;bottom:310px;left:40px;background:rgba(158,240,200,0.15);
|
| 84 |
+
border:1.5px solid #9EF0C8;border-radius:40px;padding:16px 32px;z-index:8;">
|
| 85 |
+
<span style="font:700 30px/1 'Plus Jakarta Sans',sans-serif;color:#9EF0C8;">Langsung praktek</span></div>
|
| 86 |
+
<div id="s5-chip3" class="clip" data-start="68" data-duration="10" data-track-index="6"
|
| 87 |
+
style="position:absolute;bottom:240px;left:40px;background:rgba(224,111,154,0.15);
|
| 88 |
+
border:1.5px solid #E06F9A;border-radius:40px;padding:16px 32px;z-index:8;">
|
| 89 |
+
<span style="font:700 30px/1 'Plus Jakarta Sans',sans-serif;color:#E06F9A;">Real output ✓</span></div>
|
| 90 |
+
|
| 91 |
+
<!-- SCENE 6: BRAND OUTRO 83-90s -->
|
| 92 |
+
<div id="s6-overlay" class="clip sc" data-start="83" data-duration="7" data-track-index="7"
|
| 93 |
+
style="background:linear-gradient(180deg,rgba(13,13,31,0) 0%,rgba(13,13,31,0.96) 60%);z-index:10;"></div>
|
| 94 |
+
<div id="s6-name" class="clip" data-start="83.5" data-duration="6.5" data-track-index="7"
|
| 95 |
+
style="position:absolute;bottom:320px;left:0;right:0;text-align:center;
|
| 96 |
+
font:800 72px/1.1 'Space Grotesk',sans-serif;color:#fff;z-index:11;">Dee Ferdinand</div>
|
| 97 |
+
<div id="s6-title" class="clip" data-start="84" data-duration="6" data-track-index="7"
|
| 98 |
+
style="position:absolute;bottom:260px;left:0;right:0;text-align:center;
|
| 99 |
+
font:500 36px/1 'Plus Jakarta Sans',sans-serif;color:#9990ee;z-index:11;">
|
| 100 |
+
AI Corporate Trainer · MAIN AI</div>
|
| 101 |
+
<div id="s6-handle" class="clip" data-start="84.5" data-duration="5.5" data-track-index="7"
|
| 102 |
+
style="position:absolute;bottom:210px;left:0;right:0;text-align:center;
|
| 103 |
+
font:400 28px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.48);z-index:11;">
|
| 104 |
+
@deeferdinand · deeferdinand.com</div>
|
| 105 |
+
<div id="s6-cta" class="clip" data-start="85" data-duration="5" data-track-index="7"
|
| 106 |
+
style="position:absolute;bottom:140px;left:0;right:0;text-align:center;
|
| 107 |
+
font:600 32px/1 'Plus Jakarta Sans',sans-serif;font-style:italic;color:#7C6FE0;z-index:11;">
|
| 108 |
+
Transform your team →</div>
|
| 109 |
+
|
| 110 |
+
<!-- Persistent brand bar -->
|
| 111 |
+
<div style="position:absolute;bottom:0;left:0;right:0;padding:20px 56px 40px;
|
| 112 |
+
background:linear-gradient(transparent,rgba(0,0,0,0.60));z-index:6;">
|
| 113 |
+
<span style="font:700 26px/1 'Space Grotesk',sans-serif;color:#fff;">Dee Ferdinand</span>
|
| 114 |
+
<span style="font:400 18px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.55);margin-left:8px;">· AI Corporate Trainer</span>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<!-- Grain overlay -->
|
| 118 |
+
<div style="position:absolute;inset:0;pointer-events:none;z-index:99;overflow:hidden;">
|
| 119 |
+
<div style="position:absolute;top:-50%;left:-50%;width:200%;height:200%;opacity:0.055;
|
| 120 |
+
background:url('data:image/svg+xml,%3Csvg viewBox=%270 0 256 256%27 xmlns=%27http://www.w3.org/2000/svg%27%3E%3Cfilter id=%27n%27%3E%3CfeTurbulence type=%27fractalNoise%27 baseFrequency=%270.65%27 numOctaves=%273%27 stitchTiles=%27stitch%27/%3E%3C/filter%3E%3Crect width=%27100%25%27 height=%27100%25%27 filter=%27url(%23n)%27/%3E%3C/svg%3E');"></div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<script>
|
| 125 |
+
const tl = gsap.timeline({ paused: true });
|
| 126 |
+
tl.from("#s1-rule", { scaleX:0, transformOrigin:"left", duration:0.25, ease:"expo.out" }, 0.3);
|
| 127 |
+
tl.from("#s1-name", { y:-80, autoAlpha:0, duration:0.28, ease:"expo.out" }, 0.4);
|
| 128 |
+
tl.from("#s1-tag", { y:40, autoAlpha:0, duration:0.35, ease:"power3.out" }, 0.6);
|
| 129 |
+
tl.from("#s2-card", { x:-500, autoAlpha:0, duration:0.40, ease:"power3.out" }, 8);
|
| 130 |
+
tl.from("#s3-quote-bg", { y:60, autoAlpha:0, scale:0.96, duration:0.55, ease:"back.out(1.8)" }, 30);
|
| 131 |
+
tl.to("#s3-quote-bg", { autoAlpha:0, duration:0.4, ease:"sine.inOut" }, 47);
|
| 132 |
+
tl.from("#s4-stat", { y:-60, autoAlpha:0, scale:0.85, duration:0.45, ease:"back.out(2.2)" }, 52);
|
| 133 |
+
tl.to("#s4-stat", { autoAlpha:0, duration:0.3 }, 63.5);
|
| 134 |
+
tl.from("#s5-chip1", { x:-120, autoAlpha:0, duration:0.30, ease:"power4.out" }, 66);
|
| 135 |
+
tl.from("#s5-chip2", { x:-120, autoAlpha:0, duration:0.30, ease:"power4.out" }, 66.2);
|
| 136 |
+
tl.from("#s5-chip3", { x:-120, autoAlpha:0, duration:0.30, ease:"power4.out" }, 66.4);
|
| 137 |
+
tl.to(["#s5-chip1","#s5-chip2","#s5-chip3"], { autoAlpha:0, duration:0.3 }, 77.5);
|
| 138 |
+
tl.from("#s6-overlay", { autoAlpha:0, duration:0.6, ease:"sine.inOut" }, 83);
|
| 139 |
+
tl.from("#s6-name", { y:40, autoAlpha:0, duration:0.45, ease:"power3.out" }, 83.5);
|
| 140 |
+
tl.from("#s6-title", { y:30, autoAlpha:0, duration:0.40, ease:"power3.out" }, 84);
|
| 141 |
+
tl.from("#s6-handle", { y:20, autoAlpha:0, duration:0.35, ease:"power3.out" }, 84.5);
|
| 142 |
+
tl.from("#s6-cta", { y:20, autoAlpha:0, duration:0.35, ease:"power3.out" }, 85);
|
| 143 |
+
tl.set({}, {}, 90);
|
| 144 |
+
window.__timelines = window.__timelines || {};
|
| 145 |
+
window.__timelines["dee-testimonial"] = tl;
|
| 146 |
+
</script>
|
| 147 |
+
</div>
|
| 148 |
+
</body>
|
| 149 |
+
</html>
|
scripts/patch_composition.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""patch_composition.py — inject participant name/role/quote before render"""
|
| 3 |
+
import argparse, re
|
| 4 |
+
|
| 5 |
+
def patch(html, name, role, quote):
|
| 6 |
+
html = re.sub(r'Peserta Training', name, html)
|
| 7 |
+
html = re.sub(r'AI Transformation Series · Batch 2026', role, html)
|
| 8 |
+
if quote:
|
| 9 |
+
html = re.sub(
|
| 10 |
+
r'(“)(.*?)(”)',
|
| 11 |
+
lambda m: f'“{quote}”',
|
| 12 |
+
html, flags=re.DOTALL
|
| 13 |
+
)
|
| 14 |
+
html = re.sub(r'Peserta · AI Transformation Series', f'{name} · AI Transformation Series', html)
|
| 15 |
+
return html
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
p = argparse.ArgumentParser()
|
| 19 |
+
p.add_argument("--input", required=True)
|
| 20 |
+
p.add_argument("--output", required=True)
|
| 21 |
+
p.add_argument("--name", default="Peserta Training")
|
| 22 |
+
p.add_argument("--role", default="AI Transformation Series")
|
| 23 |
+
p.add_argument("--quote", default="")
|
| 24 |
+
args = p.parse_args()
|
| 25 |
+
with open(args.input, encoding="utf-8") as f: html = f.read()
|
| 26 |
+
with open(args.output, "w", encoding="utf-8") as f: f.write(patch(html, args.name, args.role, args.quote))
|
| 27 |
+
print(f"Patched: {args.output}")
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
main()
|