kaizen-agent 1.48.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/cli/build-web.ts +4 -2
- package/cli/hermes.ts +65 -0
- package/cli/install.ts +18 -5
- package/cli/localsend.test.ts +84 -0
- package/cli/localsend.ts +131 -0
- package/cli/notes.test.ts +6 -4
- package/cli/settings.ts +1 -0
- package/cli/state.test.ts +37 -0
- package/cli/state.ts +35 -20
- package/cli/video.test.ts +479 -0
- package/cli/video.ts +507 -0
- package/cli/web.ts +127 -1
- package/package.json +1 -1
- package/skills/kaizen/config.default.yml +4 -0
- package/skills/media/kaizen-book/SKILL.md +107 -0
- package/skills/media/kaizen-book/assets/reader.html +440 -0
- package/skills/media/kaizen-book/assets/styles.json +45 -0
- package/skills/media/kaizen-book/examples/book.json +14 -0
- package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
- package/skills/media/kaizen-book/examples/manuscript.md +127 -0
- package/skills/media/kaizen-book/references/interview.md +85 -0
- package/skills/media/kaizen-book/references/proofread.md +47 -0
- package/skills/media/kaizen-book/references/writing.md +53 -0
- package/skills/media/kaizen-book/scripts/build_book.py +279 -0
- package/skills/media/kaizen-book/scripts/check.sh +10 -0
- package/skills/media/kaizen-book/scripts/check_book.py +128 -0
- package/skills/media/kaizen-book/scripts/deploy.py +58 -0
- package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
- package/skills/media/kaizen-book/scripts/setup.sh +9 -0
- package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
- package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
- package/skills/media/kaizen-video/SKILL.md +112 -0
- package/skills/{kaizen-video → media/kaizen-video}/assets/CREDITS.txt +5 -0
- package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
- package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
- package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
- package/skills/media/kaizen-video/film.html +246 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
- package/skills/{kaizen-video → media/kaizen-video}/references/presets.md +22 -0
- package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
- package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
- package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
- package/skills/{kaizen-video → media/kaizen-video}/scripts/check.sh +8 -0
- package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
- package/skills/media/kaizen-video/scripts/make_film.py +263 -0
- package/skills/{kaizen-video → media/kaizen-video}/scripts/make_tiktok.py +8 -3
- package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
- package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
- package/skills/media/kaizen-video/scripts/srt.py +9 -0
- package/skills/media/kaizen-video/scripts/still.sh +7 -0
- package/web/board/views.ts +1 -1
- package/web/board.html +2 -0
- package/web/board.js +2 -2
- package/web/notebook.js +1 -1
- package/web/notes.html +2 -0
- package/web/term.js +1 -1
- package/web/video.html +298 -0
- package/web/video.js +81 -0
- package/web/video.ts +610 -0
- package/skills/kaizen-video/SKILL.md +0 -73
- package/skills/story-video/SKILL.md +0 -108
- package/skills/story-video/engine/package.json +0 -12
- package/skills/story-video/engine/src/Root.tsx +0 -24
- package/skills/story-video/engine/src/engine/Film.tsx +0 -405
- package/skills/story-video/engine/src/engine/presets.json +0 -58
- package/skills/story-video/engine/src/index.ts +0 -3
- package/skills/story-video/engine/tsconfig.json +0 -1
- package/skills/story-video/examples/sample-film/README.md +0 -12
- package/skills/story-video/examples/sample-film/film.json +0 -16
- package/skills/story-video/references/presets.md +0 -21
- package/skills/story-video/scripts/check.sh +0 -19
- package/skills/story-video/scripts/setup.sh +0 -25
- package/skills/story-video/scripts/srt.py +0 -30
- package/skills/story-video/scripts/still.sh +0 -10
- /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Fredoka.ttf +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Montserrat.ttf +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/SpecialElite.ttf +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/music/investigations.mp3 +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/music/lights.mp3 +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/music/sunny.mp3 +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/examples/config.json +0 -0
- /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/hyperframes.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/package.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/package.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/package.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/package.json +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/scripts/render_cut.py +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/scripts/setup.sh +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/scripts/transcribe.py +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/tools/carve.mjs +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/tools/lib/main-module.mjs +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="id" data-resolution="portrait">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
7
|
+
<script src="captions.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
@font-face { font-family: "Montserrat"; src: url("assets/fonts/Montserrat.ttf"); font-weight: 100 900; }
|
|
10
|
+
@font-face { font-family: "Instrument Serif"; src: url("assets/fonts/InstrumentSerif.ttf"); }
|
|
11
|
+
@font-face { font-family: "Space Mono"; src: url("assets/fonts/SpaceMono.ttf"); font-weight: 700; }
|
|
12
|
+
:root { --ink: #0c0b0a; --cream: #efe6d6; --orange: #f7a93b; --red: #ef4b3a; --dim: #8d867b; }
|
|
13
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: var(--ink); }
|
|
15
|
+
body { font-family: "Montserrat", sans-serif; color: var(--cream); }
|
|
16
|
+
#root { position: relative; width: 100%; height: 100%; overflow: hidden; background: var(--ink); }
|
|
17
|
+
.fill { position: absolute; inset: 0; }
|
|
18
|
+
.serif { font-family: "Instrument Serif", serif; font-weight: 400; }
|
|
19
|
+
.mono { font-family: "Space Mono", monospace; font-weight: 700; }
|
|
20
|
+
.acc { color: var(--orange); }
|
|
21
|
+
|
|
22
|
+
/* Background: near-black with two soft diagonal light streaks that drift. */
|
|
23
|
+
#streaks { inset: -30%; opacity: .9;
|
|
24
|
+
background:
|
|
25
|
+
linear-gradient(115deg, transparent 38%, rgba(239,230,214,.045) 44%, transparent 50%),
|
|
26
|
+
linear-gradient(115deg, transparent 58%, rgba(247,169,59,.05) 62%, transparent 67%); }
|
|
27
|
+
#vignette { background: radial-gradient(ellipse 85% 70% at 50% 45%, transparent 55%, rgba(0,0,0,.55) 100%); }
|
|
28
|
+
|
|
29
|
+
/* The speaker sits in a cream-framed clip card, like a frame pulled from an editor. */
|
|
30
|
+
#pose { position: absolute; left: 190px; top: 470px; width: 700px; height: 930px; }
|
|
31
|
+
#cam { position: absolute; inset: 0; border-radius: 34px; overflow: hidden;
|
|
32
|
+
border: 6px solid var(--cream); background: #1a1816; box-shadow: 0 40px 100px rgba(0,0,0,.75); transform-origin: 50% 50%; }
|
|
33
|
+
#a-roll { width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%; filter: contrast(1.06) saturate(.92); }
|
|
34
|
+
#cam .chip { position: absolute; top: 22px; left: 22px; display: flex; align-items: center; gap: 10px; padding: 6px 14px;
|
|
35
|
+
border-radius: 10px; background: rgba(12,11,10,.72); font-size: 30px; color: var(--orange); }
|
|
36
|
+
#cam .chip b { width: 14px; height: 14px; border-radius: 50%; background: var(--red); }
|
|
37
|
+
#cam .shade { position: absolute; inset: 0; background: #000; opacity: 0; }
|
|
38
|
+
|
|
39
|
+
/* Hook: small serif setup line, then one huge headline. */
|
|
40
|
+
#hook { position: absolute; left: 70px; right: 70px; top: 150px; text-align: center; opacity: 0; }
|
|
41
|
+
#hook .setup { font-size: 64px; line-height: 1; opacity: .9; }
|
|
42
|
+
#hook h1 { margin-top: 8px; white-space: nowrap; font-weight: 900; font-size: 132px; line-height: .95; letter-spacing: -4px; }
|
|
43
|
+
#hook .sub { margin-top: 18px; font-size: 52px; color: var(--orange); }
|
|
44
|
+
|
|
45
|
+
/* Kinetic captions: words pop in as they are spoken; the spoken word is orange. */
|
|
46
|
+
#captions { position: absolute; left: 100px; right: 100px; top: 1450px; height: 250px; }
|
|
47
|
+
.chunk { position: absolute; inset: 0; display: flex; flex-wrap: wrap; justify-content: center; align-content: flex-start; gap: 0 22px; opacity: 0; }
|
|
48
|
+
.w { display: block; font-weight: 900; font-size: 80px; line-height: 1.08; letter-spacing: -2px; text-transform: lowercase;
|
|
49
|
+
color: var(--cream); opacity: 0; text-shadow: 0 6px 24px rgba(0,0,0,.85); }
|
|
50
|
+
|
|
51
|
+
/* Big numeral scene ("title" with "big"): outlined digit that fills up in orange. */
|
|
52
|
+
.scene { position: absolute; inset: 0; opacity: 0; }
|
|
53
|
+
.big { position: absolute; left: 40px; top: 40px; font-weight: 900; font-size: 980px; line-height: .9; letter-spacing: -40px; }
|
|
54
|
+
.big .o { color: transparent; -webkit-text-stroke: 6px var(--orange); }
|
|
55
|
+
.big .f { position: absolute; inset: 0; color: var(--orange); clip-path: inset(100% 0 0 0); }
|
|
56
|
+
.txt { position: absolute; left: 90px; right: 90px; top: 1130px; }
|
|
57
|
+
.txt .k { font-size: 62px; line-height: 1; }
|
|
58
|
+
.txt h1 { white-space: nowrap; font-weight: 900; font-size: 130px; line-height: .95; letter-spacing: -4px; margin-top: 6px; }
|
|
59
|
+
.txt .s { font-size: 64px; color: var(--orange); margin-top: 10px; }
|
|
60
|
+
.scene.center .txt { top: 760px; text-align: center; }
|
|
61
|
+
.scene .frame { width: 900px; height: 560px; margin: 0 auto 40px; border-radius: 30px; overflow: hidden; border: 6px solid var(--cream); }
|
|
62
|
+
.frame img { width: 100%; height: 100%; object-fit: cover; }
|
|
63
|
+
.scene.has-img .txt { top: 330px; }
|
|
64
|
+
|
|
65
|
+
/* Banner scene ("case"): headline on top, orange tape across the greyed clip, optional red stamp. */
|
|
66
|
+
.banner .head { position: absolute; left: 80px; right: 80px; top: 170px; text-align: center; }
|
|
67
|
+
.banner .head .k { font-size: 58px; line-height: 1; }
|
|
68
|
+
.banner .head h2 { font-weight: 900; font-size: 96px; line-height: 1; letter-spacing: -3px; margin-top: 8px; }
|
|
69
|
+
.tape { position: absolute; left: -60px; right: -60px; top: 800px; padding: 26px 80px; background: var(--orange); color: var(--ink);
|
|
70
|
+
transform: rotate(-6deg); text-align: center; font-weight: 900; font-size: 96px; line-height: 1; letter-spacing: -3px;
|
|
71
|
+
box-shadow: 0 0 0 10px var(--red), 0 30px 80px rgba(0,0,0,.7); }
|
|
72
|
+
.tape span { display: inline-block; }
|
|
73
|
+
.banner .stamp { position: absolute; left: 0; right: 0; top: 1130px; margin: auto; width: max-content; padding: 6px 30px;
|
|
74
|
+
border: 9px solid var(--red); color: var(--red); background: rgba(12,11,10,.55); font-weight: 900; font-size: 110px; letter-spacing: 2px; opacity: 0; }
|
|
75
|
+
.banner .s { position: absolute; left: 90px; right: 90px; top: 1480px; text-align: center; font-size: 60px; }
|
|
76
|
+
|
|
77
|
+
/* Photo card: slides in over the clip card's place. */
|
|
78
|
+
.photo { position: absolute; left: 150px; top: 430px; width: 780px; opacity: 0; display: flex; flex-direction: column; align-items: center; }
|
|
79
|
+
.photo .frame { width: 780px; height: 940px; border-radius: 34px; overflow: hidden; border: 6px solid var(--cream); box-shadow: 0 40px 100px rgba(0,0,0,.75); }
|
|
80
|
+
.photo p { margin-top: 26px; padding: 6px 18px; border-radius: 10px; background: rgba(239,230,214,.08); font-size: 34px; color: var(--orange); }
|
|
81
|
+
|
|
82
|
+
/* Steps list ("item"). */
|
|
83
|
+
#list { position: absolute; left: 90px; right: 90px; top: 700px; display: flex; flex-direction: column; gap: 40px; opacity: 0; }
|
|
84
|
+
#list .head { font-size: 66px; color: var(--orange); opacity: 0; }
|
|
85
|
+
.item { display: flex; align-items: center; gap: 30px; opacity: 0; }
|
|
86
|
+
.item b { flex: none; min-width: 120px; padding: 6px 14px; border-radius: 12px; background: var(--orange); color: var(--ink); text-align: center; font-size: 44px; }
|
|
87
|
+
.item span { font-weight: 900; font-size: 74px; line-height: 1; letter-spacing: -2px; }
|
|
88
|
+
|
|
89
|
+
/* Ending: stacked question, a giant orange glyph, a call to comment. */
|
|
90
|
+
.end .txt { top: 300px; text-align: center; }
|
|
91
|
+
.end .txt h1 { font-size: 120px; }
|
|
92
|
+
.end .glyph { position: absolute; left: 0; right: 0; top: 640px; text-align: center; font-weight: 900; font-size: 640px; line-height: 1; color: var(--orange);
|
|
93
|
+
text-shadow: 0 30px 90px rgba(247,169,59,.25); }
|
|
94
|
+
.end .s { position: absolute; left: 0; right: 0; top: 1500px; text-align: center; font-size: 58px; }
|
|
95
|
+
|
|
96
|
+
/* Optional mascot: drop mascot.png in the video's images/ folder. */
|
|
97
|
+
#mascot { position: absolute; left: 36px; bottom: 70px; width: 230px; height: 230px; opacity: 0; }
|
|
98
|
+
#mascot img { width: 100%; height: 100%; object-fit: contain; object-position: center bottom; }
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{DURATION}}" data-width="1080" data-height="1920">
|
|
103
|
+
<div id="streaks" class="fill"></div>
|
|
104
|
+
<div id="pose" data-layout-allow-overflow><div id="cam">
|
|
105
|
+
<video id="a-roll" class="clip" src="{{VIDEO}}" playsinline data-start="0" data-duration="{{VIDEO_DURATION}}" data-track-index="0" data-has-audio="true" data-fx-chain="{"version": 1, "nodes": [{"type": "highpass", "id": "n1", "label": "Remove Rumble", "params": {"frequency": 90, "q": 0.707, "poles": "2"}}, {"type": "compressor", "id": "n2", "label": "Even Out Loudness", "params": {"threshold": -26, "ratio": 3, "attack": 10, "release": 200, "knee": 2.83, "makeup": 6, "mix": 1}}, {"type": "gain", "id": "n3", "label": "Level Up", "params": {"gain": 5}}, {"type": "truepeak", "id": "n4", "label": "Peak Ceiling", "params": {"ceiling": -1.5, "lookahead": 3, "release": 80}}]}"></video>
|
|
106
|
+
<div class="shade"></div>
|
|
107
|
+
<div class="chip mono"><b></b><span id="tc">0:00</span></div>
|
|
108
|
+
</div></div>
|
|
109
|
+
<div id="vignette" class="fill"></div>
|
|
110
|
+
|
|
111
|
+
<div id="hook"><div class="setup serif">{{HOOK_1}}</div><h1>{{HOOK_2}}</h1><div class="sub serif">{{HOOK_SUB}}</div></div>
|
|
112
|
+
<div id="beats"></div>
|
|
113
|
+
<div id="list"></div>
|
|
114
|
+
<div id="captions"></div>
|
|
115
|
+
<div id="mascot"></div>
|
|
116
|
+
|
|
117
|
+
<audio id="music" src="assets/music.wav" data-start="0" data-duration="{{DURATION}}" data-track-index="2" data-volume="{{MUSIC_VOLUME}}"></audio>
|
|
118
|
+
<!-- SFX:BEGIN --><!-- SFX:END -->
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
const END = {{DURATION}}, VEND = {{VIDEO_DURATION}}, HOOK = 3.0, DUR = { title: 1.8, case: 3.2, photo: 2.8 };
|
|
123
|
+
const tl = gsap.timeline({ paused: true });
|
|
124
|
+
const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html) e.innerHTML = html; return e; };
|
|
125
|
+
const esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
126
|
+
// Trailing ? or ! in a headline turns orange, as in the reference ("BASA-BASI?").
|
|
127
|
+
const punch = s => esc(s).replace(/([?!.]+)$/, '<span class="acc">$1</span>');
|
|
128
|
+
// Shrink a headline wider than the safe area.
|
|
129
|
+
const fit = (n, max = 900) => n.querySelectorAll("h1, h2").forEach(h => { const w = h.scrollWidth; if (w > max) h.style.fontSize = Math.floor(parseFloat(getComputedStyle(h).fontSize) * max / w) + "px"; });
|
|
130
|
+
|
|
131
|
+
// Clip card idles with a slight tilt that flips on every jump cut, plus a slow push.
|
|
132
|
+
const REST = i => ({ x: 0, y: 0, rotation: i % 2 ? 1.6 : -1.6, scale: i % 2 ? 1.03 : 1 });
|
|
133
|
+
SEG_STARTS.forEach((s, i) => tl.fromTo("#cam", { ...REST(i) }, { scale: REST(i).scale + 0.02, duration: (SEG_STARTS[i + 1] ?? END) - s, ease: "none", immediateRender: i === 0 }, s));
|
|
134
|
+
tl.fromTo("#streaks", { x: -60 }, { x: 60, duration: END, ease: "none" }, 0);
|
|
135
|
+
// Timecode on the card counts the footage time.
|
|
136
|
+
const tc = document.getElementById("tc");
|
|
137
|
+
for (let i = 0; i <= Math.floor(VEND); i++) tl.call(() => { tc.textContent = `${Math.floor(i / 60)}:${String(i % 60).padStart(2, "0")}`; }, null, i);
|
|
138
|
+
|
|
139
|
+
// Clip card poses while a card is on screen: tucked top-right, greyed in place, or swept off to the left.
|
|
140
|
+
const POSE = {
|
|
141
|
+
aside: { x: 250, y: -330, rotation: 5, scale: 0.42 },
|
|
142
|
+
grey: { scale: 0.96 },
|
|
143
|
+
away: { x: -980, rotation: -10 },
|
|
144
|
+
};
|
|
145
|
+
const pose = (kind, t, d) => {
|
|
146
|
+
tl.to("#pose", { ...POSE[kind], duration: 0.4, ease: "power3.inOut" }, t);
|
|
147
|
+
tl.to("#cam .shade", { opacity: kind === "grey" ? 0.55 : kind === "aside" ? 0.2 : 0, duration: 0.3 }, t);
|
|
148
|
+
if (kind === "grey") tl.to("#a-roll", { filter: "contrast(1.06) saturate(0) blur(3px)", duration: 0.3 }, t);
|
|
149
|
+
if (t + d >= END) return;
|
|
150
|
+
const back = t + d - 0.35;
|
|
151
|
+
tl.to("#pose", { x: 0, y: 0, rotation: 0, scale: 1, duration: 0.35, ease: "power3.inOut" }, back);
|
|
152
|
+
tl.to("#cam .shade", { opacity: 0, duration: 0.3 }, back);
|
|
153
|
+
if (kind === "grey") tl.to("#a-roll", { filter: "contrast(1.06) saturate(.92) blur(0px)", duration: 0.3 }, back);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Hook.
|
|
157
|
+
const hook = document.getElementById("hook");
|
|
158
|
+
hook.querySelector("h1").innerHTML = punch(hook.querySelector("h1").textContent);
|
|
159
|
+
fit(hook);
|
|
160
|
+
tl.fromTo("#hook", { opacity: 0, y: -30 }, { opacity: 1, y: 0, duration: 0.4, ease: "power3.out" }, 0.05);
|
|
161
|
+
tl.fromTo("#hook h1", { scale: 1.25 }, { scale: 1, duration: 0.45, ease: "back.out(1.6)" }, 0.12);
|
|
162
|
+
tl.to("#hook", { opacity: 0, duration: 0.3 }, HOOK - 0.3);
|
|
163
|
+
|
|
164
|
+
const beats = document.getElementById("beats"), list = document.getElementById("list");
|
|
165
|
+
const pic = b => b.img ? `<div class="frame"><img src="assets/img/${esc(b.img)}" alt="" data-layout-allow-overflow></div>` : "";
|
|
166
|
+
const kb = (n, t, d) => { const im = n.querySelector(".frame img"); if (im) tl.fromTo(im, { scale: 1.12 }, { scale: 1, duration: d, ease: "none" }, t); };
|
|
167
|
+
let listStart = null;
|
|
168
|
+
BEATS.forEach((b, i) => {
|
|
169
|
+
const d = b.dur ?? DUR[b.kind];
|
|
170
|
+
if (b.kind === "title") {
|
|
171
|
+
// Keyword scene: card tucks aside; optional giant outlined numeral that fills up in orange.
|
|
172
|
+
const big = b.big ? `<div class="big"><span class="o">${esc(b.big)}</span><span class="f">${esc(b.big)}</span></div>` : "";
|
|
173
|
+
const n = el("div", "scene" + (b.big ? "" : " center") + (b.img ? " has-img" : ""), big +
|
|
174
|
+
`<div class="txt">${pic(b)}${b.kicker ? `<div class="k serif">${esc(b.kicker)}</div>` : ""}<h1>${punch(b.title)}</h1>` +
|
|
175
|
+
(b.sub ? `<div class="s serif">${esc(b.sub)}</div>` : "") + "</div>");
|
|
176
|
+
beats.append(n); fit(n); pose("aside", b.t, d); kb(n, b.t, d);
|
|
177
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
178
|
+
if (b.big) {
|
|
179
|
+
tl.fromTo(n.querySelector(".big"), { opacity: 0, y: 80 }, { opacity: 1, y: 0, duration: 0.35, ease: "power3.out" }, b.t);
|
|
180
|
+
tl.fromTo(n.querySelector(".big .f"), { clipPath: "inset(100% 0 0 0)" }, { clipPath: "inset(0% 0 0 0)", duration: Math.min(1.2, d * 0.6), ease: "power2.inOut" }, b.t + 0.3);
|
|
181
|
+
}
|
|
182
|
+
const parts = n.querySelectorAll(".txt > *");
|
|
183
|
+
parts.forEach((p, j) => tl.fromTo(p, { opacity: 0, y: 40 }, { opacity: 1, y: 0, duration: 0.3, ease: "power3.out" }, b.t + 0.12 + j * 0.22));
|
|
184
|
+
tl.to(n, { opacity: 0, duration: 0.25 }, b.t + d - 0.25);
|
|
185
|
+
} else if (b.kind === "case") {
|
|
186
|
+
// Banner scene: headline on top, orange tape across the greyed clip card, red stamp slammed on.
|
|
187
|
+
const n = el("div", "scene banner",
|
|
188
|
+
`<div class="head">${b.kicker ? `<div class="k serif">${esc(b.kicker)}</div>` : ""}${b.sub ? `<h2>${punch(b.sub)}</h2>` : ""}</div>` +
|
|
189
|
+
`<div class="tape" data-layout-allow-overflow>${esc(b.title).split(" ").map(w => `<span>${w}</span>`).join(" ")}</div>` +
|
|
190
|
+
(b.stamp ? `<div class="stamp" data-layout-allow-overlap>${esc(b.stamp)}</div>` : "") +
|
|
191
|
+
(b.note ? `<div class="s serif">${esc(b.note)}</div>` : ""));
|
|
192
|
+
beats.append(n); fit(n); pose("grey", b.t, d);
|
|
193
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
194
|
+
tl.fromTo(n.querySelector(".head"), { opacity: 0, y: -40 }, { opacity: 1, y: 0, duration: 0.3, ease: "power3.out" }, b.t + 0.05);
|
|
195
|
+
tl.fromTo(n.querySelector(".tape"), { xPercent: -110 }, { xPercent: 0, duration: 0.35, ease: "power4.out" }, b.t + 0.25);
|
|
196
|
+
tl.fromTo(n.querySelectorAll(".tape span"), { scale: 1.4, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.2, stagger: 0.07, ease: "back.out(2)" }, b.t + 0.4);
|
|
197
|
+
const st = n.querySelector(".stamp");
|
|
198
|
+
if (st) tl.fromTo(st, { opacity: 0, scale: 2.4, rotation: -20 }, { opacity: 1, scale: 1, rotation: -8, duration: 0.22, ease: "power4.in" }, b.t + 1.12 - 0.22);
|
|
199
|
+
const s = n.querySelector(".s");
|
|
200
|
+
if (s) tl.fromTo(s, { opacity: 0 }, { opacity: 1, duration: 0.3 }, b.t + 0.6);
|
|
201
|
+
tl.to(n, { opacity: 0, duration: 0.3 }, b.t + d - 0.3);
|
|
202
|
+
} else if (b.kind === "photo") {
|
|
203
|
+
// Photo card sweeps the clip card off and takes its place.
|
|
204
|
+
const n = el("div", "photo", pic(b) + (b.label ? `<p class="mono">${esc(b.label)}</p>` : ""));
|
|
205
|
+
beats.append(n); pose("away", b.t, d); kb(n, b.t, d);
|
|
206
|
+
tl.fromTo(n, { opacity: 0, x: 900, rotation: 8 }, { opacity: 1, x: 0, rotation: 1.5, duration: 0.45, ease: "power3.out" }, b.t + 0.05);
|
|
207
|
+
if (b.t + d < END) tl.to(n, { opacity: 0, x: 900, rotation: 8, duration: 0.35, ease: "power3.in" }, b.t + d - 0.35);
|
|
208
|
+
} else if (b.kind === "item") {
|
|
209
|
+
// Steps: orange mono chip plus a bold line, popped in on the word.
|
|
210
|
+
if (listStart === null) { listStart = b.t; if (b.head) { const h = el("div", "head serif"); h.textContent = b.head; list.append(h); } }
|
|
211
|
+
const k = list.querySelectorAll(".item").length + 1;
|
|
212
|
+
const n = el("div", "item", `<b class="mono">${esc(b.label ?? String(k).padStart(2, "0"))}</b><span>${punch(b.text)}</span>`);
|
|
213
|
+
list.append(n);
|
|
214
|
+
tl.fromTo(n, { opacity: 0, x: -60 }, { opacity: 1, x: 0, duration: 0.3, ease: "back.out(1.8)" }, b.t);
|
|
215
|
+
tl.fromTo(n.querySelector("b"), { scale: 1.5 }, { scale: 1, duration: 0.25, ease: "power3.out" }, b.t);
|
|
216
|
+
} else if (b.kind === "mascot") {
|
|
217
|
+
// Optional corner mascot from the video's images/, shown from its word on.
|
|
218
|
+
const m = document.getElementById("mascot"); m.innerHTML = `<img src="assets/img/${esc(b.img)}" alt="">`;
|
|
219
|
+
tl.fromTo(m, { opacity: 0, y: 120 }, { opacity: 1, y: 0, duration: 0.4, ease: "back.out(1.8)" }, b.t);
|
|
220
|
+
tl.fromTo(m.firstChild, { y: 0 }, { y: -10, duration: 0.6, ease: "sine.inOut", yoyo: true, repeat: Math.max(0, Math.floor((END - b.t) / 0.6) - 1) }, b.t + 0.4);
|
|
221
|
+
} else if (b.kind === "end") {
|
|
222
|
+
// Ending: the question stacks in, a giant orange glyph bounces in on the hit, then a call to comment.
|
|
223
|
+
const n = el("div", "scene end", `<div class="txt">${String(b.title).split("\n").map(l => `<h1>${punch(l)}</h1>`).join("")}</div>` +
|
|
224
|
+
`<div class="glyph">${esc(b.big ?? "?")}</div>` + (b.sub ? `<div class="s serif">${esc(b.sub)}</div>` : ""));
|
|
225
|
+
beats.append(n); fit(n); pose("aside", b.t, END - b.t);
|
|
226
|
+
tl.to("#pose", { opacity: 0, duration: 0.4 }, b.t + 0.2);
|
|
227
|
+
tl.set(n, { opacity: 1 }, b.t);
|
|
228
|
+
n.querySelectorAll(".txt h1").forEach((h, j) => tl.fromTo(h, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 0.3, ease: "power3.out" }, b.t + j * 0.18));
|
|
229
|
+
const land = b.t + 0.54; // the ending sting's hit
|
|
230
|
+
const g = n.querySelector(".glyph");
|
|
231
|
+
tl.fromTo(g, { opacity: 0, scale: 0.2, rotation: -20 }, { opacity: 1, scale: 1, rotation: 0, duration: 0.5, ease: "back.out(2.2)" }, land - 0.2);
|
|
232
|
+
tl.to(g, { rotation: 6, duration: 0.5, ease: "sine.inOut", yoyo: true, repeat: Math.max(0, Math.floor((END - land - 0.3) / 0.5)) }, land + 0.3);
|
|
233
|
+
const s = n.querySelector(".s");
|
|
234
|
+
if (s) tl.fromTo(s, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.4 }, land + 0.4);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
// The steps leave at the first jump cut after the last item (max 6 s later).
|
|
238
|
+
const items = BEATS.filter(b => b.kind === "item"), lastItem = items.length ? items[items.length - 1].t : 0;
|
|
239
|
+
const listEnd = Math.min(SEG_STARTS.find(s => s > lastItem + 1) ?? END, lastItem + 6);
|
|
240
|
+
if (items.length) {
|
|
241
|
+
const a = listStart - 0.4;
|
|
242
|
+
pose("aside", a, listEnd - a);
|
|
243
|
+
tl.fromTo("#list", { opacity: 0 }, { opacity: 1, duration: 0.25 }, a);
|
|
244
|
+
const h = list.querySelector(".head");
|
|
245
|
+
if (h) tl.fromTo(h, { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.3 }, a);
|
|
246
|
+
tl.to("#list", { opacity: 0, duration: 0.3 }, listEnd - 0.3);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Captions: hidden while a scene owns the screen (photo cards keep them).
|
|
250
|
+
const busy = BEATS.filter(b => !["item", "photo", "mascot"].includes(b.kind)).map(b => [b.t, b.kind === "end" ? END : b.t + (b.dur ?? DUR[b.kind])])
|
|
251
|
+
.concat(items.length ? [[listStart - 0.4, listEnd]] : []).concat([[0, HOOK]]);
|
|
252
|
+
const cap = document.getElementById("captions");
|
|
253
|
+
CAPTIONS.forEach(chunk => {
|
|
254
|
+
const start = chunk[0].s, end = chunk[chunk.length - 1].hold;
|
|
255
|
+
if (busy.some(([a, b]) => start < b && end > a)) return;
|
|
256
|
+
const n = el("div", "chunk"); cap.append(n);
|
|
257
|
+
tl.set(n, { opacity: 1 }, start);
|
|
258
|
+
tl.set(n, { opacity: 0 }, end);
|
|
259
|
+
chunk.forEach((w, j) => {
|
|
260
|
+
const s = el("span", "w"); s.textContent = w.w.replace(/[,.]+$/, ""); n.append(s);
|
|
261
|
+
const until = chunk[j + 1] ? chunk[j + 1].s : end;
|
|
262
|
+
const t0 = Math.max(start, w.s - 1 / 30);
|
|
263
|
+
tl.fromTo(s, { opacity: 0, y: 30, scale: 0.7 }, { opacity: 1, y: 0, scale: 1, duration: 0.16, ease: "back.out(2.4)" }, t0);
|
|
264
|
+
tl.set(s, { color: "#f7a93b" }, t0);
|
|
265
|
+
tl.set(s, { color: "#efe6d6" }, until);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
window.__timelines["main"] = tl;
|
|
270
|
+
</script>
|
|
271
|
+
</body>
|
|
272
|
+
</html>
|
|
@@ -52,3 +52,25 @@ To ship it to everyone who installs kaizen, copy the folder into this skill's `p
|
|
|
52
52
|
repository. Move any music or font another preset could use into the shared `assets/`, credit it in
|
|
53
53
|
`assets/CREDITS.txt`, and keep only the preset's own files in its folder: every byte here goes into the
|
|
54
54
|
kaizen package.
|
|
55
|
+
|
|
56
|
+
# Film presets (from scratch)
|
|
57
|
+
|
|
58
|
+
`make_film.py` finds a film preset by the folder holding `style.json` (same three places as above), so the two kinds never collide. Bundled: `film-cinematic-drama`, `film-promo-booth`, `film-fun-energetic`, `film-viral-social`. Each works in 9:16 and 16:9; the `window` and `card` layouts fall back to full-bleed in 9:16.
|
|
59
|
+
| preset | use for | look & motion | audio | captions | loudness |
|
|
60
|
+
|---|---|---|---|---|---|
|
|
61
|
+
| cinematic-drama | pitch opener, brand film | dark, full-bleed photos, slow push-in, rise text, grain + vignette, fades | quiet music, hit on reveal, riser into the question, room tone | subtitle bar | −16 |
|
|
62
|
+
| promo-booth | exhibition loop, deck-faithful promo (the friend's style) | photo in a framed window beside the text, wipes, slide-in text, staged builds | whoosh on cuts, tick on titles, slightly louder music | subtitle bar | −16 |
|
|
63
|
+
| fun-energetic | social posts, parents, events | cream background, rounded photo cards that bounce in, pop text, light leaks | bright track (Carefree), pops and a chime | subtitle bar | −14 |
|
|
64
|
+
| viral-social | TikTok / Reels / Shorts (9:16) | full-bleed, fast punch-in cuts, bold Inter, strong Ken Burns | Easy Lemon-type track, whooshes | kinetic 2–3 words, accent on the spoken word | −14 |
|
|
65
|
+
|
|
66
|
+
Pace: `lead` / `tail` = silence before and after each line. Drama 0.8/1.1, booth 0.45/0.7, fun 0.2/0.35, viral 0.05/0.2.
|
|
67
|
+
|
|
68
|
+
## Writing for each preset
|
|
69
|
+
- **drama**: full story arc, 70–110 s, long pauses allowed.
|
|
70
|
+
- **booth**: the deck's beats in order, 80–100 s, must make sense when joined mid-loop; real contacts on the CTA.
|
|
71
|
+
- **fun**: 35–50 s, playful lines ("Kenal rasanya?"), one problem montage, three quick benefits.
|
|
72
|
+
- **viral**: ≤ 30 s, line 1 is the hook (a sharp, relatable pain), product by second ~12, one CTA.
|
|
73
|
+
|
|
74
|
+
## Adding a preset
|
|
75
|
+
Copy a `film-*` folder to `presets/<name>/` in the project and edit its `style.json` (no `index.html`: `film.html` is shared); fields: palette, fonts (any font file in `assets/film-fonts/` or the preset's `assets/fonts/`, named like `PlayfairDisplay.ttf`), pace, text (rise|slide|pop), transition (fade|wipe|punch),
|
|
76
|
+
layout (fullbleed|window|card), grade, captions (subtitle|kinetic|none), music levels, sfx names, kenburns, loudness.
|
|
@@ -7,8 +7,8 @@ Learned by comparing deck-to-video renders against richer productions . The exam
|
|
|
7
7
|
"140 pesan", `keyboard`, `bell`), soft whoosh/tick on cuts, a hit on the reveal, a riser into the
|
|
8
8
|
question. Music ducks under speech and only lifts at the reveal — never a dramatic swell unless
|
|
9
9
|
the preset is drama.
|
|
10
|
-
2. **Captions**: always. Projectors and phones are often muted. Subtitle bar
|
|
11
|
-
2–3-word captions
|
|
10
|
+
2. **Captions**: always. Projectors and phones are often muted. Subtitle bar (cinematic, booth, fun) or kinetic
|
|
11
|
+
2–3-word captions (viral), in either shape. Captions come from `words.json`, so they follow the voice exactly.
|
|
12
12
|
3. **Staged builds inside a beat**: numbers count up, list items arrive one by one with the voice,
|
|
13
13
|
triptych panels arrive in turn. A whole slide appearing at once reads as a slideshow.
|
|
14
14
|
4. **Directed voice**: emotion tag per line, low and slow through the problem, lift at the question,
|
|
@@ -5,21 +5,21 @@ Clients revise. Make every revision cheap, traceable and reversible.
|
|
|
5
5
|
## Classify the request, then touch only that layer
|
|
6
6
|
| request | what changes | redo |
|
|
7
7
|
|---|---|---|
|
|
8
|
-
| "change this sentence / say it warmer / mispronounced X" | `lines.json` entry | `vo.py <film-dir> "" v04` (only that key; empty voice = use film.json), then
|
|
9
|
-
| "different text on screen" | beat `kick/title/sub/items` in `film.json` | master |
|
|
10
|
-
| "swap this picture" | beat `img` (+ intake for new photos) |
|
|
11
|
-
| "slower / faster / shorter" | preset `pace`, beat `hold`, drop or merge beats |
|
|
12
|
-
| "music too loud / different mood" | preset `music.base/lift` or `music.sh` new track |
|
|
13
|
-
| "other colours / logo / font" | `film.brand`, `logo` |
|
|
8
|
+
| "change this sentence / say it warmer / mispronounced X" | `lines.json` entry | `vo.py <film-dir> "" v04` (only that key; empty voice = use film.json), then build and render |
|
|
9
|
+
| "different text on screen" | beat `kick/title/sub/items` in `film.json` | build + master |
|
|
10
|
+
| "swap this picture" | beat `img` (+ intake for new photos) | build + render |
|
|
11
|
+
| "slower / faster / shorter" | preset `pace`, beat `hold`, drop or merge beats | build + render |
|
|
12
|
+
| "music too loud / different mood" | preset `music.base/lift` or `music.sh` new track | build + render |
|
|
13
|
+
| "other colours / logo / font" | `film.brand`, `logo` | build + render |
|
|
14
14
|
| "make a vertical / 15 s version" | new film entry with its own (shorter) script | vo + master |
|
|
15
15
|
| "different style" | `preset` (and usually a rewritten script) | vo + master |
|
|
16
16
|
|
|
17
17
|
Never regenerate all narration for a one-line change: voices vary between takes and credits cost money.
|
|
18
18
|
|
|
19
19
|
## Versioning
|
|
20
|
-
- `master.sh` moves the previous delivery to `
|
|
21
|
-
- Append to
|
|
22
|
-
- When a revision is rejected, restore from `
|
|
20
|
+
- `master.sh` moves the previous delivery to `output/<name>/<preset>/previous/<name>-<timestamp>.*` before writing the new one.
|
|
21
|
+
- Append to `videos/<name>/REVISIONS.md`: date, the user's words verbatim, what changed, output file.
|
|
22
|
+
- When a revision is rejected, restore from `previous/` instead of re-rendering.
|
|
23
23
|
|
|
24
24
|
## Before re-delivering
|
|
25
25
|
Re-check stills of the beats that changed, master, and say exactly what changed and what did not.
|
|
Binary file
|
|
@@ -11,4 +11,12 @@ need npx "comes with Node.js"
|
|
|
11
11
|
need python3 "Python 3.9+"
|
|
12
12
|
command -v uv >/dev/null 2>&1 && echo "ok uv" || { echo "MISSING uv — https://docs.astral.sh/uv/ (installs Python 3.11 for WhisperX)"; miss=1; }
|
|
13
13
|
[ -x "$HOME_DIR/whisperx-venv/bin/python" ] && echo "ok WhisperX ($HOME_DIR/whisperx-venv)" || echo "todo WhisperX — run setup.sh (about 2 GB of packages, plus a 3 GB model on the first transcription)"
|
|
14
|
+
# From-scratch films (make_film.py) need these too; WhisperX is footage-only.
|
|
15
|
+
opt(){ command -v "$1" >/dev/null 2>&1 && echo "ok $1" || echo "opt $1 — $2"; }
|
|
16
|
+
opt soffice "LibreOffice — only for importing .pptx decks"
|
|
17
|
+
opt pdftoppm "poppler-utils — only for importing decks"
|
|
18
|
+
python3 -c "import PIL" 2>/dev/null && echo "ok Pillow" || echo "opt Pillow — pip install pillow (from-scratch: intake.py)"
|
|
19
|
+
python3 -c "import edge_tts" 2>/dev/null && echo "ok edge-tts (free voices)" || echo "opt edge-tts — pip install edge-tts (from-scratch voice without an ElevenLabs key)"
|
|
20
|
+
python3 -c "import qrcode" 2>/dev/null && echo "ok qrcode" || echo "opt qrcode — pip install 'qrcode[pil]' (QR on the CTA)"
|
|
21
|
+
[ -n "${ELEVENLABS_API_KEY:-}" ] && echo "ok ELEVENLABS_API_KEY set" || echo "info no ELEVENLABS_API_KEY — from-scratch uses edge-tts and synthesized sound effects"
|
|
14
22
|
exit $miss
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Turn whatever the user brought into film-ready assets.
|
|
2
2
|
Usage: python3 scripts/intake.py <film-dir> [input_dir] (default input_dir: <film-dir>/inputs)
|
|
3
3
|
<input_dir> may contain, in any mix:
|
|
4
|
-
photos/ story photos -> <name>-16x9.jpg
|
|
4
|
+
photos/ story photos -> <name>-16x9.jpg, -9x16.jpg, -4x5.jpg (cropped, light grade)
|
|
5
5
|
screens/ app/web screenshots -> copied untouched; ratio recorded (use kind "shot")
|
|
6
6
|
footage/ video clips (.mp4/.mov) -> <name>.mp4 (H.264, no audio, 1080p max)
|
|
7
7
|
logo*.png logo(s) -> copied; dominant colours suggested for brand.palette
|
|
@@ -36,7 +36,7 @@ def grade(im):
|
|
|
36
36
|
for f in sorted((src / "photos").glob("*")) if (src / "photos").exists() else []:
|
|
37
37
|
if f.suffix.lower() not in IMG: continue
|
|
38
38
|
im = Image.open(f).convert("RGB")
|
|
39
|
-
for tag, size in (("16x9", (1920, 1080)), ("9x16", (1080, 1920))):
|
|
39
|
+
for tag, size in (("16x9", (1920, 1080)), ("9x16", (1080, 1920)), ("4x5", (1080, 1350))):
|
|
40
40
|
grade(crop(im, *size)).save(out / f"{f.stem}-{tag}.jpg", quality=88)
|
|
41
41
|
manifest["photos"].append(f.stem)
|
|
42
42
|
for f in sorted((src / "screens").glob("*")) if (src / "screens").exists() else []:
|