Communitygithub.com

TaurenRen/srt-whiteboard-workflow

Transforma guiones SRT en animaciones de pizarra dibujadas a mano con voz en off, grabación de pantalla y capturas clave. Se activa cuando el usuario solicita crear videos de pizarra animada, explicaciones con pizarra, convertir SRT a animación, o probar proyectos open-source como srt-whiteboard-animation.

¿Qué es srt-whiteboard-workflow?

Este skill es un flujo de trabajo completo y profesional para convertir guiones SRT o narraciones en videos animados de pizarra dibujados a mano, incluyendo voz en off, grabación de pantalla y capturas de pantalla clave. Está basado en la experiencia práctica con el proyecto open-source `srt-whiteboard-animation`. El flujo cubre siete etapas: desde la escritura del guion y storyboard, generación de imágenes con IA, anotación a nivel de píxeles, sincronización precisa de la voz con la línea de tiempo, renderizado con cámara virtual, hasta la quema de subtítulos. Está diseñado para creadores de contenido educativo, divulgación histórica o técnica, que buscan un resultado visual atractivo y de alta calidad. El skill implementa numerosas correcciones y optimizaciones basadas en errores reales, como el manejo de texturas de fondo, el escalado de la mano, la sincronización de la línea de tiempo y la optimización de la composición visual. El flujo de trabajo es reproducible, con parámetros probados para diferentes tipos de contenido, como rutas geográficas o líneas de tiempo horizontales, garantizando un resultado consistente y profesional. Es compatible con herramientas de TTS como Jimeng y utiliza ASR (faster-whisper) para alinear cada palabra con la animación.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/TaurenRen/srt-whiteboard-workflow/tree/HEAD

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Flujo de trabajo completo para animación de pizarra dibujada a mano (SRT → video de pizarra animada)

Basado en la experiencia práctica con el proyecto open-source srt-whiteboard-animation (github.com/geeklee/srt-whiteboard-animation). Resultado: Video MP4 de pizarra animada "mano que dibuja mientras narra" con voz en off + capturas clave + grabación de pantalla.

