Communitygithub.com

znlgis/video-studio

Genera vídeos chinos desde cero con código (documentales, promocionales, cortos animados): escribe un guion → narración con edge-tts → cronograma basado en duración real → renderizado cuadro por cuadro con PIL o Canvas → codificación con ffmpeg → mezcla de audio a nivel de muestra con numpy → control de calidad automático. Úsalo cuando el usuario pida "hacer un video / generar un promocional / convertir un informe en video / animar / doblar y subtitular / producir un video finalizado / arreglar desincronización de audio y video / mejorar la nitidez de un video", o cuando mencione ffmpeg, edge-tts, renderizado cuadro por cuadro, subtítulos, storyboard, banda sonora o mezcla de audio.

¿Qué es video-studio?

Video Studio (video-studio) es una habilidad para agentes de IA (como Claude Code, Cursor o Codex) que permite la producción de videos completamente generados por código, con narración en chino, renderizado determinista cuadro por cuadro y control de calidad automático. Su flujo de trabajo principal es: escribir un guion en `film.json`, generar la narración con edge-tts, construir el cronograma basado en la duración real del audio (no en estimaciones), renderizar cada cuadro como una función pura con PIL o Canvas, codificar con ffmpeg y mezclar el audio a nivel de muestras con numpy. La habilidad impone reglas estrictas: el audio debe generarse antes que el video, cada cuadro debe ser reproducible (sin aleatoriedad), y los subtítulos deben mantenerse dentro de zonas seguras. Incluye un sistema de propuestas obligatorio: antes de renderizar, `propose.py` genera múltiples opciones de diseño visual (paleta de colores, tipografía, música, ritmo) para que el usuario las revise y apruebe, evitando que todos los videos tengan el mismo aspecto. Soporta renderizado por lotes, reanudación desde puntos de interrupción y controles de calidad como VMAF y Loudnorm. Está diseñado para plataformas como Claude Code, Cursor y Codex, y se activa cuando el usuario solicita crear un video, animar una historia, arreglar problemas de sincronía A/V o mejorar la calidad visual.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/znlgis/dsh-video-studio-skill/tree/HEAD/skills/video-studio

Preguntar en tu IA favorita

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

Documentación

Video Studio (video-studio)

En una frase: video = guion + duración real de audio + funciones de cuadro deterministas + codificación ffmpeg + control de calidad automático. Tanto la imagen como el sonido se generan desde cero con código: sin素材, reproducible, reanudable y verificable mediante scripts.

0. Reglas de hierro (si se incumple alguna, el producto final tendrá problemas)

  1. Primero el audio, luego el video. La línea de tiempo debe deducirse de la duración real del TTS, nunca estimando por número de caracteres. Una línea de tiempo basada en caracteres comenzará a desviarse notablemente a partir de la frase 20.
  2. Cada cuadro es una función pura: draw(t, frame) -> Image. Prohibido random/time(), usa filmkit.hash2/hash3/vnoise para aleatoriedad. El mismo número de cuadro debe generar siempre la misma imagen.
  3. Los subtítulos y el texto grande deben estar en la zona segura: texto principal y ∈ [58, 974], subtítulos con fondo y ∈ [872, 1030], nunca superar 1030.
  4. Los cuadros originales se codifican directamente: cuadro PIL → tubería rawvideo → libx264. No uses cv2.VideoWriter para archivos intermedios (2 veces más lento y con pérdida secundaria).
  5. La mezcla de audio se hace a nivel de muestra con numpy, no uses un solo comando ffmpeg con cientos de entradas (en Windows puede producir silencio no determinista).
  6. Antes de entregar, ejecuta el script de control de calidad y revisa 3 fotogramas tú mismo (con read_image). "Puede generar imágenes" no significa "no hay problemas".
  7. El sonido sigue a la imagen: los puntos de señal (como K.CueBus / el hook cues() de scenes.py) se registran en el código de la escena, no se ajustan manualmente después. Si la imagen cambia (se modifica la ventana de permanencia, se ajusta el ritmo), los puntos de señal se actualizan automáticamente.
  8. Si no ves algo claro, añade pausa, no ralentices todo el video. hold (global) en film.json o lines[].hold (por frase) añade pausa después de la voz; la música usa bpm para alinear compases.

