Video narrativo block-toon (Estilo "Busted by the Shoeshine Man")
Usa esto cuando el usuario quiera un video narrativo vertical (720x1280 o 1080x1920, 30 fps) en primera persona, de 30 a 130 segundos, contado sobre una voz en off, con personajes 3D de bloques redondeados, movimiento continuo suave y subtítulos punzantes en color. Todo es código: escena three.js en Chromium headless, una función frame(t) determinista, capturas de pantalla enviadas a ffmpeg.
La prioridad del usuario es la fidelidad del diseño de personajes y la suavidad de la animación. No omitas las comprobaciones de personajes en el paso 5.
1. Aspecto del video de referencia (medido)
- 720x1280, 30 fps, ~122 s, AAC estéreo aproximadamente -14 LUFS con un fondo continuo y suave debajo de la voz.
- ~47 cortes duros (cortes de un solo fotograma, sin encadenados), duración media del plano ~2.6 s (rango 1.2-5 s). Dentro de un plano, la cámara SIEMPRE se mueve: acercamiento suave, alejamiento, deriva en grúa/ángulo alto, acercamiento en contrapicado, giro inclinado hasta un techo o ventana. Menos del 1 % de los fotogramas son estáticos.
- Subtítulos en el tercio inferior (parte superior aproximadamente al 64-68 % de la altura), 2-3 palabras por grupo en minúsculas, sans-serif geométrica bold redondeada (Poppins ExtraBold), base blanca con palabras clave coloreadas (oro #f5c32c, cian #48c9f0, coral #ff5b4d, rosa #ff7ac0, verde #2ecc8f, naranja #ffa21f), trazo negro grueso, pequeña escala de aparición en cada grupo.
- Entorno: interior cálido crema/tostado (pared #dcbf96, suelo #c7aa7d, piedra #d9c39b, zócalo #b99a6f, cielo #c7dcec) con acentos granate #570d0e, azul marino #1c2a49, oro #e0990d, verde azulado #1f8f8c, rojo #a3160f.
- Iluminación: suave, brillante, ligeramente cálida; sombreado degradado suave y sombras proyectadas suaves. Sin bandas de cel duras.
2. Biblia de diseño de personajes (sigue exactamente)
Silueta y proporciones
- Cada parte del cuerpo es una caja redondeada. El radio de bisel es aproximadamente el 15-18 % de la dimensión más pequeña de la parte. Nunca uses esferas o cápsulas para las partes del cuerpo.
- Cabeza: un cuboide ligeramente más ancho que alto (0.44 x 0.41 x 0.39 unidades), aproximadamente 1/4-1/3 de la altura de la figura en planos cercanos, de modo que la figura mida ~4 cabezas de altura en planos generales. Bloque de cuello corto. Torso 0.46 x 0.56 x 0.26. Brazos 0.27 + 0.26 (superior + antebrazo). Piernas 0.30 + 0.27. Altura total aproximadamente 1.7 unidades.
- Las manos son bloques de manopla rechonchos: un bloque de palma con cuatro dedos cortos de dos segmentos y un pulgar separado. Pueden curvarse de abiertas a puño y separarse. Las manos nunca deben verse como arañas.
- Banda blanca de puño en cada muñeca. Zapatos negros, un bloque plano con punta redondeada.
- Sin roturas de articulaciones visibles. Donde dos cajas redondeadas se encuentran en una articulación doblada (cadera, rodilla, hombro, codo, muñeca, cuello/torso, cadera/torso) los biseles abren un hueco o muesca que se lee como una rotura. Oculta cada articulación bajo una capa de tela del mismo color que la ropa: una esfera pequeña (
ball()) centrada en el pivote de la articulación, del tamaño de la mitad del ancho de la extremidad + ~0.002 y estirada a lo largo de la extremidad (sc[1,1.2-1.4,1]); un bloque de pelvis que une las dos caderas; un bloque de cuello sobre la costura cuello/torso; una pieza de muñeca de color piel debajo del puño. No sobredimensiones las bolas (la mitad del ancho de la extremidad + 0.015 produjo rodillas y codos grumosos). Verifica con una prueba de esfuerzo de poses dobladas (brazo levantado, caminata a medio paso, pierna levantada), con zoom.
Cara (plana pintada en la parte frontal de la cabeza, redibujada cada fotograma en una textura canvas)
- Ojos grandes y redondos: esclerótica blanca, iris verde o marrón, pupila negra, destello blanco. Línea negra gruesa del párpado superior. Los ojos pueden mirar alrededor (lookX/lookY), parpadear y entrecerrarse.
- Cejas gruesas y cónicas que se inclinan (enojado/preocupado) y se elevan. Las gafas de anillo negro son mucho más grandes que los ojos (radio del anillo aproximadamente 2x el radio del ojo). Las gafas deben ser un marco conectado: los dos anillos están unidos por un arco de puente curvo sobre la nariz y cada anillo tiene una línea de patilla que llega hasta el borde de la cara. Dos anillos flotantes se ven rotos (un usuario señaló esto en la primera hoja de reparto).
- Sin nariz. La boca es una línea/curva diminuta, o una boca abierta rellena con lengua rosa al hablar. Valor de sonrisa -1..1.
- Rubor suave opcional.
Cabello, sombreros, accesorios (bloques apilados rechonchos)
- Héroe: tupé marrón con mecha puntiaguda, piezas laterales y un bloque trasero. Otros: corto, o largo (cortinas laterales + bloque trasero). El flequillo negro es un bloque delgado angulado.
- Gorra de policía: bloque de corona azul marino + visera negra + placa dorada. Gorra plana: bloque marrón + visera corta + botón claro.
- Hombre mayor: bigote de bloque blanco + parche blanco en la sien.
- Los atuendos son de bloques de color y simples: traje (chaqueta granate, camisa blanca, corbata oscura, solapas, cinta dorada en V + placa de identificación blanca con franja azul), uniforme (azul marino, placa dorada, cinturón negro + hebilla dorada), delantal (camisa blanca, delantal marrón con botones), más variantes gris/verde/tostado/azul marino/verde azulado para extras.
- Tonos de piel: melocotón #efaa71, tostado #d89a62, marrón #8a5230, oscuro #5a3320, claro #f4c49a. Cabello marrón #59321a, negro #14110f.
Renderizado
- Contorno fino oscuro con tinta (#1c0f06) en cada pieza mediante un shader de casco invertido con ancho constante en pantalla. El ancho es una fracción de la distancia de la cámara:
w = px / ((H/2) / tan(fov/2))dondepxes el grosor de línea deseado en píxeles de pantalla (aproximadamente 0.003 para planos de video 720x1280, aproximadamente 1.5-2 px). Para hojas de personajes lejanas a 1600x900 usaK.setOutlineWidth(.0009), para tiles de busto aproximadamente .0016; el valor predeterminado es demasiado grueso en una cámara distante. Las caras y las piezas de adorno pequeñas omiten el contorno. - Material Lambert, luz hemisferio (aproximadamente 2.9) + un sol de sombra suave (aproximadamente 1.5). Sombras PCF suaves.
- Multitud de fondo usa
lod: 1(sin dedos, menos bloques de cabello). Nunca construyas un diseño único por extra; varía piel, cabello, color de atuendo y peinado.
Regla de variedad: cada personaje nombrado debe ser reconocible solo por su silueta (sombrero, forma del bloque de cabello, gafas, bigote, tono de piel, color de atuendo). Usa las mismas proporciones para todos.
Hojas de referencia (hazlas cuando el usuario pida un diseño de personaje o un entorno, antes de cualquier video)
Construye un designsheet.html y un envsheet.html junto a scene.html (mismo kit, mismo render.cjs, ejecuta node render.cjs designsheet.html out 0,1,2 --w 1600 --h 900). frame(t) elige un modo con Math.round(t); las etiquetas son una superposición DOM (Poppins) posicionada proyectando puntos 3D con new THREE.Vector3(x,y,z).project(camera); el título de la página y las muestras también son DOM.
- Archivo de reparto
cast.js: una tablaDEF(nombre, rol, 4 muestras [piel, cabello, atuendo, acento]), una tablaBUILDde un constructor por personaje (makeChar+ algunosbox()accesorios extra),makeOne(name),makeCast(), ypose(name, C)que restablece la cara y la pose cada fotograma y aplica una pose limpia de hoja de modelo. Haz que cada personaje sea único por silueta: diferentescale/headScale, peinado, sombrero, gafas, bloque de bigote, tono de piel y un color de acento. - Hoja 0 alineación: todos los personajes en una fila (cámara aproximadamente [0,1.02,15], fov 16.5), nombre + rol + cuadrados de muestra debajo de cada uno. Hoja 1 giro del protagonista en yaw 0, .75, PI/2, PI (frente, 3/4, perfil, espalda). Hoja 2 expresiones: seis bustos (neutral, feliz, enojado, sorprendido, preocupado, presuntuoso) dibujados como tiles de tijera, porque apilar seis personajes en una escena 3D coloca filas debajo del suelo y superpuestas. Patrón de tile:
renderer.autoClear=false; renderer.setScissorTest(true);luego por tilesetViewport(x, H-y-h, w, h); setScissor(...); renderer.clear(); camera.aspect = w/h; camera.updateProjectionMatrix(); renderer.render(scene, camera);y restablece el viewport, la prueba de tijera y autoClear al final deframe. - Convención de cejas:
face.brow = [height, tilt, arch], tilt > 0 = extremos interiores hacia abajo (enojado), tilt < 0 = extremos interiores hacia arriba (preocupado). EndrawFaceeso esang = -s * f.brow[1] * .55. (La primera versión lo tenía invertido y un oficial enojado parecía triste.) - Hoja de entorno 0: cuatro tiles de tijera de 760x318 (héroe general desde la entrada, pared del fondo con la ventana arqueada, la esquina del accesorio clave, una grúa en picado con
hall.ceiling.visible = false), cada uno subtitulado; coloca el reparto en el decorado para escala y haz que miren hacia la cámara o entre sí (comprueba el yaw: yaw 0 mira hacia +z hacia una cámara en +z; un personaje de espaldas a la cámara es un error de puesta en escena). Hoja 1: cada accesorio solo en un escenario de accesorios lejano (x = 400 + i*20) para que nunca aparezca en planos del pasillo, tiles 486x300. Hoja 2: plano del suelo visto desde arriba conup = [-1,0,0](el eje largo corre horizontalmente), cámara aproximadamente [-1,120,-13] mirando directamente hacia abajo, fov estrecho (aproximadamente 9.3) y etiquetas de chincheta proyectadas desde puntos del mundo. Incluye siempre una tira de paleta (10 muestras) en el encabezado. - El techo y las vigas del pasillo viven en un grupo expuesto como
H.ceiling(con sombreado desactivado, para que el sol aún ilumine el pasillo); ocúltalo para tomas de grúa y vistas desde arriba. El texto de los letreros debe tener el tamaño de su plano: un letrero de 1.9 x 0.5 toma800 56px Poppins, no 78px (recortado). - Trampa de Three.js:
Object3D.positiony.rotationson propiedades de solo lectura; establece componentes (m.rotation.x = ...,m.position.set(...)) y nuncaObject.assign(mesh, { rotation: ... }), que lanza una excepción y detiene__ready. - Entrega los PNG más los archivos
.jscon SendUserFile; vuelve a renderizar y reenvía cada hoja después de cualquier cambio en el kit para que el usuario nunca tenga hojas desactualizadas.
3. Reglas de suavidad de animación
- La animación es una función pura del tiempo:
frame(t)establece todo a partir det. Sin estado entre fotogramas, por lo que cualquier fotograma se puede renderizar en cualquier orden o en paralelo. - Usa
kf([[t0,v0],[t1,v1]], t, easeFn)para cada movimiento. UsaeaseOutpara acercamientos y llegadas,easeIOpara movimientos de grúa/látigo,easepara cosas pequeñas. Nunca lineal para una cámara o extremidad. - La cámara nunca está estática. Cada plano tiene posición + punto de mira + fov + unos pocos grados de balanceo, más una deriva de mano siempre activa
[sin(.5t)*.03, sin(.37t)*.02]. - Solo cortes duros en los límites del plano, en una palabra o tiempo. Sin encadenados. Duración media del plano 2-3 s.
- Capas de inactividad en cada personaje, todo el tiempo: balanceo de respiración, vaivén, balanceo de cabeza, parpadeos cada ~3-4 s (0.13 s suavizado), pequeños movimientos oculares rápidos.
K.idle()hace esto. - La apertura de la boca proviene del volumen real de la voz en off (
lipsync.pyescribe env.json a 30 fps). Nunca un aleteo en bucle. - Gestos: anticipación (pequeño tirón hacia atrás antes de un señalamiento/golpe), sobreimpulso (
easeBack), pausa, asentamiento. Las cabezas lideran, el torso sigue (usaheadYawantes detwist). - Ciclo de caminata: piernas opuestas, cada brazo se balancea opuesto a la pierna del mismo lado, las rodillas solo se doblan hacia atrás (
Math.max(0, knee)), balanceo 2x por ciclo, pequeña torsión e inclinación del torso. La fase está en ciclos: usa aproximadamente 1 ciclo/s para una caminata normal. - Los personajes nunca deben superponerse a los muebles: colócalos con x/z explícitos y comprueba un fotograma fijo.
4. Pipeline
- Guión: primera persona, gancho en los primeros 3 segundos, oraciones cortas contundentes, escalada, giro, remate de la última línea. Aproximadamente 2.5 palabras/s.
- Voz en off: el usuario suele proporcionar un archivo de ElevenLabs. Si no, pídelo o sintetiza una pista de marcador de posición. Guárdalo como
voice.wav. - Tiempos de palabras: obtén marcas de tiempo a nivel de palabra (alineación forzada contra el texto del guión, por ejemplo, marcas de tiempo de palabra de pocketsphinx o whisper). Guarda
words.jsoncomo[{"w":"mi","s":0.20,"e":0.41}, ...], usando la ortografía y puntuación del guión (los subtítulos usan puntuación final para romper grupos). - Sobre de sincronización labial:
python3 lipsync.py voice.wav env.json. - Lista de planos: una línea por plano: hora de inicio/fin, lo que ve el espectador, movimiento de cámara, quién actúa. Apunta a un corte cada ~2.6 s. Varía el encuadre: primer plano de cara, plano medio de dos personas, acercamiento en contrapicado, grúa en picado sobre la multitud, giro inclinado, por encima del hombro, inserto de un accesorio (teléfono, placa). Escribe los planos en
SHOTSenscene.html. - Construye el reparto + decorado a partir de
blockkit.js/props.js(ver código). Reutilizaroom(),chair(),bench(),crowd(); construye nuevos accesorios conbox()en la paleta anterior. - Primero comprueba fotogramas fijos (barato):
node render.cjs scene.html outstills 1,4,7,...; mira primeros planos de cada personaje nombrado, una caminata, un gesto, un plano general. Corrige antes de renderizar el video. - Renderiza el video (aproximadamente 1-1.7 s/fotograma a 720x1280 en 2 núcleos, por lo que 40 s son aproximadamente 15-25 min; ejecuta fragmentos en paralelo y en segundo plano):
export NODE_PATH=$(npm root -g) # playwright es global en el sandbox; de lo contrario: npm i playwright && npx playwright install chromium
S=30; for i in 0 1; do (node render.cjs scene.html part$i.mp4 --from $((i*S)) --to $(((i+1)*S)) > r$i.log 2>&1 &); done # 2 fragmentos de 30 s; espera a ambos
printf "file 'part0.mp4'\nfile 'part1.mp4'\n" > list.txt && ffmpeg -y -f concat -safe 0 -i list.txt -c copy silent.mp4
- Audio + multiplexación (fondo continuo suave debajo de la voz, aproximadamente -14 LUFS):
D=130 # >= duración del video
ffmpeg -y -f lavfi -i "sine=f=55:d=$D" -f lavfi -i "sine=f=82.4:d=$D" -f lavfi -i "sine=f=110:d=$D" -filter_complex "[0][1][2]amix=inputs=3:normalize=0,lowpass=f=500,tremolo=f=0.12:d=0.35,aformat=channel_layouts=stereo,afade=t=in:d=1[bed]" -map "[bed]" -ar 44100 bed.wav
ffmpeg -y -i silent.mp4 -i voice.wav -i bed.wav -filter_complex "[2:a]volume=0.12[b];[1:a][b]amix=inputs=2:duration=first:normalize=0,loudnorm=I=-14:TP=-1.5:LRA=7,aformat=channel_layouts=stereo[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k final.mp4
- Control de calidad antes de entregar: (a) hoja de contacto, un fotograma por cada ~2.6 s, comprueba personajes/subtítulos; (b) comprobación de movimiento, diferencia de fotograma media en un flujo gris reducido, espera 0 fotogramas estáticos:
python3 - <<'E'
import subprocess, numpy as np
W,H=90,160
a=np.frombuffer(subprocess.run(['ffmpeg','-v','error','-i','final.mp4','-vf',f'scale={W}:{H},format=gray','-f','rawvideo','-'],capture_output=True).stdout,dtype=np.uint8).reshape(-1,H,W).astype(np.float32)
d=np.abs(np.diff(a,axis=0)).mean(axis=(1,2)); print(len(a),'frames; static:',(d<.02).sum(),'; spikes(cuts):',(d>14).sum())
E
(c) sonoridad ffmpeg -i final.mp4 -af ebur128 -f null - (aproximadamente -14 LUFS), (d) los subtítulos coinciden con el habla. Entrega con SendUserFile (mp4 + un fotograma fijo de portada).
5. Configuración
mkdir -p work && cd work && npm init -y >/dev/null && npm i [email protected] @fontsource/poppins
# crea los archivos a continuación en esta carpeta: blockkit.js props.js captions.js render.cjs lipsync.py scene.html
# (o copia los archivos ya preparados de la carpeta kit/ de este repositorio, que también tiene st
…
[truncado para traducción]