Reglas de oro (violarlas implica retrabajo, todas basadas en fracasos reales)

  1. Prohibido usar imágenes de ejemplo como entrega. Las imágenes en examples/ como scene-01-monkey-mountain-banana.png solo sirven para verificar el pipeline. El usuario pide un tema específico; debes generar una imagen de ese tema. Usar imágenes de ejemplo = entrega fallida.
  2. La imagen fuente debe ser depurada. El fondo amarillo crema generado por IA tiene mucha textura granular. El renderizador adaptiveThreshold interpreta la textura del papel como tinta, resultando en líneas punteadas y ruido de fondo. Debes ejecutar scripts/clean_source.py para eliminar la textura del papel antes de renderizar.
  3. El último elemento no debe usar toda la región. mask permitida = región − regiones siguientes. Si la región del último elemento es todo el lienzo, vaciará las regiones permitidas de todos los elementos anteriores → las primeras escenas no dibujarán nada y perderán todos los colores (confirmado en pruebas). El último elemento debe elegir un área pequeña real (por ejemplo, una banda de nubes en la parte superior).
  4. La imagen de la mano debe escalarse según el lienzo. La original drawing-hand.png es 1069×1472. Pegarla directamente en 1280×720 ocuparía más del 40% de la pantalla, obstruyendo severamente la vista. En pruebas, escalar al 55% (587×809) hace que el ancho de la mano sea aproximadamente el 21% del lienzo, con una apariencia normal.
  5. La duración de la salida renderizada será menor que la anotación. El renderizador determina el número de fotogramas según los píxeles dibujables reales de cada área; sceneDurationMs es solo un límite superior. En pruebas, una anotación de 8×8000ms=64s solo renderizó 45.8s. Debes pasar --total-ms y calibrar con la duración real medida con ffprobe.
  6. La grabación de pantalla no debe terminarse con kill. Si ffmpeg se mata forzosamente, pierde el atom moov y el archivo reporta moov atom not found. Usa -t <segundos> para que la grabación termine naturalmente.
  7. Es obligatorio entregar grabación de pantalla y capturas de pantalla. Esto es parte del requisito, no una opción. Las capturas deben cubrir: imagen fuente, imagen de verificación de anotación, consola de renderizado y fotogramas clave del video final. "Reemplazar capturas con documentación" es un trabajo incompleto.
  8. Un lienzo solo cuenta una historia y debe ser de "progresión continua". Error real: Colocar la bandera roja, el puente, la montaña nevada y la pagoda en una sola imagen y dibujarlos todos a la vez → el usuario comentó "dibujo desordenado". La forma correcta es organizar los elementos a lo largo de una línea principal (ruta geográfica / línea de tiempo), con los trazos moviéndose continuamente a lo largo de la línea. El espectador ve "la mano caminando", no "detalles añadidos por todo el lienzo".
  9. La disposición espacial debe seguir la dirección real. Para dibujar la Larga Marcha, debes seguir la geografía real: Ruijin en el sureste → hacia el oeste → hacia el norte → norte de Shaanxi. Si la composición es incorrecta, ni el ritmo más preciso puede salvarla. Antes de dibujar, confirma la posición relativa de cada nodo en un mapa.
  10. El ritmo del dibujo a mano = ritmo de la narración, un elemento por frase. El durationMs de cada elemento debe ser igual a la duración real de la frase de voz correspondiente (deducida de la detección de silencios con sync_timing.py). Nunca distribuyas por igual. El trazo de la escena N debe detenerse en el momento en que "termina la frase N".
  11. Las escenas y la narración deben corresponderse una a una. Error real: La pagoda (ciudad de Wuqi) se colocó en la escena de la frase de resumen, resultando en que mientras la narración decía "llegaron a Wuqi", la imagen dibujaba otra cosa. La frase de resumen final ("Veinticinco mil li...") no debe emparejarse con un nuevo elemento. Debe caer en el "fotograma congelado de todo el dibujo completo". El renderizador rellenará automáticamente fotogramas vacíos hasta --total-ms para crear una pausa de contemplación.
  12. Los subtítulos deben quemarse por separado, no confiar en force_style. El SRT puro no tiene PlayRes; libass usa la resolución interna predeterminada para calcular FontSize/MarginV, y en pruebas no se pudieron ajustar los valores correctos. Debes usar make_srt.py → srt2ass.py (escribiendo explícitamente PlayRes = tamaño del video y anclando la parte inferior con {\an2\pos(x,y)}) para tener control.
  13. Los subtítulos solo deben tener una línea. Dos líneas ocuparán dos áreas de la pantalla simultáneamente, y para el chino, leer dos líneas interrumpe el flujo. Si una línea no cabe, divídelo en múltiples subtítulos consecutivos (distribuyendo la duración proporcionalmente al número de caracteres). No comprimas líneas ni reduzcas el tamaño de fuente. En pruebas: tamaño de letra 46px + línea simple ≤ 14 caracteres, ocupa aproximadamente el 60% del ancho de pantalla en 720p, justo para lectura en móvil.
  14. El lienzo debe poder moverse, no metas toda la imagen en un plano fijo. Los espectadores comentaron "no es necesario dibujar todo en un solo lienzo" — la forma correcta es renderizar el lienzo ampliado (2x), encuadrando solo el elemento actual a pantalla completa en cada escena. Cuando termines de dibujar, la cámara se desplaza al siguiente y finalmente se aleja para mostrar la imagen completa al final. Un plano general fijo de principio a fin significa que cada elemento ocupa solo el 10% de la pantalla, sin que se vea nada con claridad.
  15. La punta del lápiz debe seguir el contenido. El flujo ascendente contour-wipe es "borrado de abajo arriba de toda el área + punta del lápiz barriendo horizontalmente al azar", los dos están completamente desconectados → el usuario ve "la mano se mueve pero no dibuja nada". Cámbialo a pincel serpenteante: donde barre el lápiz, aparece el color.
  16. Al hablar de nodos clave, debes escribir. Solo dibujar objetos sin escribir no permite que el espectador recuerde "qué año, qué lugar". Incrusta el nombre del lugar/año en la imagen de fondo con add_handwriting.py, y luego haz que el renderizador use el modo textwrite para escribir columna por columna — el lápiz avanza a lo largo del trazo, las letras aparecen una por una, el mismo movimiento que "escribir a mano".
  17. Después de ampliar el lienzo, debes hacer dos cosas, o será lento o borroso: ① La imagen fuente debe ser preampliada con INTER_CUBIC al tamaño del lienzo (el resize interno del renderizador es INTER_AREA, que al ampliar actúa como vecino más cercano, convirtiendo las líneas en bloques); ② La aplicación de tinta debe cambiarse a procesamiento local basado en el bounding box (el flujo ascendente crea una máscara de todo el lienzo para cada segmento, 4 veces más lento en un lienzo de 2560).
  18. Antes de la mezcla de audio, el video final debe tener 3-7 segundos para el alejamiento final. La última escena no debe terminar justo en el último milisegundo de la narración, o no habrá tiempo para el movimiento de "mostrar la imagen completa".
  19. El encuadre de la cámara debe usar el "bounding box de tinta real", no la región anotada. La región es un rectángulo dibujado a mano que generalmente es más grande que el gráfico y, en imágenes densas, puede abarcar 2-3 vecinos. Usarlo directamente para calcular el zoom da como resultado "centro de la pantalla vacío, gráfico arrinconado, vecino cortado por la mitad". El renderizador ya tiene incorporado: en set_target(element, allowed), usa ink_pixels & allowed para calcular el bbox y luego expandirlo. Excepción: los subtítulos de texto no usan bbox — las etiquetas de texto suelen ser más anchas que la región, y aplicar bbox cortaría la frase completa fuera del marco (probado con "IA conversacional" que salía del encuadre).
  20. Al cambiar de escena, primero mueve la cámara y luego dibuja. Cada escena debe tener aproximadamente 5 fotogramas de inactividad al principio (0.22s); de lo contrario, el lápiz comenzará a dibujar antes de que la cámara esté en posición, dibujando fuera del encuadre, y el espectador verá "la mano se mueve pero sin contenido".
  21. --zoom-fill debe ajustarse según la densidad de la imagen: 0.86 para imágenes dispersas, 0.70 para líneas de tiempo densas; un solo parámetro para todo causará problemas en ambos casos.
  22. Los elementos alargados verticalmente no llenarán la pantalla, acéptalo. En una relación de aspecto 16:9, los elementos verticales solo pueden ocupar 60-70% de altura y 25% de ancho; esto es una restricción geométrica. Para que se vea más lleno, dibuja elementos más anchos o usa salida vertical.
  23. La narración debe generarse de una sola vez, prohibido segmentar frase por frase. Segmentar y luego concatenar = velocidad/tono/pausas inconsistentes entre segmentos + desalineación inevitable de la línea de tiempo. El usuario lo describirá como "galimatías". Jimeng puede procesar 1500+ caracteres de una sola vez (≈2 minutos); alimenta todo el texto de una vez.
  24. La línea de tiempo proviene de las marcas de tiempo de la transcripción de la narración, no de la detección de silencios. Una pausa ≠ límite de oración; si el número de oraciones no coincide, solo se puede distribuir por igual según el número de caracteres, y los subtítulos serán incorrectos. Usa asr_align.py (ffmpeg → faster-whisper word_timestamps → línea de tiempo a nivel de carácter).
  25. Si cambias la narración, debes regenerar los subtítulos y las duraciones de las anotaciones. Las marcas de tiempo de los subtítulos son un atributo del archivo de audio, no del video. Usar subtítulos antiguos con audio nuevo es como no haber hecho nada.
  26. Antes de la entrega, verifica el texto de la narración vs. la transcripción con difflib. Si la similitud es <0.8, o si hay frases completas faltantes o duplicadas → el audio tiene problemas, regenéralo. Los errores de homófonos (弘/hóng vs. 红/hóng, 尊/zūn vs. 遵/zūn, 武/wǔ vs. 吴/wú) son fenómenos ASR normales y no se consideran errores de lectura.
  27. Después de generar la narración, debes hacer una revisión de silencios. "Enviar todo el texto de una vez al TTS" no significa que la plataforma interna no lo divida en bloques: Jimeng inserta silencio absoluto de -120dB entre bloques (medido 4.00s / 2.60s en pruebas). Si la proporción de silencio es >20% o hay pausas >1.2s → primero comprime con tighten_audio.py y luego habla de alineación de la línea de tiempo.
  28. El punto de inicio de los subtítulos debe ajustarse al fotograma de voz real. ASR, en presencia de largos silencios, marca el inicio de la siguiente oración al comienzo del silencio. El espectador "verá los subtítulos mientras espera el sonido". asr_align.py corrige esto por defecto con snap_to_voice; no lo desactives.

