/diego/video-pipeline EscríbemeWrite to meEscriu-me
AI Engineer · media pipeline · Python + FFmpeg + LLMAI Engineer · media pipeline · Python + FFmpeg + LLMAI Engineer · media pipeline · Python + FFmpeg + LLM
github.com/diegollr98-design/video-pipeline

Automaticé un canal entero. Lo difícil fue construir lo que impide que publique basura.

I automated a whole channel. The hard part was building what stops it from shipping garbage.

Vaig automatitzar un canal sencer. El més difícil va ser construir el que impedeix que publiqui brossa.

Este pipeline coge una grabación de Minecraft de 13 GB y devuelve un vídeo de 30 minutos listo para subir: historia escrita por un LLM, narrada en español con voz elegida según el protagonista, subtítulos clavados palabra por palabra, intro animada, miniatura y decenas de shorts verticales. Sin que nadie toque nada. Y ahí está el problema: el modo de fallo de un sistema autónomo no es que pete, es que entregue un MP4 que parece terminado y esté roto de una forma que nadie ve. Nadie mira 30 minutos de salida. Empecé sin saber programar; lo levanté solo, con IA.

This pipeline takes a 13 GB Minecraft recording and returns a 30-minute video ready to upload: an LLM-written story, narrated in Spanish with the voice picked from the protagonist's gender, subtitles locked word by word, animated intro, thumbnail and dozens of vertical shorts. With nobody touching anything. And that's the problem: an autonomous system's failure mode isn't crashing: it's shipping an MP4 that looks finished and is broken in a way nobody sees. Nobody watches 30 minutes of output. I started not knowing how to code; I built it solo, with AI.

Aquest pipeline agafa una gravació de Minecraft de 13 GB i torna un vídeo de 30 minuts a punt per pujar: història escrita per un LLM, narrada en castellà amb veu triada segons el protagonista, subtítols clavats paraula per paraula, intro animada, miniatura i desenes de shorts verticals. Sense que ningú toqui res. I aquí hi ha el problema: el mode de fallada d'un sistema autònom no és que peti, és que lliuri un MP4 que sembla acabat i estigui trencat d'una manera que ningú veu. Ningú mira 30 minuts de sortida. Vaig començar sense saber programar; el vaig aixecar sol, amb IA.

Límite honesto: el uploader está construido (OAuth, subida reanudable y miniatura, con su pestaña en el dashboard) pero no se ha ejecutado nunca: necesita unas credenciales de Google Cloud propias que no están creadas. Por diseño sube en privado, solo el vídeo largo, y se dispara a mano desde una cola con el veredicto del auditor delante; nunca automáticamente al terminar una corrida. Lo digo aquí antes de que lo preguntes.Honest limit: the uploader is built (OAuth, resumable upload and thumbnail, with its own dashboard tab) but it has never been run: it needs its own Google Cloud credentials, which don't exist yet. By design it uploads privately, long-form video only, and is fired by hand from a queue with the auditor's verdict in front of you; never automatically at the end of a run. Saying it up front.Límit honest: l'uploader està construït (OAuth, pujada represa i miniatura, amb la seva pestanya al dashboard) però no s'ha executat mai: necessita unes credencials de Google Cloud pròpies que no estan creades. Per disseny puja en privat, només el vídeo llarg, i es dispara a mà des d'una cua amb el veredicte de l'auditor al davant; mai automàticament en acabar una execució. Ho dic aquí abans que ho preguntis.

s
error medio voz-subtítulo en el fixture de 3 min, contra una transcripción independiente · en la producción real de 30 min: 0,153 smean voice-to-subtitle error on the 3-min fixture, against an independent transcript · on the real 30-min production: 0.153 serror mitjà veu-subtítol al fixture de 3 min, contra una transcripció independent · a la producció real de 30 min: 0,153 s
bugs graves que produjeron un MP4 perfecto y reproducible. Los cuatro se cazaron midiendoserious bugs that produced a perfect, playable MP4. All four were caught by measuringbugs greus que van produir un MP4 perfecte i reproduïble. Els quatre es van caçar mesurant
/día /day/dia
peticiones al LLM: el techo REAL de producción. La arquitectura se diseñó contra élLLM requests: the REAL production ceiling. The architecture was designed against itpeticions a l'LLM: el sostre REAL de producció. L'arquitectura es va dissenyar contra ell
€
de infraestructura: tier gratuito, TTS gratis, FFmpeg y Whisper en localinfrastructure: free tier, free TTS, FFmpeg and Whisper running locallyd'infraestructura: tier gratuït, TTS gratis, FFmpeg i Whisper en local
RecorridoWalkthroughRecorregut

De una carpeta con 13 GB a un vídeo publicable

From a folder with 13 GB to a publishable video

D'una carpeta amb 13 GB a un vídeo publicable

El producto es un vídeo, así que la prueba también. Una corrida real: ingesta → historia → voz → alineación → subtítulos → intro animada → miniatura → shorts verticales. Lo que se ve en pantalla es lo fácil de enseñar; lo que sostiene la calidad son dos parámetros que solo aparecen cuando mides la salida.

The product is a video, so is the proof. A real run: ingest → story → voice → alignment → subtitles → animated intro → thumbnail → vertical shorts. What you see on screen is the easy part to show; what holds the quality up are two parameters that only surface when you measure the output.

El producte és un vídeo, així que la prova també. Una execució real: ingesta → història → veu → alineació → subtítols → intro animada → miniatura → shorts verticals. El que es veu a la pantalla és el fàcil d'ensenyar; el que sosté la qualitat són dos paràmetres que només apareixen quan mesures la sortida.

vista previa · corte heropreview · hero cutvista prèvia · tall hero

