diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/build_blender.py | 58 | ||||
| -rw-r--r-- | scripts/build_video.py | 93 | ||||
| -rw-r--r-- | scripts/gen_audio.py | 119 | ||||
| -rw-r--r-- | scripts/music.py | 166 | ||||
| -rw-r--r-- | scripts/narration.py | 431 | ||||
| -rw-r--r-- | scripts/numspell.py | 57 | ||||
| -rw-r--r-- | scripts/regen.py | 46 | ||||
| -rw-r--r-- | scripts/repick.py | 48 | ||||
| -rw-r--r-- | scripts/verify.py | 53 | ||||
| -rw-r--r-- | scripts/verify_final.py | 50 |
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 |