Estándar de directorios (obligatorio)

D:/04-Project-Tests/<YYMMDD>-<Project-Abbreviation>/
  00-Resources/       Materiales del proyecto y notas del código fuente
  01-Assets/          Imágenes originales generadas por IA, materiales de la mano
  02-Subtitles-and-Voice/ SRT, narración mp3, tabla de duraciones
  03-Annotations/     <nombre-de-imagen>.png + <nombre-de-imagen>.annotation.json (deben tener el mismo nombre y directorio, la vista previa depende de esto)
  04-Render-Output/   silent.mp4 de escena individual, mp4 final
  05-Screen-Recording-and-Screenshots/ Grabación de pantalla mp4 + capturas clave png
  06-Docs/            Scripts, registros de operación, lista de verificación

Prerrequisitos

git clone https://github.com/geeklee/srt-whiteboard-animation.git
cd srt-whiteboard-animation && python scripts/prepare_env.py   # Crea .venv con opencv/numpy/av

Todos los comandos de renderizado posteriores deben usar srt-whiteboard-animation/.venv/Scripts/python.exe.

Si aparece el error _lay_ink() takes 6 positional arguments but 7 were given, es un bug conocido del repositorio: En render_stream_whiteboard.py, la llamada self._lay_ink(writer, ink_frames, [], set(), None, allowed) tiene un None extra. Simplemente elimínalo (scripts/check_bug_fix.py puede detectar y reparar automáticamente).

Flujo de trabajo (7 pasos: storyboard → generación de imagen → pizarra de narración espacial → anotación → alineación de narración → renderizado → subtítulos)

Fase 1 · Guion y storyboard (confirma antes de continuar)

  1. Escribe 6–10 frases de narración, cada una de 6–10 segundos, duración total de 40–90 segundos.
  2. Analiza el storyboard (opcional, también se puede hacer manualmente):
    <ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py subtitles.srt --target-sec 30 --min-sec 25 --max-sec 35
    
  3. Genera una tabla de storyboard (escena / expresión central / sujeto de la imagen / texto del subtítulo), detente y espera la confirmación del usuario.

Fase 2 · Generación de imagen temática por IA

De acuerdo con las "Especificaciones visuales unificadas de generación de imágenes del proyecto", escribe el prompt de generación de imágenes. Cada uno debe contener:

  • Boceto de líneas dibujado a mano minimalista / boceto a lápiz puro / garabato contenido estilo Notion, 16:9, mucho espacio en blanco, elementos sin superposición
  • Fondo amarillo crema cálido #F5EBD7, líneas gris oscuro #333333, solo pequeños toques de rojo/naranja/azul
  • Prohibido absolutamente cualquier texto (caracteres chinos/letras/números/etiquetas/nombres de lugares/títulos/marcas de agua), prohibido realismo/3D/degradado/alta saturación

Usa ImageGen o Jimeng image para generar la imagen. Cada imagen corresponde al significado central de una escena. Los elementos deben poder dividirse con rectángulos de región.