Guion sugerido: input/ con la grabación cruda → detección de hotbar tirando pausa y escritorio → pool/ (1.120 MB → 296 MB en la validación E2E) → historia por bloques → voz + alineación forzada → el ASS sobre el gameplay → intro con rebote elástico y woosh → miniatura → los shorts verticales, cada uno con historia distinta.Suggested script: input/ with the raw recording → hotbar detection dropping pause and desktop → pool/ (1,120 MB → 296 MB in the E2E validation) → story in blocks → voice + forced alignment → the ASS over the gameplay → intro with elastic bounce and woosh → thumbnail → the vertical shorts, each with a different story.Guió suggerit: input/ amb la gravació crua → detecció de hotbar descartant pausa i escriptori → pool/ (1.120 MB → 296 MB a la validació E2E) → història per blocs → veu + alineació forçada → l'ASS sobre el gameplay → intro amb rebot elàstic i woosh → miniatura → els shorts verticals, cadascun amb història diferent.

TRAICIONÓ \pos(960,540) · PlayRes 1920×1080
vídeo largo · 1920×1080long form · 1920×1080vídeo llarg · 1920×1080
Impact 150 contornooutlinecontorn 8 px palabra a palabraword by wordparaula a paraula audio offsetaudio offsetaudio offset −100 ms sin subtítulos durante la introno subs during the introsense subtítols durant la intro
TRAICIONÓ \pos(540,960) · 1080×1920
short vertical · 9:16 · fuente 80vertical short · 9:16 · font 80short vertical · 9:16 · font 80

Posición y PlayRes son un par indivisible. Cambiar uno sin el otro no rompe nada visible en el log: produce un vídeo entero con el texto estirado. Lo aprendí en la versión vertical, que es exactamente la que nadie revisa.

Position and PlayRes are one indivisible pair. Changing one without the other breaks nothing visible in the log: it produces a whole video with stretched text. I learned that on the vertical variant, precisely the one nobody reviews.

Posició i PlayRes són un parell indivisible. Canviar-ne un sense l'altre no trenca res visible al log: produeix un vídeo sencer amb el text estirat. Ho vaig aprendre a la versió vertical, que és exactament la que ningú revisa.

Por eso los invariantes que viven en dos sitios están escritos como tales en las reglas del repo, y tocar uno obliga a revisar su gemelo.

That's why invariants living in two places are written down as such in the repo's rules, and touching one forces you to review its twin.

Per això els invariants que viuen en dos llocs estan escrits com a tals a les regles del repo, i tocar-ne un obliga a revisar el seu bessó.

Flagship · el corazón del proyectoFlagship · the heart of the projectFlagship · el cor del projecte

El bug que dura 4 fotogramas y sobrevive 30 minutos

The bug that lasts 4 frames and survives 30 minutes

El bug que dura 4 fotogrames i sobreviu 30 minuts

Durante meses los subtítulos iban por detrás de la voz. No lo bastante para que saltara a la vista, lo suficiente para que el vídeo se sintiera mal sin saber por qué. El pipeline no daba ningún error: producía MP4 perfectos. Dale al play y mira las dos pistas de abajo: la roja es el algoritmo anterior.

For months the subtitles ran behind the voice. Not enough to jump out at you, enough for the video to feel wrong without knowing why. The pipeline threw no error: it produced perfect MP4s. Hit play and watch the two tracks below: the red one is the old algorithm.

Durant mesos els subtítols anaven per darrere de la veu. No prou perquè saltés a la vista, sí prou perquè el vídeo se sentís malament sense saber per què. El pipeline no donava cap error: produïa MP4 perfectes. Dona-li al play i mira les dues pistes de sota: la vermella és l'algorisme anterior.

«Mi hermano vendió mi coche clásico para pagar sus deudas sin pedirme permiso.»"My brother sold my classic car to pay his debts without asking me."«El meu germà va vendre el meu cotxe clàssic per pagar els seus deutes sense demanar-me permís.»
0,00 s
VOZ (edge-tts)VOICE (edge-tts)VEU (edge-tts)cuándo suena de verdad cada palabrawhen each word is actually spokenquan sona de veritat cada paraula
ANTES · reparto proporcionalBEFORE · proportional splitABANS · repartiment proporcionallas palabras se repartían por nº de caracteres para rellenar la ventana ENTERAwords were spread by character count to fill the ENTIRE windowles paraules es repartien per nre. de caràcters per omplir la finestra SENCERA
AHORA · traslación con ancla duraNOW · translation with a hard anchorARA · translació amb àncora durase fija el inicio de la frase y se TRASLADAN los tiempos de Whisper, sin estirarlosthe sentence start is pinned and Whisper's times are TRANSLATED, never stretchedes fixa l'inici de la frase i es TRASLLADEN els temps de Whisper, sense estirar-los
↔ desliza las pistas para verlas enteras↔ swipe the tracks to see them in full↔ llisca les pistes per veure-les senceres
desfase ahora mismo · ANTESdrift right now · BEFOREdesfasament ara mateix · ABANS
+0,00 s
desfase ahora mismo · AHORAdrift right now · NOWdesfasament ara mateix · ARA
−0,15 s
sesgo medio de la frasemean bias of the sentencebiaix mitjà de la frase
:
La causa raíz: edge-tts no emite un solo evento por palabra: 0 WordBoundary, solo SentenceBoundary: una ventana por frase. Y esa ventana tila el audio completo, silencios incluidos: medido, termina en 7,72 s cuando la voz acaba en 6,76 s. El código repartía las palabras dentro de la ventana entera, así que cada una caía más tarde de cuando sonaba y el retraso crecía dentro de la frase. Con frases cortas colaba; con las de ~49 palabras que escriben estos modelos, llegaba a 1,5 s. Root cause: edge-tts emits no per-word event. 0 WordBoundary, only SentenceBoundary: one window per sentence. And that window tiles the entire audio, silences included: measured, it ends at 7.72 s while the voice ends at 6.76 s. The code spread words across the whole window, so each one landed later than it sounded and the lag grew inside the sentence. With short sentences it passed; with the ~49-word ones these models write, it reached 1.5 s.La causa arrel: edge-tts no emet un sol esdeveniment per paraula: 0 WordBoundary, només SentenceBoundary: una finestra per frase. I aquesta finestra abasta l'àudio complet, silencis inclosos: mesurat, acaba a 7,72 s quan la veu acaba a 6,76 s. El codi repartia les paraules dins de la finestra sencera, així que cadascuna queia més tard de quan sonava i el retard creixia dins de la frase. Amb frases curtes colava; amb les de ~49 paraules que escriuen aquests models, arribava a 1,5 s.
error medio · por régimenmean error · by regimeerror mitjà · per règim
0,153 s·0,072 s

