morfeo 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/morfeo.mjs +31 -15
- package/index.d.ts +2 -2
- package/lib/config.mjs +1 -1
- package/lib/estructura.mjs +1 -1
- package/lib/render.mjs +118 -3
- package/package.json +1 -1
- package/plantilla/AGENTS.md +3 -2
- package/python/musica.py +12 -0
- package/remotion/kit/ambientes.tsx +225 -1
- package/remotion/kit/tres.tsx +2 -2
- package/remotion/motor/Video.tsx +1 -1
- package/remotion/motor/plantillas.tsx +22 -4
- package/remotion/motor/tema.ts +4 -1
package/bin/morfeo.mjs
CHANGED
|
@@ -21,6 +21,8 @@ Videos
|
|
|
21
21
|
render [id...] Genera los videos que faltan (o los que indiques)
|
|
22
22
|
--todos regenera todos
|
|
23
23
|
--solo-fallidos reintenta solo los que fallaron en el último lote
|
|
24
|
+
--cambiados solo los que cambiaron (guion o tus componentes) desde su último render
|
|
25
|
+
--revisar al terminar cada uno, saca su hoja y lo revisa (un solo paso en vez de tres)
|
|
24
26
|
--escenas 5-7 solo esas escenas (para corregir una toma sin rehacer todo) + su hoja
|
|
25
27
|
--gpu usa la tarjeta de video (más rápido; si falla, quítalo)
|
|
26
28
|
previa <id> Solo el gancho (~8 s) para aprobarlo antes del render completo: --hasta 6
|
|
@@ -119,10 +121,18 @@ Pruébalo: npx morfeo render ${pos[0]} y revísalo: npx morfeo hoja ${pos[0]
|
|
|
119
121
|
const {render} = await import('../lib/render.mjs');
|
|
120
122
|
const escenas = op.escenas != null && op.escenas !== true ? String(op.escenas).split(/[-,]/).map(Number) : null;
|
|
121
123
|
if (escenas && (escenas.some((x) => !Number.isInteger(x) || x < 1) || pos.length !== 1)) throw new ErrorMorfeo('Uso: npx morfeo render mi-video --escenas 5 (o 5-7)');
|
|
122
|
-
const hechos = await render({proyecto, ids: pos, todos: !!op.todos || pos[0] === 'todos', soloFallidos: !!op.soloFallidos, escenas: escenas ? [escenas[0], escenas[escenas.length - 1]] : null});
|
|
124
|
+
const hechos = await render({proyecto, ids: pos[0] === 'todos' ? [] : pos, todos: !!op.todos || pos[0] === 'todos', cambiados: !!op.cambiados, soloFallidos: !!op.soloFallidos, escenas: escenas ? [escenas[0], escenas[escenas.length - 1]] : null});
|
|
123
125
|
if (escenas) {
|
|
124
126
|
const {hoja} = await import('../lib/hoja.mjs');
|
|
125
127
|
for (const h of hechos ?? []) console.log(hoja({video: h.mp4, cols: 5, filas: 2}));
|
|
128
|
+
} else if (op.revisar) {
|
|
129
|
+
const {hoja} = await import('../lib/hoja.mjs');
|
|
130
|
+
const {cargarProyecto} = await import('../lib/config.mjs');
|
|
131
|
+
const p = await cargarProyecto(proyecto);
|
|
132
|
+
for (const h of hechos ?? []) {
|
|
133
|
+
console.log(hoja({video: h.mp4}));
|
|
134
|
+
await revisarVideo(p, h.id);
|
|
135
|
+
}
|
|
126
136
|
}
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
@@ -145,25 +155,13 @@ Pruébalo: npx morfeo render ${pos[0]} y revísalo: npx morfeo hoja ${pos[0]
|
|
|
145
155
|
}
|
|
146
156
|
case 'revisar': {
|
|
147
157
|
const {cargarProyecto} = await import('../lib/config.mjs');
|
|
148
|
-
const {
|
|
158
|
+
const {REGLAS} = await import('../lib/estructura.mjs');
|
|
149
159
|
const p = await cargarProyecto(proyecto);
|
|
150
160
|
const ids = pos.length ? pos : Object.keys(p.videos);
|
|
151
|
-
const fs = await import('node:fs');
|
|
152
161
|
let total = 0;
|
|
153
162
|
for (const id of ids) {
|
|
154
163
|
if (!p.videos[id]) throw new ErrorMorfeo(`No existe el video ${id}. Usa: npx morfeo lista`);
|
|
155
|
-
|
|
156
|
-
const f = path.join(p.trabajo, 'public', 'v', id, 'datos.json');
|
|
157
|
-
const d = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null;
|
|
158
|
-
const c = revisarEstructura(p.videos[id], d);
|
|
159
|
-
// con el MP4 ya hecho: cuadros negros, tapados, planos o quietos (--sin-cuadros lo salta)
|
|
160
|
-
if (!op.sinCuadros) {
|
|
161
|
-
const {salidaDe} = await import('../lib/salida.mjs');
|
|
162
|
-
const {revisarCuadros} = await import('../lib/calidad.mjs');
|
|
163
|
-
c.push(...(await revisarCuadros(salidaDe(p, id), d)));
|
|
164
|
-
}
|
|
165
|
-
total += c.length;
|
|
166
|
-
console.log(c.length ? `${id}${d ? '' : ' (sin tiempos: renderízalo para revisar el ritmo)'}\n${consejosEnTexto(c)}` : `${id} ✔`);
|
|
164
|
+
total += await revisarVideo(p, id, !!op.sinCuadros);
|
|
167
165
|
}
|
|
168
166
|
if (total) console.log(`\nReglas completas: ${REGLAS.map((r) => r.regla.toLowerCase()).join(' · ')}`);
|
|
169
167
|
return;
|
|
@@ -256,6 +254,24 @@ Pruébalo: npx morfeo render ${pos[0]} y revísalo: npx morfeo hoja ${pos[0]
|
|
|
256
254
|
}
|
|
257
255
|
}
|
|
258
256
|
|
|
257
|
+
/** Revisa un video (estructura + cuadros del MP4 si existe), lo imprime y devuelve cuántos avisos tuvo. */
|
|
258
|
+
async function revisarVideo(p, id, sinCuadros = false) {
|
|
259
|
+
const fs = await import('node:fs');
|
|
260
|
+
const {revisarEstructura, consejosEnTexto} = await import('../lib/estructura.mjs');
|
|
261
|
+
// tiempos reales si ya se preparó la voz (después de un render); si no, solo el texto
|
|
262
|
+
const f = path.join(p.trabajo, 'public', 'v', id, 'datos.json');
|
|
263
|
+
const d = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null;
|
|
264
|
+
const c = revisarEstructura(p.videos[id], d);
|
|
265
|
+
// con el MP4 ya hecho: cuadros negros, tapados, planos o quietos (--sin-cuadros lo salta)
|
|
266
|
+
if (!sinCuadros) {
|
|
267
|
+
const {salidaDe} = await import('../lib/salida.mjs');
|
|
268
|
+
const {revisarCuadros} = await import('../lib/calidad.mjs');
|
|
269
|
+
c.push(...(await revisarCuadros(salidaDe(p, id), d)));
|
|
270
|
+
}
|
|
271
|
+
console.log(c.length ? `${id}${d ? '' : ' (sin tiempos: renderízalo para revisar el ritmo)'}\n${consejosEnTexto(c)}` : `${id} ✔`);
|
|
272
|
+
return c.length;
|
|
273
|
+
}
|
|
274
|
+
|
|
259
275
|
async function guion(pos, op, proyecto) {
|
|
260
276
|
const {cargarEnv} = await import('../lib/render.mjs');
|
|
261
277
|
const {guionConIA, guionSinIA, guardarVideo} = await import('../lib/guion.mjs');
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tipos de Morfeo: dan autocompletado en morfeo.config.mjs y videos.mjs (también en JS).
|
|
2
2
|
|
|
3
3
|
/** '#RRGGBB' o [r, g, b] */
|
|
4
|
-
export type Ambiente = 'cuarto-gamer' | 'sala' | 'arcade' | 'taller' | 'oficina';
|
|
4
|
+
export type Ambiente = 'cuarto-gamer' | 'sala' | 'arcade' | 'taller' | 'oficina' | 'tienda' | 'calle';
|
|
5
5
|
/** Postproceso 3D: 'cine' (foco + brillo + viñeta), 'miniatura' (tilt-shift), 'suave', false o ajustes propios. */
|
|
6
6
|
export type Post3D = 'cine' | 'miniatura' | 'suave' | false | {foco?: boolean | number; brillo?: number; vinieta?: number; miniatura?: number};
|
|
7
7
|
export type Color = string | [number, number, number];
|
|
@@ -297,7 +297,7 @@ export interface Video {
|
|
|
297
297
|
* El componente recibe además `escena` (1, 2, 3...), `tEscena`, `inicios` y `camara` (para paralaje).
|
|
298
298
|
*/
|
|
299
299
|
/**
|
|
300
|
-
* Ambiente 3D listo como mundo del video: 'cuarto-gamer' | 'sala' | 'arcade' (o {nombre, persona, post}).
|
|
300
|
+
* Ambiente 3D listo como mundo del video: 'cuarto-gamer' | 'sala' | 'arcade' | 'taller' | 'oficina' | 'tienda' | 'calle' (o {nombre, persona, post}).
|
|
301
301
|
* Sirve en cualquier estilo del motor de capas; cada escena elige su `plano` o toma otro ángulo sola.
|
|
302
302
|
*/
|
|
303
303
|
ambiente?: Ambiente | {nombre: Ambiente; persona?: boolean | {ropa?: string; pelo?: string; piel?: string}; reaccionar?: boolean; post?: Post3D};
|
package/lib/config.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export const ROTULOS = ['normal', 'contorno', 'pastilla', 'sello', 'paso', 'pixe
|
|
|
28
28
|
export const MODOS = ['rayosx', 'plano', 'holograma'];
|
|
29
29
|
export const ESTILOS = ['datos-curiosos', 'explicativo', 'documental', 'ilustrado', 'historia', 'mesa', 'serie', 'infografia', '3d', 'maqueta', 'minimal'];
|
|
30
30
|
export const TIPOS_CAPA = ['imagen', 'video', 'texto', 'contador', 'chip', 'etiqueta', 'numero', 'tarjeta', 'telefono', 'forma', 'icono', 'emoji', 'marca', 'particulas', 'grupo', 'modelo', 'componente', 'ambiente', 'barra', 'medidor', 'votacion', 'estado', 'noticia', 'chat', 'onda', 'globo', 'seguir', 'personaje', 'escena3d', 'ventana', 'escenario', 'recurso', 'dato', 'medida', 'trazo', 'contorno', 'fuente', 'seccion', 'insignia', 'sello', 'cursor', 'terminal', 'nombre', 'accion', 'reloj', 'cronometro', 'termometro', 'recipientes', 'ficha', 'checklist', 'mapa', 'bloqueo', 'llamada', 'recuento', 'capitulo', 'vigilancia', 'cintillo', 'fecha', 'calendario', 'dividida', 'tierra', 'linea'];
|
|
31
|
-
export const AMBIENTES = ['cuarto-gamer', 'sala', 'arcade', 'taller', 'oficina'];
|
|
31
|
+
export const AMBIENTES = ['cuarto-gamer', 'sala', 'arcade', 'taller', 'oficina', 'tienda', 'calle'];
|
|
32
32
|
export const ESCENARIOS = ['bosque', 'cuarto', 'departamento', 'tunel', 'pasillo'];
|
|
33
33
|
export const FORMAS_3D = ['moneda', 'billete', 'lingote', 'cubo', 'esfera', 'corazon', 'estrella', 'rayo', 'diamante', 'trofeo', 'cohete', 'foco', 'reloj', 'planeta', 'arbol', 'nube', 'regalo', 'llave', 'candado', 'casa', 'libro', 'globo', 'medalla', 'escudo', 'pin', 'tuberia', 'muro', 'viga', 'columna', 'escalera', 'losa', 'tanque', 'suelo'];
|
|
34
34
|
export const PROVEEDORES_VOZ = ['kokoro', 'elevenlabs', 'openai'];
|
package/lib/estructura.mjs
CHANGED
|
@@ -129,7 +129,7 @@ export function revisarEstructura(v, d) {
|
|
|
129
129
|
const capas = (s.capas ?? []).filter(Boolean);
|
|
130
130
|
if (!conMundo && (s.type ?? 'capas') === 'capas' && !capas.length && !s.rotulo) {
|
|
131
131
|
vacias++;
|
|
132
|
-
add('vacia', 'aviso', `Escena ${i + 1}: solo el subtítulo sobre el fondo. Agrega una capa o dale al video un \`ambiente: 'cuarto-gamer'\` (o 'sala', 'arcade', 'taller', 'oficina').`);
|
|
132
|
+
add('vacia', 'aviso', `Escena ${i + 1}: solo el subtítulo sobre el fondo. Agrega una capa o dale al video un \`ambiente: 'cuarto-gamer'\` (o 'sala', 'arcade', 'taller', 'oficina', 'tienda', 'calle').`);
|
|
133
133
|
}
|
|
134
134
|
if (capas.length && capas.every((c) => SOLO_TEXTO.has(c.tipo))) soloTexto++;
|
|
135
135
|
for (const c of capas) tipos.set(c.tipo, (tipos.get(c.tipo) ?? 0) + 1);
|
package/lib/render.mjs
CHANGED
|
@@ -80,6 +80,47 @@ const conHuella = (config) => ({
|
|
|
80
80
|
resolve: {...config.resolve, modules: [...(config.resolve?.modules ?? ['node_modules']), path.join(PAQUETE, 'node_modules')]},
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
const PREFIJO_BUNDLE = 'remotion-webpack-bundle-';
|
|
84
|
+
const esBundleTemporal = (dir) => path.basename(dir).startsWith(PREFIJO_BUNDLE) && fs.realpathSync(path.dirname(dir)) === fs.realpathSync(os.tmpdir());
|
|
85
|
+
|
|
86
|
+
/** Bundles que dejó un render interrumpido (o versiones anteriores de Morfeo, que no los borraban): fuera si tienen más de un día. */
|
|
87
|
+
function limpiarBundlesViejos() {
|
|
88
|
+
try {
|
|
89
|
+
const limite = Date.now() - 24 * 3600 * 1000;
|
|
90
|
+
for (const n of fs.readdirSync(os.tmpdir())) {
|
|
91
|
+
if (!n.startsWith(PREFIJO_BUNDLE)) continue;
|
|
92
|
+
const dir = path.join(os.tmpdir(), n);
|
|
93
|
+
if (fs.statSync(dir).mtimeMs < limite) fs.rmSync(dir, {recursive: true, force: true});
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Webpack guarda un caché (~300 MB) por cada huella del motor y de la configuración, y no borra los anteriores:
|
|
100
|
+
* tras varias actualizaciones eran varios GB. Se quedan los dos más recientes.
|
|
101
|
+
*/
|
|
102
|
+
function podarCacheWebpack(raiz) {
|
|
103
|
+
try {
|
|
104
|
+
let dir = raiz;
|
|
105
|
+
while (!fs.existsSync(path.join(dir, 'node_modules', '.cache', 'webpack'))) {
|
|
106
|
+
const arriba = path.dirname(dir);
|
|
107
|
+
if (arriba === dir) return;
|
|
108
|
+
dir = arriba;
|
|
109
|
+
}
|
|
110
|
+
const base = path.join(dir, 'node_modules', '.cache', 'webpack');
|
|
111
|
+
for (const grupo of fs.readdirSync(base)) {
|
|
112
|
+
if (!grupo.startsWith('remotion-')) continue;
|
|
113
|
+
const g = path.join(base, grupo);
|
|
114
|
+
const viejos = fs
|
|
115
|
+
.readdirSync(g)
|
|
116
|
+
.map((n) => ({d: path.join(g, n), m: fs.statSync(path.join(g, n)).mtimeMs}))
|
|
117
|
+
.sort((x, y) => y.m - x.m)
|
|
118
|
+
.slice(2);
|
|
119
|
+
for (const v of viejos) fs.rmSync(v.d, {recursive: true, force: true});
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
/**
|
|
84
125
|
* Entrada de Remotion. Sin componentes propios es la del paquete; con ellos, Morfeo escribe
|
|
85
126
|
* .morfeo/entrada.tsx que los importa y los registra antes de arrancar.
|
|
@@ -230,7 +271,7 @@ export function usaWebGL(p, id) {
|
|
|
230
271
|
* @param {{proyecto?: string, ids?: string[], todos?: boolean, log?: (m: string) => void, alProgreso?: (id: string, p: number) => void}} o
|
|
231
272
|
* @returns {Promise<{id: string, mp4: string, jpg: string | null, segundos: number}[]>}
|
|
232
273
|
*/
|
|
233
|
-
export async function render({proyecto = process.cwd(), ids, todos = false, soloFallidos = false, previa = null, animatica = false, escenas = null, log = console.log, alProgreso} = {}) {
|
|
274
|
+
export async function render({proyecto = process.cwd(), ids, todos = false, cambiados = false, soloFallidos = false, previa = null, animatica = false, escenas = null, log = console.log, alProgreso} = {}) {
|
|
234
275
|
// animática: todo el video a 1/3 de tamaño y menor calidad (para aprobar tomas y ritmo en segundos)
|
|
235
276
|
// escenas: [a, b] (1 = la primera) solo ese tramo, en tamaño completo
|
|
236
277
|
const parcial = !!(animatica || escenas);
|
|
@@ -250,15 +291,37 @@ export async function render({proyecto = process.cwd(), ids, todos = false, solo
|
|
|
250
291
|
}
|
|
251
292
|
// archivos ya hechos con el id o con un título viejo → su nombre actual (sin volver a renderizar)
|
|
252
293
|
ordenarSalida(p, (m) => log(m));
|
|
253
|
-
let lista = ids?.length ? ids : todos ? Object.keys(p.videos) : Object.keys(p.videos).filter((id) => !fs.existsSync(salidaDe(p, id)));
|
|
294
|
+
let lista = ids?.length ? ids : todos || cambiados ? Object.keys(p.videos) : Object.keys(p.videos).filter((id) => !fs.existsSync(salidaDe(p, id)));
|
|
254
295
|
const malos = lista.filter((id) => !p.videos[id]);
|
|
255
296
|
if (malos.length) throw new ErrorMorfeo(`No existen: ${malos.join(', ')}. Usa: npx morfeo lista`);
|
|
256
297
|
if (!lista.length) {
|
|
257
298
|
log('No hay videos pendientes. Para regenerar: npx morfeo render --todos (o npx morfeo render <id>)');
|
|
258
299
|
return [];
|
|
259
300
|
}
|
|
301
|
+
// --cambiados: solo los videos cuyo guion o componentes del proyecto cambiaron desde su último render. Se decide
|
|
302
|
+
// con el guion, antes de preparar el audio (prepararlo para 30 videos tarda más que renderizar uno).
|
|
303
|
+
// La versión de Morfeo no cuenta (rehacería todo en cada actualización): para eso está --todos.
|
|
304
|
+
const huellas = leerHuellas(p);
|
|
305
|
+
const delProyecto = parcial || previa ? '' : huellaProyecto(p);
|
|
306
|
+
const huellaDe = (id) => crypto.createHash('sha1').update(delProyecto).update(JSON.stringify(p.videos[id], (k, v) => (typeof v === 'function' ? String(v) : v))).digest('hex').slice(0, 16);
|
|
307
|
+
if (cambiados && !ids?.length) {
|
|
308
|
+
const antes = lista.length;
|
|
309
|
+
// la primera vez no hay huella de los videos ya hechos: se dan por buenos y se anota la de ahora
|
|
310
|
+
let nuevas = 0;
|
|
311
|
+
for (const id of lista)
|
|
312
|
+
if (huellas[id] == null && fs.existsSync(salidaDe(p, id))) {
|
|
313
|
+
huellas[id] = huellaDe(id);
|
|
314
|
+
nuevas++;
|
|
315
|
+
}
|
|
316
|
+
if (nuevas) guardarHuellas(p, huellas);
|
|
317
|
+
lista = lista.filter((id) => huellas[id] !== huellaDe(id) || !fs.existsSync(salidaDe(p, id)));
|
|
318
|
+
log(lista.length ? `${lista.length} de ${antes} video(s) cambiaron: ${lista.join(', ')}` : `Ningún video cambió desde su último render (${antes} al día).`);
|
|
319
|
+
if (!lista.length) return [];
|
|
320
|
+
}
|
|
260
321
|
if (!whisperListo()) log('(Whisper no está instalado: subtítulos repartidos por frase. Para tiempos exactos: npx morfeo instalar --whisper)');
|
|
322
|
+
const tPrep = Date.now();
|
|
261
323
|
const {publico, marca, extras, datos, fallidos} = await preparar({proyecto, ids: lista, log, _p: p, continuar: true});
|
|
324
|
+
const segPrep = (Date.now() - tPrep) / 1000;
|
|
262
325
|
const listos = lista.filter((id) => datos[id]);
|
|
263
326
|
// estructura: consejos medidos en videos virales (no detienen el render; se apagan con render.consejos: false)
|
|
264
327
|
if (p.render.consejos !== false)
|
|
@@ -272,17 +335,25 @@ export async function render({proyecto = process.cwd(), ids, todos = false, solo
|
|
|
272
335
|
// cuadros pedazos de texto de cuadros anteriores (rótulos o subtítulos fantasma)
|
|
273
336
|
const hechos = [];
|
|
274
337
|
const soltar = await tomarCandado(log);
|
|
338
|
+
let serveUrl;
|
|
275
339
|
try {
|
|
276
340
|
log('Empaquetando la animación...');
|
|
277
341
|
const {bundle} = await import('@remotion/bundler');
|
|
278
342
|
const {renderMedia, renderStill, selectComposition} = await import('@remotion/renderer');
|
|
279
343
|
const entrada = entradaDe(p);
|
|
280
|
-
const
|
|
344
|
+
const tBundle = Date.now();
|
|
345
|
+
limpiarBundlesViejos();
|
|
346
|
+
const raizBundle = entrada === ENTRADA_REMOTION ? path.dirname(ENTRADA_REMOTION) : p.proyecto;
|
|
347
|
+
serveUrl = await bundle({entryPoint: entrada, publicDir: publico, rootDir: raizBundle, webpackOverride: conHuella});
|
|
348
|
+
podarCacheWebpack(raizBundle);
|
|
349
|
+
const segBundle = (Date.now() - tBundle) / 1000;
|
|
281
350
|
fs.mkdirSync(p.salida, {recursive: true});
|
|
282
351
|
const chromiumOptions = {gl: p.render.gl};
|
|
283
352
|
const browserExecutable = p.render.navegador ?? (await asegurarNavegador(log));
|
|
284
353
|
// cuadros en paralelo: con GPU aguanta más; por software cada pestaña come mucha CPU y memoria
|
|
285
354
|
const nucleos = os.cpus().length;
|
|
355
|
+
// MORFEO_CONCURRENCIA=n: fija los cuadros en paralelo sin tocar el config (para medir en tu máquina)
|
|
356
|
+
if (process.env.MORFEO_CONCURRENCIA) p.render.concurrencia = Math.max(1, Number(process.env.MORFEO_CONCURRENCIA) || 1);
|
|
286
357
|
const concurrenciaBase = p.render.concurrencia ?? (p.render.gl === 'angle' ? Math.max(2, Math.min(10, Math.floor(nucleos * 0.75))) : Math.max(1, Math.min(3, Math.floor(nucleos / 4))));
|
|
287
358
|
const sinTTY = !process.stdout.isTTY;
|
|
288
359
|
for (const [i, id] of listos.entries()) {
|
|
@@ -317,6 +388,8 @@ export async function render({proyecto = process.cwd(), ids, todos = false, solo
|
|
|
317
388
|
audioBitrate: '192k',
|
|
318
389
|
imageFormat: 'jpeg',
|
|
319
390
|
jpegQuality: 92,
|
|
391
|
+
// 'faster': ~10 % menos tiempo que 'medium' y, con el mismo CRF, sin diferencia visible
|
|
392
|
+
x264Preset: 'faster',
|
|
320
393
|
concurrency: concurrencia,
|
|
321
394
|
outputLocation: mp4,
|
|
322
395
|
...(previa ? {frameRange: [0, Math.min(composition.durationInFrames - 1, Math.round(finPrevia * composition.fps))]} : {}),
|
|
@@ -346,6 +419,7 @@ export async function render({proyecto = process.cwd(), ids, todos = false, solo
|
|
|
346
419
|
log(`\n el navegador falló; reintento con ${gl === 'angle' ? 'GPU' : 'CPU'} y ${concurrencia} cuadro(s) a la vez...`);
|
|
347
420
|
}
|
|
348
421
|
}
|
|
422
|
+
const segVideo = (Date.now() - t) / 1000;
|
|
349
423
|
let jpg = null;
|
|
350
424
|
if (p.render.miniatura && !previa && !parcial) {
|
|
351
425
|
// miniatura: cuando la primera escena ya está completa (o en `miniaturaEn`), sin subtítulos
|
|
@@ -371,7 +445,13 @@ export async function render({proyecto = process.cwd(), ids, todos = false, solo
|
|
|
371
445
|
const segundos = Math.round((Date.now() - t) / 1000);
|
|
372
446
|
if (process.stdout.isTTY) process.stdout.write('\r');
|
|
373
447
|
log(`[${i + 1}/${listos.length}] ${id}: listo en ${segundos} s → ${path.relative(p.proyecto, mp4)} `);
|
|
448
|
+
// desglose para saber qué optimizar (MORFEO_TIEMPOS=1): audio y empaquetado son de todo el lote
|
|
449
|
+
if (process.env.MORFEO_TIEMPOS) log(` tiempos: audio ${segPrep.toFixed(1)} s (lote) · empaquetar ${segBundle.toFixed(1)} s (lote) · video ${segVideo.toFixed(1)} s · miniatura y portada ${(segundos - segVideo).toFixed(1)} s`);
|
|
374
450
|
hechos.push({id, mp4, jpg, segundos});
|
|
451
|
+
if (!previa && !parcial) {
|
|
452
|
+
huellas[id] = huellaDe(id);
|
|
453
|
+
guardarHuellas(p, huellas);
|
|
454
|
+
}
|
|
375
455
|
} catch (e) {
|
|
376
456
|
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
377
457
|
fallidos.push({id, error: mensajeDe(e)});
|
|
@@ -379,12 +459,47 @@ export async function render({proyecto = process.cwd(), ids, todos = false, solo
|
|
|
379
459
|
}
|
|
380
460
|
}
|
|
381
461
|
} finally {
|
|
462
|
+
// el bundle es una copia de la animación y de public/ (cientos de MB) en la carpeta temporal: Remotion no la borra
|
|
463
|
+
if (serveUrl && esBundleTemporal(serveUrl)) fs.rmSync(serveUrl, {recursive: true, force: true});
|
|
382
464
|
soltar();
|
|
383
465
|
}
|
|
384
466
|
if (previa || parcial) return hechos;
|
|
385
467
|
return terminarLote(p, loteArchivo, hechos, fallidos, log);
|
|
386
468
|
}
|
|
387
469
|
|
|
470
|
+
// Huella de cada video al renderizarlo (.morfeo/huellas.json): con ella `render --cambiados` sabe cuáles rehacer.
|
|
471
|
+
const archivoHuellas = (p) => path.join(p.trabajo, 'huellas.json');
|
|
472
|
+
function leerHuellas(p) {
|
|
473
|
+
try {
|
|
474
|
+
return JSON.parse(fs.readFileSync(archivoHuellas(p), 'utf8'));
|
|
475
|
+
} catch {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function guardarHuellas(p, h) {
|
|
480
|
+
fs.mkdirSync(p.trabajo, {recursive: true});
|
|
481
|
+
fs.writeFileSync(archivoHuellas(p), JSON.stringify(h, null, '\t'));
|
|
482
|
+
}
|
|
483
|
+
/** Componentes y config del proyecto (tsx/ts/jsx y morfeo.config.mjs): si cambian, cambia cómo se ve el video. */
|
|
484
|
+
function huellaProyecto(p) {
|
|
485
|
+
const partes = [];
|
|
486
|
+
const recorrer = (dir, n = 0) => {
|
|
487
|
+
if (n > 4) return;
|
|
488
|
+
for (const e of fs.readdirSync(dir, {withFileTypes: true}).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
489
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'guiones') continue;
|
|
490
|
+
const f = path.join(dir, e.name);
|
|
491
|
+
if (path.resolve(f) === path.resolve(p.salida)) continue;
|
|
492
|
+
if (e.isDirectory()) recorrer(f, n + 1);
|
|
493
|
+
else if (/\.(tsx?|jsx)$/.test(e.name) || e.name === 'morfeo.config.mjs') {
|
|
494
|
+
const st = fs.statSync(f);
|
|
495
|
+
partes.push(`${e.name}:${st.size}:${Math.round(st.mtimeMs)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
recorrer(p.proyecto);
|
|
500
|
+
return crypto.createHash('sha1').update(partes.join('|')).digest('hex').slice(0, 8);
|
|
501
|
+
}
|
|
502
|
+
|
|
388
503
|
const CRF = {alta: 18, media: 21, ligera: 24};
|
|
389
504
|
/** Cuadros [inicio, fin] de las escenas a..b (1 = la primera). */
|
|
390
505
|
function tramoEscenas(d, [a, b], composition) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morfeo",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Genera videos verticales animados (Remotion + Three.js) con voz, música y subtítulos, desde un guion en código o escrito por IA.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
package/plantilla/AGENTS.md
CHANGED
|
@@ -43,8 +43,9 @@ la persona te corrija algo que deba valer para los siguientes videos, ofrécele
|
|
|
43
43
|
(tiene 3 ganchos de reserva en `hooks`).
|
|
44
44
|
6. **Aprueba tomas y ritmo barato:** `npx morfeo animatica <id>` (todo el video a 1/3 de tamaño + su hoja). Si una
|
|
45
45
|
toma está mal, arréglala y vuelve a pedir solo esas escenas: `npx morfeo render <id> --escenas 5-6`.
|
|
46
|
-
7. **Renderiza:** `npx morfeo render <id
|
|
47
|
-
|
|
46
|
+
7. **Renderiza y revisa en un paso:** `npx morfeo render <id> --revisar` (video + hoja + revisión). Tras cambiar varios
|
|
47
|
+
guiones, `npx morfeo render --cambiados --revisar` rehace solo los que cambiaron.
|
|
48
|
+
8. **Revisa antes de entregar** (si no usaste `--revisar`):
|
|
48
49
|
- `npx morfeo revisar <id>`: estructura, que la voz haya dicho el guion, que nada quede bajo los botones de la app,
|
|
49
50
|
el riesgo de "diapositiva" y, en el MP4, cuadros negros, tapados (cámara pegada a algo), lisos o quietos.
|
|
50
51
|
- `npx morfeo hoja <id> --zona`: mira la hoja de contacto (es una imagen); lo pintado en rojo es lo que tapan las apps.
|
package/python/musica.py
CHANGED
|
@@ -335,6 +335,18 @@ def cama(nombre, N, rng):
|
|
|
335
335
|
n = int(0.012 * SR); g = int(rng.random() * (N - n))
|
|
336
336
|
x[g:g + n] += 0.02 * hp(rng.standard_normal(n), 2500) * np.exp(-np.arange(n) / (0.002 * SR))
|
|
337
337
|
return x
|
|
338
|
+
if nombre == "tienda":
|
|
339
|
+
x = lp(hp(rng.standard_normal(N), 250), 1300) * 0.025 * (0.6 + 0.4 * np.abs(np.sin(2 * np.pi * 0.6 * t))) + zumbido * 0.7
|
|
340
|
+
for _ in range(int(N / SR / 2)): # la tele de demostración: blips lejanos
|
|
341
|
+
add(x, 0.01 * lp(_tono(nf(74 + int(rng.random() * 12)), 0.1, 20), 2500), rng.random() * (N / SR - 0.2))
|
|
342
|
+
return x
|
|
343
|
+
if nombre == "calle":
|
|
344
|
+
viento = lp(rng.standard_normal(N), 350) * 0.05 * (0.6 + 0.4 * np.sin(2 * np.pi * 0.11 * t))
|
|
345
|
+
x = lluvia(0.06) + viento
|
|
346
|
+
for _ in range(max(1, int(N / SR / 9))): # un coche que pasa a lo lejos
|
|
347
|
+
d = 3.0; n = int(d * SR); tt = np.arange(n) / SR
|
|
348
|
+
add(x, 0.035 * lp(rng.standard_normal(n), 500) * np.sin(np.pi * tt / d) ** 2, rng.random() * max(0.1, N / SR - d))
|
|
349
|
+
return x
|
|
338
350
|
if nombre == "arcade":
|
|
339
351
|
x = lp(rng.standard_normal(N), 700) * 0.05 + zumbido
|
|
340
352
|
for _ in range(int(N / SR * 3)): # maquinitas sonando por todos lados
|
|
@@ -13,7 +13,7 @@ import {Persona3D} from './mundo3d';
|
|
|
13
13
|
import type {Expresion, Pose} from './personas';
|
|
14
14
|
import {Bloque, Lienzo3D, type Etiqueta3D, type Post3D, type Toma3D, type V3} from './tres';
|
|
15
15
|
|
|
16
|
-
export type NombreAmbiente = 'cuarto-gamer' | 'sala' | 'arcade' | 'taller' | 'oficina';
|
|
16
|
+
export type NombreAmbiente = 'cuarto-gamer' | 'sala' | 'arcade' | 'taller' | 'oficina' | 'tienda' | 'calle';
|
|
17
17
|
export type Reaccion = {en: number; expresion?: Expresion; pose?: Pose};
|
|
18
18
|
|
|
19
19
|
type Def = {
|
|
@@ -627,6 +627,192 @@ const SetOficina: React.FC<{t: number}> = ({t}) => (
|
|
|
627
627
|
</group>
|
|
628
628
|
);
|
|
629
629
|
|
|
630
|
+
|
|
631
|
+
/** Caja de juego de frente (portada de color con una franja), para exhibidores. */
|
|
632
|
+
const CajaJuego: React.FC<{pos: V3; i: number; rot?: V3}> = ({pos, i, rot}) => {
|
|
633
|
+
const c = ['#E5484D', '#3D7BFF', '#F2B632', '#30A46C', '#8B5BD6', '#FF8A3D'][i % 6];
|
|
634
|
+
return (
|
|
635
|
+
<group position={pos} rotation={rot ? [(rot[0] * Math.PI) / 180, (rot[1] * Math.PI) / 180, (rot[2] * Math.PI) / 180] : undefined}>
|
|
636
|
+
<Bloque pos={[0, 0, 0]} tam={[0.26, 0.36, 0.04]} color={c} />
|
|
637
|
+
<Bloque pos={[0, 0.13, 0.022]} tam={[0.26, 0.06, 0.004]} color="#F4F1EA" />
|
|
638
|
+
<Bloque pos={[0, -0.03, 0.022]} tam={[0.12, 0.12, 0.004]} color={hsl(0.13 * i + 0.4, 0.6, 0.72)} />
|
|
639
|
+
</group>
|
|
640
|
+
);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
/** Tienda de videojuegos: pared de juegos, mostrador con caja, tele de demostración, isla de ofertas y neón. */
|
|
644
|
+
const SetTienda: React.FC<{t: number}> = ({t}) => (
|
|
645
|
+
<group>
|
|
646
|
+
<Cuarto ancho={9} fondo={7} alto={3.4} pared="#232A44" lateral="#1D233A" piso="#3A3340" />
|
|
647
|
+
{/* pared de juegos: dos filas de estantes */}
|
|
648
|
+
{[-3.2, -1.55, 0.1].map((x, i) => (
|
|
649
|
+
<React.Fragment key={x}>
|
|
650
|
+
<Estante pos={[x, 0.75, -3.25]} semilla={i * 7} />
|
|
651
|
+
<Estante pos={[x, 2.05, -3.25]} semilla={i * 7 + 3} />
|
|
652
|
+
</React.Fragment>
|
|
653
|
+
))}
|
|
654
|
+
<Emisivo pos={[-1.55, 3.05, -3.36]} tam={[4.9, 0.05, 0.04]} color="#7FE3FF" fuerza={1.4} />
|
|
655
|
+
<pointLight position={[-1.55, 2.9, -2.6]} color="#7FE3FF" intensity={1.2} distance={5} decay={1.6} />
|
|
656
|
+
{/* tele de demostración y letrero de neón (un control) */}
|
|
657
|
+
<Tele3D pos={[2.8, 1.95, -3.32]} ancho={1.3} t={t} fase={2} luz={1.6} />
|
|
658
|
+
<group position={[2.8, 2.95, -3.36]}>
|
|
659
|
+
{[
|
|
660
|
+
[0, 0.16, 0.9, 0],
|
|
661
|
+
[0, -0.16, 0.9, 0],
|
|
662
|
+
[-0.45, 0, 0.32, 90],
|
|
663
|
+
[0.45, 0, 0.32, 90],
|
|
664
|
+
].map(([x, y, l, r], i) => (
|
|
665
|
+
<Emisivo key={i} pos={[x, y, 0]} tam={[l, 0.05, 0.04]} rot={[0, 0, (r * Math.PI) / 180]} color="#FF3B6B" fuerza={2.1 * (Math.sin(t * 11) > -0.9 ? 1 : 0.6)} />
|
|
666
|
+
))}
|
|
667
|
+
<Emisivo pos={[-0.22, 0, 0]} tam={[0.16, 0.05, 0.04]} color="#FFD23D" fuerza={2} />
|
|
668
|
+
<Emisivo pos={[-0.22, 0, 0]} tam={[0.05, 0.16, 0.04]} color="#FFD23D" fuerza={2} />
|
|
669
|
+
<Emisivo pos={[0.2, 0.03, 0]} tam={[0.06, 0.06, 0.04]} color="#3DB8FF" fuerza={2} />
|
|
670
|
+
<Emisivo pos={[0.3, -0.04, 0]} tam={[0.06, 0.06, 0.04]} color="#3DDC84" fuerza={2} />
|
|
671
|
+
<pointLight position={[0, 0, 0.6]} color="#FF3B6B" intensity={1.6} distance={4} />
|
|
672
|
+
</group>
|
|
673
|
+
{/* mostrador con caja registradora y vitrina */}
|
|
674
|
+
<Bloque pos={[2.6, 0.5, -1.2]} tam={[2.2, 1.0, 0.8]} color="#4A3B5C" />
|
|
675
|
+
<Bloque pos={[2.6, 1.02, -1.2]} tam={[2.3, 0.05, 0.9]} color="#E9E1D2" />
|
|
676
|
+
<Emisivo pos={[2.6, 0.55, -0.79]} tam={[1.9, 0.5, 0.02]} color="#9AD7FF" fuerza={0.5} />
|
|
677
|
+
{[0, 1, 2, 3].map((i) => (
|
|
678
|
+
<Bloque key={i} pos={[1.9 + i * 0.46, 0.5, -0.76]} tam={[0.3, 0.12, 0.04]} color={['#EDEDED', '#2B2B33', '#E5484D', '#3D7BFF'][i]} />
|
|
679
|
+
))}
|
|
680
|
+
<Bloque pos={[3.1, 1.2, -1.25]} tam={[0.5, 0.3, 0.45]} color="#2B2B33" />
|
|
681
|
+
<Emisivo pos={[3.1, 1.42, -1.1]} tam={[0.36, 0.2, 0.03]} rot={[-0.5, 0, 0]} color="#3DDC84" fuerza={1.2 + 0.2 * Math.sin(t * 5)} />
|
|
682
|
+
<Bloque pos={[2.0, 1.14, -1.2]} tam={[0.3, 0.18, 0.2]} color="#EDEDED" radio={0.03} />
|
|
683
|
+
{/* isla de ofertas */}
|
|
684
|
+
<Bloque pos={[-2.6, 0.45, 0.6]} tam={[1.5, 0.9, 0.8]} color="#4A3B5C" />
|
|
685
|
+
{[0, 1, 2, 3].map((i) => (
|
|
686
|
+
<CajaJuego key={i} pos={[-3.15 + i * 0.37, 1.07, 0.6]} i={i + 2} rot={[-18, 0, 0]} />
|
|
687
|
+
))}
|
|
688
|
+
<Emisivo pos={[-2.6, 0.5, 1.01]} tam={[0.9, 0.3, 0.02]} color="#FFD23D" fuerza={1.1} />
|
|
689
|
+
<pointLight position={[-2.4, 2.4, 1.4]} color="#FFE2B8" intensity={1.8} distance={4.5} decay={1.5} />
|
|
690
|
+
{/* póster y tapete */}
|
|
691
|
+
<Poster pos={[-4.38, 2.0, -1.2]} tipo="nave" rot={[0, 90, 0]} />
|
|
692
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0.4, 0.006, -0.6]}>
|
|
693
|
+
<circleGeometry args={[1.5, 28]} />
|
|
694
|
+
<meshStandardMaterial color="#5B4B8A" roughness={0.95} />
|
|
695
|
+
</mesh>
|
|
696
|
+
{/* luces del techo */}
|
|
697
|
+
{[-2.4, 1.2].map((x) => (
|
|
698
|
+
<group key={x} position={[x, 3.3, -1]}>
|
|
699
|
+
<Emisivo pos={[0, 0, 0]} tam={[1.6, 0.04, 0.14]} color="#FFE9C4" fuerza={1.4} />
|
|
700
|
+
<pointLight position={[0, -0.3, 0]} intensity={2.3} distance={7.5} decay={1.4} color="#FFE2B8" />
|
|
701
|
+
</group>
|
|
702
|
+
))}
|
|
703
|
+
<Polvo centro={[0, 1.8, -1.2]} tam={[6, 2.4, 3]} t={t} n={30} />
|
|
704
|
+
</group>
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
/** Calle de noche con lluvia: fachadas con ventanas, un local con vitrina y neón, farola, banca y coche. */
|
|
708
|
+
const SetCalle: React.FC<{t: number}> = ({t}) => (
|
|
709
|
+
<group>
|
|
710
|
+
{/* asfalto, banqueta y línea de la calle */}
|
|
711
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 1]} receiveShadow>
|
|
712
|
+
<planeGeometry args={[40, 16]} />
|
|
713
|
+
<meshStandardMaterial color="#14161E" roughness={0.35} metalness={0.3} />
|
|
714
|
+
</mesh>
|
|
715
|
+
<Bloque pos={[0, 0.07, -1.3]} tam={[40, 0.14, 3.6]} color="#3B3D48" rugosidad={0.9} />
|
|
716
|
+
{[-9, -6, -3, 0, 3, 6, 9].map((x) => (
|
|
717
|
+
<Bloque key={x} pos={[x, 0.006, 3.2]} tam={[1.4, 0.01, 0.14]} color="#D8D2B8" />
|
|
718
|
+
))}
|
|
719
|
+
{/* charcos que reflejan el neón */}
|
|
720
|
+
{[
|
|
721
|
+
[-1.2, 1.4, 1.1],
|
|
722
|
+
[1.8, 2.4, 0.8],
|
|
723
|
+
[-3.6, 3.6, 0.7],
|
|
724
|
+
].map(([x, z, r], i) => (
|
|
725
|
+
<mesh key={i} rotation={[-Math.PI / 2, 0, 0]} position={[x, 0.004, z]} scale={[1, 0.55, 1]}>
|
|
726
|
+
<circleGeometry args={[r, 20]} />
|
|
727
|
+
<meshStandardMaterial color="#232A4A" roughness={0.05} metalness={0.9} emissive="#FF3B6B" emissiveIntensity={0.05 + 0.03 * Math.sin(t * 3 + i)} />
|
|
728
|
+
</mesh>
|
|
729
|
+
))}
|
|
730
|
+
{/* fachadas */}
|
|
731
|
+
{[
|
|
732
|
+
[-5.6, 7.5, 5, '#1C2033'],
|
|
733
|
+
[0, 5.2, 5.4, '#2A2338'],
|
|
734
|
+
[5.8, 8.6, 5.6, '#1A1E2E'],
|
|
735
|
+
].map(([x, alto, ancho, color], k) => (
|
|
736
|
+
<group key={k}>
|
|
737
|
+
<Bloque pos={[x as number, (alto as number) / 2, -4.4]} tam={[ancho as number, alto as number, 2.6]} color={color as string} rugosidad={0.95} />
|
|
738
|
+
{Array.from({length: 4 * Math.floor(((alto as number) - 2.6) / 1.15)}, (_, i) => {
|
|
739
|
+
const col = i % 4;
|
|
740
|
+
const fila = Math.floor(i / 4);
|
|
741
|
+
const prendida = azarN(k * 31 + i * 7.3) > 0.45;
|
|
742
|
+
const parpadea = k === 0 && i === 5 ? 0.6 + 0.4 * (Math.sin(t * 9) > 0.2 ? 1 : 0.3) : 1;
|
|
743
|
+
return <Emisivo key={i} pos={[(x as number) - (ancho as number) / 2 + 0.85 + col * (((ancho as number) - 1.7) / 3), 3.4 + fila * 1.15, -3.08]} tam={[0.62, 0.72, 0.03]} color={prendida ? (azarN(i * 3.1 + k) > 0.6 ? '#9AD7FF' : '#FFD98A') : '#141826'} fuerza={prendida ? 0.9 * parpadea : 0.05} />;
|
|
744
|
+
})}
|
|
745
|
+
</group>
|
|
746
|
+
))}
|
|
747
|
+
{/* local: vitrina encendida, puerta, toldo y letrero */}
|
|
748
|
+
<Emisivo pos={[-0.6, 1.35, -3.08]} tam={[2.6, 1.7, 0.04]} color="#E0A050" fuerza={0.5} />
|
|
749
|
+
{[0, 1, 2, 3, 4, 5, 6, 7].map((i) => (
|
|
750
|
+
<CajaJuego key={i} pos={[-1.5 + (i % 4) * 0.6, i < 4 ? 0.95 : 1.6, -3.02]} i={i} />
|
|
751
|
+
))}
|
|
752
|
+
<Bloque pos={[-0.6, 1.28, -3.03]} tam={[2.6, 0.04, 0.12]} color="#3A2F4E" />
|
|
753
|
+
<Bloque pos={[-0.6, 0.38, -3.02]} tam={[2.7, 0.5, 0.1]} color="#3A2F4E" />
|
|
754
|
+
<Bloque pos={[1.6, 1.15, -3.06]} tam={[0.95, 2.1, 0.08]} color="#4A3B5C" />
|
|
755
|
+
<Emisivo pos={[1.6, 1.5, -3.01]} tam={[0.6, 0.9, 0.02]} color="#FFC56B" fuerza={0.6} />
|
|
756
|
+
<Bloque pos={[0.1, 2.45, -2.75]} rot={[18, 0, 0]} tam={[4.4, 0.08, 0.9]} color="#C62E3A" />
|
|
757
|
+
<group position={[0.1, 3.05, -3.04]}>
|
|
758
|
+
{[-1.2, -0.6, 0, 0.6, 1.2].map((x, i) => (
|
|
759
|
+
<Emisivo key={i} pos={[x, 0, 0]} tam={[0.42, 0.34, 0.05]} color={['#FF3B6B', '#FFD23D', '#3DDC84', '#3DB8FF', '#B98BFF'][i]} fuerza={2 * (Math.floor(t * 2.5) % 6 === i ? 0.45 : 1)} />
|
|
760
|
+
))}
|
|
761
|
+
<pointLight position={[0, 0, 1]} color="#FF6B9A" intensity={2.2} distance={6} decay={1.5} />
|
|
762
|
+
</group>
|
|
763
|
+
<pointLight position={[-0.6, 1.4, -2.2]} color="#FFC56B" intensity={2.4} distance={5.5} decay={1.5} />
|
|
764
|
+
{/* farola */}
|
|
765
|
+
<Bloque pos={[3.0, 1.8, -0.1]} tam={[0.1, 3.6, 0.1]} color="#2B2F3A" metal={0.5} />
|
|
766
|
+
<Bloque pos={[2.7, 3.58, -0.1]} tam={[0.7, 0.07, 0.1]} color="#2B2F3A" metal={0.5} />
|
|
767
|
+
<Emisivo pos={[2.45, 3.5, -0.1]} tam={[0.3, 0.08, 0.16]} color="#FFE2A8" fuerza={2.4} />
|
|
768
|
+
<pointLight position={[2.45, 3.2, -0.1]} color="#FFD9A0" intensity={3} distance={8} decay={1.4} castShadow />
|
|
769
|
+
{/* banca, bote e hidrante */}
|
|
770
|
+
<Bloque pos={[-2.9, 0.5, -2.3]} tam={[1.5, 0.08, 0.45]} color="#8A6A48" />
|
|
771
|
+
<Bloque pos={[-2.9, 0.85, -2.5]} tam={[1.5, 0.4, 0.06]} color="#8A6A48" />
|
|
772
|
+
{[-0.6, 0.6].map((l) => (
|
|
773
|
+
<Bloque key={l} pos={[-2.9 + l, 0.3, -2.3]} tam={[0.08, 0.36, 0.4]} color="#2B2F3A" />
|
|
774
|
+
))}
|
|
775
|
+
<mesh position={[-4.3, 0.55, -2.3]}>
|
|
776
|
+
<cylinderGeometry args={[0.24, 0.2, 0.8, 12]} />
|
|
777
|
+
<meshStandardMaterial color="#2F5D4A" roughness={0.8} />
|
|
778
|
+
</mesh>
|
|
779
|
+
<mesh position={[4.2, 0.42, -0.4]}>
|
|
780
|
+
<cylinderGeometry args={[0.13, 0.15, 0.56, 10]} />
|
|
781
|
+
<meshStandardMaterial color="#C62E3A" roughness={0.6} />
|
|
782
|
+
</mesh>
|
|
783
|
+
{/* coche estacionado */}
|
|
784
|
+
<group position={[-3.4, 0, 1.5]}>
|
|
785
|
+
<Bloque pos={[0, 0.5, 0]} tam={[2.5, 0.5, 1.1]} color="#2D6BFF" radio={0.08} />
|
|
786
|
+
<Bloque pos={[-0.1, 0.95, 0]} tam={[1.3, 0.42, 1.0]} color="#1E2A44" radio={0.08} />
|
|
787
|
+
<Emisivo pos={[1.26, 0.52, 0.36]} tam={[0.03, 0.12, 0.2]} color="#FFF2C8" fuerza={0.5} />
|
|
788
|
+
<Emisivo pos={[1.26, 0.52, -0.36]} tam={[0.03, 0.12, 0.2]} color="#FFF2C8" fuerza={0.5} />
|
|
789
|
+
<Emisivo pos={[-1.26, 0.55, 0.36]} tam={[0.03, 0.1, 0.22]} color="#FF3B30" fuerza={0.9} />
|
|
790
|
+
<Emisivo pos={[-1.26, 0.55, -0.36]} tam={[0.03, 0.1, 0.22]} color="#FF3B30" fuerza={0.9} />
|
|
791
|
+
{[
|
|
792
|
+
[-0.8, 0.56],
|
|
793
|
+
[0.8, 0.56],
|
|
794
|
+
[-0.8, -0.56],
|
|
795
|
+
[0.8, -0.56],
|
|
796
|
+
].map(([x, z], i) => (
|
|
797
|
+
<mesh key={i} position={[x, 0.26, z]} rotation={[Math.PI / 2, 0, 0]}>
|
|
798
|
+
<cylinderGeometry args={[0.26, 0.26, 0.16, 14]} />
|
|
799
|
+
<meshStandardMaterial color="#111216" roughness={0.9} />
|
|
800
|
+
</mesh>
|
|
801
|
+
))}
|
|
802
|
+
</group>
|
|
803
|
+
<pointLight position={[-3.2, 2.6, 3.4]} color="#8FA8FF" intensity={1.6} distance={6} decay={1.5} />
|
|
804
|
+
{/* luna y lluvia (entre las fachadas y las cámaras: una gota pegada al lente se ve como una barra) */}
|
|
805
|
+
<mesh position={[-7, 12, -16]}>
|
|
806
|
+
<sphereGeometry args={[1.1, 18, 14]} />
|
|
807
|
+
<meshStandardMaterial color="#F4F1DE" emissive="#F4F1DE" emissiveIntensity={0.9} toneMapped={false} />
|
|
808
|
+
</mesh>
|
|
809
|
+
{Array.from({length: 90}, (_, i) => {
|
|
810
|
+
const y = 6 - ((t * (5.5 + azarN(i * 1.9) * 2.5) + azarN(i * 4.7) * 6) % 6);
|
|
811
|
+
return <Emisivo key={i} pos={[(azarN(i * 2.3) - 0.5) * 12, y, -2.2 + azarN(i * 5.9) * 3.5]} tam={[0.012, 0.22, 0.012]} color="#BFD4FF" fuerza={0.55} />;
|
|
812
|
+
})}
|
|
813
|
+
</group>
|
|
814
|
+
);
|
|
815
|
+
|
|
630
816
|
const AMBIENTES: Record<NombreAmbiente, Def> = {
|
|
631
817
|
'cuarto-gamer': {
|
|
632
818
|
luz: 'noche',
|
|
@@ -731,6 +917,44 @@ const AMBIENTES: Record<NombreAmbiente, Def> = {
|
|
|
731
917
|
persona: {pos: [-1.6, 0, -0.88], rot: [0, 180, 0], pose: 'sentado'},
|
|
732
918
|
Set: SetOficina,
|
|
733
919
|
},
|
|
920
|
+
tienda: {
|
|
921
|
+
luz: 'noche',
|
|
922
|
+
fondo: '#0C0E1A',
|
|
923
|
+
niebla: [10, 24],
|
|
924
|
+
post: 'cine',
|
|
925
|
+
planos: {
|
|
926
|
+
general: {pos: [3.3, 2.6, 3.7], mirar: [-0.4, 1.3, -2.2], fov: 50, a: {pos: [2.8, 2.4, 3.1]}},
|
|
927
|
+
persona: {pos: [1.7, 1.6, -2.85], mirar: [0.4, 1.4, -1.0], fov: 44, a: {pos: [1.6, 1.58, -2.7]}},
|
|
928
|
+
estantes: {pos: [-1.3, 1.7, -0.3], mirar: [-1.5, 1.5, -3.3], fov: 46, a: {pos: [-1.5, 1.7, -0.6]}},
|
|
929
|
+
mostrador: {pos: [1.0, 1.9, 0.9], mirar: [2.7, 1.15, -1.2], fov: 46, a: {pos: [1.2, 1.85, 0.6]}},
|
|
930
|
+
tele: {pos: [2.1, 1.9, -0.1], mirar: [2.8, 2.15, -3.35], fov: 44, a: {pos: [2.2, 1.9, -0.4]}},
|
|
931
|
+
isla: {pos: [-1.1, 1.9, 2.4], mirar: [-2.6, 0.95, 0.6], fov: 44, a: {pos: [-1.3, 1.85, 2.1]}},
|
|
932
|
+
},
|
|
933
|
+
orden: ['general', 'persona', 'estantes', 'mostrador', 'tele', 'isla'],
|
|
934
|
+
ordenFondo: ['estantes', 'mostrador', 'tele', 'isla'],
|
|
935
|
+
anclas: {estantes: [-1.5, 1.6, -3.2], mostrador: [2.6, 1.2, -1.2], caja: [3.1, 1.4, -1.2], tele: [2.8, 1.95, -3.3], isla: [-2.6, 1.1, 0.6], persona: [0.4, 1.6, -1.0]},
|
|
936
|
+
persona: {pos: [0.4, 0, -1.0], rot: [0, 180, 0], pose: 'parado'},
|
|
937
|
+
Set: SetTienda,
|
|
938
|
+
},
|
|
939
|
+
calle: {
|
|
940
|
+
luz: 'noche',
|
|
941
|
+
fondo: '#0A1024',
|
|
942
|
+
niebla: [14, 38],
|
|
943
|
+
post: 'cine',
|
|
944
|
+
planos: {
|
|
945
|
+
general: {pos: [3.6, 2.3, 4.8], mirar: [-0.2, 1.7, -2.6], fov: 52, a: {pos: [3.0, 2.2, 4.2]}},
|
|
946
|
+
persona: {pos: [2.45, 1.6, -2.6], mirar: [1.3, 1.4, -0.6], fov: 44, a: {pos: [2.35, 1.58, -2.45]}},
|
|
947
|
+
vitrina: {pos: [-1.7, 1.7, 2.3], mirar: [-0.5, 1.85, -3], fov: 50, a: {pos: [-1.6, 1.7, 1.9]}},
|
|
948
|
+
edificios: {pos: [-1.6, 1.3, 3.0], mirar: [-4.6, 4.6, -3], fov: 52, a: {pos: [-1.8, 1.35, 2.7]}},
|
|
949
|
+
coche: {pos: [-0.3, 1.5, 4.7], mirar: [-3.4, 0.8, 1.5], fov: 50, a: {pos: [-0.5, 1.5, 4.4]}},
|
|
950
|
+
farola: {pos: [0.9, 1.1, 1.8], mirar: [2.6, 3.1, -0.4], fov: 52, a: {pos: [1.0, 1.15, 1.5]}},
|
|
951
|
+
},
|
|
952
|
+
orden: ['general', 'persona', 'vitrina', 'edificios', 'coche', 'farola'],
|
|
953
|
+
ordenFondo: ['vitrina', 'edificios', 'coche', 'farola'],
|
|
954
|
+
anclas: {vitrina: [-0.6, 1.4, -3.0], letrero: [0.1, 3.05, -3.0], farola: [2.45, 3.5, -0.1], coche: [-3.4, 0.9, 1.5], edificios: [-5.6, 5, -3.1], persona: [1.3, 1.6, -0.6]},
|
|
955
|
+
persona: {pos: [1.3, 0, -0.6], rot: [0, 180, 0], pose: 'parado'},
|
|
956
|
+
Set: SetCalle,
|
|
957
|
+
},
|
|
734
958
|
};
|
|
735
959
|
|
|
736
960
|
export const NOMBRES_AMBIENTES = Object.keys(AMBIENTES) as NombreAmbiente[];
|
package/remotion/kit/tres.tsx
CHANGED
|
@@ -270,14 +270,14 @@ const Post: React.FC<{post: Post3D; estado: Estado}> = ({post, estado}) => {
|
|
|
270
270
|
const dist = Math.hypot(estado.pos[0] - estado.mirar[0], estado.pos[1] - estado.mirar[1], estado.pos[2] - estado.mirar[2]);
|
|
271
271
|
const rango = typeof o.foco === 'number' ? o.foco : Math.max(1.2, dist * 0.55);
|
|
272
272
|
const efectos: React.ReactElement[] = [];
|
|
273
|
-
if (o.foco) efectos.push(<DepthOfField key="f" target={estado.mirar} worldFocusRange={rango} bokehScale={4} resolutionScale={0.
|
|
273
|
+
if (o.foco) efectos.push(<DepthOfField key="f" target={estado.mirar} worldFocusRange={rango} bokehScale={4} resolutionScale={0.5} />);
|
|
274
274
|
if (o.miniatura) efectos.push(<TiltShift2 key="m" blur={0.12 * o.miniatura} taper={0.6} start={[0.5, 0.0]} end={[0.5, 1.0]} />);
|
|
275
275
|
if (o.brillo) efectos.push(<Bloom key="b" intensity={o.brillo} luminanceThreshold={0.85} luminanceSmoothing={0.2} mipmapBlur />);
|
|
276
276
|
if (o.vinieta) efectos.push(<Vignette key="v" offset={0.3} darkness={o.vinieta} />);
|
|
277
277
|
if (!efectos.length) return null;
|
|
278
278
|
return (
|
|
279
279
|
<>
|
|
280
|
-
<EffectComposer multisampling={
|
|
280
|
+
<EffectComposer multisampling={2}>{efectos}</EffectComposer>
|
|
281
281
|
<RedibujarAlMontar />
|
|
282
282
|
</>
|
|
283
283
|
);
|
package/remotion/motor/Video.tsx
CHANGED
|
@@ -475,7 +475,7 @@ export const VideoMotor: React.FC<{d: Datos; T: number; estilos?: Record<string,
|
|
|
475
475
|
</AbsoluteFill>
|
|
476
476
|
) : null}
|
|
477
477
|
{/* un ambiente puede ser claro (oficina): sombra arriba, donde van subtítulos y titular, para que el texto blanco se lea (en el cierre, todo el cuadro) */}
|
|
478
|
-
{conAmbiente ? <AbsoluteFill style={{background: e.tipo === 'cta' ? 'rgba(0,0,0,0.
|
|
478
|
+
{conAmbiente ? <AbsoluteFill style={{background: e.tipo === 'cta' ? 'rgba(0,0,0,0.55)' : 'linear-gradient(180deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.38) 30%, rgba(0,0,0,0) 46%)'}} /> : null}
|
|
479
479
|
{enTransicion && prev ? (
|
|
480
480
|
<AbsoluteFill style={estiloPrevio}>
|
|
481
481
|
<EscenaVista e={prev} t={prev.dur + t} tema={tema} semilla={semilla} sinFondo={conMundo} />
|
|
@@ -32,7 +32,21 @@ type P = Record<string, any>;
|
|
|
32
32
|
|
|
33
33
|
export const PLANTILLAS = ['capas', 'titular', 'dato', 'lista', 'telefono', 'comparar', 'eleccion', 'cta'];
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Capas de una plantilla. Sobre un ambiente 3D el fondo cambia de toma en toma (ventanal naranja, pared gris), así
|
|
37
|
+
* que los textos sueltos (sin caja ni contorno propio) llevan contorno oscuro y los chicos una sombra más fuerte.
|
|
38
|
+
*/
|
|
35
39
|
export function capasDe(tipo: string, p: P, tema: Tema, marca: Marca): Capa[] {
|
|
40
|
+
const capas = capasPlantilla(tipo, p, tema, marca);
|
|
41
|
+
if (!tema.enAmbiente) return capas;
|
|
42
|
+
return capas.map((c) => {
|
|
43
|
+
if (c.tipo !== 'texto' || c.fondo || c.contorno != null) return c;
|
|
44
|
+
if (c.fuente === 'texto' || c.fuente === 'mono') return {...c, sombra: c.sombra ?? '0 2px 6px rgba(0,0,0,0.85), 0 0 18px rgba(0,0,0,0.6)'};
|
|
45
|
+
return {...c, contorno: 8, colorContorno: '#0A0A0E'};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function capasPlantilla(tipo: string, p: P, tema: Tema, marca: Marca): Capa[] {
|
|
36
50
|
const extra: Capa[] = p.capas ?? [];
|
|
37
51
|
switch (tipo) {
|
|
38
52
|
case 'titular': {
|
|
@@ -144,12 +158,16 @@ export function capasDe(tipo: string, p: P, tema: Tema, marca: Marca): Capa[] {
|
|
|
144
158
|
const out: Capa[] = [{tipo: 'marca', tam: 210, x: 540, y: 640, entrada: 'rebote'}];
|
|
145
159
|
if (p.pide === 'comenta') {
|
|
146
160
|
const palabra = String(p.palabra ?? 'SÍ').toUpperCase();
|
|
161
|
+
// si "Escribe PALABRA" se parte en dos renglones, el texto crece hacia abajo (arriba está el avatar) y
|
|
162
|
+
// lo demás baja con él
|
|
163
|
+
const textoCta = String(p.texto ?? `Escribe *${palabra}*`);
|
|
164
|
+
const bajaCta = (renglonesDe(textoCta.replace(/\*/g, ''), tema.fuentes.titulo, 110, 940, tema.tituloMayusculas) - 1) * 110 * 0.525;
|
|
147
165
|
out.push(
|
|
148
|
-
{tipo: 'texto', texto:
|
|
149
|
-
...(p.sub ? [{tipo: 'texto', texto: p.sub, fuente: 'texto', tam: 44, peso: 700, color: tema.colores.suave, x: 540, y: 1010, entrada: 'aparecer', en: 0.4, mayusculas: false}] : []),
|
|
166
|
+
{tipo: 'texto', texto: textoCta, x: 540, y: 900 + bajaCta, tam: 110, maxAncho: 940, entrada: 'subir', en: 0.2},
|
|
167
|
+
...(p.sub ? [{tipo: 'texto', texto: p.sub, fuente: 'texto', tam: 44, peso: 700, color: tema.colores.suave, x: 540, y: 1010 + bajaCta * 2, entrada: 'aparecer', en: 0.4, mayusculas: false}] : []),
|
|
150
168
|
// caja de comentario que se escribe sola y se envía
|
|
151
|
-
{tipo: 'terminal', x: 540, y: 1220, ancho: 760, tam: 46, titulo: 'comentario', fondo: caja, colorTexto: letra, en: 0.5, entrada: 'subir', color: tema.colores.acento, lineas: [{texto: palabra, en: 0.9}]},
|
|
152
|
-
{tipo: 'sello', texto: 'ENVIADO', x: 760, y: 1360, en: 2.1, color: tema.colores.acento, tam: 44},
|
|
169
|
+
{tipo: 'terminal', x: 540, y: 1220 + bajaCta * 2, ancho: 760, tam: 46, titulo: 'comentario', fondo: caja, colorTexto: letra, en: 0.5, entrada: 'subir', color: tema.colores.acento, lineas: [{texto: palabra, en: 0.9}]},
|
|
170
|
+
{tipo: 'sello', texto: 'ENVIADO', x: 760, y: 1360 + bajaCta * 2, en: 2.1, color: tema.colores.acento, tam: 44},
|
|
153
171
|
);
|
|
154
172
|
} else {
|
|
155
173
|
out.push(
|
package/remotion/motor/tema.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type Tema = {
|
|
|
31
31
|
};
|
|
32
32
|
fuentes: {titulo: string; texto: string; mono: string};
|
|
33
33
|
tituloMayusculas: boolean;
|
|
34
|
+
/** El video corre sobre una capa `ambiente` (lo pone el motor): los textos de las plantillas llevan contorno. */
|
|
35
|
+
enAmbiente?: boolean;
|
|
34
36
|
subtitulos: {
|
|
35
37
|
/**
|
|
36
38
|
* karaoke: frase arriba, lo dicho en un color y lo pendiente tenue · seleccion: igual, pero solo la palabra clave
|
|
@@ -274,5 +276,6 @@ export function temaDe(estilo: string | undefined, ajustes: Record<string, any>
|
|
|
274
276
|
return ESTILOS.minimal;
|
|
275
277
|
};
|
|
276
278
|
const base = resolver(estilo ?? 'minimal');
|
|
277
|
-
|
|
279
|
+
const tema = mezclar(oscuro && base.colores.texto !== '#FFFFFF' ? mezclar(base, SOBRE_NOCHE) : base, ajustes ?? {});
|
|
280
|
+
return oscuro ? {...tema, enAmbiente: true} : tema;
|
|
278
281
|
}
|