Después de generar la imagen, debes hacer tres cosas:

  1. Eliminar marca de agua: Las imágenes generadas por IA suelen tener AI生成 WORKBUDDY en la esquina inferior derecha; elimínala con cv2.inpaint.
  2. Convertir a 16:9: Las imágenes generadas suelen ser 3:2; añade fondo amarillo crema a los lados izquierdo y derecho y luego escala a 1280×720 (no recortes centrado, ya que cortarías el sujeto).
  3. Depurar: Elimina la textura del papel, conserva las líneas y los toques de color.
    <ENV_PY> scripts/clean_source.py imagen_original.png 03-Annotations/imagen_principal.png
    
    Principio: paper = filtro mediano de kernel grande, líneas = gray < paper-18, color = saturación>70 y gray<190, solo conserva líneas|color, rellena el resto con fondo de papel; fuerza 8px alrededor para eliminar residuos de borde.

Fase 2.5 · Tablero de narración espacial (determina si el video final está "desordenado", debes dibujarlo primero)

Antes de escribir el prompt de generación de imágenes, primero define la línea principal con texto. Dos categorías:

TemaLínea principalPrincipio de diseño
Ruta histórica / Batalla / MigraciónRuta geográfica realEl punto de partida está en la posición real; si la ruta va "oeste→norte→este", colócalo así; las coordenadas x/y de los nodos deben coincidir con las posiciones reales
Historia de la tecnología / Historia de la empresaUna línea de tiempo horizontalEl eje atraviesa todo el ancho, los nodos están equidistantes, los iconos se alternan arriba y abajo (arriba→abajo→arriba), evitando que todos se apilen en un lado

Restricciones estrictas:

  • Solo se permite una línea principal. No insertes elementos "de explicación complementaria" dispersos en la línea principal.
  • Los elementos no se superponen, y cada elemento debe poder ser encerrado por un rectángulo que no cruce la línea principal (de lo contrario, las regiones se restarán entre sí).
  • La línea principal debe atravesar todo el ancho: el espacio en blanco debe distribuirse uniformemente a ambos lados de la línea principal. No debe aparecer "todo apretado en la esquina inferior derecha, gran espacio vacío en la superior izquierda". Error real: el punto de partida de la Larga Marcha en el sureste estaba en la esquina inferior derecha; los primeros 20 segundos del video mostraban la esquina inferior derecha apilada y la superior izquierda completamente vacía — esto es un problema de composición; en la generación de la imagen, el punto de partida debe colocarse más hacia el centro para que la ruta use todo el lienzo.

En el prompt de generación de imágenes, especifica la posición de cada elemento en la imagen ("una pequeña bandera roja en la esquina inferior derecha", "un puente de cuerdas de hierro en el centro-izquierda"). No solo escribas "dibuja un puente" — si no se especifica la posición, la composición que dé la IA no será utilizable.

Fase 2.6 · Dos paradigmas de imagen de fondo (parámetros probados, copia directamente)

Si el video final "puede llenar la pantalla y la cámara puede moverse" se determina en la fase de generación de la imagen. Cada paradigma tiene su propio conjunto de parámetros; no los mezcles:

A. Tipo de ruta geográfica (historia/batalla/migración) — Representante: Ruta de la Larga Marcha del Ejército Rojo

ElementoValorBase
ComposiciónPunto de partida según la posición real, ruta oeste→norte→este a lo anchoComposición incorrecta, ni el ritmo más preciso puede salvarla
Número de nodos6–8 paradasUna parada = dos escenas (dibujar objeto + escribir) → 12–16 elementos
Cámara --zoom-fill0.86Mucho espacio en blanco entre elementos; al acercarse, no se ve vacío
DensidadLos elementos pueden ser grandes o pequeños (un puente puede ser 3 veces más ancho que una bandera)La ruta en sí misma tiene cosas de diferentes escalas

B. Tipo de línea de tiempo horizontal (historia de la tecnología/historia de la empresa/iteración de producto) — Representante: Historia del desarrollo de agentes inteligentes (La versión que el usuario calificó como "muy buen efecto", parámetros copiados directamente)

ElementoValorBase
EjeUn eje principal en forma de S/u onda que atraviesa todo el ancho, 8 nodos equidistantesEquidistante → velocidad de desplazamiento de la cámara constante, se ve estable
IconosAlternar arriba y abajo (arriba→abajo→arriba…), no los coloques todos en una filaUna fila solo deja la mitad del ancho de pantalla utilizable; el encuadre desperdicia la mitad vertical
EtiquetasColgar en el mismo lado exterior del icono: etiquetas de nodos superiores arriba, nodos inferiores abajoJunto con el icono en la misma pantalla, la cámara no necesita desplazarse para escribir
Tamaño de fuenteNombre principal 36–40px en estilo KaiTi, año 0.6 veces el tamaño debajoAl acercarse, ≈ 60–80px en pantalla, legible en móvil
Cámara --zoom-fill0.70Los nodos izquierda y derecha están cerca; acercarse demasiado cortaría a los vecinos por la mitad
Proporción de una parada dos escenasDibujar icono 0.60 / Escribir "nombre + año" 0.40Escribir subtítulos debe tener suficiente tiempo, o el texto no terminará de escribirse antes de cortar
El eje de tiempo en síDibujar entre dos paradas adyacentes, manejarlo en la misma escena que el icono de esa paradaDe lo contrario, el eje aparecerá todo de una vez, rompiendo el ritmo