0.5 [Regla de hierro] Primero la propuesta, luego el video (incluso si el usuario no lo pide)

Si el usuario no lo pide, igual debes presentar una propuesta. Esta es la regla de producto más importante de esta habilidad, no es una sugerencia. Razón: si el programa elige la apariencia por el usuario, este nunca participa en la decisión y el resultado inevitablemente será "todos los videos se parecen". Por lo tanto, el flujo es fijo: generar varias opciones sobre la marcha → el usuario elige una → congelar el diseño → luego producir el video.

Acción fija (lo primero en cualquier tarea de video nuevo):

python propose.py --brief "<tema/público/duración/tono>" --script film.json --n 3

Esto genera N opciones candidatas (cada una = un diseño + 4 fotogramas + costo + distancia de diseño con otras opciones), las guarda en work/proposal.json e imprime una línea para "confirmar una / si ninguna satisface, usa --revise". Todo offline, en segundos.

Reacción del usuarioLo que debes hacer
"Esa" / elige unapython propose.py --confirm --candidate B, y solo entonces permitir el renderizado
"Demasiado fría/demasiado recargada/cambia la voz/más ritmo"python propose.py --revise "<texto original>" — nueva revisión, la anterior se conserva para diff
"Ninguna sirve, vuelve a empezar"Igual que arriba, especifica los requisitos (el texto de --revise se guarda en proposal.feedback)
Dice explícitamente "tú decides"Igual presenta una propuesta, elige la que recomiendes y explica por qué, espera la aprobación del usuario

Restricciones estrictas:

  1. Antes de propose.py --confirm, render.py en stage frames/all será bloqueado por la compuerta de propuesta (advertencia por defecto, --require-proposal falla).
  2. Las candidatas deben diferir en ≥2 dimensiones de diseño (familia de colores/motivo/transiciones/easing/numérico/música/voz). Solo cambiar el color no cuenta como múltiples opciones, diversity.vs[].n < 2 se marcará.
  3. Cada candidata debe tener una intención en chino (pitch), que explique "qué quiere lograr esta opción" — no "Opción A/Opción B".
  4. No hay plantillas internas para elegir: todos los campos de design.json están abiertos (35 campos de tema + cinco grupos de firmas de lenguaje visual + sonido/música/ritmo), la paleta de colores es cualquier punto en un espacio de matices continuo, no una selección de apariencias predefinidas. Cada propuesta se genera sobre la marcha — dos ejecuciones con el mismo brief producen diseños diferentes (para reproducir, pasa --seed).
  5. Registro de unicidad entre videos (activado por defecto): --confirm registra el diseño congelado en ~/.dsh/video-studio/design-ledger.json (máximo 500 entradas). La próxima propuesta evitará activamente apariencias entregadas recientemente — si la distancia de diseño entre una candidata y cualquier video en el registro es < 2 dimensiones, se re-muestrea automáticamente con un nuevo nonce y se advierte. Este es el mecanismo para que "diferentes videos no se repitan" pase de "depender del azar" a "tener memoria". --show-ledger para consultar el registro, --no-ledger para ignorarlo en esta ejecución, la variable de entorno VIDEO_STUDIO_LEDGER cambia la ruta o se establece en off para desactivarlo. Si falla la lectura/escritura del registro, se trata como vacío, nunca bloquea la producción del video.

Solo hay un caso en el que se puede saltar la propuesta: reparación pura / re-renderizado puro (sin cambiar contenido ni diseño, solo arreglar desincronización A/V o re-producir el video). En ese caso, ejecuta sin --require-proposal y explica en la respuesta "No se hizo propuesta porque…".

0.5.1 De dónde viene la apariencia: solo dos caminos

No hay "estilos internos" para elegir. Cualquier valor de style (como auto / legacy / un id) que se escriba en film.json será rechazado explícitamente y se darán dos pasos de instrucción: el guion no determina la apariencia, la apariencia la determina work/design.json. Del mismo modo, no hay tablas THEMES/STYLES/VISUAL_LANGUAGES en la habilidad, ni presets/; la música se compone sobre la marcha con musicgen.py según parámetros, y scenes.draw_frame no tiene ramas heredadas.

Para una apariencia específica, solo hay dos caminos:

