Spaces:
Configuration error
Configuration error
AIGoose commited on
Commit ·
e7002b8
1
Parent(s): efd8a15
fix(v3): burn captions in ffmpeg, denoise voice, jump-cut pauses, title card before footage, fix headline overlap
Browse files- .github/workflows/render-testimonial.yml +97 -57
- compositions/testimonial.html +123 -180
- scripts/patch_composition.py +34 -39
.github/workflows/render-testimonial.yml
CHANGED
|
@@ -8,7 +8,7 @@ on:
|
|
| 8 |
required: true
|
| 9 |
default: '1f1qlgyLJSCQgpjrWflaqldsNE2fZ-_1X'
|
| 10 |
participant_name:
|
| 11 |
-
description: 'Participant name
|
| 12 |
required: false
|
| 13 |
default: 'Peserta Training'
|
| 14 |
participant_role:
|
|
@@ -16,7 +16,7 @@ on:
|
|
| 16 |
required: false
|
| 17 |
default: 'AI Transformation Series 2026'
|
| 18 |
pull_quote:
|
| 19 |
-
description: 'Pull quote (
|
| 20 |
required: false
|
| 21 |
default: 'Training ini langsung bisa dipraktek dan hasilnya nyata'
|
| 22 |
|
|
@@ -32,8 +32,9 @@ jobs:
|
|
| 32 |
- name: Install system deps
|
| 33 |
run: |
|
| 34 |
sudo apt-get update -qq
|
| 35 |
-
sudo apt-get install -y ffmpeg python3-pip libass-dev
|
| 36 |
pip3 install gdown huggingface_hub requests --quiet
|
|
|
|
| 37 |
|
| 38 |
- name: Setup Node.js 22
|
| 39 |
uses: actions/setup-node@v4
|
|
@@ -47,7 +48,7 @@ jobs:
|
|
| 47 |
SHELL_BIN=$(npx --yes @puppeteer/browsers install chrome-headless-shell@stable 2>&1 | tail -1 | awk '{print $NF}')
|
| 48 |
[ ! -x "$SHELL_BIN" ] && SHELL_BIN=$(find ~/.cache/puppeteer -name "chrome-headless-shell" -executable 2>/dev/null | head -1)
|
| 49 |
echo "HYPERFRAMES_BROWSER_PATH=$SHELL_BIN" >> $GITHUB_ENV
|
| 50 |
-
echo "Browser: $SHELL_BIN"
|
| 51 |
|
| 52 |
- name: Download testimonial MOV from Google Drive
|
| 53 |
run: |
|
|
@@ -62,7 +63,7 @@ jobs:
|
|
| 62 |
print(f"Downloaded {os.path.getsize(out)/1024/1024:.1f} MB")
|
| 63 |
PYEOF
|
| 64 |
|
| 65 |
-
- name: Convert
|
| 66 |
run: |
|
| 67 |
ffmpeg -y -i assets/raw.MOV \
|
| 68 |
-vf "scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280" \
|
|
@@ -71,10 +72,9 @@ jobs:
|
|
| 71 |
assets/source.mp4
|
| 72 |
echo "Source: $(du -sh assets/source.mp4)"
|
| 73 |
|
| 74 |
-
- name:
|
| 75 |
run: |
|
| 76 |
-
ffmpeg -y \
|
| 77 |
-
-i assets/source.mp4 \
|
| 78 |
-filter_complex "\
|
| 79 |
[0:v]trim=0:71,setpts=PTS-STARTPTS[v1];\
|
| 80 |
[0:a]atrim=0:71,asetpts=PTS-STARTPTS[a1];\
|
|
@@ -84,57 +84,102 @@ jobs:
|
|
| 84 |
-map "[vout]" -map "[aout]" \
|
| 85 |
-c:v libx264 -preset fast -crf 22 \
|
| 86 |
-c:a aac -b:a 128k -movflags +faststart \
|
| 87 |
-
assets/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
DUR=$(ffprobe -v quiet -show_entries format=duration \
|
| 89 |
-
-of default=noprint_wrappers=1:nokey=1 assets/
|
| 90 |
-
echo "
|
| 91 |
-
echo "
|
| 92 |
|
| 93 |
-
- name: Transcribe
|
| 94 |
env:
|
| 95 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 96 |
run: |
|
| 97 |
mkdir -p project/assets
|
| 98 |
python3 scripts/gen_captions.py \
|
| 99 |
-
--input assets/
|
| 100 |
-
--srt-out
|
| 101 |
-
head -20
|
| 102 |
|
| 103 |
-
- name:
|
| 104 |
run: |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
- name: Build project folder
|
| 126 |
run: |
|
| 127 |
-
cp assets/
|
|
|
|
|
|
|
| 128 |
python3 scripts/patch_composition.py \
|
| 129 |
-
--input
|
| 130 |
-
--output
|
| 131 |
-
--name
|
| 132 |
-
--role
|
| 133 |
-
--quote
|
| 134 |
-
--
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
env:
|
| 139 |
HYPERFRAMES_BROWSER_PATH: ${{ env.HYPERFRAMES_BROWSER_PATH }}
|
| 140 |
run: |
|
|
@@ -143,12 +188,10 @@ jobs:
|
|
| 143 |
OUT="output/bright-studio-${TS}.mp4"
|
| 144 |
echo "OUTPUT_FILE=$OUT" >> $GITHUB_ENV
|
| 145 |
npx hyperframes render project \
|
| 146 |
-
--output "$OUT"
|
| 147 |
-
--width 720 --height 1280 --fps 24 \
|
| 148 |
-
--quality standard
|
| 149 |
echo "Rendered: $OUT ($(du -sh $OUT | cut -f1))"
|
| 150 |
|
| 151 |
-
- name: Upload to Hugging Face
|
| 152 |
env:
|
| 153 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 154 |
PARTICIPANT_NAME: ${{ github.event.inputs.participant_name }}
|
|
@@ -156,15 +199,12 @@ jobs:
|
|
| 156 |
python3 - <<'PYEOF'
|
| 157 |
import os
|
| 158 |
from huggingface_hub import HfApi
|
| 159 |
-
api
|
| 160 |
out = os.environ["OUTPUT_FILE"]
|
| 161 |
fname = os.path.basename(out)
|
| 162 |
-
api.upload_file(
|
| 163 |
-
path_or_fileobj=out,
|
| 164 |
path_in_repo=f"renders/{fname}",
|
| 165 |
-
repo_id="AIgoose/video-renders",
|
| 166 |
-
repo_type="dataset",
|
| 167 |
-
)
|
| 168 |
url = f"https://huggingface.co/datasets/AIgoose/video-renders/resolve/main/renders/{fname}"
|
| 169 |
print(f"Video ready: {url}")
|
| 170 |
print(f"::notice title=Video Ready::{url}")
|
|
@@ -173,6 +213,6 @@ jobs:
|
|
| 173 |
- name: Upload artifact (backup)
|
| 174 |
uses: actions/upload-artifact@v4
|
| 175 |
with:
|
| 176 |
-
name: bright-studio-
|
| 177 |
path: output/*.mp4
|
| 178 |
retention-days: 14
|
|
|
|
| 8 |
required: true
|
| 9 |
default: '1f1qlgyLJSCQgpjrWflaqldsNE2fZ-_1X'
|
| 10 |
participant_name:
|
| 11 |
+
description: 'Participant name'
|
| 12 |
required: false
|
| 13 |
default: 'Peserta Training'
|
| 14 |
participant_role:
|
|
|
|
| 16 |
required: false
|
| 17 |
default: 'AI Transformation Series 2026'
|
| 18 |
pull_quote:
|
| 19 |
+
description: 'Pull quote (Indonesian)'
|
| 20 |
required: false
|
| 21 |
default: 'Training ini langsung bisa dipraktek dan hasilnya nyata'
|
| 22 |
|
|
|
|
| 32 |
- name: Install system deps
|
| 33 |
run: |
|
| 34 |
sudo apt-get update -qq
|
| 35 |
+
sudo apt-get install -y ffmpeg python3-pip libass-dev fonts-liberation
|
| 36 |
pip3 install gdown huggingface_hub requests --quiet
|
| 37 |
+
fc-cache -fv 2>/dev/null || true
|
| 38 |
|
| 39 |
- name: Setup Node.js 22
|
| 40 |
uses: actions/setup-node@v4
|
|
|
|
| 48 |
SHELL_BIN=$(npx --yes @puppeteer/browsers install chrome-headless-shell@stable 2>&1 | tail -1 | awk '{print $NF}')
|
| 49 |
[ ! -x "$SHELL_BIN" ] && SHELL_BIN=$(find ~/.cache/puppeteer -name "chrome-headless-shell" -executable 2>/dev/null | head -1)
|
| 50 |
echo "HYPERFRAMES_BROWSER_PATH=$SHELL_BIN" >> $GITHUB_ENV
|
| 51 |
+
echo "Browser shell: $SHELL_BIN"
|
| 52 |
|
| 53 |
- name: Download testimonial MOV from Google Drive
|
| 54 |
run: |
|
|
|
|
| 63 |
print(f"Downloaded {os.path.getsize(out)/1024/1024:.1f} MB")
|
| 64 |
PYEOF
|
| 65 |
|
| 66 |
+
- name: Convert MOV to 720p source MP4
|
| 67 |
run: |
|
| 68 |
ffmpeg -y -i assets/raw.MOV \
|
| 69 |
-vf "scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280" \
|
|
|
|
| 72 |
assets/source.mp4
|
| 73 |
echo "Source: $(du -sh assets/source.mp4)"
|
| 74 |
|
| 75 |
+
- name: Cut interviewer section (71-77s cluster)
|
| 76 |
run: |
|
| 77 |
+
ffmpeg -y -i assets/source.mp4 \
|
|
|
|
| 78 |
-filter_complex "\
|
| 79 |
[0:v]trim=0:71,setpts=PTS-STARTPTS[v1];\
|
| 80 |
[0:a]atrim=0:71,asetpts=PTS-STARTPTS[a1];\
|
|
|
|
| 84 |
-map "[vout]" -map "[aout]" \
|
| 85 |
-c:v libx264 -preset fast -crf 22 \
|
| 86 |
-c:a aac -b:a 128k -movflags +faststart \
|
| 87 |
+
assets/cut.mp4
|
| 88 |
+
echo "After cut: $(ffprobe -v quiet -show_entries format=duration \
|
| 89 |
+
-of default=noprint_wrappers=1:nokey=1 assets/cut.mp4 | cut -d. -f1)s"
|
| 90 |
+
|
| 91 |
+
- name: Denoise voice + jump-cut pauses
|
| 92 |
+
run: |
|
| 93 |
+
ffmpeg -y -i assets/cut.mp4 \
|
| 94 |
+
-filter_complex "\
|
| 95 |
+
[0:a]anlmdn=s=7:p=0.002:r=0.002:m=15[dn];\
|
| 96 |
+
[dn]silenceremove=start_periods=1:start_silence=0.3:start_threshold=-35dB\
|
| 97 |
+
:stop_periods=-1:stop_silence=0.05:stop_threshold=-35dB[clean]" \
|
| 98 |
+
-map "[0:v]" -map "[clean]" \
|
| 99 |
+
-c:v copy -c:a aac -b:a 128k \
|
| 100 |
+
assets/denoised.mp4
|
| 101 |
DUR=$(ffprobe -v quiet -show_entries format=duration \
|
| 102 |
+
-of default=noprint_wrappers=1:nokey=1 assets/denoised.mp4 | xargs printf "%.1f")
|
| 103 |
+
echo "FOOTAGE_DUR=$DUR" >> $GITHUB_ENV
|
| 104 |
+
echo "After denoise+jumpcut: ${DUR}s"
|
| 105 |
|
| 106 |
+
- name: Transcribe + generate SRT captions
|
| 107 |
env:
|
| 108 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 109 |
run: |
|
| 110 |
mkdir -p project/assets
|
| 111 |
python3 scripts/gen_captions.py \
|
| 112 |
+
--input assets/denoised.mp4 \
|
| 113 |
+
--srt-out assets/captions.srt
|
| 114 |
+
echo "SRT preview:"; head -20 assets/captions.srt || true
|
| 115 |
|
| 116 |
+
- name: Burn captions into video (ffmpeg, before HyperFrames)
|
| 117 |
run: |
|
| 118 |
+
# Install a system font for subtitle rendering
|
| 119 |
+
FONT_PATH=$(fc-list | grep -i liberation | grep -i regular | head -1 | cut -d: -f1 || echo "")
|
| 120 |
+
[ -z "$FONT_PATH" ] && FONT_PATH="/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
| 121 |
+
echo "Using font: $FONT_PATH"
|
| 122 |
+
|
| 123 |
+
ffmpeg -y -i assets/denoised.mp4 \
|
| 124 |
+
-vf "subtitles=assets/captions.srt:force_style='FontName=Liberation Sans,FontSize=22,PrimaryColour=&H001a2744,OutlineColour=&H00FFFFFF,Outline=10,Shadow=0,Alignment=2,MarginV=120,Bold=1'" \
|
| 125 |
+
-c:v libx264 -preset fast -crf 22 \
|
| 126 |
+
-c:a copy \
|
| 127 |
+
assets/captioned.mp4
|
| 128 |
+
echo "Captioned video: $(du -sh assets/captioned.mp4)"
|
| 129 |
+
|
| 130 |
+
- name: Mix CC0 background music
|
| 131 |
+
run: |
|
| 132 |
+
# Use music.mp3 from repo assets/ if present, else generate tone
|
| 133 |
+
if [ -f "assets/music.mp3" ]; then
|
| 134 |
+
echo "Using repo music.mp3"
|
| 135 |
+
MUSIC="assets/music.mp3"
|
| 136 |
+
else
|
| 137 |
+
echo "Generating lo-fi tone placeholder"
|
| 138 |
+
ffmpeg -y -f lavfi -i \
|
| 139 |
+
"aevalsrc=0.08*sin(2*PI*220*t)+0.05*sin(2*PI*440*t)+0.03*sin(2*PI*880*t):s=44100" \
|
| 140 |
+
-af "volume=0.12,lowpass=f=800,highpass=f=80" \
|
| 141 |
+
-t 120 -q:a 6 assets/music_gen.mp3 2>/dev/null
|
| 142 |
+
MUSIC="assets/music_gen.mp3"
|
| 143 |
+
fi
|
| 144 |
+
|
| 145 |
+
DUR=$(ffprobe -v quiet -show_entries format=duration \
|
| 146 |
+
-of default=noprint_wrappers=1:nokey=1 assets/captioned.mp4 | xargs printf "%.1f")
|
| 147 |
+
|
| 148 |
+
ffmpeg -y \
|
| 149 |
+
-i assets/captioned.mp4 \
|
| 150 |
+
-stream_loop -1 -i "$MUSIC" \
|
| 151 |
+
-filter_complex "\
|
| 152 |
+
[1:a]atrim=0:${DUR},asetpts=PTS-STARTPTS,volume=0.10[music];\
|
| 153 |
+
[0:a][music]amix=inputs=2:duration=first:dropout_transition=2[aout]" \
|
| 154 |
+
-map "0:v" -map "[aout]" \
|
| 155 |
+
-c:v copy -c:a aac -b:a 128k \
|
| 156 |
+
assets/clean.mp4
|
| 157 |
+
FOOTAGE_DUR=$(ffprobe -v quiet -show_entries format=duration \
|
| 158 |
+
-of default=noprint_wrappers=1:nokey=1 assets/clean.mp4 | xargs printf "%.0f")
|
| 159 |
+
TOTAL_DUR=$((FOOTAGE_DUR + 2))
|
| 160 |
+
echo "FOOTAGE_DUR=$FOOTAGE_DUR" >> $GITHUB_ENV
|
| 161 |
+
echo "TOTAL_DUR=$TOTAL_DUR" >> $GITHUB_ENV
|
| 162 |
+
echo "Final clean.mp4: ${FOOTAGE_DUR}s | total with title card: ${TOTAL_DUR}s"
|
| 163 |
|
| 164 |
- name: Build project folder
|
| 165 |
run: |
|
| 166 |
+
cp assets/clean.mp4 project/assets/clean.mp4
|
| 167 |
+
[ -f "assets/music.mp3" ] && cp assets/music.mp3 project/assets/music.mp3 || \
|
| 168 |
+
cp assets/music_gen.mp3 project/assets/music.mp3
|
| 169 |
python3 scripts/patch_composition.py \
|
| 170 |
+
--input compositions/testimonial.html \
|
| 171 |
+
--output project/index.html \
|
| 172 |
+
--name "${{ github.event.inputs.participant_name }}" \
|
| 173 |
+
--role "${{ github.event.inputs.participant_role }}" \
|
| 174 |
+
--quote "${{ github.event.inputs.pull_quote }}" \
|
| 175 |
+
--footage-dur "$FOOTAGE_DUR" \
|
| 176 |
+
--total-dur "$TOTAL_DUR"
|
| 177 |
+
echo "Project contents:"; ls -lh project/ project/assets/
|
| 178 |
+
|
| 179 |
+
- name: Lint composition
|
| 180 |
+
run: npx hyperframes lint project || true
|
| 181 |
+
|
| 182 |
+
- name: Render (720x1280 @ 24fps - Bright Studio v3)
|
| 183 |
env:
|
| 184 |
HYPERFRAMES_BROWSER_PATH: ${{ env.HYPERFRAMES_BROWSER_PATH }}
|
| 185 |
run: |
|
|
|
|
| 188 |
OUT="output/bright-studio-${TS}.mp4"
|
| 189 |
echo "OUTPUT_FILE=$OUT" >> $GITHUB_ENV
|
| 190 |
npx hyperframes render project \
|
| 191 |
+
--output "$OUT" --width 720 --height 1280 --fps 24 --quality standard
|
|
|
|
|
|
|
| 192 |
echo "Rendered: $OUT ($(du -sh $OUT | cut -f1))"
|
| 193 |
|
| 194 |
+
- name: Upload to Hugging Face
|
| 195 |
env:
|
| 196 |
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 197 |
PARTICIPANT_NAME: ${{ github.event.inputs.participant_name }}
|
|
|
|
| 199 |
python3 - <<'PYEOF'
|
| 200 |
import os
|
| 201 |
from huggingface_hub import HfApi
|
| 202 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 203 |
out = os.environ["OUTPUT_FILE"]
|
| 204 |
fname = os.path.basename(out)
|
| 205 |
+
api.upload_file(path_or_fileobj=out,
|
|
|
|
| 206 |
path_in_repo=f"renders/{fname}",
|
| 207 |
+
repo_id="AIgoose/video-renders", repo_type="dataset")
|
|
|
|
|
|
|
| 208 |
url = f"https://huggingface.co/datasets/AIgoose/video-renders/resolve/main/renders/{fname}"
|
| 209 |
print(f"Video ready: {url}")
|
| 210 |
print(f"::notice title=Video Ready::{url}")
|
|
|
|
| 213 |
- name: Upload artifact (backup)
|
| 214 |
uses: actions/upload-artifact@v4
|
| 215 |
with:
|
| 216 |
+
name: bright-studio-v3
|
| 217 |
path: output/*.mp4
|
| 218 |
retention-days: 14
|
compositions/testimonial.html
CHANGED
|
@@ -1,220 +1,163 @@
|
|
| 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:#f5f5f0; overflow:hidden; }
|
| 10 |
-
#root {
|
| 11 |
-
position:relative; width:720px; height:1280px;
|
| 12 |
-
overflow:hidden; background:#f5f5f0;
|
| 13 |
-
}
|
| 14 |
.clip { position:absolute; }
|
| 15 |
.sc { inset:0; }
|
| 16 |
-
</style>
|
| 17 |
-
</head>
|
| 18 |
-
<body>
|
| 19 |
<div id="root"
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
data-height="1280">
|
| 25 |
-
|
| 26 |
-
<!-- ============================================================
|
| 27 |
-
A-ROLL: trimmed footage fills full frame, NO muted
|
| 28 |
-
Natural bright background preserved — this IS the style
|
| 29 |
-
============================================================= -->
|
| 30 |
-
<video id="aroll" class="clip sc" src="../assets/trimmed.mp4"
|
| 31 |
-
data-start="0" data-duration="DURATION_PLACEHOLDER" data-track-index="0"
|
| 32 |
-
data-volume="1.0" playsinline
|
| 33 |
-
style="object-fit:cover;width:100%;height:100%;"></video>
|
| 34 |
|
| 35 |
-
<!--
|
| 36 |
-
STYLE SIGNATURE: 4px navy left border — always visible
|
| 37 |
-
============================================================= -->
|
| 38 |
<div style="position:absolute;top:0;left:0;width:4px;height:100%;
|
| 39 |
-
background:#1a2744;z-index:
|
| 40 |
-
|
| 41 |
-
<!--
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
AI<br>TRANS-<br>FORM</div>
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
color:#1a2744;letter-spacing:0.04em;z-index:10;">
|
| 56 |
SERIES 2026 · DEE FERDINAND AI</div>
|
| 57 |
|
| 58 |
-
<
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
===
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
color:#1a2744;letter-spacing:-0.02em;">PARTICIPANT_NAME</div>
|
| 74 |
-
<div style="font:500
|
| 75 |
-
color:#1a2744;opacity:0.
|
| 76 |
</div>
|
| 77 |
|
| 78 |
-
<!--
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
background:rgba(245,245,240,0.88);border-radius:14px;
|
| 86 |
-
padding:24px 22px 20px 28px;
|
| 87 |
-
border-left:5px solid #1a2744;">
|
| 88 |
-
<div style="font:800 64px/1.0 'Space Grotesk',sans-serif;
|
| 89 |
-
color:#1a2744;letter-spacing:-2px;margin-bottom:14px;">
|
| 90 |
“PULL_QUOTE_TEXT”</div>
|
| 91 |
-
<div style="font:500
|
| 92 |
-
color:
|
| 93 |
— PARTICIPANT_NAME · AI Transformation Series</div>
|
| 94 |
</div>
|
| 95 |
|
| 96 |
-
<!--
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
<
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
</div>
|
| 107 |
|
| 108 |
-
<!--
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
<div id="
|
| 112 |
-
style="position:absolute;
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
<div id="
|
| 124 |
-
style="position:absolute;
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
<!-- ============================================================
|
| 130 |
-
CAPTIONS: word-synced subtitles burned by ffmpeg
|
| 131 |
-
This track carries the SubRip (SRT) captions file reference
|
| 132 |
-
HyperFrames renders these as a burned-in ffmpeg subtitle filter
|
| 133 |
-
Style: white pill background, navy Plus Jakarta Sans text
|
| 134 |
-
============================================================= -->
|
| 135 |
-
<track id="captions" src="../assets/captions.srt"
|
| 136 |
-
data-start="0" data-duration="DURATION_PLACEHOLDER" data-track-index="25"
|
| 137 |
-
kind="subtitles" srclang="id" default
|
| 138 |
-
data-style="FontName=Plus Jakarta Sans,FontSize=28,PrimaryColour=&H001a2744,OutlineColour=&H00FFFFFF,Outline=12,Shadow=0,Alignment=2,MarginV=140,Bold=1"/>
|
| 139 |
-
|
| 140 |
-
<!-- ============================================================
|
| 141 |
-
MUSIC BED: CC0 acoustic at 10% — loops under voice
|
| 142 |
-
============================================================= -->
|
| 143 |
<audio src="../assets/music.mp3"
|
| 144 |
-
data-start="0" data-duration="
|
| 145 |
data-track-index="50" data-volume="0.10"></audio>
|
| 146 |
|
| 147 |
-
<!--
|
| 148 |
-
BRAND OUTRO: last 7s — full navy overlay + CTA
|
| 149 |
-
============================================================= -->
|
| 150 |
-
<div id="s6-overlay" class="clip sc" data-start="OUTRO_START" data-duration="7" data-track-index="15"
|
| 151 |
-
style="background:#1a2744;z-index:18;"></div>
|
| 152 |
-
<div id="s6-name" class="clip" data-start="OUTRO_NAME" data-duration="6.5" data-track-index="16"
|
| 153 |
-
style="position:absolute;bottom:300px;left:0;right:0;text-align:center;
|
| 154 |
-
font:900 72px/1.0 'Space Grotesk',sans-serif;color:#fff;
|
| 155 |
-
letter-spacing:-2px;z-index:19;">Dee Ferdinand</div>
|
| 156 |
-
<div id="s6-title" class="clip" data-start="OUTRO_TITLE" data-duration="6" data-track-index="17"
|
| 157 |
-
style="position:absolute;bottom:240px;left:0;right:0;text-align:center;
|
| 158 |
-
font:600 26px/1.2 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.72);z-index:19;">
|
| 159 |
-
AI Corporate Trainer · MAIN AI</div>
|
| 160 |
-
<div id="s6-handle" class="clip" data-start="OUTRO_HANDLE" data-duration="5.5" data-track-index="18"
|
| 161 |
-
style="position:absolute;bottom:196px;left:0;right:0;text-align:center;
|
| 162 |
-
font:400 20px/1 'Plus Jakarta Sans',sans-serif;color:rgba(255,255,255,0.45);z-index:19;">
|
| 163 |
-
@deeferdinand · deeferdinand.com</div>
|
| 164 |
-
<div id="s6-cta" class="clip" data-start="OUTRO_CTA" data-duration="5" data-track-index="19"
|
| 165 |
-
style="position:absolute;bottom:120px;left:0;right:0;text-align:center;
|
| 166 |
-
font:700 30px/1 'Plus Jakarta Sans',sans-serif;font-style:italic;
|
| 167 |
-
color:#a8a0ff;z-index:19;">
|
| 168 |
-
Transform your team →</div>
|
| 169 |
-
|
| 170 |
-
<!-- Persistent brand bar — always visible -->
|
| 171 |
<div style="position:absolute;bottom:0;left:4px;right:0;
|
| 172 |
-
padding:
|
| 173 |
-
background:linear-gradient(transparent,rgba(245,245,240,0.
|
| 174 |
-
<span style="font:800
|
| 175 |
-
|
| 176 |
-
<span style="font:400
|
| 177 |
-
color:#1a2744;opacity:0.
|
| 178 |
</div>
|
| 179 |
-
|
| 180 |
</div>
|
| 181 |
|
| 182 |
<script>
|
| 183 |
const tl = gsap.timeline({ paused: true });
|
| 184 |
|
| 185 |
-
|
| 186 |
-
tl.from("#
|
| 187 |
-
tl.from("#
|
| 188 |
-
tl.from("#s1-sub", { y:30, autoAlpha:0, duration:0.35, ease:"power3.out" }, 0.5);
|
| 189 |
-
|
| 190 |
-
// S2: LOWER-THIRD — slide from left (power3.out decisive)
|
| 191 |
-
tl.from("#s2-card", { x:-500, autoAlpha:0, duration:0.38, ease:"power3.out" }, 6);
|
| 192 |
|
| 193 |
-
|
| 194 |
-
tl.from("#s3-quote", { scale:0.88, autoAlpha:0, y:30, duration:0.55, ease:"back.out(1.8)" }, 20);
|
| 195 |
-
tl.to("#s3-quote", { autoAlpha:0, duration:0.3, ease:"sine.inOut" }, 37.5);
|
| 196 |
|
| 197 |
-
|
| 198 |
-
tl.
|
| 199 |
-
tl.to("#s4-stat", { autoAlpha:0, duration:0.25 }, 51.5);
|
| 200 |
|
| 201 |
-
|
| 202 |
-
tl.from("#
|
| 203 |
-
tl.from("#
|
| 204 |
-
tl.
|
| 205 |
-
tl.to(["#s5-chip1","#s5-chip2","#s5-chip3"], { autoAlpha:0, duration:0.25 }, 65.5);
|
| 206 |
|
| 207 |
-
|
| 208 |
-
tl.
|
| 209 |
-
tl.from("#s6-name", { y:40, autoAlpha:0, duration:0.45, ease:"power3.out" }, "OUTRO_NAME");
|
| 210 |
-
tl.from("#s6-title", { y:30, autoAlpha:0, duration:0.40, ease:"power3.out" }, "OUTRO_TITLE");
|
| 211 |
-
tl.from("#s6-handle", { y:20, autoAlpha:0, duration:0.35, ease:"power3.out" }, "OUTRO_HANDLE");
|
| 212 |
-
tl.from("#s6-cta", { y:20, autoAlpha:0, duration:0.35, ease:"power3.out" }, "OUTRO_CTA");
|
| 213 |
|
| 214 |
-
tl.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
|
|
|
| 216 |
window.__timelines = window.__timelines || {};
|
| 217 |
-
window.__timelines["bright-studio-
|
| 218 |
</script>
|
| 219 |
-
</body>
|
| 220 |
-
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
+
<html lang="id"><head><meta charset="UTF-8">
|
|
|
|
|
|
|
| 3 |
<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">
|
| 4 |
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
| 5 |
<style>
|
| 6 |
* { margin:0; padding:0; box-sizing:border-box; }
|
| 7 |
body { background:#f5f5f0; overflow:hidden; }
|
| 8 |
+
#root { position:relative; width:720px; height:1280px; overflow:hidden; }
|
|
|
|
|
|
|
|
|
|
| 9 |
.clip { position:absolute; }
|
| 10 |
.sc { inset:0; }
|
| 11 |
+
</style></head><body>
|
|
|
|
|
|
|
| 12 |
<div id="root"
|
| 13 |
+
data-composition-id="bright-studio-v3"
|
| 14 |
+
data-start="0"
|
| 15 |
+
data-duration="TOTAL_DUR"
|
| 16 |
+
data-width="720" data-height="1280">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
<!-- ── STYLE RULE: 4px navy left border always visible ────── -->
|
|
|
|
|
|
|
| 19 |
<div style="position:absolute;top:0;left:0;width:4px;height:100%;
|
| 20 |
+
background:#1a2744;z-index:50;pointer-events:none;"></div>
|
| 21 |
+
|
| 22 |
+
<!-- ── SCENE 0: TITLE CARD 0–2.5s (navy bg, NO footage) ──── -->
|
| 23 |
+
<div id="s0-bg" class="clip sc" data-start="0" data-duration="2.5" data-track-index="0"
|
| 24 |
+
style="background:#1a2744;z-index:5;"></div>
|
| 25 |
+
<div id="s0-line" class="clip" data-start="0.15" data-duration="2.2" data-track-index="1"
|
| 26 |
+
style="position:absolute;top:480px;left:20px;width:60px;height:3px;
|
| 27 |
+
background:#fff;z-index:6;"></div>
|
| 28 |
+
<div id="s0-head" class="clip" data-start="0.2" data-duration="2.1" data-track-index="2"
|
| 29 |
+
style="position:absolute;top:500px;left:20px;right:20px;
|
| 30 |
+
font:900 80px/0.95 'Space Grotesk',sans-serif;color:#fff;
|
| 31 |
+
letter-spacing:-2px;z-index:6;">AI<br>TRANS-<br>FORM</div>
|
| 32 |
+
<div id="s0-sub" class="clip" data-start="0.45" data-duration="1.85" data-track-index="3"
|
| 33 |
+
style="position:absolute;top:750px;left:20px;
|
| 34 |
+
font:600 22px/1.3 'Plus Jakarta Sans',sans-serif;
|
| 35 |
+
color:rgba(255,255,255,0.60);letter-spacing:0.05em;z-index:6;">
|
|
|
|
| 36 |
SERIES 2026 · DEE FERDINAND AI</div>
|
| 37 |
|
| 38 |
+
<!-- ── SCENE 1: FOOTAGE 2.2–END (captions burned in by ffmpeg) -->
|
| 39 |
+
<video id="aroll" class="clip sc" src="../assets/clean.mp4"
|
| 40 |
+
data-start="2.2" data-duration="FOOTAGE_DUR" data-track-index="4"
|
| 41 |
+
data-volume="1.0" playsinline
|
| 42 |
+
style="object-fit:cover;width:100%;height:100%;"></video>
|
| 43 |
+
|
| 44 |
+
<!-- Bottom vignette over footage (persistent after t=2.2) -->
|
| 45 |
+
<div class="clip sc" data-start="2.2" data-duration="FOOTAGE_DUR" data-track-index="5"
|
| 46 |
+
style="background:linear-gradient(to top,rgba(0,0,0,0.55) 0%,transparent 35%);
|
| 47 |
+
pointer-events:none;z-index:6;"></div>
|
| 48 |
+
|
| 49 |
+
<!-- ── SCENE 2: LOWER-THIRD ID 5–13s ──────────────────────── -->
|
| 50 |
+
<div id="s2-lt" class="clip" data-start="5" data-duration="8" data-track-index="6"
|
| 51 |
+
style="position:absolute;bottom:180px;left:0;
|
| 52 |
+
background:rgba(255,255,255,0.94);
|
| 53 |
+
border-left:5px solid #1a2744;border-radius:0 12px 12px 0;
|
| 54 |
+
padding:16px 26px 16px 22px;z-index:10;">
|
| 55 |
+
<div style="font:800 32px/1.1 'Space Grotesk',sans-serif;
|
| 56 |
color:#1a2744;letter-spacing:-0.02em;">PARTICIPANT_NAME</div>
|
| 57 |
+
<div style="font:500 17px/1.3 'Plus Jakarta Sans',sans-serif;
|
| 58 |
+
color:#1a2744;opacity:0.55;margin-top:4px;">PARTICIPANT_ROLE</div>
|
| 59 |
</div>
|
| 60 |
|
| 61 |
+
<!-- ── SCENE 3: PULL QUOTE 18–26s (8s only) ──────────────── -->
|
| 62 |
+
<div id="s3-q" class="clip" data-start="18" data-duration="8" data-track-index="7"
|
| 63 |
+
style="position:absolute;top:36px;left:12px;right:12px;z-index:10;
|
| 64 |
+
background:rgba(26,39,68,0.90);border-radius:14px;
|
| 65 |
+
padding:22px 22px 18px 22px;">
|
| 66 |
+
<div style="font:800 52px/1.05 'Space Grotesk',sans-serif;
|
| 67 |
+
color:#fff;letter-spacing:-1.5px;margin-bottom:12px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
“PULL_QUOTE_TEXT”</div>
|
| 69 |
+
<div style="font:500 17px/1.3 'Plus Jakarta Sans',sans-serif;
|
| 70 |
+
color:rgba(255,255,255,0.55);">
|
| 71 |
— PARTICIPANT_NAME · AI Transformation Series</div>
|
| 72 |
</div>
|
| 73 |
|
| 74 |
+
<!-- ── SCENE 4: CHIPS 30–42s (each on own track) ─────────── -->
|
| 75 |
+
<div id="s4-c1" class="clip" data-start="30" data-duration="12" data-track-index="8"
|
| 76 |
+
style="position:absolute;bottom:280px;left:12px;
|
| 77 |
+
background:#1a2744;border-radius:40px;padding:12px 22px;z-index:10;">
|
| 78 |
+
<span style="font:800 26px/1 'Space Grotesk',sans-serif;color:#fff;">80% hands-on</span></div>
|
| 79 |
+
<div id="s4-c2" class="clip" data-start="31.5" data-duration="10.5" data-track-index="9"
|
| 80 |
+
style="position:absolute;bottom:228px;left:12px;border:3px solid #1a2744;
|
| 81 |
+
border-radius:40px;padding:12px 22px;z-index:10;">
|
| 82 |
+
<span style="font:700 26px/1 'Plus Jakarta Sans',sans-serif;color:#1a2744;">Langsung praktek</span></div>
|
| 83 |
+
<div id="s4-c3" class="clip" data-start="33" data-duration="9" data-track-index="10"
|
| 84 |
+
style="position:absolute;bottom:176px;left:12px;border:3px solid #1a2744;
|
| 85 |
+
border-radius:40px;padding:12px 22px;z-index:10;">
|
| 86 |
+
<span style="font:700 26px/1 'Plus Jakarta Sans',sans-serif;color:#1a2744;">Real output ✓</span></div>
|
| 87 |
+
|
| 88 |
+
<!-- ── SCENE 5: STAT 44–54s ───────────────────────────────── -->
|
| 89 |
+
<div id="s5-stat" class="clip" data-start="44" data-duration="10" data-track-index="11"
|
| 90 |
+
style="position:absolute;top:36px;right:12px;text-align:right;z-index:10;">
|
| 91 |
+
<div style="font:900 100px/0.9 'Space Grotesk',sans-serif;
|
| 92 |
+
color:#1a2744;letter-spacing:-3px;">1,000+</div>
|
| 93 |
+
<div style="font:600 22px/1.3 'Plus Jakarta Sans',sans-serif;
|
| 94 |
+
color:#1a2744;opacity:0.65;margin-top:6px;">profesional terlatih<br>sejak 2023</div>
|
| 95 |
</div>
|
| 96 |
|
| 97 |
+
<!-- ── SCENE 6: BRAND OUTRO ───────────────────────────────── -->
|
| 98 |
+
<div id="s6-ov" class="clip sc" data-start="OUTRO_START" data-duration="7" data-track-index="12"
|
| 99 |
+
style="background:#1a2744;z-index:20;"></div>
|
| 100 |
+
<div id="s6-nm" class="clip" data-start="OUTRO_S1" data-duration="6.5" data-track-index="13"
|
| 101 |
+
style="position:absolute;bottom:300px;left:0;right:0;text-align:center;
|
| 102 |
+
font:900 64px/1.0 'Space Grotesk',sans-serif;color:#fff;
|
| 103 |
+
letter-spacing:-2px;z-index:21;">Dee Ferdinand</div>
|
| 104 |
+
<div id="s6-ti" class="clip" data-start="OUTRO_S2" data-duration="6" data-track-index="14"
|
| 105 |
+
style="position:absolute;bottom:246px;left:0;right:0;text-align:center;
|
| 106 |
+
font:600 24px/1.2 'Plus Jakarta Sans',sans-serif;
|
| 107 |
+
color:rgba(255,255,255,0.65);z-index:21;">AI Corporate Trainer · MAIN AI</div>
|
| 108 |
+
<div id="s6-hd" class="clip" data-start="OUTRO_S3" data-duration="5.5" data-track-index="15"
|
| 109 |
+
style="position:absolute;bottom:206px;left:0;right:0;text-align:center;
|
| 110 |
+
font:400 18px/1 'Plus Jakarta Sans',sans-serif;
|
| 111 |
+
color:rgba(255,255,255,0.40);z-index:21;">@deeferdinand · deeferdinand.com</div>
|
| 112 |
+
<div id="s6-ct" class="clip" data-start="OUTRO_S4" data-duration="5" data-track-index="16"
|
| 113 |
+
style="position:absolute;bottom:140px;left:0;right:0;text-align:center;
|
| 114 |
+
font:700 26px/1 'Plus Jakarta Sans',sans-serif;font-style:italic;
|
| 115 |
+
color:#a8a0ff;z-index:21;">Transform your team →</div>
|
| 116 |
+
|
| 117 |
+
<!-- Music (CC0 hosted in repo assets) -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
<audio src="../assets/music.mp3"
|
| 119 |
+
data-start="0" data-duration="TOTAL_DUR"
|
| 120 |
data-track-index="50" data-volume="0.10"></audio>
|
| 121 |
|
| 122 |
+
<!-- Persistent brand bar -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
<div style="position:absolute;bottom:0;left:4px;right:0;
|
| 124 |
+
padding:12px 20px 28px;
|
| 125 |
+
background:linear-gradient(transparent,rgba(245,245,240,0.92));z-index:9;">
|
| 126 |
+
<span style="font:800 19px/1 'Space Grotesk',sans-serif;color:#1a2744;
|
| 127 |
+
letter-spacing:-0.02em;">Dee Ferdinand</span>
|
| 128 |
+
<span style="font:400 13px/1 'Plus Jakarta Sans',sans-serif;
|
| 129 |
+
color:#1a2744;opacity:0.50;margin-left:5px;"> · AI Corporate Trainer</span>
|
| 130 |
</div>
|
|
|
|
| 131 |
</div>
|
| 132 |
|
| 133 |
<script>
|
| 134 |
const tl = gsap.timeline({ paused: true });
|
| 135 |
|
| 136 |
+
tl.from("#s0-line", { scaleX:0, transformOrigin:"left", duration:0.18, ease:"expo.out" }, 0.15);
|
| 137 |
+
tl.from("#s0-head", { y:-80, autoAlpha:0, duration:0.22, ease:"expo.out" }, 0.2);
|
| 138 |
+
tl.from("#s0-sub", { y:30, autoAlpha:0, duration:0.30, ease:"power3.out" }, 0.45);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
tl.from("#s2-lt", { x:-480, autoAlpha:0, duration:0.38, ease:"power3.out" }, 5);
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
tl.from("#s3-q", { scale:0.90, autoAlpha:0, duration:0.45, ease:"back.out(1.6)" }, 18);
|
| 143 |
+
tl.to("#s3-q", { autoAlpha:0, duration:0.28, ease:"sine.inOut" }, 25.7);
|
|
|
|
| 144 |
|
| 145 |
+
tl.from("#s4-c1", { x:-200, autoAlpha:0, duration:0.26, ease:"power4.out" }, 30);
|
| 146 |
+
tl.from("#s4-c2", { x:-200, autoAlpha:0, duration:0.26, ease:"power4.out" }, 31.5);
|
| 147 |
+
tl.from("#s4-c3", { x:-200, autoAlpha:0, duration:0.26, ease:"power4.out" }, 33);
|
| 148 |
+
tl.to(["#s4-c1","#s4-c2","#s4-c3"], { autoAlpha:0, duration:0.22 }, 41.5);
|
|
|
|
| 149 |
|
| 150 |
+
tl.from("#s5-stat", { y:-60, autoAlpha:0, duration:0.28, ease:"expo.out" }, 44);
|
| 151 |
+
tl.to("#s5-stat", { autoAlpha:0, duration:0.22 }, 53.5);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
+
tl.from("#s6-ov", { autoAlpha:0, duration:0.50, ease:"sine.inOut" }, "OUTRO_START");
|
| 154 |
+
tl.from("#s6-nm", { y:36, autoAlpha:0, duration:0.42, ease:"power3.out" }, "OUTRO_S1");
|
| 155 |
+
tl.from("#s6-ti", { y:28, autoAlpha:0, duration:0.38, ease:"power3.out" }, "OUTRO_S2");
|
| 156 |
+
tl.from("#s6-hd", { y:20, autoAlpha:0, duration:0.33, ease:"power3.out" }, "OUTRO_S3");
|
| 157 |
+
tl.from("#s6-ct", { y:20, autoAlpha:0, duration:0.33, ease:"power3.out" }, "OUTRO_S4");
|
| 158 |
|
| 159 |
+
tl.set({}, {}, TOTAL_DUR);
|
| 160 |
window.__timelines = window.__timelines || {};
|
| 161 |
+
window.__timelines["bright-studio-v3"] = tl;
|
| 162 |
</script>
|
| 163 |
+
</body></html>
|
|
|
scripts/patch_composition.py
CHANGED
|
@@ -1,52 +1,47 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""patch_composition.py
|
| 3 |
import argparse, re
|
| 4 |
|
| 5 |
-
def patch(html, name, role, quote,
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
html = html.replace("DURATION_PLACEHOLDER", dur)
|
| 14 |
-
html = html.replace("OUTRO_START", outro_start)
|
| 15 |
-
html = html.replace('"OUTRO_START"', f'"+={outro_start}"')
|
| 16 |
-
html = html.replace("OUTRO_NAME", outro_name)
|
| 17 |
-
html = html.replace("OUTRO_TITLE", outro_title)
|
| 18 |
-
html = html.replace("OUTRO_HANDLE",outro_handle)
|
| 19 |
-
html = html.replace("OUTRO_CTA", outro_cta)
|
| 20 |
-
|
| 21 |
-
# Also fix the GSAP string labels
|
| 22 |
-
html = re.sub(r'"OUTRO_\w+"', lambda m: {
|
| 23 |
-
'"OUTRO_START"': f'"{outro_start}"',
|
| 24 |
-
'"OUTRO_NAME"': f'"{outro_name}"',
|
| 25 |
-
'"OUTRO_TITLE"': f'"{outro_title}"',
|
| 26 |
-
'"OUTRO_HANDLE"': f'"{outro_handle}"',
|
| 27 |
-
'"OUTRO_CTA"': f'"{outro_cta}"',
|
| 28 |
-
}.get(m.group(0), m.group(0)), html)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
html = html.replace("PARTICIPANT_NAME", name)
|
| 31 |
html = html.replace("PARTICIPANT_ROLE", role)
|
| 32 |
-
|
|
|
|
| 33 |
return html
|
| 34 |
|
| 35 |
def main():
|
| 36 |
p = argparse.ArgumentParser()
|
| 37 |
-
p.add_argument("--input",
|
| 38 |
-
p.add_argument("--output",
|
| 39 |
-
p.add_argument("--name",
|
| 40 |
-
p.add_argument("--role",
|
| 41 |
-
p.add_argument("--quote",
|
| 42 |
-
p.add_argument("--
|
|
|
|
| 43 |
args = p.parse_args()
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
f.write(patch(html, args.name, args.role, args.quote, args.duration))
|
| 49 |
-
print(f"Patched: {args.output} (duration={args.duration}s, name={args.name})")
|
| 50 |
-
|
| 51 |
if __name__ == "__main__":
|
| 52 |
main()
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""patch_composition.py v2 - inject name/role/quote/durations"""
|
| 3 |
import argparse, re
|
| 4 |
|
| 5 |
+
def patch(html, name, role, quote, footage_dur, total_dur):
|
| 6 |
+
fd = int(float(footage_dur))
|
| 7 |
+
td = int(float(total_dur))
|
| 8 |
+
os = str(td - 7) # outro start
|
| 9 |
+
s1 = str(td - 6.5)
|
| 10 |
+
s2 = str(td - 6)
|
| 11 |
+
s3 = str(td - 5.5)
|
| 12 |
+
s4 = str(td - 5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
html = html.replace("TOTAL_DUR", str(td))
|
| 15 |
+
html = html.replace("FOOTAGE_DUR", str(fd))
|
| 16 |
+
html = html.replace("OUTRO_START", os)
|
| 17 |
+
html = html.replace("OUTRO_S1", s1)
|
| 18 |
+
html = html.replace("OUTRO_S2", s2)
|
| 19 |
+
html = html.replace("OUTRO_S3", s3)
|
| 20 |
+
html = html.replace("OUTRO_S4", s4)
|
| 21 |
+
html = re.sub(r'"OUTRO_START"', f'"{os}"', html)
|
| 22 |
+
html = re.sub(r'"OUTRO_S1"', f'"{s1}"', html)
|
| 23 |
+
html = re.sub(r'"OUTRO_S2"', f'"{s2}"', html)
|
| 24 |
+
html = re.sub(r'"OUTRO_S3"', f'"{s3}"', html)
|
| 25 |
+
html = re.sub(r'"OUTRO_S4"', f'"{s4}"', html)
|
| 26 |
html = html.replace("PARTICIPANT_NAME", name)
|
| 27 |
html = html.replace("PARTICIPANT_ROLE", role)
|
| 28 |
+
q80 = (quote[:78] + "...") if len(quote) > 80 else quote
|
| 29 |
+
html = html.replace("PULL_QUOTE_TEXT", q80)
|
| 30 |
return html
|
| 31 |
|
| 32 |
def main():
|
| 33 |
p = argparse.ArgumentParser()
|
| 34 |
+
p.add_argument("--input", required=True)
|
| 35 |
+
p.add_argument("--output", required=True)
|
| 36 |
+
p.add_argument("--name", default="Peserta Training")
|
| 37 |
+
p.add_argument("--role", default="AI Transformation Series 2026")
|
| 38 |
+
p.add_argument("--quote", default="Training ini langsung bisa dipraktek dan hasilnya nyata")
|
| 39 |
+
p.add_argument("--footage-dur", default="75")
|
| 40 |
+
p.add_argument("--total-dur", default="77.5")
|
| 41 |
args = p.parse_args()
|
| 42 |
+
with open(args.input, encoding="utf-8") as f: html = f.read()
|
| 43 |
+
out = patch(html, args.name, args.role, args.quote, args.footage_dur, args.total_dur)
|
| 44 |
+
with open(args.output, "w", encoding="utf-8") as f: f.write(out)
|
| 45 |
+
print(f"Patched OK — footage={args.footage_dur}s total={args.total_dur}s name={args.name}")
|
|
|
|
|
|
|
|
|
|
| 46 |
if __name__ == "__main__":
|
| 47 |
main()
|