BITÁCORA — Build del video “Nvidia: operación política”

Fecha: 10-nov-2026 Resultado: out/nvidia_operacion_politica.mp4 — 63.6s, 1080x1920 (9:16), 30fps, H.264 + AAC, -14 LUFS, 3.6MB Renderizado en: esotericwarfare@100.91.16.10 (PC con más recursos, vía SSH/Tailscale) Copias del final: en ambas máquinas, en ~/nvidia-video/out/


⚠️ Aclaración importante: NO se usó la GPU

La PC remota tiene una NVIDIA GeForce GTX 1050 (2GB) y su ffmpeg sí tiene compilados los encoders h264_nvenc (verificado con ffmpeg -encoders). Pero el build codificó con libx264 puro CPU (-c:v libx264 -preset medium -crf 18), escrito así a propósito en build.py.

La rapidez (~2 minutos de render total) vino de:

  1. CPU de 6 núcleos (AMD FX-6300) — x264 medium paraleliza solo.
  2. Caches agresivos en Pillow: fondos, sprites (chips, monedas, glows) y variantes alpha se generan una sola vez y se reutilizan frame a frame.
  3. Pipe directo a ffmpeg (-f rawvideo por stdin): los ~1900 frames nunca tocan el disco como JPEGs/PNGs intermedios, van de RAM al encoder.
  4. edge-tts en paralelo: las 7 narraciones se sintetizan concurrentes (semáforo de 3).

Si más adelante se quiere usar la GPU, el cambio es una línea en build_videos() (libx264 → h264_nvenc + -preset p4), pero ojo: el cuello de botella real es el render de frames en Pillow (CPU), no el encode. La ganancia sería parcial.


1. Qué hay en el proyecto

~/nvidia-video/
├── script.json                    # EL guion: narración, textos en pantalla, duraciones, config
├── build.py                       # pipeline completo (fotos → tts → visuales → ffmpeg)
├── rssh.sh                        # wrapper ssh con multiplexión (ver §7)
├── BITACORA-BUILD.md              # este archivo
├── assets/
│   ├── images/                    # fotos descargadas + metadatos de licencia
│   │   ├── jensen.jpg / jensen.json
│   │   ├── hotz.jpg  / hotz.json
│   │   └── (alice: sin foto → tarjeta generada en runtime)
│   ├── tts/                        # narraciones 00.mp3..06.mp3 (cache)
│   └── music.mp3                   # OPCIONAL: si existe, se mezcla a 13% con fade out
├── work/                           # escenas individuales + audio por escena + concats (cache)
└── out/
    └── nvidia_operacion_politica.mp4

2. Máquinas y conectividad

  • Local: máquina donde corre opencode. Tenía ffmpeg pero NO Pillow.
  • Remota (100.91.16.10, hostname arch): Arch Linux, kernel 7.2.14, AMD FX-6300 (6 núcleos), 7.7GB RAM, GTX 1050, Python 3.14.9, ffmpeg n9.0.2, Pillow 12.3.0, edge-tts, rsync. Ya tenía Pillow/edge-tts de trabajos anteriores (docs de Piper en ~/).
  • SSH: la conexión va por Tailscale SSH, que pidió autorización por navegador (check en login.tailscale.com, aprobado por el usuario).
  • Para no pedir autorización en cada comando se usó ControlMaster (multiplexión): un solo SSH maestro persistente 24h, todos los comandos/scp/rsync reusan ese socket. Wrapper: rssh.sh en este proyecto (idéntico al usado en /tmp/opencode/rssh).

3. Pipeline de build.py (5 etapas)

  1. Fotos (Commons, solo licencia libre): consulta la API de Wikimedia Commons (prop=imageinfo + extmetadata), valida que la licencia sea CC BY / CC BY-SA / CC0 / dominio público (rechaza fair use y no-libres), baja el thumb a 1080px de ancho y guarda artista+licencia en .json para renderizar la atribución EN PANTALLA (abajo, gris, chico).
    • Jensen: File:Jensen huang stanford 2026-04-30 008 (cropped).jpg — CC BY-SA 4.0
    • Hotz: File:George Hotz speaks onstage during TechCrunch Disrupt SF 2016 01.jpg — CC BY 2.0
    • Alice Huang: SIN FOTO. No existe foto con licencia libre de ella. Los resultados de Commons para “Alice Huang” son OTRA persona (una dentista de la US Navy). Se genera una tarjeta dossier con silueta + nombre. Usar la foto de una persona equivocada en un video sobre una presunta espía = riesgo legal directo.
  2. Narración: edge-tts, voz es-AR-TomasNeural, rate +30%.
    • TTS cacheado en assets/tts/ (con --force-tts se regenera).
    • Duración de cada escena = max(min_duration, tts + lead_in + tail).
  3. Visuales: Pillow frame a frame (1080x1920@30). Estilo dark-tech verde Nvidia, escena final ámbar de alto contraste. Cada escena es una función render (r_hero, r_dossier, r_lupa, r_list3, r_data, r_grid, r_punch):
    • S1: foto Jensen oscurecida + duo-tono verde, chip que late, titular.
    • S2: grid tenue + tarjeta dossier Alice + 3 tarjetas que entran escalonadas.
    • S3: foto Hotz + tarjeta con ícono de lupa, sub aparece a los 1.5s.
    • S4: lista numerada 1/2/3 con entrada escalonada.
    • S5: “25%” ámbar + moneda que cae en loop + panel “CONTROL ESTATAL” con grilla de nodos que se apagan (pasan a oscuro con X roja).
    • S6: grilla 4x5 de chips que se encienden en ola (“AI” adentro).
    • S7: fondo ámbar, texto oscuro con punch de escala (1.35→1.0), sub y pie de fuentes; fade a negro final. Barra de progreso en las 7 escenas.
    • Atribución de fotos en S1 y S3 (requisito CC).
  4. Audio: por escena = adelay(150ms) + apad hasta la duración exacta → AAC 48kHz estéreo 192k. Concat por demuxer. Sin música (no hay assets/music.mp3; si se agrega, se mezcla a volumen 0.13 con fade out).
  5. Final: concat video + audio, loudnorm I=-14 TP=-1.5 (estándar redes), -movflags +faststart.