Los dos son medidas reales de objetos distintos, y publico las dos: 0,153 s es la primera producción real de 30 minutos; 0,072 s, el fixture de 3 minutos con el que corre el gate. Todo contra una transcripción independiente del audio, no contra el propio texto. Esa medición, la que me deja peor, es la que destapó el bug de anclaje, ya corregido: el fixture post-fix da 0,043 s. Un solo número aquí habría sido el bonito.Both are real measurements of different objects, and I publish both: 0.153 s is the first real 30-minute production; 0.072 s, the 3-minute fixture the gate runs on. All against an independent transcript of the audio, not the source text. That measurement, the one that makes me look worse, is what exposed the anchoring bug, now fixed: the post-fix fixture reads 0.043 s. A single number here would have been the pretty one.Tots dos són mesures reals d'objectes diferents, i publico les dues: 0,153 s és la primera producció real de 30 minuts; 0,072 s, el fixture de 3 minuts amb què corre el gate. Tot contra una transcripció independent de l'àudio, no contra el text mateix. Aquesta mesura, la que em deixa pitjor, és la que va destapar el bug d'ancoratge, ja corregit: el fixture post-fix dona 0,043 s. Un sol número aquí hauria estat el bonic.

error máximomax errorerror màxim
1,064 s→0,400 s

El máximo importa más que la media: es el fotograma en el que el espectador nota que algo va mal. Y es el único umbral que el baseline del 10-ago no cumple (objetivo ≤0,30 s): son 0,400 s en 1 palabra de 422, el monosílabo «Y». Está escrito en el propio data/eval/2026-08-10.json.The max matters more than the mean: it's the frame where the viewer notices something is off. It is also the one threshold the 10-Aug baseline fails (target ≤0.30 s): 0.400 s on 1 word out of 422, the monosyllable "Y". It is written in data/eval/2026-08-10.json itself.El màxim importa més que la mitjana: és el fotograma en què l'espectador nota que alguna cosa va malament. I és l'únic llindar que el baseline del 10-ago no compleix (objectiu ≤0,30 s): són 0,400 s en 1 paraula de 422, el monosíl·lab «Y». Està escrit al mateix data/eval/2026-08-10.json.

sesgo (signo)bias (sign)biaix (signe)
+0,435 s→−0,067 s

El signo es el objetivo real: el subtítulo debe ir delante de la voz, nunca detrás.The sign is the real target: the subtitle must run ahead of the voice, never behind.El signe és l'objectiu real: el subtítol ha d'anar davant de la veu, mai darrere.

# el fix NO fue una constante: fue cambiar de mecanismo # modules/tts_engine.py :: _validate_and_fix_alignment ANTES — repartir para RELLENAR la ventana for w in palabras: w.start = ventana.ini + ventana.dur * (chars/total) # ⇒ la palabra se estira hasta el silencio final AHORA — anclar el inicio y TRASLADAR delta = ventana.ini - whisper[0].start for w in palabras: old_dur = w.end - w.start # ← preservar w.start += delta w.end = w.start + old_dur # ← o subtítulo # congelado # El silencio final de la ventana queda LIBRE: por eso # el subtítulo desaparece en las pausas, como pedía la # especificación. El bug y la feature eran lo mismo.
# the fix was NOT a constant: it was changing the mechanism # modules/tts_engine.py :: _validate_and_fix_alignment BEFORE — spread to FILL the window for w in words: w.start = window.ini + window.dur * (chars/total) # ⇒ the word stretches into the trailing silence NOW — pin the start and TRANSLATE delta = window.ini - whisper[0].start for w in words: old_dur = w.end - w.start # ← preserve w.start += delta w.end = w.start + old_dur # ← or frozen # subtitle # The window's trailing silence is left FREE: that's why # the subtitle disappears during pauses, exactly as the # spec asked. The bug and the feature were the same thing.
# el fix NO va ser una constant: va ser canviar de mecanisme # modules/tts_engine.py :: _validate_and_fix_alignment ABANS — repartir per OMPLIR la finestra for w in palabras: w.start = ventana.ini + ventana.dur * (chars/total) # ⇒ la paraula s'estira fins al silenci final ARA — ancorar l'inici i TRASLLADAR delta = ventana.ini - whisper[0].start for w in palabras: old_dur = w.end - w.start # ← preservar w.start += delta w.end = w.start + old_dur # ← o subtítol # congelat # El silenci final de la finestra queda LLIURE: per això # el subtítol desapareix a les pauses, com demanava l' # especificació. El bug i la feature eren el mateix.

Durante meses se afinaron parámetros asumiendo que el reparto proporcional era correcto. No lo era, y ninguna constante iba a arreglarlo: había que cambiar el mecanismo.

La lección que me llevo no es sobre subtítulos. Es que antes de optimizar un enfoque hay que comprobar que el enfoque es el correcto. Medir dónde está el problema es más barato que optimizar donde no está.

Detalle que cuesta un vídeo entero: al mover start hay que preservar la duración y recalcular end. Si no, el subtítulo se congela en pantalla: otro fallo perfectamente silencioso.

El vídeo se reproducía bien. Lo que estaba roto era invisible salvo midiendo.

For months parameters were tuned assuming the proportional split was correct. It wasn't, and no constant was going to fix it: the mechanism had to change.

The lesson isn't about subtitles. It's that before optimizing an approach you must check the approach is the right one. Measuring where the problem is costs less than optimizing where it isn't.

A detail that costs a whole video: when moving start you must preserve the duration and recompute end. Otherwise the subtitle freezes on screen: another perfectly silent failure.