# ① Pedir en lenguaje natural (reconoce palabras clave en must_have/avoid)
python propose.py --brief "presentación tecnológica, azul oscuro, con formato panorámico" --script film.json

# ② Escribir design.json directamente (todos los campos abiertos, paleta de colores cualquier RGB)
python render.py --script film.json --stage all     # Requisito: work/design.json ya existe

Para más detalles, consulta references/proposal.md y schemas/design.schema.json.


1. Primeros pasos en cinco pasos (sin copiar nada: sin copiar scripts, sin copiar plantillas)

Esta ruta no implica ninguna "copia". Los scripts de la habilidad se ejecutan directamente desde el directorio de la habilidad; tu directorio de proyecto solo necesita un film.json (además de los directorios work/ y out/ generados durante la ejecución).

<SKILL_DIR> se refiere al resourceBase.path que la herramienta skill informa al cargar esta habilidad. Una vez instalado como plugin, será el directorio skills/video-studio/ dentro del paquete (el node_modules/@dsh-gis/dsh-video-studio-skill del perfil es un enlace). Si no se encuentra, localiza por nombre: find ~ -path '*/skills/video-studio/scripts/filmkit.py'.

<REPO> se refiere a la raíz del paquete (raíz del repositorio, o node_modules/@dsh-gis/dsh-video-studio-skill/ en el perfil después de instalar), es decir, dos niveles por encima de <SKILL_DIR>. tools/ y schemas/ están en <REPO>, no en <SKILL_DIR>.

# 1) Escribe solo un film.json (el único archivo que necesitas escribir manualmente en el directorio del proyecto)
mkdir -p myfilm && cd myfilm
cat > film.json <<'JSON'
{
  "title": "Mi video",
  "lines": [
    {"id": "l01", "kind": "hook",  "chapter": "01 · Inicio",
     "text": "Esto es una imagen y sonido generados desde cero con código.", "sub": "Sin素材, todo con código generado", "big": "Sin素材, todo con código generado"},
    {"id": "l02", "kind": "body",  "chapter": "02 · Cuerpo", "kicker": "PIPELINE",
     "text": "Todo el pipeline tiene solo cinco pasos: escribir el guion, generar la narración, organizar la línea de tiempo, renderizar cuadro por cuadro y mezclar el audio.",
     "sub": "Cinco pasos para el video, cada paso se guarda", "big": "Cinco pasos",
     "items": [{"text": "Guion film.json"}, {"text": "Narración"}, {"text": "Línea de tiempo"}, {"text": "Renderizado cuadro por cuadro"}, {"text": "Mezcla de audio"}]},
    {"id": "l03", "kind": "outro", "chapter": "99 · Cierre",
     "text": "Primero define el guion, luego el sonido, y al final las imágenes.", "sub": "El orden es más importante que el esfuerzo", "big": "El orden es más importante que el esfuerzo"}
  ]
}
JSON
#    lines[].text = texto a leer (habla natural); sub = subtítulo en pantalla (corto); kind = hook/body/quote/outro/emphasis/calm
#    El estilo visual/música/voz/ritmo los determina design, en film.json **no** hay campo style que escribir.
#    Validación del contrato (opcional, unos segundos):
python <SKILL_DIR>/scripts/filmcheck.py film.json

# 2) Primero, autoverificación del entorno (opcional; 69 compuertas de regresión, unos 10–20 segundos, sin conexión; los módulos opcionales faltantes se marcarán como SKIP)
python <SKILL_DIR>/scripts/selfcheck.py

# 3) [Regla de hierro] Primero la propuesta, luego el video — el mismo brief produce resultados diferentes cada vez, así que el usuario debe revisarlos
python <SKILL_DIR>/scripts/propose.py \
       --brief "Tres años de los grandes modelos de IA, documental de 30 segundos, azul oscuro" \
       --script film.json --work work
#    Genera 3 opciones: cada una con su diseño + hoja de contactos de fotogramas (work/proposals/rev1/_compare.png) + distancia de diseño,
#    resultado en 2 segundos, todo offline. Mira la imagen comparativa, elige una.

# 4) Confirmar la opción, congelar el diseño (work/design.json) — a partir de aquí, el renderizado solo usará este diseño
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
#    Si no te gusta, cambia: --revise "Más frío, menos decoración, voz femenina" (nueva revisión, la anterior se conserva para diff)
#    Para reproducir una ejecución anterior: añade --seed 42 en la primera propuesta (sin seed = nuevo nonce cada vez, esta es la fuente de "cada video es diferente")

