Video promocional motion (HTML → MP4)
Entrega: MP4 listo (H.264, yuv420p, AAC) enviado con SendUserFile. Responde en pt-BR, corto.
1. Briefing mínimo (no preguntes de más)
Define por defecto y solo pregunta lo que cambia el resultado:
- Formato: 9:16 (1080×1920) por defecto; 16:9 (1920×1080) si se pide. Duración: anuncio Insta 20–35 s; pacing LENTO (cada paso ~1,5–4 s) — el usuario ya se quejó de "parece 2x".
- Público: joven/SaaS → SFX brillantes (campanas, pops, clicks). Público más adulto/premium (relojes, joyas) → sobrio: pads, impactos suaves, whooshes, shimmers; NADA de pop infantil.
- Marca: paleta, fuente, logo, CTA (WhatsApp/URL), prints/fotos. Música: si el usuario menciona una pista (Spotify, etc.), NO accedes al audio — avísale, usa pista original y ofrécete a sincronizar si él envía el mp3 (y recuerda la licencia para anuncios).
2. Preparar assets
- Fotos de producto: quitar fondo con
rembg(modelo isnet-general-use) → PNG con alpha para efecto "3D" (flotar, rotateY/rotateX con perspective, brillosheenenmascarado por el alpha del PNG mediante-webkit-mask-image). - Capturas de baja resolución: upscale Lanczos 3× + UnsharpMask. Elimina textos de anuncio de la foto original cuando el usuario pida "sin anuncio".
- Fuentes: Inter / Inter Display ExtraBold (SaaS, estilo plano), Lora serif + cursiva dorada (premium), Poppins.
3. Estilo que el usuario aprobó
- Bloques de color planos, tarjetas grandes, tipografía gigante con revelado por palabra enmascarada, mucho movimiento continuo (parallax lento, zoom lento 1→1.15, estrellas/partículas), cortes con wipes de 3 paneles de colores sincronizados con el beat.
- SIN cámara temblorosa (el usuario rechazó shake): shake ×0.12 como máximo, sin rotación de cámara, drift ≤3 px.
- Estructura narrativa: gancho/problema → solución → 2–3 características/integración → prueba/beneficio → CTA. Cada escena 3–5 s, wipe entre ellas. En SaaS, mostrar el PROBLEMA de forma concreta (mensaje sin respuesta, venta perdida −$ARS) antes de la solución.
- Textos cortos, 2–4 palabras por línea, en pt-BR, con una palabra destacada (color/cursiva). Respetar zona segura de Instagram (evitar ~250 px superior y ~400 px inferior en 9:16 para CTA/leyenda).
4. Motor (HTML determinista)
Un archivo video.html con window.seek(T) que posiciona TODO en función del tiempo T (sin requestAnimationFrame/CSS animations). Núcleo:
const cl=(x,a=0,b=1)=>Math.min(b,Math.max(a,x)),lerp=(a,b,p)=>a+(b-a)*p;
const E={outBack:(p,c=1.8)=>1+(c+1)*Math.pow(p-1,3)+c*Math.pow(p-1,2),outExpo:p=>p>=1?1:1-Math.pow(2,-10*p),outQuart:p=>1-Math.pow(1-p,4),inOutQuart:p=>p<.5?8*p**4:1-Math.pow(-2*p+2,4)/2,inOutCubic:p=>p<.5?4*p**3:1-Math.pow(-2*p+2,3)/2,lin:p=>p,inCubic:p=>p**3,inOutSine:p=>-(Math.cos(Math.PI*p)-1)/2};
const ez=(T,a,d,f=E.outExpo)=>f(cl((T-a)/d));
function mk(parent,html,cls='',style=''){const d=document.createElement('div');d.className='abs '+cls;d.style.cssText=style;d.innerHTML=html;parent.appendChild(d);return d} // .abs{position:absolute;left:50%;top:50%}
function P(el,o){const x=o.x||0,y=o.y||0,s=o.s==null?1:o.s,op=o.o==null?1:o.o,r=o.r||0,rx=o.rx||0,ry=o.ry||0;el.style.transform=`perspective(2400px) translate(-50%,-50%) translate(${x}px,${y}px) rotateX(${rx}deg) rotateY(${ry}deg) rotate(${r}deg) scale(${s})`;el.style.opacity=op;el.style.visibility=op<=.003?'hidden':'visible'}
function words(el,arr){el.innerHTML=arr.map(w=>`<span class="mk"><span class="wd ${w[1]||''}">${w[0]}</span></span>`).join(' ')} // .mk{display:inline-block;overflow:hidden;padding:.14em .05em .24em;margin:-.14em -.05em -.24em}
function lineIO(el,T,tin,tout,step=.1,d=1.0){[...el.querySelectorAll('.wd')].forEach((w,i)=>{const a=ez(T,tin+i*step,d,E.outQuart),b=tout?ez(T,tout+i*step*.5,.5,E.inCubic):0;w.style.transform=`translateY(${(1-a)*118-b*118}%)`;w.style.opacity=cl(a*2.2,0,1)*(1-b)})}
function addScene(a,b,bg,build,upd,cam){/* crea div.sc con bg, inner; scenes.push({a,b,root,inner,obj:build(inner),upd,cam}) */}
function runScenes(T){/* mostrar solo si T en [a-.02,b+.02]; inner.style.transform=translate/scale de cam(t,T); s.upd(t,obj,T) */}
// wipes: 3 paneles [color oscuro, dorado/accent, color de fondo de la siguiente escena], cada uno ez(T,t0+i*.09,.38,E.inOutQuart) entrando y saliendo (t0=wipe-.55); alternar dirección x / xr
function seek(T){runScenes(T);drawWipes(T);drawFX(T);drawGrain(T);/* fundido a negro final */}
Capas fijas encima del mundo: canvas de polvo/partículas, wipes, grano de película (alpha .07), viñeta, velo negro para fundido entrada/salida.
Efecto 3D de producto: P(hero,{ry:Math.sin(T*.7)*8, rx:Math.sin(T*.5)*3, y:Math.sin(T*1.1)*14, s}) + sweep() (franja de brillo mix-blend-mode:soft-light enmascarada por el PNG) + glow radial dorado detrás + sombra drop-shadow.
Apertura tipo reloj (aprobada por el usuario en la v1 de los relojes): SVG 760 px con 60 marcas (cada 5 más gruesas/doradas), arco dorado que se dibuja (stroke-dashoffset), manecilla que avanza 6° cada 0,5 s con E.outBack, marcas ya recorridas se encienden; texto "El tiempo pasa para todos. / Pocos dejan huella."; estrellitas ✦ girando. Reutiliza este tipo de apertura temática cuando el producto lo permita.
Línea de tiempo con velocidad variable: SEG=[[ini,fin,factor],…] aplicado en seek Y en la función M(t) del audio para que los SFX sigan los cortes.
5. Render (determinista, retomable)
- Playwright Chromium viewport 1080×1920 (o 1920×1080),
page.evaluate('seek(t)')+screenshot(type='jpeg',quality=92). - 60 fps con motion blur: 2 sub-fotogramas por frame (t=(f+(s+.5)/2-.5)/60), promedio en numpy float32 → JPEG q95 →
ffmpeg -f image2pipe -c:v mjpeg -i - -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p seg/NNNNN.mp4. - Chunks de 120 fotogramas en
seg/, 2 procesos paralelos (chunks pares/impares), salta chunk existente (el contenedor puede reiniciarse). Ejecuta consetsid nohup ./go.sh &y usa Monitor con until-loop en un archivo done (sleep en primer plano está bloqueado; reactiva Monitor si expira en 30 min). ~0,38 s por screenshot, 2 CPUs: 24 s ≈ 1440 fotogramas ≈ 25–30 min. - ¿Solo cambió el principio? Elimina solo los chunks afectados y vuelve a renderizar; si solo cambió el audio, remuxea con
-c:v copy. - Concat:
ffmpeg -f concat -safe 0 -i list.txt -i audio.wav -c:v libx264 -crf 21 -pix_fmt yuv420p -c:a aac -b:a 192k -shortest -movflags +faststart out.mp4. - Antes de renderizar todo, toma 2–4 capturas de
seek()en tiempos clave y VERIFICA texto cortado/desbordado en ambos formatos (ya pasó con fuente demasiado grande), y errores JS (page.on('pageerror')).
6. Audio (numpy/scipy, sintetizado)
Helpers: lp/hp/bp (butter), add(buf,t,x,g), tt(d); sonidos: thump (seno 46–96 Hz decayendo), impact (thump+ruido lp 700), riser (ruido con banda subiendo), swish (ruido bp 1.8–5.5 kHz, ventana sin²), shimmer (parciales inarmónicos con decaimiento), tick (seno ~900 Hz, decay 90), pad (3 senos desafinados lp 1.4 kHz con fade), pluck; reverberación por convolución con ruido decayente.
- SaaS/joven: campanas brillantes, pops, clicks, whooshes — NO usar graves pesados (el usuario odió "todo grave").
- Premium/anciano: riser+impact en los wipes, swish en los revelados de texto, shimmer al entrar el producto, ticks en las listas, tick de reloj cada 0,5 s en la apertura de reloj, pads en acordes lentos (Dm–Bb–F–C), golpe suave ~90 bpm. Beat 80–90 bpm combina con cortes cada 4 s.
- MASTERIZACIÓN (evita "distorsionado"): NUNCA
tanhen una mezcla caliente. Hazx=x/np.abs(x).max()*.85, guarda wav y ejecutaffmpeg -af loudnorm=I=-16:TP=-2:LRA=11 -ar 44100. Verifica pico < −1 dBFS y factor de cresta > 10 dB.
7. Entrega e iteración
Envía el MP4 con una leyenda de 1 línea; menciona lo que cambió y lo que NO se verificó (ej.: "revisé fotogramas y métricas, no escuché"). Comentarios típicos y respuesta: "muy rápido" → escala la línea de tiempo ×1.5; "tembloroso" → reduce shake; "sonido malo" → cambia el perfil de SFX (brillante vs. sobrio); "distorsionado" → remasteriza solo el audio y remuxea; "me gustó la animación X de la v1" → recupera la escena X, renderiza solo los chunks afectados. Si el usuario pide un prompt/documentación reutilizable, genera un doc con este flujo.