/diego/video-pipeline EscríbemeWrite to me
AI 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.

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.

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.

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 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 measuring
/día /day
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 it
de infraestructura: tier gratuito, TTS gratis, FFmpeg y Whisper en localinfrastructure: free tier, free TTS, FFmpeg and Whisper running locally
RecorridoWalkthrough

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

From a folder with 13 GB to a publishable video

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.

vista previa · corte heropreview · hero cut

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.

TRAICIONÓ \pos(960,540) · PlayRes 1920×1080
vídeo largo · 1920×1080long form · 1920×1080
Impact 150 contornooutline 8 px palabra a palabraword by word audio offsetaudio offset −100 ms sin subtítulos durante la introno subs during the intro
TRAICIONÓ \pos(540,960) · 1080×1920
short vertical · 9:16 · fuente 80vertical short · 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.

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.

Flagship · el corazón del proyectoFlagship · the heart of the project

El bug que dura 4 fotogramas y sobrevive 30 minutos

The bug that lasts 4 frames and survives 30 minutes

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.

«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."
0,00 s
VOZ (edge-tts)VOICE (edge-tts)cuándo suena de verdad cada palabrawhen each word is actually spoken
ANTES · reparto proporcionalBEFORE · proportional splitlas palabras se repartían por nº de caracteres para rellenar la ventana ENTERAwords were spread by character count to fill the ENTIRE window
AHORA · traslación con ancla duraNOW · translation with a hard anchorse 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 stretched
↔ desliza las pistas para verlas enteras↔ swipe the tracks to see them in full
desfase ahora mismo · ANTESdrift right now · BEFORE
+0,00 s
desfase ahora mismo · AHORAdrift right now · NOW
−0,15 s
sesgo medio de la frasemean bias of the sentence
:
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.
error medio · por régimenmean error · by regime
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.

error máximomax error
1,064 s0,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.

sesgo (signo)bias (sign)
+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 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.

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.

Trabajar con LLMs en producciónWorking with LLMs in production

Tres cosas que el prompt prometió y no cumplió

Three things the prompt promised and didn't deliver

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.

Episodio 01 · respiraciónEpisode 01 · breathing

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

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.

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.
_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.
Episodio 02 · el títuloEpisode 02 · the title

«Empieza el speech con el título»"Start the speech with the title"

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.

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.
_ensure_title_at_start lo fuerza tras generar, borrando el solapamiento parcial._ensure_title_at_start forces it after generation, stripping the partial overlap.
Episodio 03 · variedadEpisode 03 · variety

«Que sea DISTINTA a cualquier otra»"Make it DIFFERENT from any other"

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.

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".
→ 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.
# 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

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.

Los cuatro que terminaron sin errorThe four that finished without error

Verde local ≠ funciona

Green locally ≠ works

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.

El falloThe failure Lo que decía la señal fácilWhat the easy signal said Lo que dijo la mediciónWhat the measurement said La defensa que existe hoyThe defense that exists today
Demuxer concatconcat demuxerrutas 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 cwd «la ingesta terminó»"ingest finished" 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 shortcut rutas absolutasabsolute paths+ un segmento que falla ya no entra en la lista+ a failed segment no longer enters the list
Subtítulos por detrásSubtitles running latereparto proporcional sobre una ventana que incluye los silenciosproportional split over a window that includes the silences «los subtítulos salen»"the subtitles show up" +0,435 s de sesgo medio · 1,064 s de máximomean bias · 1.064 s max anclas durashard anchorsy el sincronismo es la métrica primaria del gateand sync is the gate's primary metric
Shorts clonadosCloned shortsN llamadas independientes con un prompt idénticoN independent calls with an identical prompt «se generaron 4 shorts»"4 shorts were generated" los 4 con el mismo argumento; solo cambiaba el finalall 4 with the same plot; only the ending changed avoid[]+ prohibido validar con --no-shorts+ validating with --no-shorts is banned
Comas del promptThe prompt's commasuna regla de estilo que el modelo cumplía a ratosa style rule the model followed sometimes «la regla está en el prompt»"the rule is in the prompt" 167 · 129 · 0 · 0 en 4 generacionesacross 4 generations impuesto en códigoenforced in codecon un invariante: no cambia el nº de palabraswith an invariant: it never changes the word count
Y una trampa de mediciónAnd a measurement trapun fixture de sincronismo construido repitiendo el mismo párrafo 4 vecesa sync fixture built by repeating the same paragraph 4 times «2,525 s de error, está roto»"2.525 s of error, it's broken" 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 copy fixtures no repetidosnon-repeating fixturesun valor idéntico entre corridas es la firma del artefactoan identical value across runs is the artifact's signature

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.

La restricción que mandaThe binding constraint

Aquí el coste no es dinero: es cuota

Here cost isn't money: it'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.

minutos de gameplay por vídeominutes of gameplay per video 30 min
palabras objetivotarget words
4.800
bloques de historiastory blocks
3 peticionesrequests
shorts verticalesvertical shorts
45 peticionesrequests
total por vídeototal per video
48
48 / 1000
peticiones consumidas por UN vídeorequests consumed by ONE video tope diario de OpenRouter: 1.000OpenRouter daily cap: 1,000
20vídeos al díavideos per day
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.
0

búsquedas para medir 40 canalessearches to measure 40 channels

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.

~470

unidades por escaneounits per scan

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.

0

canales perdidos por un cortechannels lost to a cutoff

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.

La cadena de producciónThe production chain

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

Nine stages, eight where a failure is 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.

↔ 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
El gemelo que nadie miraThe twin nobody looks at

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.

Cómo trabajo con IAHow I work with AI

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

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.

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 rule PANEL ADVERSARIAL · su trabajo es demostrar que el vídeo está roto ADVERSARIAL PANEL · their job is to prove the video is broken EL CAMBIOTHE CHANGE ¿superficie sensible?sensitive surface? /eval la cadena entera · con shortsthe whole chain · with shorts bug-hunter causa raíz, sin contextoroot cause, no context output-audit mide el ASS, el WAV, el MP4measures ASS, WAV, MP4 EL VÍDEOTHE VIDEO bajo ataqueunder attack DIEGO · juzgaDIEGO · judges ¿engancha? ¿suena natural?does it hook? sound natural? A output/TO output/ vídeo + miniatura + títulovideo + thumbnail + title AL LEDGERTO THE LEDGER hecho, no reglaa fact, not a rule si empeora el baseline → NO se cierra if the baseline worsens → it does NOT close

Pellizca para acercar y leer el detalle ↔Pinch to zoom in and read the detail ↔

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.

Verificación por ejecuciónVerification by execution

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.

Prueba el caso de falloTest the failure case

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.

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.

Disponible para trabajarAvailable for work

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.

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.

YOUTUBE PIPELINE · Diego · 2026 Pipeline autónomo · construido con IA · sin formación técnica previaAutonomous pipeline · built with AI · no prior technical background