subtify / CLAUDE.md
Maximofn's picture
Subir gradio a 5.49.1, documentar aprendizajes y dejar de ignorar requirements.txt
fe5c50b
|
Raw
History Blame Contribute Delete
9.52 kB

A newer version of the Gradio SDK is available: 6.24.0

Upgrade

subtify

Genera subtítulos a partir de vídeos: descarga, extrae audio, transcribe, diariza, traduce e incrusta los subtítulos. Corre como Space de Hugging Face (Maximofn/subtify) sobre ZeroGPU.

Norma de documentación

Todo aprendizaje se documenta en este fichero. Si hay que investigar algo —una API, por qué falla un modelo, el formato raro que devuelve una librería— la conclusión se apunta aquí antes de seguir. El objetivo es no volver a investigar dos veces lo mismo.

Aplica también a los callejones sin salida: saber que algo no funciona y por qué ahorra tanto tiempo como saber qué funciona.

Objetivo del producto

Cuando hay varios hablantes, transcribir a cada uno por separado y pintar sus subtítulos en un color distinto.

A futuro, sobre esa misma base: traducir la transcripción, clonar la voz de cada hablante, generar TTS y hacer lipsync — es decir, a partir de un vídeo en un idioma, producir el vídeo en otro.

Por eso hacen falta timestamps por palabra, no solo por frase.

Arquitectura de modelos

Dos familias de modelos con papeles distintos:

  • STT — transcribe con timestamps de palabra. Es la rejilla fina que necesitan la traducción, el TTS y el lipsync.
  • STT+d — transcribe y diariza a la vez, devolviendo turnos de habla con etiqueta de hablante. Es lo que permite colorear por hablante.

Las dos salidas se cruzan por solapamiento temporal: cada palabra cae dentro del turno que la contiene.

Para la diarización no se confiará en un solo modelo: la idea es combinar varios métodos y contrastarlos.

Elección actual

Papel Modelo Por qué
STT principal nvidia/parakeet-tdt-0.6b-v3 3-5× más rápido que Whisper turbo, hasta 5× menos VRAM, timestamps de palabra
STT fallback openai/whisper-large-v3 Cubre los 99 idiomas; se usa cuando el idioma no está entre los 25 de Parakeet
Detección de idioma openai/whisper-large-v3 Solo cuando el selector de idioma se deja vacío
STT+d OpenMOSS-Team/MOSS-Transcribe-Diarize Transcribe y diariza en una pasada con 2,4 GB

Benchmark de modelos STT (agosto 2026)

Medido sobre los vídeos del repo en ZeroGPU (H200). Spaces: Maximofn/subtify-stt-benchmark y Maximofn/subtify-stt-benchmark-qwen.

Podcast en español, 327 s, dos hablantes:

Modelo Tiempo RTFx VRAM Timestamps Diariza
Cohere Transcribe 2,3 s 144× 4,9 GB no no
Parakeet TDT v3 22,1 s 14,8× 4,6 GB palabra no
Voxtral Mini 3B 24,9 s 13,1× 9,5 GB no (ver abajo) no
Qwen3-ASR 1.7B 35,9 s 9,1× 5,7 GB palabra no
MOSS-Transcribe-Diarize 46,4 s 7,0× 2,4 GB frase
Whisper large-v3-turbo 64,3 s 5,1× 24,2 GB palabra no
distil-whisper large-v3 66,5 s 4,9× 24,2 GB palabra no
Granite Speech 4.1 Plus 199,1 s 1,6× 4,3 GB palabra

Vídeo en inglés de 598 s: Parakeet 22,8 s frente a los 112,9 s de Whisper turbo — la ventaja crece con la duración, porque en clips cortos domina el arranque en frío.

distil-whisper/distil-large-v3 es solo inglés. Era el transcriptor por defecto y sobre audio en español no transcribía: traducía, y mal.

Trampas de cada modelo

Todas comprobadas en el benchmark; el código que las resuelve está en los Spaces.

Parakeet TDT v3

  • El pipeline genérico de transformers no devuelve timestamps: es un transducer, no un seq2seq. Hay que usar AutoModelForTDT y processor.decode(sequences, durations=output.durations).
  • Los timestamps son de subtoken BPE, no de palabra: 'Lo', 'ok', 'ing'. Hay que reagruparlos recorriendo el texto y consumiendo subtokens por longitud.
  • Detecta el idioma automáticamente y no acepta que se lo fuercen.
  • Cubre 25 idiomas europeos, no los 99 de Whisper.

MOSS-Transcribe-Diarize

  • Su prompt por defecto está en chino. Sin pasarlo, el modelo no transcribe: se pone a traducir el audio a otro idioma.
  • Sin add_generation_prompt=True ignora la instrucción de emitir timestamps y devuelve solo la etiqueta de hablante.
  • Aguanta 90 minutos de una pasada. No trocear: al hacerlo, [S01] deja de referirse a la misma persona entre ventanas.
  • Formato de salida: [inicio][Sxx]texto[fin].

