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)
- 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.
- Cada cuadro es una función pura:
draw(t, frame) -> Image. Prohibidorandom/time(), usafilmkit.hash2/hash3/vnoisepara aleatoriedad. El mismo número de cuadro debe generar siempre la misma imagen. - Los subtítulos y el texto grande deben estar en la zona segura: texto principal
y ∈ [58, 974], subtítulos con fondoy ∈ [872, 1030], nunca superar 1030. - Los cuadros originales se codifican directamente: cuadro PIL → tubería
rawvideo→libx264. No usescv2.VideoWriterpara archivos intermedios (2 veces más lento y con pérdida secundaria). - 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).
- 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". - El sonido sigue a la imagen: los puntos de señal (como
K.CueBus/ el hookcues()descenes.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. - Si no ves algo claro, añade pausa, no ralentices todo el video.
hold(global) enfilm.jsonolines[].hold(por frase) añade pausa después de la voz; la música usabpmpara 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 usuario | Lo que debes hacer |
|---|---|
| "Esa" / elige una | python 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:
- Antes de
propose.py --confirm,render.pyen stageframes/allserá bloqueado por la compuerta de propuesta (advertencia por defecto,--require-proposalfalla). - 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 < 2se marcará. - 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". - No hay plantillas internas para elegir: todos los campos de
design.jsonestá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). - Registro de unicidad entre videos (activado por defecto):
--confirmregistra 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-ledgerpara consultar el registro,--no-ledgerpara ignorarlo en esta ejecución, la variable de entornoVIDEO_STUDIO_LEDGERcambia la ruta o se establece enoffpara 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 directorioswork/yout/generados durante la ejecución).
<SKILL_DIR>se refiere alresourceBase.pathque la herramientaskillinforma al cargar esta habilidad. Una vez instalado como plugin, será el directorioskills/video-studio/dentro del paquete (elnode_modules/@dsh-gis/dsh-video-studio-skilldel 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, onode_modules/@dsh-gis/dsh-video-studio-skill/en el perfil después de instalar), es decir, dos niveles por encima de<SKILL_DIR>.tools/yschemas/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.pypersonalizado, 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 comoseg_<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 comototal_frames / jobs / 4redondeado al segundo entero (jobs=4, 900 cuadros → aproximadamente 8 segundos por fragmento). Se puede sobrescribir conchunk_framesenfilm.json.Después de cambiar el diálogo / scenes.py / el diseño / los parámetros de codificación,
--resumerechazará reutilizar los fragmentos antiguos (el plan se almacena enwork/segments/_plan.jsoncon 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)
| Etapa | Resultado | Acción clave | Error común |
|---|---|---|---|
| P0.5 Definir diseño | work/design.json + work/style.json | propose.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ítulos | Saltar → render.py falla (sin design no hay apariencia); renderizar sin confirmar genera advertencia de propuesta |
| P0 Definir referencia | CONVENTIONS.md | Resolución/fps/duración/BPM → conversión de cuadros calculada de una vez, todo el proyecto la referencia | Cada archivo escribe números mágicos como 150 → los límites de escena no coinciden |
| P1 Escribir guion | film.json | Cada frase se divide en text (lectura) y sub (subtítulo); cada segmento tiene un kind; opcional lines[].type para fijar la composición | Un 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 hechos | FACTS.md | Cada 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 TTS | work/tts/*.mp3 + tts.json | edge-tts sintetiza frase por frase, ffmpeg -f f32le mide la duración real, caché en disco | Estimar duración por número de caracteres → subtítulos desviados |
| P4 Organizar línea de tiempo | work/timeline.json + sub.srt | dur = lead + voz real + tail, la frase debe estar dentro de este segmento | Segmento demasiado corto → la narración se corta en el siguiente segmento |
| P5 Mezcla de audio | work/audio.wav | Bus numpy: narración + música + efectos se superponen a nivel de muestra | Mezcla de múltiples entradas con ffmpeg → silencio no determinista |
| P6 Maestro | 3 fotogramas | Renderiza 3 fotogramas para ver composición/paleta/grosor de fuente, luego renderiza todo el video | Renderizar todo el video directamente → 36000 cuadros después, descubres que la fuente es fea |
| P7 Producto final | out/film.mp4 | rawvideo→libx264 CRF18 + dos pasadas loudnorm a -16 LUFS | Tasa de bits demasiado baja/sonoridad -25 LUFS → "parece un borrador" |
Nota: El valor por defecto de
filmkit/render.pyescrf=18;assets/film.jsones 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 escribescrf, será 18; si buscas texto más nítido, puedes usar 17 (volumen aproximadamente +30%).
3. Cómo elegir el motor de renderizado
| Escenario | Elección | Velocidad real | Adecuado para |
|---|---|---|---|
| Gráficos/datos/divulgación/estilo UI | PIL (numpy+cv2 como apoyo) — predeterminado de filmkit | 40–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 suaves | Navegador 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 puras | manim (si está instalado) | Depende de la escena | Matemá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 dework/design.jsonson leídos porassets/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 siguendesign.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) | |
|---|---|---|
| Dependencias | numpy + Pillow (+cv2 para comparación de nitidez) | + playwright + Chromium/Edge |
| Velocidad | 40–80 cuadros/segundo | 8–12 cuadros/segundo (captura de cuadro + codificación, un orden de magnitud más lento) |
| Lenguaje visual | Composición de mapas de bits, degradados, geometría, texto | Canvas2D/WebGL: partículas, deformación, sombreadores, animación vectorial |
| Determinismo | Natural (dibujo con funciones puras) | Garantizado mediante "seek preciso de la línea de tiempo a f/FPS" (window.renderFrame) |
| Depuración | Cambia un cuadro, ejecuta un cuadro | En el navegador index.html?dev=1&t=segundo para ver directamente, fácil de depurar errores |
| Adecuado para | Gráficos/datos/divulgación/promocionales | Videos 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):
- Todos los interpoladores deben colocarse en una línea de tiempo principal pausada; renderizar el cuadro f = hacer seek a
f / FPSsegundos; no debe depender de la reproducción. - El contenido procedimental como bucles/partículas/ondas debe usar un actualizador que "calcule directamente por tiempo" (
update(fn)), no usarsetIntervalpara acumular. - 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_linesse ajusta automáticamente conmax_w=1560 px), 24 caracteres es un "límite superior aproximado", no una verificación estricta — la constanteSUB_MAX_CHARS=24no 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
| Capa | Quién lo hace | Se guarda en | Descripción |
|---|---|---|---|
| Narración | filmkit.tts (edge-tts) | work/tts/*.mp3 | La única autoridad de duración: la línea de tiempo se deduce de ella |
| Música | musicgen.py (composición parametrizada) | work/music.wav | Se 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 sonido | sfxkit.py (modelado físico) | work/sfx.wav | Se 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 sfxaplica 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]