Tres advertencias estrictas para el tipo B:

  1. No uses bounding box de tinta para subtítulos de texto (ver excepción de regla de oro 19). Las etiquetas de texto suelen ser más anchas que la región; aplicar bbox cortaría la frase completa fuera del marco.
  2. La región de la etiqueta debe tener altura para dos líneas "nombre principal + año", da 80–90px de altura; si solo das para una línea del nombre principal, se cortará el año.
  3. El final debe alejarse para mostrar todo el eje. El valor narrativo de la línea de tiempo está en su "alcance general". Si no te alejas en los últimos 3–5 segundos, es como no haber dibujado nada.

Línea de base probada (1280×720 / 24fps): 8 nodos × 2 escenas = 16 elementos, video final 43.5s, renderizado 33s, --zoom-fill 0.70 --zoom-max 7 --hand-height 350.

Fase 3 · Anotación a nivel de píxel (la más propensa a errores, debe verificarse)

Primero mide las coordenadas, no estimes visualmente. Lección real: Confundir el tamaño de visualización de vista previa de 640×360 con las coordenadas reales de 1280×720, resultando en dos rondas de coordenadas completamente incorrectas. La forma correcta es usar detección de color/líneas para obtener el bbox real:

<ENV_PY> scripts/detect_marks.py 03-Annotations/imagen_principal.png etiqueta   # bbox de componentes conectados de toques rojo/naranja/azul
<ENV_PY> scripts/detect_lines.py 03-Annotations/imagen_principal.png etiqueta   # bbox de grupos de líneas gris oscuro

Para calibración visual, superpone una regla: scripts/grid_ruler.py imagen_principal.png con_cuadricula.png (cuadrícula de 100px, marcando píxeles reales).

Autoverificación de cobertura de región (determina si el video final está completo):

  • Los intervalos x de todas las regiones deben formar una banda continua (se permite superposición, no se permiten espacios)
  • Los sujetos transversales (rutas, líneas de tiempo) deben estar completamente cubiertos por algunas regiones, o el video final tendrá líneas rotas
  • El último elemento debe elegir un área pequeña por separado (ver regla de oro 3)

Escribe 03-Annotations/<nombre>.annotation.json:

{
  "sceneId": "scene-01",
  "canvas": { "width": 1280, "height": 720 },
  "storyBasis": "Resumen del evento del subtítulo de esta escena",
  "sceneDurationMs": 56000,
  "elements": [
    {
      "id": "ruijin", "label": "Salida·Ruijin", "sequence": 1,
      "narrativeRole": "Punto de partida de la Larga Marcha", "subtitle": "Texto original del subtítulo correspondiente",
      "type": "point",
      "region": { "x": 265, "y": 515, "width": 145, "height": 100 },
      "reveal": { "direction": "left_to_right", "startMs": 0, "durationMs": 8100,
                  "maskPaddingPx": 16, "protectedRegions": [] },
      "handPath": { "start": [275, 570], "end": [400, 575], "easing": "easeInOut" }
    }
  ]
}
  • canvas debe ser igual a los píxeles reales de la imagen fuente; region debe ser en píxeles enteros y estar dentro del lienzo.
  • sequence sigue el orden narrativo, no visual de izquierda a derecha.
  • Bajo el método de dibujo stream, direction/handPath solo se usan para la demostración de la vista previa, no afectan los trazos del video final.

Genera una imagen de verificación y confírmala visualmente fotograma por fotograma:

<ENV_PY> scripts/region_sheet.py imagen_principal.png anotacion.json 05-Screen-Recording-and-Screenshots/verificacion-region.png

Cada rectángulo debe encerrar exactamente el elemento objetivo; si encierra un espacio en blanco u otro elemento → coordenadas incorrectas, vuelve al paso anterior.

Fase 4 · Narración y alineación de línea de tiempo

① Narración: Generar todo el texto de una vez, nunca por frases (lección de retrabajo del 2026-11-05)

"<TTS_PYTHON>  # Cualquier Python 3.9+ con requests instalado" \
  "jimeng-suite/scripts/jimeng_cli.py" audio \
  --text "Narración completa, un párrafo, usa puntos para separar oraciones" \
  --model "SeedAudio 1.0" --submode "Narración versátil" --voice "狒狒_讲解" \
  --out "xxx-voice.mp3" --outdir "<proyecto>/02-Subtitles-and-Voice" --timeout 900
  • Seed TTS + Texto a voz se retiró el 2026-10-24, no lo uses más.
  • Alimenta todo el texto de una vez con --text. Puede procesar 1500+ caracteres de una sola vez (≈2 minutos de audio). No lo dividas en decenas de segmentos de tres segundos: Error real: Dividir un guion de 56 segundos en 12 segmentos, generarlos por separado y luego concatenarlos resultó en velocidad/tono/pausas inconsistentes entre segmentos, sonando como "galimatías", y después de la concatenación, la línea de tiempo y los subtítulos estaban desalineados — el usuario lo calificó directamente como no apto.
  • Solo divide por párrafos narrativos (un grupo de oraciones por párrafo), cada párrafo de 15–40 segundos, todo el video de 1–3 párrafos; si puedes usar un párrafo, no uses dos.
  • La revisión de contenido bloqueará: términos militares/violentos/dañinos (batalla sangrienta, aniquilar, derrotar, enemigo) devuelven JimengContentRejectedError. Reescribe con narrativa neutral ("pagó un precio pesado", "bajo bloqueo de fuego"), escribir años en números chinos (一九三四年) es más seguro.
  • 99 segundos de diálogo probados produjeron un video de 62 segundos (1.19MB / 99.05s).

