aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 31cb19d2dee6c04e514c1affb4da6ded3a7c1c5f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# 24 vertical TikTok videos — generated 100% locally

[Español](README.es.md)

Everything runs offline: **Manim** for 2D animation, **Blender** (EEVEE, headless)
for the 3D scenes, **Godot** (Jolt physics engine, headless) for rigid-body
physics, **qwentts.cpp** (a cloned Rioplatense voice) for the narration and the
`keysound` piano samples for the music.

The videos are in Spanish: narration, subtitles and on-screen text are Spanish on
purpose. The code is in English.

## The videos (`out/`)

| File | Topic | Length | Hook |
|---|---|---|---|
| `montyhall.mp4` | The Monty Hall problem | 39.5 s | sparks debate in the comments |
| `nueves.mp4` | 0,999… = 1 | 38.8 s | sparks debate in the comments |
| `cumple.mp4` | The birthday paradox | 38.6 s | surprising fact |
| `luna.mp4` | Folding paper to the Moon | 38.4 s | surprising fact |
| `japones.mp4` | Multiplying with lines | 38.8 s | useful, gets saved and shared |
| `benford.mp4` | Benford's law (spotting fraud) | 42.8 s | surprising fact + useful |
| `hilbert.mp4` | The infinite hotel | 37.1 s | sparks debate in the comments |
| `reuleaux.mp4` | Why manhole covers are round | 36.0 s | satisfying to watch |
| `regla72.mp4` | The rule of 72 | 35.5 s | useful, gets saved |
| `buffon.mp4` | Getting π by dropping needles | 36.7 s | mathematical beauty |
| `collatz.mp4` | The Collatz conjecture (3n+1) | 46.2 s | unsolved mystery |
| `simpson.mp4` | Simpson's paradox | 53.0 s | debate + useful |
| `cuerda.mp4` | A rope around the Earth | 45.4 s | surprising fact |
| `gabriel.mp4` | Gabriel's horn | 40.5 s | mathematical beauty |
| `mobius.mp4` | Möbius strip | 46.3 s | try it at home |
| `bayes.mp4` | False positive (medical test) | 46.9 s | debate + useful |
| `cicloide.mp4` | Brachistochrone: the fastest curve | 47.6 s | satisfying to watch |
| `dados.mp4` | Non-transitive dice | 50.6 s | sparks debate in the comments |
| `bolapeluda.mp4` | Hairy ball theorem (3D) | 41.9 s | satisfying to watch |
| `caos.mp4` | Three double pendulums and 1 mm (3D) | 40.6 s | hypnotic, watched to the end |
| `diferencial.mp4` | How a differential works (3D) | 44.1 s | useful, gets saved and shared |
| `galton.mp4` | Galton board: 500 balls draw the bell curve (Blender + Godot) | 45.7 s | satisfying + question in the comments |
| `bloques.mp4` | Harmonic stack: blocks overhanging the table (Blender + Godot) | 48.0 s | try it at home + surprising fact |
| `domino.mp4` | Dominoes ×1.5: from 5 mm to the Aconcagua (Blender + Godot) | 66.7 s | exponential + question in the comments (over 1 min) |

