aboutsummaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/build_blender.py58
-rw-r--r--scripts/build_video.py93
-rw-r--r--scripts/gen_audio.py119
-rw-r--r--scripts/music.py166
-rw-r--r--scripts/narration.py431
-rw-r--r--scripts/numspell.py57
-rw-r--r--scripts/regen.py46
-rw-r--r--scripts/repick.py48
-rw-r--r--scripts/verify.py53
-rw-r--r--scripts/verify_final.py50
10 files changed, 1121 insertions, 0 deletions
diff --git a/scripts/build_blender.py b/scripts/build_blender.py
new file mode 100644
index 0000000..41731cf
--- /dev/null
+++ b/scripts/build_blender.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+"""Arma el video final de las escenas hechas en Blender.
+
+Compone en una sola pasada de ffmpeg:
+ fondo punteado + secuencia PNG con alfa (3D) + overlay de Manim + audio
+El overlay trae chip, barra de progreso y subtitulos, o sea que el estilo sale
+del mismo codigo que los videos de Manim.
+"""
+import glob, os, subprocess, sys
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.join(ROOT, "scripts"))
+import build_video
+
+FPS = 30
+
+
+def componer(name, con_musica=True):
+ fondo = os.path.join(ROOT, "media", "images", "overlay", "fondo.png")
+ carpeta = os.path.join(ROOT, "render", name)
+ seq = os.path.join(carpeta, "f%04d.png")
+ overlay = os.path.join(ROOT, "media", "videos", "overlay", "1920p30", f"{name}.mov")
+ for p in (fondo, overlay):
+ if not os.path.exists(p):
+ raise SystemExit(f"falta {p}")
+ n = len(glob.glob(os.path.join(carpeta, "f*.png")))
+ if n < 30:
+ raise SystemExit(f"[{name}] solo hay {n} frames renderizados")
+
+ wav = build_video.build_track(name, con_musica)
+ out = os.path.join(ROOT, "out", f"{name}.mp4")
+ cmd = [
+ "ffmpeg", "-v", "error", "-y",
+ "-loop", "1", "-framerate", str(FPS), "-i", fondo,
+ "-framerate", str(FPS), "-start_number", "1", "-i", seq,
+ "-i", overlay,
+ "-i", wav,
+ "-filter_complex",
+ "[0:v][1:v]overlay=format=auto[a];[a][2:v]overlay=format=auto,format=yuv420p[v]",
+ "-map", "[v]", "-map", "3:a",
+ "-filter:a", "loudnorm=I=-14:TP=-1.5:LRA=11,aresample=48000",
+ "-frames:v", str(n),
+ "-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p",
+ "-preset", "slow", "-crf", "20", "-r", str(FPS),
+ "-c:a", "aac", "-b:a", "192k", "-ac", "2",
+ "-movflags", "+faststart", "-shortest", out,
+ ]
+ subprocess.run(cmd, check=True)
+ d = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
+ "-of", "csv=p=0", out], capture_output=True, text=True).stdout.strip()
+ print(f"[{name}] {out} {float(d):.1f}s {os.path.getsize(out)/1e6:.1f} MB "
+ f"({n} frames 3D)", flush=True)
+ return out
+
+
+if __name__ == "__main__":
+ for n in (sys.argv[1:] or ["bolapeluda", "caos", "diferencial"]):
+ componer(n)
diff --git a/scripts/build_video.py b/scripts/build_video.py
new file mode 100644
index 0000000..572e3b5
--- /dev/null
+++ b/scripts/build_video.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+"""Arma la pista final (voz + piano con ducking) siguiendo el timeline de Manim
+y la pega al video renderizado."""
+import json, os, subprocess, sys, wave
+import numpy as np
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.join(ROOT, "scripts"))
+import music
+
+SR = 44100
+SCENE_DIR = {"montyhall": "v1_montyhall", "nueves": "v2_nueves", "cumple": "v3_cumple",
+ "luna": "v4_luna", "japones": "v5_japones",
+ "benford": "v6_benford", "hilbert": "v7_hilbert", "reuleaux": "v8_reuleaux",
+ "regla72": "v9_regla72", "buffon": "v10_buffon",
+ "collatz": "v11_collatz", "simpson": "v12_simpson",
+ "cuerda": "v13_cuerda", "gabriel": "v14_gabriel",
+ "mobius": "v15_mobius", "bayes": "v16_bayes",
+ "cicloide": "v17_cicloide", "dados": "v18_dados"}
+
+
+def read_wav(p):
+ with wave.open(p) as w:
+ a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
+ ch, sr = w.getnchannels(), w.getframerate()
+ a = a.astype(np.float32) / 32768.0
+ if ch > 1:
+ a = a.reshape(-1, ch).mean(axis=1)
+ return a, sr
+
+
+def to_sr(a, sr_in, sr_out=SR):
+ if sr_in == sr_out:
+ return a
+ n = int(round(len(a) * sr_out / sr_in))
+ return np.interp(np.linspace(0, len(a) - 1, n), np.arange(len(a)), a).astype(np.float32)
+
+
+def build_track(name, with_music=True):
+ tl = json.load(open(os.path.join(ROOT, "out", f"{name}_timeline.json")))
+ segs = json.load(open(os.path.join(ROOT, "audio", name, "segments.json")))["segments"]
+ total = tl["total"] + 0.25
+ speech = np.zeros(int(total * SR), dtype=np.float32)
+
+ for beat in tl["beats"]:
+ a, sr = read_wav(segs[beat["i"]]["wav"])
+ a = to_sr(a, sr)
+ off = int(beat["start"] * SR)
+ end = min(off + len(a), len(speech))
+ speech[off:end] += a[:end - off]
+
+ speech *= min(1.0, 0.90 / (np.abs(speech).max() + 1e-9))
+ st = np.stack([speech, speech], axis=1)
+
+ if with_music:
+ bed = music.bed(name, total, gain=0.32, seed=abs(hash(name)) % 9999)
+ if len(bed) < len(st):
+ bed = np.pad(bed, ((0, len(st) - len(bed)), (0, 0)))
+ bed = music.duck(bed[:len(st)], st)
+ mix = st * 0.96 + bed
+ else:
+ mix = st
+
+ mix = np.clip(mix / max(1.0, np.abs(mix).max() / 0.97), -1, 1)
+ path = os.path.join(ROOT, "tmp", f"{name}_mix.wav")
+ with wave.open(path, "w") as w:
+ w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR)
+ w.writeframes((mix * 32767).astype(np.int16).tobytes())
+ return path
+
+
+def mux(name, video=None, with_music=True):
+ if video is None:
+ video = os.path.join(ROOT, "media", "videos", SCENE_DIR[name], "1920p30", f"{name}.mp4")
+ wav = build_track(name, with_music)
+ out = os.path.join(ROOT, "out", f"{name}.mp4")
+ subprocess.run([
+ "ffmpeg", "-v", "error", "-y", "-i", video, "-i", wav,
+ "-filter:a", "loudnorm=I=-14:TP=-1.5:LRA=11,aresample=48000",
+ "-map", "0:v:0", "-map", "1:a:0",
+ "-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p",
+ "-preset", "slow", "-crf", "20", "-r", "30",
+ "-c:a", "aac", "-b:a", "192k", "-ac", "2",
+ "-movflags", "+faststart", "-shortest", out], check=True)
+ d = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
+ "-of", "csv=p=0", out], capture_output=True, text=True).stdout.strip()
+ print(f"[{name}] {out} {float(d):.1f}s {os.path.getsize(out)/1e6:.1f} MB", flush=True)
+ return out
+
+
+if __name__ == "__main__":
+ for n in (sys.argv[1:] or list(SCENE_DIR)):
+ mux(n)
diff --git a/scripts/gen_audio.py b/scripts/gen_audio.py
new file mode 100644
index 0000000..9dbbb47
--- /dev/null
+++ b/scripts/gen_audio.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+"""Genera un WAV por segmento con qwen-tts (voz clonada rioplatense),
+verifica cada uno transcribiendo con whisper y reintenta los que salen mal."""
+import json, os, re, shutil, subprocess, sys, unicodedata, wave
+from difflib import SequenceMatcher
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from narration import SCRIPTS
+from numspell import expand
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+TTS = os.path.expanduser("~/GIT-MIRRO/qwentts.cpp/build/qwen-tts")
+MODEL = os.path.expanduser("~/GIT-MIRRO/qwentts.cpp/models/qwen-talker-1.7b-base-Q8_0.gguf")
+CODEC = os.path.expanduser("~/GIT-MIRRO/qwentts.cpp/models/qwen-tokenizer-12hz-Q8_0.gguf")
+REF = dict(spk="/tmp/Firefox/output.spk", rvq="/tmp/Firefox/output.rvq", txt="/tmp/Firefox/output.txt")
+
+MIN_RATIO = 0.90 # similitud minima aceptada contra la transcripcion
+TIMEOUT = 150 # segundos por segmento; mas que eso es generacion desbocada
+MAX_ROUNDS = 4
+
+def norm(s):
+ s = expand(s)
+ s = unicodedata.normalize("NFD", s.lower())
+ s = "".join(c for c in s if unicodedata.category(c) != "Mn")
+ s = re.sub(r"[^a-z0-9ñ ]+", " ", s)
+ return " ".join(s.split())
+
+def dur(path):
+ with wave.open(path) as w:
+ return w.getnframes() / w.getframerate()
+
+def synth(text, out, seed, timeout=TIMEOUT):
+ cmd = [TTS, "--model", MODEL, "--codec", CODEC,
+ "--ref-spk", REF["spk"], "--ref-rvq", REF["rvq"], "--ref-text", REF["txt"],
+ "--lang", "spanish", "--seed", str(seed), "--temp", "0.85", "--top-p", "0.95",
+ "-o", out]
+ subprocess.run(cmd, input=text.encode(), stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, check=True, timeout=timeout)
+
+
+def synth_seguro(text, out, seed, etiqueta=""):
+ """A veces el modelo no emite fin y genera hasta el infinito: corto y cambio semilla."""
+ for k in range(3):
+ try:
+ synth(text, out, seed + 7919 * k)
+ return seed + 7919 * k
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
+ print(f" {etiqueta} se colgo con semilla {seed + 7919 * k}, reintento", flush=True)
+ raise RuntimeError(f"{etiqueta}: tres intentos colgados")
+
+def transcribe(files, workdir):
+ """Una sola invocacion de whisper para todos los archivos (carga el modelo una vez)."""
+ if not files:
+ return {}
+ subprocess.run(["conda", "run", "-n", "stt", "--no-capture-output",
+ "whisper-ctranslate2", "--language", "es", "--model", "small",
+ "--output_dir", workdir, "--output_format", "txt", *files],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
+ out = {}
+ for f in files:
+ t = os.path.join(workdir, os.path.splitext(os.path.basename(f))[0] + ".txt")
+ out[f] = open(t, encoding="utf-8").read().strip() if os.path.exists(t) else ""
+ return out
+
+def build(name):
+ spec = SCRIPTS[name]
+ outdir = os.path.join(ROOT, "audio", name)
+ work = os.path.join(ROOT, "tmp", name)
+ os.makedirs(outdir, exist_ok=True); os.makedirs(work, exist_ok=True)
+
+ segs = spec["segments"]
+ paths = [os.path.join(outdir, f"{i:02d}.wav") for i in range(len(segs))]
+ seeds = [1000 + i for i in range(len(segs))]
+ pend = list(range(len(segs)))
+ score = {}
+ best = {} # i -> (ratio, ruta del mejor intento)
+ keep = os.path.join(work, "best")
+ os.makedirs(keep, exist_ok=True)
+
+ for rnd in range(MAX_ROUNDS):
+ if not pend:
+ break
+ for i in pend:
+ seeds[i] = synth_seguro(segs[i][0], paths[i], seeds[i], f"[{name}] {i:02d}")
+ tr = transcribe([paths[i] for i in pend], work)
+ nxt = []
+ for i in pend:
+ r = SequenceMatcher(None, norm(segs[i][0]), norm(tr[paths[i]])).ratio()
+ score[i] = r
+ if i not in best or r > best[i][0]:
+ b = os.path.join(keep, f"{i:02d}.wav")
+ shutil.copy(paths[i], b)
+ best[i] = (r, b)
+ flag = "ok " if r >= MIN_RATIO else "RE "
+ print(f" [{name}] {i:02d} r={r:.2f} {flag}{dur(paths[i]):5.2f}s | {tr[paths[i]][:60]}", flush=True)
+ if r < MIN_RATIO:
+ seeds[i] += 7919
+ nxt.append(i)
+ pend = nxt
+ if pend:
+ print(f" [{name}] ronda {rnd+2}: reintentando {pend}", flush=True)
+
+ for i, (r, b) in best.items(): # me quedo con el mejor intento, no el ultimo
+ shutil.copy(b, paths[i])
+ score[i] = r
+
+ data = {"name": name, "title": spec["title"], "segments": [
+ {"i": i, "tts": segs[i][0], "sub": segs[i][1], "wav": paths[i],
+ "dur": round(dur(paths[i]), 3), "score": round(score.get(i, 0), 3)}
+ for i in range(len(segs))]}
+ data["total_audio"] = round(sum(s["dur"] for s in data["segments"]), 2)
+ with open(os.path.join(outdir, "segments.json"), "w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+ bad = [s["i"] for s in data["segments"] if s["score"] < MIN_RATIO]
+ print(f"[{name}] listo. audio={data['total_audio']}s sin_verificar={bad}", flush=True)
+
+if __name__ == "__main__":
+ for n in (sys.argv[1:] or list(SCRIPTS)):
+ build(n)
diff --git a/scripts/music.py b/scripts/music.py
new file mode 100644
index 0000000..a3aee16
--- /dev/null
+++ b/scripts/music.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+"""Cama musical original hecha con las muestras de piano de keysound.
+
+Muestra NN => nota MIDI NN+20 (01-A-大字2组 = A0 = MIDI 21)
+Las progresiones son propias, para que el audio sea libre de derechos.
+"""
+import glob, os, wave
+import numpy as np
+
+PIANO_DIR = "/mnt/Data/GitHub/keysound/audio/piano"
+SR = 44100
+
+_paths, _cache = {}, {}
+for _p in glob.glob(os.path.join(PIANO_DIR, "*.wav")):
+ _paths[int(os.path.basename(_p).split("-")[0]) + 20] = _p
+
+NOTE = {"C": 0, "C#": 1, "D": 2, "D#": 3, "E": 4, "F": 5,
+ "F#": 6, "G": 7, "G#": 8, "A": 9, "A#": 10, "B": 11}
+TRIAD = {"maj": [0, 4, 7], "min": [0, 3, 7]}
+
+
+def midi(name, octave):
+ return 12 * (octave + 1) + NOTE[name]
+
+
+def load(m):
+ m = max(21, min(108, m))
+ if m not in _cache:
+ with wave.open(_paths[m], "rb") as w:
+ d = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
+ d = d.reshape(-1, w.getnchannels()).astype(np.float32) / 32768.0
+ if d.shape[1] == 1:
+ d = np.repeat(d, 2, axis=1)
+ _cache[m] = d
+ return _cache[m]
+
+
+def chord(sym):
+ """'Am' -> (bajo, triada). Soporta sostenidos y sufijo m."""
+ q = "min" if sym.endswith("m") else "maj"
+ root = sym[:-1] if sym.endswith("m") else sym
+ r = NOTE[root]
+ bass = 12 * (2 + 1) + r # octava 2
+ triad = [12 * (4 + 1) + r + i for i in TRIAD[q]]
+ return bass, triad
+
+
+# progresion + tempo por video
+BEDS = {
+ "montyhall": (["Am", "F", "C", "G"], 92),
+ "nueves": (["Em", "C", "G", "D"], 88),
+ "cumple": (["C", "G", "Am", "F"], 96),
+ "luna": (["Dm", "A#", "F", "C"], 82),
+ "japones": (["G", "D", "Em", "C"], 100),
+ "benford": (["Am", "Em", "F", "G"], 94),
+ "hilbert": (["C", "Em", "Am", "F"], 80),
+ "reuleaux": (["F", "C", "Dm", "A#"], 98),
+ "regla72": (["G", "Em", "C", "D"], 104),
+ "buffon": (["Am", "G", "F", "E"], 86),
+ "collatz": (["Am", "Em", "Dm", "E"], 90),
+ "simpson": (["Cm", "G#", "A#", "Gm"], 96),
+ "cuerda": (["F", "Am", "Dm", "C"], 84),
+ "gabriel": (["Em", "Bm", "C", "G"], 76),
+ "mobius": (["D", "A", "Bm", "G"], 102),
+ "bayes": (["Dm", "Gm", "A#", "A"], 86),
+ "cicloide": (["A", "E", "F#m", "D"], 106),
+ "dados": (["Em", "Am", "D", "G"], 94),
+ "bolapeluda": (["C", "Am", "Dm", "G"], 100),
+ "caos": (["Am", "F", "Dm", "E"], 88),
+ "diferencial": (["Bm", "G", "D", "A"], 112, "motor"),
+ "galton": (["D", "Bm", "G", "A"], 96),
+ "bloques": (["E", "C#m", "A", "B"], 100),
+ "domino": (["G", "D", "Em", "C"], 104, "motor"),
+}
+
+# melodias propias: por compas, (tiempo en negras, nota MIDI, duracion en negras)
+MELODIAS = {
+ "motor": [
+ [(0, 78, 1.5), (1.5, 74, 0.5), (2, 76, 1), (3, 78, 1)], # Bm
+ [(0, 79, 1.5), (1.5, 78, 0.5), (2, 76, 1), (3, 74, 1)], # G
+ [(0, 78, 1), (1, 81, 1), (2, 78, 0.5), (2.5, 76, 0.5), (3, 74, 1)], # D
+ [(0, 73, 2), (2, 76, 1), (3, 69, 1)], # A
+ ],
+}
+
+
+def bed(name, total_s, gain=0.30, seed=0):
+ prog, bpm = BEDS[name][:2]
+ estilo = BEDS[name][2] if len(BEDS[name]) > 2 else None
+ beat = 60.0 / bpm
+ measure = 4 * beat
+ eighth = beat / 2
+ n = int((total_s + 3.0) * SR)
+ buf = np.zeros((n, 2), dtype=np.float32)
+ rng = np.random.default_rng(seed)
+
+ def hit(m, t, g, max_len=None):
+ s = load(m) * g
+ if max_len is not None:
+ k = min(len(s), int(max_len * SR))
+ s = s[:k].copy()
+ f = min(int(0.06 * SR), k)
+ s[-f:] *= np.linspace(1, 0, f)[:, None]
+ i = int(t * SR)
+ if i >= n:
+ return
+ buf[i:i + len(s)] += s[:n - i]
+
+ k = 0
+ t = 0.0
+ while t < total_s + 0.5:
+ sym = prog[k % len(prog)]
+ bass, triad = chord(sym)
+ if estilo == "motor":
+ # bajo en corcheas, como un motor; acorde en 1 y 3; melodia arriba
+ for j in range(8):
+ hit(bass + (12 if j % 2 else 0), t + j * eighth,
+ 0.34 if j % 2 == 0 else 0.20, max_len=eighth * 0.9)
+ for b_ in (0, 2):
+ for nota in triad:
+ hit(nota - 12, t + b_ * beat, 0.13, max_len=beat * 1.8)
+ vuelta = (k // 4) % 2 # la segunda vuelta, una octava arriba
+ for (b_, nota, d) in MELODIAS[estilo][k % 4]:
+ hit(nota + 12 * vuelta, t + b_ * beat, 0.30, max_len=d * beat * 1.05)
+ t += measure
+ k += 1
+ continue
+ hit(bass, t, 0.50, max_len=measure * 0.95)
+ hit(bass + 12, t + 2 * beat, 0.26, max_len=beat * 1.6)
+ pattern = [triad[0], triad[1], triad[2], triad[1],
+ triad[2], triad[1], triad[2], triad[0]]
+ for j, note in enumerate(pattern):
+ g = 0.30 if j % 2 == 0 else 0.20
+ hit(note, t + j * eighth, g, max_len=beat * 1.15)
+ if k % 4 == 3: # brillito al cerrar la vuelta
+ hit(triad[2] + 12, t + 3.5 * beat, 0.16, max_len=beat)
+ t += measure
+ k += 1
+
+ buf = buf[:int(total_s * SR)]
+ peak = np.abs(buf).max() + 1e-9
+ buf *= gain / peak
+ # entrada y salida suaves
+ fi, fo = int(1.2 * SR), int(2.0 * SR)
+ buf[:fi] *= np.linspace(0, 1, fi)[:, None]
+ buf[-fo:] *= np.linspace(1, 0, fo)[:, None]
+ return buf
+
+
+def duck(music, speech, sr=SR, floor_db=-11.0, block=512, release=0.45):
+ """Baja la musica cuando hay voz (sidechain simple)."""
+ mono = np.abs(speech).mean(axis=1) if speech.ndim > 1 else np.abs(speech)
+ nb = len(mono) // block
+ env = mono[:nb * block].reshape(nb, block).max(axis=1)
+ rel = np.exp(-block / (release * sr))
+ out = np.zeros(nb, dtype=np.float32)
+ acc = 0.0
+ for i, v in enumerate(env):
+ acc = v if v > acc else acc * rel + v * (1 - rel)
+ out[i] = acc
+ g_floor = 10 ** (floor_db / 20)
+ active = np.clip(out / 0.03, 0, 1)
+ gain = 1.0 - (1.0 - g_floor) * active
+ idx = np.linspace(0, nb - 1, len(music))
+ g = np.interp(idx, np.arange(nb), gain).astype(np.float32)
+ return music * g[:, None]
diff --git a/scripts/narration.py b/scripts/narration.py
new file mode 100644
index 0000000..d151fa8
--- /dev/null
+++ b/scripts/narration.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+"""Guiones de los 5 videos. 'tts' = texto que se locuta (numeros en letras),
+'sub' = subtitulo en pantalla (mas corto y legible)."""
+
+SCRIPTS = {
+
+"montyhall": {
+ "title": "El juego que casi todos pierden",
+ "segments": [
+ ("Tres puertas. Atras de una hay un auto cero kilometro.", "3 puertas.\nAtrás de una: un auto 0 km."),
+ ("Atras de las otras dos, una cabra.", "Atrás de las otras dos:\nuna cabra."),
+ ("Vos elegis la puerta uno.", "Elegís la puerta 1."),
+ ("El conductor, que sabe donde esta el auto, abre la puerta tres. Cabra.", "El conductor abre la 3.\nCabra."),
+ ("Y te pregunta: te quedas con la uno, o cambias a la dos?", "¿Te quedás con la 1\no cambiás a la 2?"),
+ ("Casi todos dicen que da igual. Cincuenta y cincuenta. Y estan mal.", "Casi todos dicen 50 y 50.\nEstán mal."),
+ ("Cuando elegiste, tenias un tercio de chance de acertar. Eso nunca cambio.", "Cuando elegiste tenías 1/3.\nEso no cambió."),
+ ("O sea que las otras dos puertas juntas suman dos tercios.", "Las otras dos juntas: 2/3."),
+ ("El conductor te saco la mala. Todo ese dos tercios se mudo a la puerta dos.", "Él te sacó la mala.\nLos 2/3 se mudan a la 2."),
+ ("Si cambias, ganas el sesenta y siete por ciento de las veces. El doble.", "Cambiando ganás el 67%.\nEl doble."),
+ ("Cambia siempre. Y escribi abajo si vos tambien decias cincuenta y cincuenta.", "Cambiá siempre.\n¿Vos qué decías?"),
+ ],
+},
+
+"nueves": {
+ "title": "0.999... es igual a 1",
+ "segments": [
+ ("Cero coma nueve, nueve, nueve, infinitos nueves.", "0,999999...\ninfinitos nueves"),
+ ("Eso es exactamente igual a uno. Y te vas a enojar.", "Eso es exactamente = 1.\nY te vas a enojar."),
+ ("Ya se lo que pensas: le falta un poquito.", "«Le falta un poquito»"),
+ ("Perfecto. Decime cuanto le falta. Escribi ese numero.", "Decime cuánto le falta.\nEscribilo."),
+ ("No podes. Porque si existiera un numero en el medio, yo le agrego mas nueves y te lo paso.", "No podés.\nSiempre agrego otro 9."),
+ ("Y dos numeros sin nada en el medio son el mismo numero.", "Dos números sin nada\nen el medio son el mismo."),
+ ("Mira la prueba mas linda. Un tercio es cero coma tres periodico.", "1/3 = 0,3333..."),
+ ("Multiplicamos los dos lados por tres.", "×3 de los dos lados"),
+ ("Tres tercios es uno. Y cero coma tres periodico por tres es cero coma nueve periodico.", "3/3 = 1\n0,333... × 3 = 0,999..."),
+ ("Listo. Son el mismo numero. No se acerca a uno: es uno.", "Son el mismo número.\nNo se acerca: ES 1."),
+ ("Deci en los comentarios si te convenci o si me queres pelear.", "¿Te convencí\no me querés pelear?"),
+ ],
+},
+
+"cumple": {
+ "title": "La paradoja del cumpleaños",
+ "segments": [
+ ("Cuanta gente necesitas en una pieza para que dos cumplan el mismo dia?", "¿Cuánta gente para que dos\ncumplan el mismo día?"),
+ ("La mayoria dice ciento ochenta y pico. La respuesta es veintitres.", "La mayoría dice 180.\nLa respuesta: 23."),
+ ("Con veintitres personas ya tenes cincuenta por ciento de chance. Mitad y mitad.", "Con 23 personas: 50%.\nMitad y mitad."),
+ ("El error es pensar que te comparas vos contra todos los demas.", "El error: pensar que sos\nvos contra todos."),
+ ("Pero no. Se comparan todos contra todos.", "Se comparan\ntodos contra todos."),
+ ("Con veintitres personas hay doscientos cincuenta y tres parejas posibles.", "23 personas =\n253 parejas posibles."),
+ ("Cada pareja tiene una chance chiquita. Pero doscientas cincuenta y tres veces.", "Chance chica,\npero 253 veces."),
+ ('Con treinta personas, ya es el setenta por ciento.', '30 personas → 70%'),
+ ('Con cincuenta personas, el noventa y siete por ciento.', '50 personas → 97%'),
+ ('Con setenta personas, el noventa y nueve coma nueve. Practicamente seguro.', '70 personas → 99,9%'),
+ ("Probalo en tu grupo del trabajo. Despues me contas.", "Probalo en tu grupo.\nDespués contame."),
+ ],
+},
+
+"luna": {
+ "title": "Doblar un papel hasta la Luna",
+ "segments": [
+ ("Agarra una hoja de papel y doblala cuarenta y dos veces. Llegas a la Luna.", "Doblá una hoja 42 veces\ny llegás a la Luna."),
+ ("Suena a mentira. Hagamos la cuenta juntos.", "Suena a mentira.\nHagamos la cuenta."),
+ ("Una hoja mide un decimo de milimetro de espesor.", "1 hoja = 0,1 mm"),
+ ("Un doblez, dos decimos. Dos dobleces, cuatro decimos.", "1 doblez: 0,2 mm\n2 dobleces: 0,4 mm"),
+ ("Con diez dobleces tenes diez centimetros. Un ladrillo.", "10 dobleces → 10 cm"),
+ ("Con veinte dobleces, cien metros. Un edificio de treinta pisos.", "20 dobleces → 100 m"),
+ ("Con treinta dobleces, cien kilometros. Ya saliste de la atmosfera.", "30 dobleces → 100 km\n(fuera de la atmósfera)"),
+ ("Con cuarenta dobleces, cien mil kilometros.", "40 dobleces → 100.000 km"),
+ ("Con cuarenta y dos, cuatrocientos cuarenta mil kilometros.", "42 dobleces →\n440.000 km"),
+ ("Y la Luna esta a trescientos ochenta y cuatro mil. Te pasaste de largo.", "La Luna está a 384.000 km.\nTe pasaste."),
+ ("Eso es crecimiento exponencial. Por eso nadie lo ve venir.", "Eso es crecimiento\nexponencial."),
+ ],
+},
+
+"japones": {
+ "title": "Multiplicar con líneas",
+ "segments": [
+ ("Asi multiplican en Japon y no necesitas saberte las tablas.", "Así multiplican en Japón.\nSin tablas."),
+ ("Vamos con veintiuno por trece.", "21 × 13"),
+ ("El veintiuno lo dibujas asi: dos lineas, espacio, una linea.", "21 → 2 líneas\ny 1 línea"),
+ ('El trece va cruzado. Una linea, y despues tres lineas.', '13 → 1 línea\ny 3 líneas'),
+ ('Ahora contas los cruces por zona. La zona izquierda tiene dos cruces.', 'Zona izquierda:\n2 cruces'),
+ ("Zona del medio: seis mas uno. Siete cruces.", "Zona del medio:\n6 + 1 = 7"),
+ ("Zona derecha: tres cruces.", "Zona derecha:\n3 cruces"),
+ ("Dos, siete, tres. Doscientos setenta y tres.", "2 | 7 | 3\n= 273"),
+ ('Y efectivamente, veintiuno por trece da doscientos setenta y tres.', '21 × 13 = 273 ✓'),
+ ("Si una zona te da mas de nueve, llevas una a la de al lado. Igual que siempre.", "¿Más de 9?\nLlevás una al lado."),
+ ("Guardalo para cuando tus hijos te pidan ayuda con la tarea.", "Guardalo para\nla tarea de tus hijos."),
+ ],
+},
+
+
+"benford": {
+ "title": 'Así se detecta el fraude',
+ "segments": [
+ ('Agarra cualquier lista de numeros reales. Facturas, poblaciones, sueldos.', 'Cualquier lista real:\nfacturas, sueldos, ciudades'),
+ ('Fijate con que digito empiezan. Uno, dos, tres.', '¿Con qué dígito\nempiezan?'),
+ ('Vos dirias que todos aparecen mas o menos lo mismo. Once por ciento cada uno.', 'Dirías: 11% cada uno.'),
+ ('Y no. El uno aparece el treinta por ciento de las veces.', 'El 1 aparece el 30%\nde las veces.'),
+ ('El dos, el diecisiete. Y va bajando hasta el nueve, que aparece el cuatro por ciento.', 'El 2: 17%.\nEl 9: apenas 4%.'),
+ ('Se llama ley de Benford, y pasa en casi todo lo que crece.', 'Ley de Benford'),
+ ('Poblaciones, precios, likes, el largo de los rios.', 'Poblaciones, precios,\nlikes, ríos…'),
+ ('Y aca viene lo bueno. Cuando alguien inventa numeros, los inventa parejos.', 'El que inventa números\nlos inventa parejos.'),
+ ('Entonces el patron se rompe, y eso se ve de lejos.', 'El patrón se rompe.'),
+ ('Asi agarraron fraudes contables y elecciones dibujadas, de verdad.', 'Así agarraron fraudes\ncontables reales.'),
+ ('Proba con los gastos de tu tarjeta. Te va a dar.', 'Probá con tu\nresumen de tarjeta.'),
+ ],
+},
+
+"hilbert": {
+ "title": 'El hotel infinito',
+ "segments": [
+ ('Un hotel con infinitas habitaciones, y todas ocupadas.', 'Hotel con infinitas\nhabitaciones. Lleno.'),
+ ('Llega un huesped mas. Hay lugar?', 'Llega uno más.\n¿Hay lugar?'),
+ ('Si. El de la uno se pasa a la dos, el de la dos a la tres, y asi.', 'El 1 pasa a la 2,\nel 2 a la 3…'),
+ ('Todos se corren uno. La habitacion uno queda libre.', 'Todos se corren uno.\nLa 1 queda libre.'),
+ ('Ahora llegan infinitos huespedes de golpe. Ahi ya no entra nadie, no?', 'Ahora llegan INFINITOS.\n¿Ahí sí no entran?'),
+ ('Entran. Cada uno se pasa a la habitacion del doble de su numero.', 'Cada uno pasa\nal doble de su número.'),
+ ('El uno va a la dos, el dos a la cuatro, el tres a la seis.', '1→2, 2→4, 3→6…'),
+ ('Quedan libres todas las impares. Infinitas habitaciones vacias.', 'Quedan libres\ntodas las impares.'),
+ ('El hotel estaba lleno y le entraron infinitas personas mas.', 'Estaba lleno y entraron\ninfinitos más.'),
+ ('Eso es el infinito. No es un numero grande, es otra cosa.', 'El infinito no es\nun número grande.'),
+ ('Se llama hotel de Hilbert. Contaselo a alguien y mirale la cara.', 'Hotel de Hilbert.\nContáselo a alguien.'),
+ ],
+},
+
+"reuleaux": {
+ "title": 'Por qué son redondas',
+ "segments": [
+ ('Por que las tapas de alcantarilla son redondas?', '¿Por qué las tapas\nson redondas?'),
+ ('Porque son las unicas que no se pueden caer adentro del pozo.', 'Porque no se pueden\ncaer adentro.'),
+ ('Una tapa cuadrada, girada en diagonal, se cae.', 'La cuadrada,\nen diagonal, se cae.'),
+ ('Porque la diagonal del cuadrado es mas larga que el lado.', 'La diagonal es más\nlarga que el lado.'),
+ ('El circulo no tiene ese problema. Mide lo mismo en todas las direcciones.', 'El círculo mide igual\nen toda dirección.'),
+ ('Pero ojo, que no es la unica figura que cumple eso.', 'No es la única\nque lo cumple.'),
+ ('Mira este triangulo con los lados curvos.', 'Mirá este triángulo\nde lados curvos.'),
+ ('Se llama triangulo de Reuleaux, y tambien mide lo mismo en todas las direcciones.', 'Triángulo de Reuleaux'),
+ ('Rueda perfecto entre dos paredes, aunque no sea redondo.', 'Rueda perfecto,\naunque no sea redondo.'),
+ ('Si lo usas de rueda, la carga de arriba no sube ni baja.', 'De rueda: la carga\nno sube ni baja.'),
+ ('Ahora ya sabes por que son redondas. Contalo en el proximo asado.', 'Contalo en\nel próximo asado.'),
+ ],
+},
+
+"regla72": {
+ "title": 'La regla del 72',
+ "segments": [
+ ('Cuanto tarda tu plata en duplicarse? Hay una cuenta que te lo dice en dos segundos.', '¿Cuánto tarda tu plata\nen duplicarse?'),
+ ('Se llama la regla del setenta y dos.', 'La regla del 72'),
+ ('Dividis setenta y dos por la tasa anual, y te da los años.', '72 ÷ tasa anual\n= años'),
+ ('Al seis por ciento anual: setenta y dos dividido seis, doce años.', '6% anual →\n72 ÷ 6 = 12 años'),
+ ('Al nueve por ciento: ocho años.', '9% anual →\n8 años'),
+ ('Al doce por ciento: seis años.', '12% anual →\n6 años'),
+ ('Y lo mejor es que funciona para el otro lado tambien.', 'También funciona\nal revés.'),
+ ('Con treinta por ciento de inflacion anual, tu plata vale la mitad en dos años y medio.', '30% de inflación:\nvale la mitad en 2,4 años'),
+ ('Setenta y dos dividido treinta: dos coma cuatro.', '72 ÷ 30 = 2,4'),
+ ('Es una aproximacion, pero se clava.', 'Es aproximada,\npero se clava.'),
+ ('Guardate esta. Es la cuenta que mas veces vas a usar en tu vida.', 'Guardala.\nLa vas a usar siempre.'),
+ ],
+},
+
+"buffon": {
+ "title": 'Sacar pi tirando agujas',
+ "segments": [
+ ('Te tiro agujas al piso y te saco el numero pi. En serio.', 'Tiro agujas al piso\ny sale π.'),
+ ('Piso rayado. La distancia entre rayas es igual al largo de la aguja.', 'Rayas separadas\nel largo de la aguja.'),
+ ('Ahora tiras agujas al azar. Muchas.', 'Tirás agujas\nal azar.'),
+ ('Algunas van a cruzar una raya. Otras no.', 'Algunas cruzan.\nOtras no.'),
+ ('Contas cuantas tiraste, y cuantas cruzaron.', 'Contás cuántas\ny cuántas cruzan.'),
+ ('Y dividis el doble del total por las que cruzaron.', '2 × total ÷ cruces'),
+ ('Y el resultado que te da es el numero pi.', '= π'),
+ ('Con sesenta agujas ya se acerca. Con doscientas, se acerca mucho mas.', '60 agujas: se acerca.\n200: mucho más.'),
+ ('Tres coma uno cuatro uno cinco.', '3,1415…'),
+ ('Y no hay ningun circulo en la escena. Igual aparece pi.', 'No hay ningún círculo.\nY aparece π.'),
+ ('Se llama aguja de Buffon. Lo podes hacer en tu casa con fosforos.', 'Aguja de Buffon.\nHacelo con fósforos.'),
+ ],
+},
+"collatz": {
+ "title": "El problema que nadie pudo resolver",
+ "segments": [
+ ('Agarra cualquier numero entero. El que se te ocurra.', 'Agarrá cualquier número.'),
+ ('Si es par, lo dividis por dos. Si es impar, lo multiplicas por tres y le sumas uno.', 'Par → ÷2\nImpar → ×3+1'),
+ ('Y repetis para siempre. Probemos con el seis.', 'Y repetís.\nProbemos con el 6.'),
+ ('Seis, tres, diez, cinco, dieciseis, ocho, cuatro, dos, uno.', '6 → 3 → 10 → 5 → 16\n→ 8 → 4 → 2 → 1'),
+ ('Cayo al uno. Probemos ahora con el siete.', 'Cayó al 1.\nAhora el 7.'),
+ ('Sube, baja, vuelve a subir mucho mas alto, y al final tambien termina en uno.', 'Sube, baja, sube más...\ny termina en 1.'),
+ ('El veintisiete es el mas rebelde. Trepa hasta nueve mil doscientos treinta y dos.', 'El 27 trepa\nhasta 9.232.'),
+ ('Pero despues de ciento once pasos, tambien se cae al uno.', 'Y en 111 pasos\ncae al 1.'),
+ ('Lo probaron con computadoras hasta numeros de veinte cifras. Todos, absolutamente todos, caen al uno.', 'Probado hasta\n20 cifras. Todos caen.'),
+ ('Pero nadie en el mundo consiguio demostrarlo. Hace casi noventa años que el problema sigue abierto.', 'Nadie pudo demostrarlo.\n90 años abierto.'),
+ ('Se llama conjetura de Collatz. Deja tu numero en los comentarios y lo probamos.', 'Conjetura de Collatz.\nDejá tu número.'),
+ ],
+},
+
+"simpson": {
+ "title": "El promedio te está mintiendo",
+ "segments": [
+ ('Dos tratamientos para la misma enfermedad. Les decimos A y B.', 'Dos tratamientos: A y B.'),
+ ('En los casos leves, el A cura al noventa y tres por ciento. El B, al ochenta y siete.', 'Casos leves:\nA 93% · B 87%'),
+ ('Gana el A. Y en los casos graves, el A cura al setenta y tres por ciento. El B, al sesenta y nueve.', 'Gana A.\nGraves: A 73% · B 69%'),
+ ('Tambien gana el A. Entonces el A es mejor siempre, no?', 'Gana A otra vez.\nA es mejor, ¿no?'),
+ ('Sumamos todos los pacientes. El A cura al setenta y ocho por ciento. El B, al ochenta y tres.', 'Todos juntos:\nA 78% · B 83%'),
+ ('Gana el B. Y no hay ningun error en las cuentas.', 'Gana B.\nY no hay ningún error.'),
+ ('Esto se llama la paradoja de Simpson, y pasa por como estan repartidos los pacientes.', 'Paradoja de Simpson.'),
+ ('Casi todos los casos graves, que son los dificiles de curar, se los dieron al tratamiento A.', 'A recibió casi todos\nlos casos graves.'),
+ ('Al B le mandaron casi todos los leves, que son faciles. Por eso su promedio sube.', 'B recibió los leves.\nPor eso su promedio sube.'),
+ ('El promedio total esta mezclando dos grupos distintos. Y esos numeros son reales, salen de un estudio medico.', 'El promedio mezcla\ndos grupos distintos.'),
+ ('Cada vez que te muestren un promedio, pregunta: promedio de quienes? Mandaselo al que te tira estadisticas.', 'Preguntá siempre:\n¿promedio de quiénes?'),
+ ],
+},
+
+"cuerda": {
+ "title": "Una cuerda alrededor de la Tierra",
+ "segments": [
+ ('Imaginate una cuerda que da toda la vuelta a la Tierra, pegadita al suelo, por el ecuador.', 'Una cuerda que rodea\nla Tierra entera.'),
+ ('Son unos cuarenta mil kilometros de cuerda.', '40.000 km de cuerda.'),
+ ('Ahora le agregamos un metro. Un metro solo.', 'Le agregamos 1 metro.'),
+ ('Y la levantamos pareja, a la misma altura en todo el planeta.', 'Y la levantamos pareja\nen todo el planeta.'),
+ ('Cuanto se separa del suelo? Casi todos dicen que nada, un pelo.', '¿Cuánto se separa?\nCasi nadie acierta.'),
+ ('Se separa quince coma nueve centimetros. Te pasa un gato caminando por abajo.', '15,9 cm.\nPasa un gato por abajo.'),
+ ('La cuenta es de una sola linea. El radio de un circulo es su perimetro dividido dos pi.', 'radio = perímetro ÷ 2π'),
+ ('Si al perimetro le sumas un metro, el radio crece un metro dividido dos pi.', '+1 m de perímetro\n→ +1÷2π de radio'),
+ ('Eso da quince coma nueve centimetros. Y fijate bien: el tamaño de la Tierra no aparece en la cuenta.', '= 15,9 cm\nLa Tierra ni aparece.'),
+ ('O sea que si haces lo mismo con una pelota de tenis, la cuerda tambien se levanta quince coma nueve centimetros.', 'Con una pelota de tenis:\ntambién 15,9 cm.'),
+ ('El tamaño no importa. Mandaselo al que te dijo que no se levantaba nada.', 'El tamaño no importa.\nMandáselo al que dudó.'),
+ ],
+},
+
+"gabriel": {
+ "title": "La figura que no se puede pintar",
+ "segments": [
+ ('Esta figura rompe la cabeza. Se llama el cuerno de Gabriel.', 'El cuerno de Gabriel.'),
+ ('Agarras la curva uno dividido equis, y la haces girar como en un torno.', 'Tomás y = 1÷x\ny la hacés girar.'),
+ ('Te queda un cuerno infinito, cada vez mas finito, que nunca termina.', 'Un cuerno infinito,\ncada vez más fino.'),
+ ('Primera pregunta: cuanta pintura entra adentro?', '¿Cuánta pintura\nentra adentro?'),
+ ('El volumen da exactamente pi. Un numero finito. Tres litros y monedas.', 'Volumen = π\n≈ 3,14 litros'),
+ ('Lo llenas con un balde chico y te sobra.', 'Lo llenás con un balde\ny te sobra.'),
+ ('Segunda pregunta: y si lo queres pintar por fuera?', '¿Y si lo pintás\npor fuera?'),
+ ('La superficie es infinita. No te alcanza toda la pintura del universo.', 'La superficie\nes INFINITA.'),
+ ('O sea: podes llenarlo de pintura por dentro, pero no podes pintarlo.', 'Podés llenarlo,\npero no pintarlo.'),
+ ('La trampa es que la pintura de verdad tiene espesor, y el cuerno se hace mas fino que cualquier espesor.', 'La trampa: la pintura\nreal tiene espesor.'),
+ ('Con el infinito, la intuicion se rompe. Segui para mas matematica rara.', 'Con el infinito\nla intuición se rompe.'),
+ ],
+},
+
+"mobius": {
+ "title": "Cortá esto y no vas a creer",
+ "segments": [
+ ('Agarra una tira de papel. Cualquiera sirve.', 'Agarrá una tira de papel.'),
+ ('Le das media vuelta a una punta, y recien ahi pegas los dos extremos.', 'Media vuelta\ny pegá las puntas.'),
+ ('Eso es una cinta de Mobius, y tiene una sola cara.', 'Cinta de Möbius.\nUna sola cara.'),
+ ('Si apoyas el dedo y lo arrastras sin levantarlo, pintas todo. El adentro y el afuera son lo mismo.', 'Pintás todo\nsin levantar el dedo.'),
+ ('Tambien tiene un solo borde. Es un camino unico que da dos vueltas enteras.', 'Y un solo borde.'),
+ ('Ahora hace esto: cortala al medio, a lo largo, hasta volver al principio.', 'Cortala al medio,\na lo largo.'),
+ ('Esperarias que queden dos cintas. Pero no. Queda una sola, el doble de larga.', 'No salen dos.\nSale una, el doble de larga.'),
+ ('Y esa cinta nueva ya tiene dos caras. Perdio la magia.', 'Y esa ya tiene\ndos caras.'),
+ ('Probemos de nuevo, con otra cinta. Pero ahora cortando a un tercio del borde.', 'Otra vez, pero cortando\na 1/3 del borde.'),
+ ('Salen dos cintas de distinto tamaño, y quedan enganchadas una con la otra.', 'Salen 2 cintas,\nenganchadas entre sí.'),
+ ('Agarra una hoja y la cinta de embalar y hacelo ahora. Te espero en los comentarios.', 'Hacelo en casa.\nContame cómo te fue.'),
+ ],
+},
+
+
+"bayes": {
+ "title": "Tu test dio positivo",
+ "segments": [
+ ('Te haces un analisis para detectar una enfermedad. El test acierta el noventa y nueve por ciento de las veces.', 'Un test que acierta\nel 99% de las veces.'),
+ ('Te da positivo. Que chance hay de que estes enfermo de verdad?', 'Te da POSITIVO.\n¿Qué chance hay\nde estar enfermo?'),
+ ('Casi todos contestan noventa y nueve por ciento. Un monton de medicos tambien.', 'Casi todos dicen 99%.\nMuchos médicos también.'),
+ ('Es nueve por ciento. Y el test no fallo: esta funcionando perfecto.', 'Es 9%.\nY el test no falló.'),
+ ('Mira. Esta enfermedad la tiene una persona de cada mil.', 'La enfermedad la tiene\n1 de cada 1000.'),
+ ('Juntamos mil personas. Una sola esta enferma.', '1000 personas.\nUna está enferma.'),
+ ('Les hacemos el test a todas. A la enferma la encuentra.', 'El test la encuentra.\n1 posi