② Revisión de silencios y compresión de ritmo (obligatorio, lección de retrabajo v5)

"Alimentar todo el texto de una vez al TTS" no significa que la plataforma interna haya sintetizado un solo segmento: Jimeng divide el texto largo internamente en bloques y los concatena, insertando silencio absoluto de -120dB entre bloques. En la prueba de la Larga Marcha v4 con 317 palabras / 99.05s, 31 pausas ≥0.4s sumaron 27.2s (27.5%), incluyendo dos de silencio de muestreo cero puro (4.00s @14.05, 2.60s @70.60 — las pausas naturales tienen ruido de fondo, aproximadamente -50~-60dB, no llegan a -120dB). La voz efectiva real fue solo de 71.85s (4.41 palabras/s, no es lenta) — "ritmo lento, extraño" fue causado completamente por el silencio.

# 1) Revisión: cualquier silencio >1.2s debe tratarse
ffmpeg -hide_banner -i xxx-voice.mp3 -af "silencedetect=noise=-38dB:d=0.35" -f null - 2>&1 | grep silence_start

# 2) Compresión: conserva las pausas naturales por niveles, no modifica ningún contenido de voz
<ENV_PY> scripts/tighten_audio.py 02-Subtitles-and-Voice/xxx-voice.mp3 \
  02-Subtitles-and-Voice/xxx-voice-tight.mp3 --report 06-Docs/_tighten.json

tighten_audio.py: ventana RMS de 50ms, <-40dB se considera silencio. Las pausas ≥1.0s se dejan en 0.55s, pausas de 0.35–1.0s se dejan en 0.32s, el inicio se deja en 0.12s, y se vuelve a concatenar según la muestra. Medido v4→v5: 99.05s → 83.15s, proporción de silencio 27.8% → 14.0%, sin pausa >0.7s.

La compresión cambia la longitud del audio. A partir de ahí, la transcripción ASR, el timing, SRT, ASS, las duraciones de anotación y el total-ms de renderizado deben volver a ejecutarse basándose en el audio comprimido (regla de oro 25). --reuse-asr solo se usa para ajustar saltos de línea en subtítulos; una vez que el audio cambia, nunca reutilices el volcado antiguo.

③ Línea de tiempo: deduce de las marcas de tiempo de la propia transcripción de la narración, no adivines con detección de silencios

<ENV_PY> scripts/asr_align.py 02-Subtitles-and-Voice/xxx-voice.mp3 06-Docs/texto.txt \
  --model small \
  --out-timing 03-Annotations/xxx.annotation.timing.json \
  --out-srt 02-Subtitles-and-Voice/xxx.srt --max-chars 18 \
  --dump-asr 06-Docs/_asr.json

Lo que hace asr_align.py: ffmpeg convierte a 16k mono → faster-whisper (word_timestamps=True) → crea una línea de tiempo carácter por carácter → localiza cada oración del texto en la transcripción completa (búsqueda exacta → ancla de primeros 8 caracteres → respaldo) → genera timing.json (compatible con segments[i/start/end/text]) y SRT de una línea.

¿Por qué no usar silencedetect? El silencio solo indica "aquí hay una pausa"; las pausas para respirar, enfatizar, etc., también ocurren. Si el número de oraciones y pausas no coinciden, solo se puede distribuir por igual según el número de caracteres, y los subtítulos serán incorrectos.

asr_align.py también realiza por defecto ajuste de límite de oración (snap_to_voice): faster-whisper, en presencia de largos silencios, tiende a marcar el start de la siguiente oración al comienzo del silencio, lo que hace que los subtítulos aparezcan antes de tiempo y el espectador mire las letras esperando el sonido (en v4, aparecieron 4 segundos antes). El script encuentra el punto de inicio de voz real en una cuadrícula de 20ms y lo corrige; --no-snap puede desactivarlo.

④ Debes verificar el contenido del audio (el único estándar duro para juzgar "galimatías"):

<ENV_PY> -c "..."   # difflib compara texto completo vs _asr.json

La similitud debe ser >0.8; para nombres propios en chino, el modelo small solo cometerá errores de homófonos (弘/hóng vs. 红/hóng, 尊/zūn vs. 遵/zūn, 武/wǔ vs. 吴/wú, 锁/suǒ vs. 索/suǒ). Si la estructura (orden de oraciones, longitud de oraciones) es consistente, la lectura es correcta. Si aparecen oraciones completas faltantes, duplicadas o saltos → el audio en sí tiene problemas, regenéralo.