# 5) Primero ve los fotogramas, luego produce el video completo; si no se especifica --scenes, se usa el renderizador interno de la habilidad
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills \
       --stills "l01:3,l02:5,l03:2.5"
#    → work/stills/_sheet.png: usa read_image para verlo tú mismo, no te lo saltes

python <SKILL_DIR>/scripts/render.py --script film.json --work work \
       --out out/film.mp4 --stage all
#    TTS→maquetación→música→efectos→mezcla→cuadros→codificación→control de calidad, todo en uno; los productos intermedios están todos en work/, se puede reanudar desde puntos de interrupción.

Sobre --scenes (la clave de "sin copiar"): cuando render.py no usa --scenes, el orden de búsqueda es ① scenes.py en el mismo directorio que el guion → ② assets/scenes.py interno de la habilidad. Por defecto, no necesitas escribir nada, ni copiar nada, el renderizador interno produce el video directamente; solo si quieres modificar las imágenes, coloca tu propio scenes.py (que implemente draw_frame y opcionalmente cues()) en el mismo directorio que el guion; se detectará automáticamente — también puedes usar explícitamente --scenes my_scenes.py. El código fuente del renderizador interno es el mejor ejemplo: <SKILL_DIR>/assets/scenes.py.

# Firma real (seis parámetros, si falta uno, falla): `scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
    # scene    : diccionario de este segmento en la línea de tiempo (id / kind / scene_type / sub / big / items / stats ...)
    # t_local  : tiempo dentro del segmento (segundos, 0..dur) — las animaciones de entrada/duración se basan en esto
    # dur      : duración total de este segmento (segundos)
    # t_global : tiempo total del video (segundos) — animaciones continuas entre segmentos (paneo lento, respiración) lo usan
    # script   : diccionario completo del guion/plan (plan, color del tema, words, etc.; tiene caché, no lo modifiques en el lugar)
    # frame    : número de cuadro absoluto (entero) — úsalo para ruido determinista/fase, no uses time()
    return img   # PIL.Image, debe ser 1920x1080 RGB (lo más seguro es empezar con K.new_frame())

Al escribir un scenes.py personalizado, primero copia un renderizador (como _draw_hero) de <SKILL_DIR>/assets/scenes.py, que ya ha resuelto los problemas de funciones puras, maquetación en caché y plantillas pre-horneadas.

Por qué primero la propuesta: no hay "estilos internos" para elegir, la apariencia la determina design.json. Dos ejecuciones con el mismo brief producen diseños diferentes (la paleta de colores, el motivo, las transiciones, la música, la voz pueden variar), por lo que el flujo debe ser "generar opciones → persona confirma → congelar → producir video"; si renderizas sin confirmar, render.py mostrará una advertencia de propuesta (con --require-proposal, falla directamente). El contrato de diseño está en references/proposal.md.

Ejecución por etapas (cada etapa hace solo su trabajo, se puede re-ejecutar de forma independiente después de guardar):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style   # Analiza el design confirmado, imprime tema/lenguaje visual/rotación de maquetación/estilo de subtítulos
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts      # Solo sintetiza la narración + duración real (caché con validación de entrada)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout   # Solo organiza la línea de tiempo + elige la composición, genera timeline.json / sub.srt / storyboard.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues     # Solo genera los puntos de señal -> work/cues.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score    # Solo genera la música -> work/music.wav (musicgen compone sobre la marcha)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx      # Solo genera la pista de efectos de sonido (sfxkit, según cues.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio    # Solo mezcla (narración + música + efectos, prioridad a la narración)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames   # Solo renderiza los cuadros (--engine pil|web, --jobs N paralelismo por fragmentos)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux      # Solo sintetiza (copia de flujo de video, dos pasadas loudnorm)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa       # Solo control de calidad (solo lectura de entrada, solo escribe work/qa/)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills   # Solo renderiza fotogramas maestro + hoja de contactos
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills   # Firma manual de una compuerta (escribe work/signoff.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain  # Solo informa "qué etapas están desactualizadas, por qué, qué ejecutar", no ejecuta ninguna etapa

