# 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/.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/.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/.bin`). 3. `blender -b -P blender/.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//` (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/_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