The video played fine. What was broken was invisible unless you measured it.

Durant mesos es van afinar paràmetres assumint que el repartiment proporcional era correcte. No ho era, i cap constant no ho arreglaria: calia canviar el mecanisme.

La lliçó que me'n duc no va de subtítols. És que abans d'optimitzar un enfocament cal comprovar que l'enfocament és el correcte. Mesurar on és el problema és més barat que optimitzar on no hi és.

Detall que costa un vídeo sencer: en moure start cal preservar la durada i recalcular end. Si no, el subtítol es congela a la pantalla: una altra fallada perfectament silenciosa.

El vídeo es reproduïa bé. El que estava trencat era invisible tret de mesurant.

Trabajar con LLMs en producciónWorking with LLMs in productionTreballar amb LLMs en producció

Tres cosas que el prompt prometió y no cumplió

Three things the prompt promised and didn't deliver

Tres coses que el prompt va prometre i no va complir

Esta es la regla más cara del proyecto, y la pagué tres veces: una garantía escrita en prosa dentro de un prompt no está garantizada. El modelo la cumple casi siempre, y «casi siempre» en un sistema autónomo significa que el fallo llega a la salida sin que nadie lo vea. Lo que importa no es que lo diga el prompt: es quién lo comprueba después.

This is the project's most expensive rule, and I paid for it three times: a guarantee written in prose inside a prompt is not guaranteed. The model complies almost always, and "almost always" in an autonomous system means the failure reaches the output unseen. What matters isn't that the prompt says it: it's who checks afterwards.

Aquesta és la regla més cara del projecte, i la vaig pagar tres vegades: una garantia escrita en prosa dins d'un prompt no està garantida. El model la compleix gairebé sempre, i «gairebé sempre» en un sistema autònom vol dir que la fallada arriba a la sortida sense que ningú la vegi. El que importa no és que ho digui el prompt: és qui ho comprova després.

Episodio 01 · respiraciónEpisode 01 · breathingEpisodi 01 · respiració

«Pon una coma cada 8-12 palabras»"Add a comma every 8-12 words"«Posa una coma cada 8-12 paraules»

Sin comas, el TTS se inventa dónde respirar: pausas de 0,44 s en mitad de una idea.Without commas the TTS invents where to breathe: 0.44 s pauses mid-idea.Sense comes, el TTS s'inventa on respirar: pauses de 0,44 s enmig d'una idea.

Medido con la regla puesta:
167 · 129 · 0 · 0 comas en 4 generaciones.
Frases de 19 a 67 palabras de media.
Measured with the rule in place:
167 · 129 · 0 · 0 commas across 4 runs.
Sentences averaging 19 to 67 words.
Mesurat amb la regla posada:
167 · 129 · 0 · 0 comes en 4 generacions.
Frases de 19 a 67 paraules de mitjana.
→ _ensure_breathing_commas las inserta en código, solo delante de conectores. Pausas inesperadas 2 → 0.→ _ensure_breathing_commas inserts them in code, only before connectors. Unexpected pauses 2 → 0.→ _ensure_breathing_commas les insereix en codi, només davant de connectors. Pauses inesperades 2 → 0.
Episodio 02 · el títuloEpisode 02 · the titleEpisodi 02 · el títol

«Empieza el speech con el título»"Start the speech with the title"«Comença el speech amb el títol»

La intro animada dura exactamente lo que el narrador tarda en decir el título. Si no coincide, la intro se corta a media frase.The animated intro lasts exactly as long as the narrator takes to say the title. If it doesn't match, the intro cuts mid-sentence.La intro animada dura exactament el que el narrador triga a dir el títol. Si no coincideix, la intro es talla a mitja frase.

El modelo lo cumplía… hasta que no.
Y el fallo no aborta nada: produce un vídeo entero con la intro descuadrada.
The model complied… until it didn't.
And the failure aborts nothing: it produces a whole video with a misaligned intro.
El model ho complia… fins que no.
I la fallada no avorta res: produeix un vídeo sencer amb la intro descentrada.
→ _ensure_title_at_start lo fuerza tras generar, borrando el solapamiento parcial.→ _ensure_title_at_start forces it after generation, stripping the partial overlap.→ _ensure_title_at_start ho força després de generar, esborrant el solapament parcial.
Episodio 03 · variedadEpisode 03 · varietyEpisodi 03 · varietat

«Que sea DISTINTA a cualquier otra»"Make it DIFFERENT from any other"«Que sigui DIFERENT a qualsevol altra»

Cada short es una llamada independiente con el mismo prompt. El modelo no podía saber cuáles eran «las otras».Each short is an independent call with the same prompt. The model could not know which "the others" were.Cada short és una crida independent amb el mateix prompt. El model no podia saber quines eren «les altres».

Corrida real: los 4 shorts con el mismo argumento.
Solo cambiaba el final… y una vez «hermano» por «hermana».
Real run: all 4 shorts with the same plot.
Only the ending changed… and once "brother" for "sister".
Execució real: els 4 shorts amb el mateix argument.
Només canviava el final… i un cop «germà» per «germana».
→ Se le pasan los títulos ya generados (avoid), incluidos los de corridas anteriores leídos del disco.→ It receives the already-generated titles (avoid), including previous runs' read from disk.→ Se li passen els títols ja generats (avoid), inclosos els d'execucions anteriors llegits del disc.
# y el guardia que corona los tres: validar la salida # modules/script_generator.py :: _validar_salida # CASO REAL: un modelo de razonamiento escribió su # razonamiento EN VEZ de la historia. El título salió: "The user wants a viral micro-story script for Shorts." # …y ese título fue a la MINIATURA y a la INTRO. # El vídeo: 4,5 segundos. Terminó sin error. if not (min_pal <= len(titulo.split()) <= 45): return False, # título implausible for marcador in ("the user", "here is", "```", …): if marcador in titulo.lower(): return False if aciertos_es < 2 and not tildes: return False # → reintento con una instrucción más dura
# and the guard that crowns all three: validate the output # modules/script_generator.py :: _validar_salida # REAL CASE: a reasoning model wrote its reasoning # INSTEAD of the story. The title came out as: "The user wants a viral micro-story script for Shorts." # …and that title went to the THUMBNAIL and the INTRO. # The video: 4.5 seconds. It finished without error. if not (min_w <= len(title.split()) <= 45): return False, # implausible title for marker in ("the user", "here is", "```", …): if marker in title.lower(): return False if es_hits < 2 and not accents: return False # → retry with a harder instruction
# i el guàrdia que corona els tres: validar la sortida # modules/script_generator.py :: _validar_salida # CAS REAL: un model de raonament va escriure el seu # raonament EN COMPTES de la història. El títol va sortir: "The user wants a viral micro-story script for Shorts." # …i aquell títol va anar a la MINIATURA i a la INTRO. # El vídeo: 4,5 segons. Va acabar sense error. if not (min_pal <= len(titulo.split()) <= 45): return False, # títol implausible for marcador in ("the user", "here is", "```", …): if marcador in titulo.lower(): return False if aciertos_es < 2 and not tildes: return False # → reintent amb una instrucció més dura