⑤ Tres reglas para saltos de línea en subtítulos (todas en asr_align.py::split_subs):

  • Primero divide por puntuación en oraciones pequeñas. Solo si el bloque actual es < limit×0.55, fusiona con la siguiente oración pequeña — de lo contrario, aparecerán concatenaciones como "emprendió el famoso camino de la Larga Marcha ese año" que cruzan oraciones pequeñas.
  • Después de fusionar, no debe exceder el límite (no dejes tolerancia de +3; 20 caracteres divididos equitativamente producirán "…qué tan lejos caminar solo" con una sola palabra colgando).
  • Solo si un bloque individual sigue siendo demasiado largo, divídelo equitativamente; las últimas ≤4 caracteres se fusionan con la entrada anterior.

⑥ Espacio en blanco al final (método de implementación de la regla de oro 18): Agrega "tail": 0.30 a la última parada. El último 30% de ese segmento no programa ningún elemento, y el renderizador usará ese tiempo para alejarse y congelarse. Probado en un video de 99 segundos, dejando 4 segundos, justo para ver la imagen completa.

⑦ Asignación de duración del storyboard: Un elemento por oración (o una acción dentro de una oración). durationMs se toma directamente de la duración de esa oración. Dentro del segmento, se divide por ratio" en "dibujar objeto / escribir". Ver scripts/make_annotation_v3.py`.

Fase 5 · Renderizado, mezcla y verificación

<ENV_PY> srt-whiteboard-animation/scripts/render_stream_whiteboard.py \
  03-Annotations/imagen_principal.png 03-Annotations/imagen_principal.annotation.json 04-Render-Output/xxx-silent.mp4 \
  01-Assets/hand-55pct.png \
  --cap-long-edge 1280 --fps 24 --total-ms <milisegundos_de_narración>
  • Cuando --cap-long-edge es igual al borde largo de la imagen fuente, no se escala más; para vista previa, se puede reducir para acelerar.
  • --total-ms pasa la duración de la narración (milisegundos) para asegurar que la imagen no termine antes que la narración.
  • La última línea de salida es OUTPUT=ruta.

Mezclar la pista de audio:

ffmpeg -y -i xxx-silent.mp4 -i narracion.mp3 -c:v copy -c:a aac -b:a 192k -shortest video_final.mp4

Tres puntos de verificación obligatorios (toma un fotograma de cada uno):

  1. Inicio limpio con fondo amarillo crema, sin líneas prematuras.
  2. Parte media: líneas continuas, colores correctos (rojo/naranja/azul presentes).
  3. Final: imagen completa, sin líneas rotas ni grandes espacios en blanco.

Si la desviación de duración es >1s, ajusta --total-ms o modifica ligeramente cada durationMs y vuelve a renderizar.

Fase 5.5 · Cámara y escritura (núcleo de v3, determina "si parece que está dibujando")

El renderizador ascendente dibuja en un lienzo fijo de principio a fin. Para que la imagen se parezca a un lenguaje de cámara, agrega una subclase render_camera_whiteboard.py, sin modificar el código del repositorio (scripts/render_camera_whiteboard.py ya se puede reutilizar):

<ENV_PY> scripts/render_camera_whiteboard.py 03-Annotations/imagen_principal.png 03-Annotations/imagen_principal.annotation.json \
  04-Render-Output/xxx-silent.mp4 01-Assets/hand.png \
  --canvas-long-edge 2560 --frame 1280x720 --fps 24 --grid-edge 20 \
  --zoom-fill 0.86 --zoom-max 7 --hand-height 350 --total-ms 56064

Hace seis cosas:

MecanismoImplementaciónPor qué
Encuadre de cámaraRenderiza el lienzo a 2560×1440, cada fotograma se recorta según el viewport y se escala a 1280×720Los elementos pueden "dibujarse a pantalla completa", y cuando termines, la cámara se desplaza
Encuadre por bounding box de tintaMarco objetivo = "bbox de tinta real dentro de la máscara allowed de esa escena", no la regiónLa región suele ser más grande que el gráfico e incluso puede abarcar vecinos; usarla directamente para el encuadre → centro de la pantalla vacío, gráfico arrinconado (v1 de v3 falló aquí)
Seguimiento de cámaraSuavizado exponencial entre escenas adyacentes tau 0.20/0.30, pad de escritura 0.34, pad de dibujo 0.05Produce naturalmente un desplazamiento de cámara, sin necesidad de fotogramas de transición
Amortiguación de cambio de escenaCada escena comienza con 0.22s (aprox. 5 fotogramas) de inactividad antes de dibujarLa cámara tarda 0.3–0.6s en moverse de la escena anterior; dibujar durante este tiempo hará que "el lápiz dibuje fuera del encuadre", una de las principales causas de "la mano se mueve pero no hay contenido"
Imagen completa al finalDespués de la última escena, el objetivo se establece en todo el lienzo, se añaden fotogramas hasta --total-ms"Mostrar una imagen completa al final"
Tamaño fijo de la mano en pantallaLa mano no se dibuja en el lienzo, se superpone en el fotograma de salida (--hand-height 350)De lo contrario, al acercarse, la mano ocuparía toda la pantalla; 430 probado sigue siendo demasiado grande, cubriendo la escritura

--zoom-fill debe ajustarse según la densidad de la imagen de fondo (un mismo conjunto de parámetros no puede ser universal):

Tipo de imagen de fondozoom-fillRazón
Dispersa (mapa de ruta/diagrama de flujo, mucho espacio en blanco entre elementos)0.86Puedes acercarte con confianza, la imagen no se verá vacía
Densa (línea de tiempo horizontal, elementos adyacentes izquierda y derecha)0.70Acercarse demasiado cortará a los vecinos por la mitad, pareciendo desordenado