Granite Speech 4.1 Plus

  • En modo timestamps devuelve todo en minúsculas y sin puntuación. Hacen falta dos pasadas (una para el texto, otra para los tiempos) alineadas con SequenceMatcher.
  • Los tiempos vienen en centisegundos módulo 1000: el contador se reinicia cada 10 segundos y hay que desenrollar el rollover.

Voxtral

  • El modelo abierto vía transformers no da timestamps.
  • La API de Mistral sí los da con timestamp_granularities=["word"], aunque entonces no admite fijar language a la vez. Son dos productos distintos.
  • Hay una implementación funcionando en el proyecto del podcast (Welcome to La Secta, src/transcription/stt.py, clase VoxtralTranscriber).

Qwen3-ASR

  • Su paquete qwen-asr fija transformers==4.57.6, incompatible con el 5.x que necesitan Parakeet y Cohere. Requiere un entorno aparte.
  • El forced aligner devuelve objetos ForcedAlignItem, que no son indexables.
  • El aligner cubre hasta 5 minutos: en audios más largos pierde palabras.

Descartados

  • Canary-1B-v2: solo NeMo, dependencias incompatibles.
  • Cohere Transcribe: el mejor texto y rapidísimo, pero sin timestamps.
  • ARK-ASR-3B y Granite 4.1 2B a secas: sin timestamps.

ZeroGPU

  • La GPU se presta solo mientras corre una función @spaces.GPU. El modelo hay que cargarlo dentro de la función, no al importar el módulo.
  • Reserva crédito según el duration declarado, no según el uso real. Declarar 900 s aparta 900 s de cuota aunque se usen 20.
  • No admite ejecuciones concurrentes: dos tareas a la vez agotan la asignación y fallan con «too many ZeroGPU credits allocated to running tasks».
  • El wrapper cruza un proceso y solo propaga el nombre de la clase de excepción, sin traceback. Hay que capturar los errores dentro de la función decorada.
  • El filesystem no sobrevive a un rebuild: los resultados hay que guardarlos fuera.
  • Los modelos gated necesitan un secret HF_TOKEN en el Space.
  • El decorador va en la función que calcula, no en la que construye la interfaz. En app.py estaba sobre subtify(), que solo monta los componentes de Gradio: la GPU se pedía al arrancar y se soltaba enseguida, así que las transcripciones corrían sin GPU. Ahora está sobre diarize() y trascribe_audio().

Descarga de vídeos

download.py importa pytube, que lleva años roto para YouTube. El módulo que funciona es youtube_download.py, con yt-dlp, y no siempre a la primera:

  • El cliente por defecto puede dar 403 Forbidden.
  • web, ios y tv a veces listan formatos que luego no sirven.
  • Ningún cliente funciona siempre: en una prueba tiró mweb y en la siguiente hubo que caer a android. Por eso se recorre una lista de clientes en cascada.
  • Para algunos vídeos solo queda el formato 18 (mp4 360p con audio incrustado); hay que bajarlo y extraer el audio con ffmpeg.

Desde el Space no se puede descargar de YouTube. Responde «Sign in to confirm you're not a bot»: es el bloqueo antibot contra IPs de centro de datos. La misma URL se descarga sin problema desde un ordenador de casa. En el Space, la vía que funciona es subir el fichero; la UI lo dice cuando detecta ese error.

Gradio

  • El Space corre la versión que declara sdk_version en el README, no la de requirements.txt. Estaba en 5.13.2 (enero de 2025).
  • Esa versión tiene un bug al convertir el esquema de los componentes: da TypeError: argument of type 'bool' is not iterable, tumba el endpoint /info y entonces todos los botones responden «No API found», aunque el evento esté bien registrado y la función jamás llegue a ejecutarse.
  • app.py lo parchea en _patch_gradio_schema_bug(). La solución de fondo es subir sdk_version, y entonces el parche sobra.

Subida de ficheros al Space

Por API solo se aceptan ficheros .py. Los .md y .txt generan el commit pero llegan vacíos, sin aviso de error: por eso requirements.txt y este CLAUDE.md hay que editarlos a mano desde la web de Hugging Face.

Pendientes de investigar

  • Modelo dedicado de identificación de idioma. Ahora se usa Whisper para detectar, lo que obliga a cargarlo (6-8 s) aunque luego transcriba Parakeet. Alternativa: speechbrain/lang-id-voxlingua107-ecapa (107 idiomas, ~25 M parámetros, milisegundos). Revisar cuando el pipeline esté funcionando.

  • Inconsistencia de timestamps entre motores, para el lipsync. Whisper y Parakeet marcan el inicio de palabra de forma distinta: para «OpenAI», Whisper daba 0,00 s y Parakeet 0,16 s. Diferencias de 100-300 ms. Para subtítulos es irrelevante, pero el desfase audio-vídeo se percibe a partir de ~125 ms. Como un vídeo en español usaría Parakeet y uno en japonés Whisper, el sync tendría carácter distinto según el idioma, y una calibración del lipsync solo valdría para uno de los dos. Medir contra referencia antes de construir el doblaje.

  • Diarización con más de dos hablantes. MOSS solo se ha probado con dos voces. Validar antes de apostar por él para sustituir a pyannote.