aboutsummaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 20:50:41 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 20:50:41 -0300
commitfafaebb051907a848a9406f9da19669c81a83a3b (patch)
treec30ea26e6b549e5523af2bae5c39569e9a946b12 /scripts
parent59355909f2de9236af8168a26c70bcf6caa3b285 (diff)
download100cia-videos-main.tar.gz
100cia-videos-main.zip
Translate code, comments and logs to English; English README; configurable paths and env varsHEADmain
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.
Diffstat (limited to 'scripts')
-rw-r--r--scripts/build_blender.py32
-rw-r--r--scripts/build_video.py4
-rw-r--r--scripts/gen_audio.py39
-rw-r--r--scripts/music.py40
-rw-r--r--scripts/narration.py4
-rw-r--r--scripts/numspell.py18
-rw-r--r--scripts/regen.py6
-rw-r--r--scripts/repick.py4
-rw-r--r--scripts/verify.py2
-rw-r--r--scripts/verify_final.py32
10 files changed, 93 insertions, 88 deletions
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__":