Tres de los trabajos de este proyecto duran horas: el barrido de fuentes de la etapa 3, la transcripción de la etapa 5 y la segmentación que viene después. Los tres comparten la misma incomodidad — corren mucho rato, contra servidores que no controlas o sobre un portátil que se puede dormir — y se van a interrumpir. La pregunta no es si, es cuánto pierdes cuando pase.
El servidor te va a frenar
En la etapa 8 apareció de pasada el rate limiting: un servidor estrangulando las peticiones de quien pide demasiado. Aquí toca verlo con números, porque el umbral llega antes de lo que parece.
Esto es lo que se le pidió a YouTube en unas pocas horas:
| Actividad | Peticiones |
|---|---|
| 44 consultas de búsqueda | 44 |
| Listar vídeos minando canales | 2.218 |
| Descargar subtítulos | 111 |
| Descargar audio para Whisper | 100+ |
| Total, en una tarde | ~3.500 |
A partir de ahí empezaron a llegar dos respuestas:
HTTP Error 429: Too Many Requests
ERROR: Sign in to confirm you're not a botLo importante es qué no son. No es un bloqueo, ni una cuenta marcada, ni nada permanente. Son límites de ráfaga: pediste mucho en poco rato, y el servidor te aparta un tiempo. Se levantan solos.
Conviene saberlo porque el instinto al ver un 429 es pensar que has hecho algo irreparable y abandonar la fuente. Casi nunca es eso.
Tres respuestas, y cuándo vale cada una
| Respuesta | Cuándo | Coste |
|---|---|---|
| Esperar | Cuando no tienes prisa y el lote puede seguir mañana | Cero, salvo tu paciencia |
| Frenar — una pausa de cortesía entre peticiones | Siempre, y sobre todo contra sitios pequeños | Segundos por petición |
Identificarte — --cookies-from-browser | Cuando el error pide expresamente que inicies sesión | Un flag |
La tercera merece una aclaración, porque suena a truco y no lo es. --cookies-from-browser coge las cookies de sesión de tu navegador y las manda con la petición, o sea que dejas de ser un cliente anónimo y pasas a ser tú, con tu cuenta:
yt-dlp --cookies-from-browser chrome --write-auto-subs --skip-download ...Eso no es saltarse nada. Es exactamente lo que el mensaje de error está pidiendo — «inicia sesión para confirmar que no eres un bot» — y responde a la pregunta que el servidor hace: quién eres. Lo que sí sigue en pie es la pausa de cortesía: identificarte no te da derecho a apretar más.
Reintentar con espera creciente
Cuando falla una petición dentro de un lote de cien, la tentación es apuntarla y seguir. Es la decisión equivocada: un 429 es temporal, pero un vídeo saltado es permanente — nadie vuelve a por él, y acabas con un corpus con agujeros que no sabes que tiene.
Lo correcto es reintentar, esperando cada vez más. Se llama backoff: si el servidor te está pidiendo aire, dárselo en cantidades crecientes hasta que respire.
for attempt in 1 2 3; do
yt-dlp -x --audio-format wav -o "wav/%(id)s.%(ext)s" -- "$id" && break
echo "$id: intento $attempt fallido, esperando"
sleep $((attempt * 45))
done45 segundos, luego 90, luego 135. Tres intentos y casi cuatro minutos de espera acumulada para un vídeo que, si lo saltas, no recuperas nunca.
Escribe la salida según sale, no al final
Esta es la regla que este proyecto ha aprendido dos veces, con dos fallos distintos y la misma forma.
La primera, en la etapa 5: el lote de Whisper escribía en un directorio temporal del sistema, de esos que se limpian solos. Seis horas de cómputo a merced de una limpieza automática. Se arregló con un segundo proceso copiando al repositorio cada 30 segundos.
La segunda es más traicionera, porque el código parecía correcto. El script de segmentación escribía cada frase a su fichero según la extraía... o eso creía:
with open(OUTPUT, "a", encoding="utf-8") as handle:
for window in windows:
for phrase in segment(window):
handle.write(json.dumps(phrase, ensure_ascii=False) + "\n")handle.write() no escribe en disco. Escribe en un búfer en memoria, y el sistema lo vuelca cuando se llena o cuando se cierra el fichero — que aquí sería al terminar las 400.000 palabras. A mitad de ejecución había 135 frases extraídas y el fichero de salida vacío. Un error en la ventana 290 se habría llevado por delante las 289 anteriores.
La corrección es una línea:
with open(OUTPUT, "a", encoding="utf-8") as handle:
for window in windows:
for phrase in segment(window):
handle.write(json.dumps(phrase, ensure_ascii=False) + "\n")
handle.flush() # ahora sí está en discoY la otra mitad de la regla: al arrancar, lee lo que ya hay escrito y empieza por donde se quedó. Es el mismo [ -s "out/$id.txt" ] && continue de la etapa 5, en otro lenguaje. Escribir según sale sin releer al arrancar solo sirve para no perder trabajo; releer al arrancar es lo que convierte «se cortó» en «tardó un poco más».
Todo proceso largo tiene que cumplir dos cosas: dejar su salida en sitio seguro y según la produce, y saber reanudarse leyendo lo que ya hay. Las dos juntas cuestan tres líneas.
Y ojo con el matiz de la segunda vez: que tu código llame a write() en el bucle no significa que el dato esté a salvo. Compruébalo mirando el fichero desde otra terminal mientras corre.
Estimar cuándo acaba, sin inventárselo
Antes de lanzar la segmentación había una estimación de coste: 7 dólares. Salió de multiplicar a ojo palabras por precio.
La forma honesta es dejarlo correr un rato y medir el ritmo real:
# cuánto lleva corriendo
ps -o etime= -p $(pgrep -f perico.corpus.segment)
# 01:12:33
# cuánto lleva hecho
wc -l < data/clean/segments.jsonlCon esas dos cifras sale el ritmo de verdad, y de ahí la proyección. El número medido fueron 13 dólares, casi el doble de la estimación. No es un desastre —trece dólares siguen siendo trece dólares— pero si la estimación hubiera sido de 200 y la real de 400, la decisión de lanzarlo era otra.
Es el mismo patrón que persigue la etapa 3 y que reaparece en la 8: extrapolar sale barato y contar sale exacto. Aquí contar cuesta esperar una hora con el proceso ya en marcha.
Epílogo: dos procesos, un fichero, 353 duplicados
La reanudación de esta etapa funciona así: el script lee lo que ya hay en el fichero de salida, se salta lo que ya está hecho y añade lo nuevo al final. Funciona perfectamente… mientras haya un solo proceso escribiendo.
En algún momento se lanzaron dos segmentadores a la vez sobre el mismo fichero. Cada uno cargó al arrancar su propia lista de frases ya vistas, ninguno veía las que escribía el otro, y los dos añadieron. Resultado: 353 frases, una de cada diez del índice, estaban dos veces. Lo destapó una métrica que no buscaba eso, la tasa de repetición, cuando la frase más repetida resultó estar duplicada en la tabla.
La deduplicación del script era correcta desde el primer commit, pero no puede ver lo que hace otro proceso. Y la regla «un fichero, un dueño» estaba escrita en AGENTS.md, pero nada la hacía cumplir.
Ahora sí la hace cumplir el código. El script toma un cerrojo del sistema operativo (flock) sobre un fichero .lock al lado de su salida, antes de leer qué hay hecho, y lo suelta al terminar. Un segundo proceso falla al instante, antes de llamar a ninguna API. Y si el proceso muere, el sistema operativo suelta el cerrojo solo, así que un fallo nunca lo deja bloqueado para siempre.
Es la misma lección que la del segmentador que se inventaba frases, ahora con los procesos en lugar del modelo. Si algo importa, pedirlo no basta: hay que comprobarlo en el código, en este caso con un cerrojo. Y detrás, un test que falla si alguna frase aparece dos veces en el corpus, por si el cerrojo algún día no basta.
Lo que se lleva uno de esta etapa
- Un
429no es un castigo, es un semáforo. Espera, frena o identifícate; casi nunca hay que abandonar la fuente. - Reintenta con espera creciente antes que saltar el elemento. El fallo es temporal, el hueco en tus datos no.
- Escribe según produces, y comprueba que de verdad llega al disco. Un
write()en el bucle no basta si el búfer se vacía al final. - Reanudar leyendo lo ya hecho convierte una interrupción en un retraso.
- Mide el ritmo real antes de proyectar. Una hora de ejecución te da un número; multiplicar a ojo te dio la mitad del correcto.