Format: 1080×1920, 30 fps, H.264 + AAC 48 kHz stereo, **−13.8 to −15.3 LUFS**
measured (TikTok's target is −14, so it neither lowers nor raises them), true
peak below −1 dBTP. Burned-in subtitles: 80 % watch without sound.

## Suggested post texts (in Spanish, as published)

**montyhall** — ¿Te quedás o cambiás? El 87% se equivoca 🚪
`#matematica #montyhall #datocurioso #probabilidad #argentina`

**nueves** — 0,999… es EXACTAMENTE 1. Peleame en los comentarios 🧮
`#matematica #mindblown #datocurioso #numeros #viral`

**cumple** — Con 23 personas ya hay 50% de que dos cumplan el mismo día 🎂
`#paradoja #datocurioso #matematica #probabilidad #sabiasque`

**luna** — Doblá un papel 42 veces y llegás a la Luna 🌙
`#exponencial #datocurioso #ciencia #mindblown #sabiasque`

**japones** — Así multiplican en Japón, sin tablas ✏️
`#trucosmatematicos #japon #escuela #tarea #aprendeentiktok`

**benford** — Los números inventados se notan. Así agarran fraudes 🕵️
`#benford #fraude #estadistica #datocurioso #sabiasque`

**hilbert** — Un hotel lleno con infinitas habitaciones… y entran infinitos más 🏨
`#infinito #matematica #paradoja #mindblown #hilbert`

**reuleaux** — Por qué las tapas de alcantarilla son redondas 🛞
`#datocurioso #geometria #ingenieria #sabiasque #reuleaux`

**regla72** — La cuenta que te dice cuándo se duplica tu plata 💸
`#finanzaspersonales #inversiones #interescompuesto #inflacion #argentina`

**buffon** — Tiro agujas al piso y sale π. Sin ningún círculo 🎲
`#pi #matematica #azar #datocurioso #mindblown`

**collatz** — Elegí cualquier número. Nadie pudo demostrar por qué siempre cae al 1 🌀
`#collatz #matematica #misterio #datocurioso #mindblown`

**simpson** — Gana en los dos grupos y pierde en el total. Así te miente un promedio 📊
`#estadistica #paradoja #datos #sabiasque #matematica`

**cuerda** — Le agrego 1 metro a una cuerda que rodea la Tierra. ¿Cuánto se levanta? 🌍
`#datocurioso #matematica #mindblown #sabiasque #ciencia`

**gabriel** — Lo podés llenar de pintura, pero no lo podés pintar 🎺
`#infinito #matematica #paradoja #calculo #mindblown`

**mobius** — Cortá esta cinta al medio y NO van a salir dos ✂️📄
`#mobius #topologia #experimento #hacelovos #datocurioso`

**bayes** — El test acierta el 99% y te dio positivo. La chance de estar enfermo es 9% 🧪
`#probabilidad #estadistica #salud #sabiasque #bayes`

**cicloide** — La recta es el camino más corto… y pierde la carrera 🎢
`#fisica #matematica #satisfying #mindblown #cicloide`

**dados** — Elegí el dado que quieras. Elijo después y te gano igual 🎲
`#probabilidad #dados #juegos #datocurioso #matematica`

**bolapeluda** — No podés peinar una esfera sin dejar un remolino. Es un teorema 🌀
`#topologia #matematica #3d #datocurioso #mindblown`

**caos** — Tres péndulos idénticos salvo 1 milímetro. A los 5 segundos no se parecen en nada 🎲
`#caos #fisica #simulacion #mindblown #efectomariposa`

**diferencial** — Por qué tu auto puede doblar sin que una rueda patine ⚙️
`#mecanica #autos #ingenieria #sabiasque #comofunciona`

**galton** — 500 bolitas, cada una al azar… y todas juntas dibujan una campana 🔔
`#probabilidad #estadistica #galton #satisfying #matematica`

**bloques** — Apilé bloques hasta que el de arriba quedó en el aire. ¿Hasta dónde llegás vos? 📚
`#fisica #experimento #matematica #paraHacerEnCasa #sabiasque`

**domino** — Una ficha de 5 mm puede tirar un edificio. ¿En qué ficha se cae tu casa? 🏠
`#domino #fisica #exponencial #satisfying #sabiasque`

Post one a day at the same time, and rotate the three kinds:

- **debate** (comments, which push reach the most): montyhall, nueves, hilbert,
  simpson, cuerda, dados
- **useful** (saved and shared): japones, regla72, benford, bayes, diferencial, bloques
- **spectacle** (watched to the end): reuleaux, buffon, mobius, gabriel, collatz,
  luna, cumple, cicloide, bolapeluda, caos, galton, domino

The 3D ones (`bolapeluda`, `caos`, `diferencial`, `galton`, `bloques`, `domino`)
are made in Blender: they break the visual monotony of Manim without changing the
channel style, because the UI layer (chip, bar and subtitles) is the same.

`mobius` is the only one with a physical call to action ("do it with a sheet of
paper"): those tend to bring response videos, which count double.

### Blender + Godot (`galton`, `bloques`, `domino`)

Blender designs and Godot computes the physics:

1. `MODE=sim blender -b -P blender/<name>.py` builds the scene and exports only
   the physical objects to glTF. The name prefix tells Godot what each one is
   (`static_` static mesh, `box_` static box, `gate_` static and removed by an
   event, `ball_` rigid sphere, `block_` rigid box). Then it runs
   `godot --headless --fixed-fps 240 -- godot/work/<name>.json`.
2. `godot/sim.gd` loads the glb at runtime, creates the bodies, applies the
   script events (release, remove the gate) at the exact second of the timeline
   and records each body's pose at 30 fps in Blender coordinates
   (`godot/work/<name>.bin`).
3. `blender -b -P blender/<name>.py` reads those poses back and renders with the
   real materials (procedural wood, brass, steel).

None of the motion is animated by hand. Some numbers that come out of the
simulation:

- **galton**: bins `[0 1 7 29 70 104 96 83 65 35 10 0 0]` (standard deviation of
  1.82 bins; the ideal binomial for 12 rows gives 1.73). The middle column holds
  **96**: that is the answer to the closing question.
- **bloques**: the stack of 5 with harmonic shifts ×0.95 holds with the top block
  entirely in the air (8.5 % of a block). The tower of 16 does not move until one
  more block is put on its top left end: 10 of 17 fall.

Two tweaks Godot needed:

- **galton**: linear damping of 6/s, acting as friction against the glass.
  Without it the balls pick up too much sideways speed and the distribution
  comes out flat.
- **bloques**: physics at 1920 Hz and the world ×5 (`"hz"` and `"scale"` in the
  config). Jolt tolerates 2 cm of penetration and at 240 Hz the tower of 16 sank
  and toppled on its own.

The prefixes also set the order Godot creates the bodies in (Blender exports
alphabetically), and in a chaotic scene that order changes the result: keep new
prefixes in the same alphabetical order (`ball_ < box_ < gate_ < static_`).

On an Optimus laptop EEVEE uses the Intel GPU unless started with
`__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia`, which is 3.6 times
faster (1.5 s/frame versus 5.5 on galton).

## Regenerating

```bash
conda run -n manim python scripts/gen_audio.py montyhall     # voice + check
conda run -n manim manim render -v ERROR --disable_caching \
    --format=mp4 -o montyhall scenes/v1_montyhall.py MontyHall
conda run -n manim python scripts/build_video.py montyhall   # music + mix + mux
conda run -n manim python scripts/verify_final.py montyhall  # QA on the final MP4
```

`DRAFT=1` before rendering drops to 540×960 to iterate fast.

## Layout

```
scripts/narration.py   scripts: (text that is spoken, on-screen subtitle)
scripts/gen_audio.py   synthesizes each sentence and checks it by transcribing with Whisper
scripts/numspell.py    turns "99,9%" into "noventa y nueve coma nueve por ciento"
                       so the transcription can be compared with the script
scripts/regen.py       regenerates one sentence trying 5 seeds
scripts/repick.py      picks the best take already synthesized again
scripts/verify.py      QA: transcribes every WAV and flags doubtful ones
scripts/verify_final.py  final QA: extracts the audio of the mixed MP4 and transcribes it,
                       which proves the voice is intelligible over the piano
scripts/music.py       piano bed (original progression) + ducking under the voice
scripts/build_video.py builds the track following the timeline and muxes it onto the video
scenes/tiktok.py       base: 9:16, subtitles, progress bar, "beats"
scenes/v*.py           one scene per video (Manim)
scenes/overlay.py      UI layer for the 3D videos: the same bar, chip and subtitles,
                       rendered with alpha
blender/base.py        base of the 3D scenes: palette, camera, primitives, timing
blender/*.py           one scene per 3D video
godot/sim.gd           generic rigid-body simulator fed by Blender
scripts/build_blender.py  composites background + 3D + overlay + audio in one pass
brand/                 channel logo and avatar (make_logo.py)
```

### The 3D videos

Rendered headless with Blender 5.2 and EEVEE:

```bash
conda run -n manim manim -s --disable_caching -o fondo scenes/overlay.py Backdrop  # dot pattern
NAME=caos CHIP=Caos CHIPCOL=rosa conda run -n manim manim --transparent \
    --disable_caching -o caos scenes/overlay.py Overlay   # UI layer + timeline
/path/to/blender -b --factory-startup --python blender/caos.py  # PNG sequence with alpha
conda run -n manim python scripts/build_blender.py caos    # compositing + audio
```

Decisions that paid for themselves:

- **No keyframes.** The render walks the frames one by one and calls
  `refresh(f)` before each one, which places everything. That way physics is
  computed exactly on every frame, skipped frames can be rendered to check the
  framing, and there is no fighting with Blender 5.x's layered actions API.
- **The background is not in the 3D scene.** Blender renders with
  `film_transparent` and the dotted background is composited underneath with
  ffmpeg, so it is pixel for pixel the same as in the 18 Manim videos.
- **Metal needs something to reflect.** `studio_world()` puts a horizon gradient
  in the world: it is not visible (the film is transparent) but it is the only
  thing that separates steel from grey plastic. Without it, a metallic material
  looks flat.

`blender/base.py` generates the mechanical parts in code:

| Function | What it does |
|---|---|
| `bevel_gear` | Bevel gear with a cone half-angle and spiral teeth. Two mesh at 90° when their γ add up to 90: 45+45 for the differential ones, 72+18 for ring gear and pinion (and there the tooth ratio is fixed by sin72/sin18 = 3.07, which is why they are 43 and 14) |
| `full_wheel` | Tire with lugs and grooves + alloy rim with spokes + brake disc |
| `ring_sector` | Curved wall: two of these and two flanges make a differential housing with windows, which is what lets you see the gears inside |
| `flat_ring`, `screws` | Flanges and bolt circles |

The intermediate PNGs stay in `render/<name>/` (about 4 GB for three videos). They
allow recompositing with other audio or subtitles without rendering again, which
takes 30 to 55 minutes per video; if they are not needed, delete them.

`SAMPLES=8` lowers the EEVEE samples (16 by default, 64 stock: that is 10 s/frame
on a GTX 1050 and 1.6 s/frame with 8). `EVERY=55` renders one frame out of every
55 to build a contact sheet without waiting for the whole render.

Sync is not estimated: the scene writes `out/<name>_timeline.json` with the real
time of each block and the audio is placed at those exact marks.

## Environment variables

| Variable | What for |
|---|---|
| `PROJECT` | project root (defaults to the repository directory) |
| `MODE=sim` | in the Godot scenes: recompute the physics instead of rendering |
| `SAMPLES`, `PCT` | EEVEE samples and resolution percentage |
| `FROM_FRAME`, `TO_FRAME`, `EVERY`, `RESUME` | which frames to render |
| `QWENTTS_DIR`, `VOICE_REF` | qwentts.cpp and the reference voice for the narration |
| `PIANO_DIR` | keysound piano samples |

The older Spanish names (`PROY`, `MODO`, `MUESTRAS`, `DESDE`, `HASTA`, `SALTO`,
`SEGUIR`, `PRUEBA`) are still accepted.

## Voice

The repository does not include the cloned voice. `VOICE_REF` is the path prefix
of its three files (`.spk`, `.rvq` and `.txt`), extracted from a recording with
`qwen-codec --talker`.

## What is not in git

Everything generated: `render/` (intermediate PNGs), `media/` (Manim output),
`audio/` (narration), `out/` (final videos and timelines), `tmp/` and
`godot/work/` (simulations). They are regenerated with the steps above.

## About the music

The progressions (Am–F–C–G, Em–C–G–D, Am–Em–Dm–E, Cm–G#–A#–Gm, A–E–F#m–D, etc.) are
original and played with loose piano samples, so the audio is yours and Content
ID will not flag it. If you prefer a trending TikTok sound, upload anyway and add
it from the app: the voice is already mixed at −14 LUFS and stays intelligible
over it.

## License

MIT