El corolario práctico, y es transferible a cualquier producto con un LLM dentro: toda salida de modelo que alimente algo irreversible pasa por un validador. Aquí «irreversible» es la miniatura y la intro de un vídeo que se sube.

Y su reverso, que también aplico: cuando una decisión exige un juicio que el modelo no puede dar de forma fiable, no le pido su mejor intento, porque su mejor intento produce ruido plausible, que es peor que un hueco: nadie lo detecta.

Una garantía prometida en prosa no está garantizada hasta que un if la fuerza.

The practical corollary, transferable to any product with an LLM inside: every model output feeding something irreversible goes through a validator. Here "irreversible" is the thumbnail and intro of a video that gets uploaded.

And its flip side, which I also apply: when a decision needs judgment the model cannot reliably provide, I don't ask for its best attempt, because its best attempt produces plausible noise, which is worse than a gap: nobody detects it.

A guarantee promised in prose isn't guaranteed until an if enforces it.

El corol·lari pràctic, i és transferible a qualsevol producte amb un LLM a dins: tota sortida de model que alimenti alguna cosa irreversible passa per un validador. Aquí «irreversible» és la miniatura i la intro d'un vídeo que es puja.

I el seu revers, que també aplico: quan una decisió exigeix un judici que el model no pot donar de manera fiable, no li demano el seu millor intent, perquè el seu millor intent produeix soroll plausible, que és pitjor que un buit: ningú no el detecta.

Una garantia promesa en prosa no està garantida fins que un if la força.

Los cuatro que terminaron sin errorThe four that finished without errorEls quatre que van acabar sense error

Verde local ≠ funciona

Green locally ≠ works

Verd local ≠ funciona

Los cuatro bugs graves de este repo produjeron vídeos completos y reproducibles. Ninguno se habría cazado leyendo el código, y ninguno disparó una alarma. Lo que los delató fue medir la salida. Esta tabla es, para mí, el activo del proyecto: la diferencia entre la señal fácil y el dato.

All four serious bugs in this repo produced complete, playable videos. None would have been caught by reading the code, and none raised an alarm. What exposed them was measuring the output. This table is, to me, the project's real asset: the gap between the easy signal and the datum.

Els quatre bugs greus d'aquest repo van produir vídeos complets i reproduïbles. Cap s'hauria caçat llegint el codi, i cap va disparar una alarma. El que els va delatar va ser mesurar la sortida. Aquesta taula és, per a mi, l'actiu del projecte: la diferència entre el senyal fàcil i la dada.

El falloThe failureLa fallada Lo que decía la señal fácilWhat the easy signal saidEl que deia el senyal fàcil Lo que dijo la mediciónWhat the measurement saidEl que va dir la mesura La defensa que existe hoyThe defense that exists todayLa defensa que existeix avui
Demuxer concatconcat demuxerDemuxer concatrutas relativas en la lista, resueltas respecto al directorio del fichero y no al cwdrelative paths in the list, resolved against the file's directory, not the cwdrutes relatives a la llista, resoltes respecte al directori del fitxer i no al cwd «la ingesta terminó»"ingest finished"«la ingesta ha acabat» nunca funcionó. La única corrida real era 95% gameplay y tomaba el atajo de -ss/-toit never worked. The only real run was 95% gameplay and took the -ss/-to shortcutmai va funcionar. L'única execució real era 95% gameplay i prenia la drecera de -ss/-to rutas absolutasabsolute pathsrutes absolutes+ un segmento que falla ya no entra en la lista+ a failed segment no longer enters the list+ un segment que falla ja no entra a la llista
Subtítulos por detrásSubtitles running lateSubtítols per darrerereparto proporcional sobre una ventana que incluye los silenciosproportional split over a window that includes the silencesrepartiment proporcional sobre una finestra que inclou els silencis «los subtítulos salen»"the subtitles show up"«els subtítols surten» +0,435 s de sesgo medio · 1,064 s de máximomean bias · 1.064 s maxde biaix mitjà · 1,064 s de màxim anclas durashard anchorsàncores duresy el sincronismo es la métrica primaria del gateand sync is the gate's primary metrici el sincronisme és la mètrica primària del gate
Shorts clonadosCloned shortsShorts clonatsN llamadas independientes con un prompt idénticoN independent calls with an identical promptN crides independents amb un prompt idèntic «se generaron 4 shorts»"4 shorts were generated"«s'han generat 4 shorts» los 4 con el mismo argumento; solo cambiaba el finalall 4 with the same plot; only the ending changedels 4 amb el mateix argument; només canviava el final avoid[]+ prohibido validar con --no-shorts+ validating with --no-shorts is banned+ prohibit validar amb --no-shorts
Comas del promptThe prompt's commasComes del promptuna regla de estilo que el modelo cumplía a ratosa style rule the model followed sometimesuna regla d'estil que el model complia a estones «la regla está en el prompt»"the rule is in the prompt"«la regla és al prompt» 167 · 129 · 0 · 0 en 4 generacionesacross 4 generationsen 4 generacions impuesto en códigoenforced in codeimposat en codicon un invariante: no cambia el nº de palabraswith an invariant: it never changes the word countamb un invariant: no canvia el nre. de paraules
Y una trampa de mediciónAnd a measurement trapI una trampa de mesuraun fixture de sincronismo construido repitiendo el mismo párrafo 4 vecesa sync fixture built by repeating the same paragraph 4 timesun fixture de sincronisme construït repetint el mateix paràgraf 4 vegades «2,525 s de error, está roto»"2.525 s of error, it's broken"«2,525 s d'error, està trencat» el mismo valor exacto en 3 de 5 repeticiones → el emparejador enganchaba la copia equivocadathe exact same value in 3 of 5 repeats → the matcher latched onto the wrong copyel mateix valor exacte en 3 de 5 repeticions → l'emparellador enganxava la còpia equivocada fixtures no repetidosnon-repeating fixturesfixtures no repetitsun valor idéntico entre corridas es la firma del artefactoan identical value across runs is the artifact's signatureun valor idèntic entre execucions és la signatura de l'artefacte