El orden completo de --stage all es: style → tts → layout → cues → score → sfx → audio → frames → mux → qa. Si algo falla en cualquier paso, se detiene ahí, no continúa con entradas incorrectas.

Calcula el costo antes de renderizar (opcional; estimate.py es una herramienta del repositorio, se invoca según <REPO>):

python <REPO>/tools/estimate.py --script film.json --jobs 4

Estima el número de cuadros, la duración de cada etapa, el disco y la sugerencia de fragmentación (con --json se puede leer por máquina). Es un límite superior del intervalo, no una promesa; si existe work/timeline.json, usa la duración real; de lo contrario, estima por número de caracteres. Para omitir el VMAF de todo el video, escribe "qa": {"vmaf": {"mode": "off"}} en film.json (tanto estimate como render leen este campo, G11 se marca como skipped).

Revisa el comprobante de entrega antes de firmar:

cat out/QC-REPORT.md     # La matriz de compuertas se divide en pass / fail / skipped; skipped significa que no se ejecutó esta vez, no equivale a aprobado
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release

La firma está vinculada a la huella del contenido: si cambias el guion / scenes.py / el diseño, la firma anterior se invalida automáticamente, debes revisar y volver a firmar. Para forzar la entrega (skipped también se considera fallo): añade --strict-gates.

Renderizado de videos largos (paralelismo por fragmentos + reanudación desde puntos de interrupción):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4 --resume

Los fragmentos se guardan en work/segments/, nombrados como seg_<cuadro_inicio>_<cuadro_fin>.mp4; antes de unirlos, se detectan explícitamente los huecos, si falta algún fragmento, se produce un error directamente, no se permite "producir el video aunque falte un segmento". El chunk por defecto se calcula como total_frames / jobs / 4 redondeado al segundo entero (jobs=4, 900 cuadros → aproximadamente 8 segundos por fragmento). Se puede sobrescribir con chunk_frames en film.json.

Después de cambiar el diálogo / scenes.py / el diseño / los parámetros de codificación, --resume rechazará reutilizar los fragmentos antiguos (el plan se almacena en work/segments/_plan.json con la huella del plan), y te sugerirá rm -rf work/segments — esto es intencionado: si se unen fragmentos antiguos con una nueva línea de tiempo, se producirá un video "con imagen y narración desincronizadas", y la compuerta de duración no lo detectará.

2. Ocho etapas estándar (P0–P7)

