morfeo 0.2.0 → 0.3.0

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 CHANGED
@@ -168,6 +168,7 @@ async function main() {
168
168
  serie: 'series con personaje y capítulos: HUD (días, barras), cartas VS, "comenta", subtítulos en bloque',
169
169
  infografia: 'infografía plana: medidores, barras, globos, pantalla dividida, rayos X',
170
170
  '3d': 'escenas 3D con recorrido de cámara (tu .glb), una palabra grande a la vez',
171
+ maqueta: 'diorama 3D low-poly con corte de tierra: una palabra al centro, etiquetas con flecha, cortes rápidos (kit diorama)',
171
172
  minimal: 'oscuro y limpio, para lo que sea',
172
173
  };
173
174
  for (const e of ESTILOS) console.log(` ${e.padEnd(16)} ${desc[e] ?? ''}`);
package/index.d.ts CHANGED
@@ -21,7 +21,7 @@ interface EscenaBase {
21
21
  audio?: string;
22
22
  }
23
23
 
24
- export type Estilo = 'datos-curiosos' | 'explicativo' | 'documental' | 'ilustrado' | 'historia' | 'mesa' | 'serie' | 'infografia' | '3d' | 'minimal' | (string & {});
24
+ export type Estilo = 'datos-curiosos' | 'explicativo' | 'documental' | 'ilustrado' | 'historia' | 'mesa' | 'serie' | 'infografia' | '3d' | 'maqueta' | 'minimal' | (string & {});
25
25
 
26
26
  /** Objetos 3D incluidos para capas {tipo: 'modelo', forma}: mejor opción que un emoji. */
27
27
  export type Objeto3D = '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';
@@ -84,6 +84,14 @@ export type Capa = CapaBase &
84
84
  | {tipo: 'terminal'; lineas: (string | {texto: string; en?: Momento; salida?: boolean; color?: string})[]; titulo?: string; ancho?: number; tam?: number; fondo?: string; velocidad?: number}
85
85
  | {tipo: 'nombre'; texto: string; sub?: string; tam?: number; fondo?: string; colorTexto?: string; borde?: string}
86
86
  | {tipo: 'accion'; texto: string; tam?: number; color?: string; fondo?: string; colorTexto?: string}
87
+ /** Tarjetas de datos (estilo maqueta): se llenan o palomean solas desde `en`. */
88
+ | {tipo: 'reloj'; de?: number; a?: number; max?: number; segmentos?: [number, number, string][]; etiqueta?: string | false; tam?: number; color?: string; duracion?: number}
89
+ | {tipo: 'cronometro'; de?: number; a?: number; max?: number; unidad?: 'seg' | 'min'; tam?: number; color?: string; duracion?: number}
90
+ | {tipo: 'termometro'; valor: number; de?: number; min?: number; max?: number; unidad?: string; alto?: number; color?: string; decimales?: number; mostrar?: boolean}
91
+ | {tipo: 'recipientes'; n?: number; valor?: number; forma?: 'garrafon' | 'vaso'; total?: string; color?: string; tamPieza?: number; duracion?: number}
92
+ | {tipo: 'ficha'; titulo?: string; foto?: string; campos: [string, string][]; ancho?: number; color?: string; fondoFoto?: string}
93
+ | {tipo: 'checklist'; items: (string | {texto: string; en?: Momento; pendiente?: boolean})[]; titulo?: string; tam?: number; color?: string; cada?: number}
94
+ | {tipo: 'mapa'; ruta?: [number, number][]; distancia?: string; ancho?: number; color?: string; fondo?: string; inclinar?: number; duracion?: number}
87
95
  | {tipo: 'cursor'; a: [number, number]; clic?: Momento; duracion?: number; tam?: number; color?: string}
88
96
  | {tipo: 'texto'; texto: string; tam?: number; color?: string; acento?: string; fuente?: 'titulo' | 'texto' | 'mono' | (string & {}); peso?: number; mayusculas?: boolean; contorno?: number; colorContorno?: string; fondo?: string; relleno?: string; radio?: number; maxAncho?: number | 'auto'; alinear?: 'center' | 'left' | 'right'; interlineado?: number; escribir?: boolean | number; subrayado?: boolean}
89
97
  | {tipo: 'contador'; de?: number; a: number; prefijo?: string; sufijo?: string; decimales?: number; miles?: string; decimal?: string; duracion?: number; curva?: 'suave' | 'lineal' | 'exponencial'; formato?: 'grande' | 'lcd' | 'chip'; tam?: number; color?: string; fondo?: string}