La última fila es la que más me enseñó: desconfiar de mi propia medición. Un número que sale idéntico tres veces no es precisión, es un artefacto del test. Si la herramienta puede mentir, el dato tampoco vale.The last row taught me the most: distrust my own measurement. A number that comes out identical three times isn't precision, it's a test artifact. If the tool can lie, the datum is worthless too.L'última fila és la que més em va ensenyar: desconfiar de la meva pròpia mesura. Un número que surt idèntic tres vegades no és precisió, és un artefacte del test. Si l'eina pot mentir, la dada tampoc val.

La restricción que mandaThe binding constraintLa restricció que mana

Aquí el coste no es dinero: es cuota

Here cost isn't money: it's quota

Aquí el cost no són diners: és quota

El proyecto corre a 0 €, y por eso mismo el recurso escaso es otro: 1.000 peticiones al día en OpenRouter y la cuota diaria de la YouTube Data API. Eso no es un detalle de facturación: es una restricción de arquitectura. Durante meses mi propia documentación dijo «50/día», y tres auditorías adversariales repitieron el dato falso porque estaba escrito en un .md, hasta que se consultó la API. Está en mi ledger como [DOC-01]: la prosa no es prueba, ni siquiera la mía. Mueve el deslizador: la fórmula es la misma que corre en main.py.

The project runs at €0, and precisely because of that the scarce resource is different: 1,000 requests per day on OpenRouter and the daily quota of the YouTube Data API. That isn't a billing detail: it's an architectural constraint. For months my own documentation said «50/day», and three adversarial audits repeated the false figure because it was written in a .md, until someone queried the API. It's in my ledger as [DOC-01]: prose isn't proof, not even mine. Move the slider: the formula is the one running in main.py.

El projecte corre a 0 €, i precisament per això el recurs escàs és un altre: 1.000 peticions al dia a OpenRouter i la quota diària de la YouTube Data API. Això no és un detall de facturació: és una restricció d'arquitectura. Durant mesos la meva pròpia documentació deia «50/dia», i tres auditories adversarials van repetir la dada falsa perquè estava escrita en un .md, fins que es va consultar l'API. És al meu ledger com a [DOC-01]: la prosa no és prova, ni tan sols la meva. Mou el lliscador: la fórmula és la mateixa que corre a main.py.

minutos de gameplay por vídeominutes of gameplay per videominuts de gameplay per vídeo 30 min
palabras objetivotarget wordsparaules objectiu
4.800
bloques de historiastory blocksblocs d'història
3 peticionesrequestspeticions
shorts verticalesvertical shortsshorts verticals
45 peticionesrequestspeticions
total por vídeototal per videototal per vídeo
48
48 / 1000
peticiones consumidas por UN vídeorequests consumed by ONE videopeticions consumides per UN vídeo tope diario de OpenRouter: 1.000OpenRouter daily cap: 1,000sostre diari d'OpenRouter: 1.000
20vídeos al díavideos per dayvídeos al dia
La fórmula es la del código, no una estimación. palabras = clamp(minutos × 196 wpm, 500, 8000) · bloques = max(2, ceil(palabras / 2000)) · short = (200 / 200) × 60 / 1,5 = 40 s · shorts = max(1, floor(segundos de gameplay / 40)). A eso hay que sumarle los reintentos y el análisis de competencia, que clasifica los canales candidatos en lotes de 8 y repite hasta 3 pasadas partiendo el lote (8 → 4 → 2) con los que se quedan sin veredicto. Los dos ritmos son knobs separados a propósito (196 wpm la historia larga, 200 los shorts): compartir la clave hacía que recalibrar la historia moviese en silencio cuántos shorts se generan, y con ellos el gasto. Con el tope diario en 1.000 la cuota ya no es la restricción que muerde, pero saber el coste antes de lanzar sigue siendo lo que evita morir a mitad de corrida con un 429. The formula is the code's, not an estimate. words = clamp(minutes × 196 wpm, 500, 8000) · blocks = max(2, ceil(words / 2000)) · short = (200 / 200) × 60 / 1.5 = 40 s · shorts = max(1, floor(gameplay seconds / 40)). On top of that: retries, and the competition analysis, which classifies candidate channels in batches of 8 and repeats for up to 3 passes, halving the batch (8 → 4 → 2) with whatever is left unresolved. The two paces are deliberately separate knobs (196 wpm for the long story, 200 for shorts): sharing the key meant recalibrating the story silently moved how many shorts get generated, and with them the spend. With the daily cap at 1,000 the quota is no longer the binding constraint, but knowing the cost before launching is still what stops a run dying halfway with a 429.La fórmula és la del codi, no una estimació. palabras = clamp(minutos × 196 wpm, 500, 8000) · bloques = max(2, ceil(palabras / 2000)) · short = (200 / 200) × 60 / 1,5 = 40 s · shorts = max(1, floor(segundos de gameplay / 40)). A això cal sumar-hi els reintents i l'anàlisi de competència, que classifica els canals candidats en lots de 8 i repeteix fins a 3 passades partint el lot (8 → 4 → 2) amb els que es queden sense veredicte. Els dos ritmes són knobs separats a propòsit (196 wpm la història llarga, 200 els shorts): compartir la clau feia que recalibrar la història mogués en silenci quants shorts es generen, i amb ells la despesa. Amb el sostre diari a 1.000 la quota ja no és la restricció que mossega, però saber el cost abans de llançar segueix sent el que evita morir a mitja execució amb un 429.
0