EtapaResultadoAcción claveError común
P0.5 Definir diseñowork/design.json + work/style.jsonpropose.py genera opciones → --confirm congela el diseño; --stage style solo analiza el diseño confirmado e imprime tema/lenguaje visual/rotación de maquetación/subtítulosSaltar → render.py falla (sin design no hay apariencia); renderizar sin confirmar genera advertencia de propuesta
P0 Definir referenciaCONVENTIONS.mdResolución/fps/duración/BPM → conversión de cuadros calculada de una vez, todo el proyecto la referenciaCada archivo escribe números mágicos como 150 → los límites de escena no coinciden
P1 Escribir guionfilm.jsonCada frase se divide en text (lectura) y sub (subtítulo); cada segmento tiene un kind; opcional lines[].type para fijar la composiciónUn campo usado para dos propósitos → subtítulos demasiado largos o lectura poco natural; no escribas style (ya no afecta a la imagen, la apariencia la determina work/design.json)
P2 Verificación de hechosFACTS.mdCada número/año en pantalla tiene un número de fuente; crear una lista negra de "no se puede mostrar"Saltar este paso → los números en imagen y narración no coinciden, el primer comentario te señala el error
P3 TTSwork/tts/*.mp3 + tts.jsonedge-tts sintetiza frase por frase, ffmpeg -f f32le mide la duración real, caché en discoEstimar duración por número de caracteres → subtítulos desviados
P4 Organizar línea de tiempowork/timeline.json + sub.srtdur = lead + voz real + tail, la frase debe estar dentro de este segmentoSegmento demasiado corto → la narración se corta en el siguiente segmento
P5 Mezcla de audiowork/audio.wavBus numpy: narración + música + efectos se superponen a nivel de muestraMezcla de múltiples entradas con ffmpeg → silencio no determinista
P6 Maestro3 fotogramasRenderiza 3 fotogramas para ver composición/paleta/grosor de fuente, luego renderiza todo el videoRenderizar todo el video directamente → 36000 cuadros después, descubres que la fuente es fea
P7 Producto finalout/film.mp4rawvideo→libx264 CRF18 + dos pasadas loudnorm a -16 LUFSTasa de bits demasiado baja/sonoridad -25 LUFS → "parece un borrador"

Nota: El valor por defecto de filmkit / render.py es crf=18; assets/film.json es solo un test fixture (para pruebas de regresión de cadena completa y fotogramas de referencia/compuertas, no es una plantilla para copiar), y usa 17. Al escribir tu propio guion, si no escribes crf, será 18; si buscas texto más nítido, puedes usar 17 (volumen aproximadamente +30%).

3. Cómo elegir el motor de renderizado

EscenarioElecciónVelocidad realAdecuado para
Gráficos/datos/divulgación/estilo UIPIL (numpy+cv2 como apoyo) — predeterminado de filmkit40–80 fps @1080p30 (ejemplo: 75)La mayoría de los videos de divulgación científica en chino, informes convertidos a video
Animaciones vectoriales/partículas/3D/textura de papel suavesNavegador sin cabeza Canvas2D/WebGL + captura de pantalla CDP → image2pipe (webengine.py)80–130 ms/cuadro (gráficos integrados, captura PNG)Videos largos de nivel documental, que requieren un lenguaje visual complejo
Animaciones de datos purasmanim (si está instalado)Depende de la escenaMatemáticas/demostración de fórmulas

Las velocidades de la tabla anterior son mediciones reales en una sola máquina sin carga (cambian con otra máquina o imágenes más pesadas; consulte §12). No tomes los intervalos como garantía.

El diseño solo cubre el motor PIL (engine: pil, predeterminado): la paleta de colores/fuente/maquetación/motivo de work/design.json son leídos por assets/scenes.py. En --engine web, la página la escribes tú mismo (assets/web/ es solo un ejemplo que funciona, no una plantilla de producción de video), la imagen no sigue el diseño; pero la música y la narración siguen design.json (no dependen del motor de renderizado).

Por defecto, se usa PIL: menos dependencias (solo numpy+Pillow), inicio rápido, determinismo fuerte, bajo costo de depuración. Solo cuando se necesita "deformación de partículas en un solo plano / textura de sombreador en tiempo real" se usa la opción del navegador:

cp -r <SKILL_DIR>/assets/web ./web                     # Plantilla de página (Canvas2D nativo, cero bibliotecas de terceros)
pip install playwright && python -m playwright install chromium   # Solo necesario si se usa el motor web
python render.py ... --engine web --stage cues        # Exportar puntos de señal desde la página
python render.py ... --engine web --stage frames --jobs 3 --resume
python webengine.py textqa ./web/index.html            # Superposición de texto/fuera de la imagen/aparición y desaparición rápida/nunca apareció

Compensaciones de las dos rutas (piensa bien antes de elegir, cambiar de motor requiere rehacer las imágenes):

PIL (engine: pil)Navegador (engine: web)
Dependenciasnumpy + Pillow (+cv2 para comparación de nitidez)+ playwright + Chromium/Edge
Velocidad40–80 cuadros/segundo8–12 cuadros/segundo (captura de cuadro + codificación, un orden de magnitud más lento)
Lenguaje visualComposición de mapas de bits, degradados, geometría, textoCanvas2D/WebGL: partículas, deformación, sombreadores, animación vectorial
DeterminismoNatural (dibujo con funciones puras)Garantizado mediante "seek preciso de la línea de tiempo a f/FPS" (window.renderFrame)
DepuraciónCambia un cuadro, ejecuta un cuadroEn el navegador index.html?dev=1&t=segundo para ver directamente, fácil de depurar errores
Adecuado paraGráficos/datos/divulgación/promocionalesVideos largos de nivel documental, que requieren un lenguaje visual continuo

Tres restricciones estrictas del motor del navegador (webengine.py ya las implementa, pero si escribes tu propia página, también debes cumplirlas):

  1. Todos los interpoladores deben colocarse en una línea de tiempo principal pausada; renderizar el cuadro f = hacer seek a f / FPS segundos; no debe depender de la reproducción.
  2. El contenido procedimental como bucles/partículas/ondas debe usar un actualizador que "calcule directamente por tiempo" (update(fn)), no usar setInterval para acumular.
  3. La página debe exponer window.ready / DURATION / renderFrame(f) / getCues(); no debe conectarse a CDN ni solicitar red.

Para lograr reproducibilidad a nivel de byte entre sesiones del navegador (fallo y reinicio, múltiples renderizados para diff, control de calidad para comparar dos imágenes), activa el modo determinista: --deterministic o variable de entorno WEBENGINE_DETERMINISTIC=1 (desactiva la rasterización por GPU, medidas 8/8 sesiones consistentes, costo aproximadamente +22%/cuadro). Por defecto no está activado: dentro de la misma sesión es consistente por defecto, entre sesiones solo hay diferencias de ≤4 píxeles, ±1 nivel de antialiasing.

4. Lista de verificación de calidad de imagen (estos elementos determinan si "parece profesional")

  • Brillo y profundidad: fondo oscuro (#0a1420) + acento brillante en la parte superior; no uses negro puro ni blanco puro, la pantalla del usuario eliminará los degradados.
  • Grosor y tamaño de fuente: texto grande 84–96px, subtítulos 34–38px, subtítulos 46px; para chino, usa msyhbd.ttc (Microsoft YaHei Bold).
  • Subtítulos: ≤ 2 líneas, aproximadamente 24 caracteres chinos por línea, con fondo semitransparente, y ≥ 872. El salto de línea real se basa en el ancho en píxeles (fit_sub_lines se ajusta automáticamente con max_w=1560 px), 24 caracteres es un "límite superior aproximado", no una verificación estricta — la constante SUB_MAX_CHARS=24 no es referenciada por ningún código actualmente, no confíes en que te detenga.
  • Ritmo: cada 3–6 segundos debe haber un cambio visual (nueva tarjeta / desplazamiento / desplazamiento de números); más de 6 segundos sin cambios, el espectador se va.
  • Cámara: solo paneos lentos del 3–15%, no alejamientos/acercamientos grandes.
  • Paleta de colores: un color principal + un color de acento para todo el video, no más de 4 matices.
  • Audio: maestro -16 LUFS / pico real ≤ -1 dBTP; la narración debe ser 12 dB más alta que los silencios; dejar 1.4 dB de margen antes de AAC.

5. Sonido: música + efectos de sonido + puntos de señal

La idea de esta sección proviene de la revisión de un video real (ai-history-film): el sonido no es malo porque "no suena lo suficientemente fuerte", sino porque "el sonido no conoce la imagen". El orden de resolución siempre es: primero, que el sonido sepa lo que está pasando en la imagen (puntos de señal), luego el timbre, y por último el master.

5.1 Tres capas de sonido

CapaQuién lo haceSe guarda enDescripción
Narraciónfilmkit.tts (edge-tts)work/tts/*.mp3La única autoridad de duración: la línea de tiempo se deduce de ella
Músicamusicgen.py (composición parametrizada)work/music.wavSe genera sobre la marcha según bpm/key/scale/mood/seed del diseño + parámetros de composición; no hay estilos predefinidos para elegir
Efectos de sonidosfxkit.py (modelado físico)work/sfx.wavSe sintetiza según los puntos de señal registrados en cues.json

Las tres pistas se mezclan en --stage audio: cuando suena un efecto, la música cede automáticamente (duck_db por defecto 4 dB, cinema/epic 5.5 dB — con una cesión de 3 dB, la música casi iguala al efecto durante los pasajes de transición, y la audición se vuelve confusa), finalmente se pasa a ffmpeg con dos pasadas loudnorm a -16 LUFS / pico real ≤ -1 dBTP. No hagas la normalización final del volumen tú mismo en numpy — tienes loudnorm, que es mucho más preciso que la normalización manual, y el pico real es la compuerta.

Separación dentro del video: la música ya tiene transiciones en los límites de los segmentos (el compositor escribe swell / riser / sub_boom), la pista sfx no repite whoosh/riser en el mismo límite — --stage sfx aplica automáticamente -3 dB a los cues de transición que caen dentro de [límite - 2.2 s, límite] (`sfx.duck_transitions...

…

[truncated for translation]

Skills relacionados