kaizen-agent 1.47.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 +111 -84
- package/cli/build-web.ts +4 -2
- package/cli/hermes.ts +65 -0
- package/cli/install.ts +24 -10
- 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/cli/worktree.test.ts +3 -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/media/kaizen-video/assets/CREDITS.txt +16 -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/assets/fonts/Fredoka.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/Montserrat.ttf +0 -0
- package/skills/media/kaizen-video/assets/fonts/SpecialElite.ttf +0 -0
- package/skills/media/kaizen-video/assets/music/investigations.mp3 +0 -0
- package/skills/media/kaizen-video/assets/music/lights.mp3 +0 -0
- package/skills/media/kaizen-video/assets/music/sunny.mp3 +0 -0
- package/skills/media/kaizen-video/examples/config.json +152 -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/hyperframes.json +12 -0
- package/skills/media/kaizen-video/presets/educational-warning/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/educational-warning/README.md +26 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/educational-warning/index.html +236 -0
- package/skills/media/kaizen-video/presets/educational-warning/package.json +10 -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-cheerful/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/README.md +35 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/index.html +260 -0
- package/skills/media/kaizen-video/presets/tiktok-cheerful/package.json +10 -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/media/kaizen-video/presets/tiktok-mystery/CREDITS.txt +12 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/README.md +51 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/index.html +260 -0
- package/skills/media/kaizen-video/presets/tiktok-mystery/package.json +10 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/CREDITS.txt +8 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/README.md +24 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/index.html +266 -0
- package/skills/media/kaizen-video/presets/tiktok-scam-alert/package.json +10 -0
- package/skills/media/kaizen-video/references/presets.md +76 -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/media/kaizen-video/scripts/check.sh +22 -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/media/kaizen-video/scripts/make_tiktok.py +213 -0
- package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
- package/skills/media/kaizen-video/scripts/render_cut.py +88 -0
- package/skills/media/kaizen-video/scripts/setup.sh +14 -0
- 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/skills/media/kaizen-video/scripts/transcribe.py +45 -0
- package/skills/media/kaizen-video/tools/carve.mjs +560 -0
- package/skills/media/kaizen-video/tools/lib/main-module.mjs +13 -0
- package/web/board/views.ts +1 -1
- package/web/board.html +3 -1
- package/web/board.js +2 -2
- package/web/notebook.js +1 -1
- package/web/notes.html +2 -0
- package/web/term.js +38 -36
- package/web/term.ts +43 -10
- package/web/video.html +298 -0
- package/web/video.js +81 -0
- package/web/video.ts +610 -0
- 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/{story-video → media/kaizen-video}/examples/sample-film/lines.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/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "film-horor-cina",
|
|
3
|
+
"preset": "tiktok-cheerful",
|
|
4
|
+
"video": "work/cut.mp4",
|
|
5
|
+
"words": "transcript/words.json",
|
|
6
|
+
"plan": "transcript/render-plan.json",
|
|
7
|
+
"fix": {
|
|
8
|
+
"Mainan": "Mainland",
|
|
9
|
+
"role": "lore",
|
|
10
|
+
"horror": "horor"
|
|
11
|
+
},
|
|
12
|
+
"cold_open": {
|
|
13
|
+
"from": "film",
|
|
14
|
+
"after": 6,
|
|
15
|
+
"to": "hantunya"
|
|
16
|
+
},
|
|
17
|
+
"hook": {
|
|
18
|
+
"line1": "FILM HOROR",
|
|
19
|
+
"line2": "TANPA HANTU?",
|
|
20
|
+
"sub": "Kenapa dilarang di negara ini?"
|
|
21
|
+
},
|
|
22
|
+
"music": "sunny",
|
|
23
|
+
"music_volume": 0.30,
|
|
24
|
+
"beats": [
|
|
25
|
+
{
|
|
26
|
+
"kind": "title",
|
|
27
|
+
"word": "cina",
|
|
28
|
+
"img": "tiananmen.jpg",
|
|
29
|
+
"title": "CINA",
|
|
30
|
+
"sub": "Negara yang membatasi film horor"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"kind": "case",
|
|
34
|
+
"word": "vampir",
|
|
35
|
+
"img": "jiangshi.jpg",
|
|
36
|
+
"kicker": "BUKTI #1",
|
|
37
|
+
"title": "JIANGSHI",
|
|
38
|
+
"sub": "Vampir lompat-lompat — produksi Hongkong, bukan Cina daratan"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"kind": "photo",
|
|
42
|
+
"word": "hongkong",
|
|
43
|
+
"img": "hongkong.jpg",
|
|
44
|
+
"label": "Hongkong — bukan China Mainland"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"kind": "title",
|
|
48
|
+
"word": "rrc",
|
|
49
|
+
"title": "RRC",
|
|
50
|
+
"sub": "China Mainland"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"kind": "case",
|
|
54
|
+
"word": "dilarang",
|
|
55
|
+
"kicker": "ARSIP PEMERINTAH",
|
|
56
|
+
"title": "FILM TAHAYUL",
|
|
57
|
+
"stamp": "DILARANG"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"kind": "photo",
|
|
61
|
+
"word": "film",
|
|
62
|
+
"after": 50,
|
|
63
|
+
"img": "projector.jpg",
|
|
64
|
+
"label": "Film & media"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"kind": "title",
|
|
68
|
+
"word": "rasionalitas",
|
|
69
|
+
"title": "RASIONAL",
|
|
70
|
+
"sub": "Mistis & goib tidak boleh tampil"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"kind": "photo",
|
|
74
|
+
"word": "mistis",
|
|
75
|
+
"img": "ghosts.jpg",
|
|
76
|
+
"label": "Lukisan hantu, abad ke-18"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"kind": "title",
|
|
80
|
+
"word": "pengecualian",
|
|
81
|
+
"title": "PENGECUALIAN",
|
|
82
|
+
"sub": "Hantu boleh muncul... dengan syarat"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"kind": "case",
|
|
86
|
+
"word": "ancient",
|
|
87
|
+
"img": "mask.jpg",
|
|
88
|
+
"kicker": "SYARAT",
|
|
89
|
+
"title": "MASA LAMPAU",
|
|
90
|
+
"sub": "Setting kuno atau cerita rakyat — bukan era modern"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"kind": "title",
|
|
94
|
+
"word": "plot",
|
|
95
|
+
"title": "PLOT TWIST",
|
|
96
|
+
"sub": "Selalu ada penjelasan logis"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"kind": "item",
|
|
100
|
+
"word": "jiwa",
|
|
101
|
+
"text": "Sakit jiwa"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"kind": "item",
|
|
105
|
+
"word": "nyamar",
|
|
106
|
+
"text": "Orang menyamar"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"kind": "item",
|
|
110
|
+
"word": "mutasi",
|
|
111
|
+
"text": "Mutasi genetik"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"kind": "end",
|
|
115
|
+
"word": "bukan",
|
|
116
|
+
"after": 105,
|
|
117
|
+
"title": "PASTI BUKAN HANTU"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"reactions": [
|
|
121
|
+
{
|
|
122
|
+
"sfx": "pop",
|
|
123
|
+
"word": "negara",
|
|
124
|
+
"after": 10,
|
|
125
|
+
"offset": -0.1,
|
|
126
|
+
"volume": 0.6,
|
|
127
|
+
"note": "Negara apakah itu?"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"sfx": "boing",
|
|
131
|
+
"word": "kenapa",
|
|
132
|
+
"after": 50,
|
|
133
|
+
"volume": 0.55,
|
|
134
|
+
"note": "Kenapa kayak gitu?"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"sfx": "chime",
|
|
138
|
+
"word": "film",
|
|
139
|
+
"after": 50,
|
|
140
|
+
"volume": 0.55,
|
|
141
|
+
"note": "film, media"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"sfx": "whoosh",
|
|
145
|
+
"word": "plot",
|
|
146
|
+
"offset": -0.35,
|
|
147
|
+
"volume": 0.55,
|
|
148
|
+
"note": "plot twist"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"tail": 2.5
|
|
152
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Sample film (fictional product, no media needed)
|
|
2
|
+
Runs with the free voice and synthesized sound effects. Run from a project folder:
|
|
3
|
+
```bash
|
|
4
|
+
S=<skill>/scripts
|
|
5
|
+
mkdir -p videos/sample-film && cp <skill>/examples/sample-film/{film.json,lines.json} videos/sample-film/
|
|
6
|
+
python3 $S/vo.py videos/sample-film # needs: pip install edge-tts
|
|
7
|
+
python3 $S/make_film.py videos/sample-film --render
|
|
8
|
+
```
|
|
9
|
+
Music comes from `"music": "investigations"` (bundled). Switch shape with `"format": "16x9"` or `"9x16"`.
|
|
10
|
+
The statistic is a placeholder: real films only show sourced numbers.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sample-film",
|
|
3
|
+
"preset": "film-cinematic-drama",
|
|
4
|
+
"language": "en",
|
|
5
|
+
"voiceProvider": "edge",
|
|
6
|
+
"edgeVoice": "en-US-AriaNeural",
|
|
7
|
+
"brand": {
|
|
8
|
+
"palette": {
|
|
9
|
+
"accent": "#D9A441",
|
|
10
|
+
"accentBg": "#D9A441"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"labels": {
|
|
14
|
+
"source": "Source",
|
|
15
|
+
"next": "Next step"
|
|
16
|
+
},
|
|
17
|
+
"cta": {
|
|
18
|
+
"title": "See your own numbers in 20 minutes.",
|
|
19
|
+
"sub": "Book a demo",
|
|
20
|
+
"url": "[website]"
|
|
21
|
+
},
|
|
22
|
+
"beats": [
|
|
23
|
+
{
|
|
24
|
+
"vo": "v01",
|
|
25
|
+
"kind": "black",
|
|
26
|
+
"title": "Every caf\u00e9 closes at ten.",
|
|
27
|
+
"sub": "The real work starts after.",
|
|
28
|
+
"ambience": "amb-night"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"vo": "v02",
|
|
32
|
+
"kind": "stat",
|
|
33
|
+
"kick": "The hidden shift",
|
|
34
|
+
"stat": [
|
|
35
|
+
{
|
|
36
|
+
"value": 4,
|
|
37
|
+
"suffix": "+ hours",
|
|
38
|
+
"label": "a week reconciling receipts (example figure \u2014 replace with a sourced one)",
|
|
39
|
+
"source": "[source]"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"vo": "v03",
|
|
45
|
+
"kind": "question",
|
|
46
|
+
"title": "What if the books closed themselves?"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"vo": "v04",
|
|
50
|
+
"kind": "reveal",
|
|
51
|
+
"title": "Ledgerly",
|
|
52
|
+
"sub": "Bookkeeping that closes with the caf\u00e9.",
|
|
53
|
+
"line": "Invoices \u00b7 Cash \u00b7 Reports"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"vo": "v05",
|
|
57
|
+
"kind": "cta",
|
|
58
|
+
"hold": 2.5
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"format": "16x9",
|
|
62
|
+
"music": "investigations"
|
|
63
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{LANG}}" data-resolution="{{RES}}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width={{W}}, height={{H}}" />
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
7
|
+
<script src="film-data.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
/*FONTS*/
|
|
10
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
+
html, body { width: {{W}}px; height: {{H}}px; overflow: hidden; }
|
|
12
|
+
#root { position: relative; width: 100%; height: 100%; overflow: hidden; }
|
|
13
|
+
.fill { position: absolute; inset: 0; display: flex; flex-direction: column; }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{DURATION}}" data-width="{{W}}" data-height="{{H}}">
|
|
18
|
+
<div id="beats" class="fill"></div>
|
|
19
|
+
<div id="grade" class="fill" style="pointer-events: none"></div>
|
|
20
|
+
<div id="captions" class="fill" style="pointer-events: none"></div>
|
|
21
|
+
<!-- AUDIO -->
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
// One film = FILM (film-data.js, written by make_film.py): the style, the beats with their absolute start/len
|
|
26
|
+
// in seconds, and the caption groups. One function per beat kind builds its DOM and its tweens.
|
|
27
|
+
const F = FILM, S = F.style, P = S.palette, FD = S.fonts, W = F.W, H = F.H, portrait = H > W, short = portrait && H < W * 1.5; // short: 4:5, a portrait frame 1350 tall
|
|
28
|
+
const pad = portrait ? 80 : 140, lead = S.pace.lead, f = n => n / 30;
|
|
29
|
+
const isInter = FD.display === "Inter", light = P.bg.toUpperCase() > "#C";
|
|
30
|
+
const kinetic = S.captions === "kinetic"; // big captions take more of the bottom edge
|
|
31
|
+
const tl = gsap.timeline({ paused: true });
|
|
32
|
+
const $ = (css, html = "", parent) => { const e = document.createElement("div"); Object.assign(e.style, css); e.innerHTML = html; if (parent) parent.append(e); return e; };
|
|
33
|
+
const FILL = { position: "absolute", inset: 0, display: "flex", flexDirection: "column" };
|
|
34
|
+
const body = (size, color = P.muted, extra = {}) => ({ fontFamily: `"${FD.body}"`, fontSize: size + "px", color, ...extra });
|
|
35
|
+
|
|
36
|
+
// Text entrance by style: rise | slide | pop. `at` is seconds after the beat starts.
|
|
37
|
+
const enter = (b, el, at, origin = "left center") => {
|
|
38
|
+
const t = b.start + at, pop = S.text === "pop";
|
|
39
|
+
const from = { opacity: 0, ...(S.text === "rise" ? { y: 26 } : S.text === "slide" ? { x: -50 } : { scale: 0.6 }) };
|
|
40
|
+
gsap.set(el, { transformOrigin: origin });
|
|
41
|
+
tl.fromTo(el, from, { opacity: 1, x: 0, y: 0, scale: 1, duration: pop ? 0.4 : 0.5, ease: pop ? "back.out(2.2)" : "power2.out" }, t);
|
|
42
|
+
};
|
|
43
|
+
const kick = (b, text, at, parent) => { const e = $({ ...body(22, P.accent), fontWeight: 700, letterSpacing: "9px", textTransform: "uppercase" }, text, parent); enter(b, e, at); return e; };
|
|
44
|
+
const title = (b, text, at, size, o = {}, parent) => {
|
|
45
|
+
const e = $({ fontFamily: `"${FD.display}"`, fontWeight: isInter ? 900 : 400, fontStyle: o.italic ? "italic" : "normal", fontSize: size + "px",
|
|
46
|
+
lineHeight: 1.08, color: o.color || P.fg, letterSpacing: isInter ? "-1px" : "0", textAlign: o.center ? "center" : "left" }, text, parent);
|
|
47
|
+
enter(b, e, at, o.center ? "center" : "left center"); return e;
|
|
48
|
+
};
|
|
49
|
+
const italic = !isInter;
|
|
50
|
+
const textBlock = (b, parent, maxW, size = 88) => {
|
|
51
|
+
const box = $({ maxWidth: maxW + "px" }, "", parent);
|
|
52
|
+
if (b.kick) kick(b, b.kick, lead, box);
|
|
53
|
+
if (b.title) title(b, b.title, lead + 0.2, size, {}, box).style.marginTop = "22px";
|
|
54
|
+
if (b.sub) { const s = $(body(30, P.muted, { lineHeight: 1.4, marginTop: "28px" }), b.sub, box); enter(b, s, lead + 1.0); }
|
|
55
|
+
return box;
|
|
56
|
+
};
|
|
57
|
+
const photo = (b, parent, amount, style = {}) => {
|
|
58
|
+
const box = $({ ...FILL, overflow: "hidden", ...style }, "", parent);
|
|
59
|
+
const im = document.createElement("img"); im.src = b.img; im.setAttribute("data-layout-allow-overflow", "");
|
|
60
|
+
Object.assign(im.style, { width: "100%", height: "100%", objectFit: "cover" }); box.append(im);
|
|
61
|
+
tl.fromTo(im, { scale: 1, x: 0 }, { scale: 1 + amount, x: -24, duration: b.len, ease: "none" }, b.start);
|
|
62
|
+
return box;
|
|
63
|
+
};
|
|
64
|
+
const logoFor = () => (light && F.logoOnLight) || F.logo;
|
|
65
|
+
const items = (b, parent, line) => {
|
|
66
|
+
if (!b.items) return;
|
|
67
|
+
const box = $({ marginTop: "34px" }, "", parent);
|
|
68
|
+
b.items.forEach((it, i) => {
|
|
69
|
+
const e = $({ fontFamily: `"${FD.display}"`, fontStyle: italic ? "italic" : "normal", fontSize: "40px", color: P.accent, marginTop: "10px" }, it, box);
|
|
70
|
+
enter(b, e, lead + 0.67 + ((line - 0.67) * i) / b.items.length);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const KINDS = {
|
|
75
|
+
black(b, v, line) {
|
|
76
|
+
const c = $({ ...FILL, justifyContent: "center", alignItems: "center", textAlign: "center", padding: pad + "px" }, "", v);
|
|
77
|
+
title(b, b.title, lead, portrait ? 96 : 84, { italic, center: true }, c);
|
|
78
|
+
if (b.sub) { const s = $(body(34), b.sub, c); s.style.marginTop = "30px"; enter(b, s, lead + line * 0.55, "center"); }
|
|
79
|
+
},
|
|
80
|
+
question(b, v) {
|
|
81
|
+
const c = $({ ...FILL, justifyContent: "center", alignItems: "center", textAlign: "center", padding: pad * 1.5 + "px" }, "", v);
|
|
82
|
+
title(b, b.title, lead, portrait ? 92 : 88, { italic, center: true, color: P.bg }, c);
|
|
83
|
+
},
|
|
84
|
+
reveal(b, v, line) {
|
|
85
|
+
const c = $({ ...FILL, justifyContent: "center", alignItems: "center", textAlign: "center", padding: pad + "px" }, "", v);
|
|
86
|
+
const logo = logoFor();
|
|
87
|
+
if (logo) {
|
|
88
|
+
const im = document.createElement("img"); im.src = logo; Object.assign(im.style, { width: (portrait ? 760 : 820) + "px" }); c.append(im);
|
|
89
|
+
tl.fromTo(im, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.8, ease: "power2.out" }, b.start + lead);
|
|
90
|
+
tl.fromTo(im, { scale: 1.04 }, { scale: 1, duration: b.len - lead, ease: "none" }, b.start + lead);
|
|
91
|
+
} else {
|
|
92
|
+
const t = title(b, b.title, lead, 170, { center: true }, c);
|
|
93
|
+
t.style.maxWidth = W - pad * 2 + "px"; t.style.overflowWrap = "anywhere";
|
|
94
|
+
if (t.scrollWidth > W - pad * 2 || b.title.length * 170 * 0.55 > W - pad * 2) t.style.fontSize = Math.floor(Math.min(170, (W - pad * 2) / (b.title.length * 0.58))) + "px";
|
|
95
|
+
}
|
|
96
|
+
if (b.sub) { const s = title(b, b.sub, lead + 1.0, 46, { italic, center: true }, c); s.style.marginTop = "40px"; }
|
|
97
|
+
if (b.line) { const k = kick(b, b.line, lead + f(50), c); k.style.marginTop = "34px"; }
|
|
98
|
+
},
|
|
99
|
+
stat(b, v, line) {
|
|
100
|
+
const c = $({ ...FILL, padding: pad + "px", justifyContent: "center" }, "", v);
|
|
101
|
+
if (b.kick) kick(b, b.kick, lead, c);
|
|
102
|
+
const row = $({ display: "flex", flexDirection: portrait ? "column" : "row", gap: (short ? 36 : 70) + "px", marginTop: "40px" }, "", c);
|
|
103
|
+
const n = b.stat.length;
|
|
104
|
+
b.stat.forEach((st, i) => {
|
|
105
|
+
const at = lead + (line * i) / n, cell = $({}, "", row);
|
|
106
|
+
const num = $({ fontFamily: `"${FD.display}"`, fontSize: (short ? 112 : 150) + "px", color: P.accent, lineHeight: 1 }, "", cell);
|
|
107
|
+
const show = x => (num.textContent = Math.round(x).toLocaleString(F.language === "id" ? "id-ID" : "en-US") + (st.suffix || ""));
|
|
108
|
+
show(0);
|
|
109
|
+
const o = { x: 0 };
|
|
110
|
+
tl.to(o, { x: st.value, duration: 40 / 30, ease: "none", onUpdate: () => show(o.x) }, b.start + at);
|
|
111
|
+
$(body(30, P.muted, { marginTop: "12px", maxWidth: "560px" }), st.label, cell);
|
|
112
|
+
enter(b, cell, at);
|
|
113
|
+
});
|
|
114
|
+
if (b.title) { const t = title(b, b.title, lead + line * 0.7, 52, { italic }, c); t.style.marginTop = "50px"; }
|
|
115
|
+
const src = b.stat.map(s => s.source).filter(Boolean);
|
|
116
|
+
if (src.length) $({ position: "absolute", left: pad + "px", bottom: (kinetic ? (short ? 400 : portrait ? 520 : 260) : short ? 290 : portrait ? 330 : 70) + "px", ...body(18, P.muted, { opacity: 0.8 }) },
|
|
117
|
+
`${F.labels.source}: ${src.join(" · ")}`, c);
|
|
118
|
+
},
|
|
119
|
+
triptych(b, v, line) {
|
|
120
|
+
const c = $({ ...FILL, padding: pad + "px", justifyContent: "center" }, "", v);
|
|
121
|
+
const cw = portrait ? 270 : 420, ch = portrait ? 420 : 520;
|
|
122
|
+
if (b.kick) kick(b, b.kick, lead, c);
|
|
123
|
+
if (b.title) title(b, b.title, lead + 0.13, 60, {}, c).style.marginTop = "18px";
|
|
124
|
+
const row = $({ display: "flex", gap: "36px", marginTop: "50px" }, "", c);
|
|
125
|
+
b.panels.forEach((pn, i) => {
|
|
126
|
+
const cell = $({}, "", row);
|
|
127
|
+
const fr = $({ width: cw + "px", height: ch + "px", overflow: "hidden", border: `2px solid ${P.accent}` }, `<img src="${pn.img}" data-layout-allow-overflow style="width:100%;height:100%;object-fit:cover">`, cell);
|
|
128
|
+
$({ fontFamily: `"${FD.display}"`, fontSize: "34px", color: P.fg, marginTop: "16px", maxWidth: cw + "px" }, pn.title, cell);
|
|
129
|
+
$(body(22, P.muted, { marginTop: "6px", maxWidth: cw + "px" }), pn.sub || "", cell);
|
|
130
|
+
enter(b, cell, lead + (line * (i + 0.3)) / 3);
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
shot(b, v) {
|
|
134
|
+
const c = $({ ...FILL, padding: pad + "px", flexDirection: portrait ? "column" : "row", alignItems: "center", justifyContent: portrait ? "center" : "space-between", gap: "50px" }, "", v);
|
|
135
|
+
textBlock(b, c, portrait ? 900 : 560, 72);
|
|
136
|
+
// fit the screenshot inside its box, whatever its ratio (phone screenshots are tall)
|
|
137
|
+
const r = b.shotRatio || 1.6, mw = portrait ? 880 : 1000, mh = portrait ? 800 : 780, w = Math.min(mw, mh * r), h = w / r;
|
|
138
|
+
const fr = $({ padding: "10px", background: "#F3EDE3", border: `2px solid ${P.accent}` }, `<img src="${b.img}" data-layout-allow-overflow style="width:${w}px;height:${h}px;display:block">`, c);
|
|
139
|
+
enter(b, fr, lead + 0.33);
|
|
140
|
+
tl.fromTo(fr, { scale: 1 }, { scale: 1.04, duration: b.len, ease: "none" }, b.start);
|
|
141
|
+
},
|
|
142
|
+
cta(b, v) {
|
|
143
|
+
const c = F.cta;
|
|
144
|
+
if (b.img) { const g = $({ ...FILL, opacity: 0.35 }, "", v); photo(b, g, S.kenburns); }
|
|
145
|
+
const col = $({ ...FILL, padding: pad + "px", justifyContent: "center", alignItems: portrait ? "center" : "flex-start", textAlign: portrait ? "center" : "left" }, "", v);
|
|
146
|
+
kick(b, c.kick || F.labels.next, lead, col);
|
|
147
|
+
title(b, c.title, lead + 0.2, portrait ? 96 : 86, { center: portrait }, col).style.marginTop = "24px";
|
|
148
|
+
if (c.sub) kick(b, c.sub, lead + 1.0, col).style.marginTop = "40px";
|
|
149
|
+
(c.lines || []).forEach((l, i) => { const e = $(body(38, P.fg, { marginTop: "12px" }), l, col); enter(b, e, lead + 1.2 + i * 0.2); });
|
|
150
|
+
if (c.url) { const e = $(body(34, P.muted, { marginTop: "12px" }), c.url, col); enter(b, e, lead + 1.6); }
|
|
151
|
+
const qr = () => { const e = $({}, `<img src="${F.qr}" style="width:${portrait ? 360 : 330}px;height:${portrait ? 360 : 330}px;border-radius:8px;display:block">`); return e; };
|
|
152
|
+
if (portrait && F.qr) { const e = qr(); col.append(e); e.style.marginTop = "50px"; enter(b, e, lead + 1.8, "center"); }
|
|
153
|
+
const logo = logoFor();
|
|
154
|
+
if (logo) { const e = $({}, `<img src="${logo}" style="width:360px;display:block">`, col); e.style.marginTop = "60px"; enter(b, e, lead + 2.1, "center"); }
|
|
155
|
+
if (!portrait && F.qr) { const e = qr(); Object.assign(e.style, { position: "absolute", right: pad + "px", bottom: "150px" }); v.append(e); enter(b, e, lead + 1.8); }
|
|
156
|
+
},
|
|
157
|
+
media(b, v, line) { // photo | footage | list: the layout depends on the style
|
|
158
|
+
const slot = (parent, style) => {
|
|
159
|
+
const box = $({ overflow: "hidden", ...style }, "", parent);
|
|
160
|
+
if (b.kind === "footage" && b.video) {
|
|
161
|
+
const vid = document.createElement("video"); vid.id = "fv-" + b.n; vid.muted = true; vid.setAttribute("muted", ""); vid.playsInline = true; vid.src = b.video;
|
|
162
|
+
vid.dataset.start = b.start; vid.dataset.duration = b.len; vid.dataset.trackIndex = 8;
|
|
163
|
+
Object.assign(vid.style, { width: "100%", height: "100%", objectFit: "cover", display: "block" }); box.append(vid);
|
|
164
|
+
} else if (b.img) { Object.assign(box.style, { display: "flex", flexDirection: "column" }); photo(b, box, S.kenburns, { position: "relative", flex: 1 }); }
|
|
165
|
+
return box;
|
|
166
|
+
};
|
|
167
|
+
if (S.layout === "window" && !portrait) {
|
|
168
|
+
const c = $({ ...FILL, flexDirection: "row" }, "", v);
|
|
169
|
+
const left = $({ width: "52%", padding: `0 ${pad}px`, display: "flex", flexDirection: "column", justifyContent: "center" }, "", c);
|
|
170
|
+
textBlock(b, left, 820, 76); items(b, left, line);
|
|
171
|
+
const win = slot(c, { width: "40%", margin: "110px 0", border: `2px solid ${P.accent}`, position: "relative" });
|
|
172
|
+
tl.fromTo(win, { clipPath: "inset(50% 0 50% 0)" }, { clipPath: "inset(0% 0 0% 0)", duration: 0.6, ease: "power2.out" }, b.start + lead);
|
|
173
|
+
} else if (S.layout === "card" && !portrait) {
|
|
174
|
+
const c = $({ ...FILL, flexDirection: "row", alignItems: "center" }, "", v);
|
|
175
|
+
const left = $({ width: "50%", padding: `0 ${pad}px` }, "", c);
|
|
176
|
+
textBlock(b, left, 780, 80); items(b, left, line);
|
|
177
|
+
const card = slot(c, { width: "760px", height: "760px", borderRadius: "40px", boxShadow: "0 30px 80px rgba(0,0,0,0.25)", position: "relative" });
|
|
178
|
+
tl.fromTo(card, { rotation: 10, scale: 0.8 }, { rotation: 2, scale: 1, duration: 0.6, ease: "back.out(2)" }, b.start + lead);
|
|
179
|
+
} else {
|
|
180
|
+
const top = portrait && S.captions === "kinetic", bottom = !top && (portrait || b.side !== "left");
|
|
181
|
+
slot(v, { ...FILL, position: "absolute" });
|
|
182
|
+
const g = top ? `linear-gradient(to bottom, ${P.bg}E6 0%, transparent 40%, transparent 55%, ${P.bg}CC 100%)`
|
|
183
|
+
: bottom ? `linear-gradient(to top, ${P.bg}F0 0%, ${P.bg}99 35%, transparent 65%)`
|
|
184
|
+
: `linear-gradient(to right, ${P.bg}F2 0%, ${P.bg}AA 40%, transparent 70%)`;
|
|
185
|
+
$({ ...FILL, background: g }, "", v);
|
|
186
|
+
const c = $({ ...FILL, padding: top ? `200px ${pad}px 0` : bottom ? `0 ${pad}px ${portrait ? 420 : kinetic ? 260 : 150}px` : `0 ${pad}px`,
|
|
187
|
+
justifyContent: top ? "flex-start" : bottom ? "flex-end" : "center" }, "", v);
|
|
188
|
+
textBlock(b, c, bottom ? 1400 : 900, portrait ? 92 : bottom ? 92 : 80); items(b, c, line);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Beats: wrapper with the style's transition, then the kind's content.
|
|
194
|
+
F.beats.forEach((b, i) => {
|
|
195
|
+
const line = b.dur || b.len, end = b.start + b.len, T = S.transition;
|
|
196
|
+
const v = $({ ...FILL, opacity: 0, background: b.kind === "question" ? P.accentBg : "transparent" }, "", document.getElementById("beats"));
|
|
197
|
+
(KINDS[b.kind] || KINDS.media)(b, v, line);
|
|
198
|
+
const out = { opacity: 0, duration: f(10), ease: "none" };
|
|
199
|
+
if (T === "wipe") tl.fromTo(v, { opacity: 1, clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: f(14), ease: "power2.out" }, b.start);
|
|
200
|
+
else if (T === "punch") tl.fromTo(v, { opacity: 0, scale: 1.12 }, { opacity: 1, scale: 1, duration: f(8), ease: "power2.out" }, b.start);
|
|
201
|
+
else tl.fromTo(v, { opacity: 0 }, { opacity: 1, duration: f(10), ease: "none" }, b.start);
|
|
202
|
+
if (b.len > f(20)) tl.to(v, out, end - f(10));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Grade: vignette, light leak on every cut, drifting grain.
|
|
206
|
+
const G = S.grade, grade = document.getElementById("grade");
|
|
207
|
+
if (G.vignette > 0) $({ ...FILL, background: `radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,${G.vignette}) 100%)` }, "", grade);
|
|
208
|
+
if (G.leak) F.beats.forEach((b, i) => {
|
|
209
|
+
const x = 20 + ((i * 37) % 60), e = $({ ...FILL, opacity: 0, mixBlendMode: "screen", background: `radial-gradient(circle at ${x}% 30%, rgba(255,190,120,0.55) 0%, transparent 55%)` }, "", grade);
|
|
210
|
+
tl.fromTo(e, { opacity: 0 }, { opacity: 1, duration: f(8), ease: "none" }, b.start).to(e, { opacity: 0, duration: f(18), ease: "none" }, b.start + f(8));
|
|
211
|
+
});
|
|
212
|
+
if (G.grain > 0) {
|
|
213
|
+
const g = $({ position: "absolute", inset: "-60px", opacity: G.grain, mixBlendMode: "overlay" },
|
|
214
|
+
`<svg width="${W + 120}" height="${H + 120}"><filter id="g"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7"/></filter><rect width="100%" height="100%" filter="url(#g)"/></svg>`, grade);
|
|
215
|
+
for (let k = 0; k < F.total * 8; k++) tl.set(g, { x: (k * 53) % 60 - 30, y: (k * 29) % 60 - 30 }, k / 8);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Captions from the voice's word timings (make_film.py cut them into groups).
|
|
219
|
+
const cap = document.getElementById("captions");
|
|
220
|
+
F.captions.forEach(c => {
|
|
221
|
+
const subtitle = S.captions === "subtitle";
|
|
222
|
+
const box = $({ ...FILL, opacity: 0, justifyContent: "flex-end", alignItems: "center", pointerEvents: "none",
|
|
223
|
+
paddingBottom: (subtitle ? (portrait ? 220 : 56) : (portrait ? 300 : 120)) + "px" }, "", cap);
|
|
224
|
+
let inner;
|
|
225
|
+
if (subtitle) {
|
|
226
|
+
inner = $({ ...body(portrait ? 40 : 32, light ? P.fg : "#FFFFFF"), textAlign: "center", background: light ? "rgba(255,255,255,0.75)" : "rgba(0,0,0,0.45)",
|
|
227
|
+
padding: "8px 18px", borderRadius: "6px", maxWidth: W - 120 + "px" }, c.words.map(w => w.w).join(" "), box);
|
|
228
|
+
} else {
|
|
229
|
+
inner = $({ display: "flex", flexWrap: "wrap", justifyContent: "center", maxWidth: (portrait ? 940 : 1500) + "px" }, "", box);
|
|
230
|
+
c.words.forEach(w => {
|
|
231
|
+
const s = document.createElement("span"); s.textContent = w.w;
|
|
232
|
+
Object.assign(s.style, { fontFamily: '"Inter"', fontWeight: 900, fontSize: (portrait ? 84 : 72) + "px", lineHeight: 1.15, textTransform: "uppercase", color: "#FFFFFF",
|
|
233
|
+
margin: "0 10px", webkitTextStroke: "3px #000", paintOrder: "stroke", textShadow: "0 6px 18px rgba(0,0,0,0.6)" });
|
|
234
|
+
inner.append(s);
|
|
235
|
+
tl.set(s, { color: P.accent }, w.s).set(s, { color: "#FFFFFF" }, w.e);
|
|
236
|
+
});
|
|
237
|
+
tl.fromTo(inner, { scale: 0.85 }, { scale: 1, duration: f(10), ease: "back.out(2)" }, c.s);
|
|
238
|
+
}
|
|
239
|
+
tl.set(box, { opacity: 1 }, c.s).set(box, { opacity: 0 }, c.e);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
document.getElementById("root").style.background = P.bg;
|
|
243
|
+
window.__timelines["main"] = tl;
|
|
244
|
+
</script>
|
|
245
|
+
</body>
|
|
246
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
|
|
3
|
+
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
|
|
4
|
+
"paths": {
|
|
5
|
+
"blocks": "compositions",
|
|
6
|
+
"components": "compositions/components",
|
|
7
|
+
"assets": "assets"
|
|
8
|
+
},
|
|
9
|
+
"media": {
|
|
10
|
+
"autoProxy": true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Educational-warning preset
|
|
2
|
+
|
|
3
|
+
A bright 1080×1920 HyperFrames style for health and consumer warning explainers. It is light and calm rather than dark or scary. It uses the same config keys and card types as the other presets, but its look and motion are its own.
|
|
4
|
+
|
|
5
|
+
## Look and motion
|
|
6
|
+
|
|
7
|
+
- **Grade:** natural and slightly lifted, with no vignette and no grain. Grid paper sits behind the camera.
|
|
8
|
+
- **Transition:** an amber band and a navy band swipe across the frame. Meanwhile the camera steps back onto the grid paper (it shrinks, rounds its corners and gets a light wash) for as long as a card is up.
|
|
9
|
+
- **Jump cuts:** a small settle-in zoom at the start of each segment.
|
|
10
|
+
- **Hook:** a white card. The warning sign bounces in, `line1` (red) and `line2` rise out of masks, and `sub` pops in as an amber pill.
|
|
11
|
+
- **Captions:** a white box. Upcoming words are grey, the current word sits on an amber marker, and spoken words turn dark.
|
|
12
|
+
- **Card types:**
|
|
13
|
+
- `title`: a dark headline on white, with an amber marker stroke that grows behind it and an optional teal `sub` pill.
|
|
14
|
+
- `case`: a fact card with a teal header (`kicker`). It flips up from its top edge. `stamp` becomes a round red badge.
|
|
15
|
+
- `photo`: a white print with a red "CONTOH" tag and a red circle drawn on the image. Optional keys: `dur` (seconds), `pos` (CSS object-position), and `cx`/`cy`/`rx`/`ry` (circle center and radii, as % of the image).
|
|
16
|
+
- `item`: a numbered checklist on a clipboard card titled "SEBELUM BELI, INGAT:". Items slide in from a blur.
|
|
17
|
+
- `end`: a hanging warning sign with the title under it. It drops in and swings to rest on the chord hit.
|
|
18
|
+
|
|
19
|
+
## Audio
|
|
20
|
+
|
|
21
|
+
- **Music:** `lights.mp3` (Rafael Krux, CC BY 4.0). Use a low `music_volume`, around 0.2.
|
|
22
|
+
- **Sound effects** (synthesized with ffmpeg, CC0): `shutter` (swipe), `stamp` (badge pop), `reveal` (two-note tick) and `ending` (riser into a chord at 1.2 s). Peaks are about −7 to −8.5 dBFS, and the ending is about −4 dBFS.
|
|
23
|
+
|
|
24
|
+
## Censoring screen recordings
|
|
25
|
+
|
|
26
|
+
If the footage has someone else's account or comments in it, blur those areas into `videos/<name>/work/censored.mp4`. `render_cut.py` uses that file in place of `raw/` when it exists. Blur each area with a cropped `boxblur` overlaid back in place, limited in time with `enable='between(t,a,b)'`, for example `[0:v]split[b][r];[r]crop=230:50:40:36,boxblur=10:3:5:3[c];[b][c]overlay=40:36:enable='between(t,0,6.85)'`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|