búsquedas para medir 40 canalessearches to measure 40 channelscerques per mesurar 40 canals

Los últimos vídeos de un canal se leen por su playlist de subidas, no con search.list. El presupuesto de búsquedas es el recurso más escaso de la API, y así medir 40 canales no gasta ni una.A channel's latest videos are read via its uploads playlist, not search.list. The search budget is the API's scarcest resource, and this way measuring 40 channels spends none of it.Els últims vídeos d'un canal es llegeixen per la seva playlist de pujades, no amb search.list. El pressupost de cerques és el recurs més escàs de l'API, i així mesurar 40 canals no en gasta ni una.

~470

unidades por escaneounits per scanunitats per escaneig

De las cuales 400 son las 4 búsquedas de descubrimiento: el gasto dominante. Por eso las keywords rotan entre escaneos y el contador se corta antes de que Google devuelva un 403.Of which 400 are the 4 discovery searches: the dominant cost. That's why keywords rotate between scans and the counter cuts off before Google returns a 403.De les quals 400 són les 4 cerques de descobriment: la despesa dominant. Per això les keywords roten entre escanejos i el comptador es talla abans que Google torni un 403.

0

canales perdidos por un cortechannels lost to a cutoffcanals perduts per un tall

Si la cuota se acaba a mitad, el canal se deja intacto para la próxima corrida. Antes quedaba marcado «sin vídeos recientes» de forma permanente y la lista se vaciaba sola.If quota runs out mid-scan, the channel is left untouched for the next run. It used to be permanently flagged "no recent videos" and the list emptied itself.Si la quota s'acaba a mitges, el canal es deixa intacte per a la propera execució. Abans quedava marcat «sense vídeos recents» de manera permanent i la llista es buidava sola.

La cadena de producciónThe production chainLa cadena de producció

Nueve etapas, y ocho en las que un fallo es invisible

Nine stages, eight where a failure is invisible

Nou etapes, i vuit en què una fallada és invisible

No todas las etapas merecen la misma ceremonia. Las marcadas como superficie sensible son aquellas donde un cambio puede romper la salida sin romper la ejecución, y esas no se cierran sin medir. Pasa el ratón por una etapa.

Not every stage deserves the same ceremony. The ones marked sensitive surface are those where a change can break the output without breaking the run, and those don't close without measuring. Hover a stage.

No totes les etapes mereixen la mateixa cerimònia. Les marcades com a superfície sensible són aquelles on un canvi pot trencar la sortida sense trencar l'execució, i aquestes no es tanquen sense mesurar. Passa el ratolí per una etapa.

↔ desliza para ver la cadena completa · pasa el ratón o toca una etapa↔ swipe to see the whole chain · hover or tap a stage↔ llisca per veure la cadena completa · passa el ratolí o toca una etapa
El gemelo que nadie miraThe twin nobody looks atEl bessó que ningú mira

Los shorts verticales comparten con el vídeo largo la intro animada, el woosh, la alineación y los subtítulos, pero con PlayRes, posición y velocidad distintas. Un arreglo en uno casi siempre tiene gemelo en el otro, y el de los shorts es el que nadie revisa: ahí vivieron los 4 shorts idénticos. Por eso la regla del repo no dice «arregla la función», dice «enumera las dos etapas», y correr con --no-shorts está prohibido para validar un cambio.

Vertical shorts share the animated intro, the woosh, the alignment and the subtitles with the long video, but with different PlayRes, position and speed. A fix in one almost always has a twin in the other, and the shorts' one is what nobody reviews: that's where the 4 identical shorts lived. That's why the repo's rule doesn't say "fix the function", it says "enumerate both stages", and running with --no-shorts is banned for validating a change.

Els shorts verticals comparteixen amb el vídeo llarg la intro animada, el woosh, l'alineació i els subtítols, però amb PlayRes, posició i velocitat diferents. Un arranjament en un gairebé sempre té bessó a l'altre, i el dels shorts és el que ningú revisa: allà van viure els 4 shorts idèntics. Per això la regla del repo no diu «arregla la funció», diu «enumera les dues etapes», i córrer amb --no-shorts està prohibit per validar un canvi.

Cómo trabajo con IAHow I work with AICom treballo amb IA

Un gate con dientes, y un panel que intenta romper mi propio vídeo

A gate with teeth, and a panel trying to break my own video

Un gate amb dents, i un panell que intenta trencar el meu propi vídeo

No le pido a la IA una respuesta. Un orquestador delega la implementación en agentes en paralelo, y después un panel escéptico ataca lo construido: bug-hunter localiza la causa raíz sin contexto previo, output-audit intenta demostrar que el vídeo está roto midiendo sus artefactos, y /eval corre la cadena entera contra un clip fijo y compara con el baseline. El orquestador re-deriva cada veredicto ejecutando, nunca por informe. Yo juzgo lo que ninguna capa puede juzgar.

I don't ask the AI for an answer. An orchestrator delegates implementation to parallel agents, then a skeptical panel attacks what was built: bug-hunter finds the root cause with no prior context, output-audit tries to prove the video is broken by measuring its artifacts, and /eval runs the whole chain against a fixed clip and compares to the baseline. The orchestrator re-derives every verdict by executing, never by report. I judge what no layer can judge.

