From fafaebb051907a848a9406f9da19669c81a83a3b Mon Sep 17 00:00:00 2001 From: Elvis Claros Castro Date: Sat, 26 Sep 2026 20:50:41 -0300 Subject: Translate code, comments and logs to English; English README; configurable paths and env vars Identifiers, docstrings, comments and console messages are now in English. Narration, subtitles and on-screen text stay in Spanish (they are the video content). The Blender <-> Godot physics protocol uses English keys and body prefixes chosen to keep the original creation order, so cached simulations and renders stay bit-identical. The old Spanish environment variable names are still accepted. --- scripts/build_blender.py | 32 ++++++++++++++++---------------- scripts/build_video.py | 4 ++-- scripts/gen_audio.py | 39 ++++++++++++++++++++++----------------- scripts/music.py | 40 ++++++++++++++++++++-------------------- scripts/narration.py | 4 ++-- scripts/numspell.py | 18 +++++++++--------- scripts/regen.py | 6 +++--- scripts/repick.py | 4 ++-- scripts/verify.py | 2 +- scripts/verify_final.py | 32 ++++++++++++++++---------------- 10 files changed, 93 insertions(+), 88 deletions(-) (limited to 'scripts') diff --git a/scripts/build_blender.py b/scripts/build_blender.py index 41731cf..1a80d97 100644 --- a/scripts/build_blender.py +++ b/scripts/build_blender.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""Arma el video final de las escenas hechas en Blender. +"""Builds the final video of the scenes made in 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. +Composites in a single ffmpeg pass: + dotted background + PNG sequence with alpha (3D) + Manim overlay + audio +The overlay carries the chip, progress bar and subtitles, so the style comes +from the same code as the Manim videos. """ import glob, os, subprocess, sys @@ -15,23 +15,23 @@ 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") +def compose(name, add_music=True): + background = os.path.join(ROOT, "media", "images", "overlay", "fondo.png") + folder = os.path.join(ROOT, "render", name) + seq = os.path.join(folder, "f%04d.png") overlay = os.path.join(ROOT, "media", "videos", "overlay", "1920p30", f"{name}.mov") - for p in (fondo, overlay): + for p in (background, overlay): if not os.path.exists(p): - raise SystemExit(f"falta {p}") - n = len(glob.glob(os.path.join(carpeta, "f*.png"))) + raise SystemExit(f"missing {p}") + n = len(glob.glob(os.path.join(folder, "f*.png"))) if n < 30: - raise SystemExit(f"[{name}] solo hay {n} frames renderizados") + raise SystemExit(f"[{name}] only {n} frames rendered") - wav = build_video.build_track(name, con_musica) + wav = build_video.build_track(name, add_music) out = os.path.join(ROOT, "out", f"{name}.mp4") cmd = [ "ffmpeg", "-v", "error", "-y", - "-loop", "1", "-framerate", str(FPS), "-i", fondo, + "-loop", "1", "-framerate", str(FPS), "-i", background, "-framerate", str(FPS), "-start_number", "1", "-i", seq, "-i", overlay, "-i", wav, @@ -55,4 +55,4 @@ def componer(name, con_musica=True): if __name__ == "__main__": for n in (sys.argv[1:] or ["bolapeluda", "caos", "diferencial"]): - componer(n) + compose(n) diff --git a/scripts/build_video.py b/scripts/build_video.py index 572e3b5..046625e 100644 --- a/scripts/build_video.py +++ b/scripts/build_video.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Arma la pista final (voz + piano con ducking) siguiendo el timeline de Manim -y la pega al video renderizado.""" +"""Builds the final track (voice + piano with ducking) following the Manim +timeline and muxes it onto the rendered video.""" import json, os, subprocess, sys, wave import numpy as np diff --git a/scripts/gen_audio.py b/scripts/gen_audio.py index 9dbbb47..4b2cb29 100644 --- a/scripts/gen_audio.py +++ b/scripts/gen_audio.py @@ -1,6 +1,6 @@ # -*- 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.""" +"""Generates one WAV per segment with qwen-tts (cloned Rioplatense voice), +checks each one by transcribing it with whisper and retries the bad ones.""" import json, os, re, shutil, subprocess, sys, unicodedata, wave from difflib import SequenceMatcher @@ -9,13 +9,18 @@ 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") +# qwentts.cpp checkout and the cloned reference voice. The voice is not part of +# the repository: VOICE_REF is the path prefix of its .spk/.rvq/.txt files +# (produced by `qwen-codec --talker` from a recording and its transcript). +QWENTTS = os.path.expanduser(os.environ.get("QWENTTS_DIR", "~/GIT-MIRRO/qwentts.cpp")) +TTS = os.path.join(QWENTTS, "build", "qwen-tts") +MODEL = os.path.join(QWENTTS, "models", "qwen-talker-1.7b-base-Q8_0.gguf") +CODEC = os.path.join(QWENTTS, "models", "qwen-tokenizer-12hz-Q8_0.gguf") +VOICE_REF = os.path.expanduser(os.environ.get("VOICE_REF", "/tmp/Firefox/output")) +REF = dict(spk=VOICE_REF + ".spk", rvq=VOICE_REF + ".rvq", txt=VOICE_REF + ".txt") -MIN_RATIO = 0.90 # similitud minima aceptada contra la transcripcion -TIMEOUT = 150 # segundos por segmento; mas que eso es generacion desbocada +MIN_RATIO = 0.90 # minimum accepted similarity against the transcription +TIMEOUT = 150 # seconds per segment; more than that is runaway generation MAX_ROUNDS = 4 def norm(s): @@ -38,18 +43,18 @@ def synth(text, out, seed, timeout=TIMEOUT): 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.""" +def synth_safe(text, out, seed, label=""): + """Sometimes the model never emits an end and generates forever: cut and change the seed.""" 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") + print(f" {label} hung with seed {seed + 7919 * k}, retrying", flush=True) + raise RuntimeError(f"{label}: three attempts hung") def transcribe(files, workdir): - """Una sola invocacion de whisper para todos los archivos (carga el modelo una vez).""" + """A single whisper invocation for all files (the model loads once).""" if not files: return {} subprocess.run(["conda", "run", "-n", "stt", "--no-capture-output", @@ -73,7 +78,7 @@ def build(name): seeds = [1000 + i for i in range(len(segs))] pend = list(range(len(segs))) score = {} - best = {} # i -> (ratio, ruta del mejor intento) + best = {} # i -> (ratio, path of the best attempt) keep = os.path.join(work, "best") os.makedirs(keep, exist_ok=True) @@ -81,7 +86,7 @@ def build(name): if not pend: break for i in pend: - seeds[i] = synth_seguro(segs[i][0], paths[i], seeds[i], f"[{name}] {i:02d}") + seeds[i] = synth_safe(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: @@ -100,7 +105,7 @@ def build(name): 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 + for i, (r, b) in best.items(): # keep the best attempt, not the last one shutil.copy(b, paths[i]) score[i] = r @@ -112,7 +117,7 @@ def build(name): 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) + print(f"[{name}] done. audio={data['total_audio']}s unverified={bad}", flush=True) if __name__ == "__main__": for n in (sys.argv[1:] or list(SCRIPTS)): diff --git a/scripts/music.py b/scripts/music.py index a3aee16..2ab6ed2 100644 --- a/scripts/music.py +++ b/scripts/music.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- -"""Cama musical original hecha con las muestras de piano de keysound. +"""Original music bed made with the keysound piano samples. -Muestra NN => nota MIDI NN+20 (01-A-大字2组 = A0 = MIDI 21) -Las progresiones son propias, para que el audio sea libre de derechos. +Sample NN => MIDI note NN+20 (01-A-大字2组 = A0 = MIDI 21) +The progressions are original, so the audio is royalty free. """ import glob, os, wave import numpy as np -PIANO_DIR = "/mnt/Data/GitHub/keysound/audio/piano" +PIANO_DIR = os.environ.get("PIANO_DIR", "/mnt/Data/GitHub/keysound/audio/piano") # keysound samples SR = 44100 _paths, _cache = {}, {} @@ -36,16 +36,16 @@ def load(m): def chord(sym): - """'Am' -> (bajo, triada). Soporta sostenidos y sufijo m.""" + """'Am' -> (bass, triad). Supports sharps and the m suffix.""" 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 + bass = 12 * (2 + 1) + r # octave 2 triad = [12 * (4 + 1) + r + i for i in TRIAD[q]] return bass, triad -# progresion + tempo por video +# progression + tempo per video BEDS = { "montyhall": (["Am", "F", "C", "G"], 92), "nueves": (["Em", "C", "G", "D"], 88), @@ -73,8 +73,8 @@ BEDS = { "domino": (["G", "D", "Em", "C"], 104, "motor"), } -# melodias propias: por compas, (tiempo en negras, nota MIDI, duracion en negras) -MELODIAS = { +# original melodies: per bar, (time in quarter notes, MIDI note, duration in quarter notes) +MELODIES = { "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 @@ -86,7 +86,7 @@ MELODIAS = { 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 + style = BEDS[name][2] if len(BEDS[name]) > 2 else None beat = 60.0 / bpm measure = 4 * beat eighth = beat / 2 @@ -111,17 +111,17 @@ def bed(name, total_s, gain=0.30, seed=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 + if style == "motor": + # bass in eighth notes, like an engine; chord on 1 and 3; melody on top 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) + for note_txt in triad: + hit(note_txt - 12, t + b_ * beat, 0.13, max_len=beat * 1.8) + turn = (k // 4) % 2 # the second pass, an octave up + for (b_, note_txt, d) in MELODIES[style][k % 4]: + hit(note_txt + 12 * turn, t + b_ * beat, 0.30, max_len=d * beat * 1.05) t += measure k += 1 continue @@ -132,7 +132,7 @@ def bed(name, total_s, gain=0.30, seed=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 + if k % 4 == 3: # a little sparkle when the pass closes hit(triad[2] + 12, t + 3.5 * beat, 0.16, max_len=beat) t += measure k += 1 @@ -140,7 +140,7 @@ def bed(name, total_s, gain=0.30, seed=0): buf = buf[:int(total_s * SR)] peak = np.abs(buf).max() + 1e-9 buf *= gain / peak - # entrada y salida suaves + # soft fade in and out 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] @@ -148,7 +148,7 @@ def bed(name, total_s, gain=0.30, seed=0): def duck(music, speech, sr=SR, floor_db=-11.0, block=512, release=0.45): - """Baja la musica cuando hay voz (sidechain simple).""" + """Lowers the music when there is voice (simple sidechain).""" 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) diff --git a/scripts/narration.py b/scripts/narration.py index d151fa8..e4471a0 100644 --- a/scripts/narration.py +++ b/scripts/narration.py @@ -1,6 +1,6 @@ # -*- 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 of the videos. 'tts' = text that is spoken (numbers spelled out), +'sub' = on-screen subtitle (shorter and easier to read).""" SCRIPTS = { diff --git a/scripts/numspell.py b/scripts/numspell.py index 2e416e3..9b5979e 100644 --- a/scripts/numspell.py +++ b/scripts/numspell.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Pasa los numeros de una transcripcion a palabras, para poder compararla -con el guion (que escribe todo en letras).""" +"""Turns the numbers in a transcription into words, so it can be compared +with the script (which spells everything out).""" import re U = ["cero", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve", @@ -30,11 +30,11 @@ def spell_int(n): return _lt1000(n) if n < 1000000: m, r = divmod(n, 1000) - pre = "mil" if m == 1 else _lt1000(m) + " mil" - return pre + (" " + _lt1000(r) if r else "") + pre_frames = "mil" if m == 1 else _lt1000(m) + " mil" + return pre_frames + (" " + _lt1000(r) if r else "") m, r = divmod(n, 1000000) - pre = "un millon" if m == 1 else _lt1000(m) + " millones" - return pre + (" " + spell_int(r) if r else "") + pre_frames = "un millon" if m == 1 else _lt1000(m) + " millones" + return pre_frames + (" " + spell_int(r) if r else "") def _tok(m): @@ -42,10 +42,10 @@ def _tok(m): s = re.sub(r"\.(?=\d{3}\b)", "", s) # 384.000 -> 384000 if "," in s: a, b = s.split(",", 1) - ent = spell_int(int(a or 0)) + whole = spell_int(int(a or 0)) dec = " ".join(U[int(c)] for c in b if c.isdigit()) - return f"{ent} coma {dec}" - if "." in s: # 0.5 estilo ingles + return f"{whole} coma {dec}" + if "." in s: # 0.5 English style a, b = s.split(".", 1) return f"{spell_int(int(a or 0))} coma " + " ".join(U[int(c)] for c in b if c.isdigit()) return spell_int(int(s)) diff --git a/scripts/regen.py b/scripts/regen.py index 7ff69a8..4158d12 100644 --- a/scripts/regen.py +++ b/scripts/regen.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -"""Regenera segmentos puntuales probando varias semillas y quedandose con la mejor.""" +"""Regenerates specific segments trying several seeds and keeping the best.""" import json, os, subprocess, sys, shutil sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from narration import SCRIPTS -from gen_audio import synth_seguro, transcribe, dur, ROOT +from gen_audio import synth_safe, transcribe, dur, ROOT from verify import norm from difflib import SequenceMatcher @@ -22,7 +22,7 @@ def main(name, idxs): cands = [] for k in range(TRIES): c = os.path.join(work, f"{i:02d}_{k}.wav") - synth_seguro(text, c, 4242 + 977 * k, f"[{name}] {i:02d}") + synth_safe(text, c, 4242 + 977 * k, f"[{name}] {i:02d}") cands.append(c) tr = transcribe(cands, work) scored = sorted(((SequenceMatcher(None, norm(text, False), diff --git a/scripts/repick.py b/scripts/repick.py index 5f75bcf..6a32e8a 100644 --- a/scripts/repick.py +++ b/scripts/repick.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Vuelve a elegir el mejor candidato ya sintetizado, comparando numeros en palabras.""" +"""Picks the best already synthesized candidate again, comparing numbers as words.""" import json, os, shutil, sys, wave from difflib import SequenceMatcher @@ -29,7 +29,7 @@ def main(name, idxs): r = SequenceMatcher(None, norm(segs[i][0]), norm(got)).ratio() cands.append((r, k, w, got)) if not cands: - print(f" {i:02d} sin candidatos"); continue + print(f" {i:02d} no candidates"); continue cands.sort(reverse=True) for r, k, w, got in cands: print(f" {i:02d} seed{k} r={r:.2f} {dur(w):.2f}s | {got[:72]}") diff --git a/scripts/verify.py b/scripts/verify.py index b35eb44..084c911 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Transcribe los WAV finales y compara ignorando la forma de escribir numeros.""" +"""Transcribes the final WAVs and compares them ignoring how numbers are written.""" import json, os, re, subprocess, sys, unicodedata from difflib import SequenceMatcher diff --git a/scripts/verify_final.py b/scripts/verify_final.py index 7be410b..1ed848e 100644 --- a/scripts/verify_final.py +++ b/scripts/verify_final.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -"""QA sobre el MP4 terminado: extrae el audio ya mezclado con la musica, -lo corta segun el timeline y lo transcribe. Prueba que se entiende la voz -por encima de la cama, no solo que el WAV suelto estaba bien.""" +"""QA on the finished MP4: extracts the audio already mixed with the music, +cuts it by the timeline and transcribes it. It proves the voice is +intelligible over the bed, not just that the loose WAV was fine.""" import json, os, subprocess, sys from difflib import SequenceMatcher @@ -11,39 +11,39 @@ from narration import SCRIPTS from verify import norm from gen_audio import transcribe -LIMITE = 0.88 +LIMIT = 0.88 def main(names): - peor = [] + worst = [] for name in names: mp4 = os.path.join(ROOT, "out", f"{name}.mp4") 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"] work = os.path.join(ROOT, "tmp", name + "_fin") os.makedirs(work, exist_ok=True) - entero = os.path.join(work, "todo.wav") + integer = os.path.join(work, "todo.wav") subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", mp4, "-vn", - "-ac", "1", "-ar", "16000", entero], check=True) + "-ac", "1", "-ar", "16000", integer], check=True) - trozos = [] + chunks = [] for bt in tl["beats"]: p = os.path.join(work, f"{bt['i']:02d}.wav") subprocess.run(["ffmpeg", "-v", "error", "-y", "-ss", f"{bt['start']:.3f}", - "-t", f"{segs[bt['i']]['dur'] + 0.25:.3f}", "-i", entero, + "-t", f"{segs[bt['i']]['dur'] + 0.25:.3f}", "-i", integer, "-c", "copy", p], check=True) - trozos.append(p) - tr = transcribe(trozos, work) + chunks.append(p) + tr = transcribe(chunks, work) print(f"\n=== {name} (mezcla final) ===") - for bt, p in zip(tl["beats"], trozos): + for bt, p in zip(tl["beats"], chunks): i = bt["i"] got = tr[p] r = SequenceMatcher(None, norm(segs[i]["tts"]), norm(got)).ratio() - if r < LIMITE: - peor.append((name, i, r)) - print(f" {i:02d} {'OK ' if r >= LIMITE else 'REV'} {r:.2f} | {got}") - print("\n>> a revisar:", peor if peor else "nada") + if r < LIMIT: + worst.append((name, i, r)) + print(f" {i:02d} {'OK ' if r >= LIMIT else 'REV'} {r:.2f} | {got}") + print("\n>> a revisar:", worst if worst else "nada") if __name__ == "__main__": -- cgit v1.2.3