@@ -144,6 +152,9 @@ interface EscenaMotorBase extends EscenaBase {
144
152
  rotulo?: string;
145
153
  rotuloEn?: Momento;
146
154
  rotuloTam?: number;
155
+ /** 'normal' | 'contorno' (blanco con contorno, clave en acento) | 'pastilla' (caja oscura inclinada: ¡CUIDADO!). Por defecto, el del estilo. */
156
+ rotuloEstilo?: 'normal' | 'contorno' | 'pastilla';
157
+ rotuloInclinar?: number;
147
158
  /** Palabras clave de la escena para los subtítulos (la que lleva caja o color de acento en cada frase). */
148
159
  claves?: string[];
149
160
  /** Ajustes del tema solo para esta escena. */
@@ -171,7 +182,7 @@ export interface Tema {
171
182
  colores: {texto: string; acento: string; acento2: string; suave: string; tarjeta: string; tarjetaTexto: string; borde: string};
172
183
  fuentes: {titulo: string; texto: string; mono: string};
173
184
  tituloMayusculas: boolean;
174
- subtitulos: {tipo: 'karaoke' | 'seleccion' | 'pop' | 'bloque' | 'palabra' | 'caja' | 'ninguno'; y: number; tam: number; color: string; pendiente: string; resalte: string; maxPalabras: number; mayusculas?: boolean; contorno?: number; fondo?: string};
185
+ subtitulos: {tipo: 'karaoke' | 'seleccion' | 'pop' | 'bloque' | 'palabra' | 'caja' | 'ninguno'; y: number; tam: number; color: string; pendiente: string; resalte: string; maxPalabras: number; mayusculas?: boolean | 'inicial'; contorno?: number; fondo?: string};
175
186
  transicion: Transicion;
176
187
  /** Ritmo de las animaciones: duraciones (s) y curva de las entradas. */
177
188
  movimiento?: {entrada?: number; salida?: number; transicion?: number; curva?: 'suave' | 'resorte' | 'seca'};
@@ -182,6 +193,8 @@ export interface Tema {
182
193
  desenfoqueBordes?: number;
183
194
  vinetaColor?: string;
184
195
  };
196
+ /** Rótulo de cada escena: estilo 'normal' | 'contorno' | 'pastilla', inclinación, altura (px) y tamaño. */
197
+ rotulo?: {estilo?: 'normal' | 'contorno' | 'pastilla'; inclinar?: number; y?: number; tam?: number};
185
198
  marca: 'esquina' | 'arriba' | 'ninguna';
186
199
  zoomLento: number;
187
200
  }
@@ -233,6 +246,8 @@ export interface Video {
233
246
  mundo?: Capa[];
234
247
  /** Pastilla fija arriba para series: 'GYM CON CIENCIA · PARTE 5'. */
235
248
  serie?: string;
249
+ /** 'largo' = explicador de 90–130 s (lo asume el estilo maqueta): cambia las reglas de `morfeo revisar`. */
250
+ formato?: 'corto' | 'largo';
236
251
  /** Ajustes del estilo para este video (colores, fondo, subtítulos...). */
237
252
  tema?: Partial<Tema> & Record<string, unknown>;
238
253
  /** Título para publicar. */
package/lib/config.mjs CHANGED
@@ -22,8 +22,8 @@ const parecidas = (nombre, lista, n = 3) => {
22
22
  export const TIPOS_CLASICOS = ['title', 'year', 'text', 'icon', 'compare', 'card', 'dialog', 'aliens', 'memory', 'morph', 'cta'];
23
23
  export const TIPOS_MOTOR = ['capas', 'titular', 'dato', 'lista', 'telefono', 'comparar', 'eleccion', 'cta'];
24
24
  export const TIPOS_ESCENA = [...new Set([...TIPOS_CLASICOS, ...TIPOS_MOTOR])];
25
- export const ESTILOS = ['datos-curiosos', 'explicativo', 'documental', 'ilustrado', 'historia', 'mesa', 'serie', 'infografia', '3d', 'minimal'];
26
- export const TIPOS_CAPA = ['imagen', 'video', 'texto', 'contador', 'chip', 'etiqueta', 'numero', 'tarjeta', 'telefono', 'forma', 'icono', 'emoji', 'marca', 'particulas', 'grupo', 'modelo', 'componente', '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'];
25
+ export const ESTILOS = ['datos-curiosos', 'explicativo', 'documental', 'ilustrado', 'historia', 'mesa', 'serie', 'infografia', '3d', 'maqueta', 'minimal'];
26
+ export const TIPOS_CAPA = ['imagen', 'video', 'texto', 'contador', 'chip', 'etiqueta', 'numero', 'tarjeta', 'telefono', 'forma', 'icono', 'emoji', 'marca', 'particulas', 'grupo', 'modelo', 'componente', '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'];
27
27
  export const ESCENARIOS = ['bosque', 'cuarto', 'departamento', 'tunel', 'pasillo'];
28
28
  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'];
29
29
  export const PROVEEDORES_VOZ = ['kokoro', 'elevenlabs', 'openai'];
@@ -18,6 +18,12 @@
18
18
  // - Series: una pastilla fija arriba ("TITO DESDE CERO · EP. 1", "GYM CON CIENCIA · PARTE 5") y contadores persistentes.
19
19
  // - Cierre con una acción concreta, no solo "sígueme": elegir A o B en comentarios, "escribe PASO si entendiste",
20
20
  // "mándaselo al que dice que...", cuenta regresiva para la próxima parte.
21
+ //
22
+ // Formato largo "explicador de maqueta" (2026-11, 4 videos de 102–132 s: castor, tinaco, buey, caseta de cobro):
23
+ // - Un solo diorama 3D low-poly con corte de tierra; 24–25 cortes por minuto (uno cada ~2,4 s) entre ángulos del
24
+ // mismo mundo. Una palabra a la vez al centro, rótulos con contorno, etiquetas con flecha pegadas a los objetos.
25
+ // - Gancho "Imagina que eres un castor…" o "¿Cómo se hace una caseta de cobro?"; cierre "¡Felicidades, eres…!" o un
26
+ // VS de "ya lo sabía / no lo sabía". Aquí 90–130 s está bien: el mundo y los cortes sostienen la atención.
21
27
 
22
28
  /** Las reglas, en el orden en que se le explican a la IA y a quien escribe un guion. */
23
29
  export const REGLAS = [
@@ -30,7 +36,7 @@ export const REGLAS = [
30
36
  {id: 'escalada', regla: 'Que suba la apuesta', como: 'Cada escena más grave, más grande o más rara que la anterior; lo más fuerte cerca del final, no al principio.'},
31
37
  {id: 'cierre', regla: 'Cierre con una acción concreta', como: 'Pide algo fácil y específico: elegir A o B en comentarios, escribir una palabra ("escribe PASO"), mandárselo a alguien ("mándaselo al que dice que…") o esperar la parte 2. "Sígueme" solo no basta.'},
32
38
  {id: 'bucle', regla: 'Final que conecta con el inicio', como: 'La última frase puede regresar a la primera imagen o dejar una pregunta abierta, para que se vuelva a ver.'},
33
- {id: 'duracion', regla: 'Entre 30 y 65 segundos', como: 'La mayoría de los de referencia dura ~1 minuto. Más de 75 s solo si es una serie o una historia que lo sostiene.'},
39
+ {id: 'duracion', regla: 'Entre 30 y 65 segundos (o 90–130 en maqueta)', como: 'La mayoría de los de referencia dura ~1 minuto. Más de 75 s solo si es una serie, una historia o un explicador de maqueta (estilo maqueta: 90–130 s con un corte de cámara cada 2–3 s dentro del mismo mundo).'},
34
40
  ];
35
41
 
36
42
  /** Las reglas como texto para el prompt de la IA. */
@@ -38,7 +44,7 @@ export const REGLAS_TEXTO = `Estructura (medida en videos virales; síguela):
38
44
  ${REGLAS.map((r, i) => `${i + 1}. ${r.regla}: ${r.como}`).join('\n')}`;
39
45
 
40
46
  const palabrasDe = (s) => String(s ?? '').trim().split(/\s+/).filter(Boolean);
41
- const GANCHOS = /^(¿|imagina|y si|si tu|si te|si un|qué pasaría|que pasaria|este es|esta es|esto es|nadie|nunca|todos|a \S+ |hay |tu |tú |te |tienes|cuando)/i;
47
+ const GANCHOS = /^(¿|¡|imagina|y si|cómo|como se|si tu|si te|si un|qué pasaría|que pasaria|este es|esta es|esto es|nadie|nunca|todos|a \S+ |hay |tu |tú |te |tienes|cuando)/i;
42
48
 
43
49
  /**
44
50
  * Revisa la estructura de un video y devuelve consejos (no bloquea el render).
@@ -54,6 +60,8 @@ export function revisarEstructura(v, d) {
54
60
  if (!cuerpo.length) return out;
55
61
  const primera = cuerpo[0];
56
62
  const clasico = (v.estilo ?? 'datos-curiosos') === 'datos-curiosos';
63
+ // formato largo: explicador de maqueta (o `formato: 'largo'` en el video)
64
+ const largo = v.estilo === 'maqueta' || v.formato === 'largo';
57
65
 
58
66
  // 1) gancho
59
67
  const dicho = String(primera.say ?? '').trim();
@@ -67,7 +75,9 @@ export function revisarEstructura(v, d) {
67
75
  const es = d?.escenas?.filter((e) => e.tipo !== 'cta') ?? [];
68
76
  if (es.length) {
69
77
  const dur = d.duracion;
70
- if (dur > 75) add('duracion', 'idea', `Dura ${Math.round(dur)} s: los de referencia rondan 60 s. Si no es una serie o una historia, recorta.`);
78
+ if (largo && dur > 140) add('duracion', 'idea', `Dura ${Math.round(dur)} s: los explicadores de maqueta de referencia van de 100 a 130 s.`);
79
+ else if (largo && dur < 60) add('duracion', 'idea', `Dura ${Math.round(dur)} s: en maqueta los de referencia duran 100–130 s; hay espacio para otra pregunta y su respuesta.`);
80
+ else if (!largo && dur > 75) add('duracion', 'idea', `Dura ${Math.round(dur)} s: los de referencia rondan 60 s. Si no es una serie o una historia, recorta (o usa estilo maqueta para un explicador largo).`);
71
81
  if (dur < 20) add('duracion', 'idea', `Dura ${Math.round(dur)} s: un poco más de desarrollo (una pregunta y su respuesta) suele retener mejor.`);
72
82
  const e0 = es[0];
73
83
  if (e0 && e0.habla?.[0] > 0.5) add('gancho', 'aviso', `La voz tarda ${e0.habla[0].toFixed(1)} s en empezar: debe arrancar de inmediato.`);
@@ -77,7 +87,8 @@ export function revisarEstructura(v, d) {
77
87
  // algo que cambie a media escena: capas con `en`, cámara con rango, toma con movimiento, o un mundo vivo
78
88
  const capas = s.capas ?? [];
79
89
  const cambia = capas.some((c) => c.en != null && c.en !== 0 && c.en !== 'inicio') || Array.isArray(s.camara?.zoom) || s.toma?.a || capas.some((c) => c.mover || c.cambios);
80
- if (voz > 6 && !cambia && !v.mundo?.length) add('ritmo', 'aviso', `Escena ${i + 1}: ${voz.toFixed(1)} s de voz sin que nada cambie. Pártela en dos o haz que algo entre a media frase.`);
90
+ if (largo && voz > 4.5) add('ritmo', 'idea', `Escena ${i + 1}: ${voz.toFixed(1)} s en una sola toma; en maqueta la cámara corta cada 2–3 s. Pártela en dos tomas.`);
91
+ else if (voz > 6 && !cambia && !v.mundo?.length) add('ritmo', 'aviso', `Escena ${i + 1}: ${voz.toFixed(1)} s de voz sin que nada cambie. Pártela en dos o haz que algo entre a media frase.`);
81
92
  else if (voz > 9) add('ritmo', 'idea', `Escena ${i + 1}: ${voz.toFixed(1)} s de voz en una sola escena; en los de referencia ninguna idea pasa de ~5 s.`);
82
93
  });
83
94
  }
package/lib/instalar.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  venvListo,
23
23
  versionPython,
24
24
  } from './entorno.mjs';
25
- import {whisperListo, instalarWhisper} from './whisper.mjs';
25
+ import {faltaParaWhisper, whisperListo, instalarWhisper} from './whisper.mjs';
26
26
 
27
27
  const ok = (m) => console.log(` ✔ ${m}`);
28
28
  const no = (m) => console.log(` ✘ ${m}`);
@@ -154,7 +154,7 @@ export async function instalar(op = {}) {
154
154
  if (process.platform === 'darwin' && spawnSync('xcode-select', ['-p'], {stdio: 'ignore'}).status !== 0) {
155
155
  no('Whisper se compila y necesita las herramientas de Apple.');
156
156
  info('Corre: xcode-select --install y cuando termine: npx morfeo instalar --whisper');
157
- } else {
157
+ } else if (await herramientasWhisper(op)) {
158
158
  await instalarWhisper();
159
159
  ok('Whisper listo');
160
160
  }
@@ -164,6 +164,22 @@ export async function instalar(op = {}) {
164
164
  console.log('\nTodo listo. Prueba: npx morfeo nuevo mi-canal && cd mi-canal && npx morfeo render');
165
165
  }
166
166
 
167
+ /** Revisa git, make y cmake antes de compilar whisper.cpp; en Mac con Homebrew ofrece instalar cmake. */
168
+ async function herramientasWhisper(op) {
169
+ let falta = faltaParaWhisper();
170
+ if (falta.length === 1 && falta[0] === 'cmake' && process.platform === 'darwin' && tiene('brew')) {
171
+ if (await preguntar('Whisper necesita cmake para compilarse. ¿Lo instalo con Homebrew?', {...op, porDefecto: true})) {
172
+ correr('brew', ['install', 'cmake']);
173
+ falta = faltaParaWhisper();
174
+ }
175
+ }
176
+ if (!falta.length) return true;
177
+ no(`Whisper se compila y falta: ${falta.join(', ')}.`);
178
+ if (process.platform === 'darwin') info(`Corre: brew install ${falta.join(' ')} y cuando termine: npx morfeo instalar --whisper`);
179
+ else info(`Instálalo con tu gestor de paquetes (por ejemplo: sudo apt install ${falta.join(' ')} ) y luego: npx morfeo instalar --whisper`);
180
+ return false;
181
+ }
182
+
167
183
  /** Devuelve el estado de cada pieza; con imprimir=true lo muestra. */
168
184
  export async function doctor({imprimir = true} = {}) {
169
185
  const estado = [];
package/lib/whisper.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // Whisper (opcional): tiempos reales de cada palabra para los subtítulos.
2
2
  import {downloadWhisperModel, installWhisperCpp, toCaptions, transcribe} from '@remotion/install-whisper-cpp';
3
- import {execFileSync} from 'node:child_process';
3
+ import {execFileSync, spawnSync} from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import {WHISPER, pythonVenv} from './entorno.mjs';
@@ -10,8 +10,24 @@ export const MODELO_WHISPER = 'small';
10
10
 
11
11
  export const whisperListo = () => fs.existsSync(path.join(WHISPER, `ggml-${MODELO_WHISPER}.bin`)) && fs.existsSync(path.join(WHISPER, '.listo'));
12
12
 
13
+ /** Herramientas que whisper.cpp necesita para compilarse (en Windows se descarga ya compilado). */
14
+ export function faltaParaWhisper() {
15
+ if (process.platform === 'win32') return [];
16
+ return ['git', 'make', 'cmake'].filter((cmd) => spawnSync('which', [cmd], {stdio: 'ignore'}).status !== 0);
17
+ }
18
+
19
+ // Mismo lugar donde Remotion busca el ejecutable desde whisper.cpp 1.7.4.
20
+ const compilado = () => fs.existsSync(path.join(WHISPER, 'build', 'bin', process.platform === 'win32' ? 'whisper-cli.exe' : 'whisper-cli'));
21
+
13
22
  export async function instalarWhisper() {
14
- await installWhisperCpp({to: WHISPER, version: VERSION_CPP, printOutput: true});
23
+ // Un intento anterior que falló a medias deja la carpeta sin el ejecutable, y Remotion se niega a seguir: se limpia.
24
+ if (fs.existsSync(WHISPER) && !compilado()) fs.rmSync(WHISPER, {recursive: true, force: true});
25
+ try {
26
+ await installWhisperCpp({to: WHISPER, version: VERSION_CPP, printOutput: true});
27
+ } catch (e) {
28
+ if (!compilado()) fs.rmSync(WHISPER, {recursive: true, force: true});
29
+ throw e;
30
+ }
15
31
  await downloadWhisperModel({model: MODELO_WHISPER, folder: WHISPER, printOutput: true});
16
32
  fs.writeFileSync(path.join(WHISPER, '.listo'), VERSION_CPP);
17
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morfeo",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",