No li demano a la IA una resposta. Un orquestrador delega la implementació en agents en paral·lel, i després un panell escèptic ataca el que s'ha construït: bug-hunter localitza la causa arrel sense context previ, output-audit intenta demostrar que el vídeo està trencat mesurant els seus artefactes, i /eval corre la cadena sencera contra un clip fix i compara amb el baseline. L'orquestrador re-deriva cada veredicte executant, mai per informe. Jo jutjo el que cap capa pot jutjar.

el incidente se registra como HECHO · solo /optimize lo promueve a regla the incident is logged as a FACT · only /optimize promotes it to a rulel'incident es registra com a FET · només /optimize el promou a regla PANEL ADVERSARIAL · su trabajo es demostrar que el vídeo está roto ADVERSARIAL PANEL · their job is to prove the video is brokenPANELL ADVERSARIAL · la seva feina és demostrar que el vídeo està trencat EL CAMBIOTHE CHANGEEL CANVI ¿superficie sensible?sensitive surface?superfície sensible? /eval la cadena entera · con shortsthe whole chain · with shortsla cadena sencera · amb shorts bug-hunter causa raíz, sin contextoroot cause, no contextcausa arrel, sense context output-audit mide el ASS, el WAV, el MP4measures ASS, WAV, MP4mesura l'ASS, el WAV, l'MP4 EL VÍDEOTHE VIDEOEL VÍDEO bajo ataqueunder attacksota atac DIEGO · juzgaDIEGO · judgesDIEGO · jutja ¿engancha? ¿suena natural?does it hook? sound natural?enganxa? sona natural? A output/TO output/A output/ vídeo + miniatura + títulovideo + thumbnail + titlevídeo + miniatura + títol ✓ AL LEDGERTO THE LEDGERAL LEDGER hecho, no reglaa fact, not a rulefet, no regla ✗ si empeora el baseline → NO se cierra if the baseline worsens → it does NOT closesi empitjora el baseline → NO es tanca

Pellizca para acercar y leer el detalle ↔Pinch to zoom in and read the detail ↔Pessiga per apropar i llegir el detall ↔

El gate no avisa: bloquea. Un cambio en superficie sensible que empeore el sincronismo respecto al baseline no se cierra: no es una advertencia, es un no. Y la última capa siempre soy yo: ninguna medición dice si una historia engancha, y fingir lo contrario sería el mismo error que confiar en que el modelo obedezca el prompt.

The gate doesn't warn: it blocks. A sensitive-surface change that worsens sync against the baseline does not close: it isn't a warning, it's a no. And the last layer is always me: no measurement tells you whether a story hooks, and pretending otherwise would be the same mistake as trusting the model to obey the prompt.

El gate no avisa: bloqueja. Un canvi en superfície sensible que empitjori el sincronisme respecte al baseline no es tanca: no és un advertiment, és un no. I l'última capa sempre soc jo: cap mesura diu si una història enganxa, i fingir el contrari seria el mateix error que confiar que el model obeeixi el prompt.

Verificación por ejecuciónVerification by executionVerificació per execució

Ningún «el subagente reportó que OK» cierra nada. Corro yo el ffprobe, la medición de alineación y el /eval, y pego la salida real en el siguiente paso. Un informe no es evidencia; una salida sí.No "the subagent reported OK" closes anything. I run ffprobe, the alignment measurement and /eval myself, and paste the real output into the next step. A report isn't evidence; output is.Cap «el subagent ha reportat que OK» tanca res. Corro jo el ffprobe, la mesura d'alineació i el /eval, i enganxo la sortida real al pas següent. Un informe no és evidència; una sortida sí.

Prueba el caso de falloTest the failure caseProva el cas de fallada

Un gate nuevo es superficie nueva: antes de darlo por bueno le paso el conjunto vacío, el dato ausente y el valor desconocido. Y verifico sobre el caso que el módulo existe para cubrir: una grabación CON pausas, no el clip fácil que toma el atajo y pasa siempre.A new gate is new surface: before trusting it I feed it the empty set, the missing datum and the unknown value. And I verify against the case the module exists to cover: a recording WITH pauses, not the easy clip that takes the shortcut and always passes.Un gate nou és superfície nova: abans de donar-lo per bo li passo el conjunt buit, la dada absent i el valor desconegut. I verifico sobre el cas que el mòdul existeix per cobrir: una gravació AMB pauses, no el clip fàcil que pren la drecera i passa sempre.

PythonFFmpegfaster-whisperstable-tsedge-ttsforced alignment OpenRouterYouTube Data API v3StreamlitPillowASS / SRTNVENC

El resto del método (la refutación asimétrica, el ledger que solo asciende a regla con freno) es el mismo en los cinco sistemas y está contado una vez: en el índice.The rest of the method (asymmetric refutation, the ledger that only promotes to a rule with a brake) is the same across all five systems and is told once: on the index.La resta del mètode (la refutació asimètrica, el ledger que només ascendeix a regla amb fre) és el mateix als cinc sistemes i està explicat una vegada: a l'índex.

Disponible para trabajarAvailable for workDisponible per treballar

Cojo un problema difuso, lo descompongo y orquesto IA para construir cada pieza, sin perder el rigor.

I take a fuzzy problem, break it down and orchestrate AI to build every piece, without losing the rigor.

Agafo un problema difús, el descomponc i orquestro IA per construir cada peça, sense perdre el rigor.

Si buscas a alguien para applied-AI, automatización o forward-deployed engineering: que construya sistemas autónomos y las medidas que impiden que se engañen a sí mismos: hablemos.

If you're looking for someone in applied-AI, automation or forward-deployed engineering: someone who builds autonomous systems and the measurements that stop them fooling themselves: let's talk.

Si busques algú per a applied-AI, automatització o forward-deployed engineering: que construeixi sistemes autònoms i les mesures que impedeixen que s'enganyin a si mateixos: parlem-ne.

YOUTUBE PIPELINE · Diego · 2026 Pipeline autónomo · construido con IA · sin formación técnica previaAutonomous pipeline · built with AI · no prior technical backgroundPipeline autònom · construït amb IA · sense formació tècnica prèvia