4. Problemas encontrados y cómo se resolvieron

Problema Solución
Tailscale SSH pedía check por navegador usuario aprobó el link; luego ControlMaster para no repetir
edge-tts 7.2.8 → 403 en speech.platform.bing.com pip3 install --user --break-system-packages --upgrade edge-tts → 7.3.1 funcionó
Duración inicial 71.9s (objetivo 55-65s) rate +15%→+30%, pausas 0.2/0.4→0.15/0.3s, min_duration por escena recalibrados → 63.56s
Sin foto libre de Alice Huang tarjeta generada con silueta (§3.1)
(Preventivo) licencias solo CC/dominio público; atribución renderizada en pantalla

5. Verificaciones realizadas (sin ver el video a ojo)

  • ffprobe: h264 1080x1920, aac, duración 63.60s video / 63.63s audio.
  • 1 frame al medio de cada escena + análisis programático:
    • Escenas 1-6 oscuras (brightness 13-28), S7 ámbar (brightness 130, 95.9% ámbar).
    • Presencia de verde (S6 grid: 6.4%) y ámbar (S5: 3.1%) donde corresponde.
    • Texto confirmado en las 7 escenas: desvío estándar 39-75 en las bandas de titulares (si fuera texto faltante daría <10).
  • Fotos visibles detrás del oscurecido (S1/S3 tienen más brillo que las escenas planas).

6. Cómo retocar y re-renderizar

# en la REMOTA (o local si tiene Pillow):
cd ~/nvidia-video
python3 build.py                  # rehace solo lo que cambió (caches por escena)
python3 build.py --tts-only       # tabla de duraciones sin renderizar
python3 build.py --rate "+25%"    # cambia velocidad de voz (regenera TTS)
python3 build.py --force-tts      # regenera narraciones
python3 build.py --fresh           # borra work/ y rehace todo
  • Textos/narración/duraciones → script.json (castellano, comentado por escena).
  • Estilo visual (colores, tamaños, animaciones) → constantes y funciones en build.py.
  • Música: dejar un mp3 libre de copyright en assets/music.mp3 → se mezcla solo en la etapa final (los visuales NO se re-renderizan).
  • Detalles del guion vigentes: S6 dice “Huang era el último gran defensor” (la versión corregida lo cambió de “es”); no hay CTA final (la corrección lo sacó). Ambos son ediciones de una línea en script.json.

7. Comandos clave usados (reproducibilidad)

# wrapper ssh multiplexado (también copiado como ~/nvidia-video/rssh.sh)
cat > rssh.sh <<'EOF'
#!/bin/bash
exec ssh -o BatchMode=yes -o ControlMaster=auto \
  -o ControlPath=/tmp/opencode/tssh-%r@%h-%p -o ControlPersist=86400 \
  -o ServerAliveInterval=30 -o ConnectTimeout=20 "$@"
EOF
chmod +x rssh.sh

# sync del proyecto → remota
rsync -az -e "./rssh.sh" ~/nvidia-video/ esotericwarfare@100.91.16.10:nvidia-video/

# build completo en la remota
./rssh.sh esotericwarfare@100.91.16.10 'cd ~/nvidia-video && python3 build.py'

# traer el resultado a local
scp -o ControlPath=/tmp/opencode/tssh-%r@%h-%p \
  esotericwarfare@100.91.16.10:nvidia-video/out/nvidia_operacion_politica.mp4 ~/nvidia-video/out/

# verlo
mpv ~/nvidia-video/out/nvidia_operacion_politica.mp4

Tiempos de render reales por escena (remota, CPU): S1 8.3s, S2 19.8s, S3 13.2s, S4 13.7s, S5 21.2s, S6 9.4s, S7 15.4s → ~101s de render total para 1907 frames.