Restricción geométrica de 16:9: Los elementos alargados verticalmente (tablero de ajedrez, teléfono) están limitados por la altura de la imagen; a pantalla completa solo pueden alcanzar 60-70% vertical y 25% horizontal. Esto es geométrico, no un bug. Para que se vea más lleno, dibuja elementos más anchos o usa salida vertical.

Escritura (escribir nombres de lugares/años):

# 1) Primero, incrusta las etiquetas en la imagen de fondo (las coordenadas deben ser las de anotación de 1280×720, el script organizará automáticamente dos líneas según el tamaño de fuente)
<ENV_PY> scripts/add_handwriting.py fondo2x.png 03-Annotations/imagen_principal-v3.png 02-Subtitles-and-Voice/etiquetas.json
# 2) En la anotación, asigna a esta escena "inkPath": "textwrite", la región es el cuadro de texto

textwrite avanza por columna de derecha a izquierda, con la punta del lápiz al frente de la revelación, asegurando que el texto se escriba completamente (el seguimiento del esqueleto pierde trazos finos, no lo uses). El primer 30% de los fotogramas mantiene el lápiz esperando a que la cámara esté en posición, luego comienza a escribir — de lo contrario, el texto se escribirá fuera del encuadre. Diseño de etiquetas: nombre principal 34–40px en estilo KaiTi (C:/Windows/Fonts/simkai.ttf) + año 0.6 veces el tamaño de fuente debajo, color (58,56,52) del mismo tono que el boceto pero ligeramente más claro.

División de escenas: Una parada = dos escenas. Primero dibuja el objeto (0.6 de la duración), inmediatamente después escribe (0.4 de la duración). El encuadre de la escena de escritura = su propio cuadro de texto, para que la cámara se acerque ligeramente, como "el lápiz se mueve a un lado para escribir".

Fase 6 · Quemado de subtítulos (SRT → ASS → quemado en el video)

No marques la línea de tiempo de los subtítulos por separado, usa directamente el .timing.json producido en la fase 4 — ya está alineado oración por oración con la narración. El momento en que cambia el subtítulo es el momento en que el lápiz se detiene, eso es "ritmo alineado".

# 1) timing.json -> SRT (**una línea**, los demasiado largos se dividen automáticamente en múltiples subtítulos consecutivos)
<ENV_PY> scripts/make_srt.py 03-Annotations/imagen_principal.annotation.timing.json 02-Subtitles-and-Voice/x.srt \
  --max-chars 14

# 2) SRT -> ASS (PlayRes explícito + \pos anclando la parte inferior, tamaño de fuente/posición son píxeles reales)
<ENV_PY> scripts/srt2ass.py ...

Importante: Asegúrate de que la duración total del video final coincida con la duración del audio. Después de la mezcla, verifica con ffprobe:

ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video_final.mp4

Fase 7 · Grabación de pantalla y capturas (requisito, no opcional)

Grabación de pantalla del proceso completo de renderizado (debe terminar naturalmente, no con kill):

ffmpeg -y -f gdigrab -framerate 15 -offset_x 0 -offset_y 0 -video_size 1280x720 -i desktop \
  -c:v libx264 -preset ultrafast -crf 28 \
  -t <segundos_de_renderizado_más_5> 05-Recording-and-Screenshots/render-full.mp4
  • No uses -qscale para captura de escritorio, produce artefactos; -crf 28 es suficiente.
  • Asegúrate de que la duración de la grabación cubra todo el proceso de renderizado.

Capturas de pantalla clave (todas en formato png):

  1. Imagen fuente: La imagen original después de la limpieza, sin anotaciones.
  2. Verificación de anotación: La hoja de regiones generada por region_sheet.py.
  3. Consola de renderizado: La ventana del terminal mostrando el progreso del renderizado (sin errores).
  4. Fotogramas clave del video final: 3-4 fotogramas representativos del video final.

Estructura de archivos:

05-Recording-and-Screenshots/
  source-image.png                     # Imagen fuente
  annotation-verification.png          # Verificación de anotación
  render-console.png                   # Consola de renderizado
  final-keyframe-1.png                 # Fotograma clave 1
  final-keyframe-2.png                 # Fotograma clave 2
  final-keyframe-3.png                 # Fotograma clave 3
  final-keyframe-4.png                 # Fotograma clave 4
  render-full.mp4                      # Grabación de pantalla completa

Lista de verificación final

  • Todos los elementos se dibujan en orden según la línea de tiempo.
  • Los subtítulos y la narración están sincronizados.
  • La mano y el lápiz se mueven con el contenido.
  • El fondo está limpio, sin textura de papel.
  • Los colores (rojo/naranja/azul) se conservan correctamente.
  • La composición sigue la línea principal (geográfica o temporal).
  • El último elemento no usa toda la imagen.
  • El video final tiene la duración correcta, con espacio para el alejamiento final.
  • Los subtítulos son de una sola línea y legibles.
  • Se ha verificado la similitud del texto de la narración con la transcripción.
  • Se ha comprimido el silencio excesivo en el audio.
  • Las capturas de pantalla y la grabación de pantalla están completas.

Skills relacionados