A newer version of the Gradio SDK is available: 6.24.0
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 | sí |
| 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 | sí |
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
transformersno devuelve timestamps: es un transducer, no un seq2seq. Hay que usarAutoModelForTDTyprocessor.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=Trueignora 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
transformersno da timestamps. - La API de Mistral sí los da con
timestamp_granularities=["word"], aunque entonces no admite fijarlanguagea la vez. Son dos productos distintos. - Hay una implementación funcionando en el proyecto del podcast
(
Welcome to La Secta,src/transcription/stt.py, claseVoxtralTranscriber).
Qwen3-ASR
- Su paquete
qwen-asrfijatransformers==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
durationdeclarado, 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_TOKENen el Space. - El decorador va en la función que calcula, no en la que construye la interfaz.
En
app.pyestaba sobresubtify(), 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á sobrediarize()ytrascribe_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,iosytva veces listan formatos que luego no sirven.- Ningún cliente funciona siempre: en una prueba tiró
mweby en la siguiente hubo que caer aandroid. 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_versionen el README, no la derequirements.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/infoy entonces todos los botones responden «No API found», aunque el evento esté bien registrado y la función jamás llegue a ejecutarse. app.pylo parchea en_patch_gradio_schema_bug(). La solución de fondo es subirsdk_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.