morfeo 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -21,6 +21,7 @@ la voz, la música, los efectos de sonido, los subtítulos, las animaciones y la
21
21
 
22
22
  - **Voces:** Kokoro (gratis, en tu computadora, más de 50 voces y mezclas), ElevenLabs, OpenAI, tu propia
23
23
  grabación por escena o cualquier servicio que conectes con una función.
24
+ - **Objetos 2D y 3D:** una biblioteca con diccionario temático en español (`{tipo: 'recurso', buscar: 'tubería', estilo: '3d'}`) que busca en Kenney, Poly Haven e Iconify (miles de modelos e ilustraciones libres), más objetos 3D y piezas de construcción hechos en código (tuberías con agua, muros, vigas…) y escenarios ilustrados animados. Los emojis quedan como último recurso.
24
25
  - **Música:** 7 estilos generados sin derechos de autor, o tu propia pista (mp3, wav, flac, ogg). Baja sola cuando habla la voz.
25
26
  - **Guiones:** los escribes tú, o Claude escribe el video completo a partir de un tema o de tu texto.
26
27
  - **Subtítulos:** palabra por palabra; con Whisper quedan sincronizados con la voz real.
package/bin/morfeo.mjs CHANGED
@@ -22,6 +22,8 @@ Videos
22
22
  --gpu usa la tarjeta de video (más rápido; si falla, quítalo)
23
23
  studio Abre el editor en el navegador para ver y ajustar en vivo
24
24
  estilos Lista los estilos visuales disponibles
25
+ recursos temas Diccionario de objetos 2D/3D por temática
26
+ recursos buscar "x" Busca un objeto (--3d, --realista, --2d --variante clay)
25
27
 
26
28
  Guiones
27
29
  guion "tema" La IA (Claude) escribe un video completo sobre el tema
@@ -46,7 +48,7 @@ function opciones(argv) {
46
48
  const [k, v] = a.slice(2).split('=');
47
49
  const clave = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
48
50
  const sig = argv[i + 1];
49
- const conValor = ['proyecto', 'desde', 'id', 'voz', 'duracion', 'idioma', 'estilo', 'tono', 'musica', 'titulo', 'modelo', 'puerto'];
51
+ const conValor = ['proyecto', 'desde', 'id', 'voz', 'duracion', 'idioma', 'estilo', 'tono', 'variante', 'fuente', 'musica', 'titulo', 'modelo', 'puerto'];
50
52
  if (v != null) op[clave] = v;
51
53
  else if (conValor.includes(clave) && sig != null && !sig.startsWith('--')) op[clave] = argv[++i];
52
54
  else op[clave] = true;
@@ -97,6 +99,29 @@ async function main() {
97
99
  const {studio} = await import('../lib/render.mjs');
98
100
  return studio({proyecto, puerto: op.puerto});
99
101
  }
102
+ case 'recursos':
103
+ case 'recurso': {
104
+ const [sub, ...resto] = pos;
105
+ const {buscarRecursos, VARIANTES_2D} = await import('../lib/recursos.mjs');
106
+ const {listaTemas, traducir} = await import('../lib/diccionario.mjs');
107
+ if (sub === 'temas' || !sub) {
108
+ for (const [t, terminos] of Object.entries(listaTemas())) console.log(` ${t.padEnd(14)} ${terminos.map((x) => x.replace(/_/g, ' ')).join(', ')}`);
109
+ console.log(`\nEstilos: 2d (variantes: ${Object.keys(VARIANTES_2D).join(', ')}), 3d, realista`);
110
+ console.log('Busca con: npx morfeo recursos buscar "tubería" --3d (o --2d --variante clay, o --realista)');
111
+ return;
112
+ }
113
+ if (sub !== 'buscar') throw new ErrorMorfeo('Uso: npx morfeo recursos temas | npx morfeo recursos buscar "término" [--2d|--3d|--realista] [--variante clay]');
114
+ const termino = resto.join(' ').trim();
115
+ if (!termino) throw new ErrorMorfeo('¿Qué busco? npx morfeo recursos buscar "tubería"');
116
+ const estilo = op['2d'] ? '2d' : op.realista ? 'realista' : '3d';
117
+ const t = traducir(termino);
118
+ console.log(`Buscando "${termino}" (${estilo}${op.variante ? ' ' + op.variante : ''}) → ${t.buscar.join(', ')}${t.tema ? ` · tema ${t.tema}` : ''}\n`);
119
+ const r = await buscarRecursos(termino, {estilo, variante: op.variante, fuente: op.fuente});
120
+ if (!r.length) return console.log('Sin resultados. Prueba otra palabra u otro estilo.');
121
+ r.slice(0, 15).forEach((x, i) => console.log(` ${String(i).padStart(2)} ${x.nombre.padEnd(30).slice(0, 30)} ${x.fuente.padEnd(10)} ${String(x.coleccion).padEnd(26).slice(0, 26)} ${x.licencia}`));
122
+ console.log(`\nÚsalo: {tipo: 'recurso', buscar: '${termino}', estilo: '${estilo}'${op.variante ? `, variante: '${op.variante}'` : ''}} (indice: n para elegir otro)`);
123
+ return;
124
+ }
100
125
  case 'estilos': {
101
126
  const {ESTILOS} = await import('../lib/config.mjs');
102
127
  const desc = {
package/index.d.ts CHANGED
@@ -23,6 +23,9 @@ interface EscenaBase {
23
23
 
24
24
  export type Estilo = 'datos-curiosos' | 'explicativo' | 'documental' | 'ilustrado' | 'historia' | 'mesa' | 'serie' | 'infografia' | '3d' | 'minimal' | (string & {});
25
25
 
26
+ /** Objetos 3D incluidos para capas {tipo: 'modelo', forma}: mejor opción que un emoji. */
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';
28
+
26
29
  /** Momento: segundos desde que empieza la escena, una palabra de `say` ("orcas"), "voz", "fin", o "palabra+0.3". */
27
30
  export type Momento = number | string;
28
31
  export type Entrada = 'escala' | 'rebote' | 'aparecer' | 'subir' | 'caer' | 'girar' | 'deslizar-izq' | 'deslizar-der' | 'deslizar-arriba' | 'deslizar-abajo' | 'ninguna';
@@ -74,7 +77,7 @@ export type Capa = CapaBase &
74
77
  | {tipo: 'marca'; tam?: number}
75
78
  | {tipo: 'particulas'; particulas?: 'burbujas' | 'polvo' | 'estrellas' | 'nieve' | 'confeti'; cantidad?: number; color?: string}
76
79
  | {tipo: 'grupo'; capas?: Capa[]; capa?: Capa; repetir?: number; disposicion?: 'circulo' | 'fila' | 'columna' | 'cuadricula' | 'pila' | 'aleatorio'; radio?: number; espacio?: number; espacioV?: number; columnas?: number; orientar?: boolean | number; retraso?: number; area?: [number, number]; girarAzar?: number; desorden?: number}
77
- | {tipo: 'modelo'; src?: string; forma?: 'moneda' | 'billete' | 'cubo' | 'esfera' | 'lingote'; color?: string; metal?: number; rugosidad?: number; ancho?: number; alto?: number; tamano?: number; rotacion?: [number, number, number]; girar3d?: [number, number, number]; cantidad?: number; crecer?: boolean; duracion?: number; distancia?: number; camaraY?: number; luz?: number; reflejos?: number}
80
+ | {tipo: 'modelo'; src?: string; forma?: 'moneda' | 'billete' | 'cubo' | 'esfera' | 'lingote' | Objeto3D; color?: string; metal?: number; rugosidad?: number; ancho?: number; alto?: number; tamano?: number; rotacion?: [number, number, number]; girar3d?: [number, number, number]; cantidad?: number; crecer?: boolean; duracion?: number; distancia?: number; camaraY?: number; luz?: number; reflejos?: number; /** levanta hacia la cámara los modelos planos acostados (por defecto en recursos) */ levantar?: boolean; /** tuberia */ puntos?: [number, number, number][]; radio?: number; material?: 'pvc' | 'cobre' | 'acero' | 'vidrio'; agua?: boolean; velocidad?: number; colorAgua?: string; gotas?: number; /** muro */ columnas?: number; filas?: number; colorLadrillo?: string; colorMezcla?: string; /** viga */ largo?: number; vertical?: boolean; /** escalera */ escalones?: number; /** suelo */ capas?: [string, number][]}
78
81
  | {tipo: 'componente'; nombre: string; props?: Record<string, unknown>}
79
82
  | {tipo: 'personaje'; nombre: string; pose?: string; poses?: {en: Momento; pose: string}[]; hablar?: boolean; ancho?: number}
80
83
  | {tipo: 'escena3d'; src: string; camara?: {en?: Momento; pos: [number, number, number]; mirar?: [number, number, number]; fov?: number}[]; animacion?: boolean | string | string[]; velocidad?: number; mostrar?: {nodo: string; en: Momento}[]; ocultar?: {nodo: string; en: Momento}[]; fondo?: string | null; niebla?: [number, number]; luz?: number; luzAmbiente?: number; sol?: [number, number, number]; reflejos?: number; sombras?: boolean; ancho?: number; alto?: number}
@@ -87,6 +90,8 @@ export type Capa = CapaBase &
87
90
  | {tipo: 'onda'; barras?: number; ancho?: number; alto?: number; color?: string; siempre?: boolean}
88
91
  | {tipo: 'globo'; texto: string; cola?: 'abajo' | 'arriba' | 'izq' | 'der' | 'ninguna'; fondo?: string; color?: string; borde?: string; tam?: number; maxAncho?: number}
89
92
  | {tipo: 'seguir'; usuario?: string; texto?: string; pulsar?: number}
93
+ | {tipo: 'recurso'; buscar: string; estilo?: '2d' | '3d' | 'realista'; variante?: 'color' | 'clay' | 'glass' | 'kawaii' | 'retro' | 'pastel' | 'sticker' | 'skeuo' | 'plush' | 'anime' | 'luxe' | 'chrome' | 'liquid' | 'bauhaus' | 'brutal' | 'gothic' | 'linea' | 'plano'; fuente?: 'kenney' | 'polyhaven' | 'iconify' | 'polypizza'; indice?: number; color?: string; ancho?: number; alto?: number; girar3d?: [number, number, number]; rotacion?: [number, number, number]}
94
+ | {tipo: 'escenario'; nombre: Escenario; parte?: 'fondo' | 'frente'; [opcion: string]: unknown}
90
95
  | {tipo: 'ventana'; diametro?: number; src?: string; capas?: Capa[]; borde?: 'metal' | 'ninguno' | (string & {}); grosor?: number; remaches?: number | false; zoom?: number | [number, number]; fondo?: string}
91
96
  );
92
97
 
@@ -94,7 +99,11 @@ export type FondoEscena =
94
99
  | {tipo: 'color'; color: string}
95
100
  | {tipo: 'degradado'; colores: string[]; angulo?: number}
96
101
  | {tipo: 'imagen'; src?: string; generar?: string; oscurecer?: number; desenfoque?: number}
97
- | {tipo: 'video'; src: string; oscurecer?: number};
102
+ | {tipo: 'video'; src: string; oscurecer?: number}
103
+ | {tipo: 'escenario'; nombre: Escenario; oscurecer?: number; [opcion: string]: unknown};
104
+
105
+ /** Escenarios ilustrados incluidos (animados). bosque: paneo, luna {cara, cantar, bajar, a}; cuarto: dia, parte; tunel: llegar; pasillo: golpe. */
106
+ export type Escenario = 'bosque' | 'cuarto' | 'departamento' | 'tunel' | 'pasillo';
98
107
 
99
108
  interface EscenaMotorBase extends EscenaBase {
100
109
  capas?: Capa[];
@@ -110,11 +119,11 @@ interface EscenaMotorBase extends EscenaBase {
110
119
  export type EscenaMotor =
111
120
  | (EscenaMotorBase & {type?: 'capas'})
112
121
  | (EscenaMotorBase & {type: 'titular'; texto: string; etiqueta?: string; numero?: number | string; sub?: string; y?: number; tam?: number})
113
- | (EscenaMotorBase & {type: 'dato'; valor: number; de?: number; prefijo?: string; sufijo?: string; decimales?: number; curva?: 'suave' | 'lineal' | 'exponencial'; formato?: 'grande' | 'lcd' | 'chip'; texto?: string; imagen?: string | {generar: string}; tam?: number; y?: number; en?: Momento; duracion?: number})
122
+ | (EscenaMotorBase & {type: 'dato'; valor: number; de?: number; prefijo?: string; sufijo?: string; decimales?: number; curva?: 'suave' | 'lineal' | 'exponencial'; formato?: 'grande' | 'lcd' | 'chip'; texto?: string; objeto?: Objeto3D; imagen?: string | {generar: string}; tam?: number; y?: number; en?: Momento; duracion?: number})
114
123
  | (EscenaMotorBase & {type: 'lista'; titulo?: string; cabecera?: string; items: (string | {texto: string; check?: Momento | boolean})[]; ancho?: number; y?: number})
115
124
  | (EscenaMotorBase & {type: 'telefono'; src?: string; capasPantalla?: Capa[]; texto?: string; ancho?: number; y?: number})
116
125
  | (EscenaMotorBase & {type: 'comparar'; a: string | {texto: string; imagen?: string; color?: string}; b: string | {texto: string; imagen?: string; color?: string}; at?: string})
117
- | (EscenaMotorBase & {type: 'eleccion'; pregunta?: string; opciones: (string | {texto: string; sub?: string; emoji?: string; icono?: Icono; imagen?: string; color?: string; fondo?: string; en?: Momento})[]; formato?: 'cartas' | 'botones'; comenta?: string | false; enComenta?: Momento; y?: number})
126
+ | (EscenaMotorBase & {type: 'eleccion'; pregunta?: string; opciones: (string | {texto: string; sub?: string; objeto?: Objeto3D; emoji?: string; icono?: Icono; imagen?: string; color?: string; fondo?: string; en?: Momento})[]; formato?: 'cartas' | 'botones'; comenta?: string | false; enComenta?: Momento; y?: number})
118
127
  | (Partial<EscenaMotorBase> & {type: 'cta'});
119
128
 
120
129
  export interface Tema {
package/lib/config.mjs CHANGED
@@ -23,7 +23,9 @@ export const TIPOS_CLASICOS = ['title', 'year', 'text', 'icon', 'compare', 'card
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
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'];
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'];
27
+ export const ESCENARIOS = ['bosque', 'cuarto', 'departamento', 'tunel', 'pasillo'];
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'];
27
29
  export const PROVEEDORES_VOZ = ['kokoro', 'elevenlabs', 'openai'];
28
30
  export const MUSICAS = ['lofi', 'lofi2', 'chillpop', 'synth', 'house', 'ballad', 'trap'];
29
31
 
@@ -106,7 +108,7 @@ export async function cargarProyecto(proyecto = process.cwd()) {
106
108
  const estilosPropios = cfg.estilos ?? {};
107
109
  const estilo = cfg.estilo ?? 'datos-curiosos';
108
110
  const assetsDir = path.resolve(proyecto, cfg.assets ?? 'assets');
109
- const ctx = {proyecto, marca, voces: cfg.voces ?? {}, musicas: cfg.musicas ?? {}, errores, estilo, estilosPropios, assetsDir};
111
+ const ctx = {proyecto, marca, voces: cfg.voces ?? {}, musicas: cfg.musicas ?? {}, errores, estilo, estilosPropios, assetsDir, personajes: cfg.personajes ?? {}};
110
112
  if (!ESTILOS.includes(estilo) && !estilosPropios[estilo]) errores.push(`estilo "${estilo}" no existe. Opciones: ${[...ESTILOS, ...Object.keys(estilosPropios)].join(', ')}`);
111
113
  const componentes = {};
112
114
  for (const [nombre, ruta] of Object.entries(cfg.componentes ?? {})) {
@@ -155,6 +157,32 @@ export async function cargarProyecto(proyecto = process.cwd()) {
155
157
  };
156
158
  }
157
159
 
160
+ /** Capas {tipo: 'personaje'} que piden un personaje (o una pose) que no está en el config. */
161
+ function revisarPersonajes(x, personajes, err) {
162
+ if (Array.isArray(x)) return x.forEach((v) => revisarPersonajes(v, personajes, err));
163
+ if (!x || typeof x !== 'object') return;
164
+ if (x.tipo === 'personaje') {
165
+ const def = personajes[x.nombre];
166
+ if (!def) err(`el personaje "${x.nombre}" no existe. Defínelo en morfeo.config.mjs → personajes. Hay: ${Object.keys(personajes).join(', ') || 'ninguno'}`);
167
+ else {
168
+ const poses = Object.keys(def.poses ?? {});
169
+ for (const p of [x.pose, ...(x.poses ?? []).map((k) => k?.pose)].filter(Boolean))
170
+ if (!poses.includes(p)) err(`el personaje "${x.nombre}" no tiene la pose "${p}". Tiene: ${poses.join(', ')}`);
171
+ }
172
+ }
173
+ for (const v of Object.values(x)) if (v && typeof v === 'object') revisarPersonajes(v, personajes, err);
174
+ }
175
+
176
+ /** Fondos y capas {tipo: 'escenario'} con un nombre que no existe. */
177
+ function revisarEscenarios(x, err) {
178
+ if (Array.isArray(x)) return x.forEach((v) => revisarEscenarios(v, err));
179
+ if (!x || typeof x !== 'object') return;
180
+ if (x.tipo === 'escenario' && !ESCENARIOS.includes(x.nombre)) err(`el escenario "${x.nombre}" no existe. Hay: ${ESCENARIOS.join(', ')}`);
181
+ if (x.tipo === 'modelo' && x.forma && !x.src && !FORMAS_3D.includes(x.forma)) err(`la forma 3D "${x.forma}" no existe. Hay: ${FORMAS_3D.join(', ')}`);
182
+ if (x.objeto && typeof x.objeto === 'string' && !FORMAS_3D.includes(x.objeto)) err(`el objeto 3D "${x.objeto}" no existe. Hay: ${FORMAS_3D.join(', ')}`);
183
+ for (const v of Object.values(x)) if (v && typeof v === 'object') revisarEscenarios(v, err);
184
+ }
185
+
158
186
  const CAMPOS_ARCHIVO = new Set(['src', 'imagen']);
159
187
  /** Busca `src`/`imagen` que apunten a assets/ y avisa de los que no existen. */
160
188
  function revisarArchivos(x, assetsDir, err, ruta = '') {
@@ -175,6 +203,10 @@ function validarCapas(capas, donde, err) {
175
203
  if (!c || typeof c !== 'object') return err(`${d} debe ser un objeto`);
176
204
  if (!TIPOS_CAPA.includes(c.tipo)) return err(`${d}: tipo "${c.tipo}" no existe. Opciones: ${TIPOS_CAPA.join(', ')}`);
177
205
  if ((c.tipo === 'imagen' || c.tipo === 'video') && !c.src && !c.generar) err(`${d}: falta \`src\` (archivo en assets/) o \`generar\` (descripción para la IA)`);
206
+ if (c.tipo === 'recurso') {
207
+ if (!c.buscar) err(`${d}: falta \`buscar\` (qué objeto: 'tubería', 'casa', 'pizza'...)`);
208
+ if (c.estilo && !['2d', '3d', 'realista'].includes(c.estilo)) err(`${d}: estilo "${c.estilo}" no existe. Usa '2d', '3d' o 'realista'`);
209
+ }
178
210
  if (c.capas) validarCapas(c.capas, d, err);
179
211
  if (c.capa) validarCapas([c.capa], d, err);
180
212
  });
@@ -252,7 +284,8 @@ function validarVideo(id, v, ctx) {
252
284
  const scenes = v.scenes.map((s0, i) => {
253
285
  const n = `escena ${i + 1}`;
254
286
  if (!s0 || typeof s0 !== 'object') return err(`${n} debe ser un objeto`), s0;
255
- const s = {...s0, type: s0.type ?? (s0.capas ? 'capas' : undefined)};
287
+ // sin `type`: escena de capas (con o sin capas: el fondo y los subtítulos mientras habla)
288
+ const s = {...s0, type: s0.type ?? 'capas'};
256
289
  if (!permitidos.includes(s.type))
257
290
  err(
258
291
  TIPOS_CLASICOS.includes(s.type)
@@ -280,5 +313,7 @@ function validarVideo(id, v, ctx) {
280
313
  if (v.hud != null) validarCapas(v.hud, 'hud', err);
281
314
  // archivos de assets/ que piden las escenas y el HUD: mejor avisar ahora que a mitad del render
282
315
  revisarArchivos({scenes, hud: v.hud, tema: v.tema}, ctx.assetsDir, err);
316
+ revisarPersonajes({scenes, hud: v.hud}, ctx.personajes, err);
317
+ revisarEscenarios({scenes, hud: v.hud, tema: v.tema}, err);
283
318
  return {...v, estilo, voice, music: normalizarMusica(v.music, ctx, err), scenes};
284
319
  }
@@ -0,0 +1,132 @@
1
+ // Diccionario temático: términos en español → búsquedas en inglés para las bibliotecas de recursos.
2
+ // Sirve para {tipo: 'recurso', buscar: 'tubería'} y para que la IA elija ilustraciones según la temática.
3
+ // Cada término puede tener varias búsquedas (de la más específica a la más general).
4
+ // Si un término no está, se busca tal cual (y en inglés si viene en inglés).
5
+
6
+ export const TEMAS = {
7
+ objetos: {
8
+ barril: ['barrel'], caja: ['box', 'crate'], bolsa: ['bag'], maleta: ['suitcase'], paraguas: ['umbrella'], sombrero: ['hat'], gorra: ['cap'],
9
+ lentes: ['glasses'], zapato: ['shoe'], camisa: ['shirt'], reloj_de_pulsera: ['watch'], anillo: ['ring'], bolso: ['handbag', 'bag'], cubeta: ['bucket'],
10
+ escoba: ['broom'], tijeras: ['scissors'], cuerda: ['rope'], cadena: ['chain'], iman: ['magnet'], brujula: ['compass'], mapa: ['map'], sobre: ['envelope'],
11
+ carta_correo: ['letter', 'envelope'], paquete: ['package', 'parcel', 'box'], etiqueta: ['tag', 'label'], campana: ['bell'], trompeta: ['trumpet'],
12
+ guitarra: ['guitar'], piano: ['piano'], tambor: ['drum'], pincel: ['paintbrush', 'brush'], pintura: ['paint'], cuadro: ['painting', 'picture frame'],
13
+ vela_aromatica: ['candle'], jarron: ['vase'], maceta: ['pot', 'plant pot'], tetera: ['teapot'], olla: ['pot', 'cooking pot'], sarten: ['pan', 'frying pan'],
14
+ cuchara: ['spoon'], tenedor: ['fork'], frasco: ['jar'], lata: ['can'], extintor: ['fire extinguisher'], botiquin: ['first aid kit'], escalera_mano: ['ladder'],
15
+ silla_ruedas: ['wheelchair'], muleta: ['crutch'], carrito: ['cart', 'shopping cart'], tienda_campana: ['tent'], fogata: ['campfire'], hacha: ['axe'],
16
+ arco: ['bow'], flecha: ['arrow'], bote_basura: ['trash can', 'bin'], periodico: ['newspaper'], revista: ['magazine'], boleto: ['ticket'], pasaporte: ['passport'],
17
+ },
18
+ hogar: {
19
+ casa: ['house', 'home'], cuarto: ['bedroom', 'room'], cama: ['bed'], sillon: ['sofa', 'couch', 'armchair'], sofa: ['sofa', 'couch'],
20
+ silla: ['chair'], mesa: ['table'], escritorio: ['desk'], lampara: ['lamp'], librero: ['bookcase', 'shelf'], refrigerador: ['fridge', 'refrigerator'],
21
+ estufa: ['stove', 'oven'], horno: ['oven'], lavabo: ['sink', 'bathroom sink'], fregadero: ['kitchen sink', 'sink'], inodoro: ['toilet'], banera: ['bathtub'],
22
+ regadera: ['shower'], espejo: ['mirror'], puerta: ['door'], ventana: ['window'], televisor: ['television', 'tv', 'monitor'], television: ['television', 'tv', 'monitor'], tele: ['television', 'tv', 'monitor'], tv: ['tv', 'television', 'monitor'], planta: ['plant', 'potted plant'],
23
+ alfombra: ['rug'], ropero: ['wardrobe', 'closet'], buro: ['nightstand'], cajon: ['drawer', 'cabinet'], lavadora: ['washer', 'washing machine'], microondas: ['microwave'], licuadora: ['blender'], ventilador: ['fan'], almohada: ['pillow'], cortina: ['curtain'], reloj: ['clock'], llave: ['key'], candado: ['padlock', 'lock'], cocina: ['kitchen'], bano: ['bathroom'],
24
+ },
25
+ construccion: {
26
+ tuberia: ['pipe', 'pipes'], tubo: ['pipe'], valvula: ['valve'], llave_de_agua: ['faucet', 'tap'], drenaje: ['drain', 'sewer'], manguera: ['hose'],
27
+ ladrillo: ['brick'], muro: ['wall', 'brick wall'], pared: ['wall'], viga: ['beam', 'girder'], columna: ['column', 'pillar'], escalera: ['ladder', 'stairs'],
28
+ andamio: ['scaffold'], cemento: ['cement', 'concrete'], concreto: ['concrete'], grua: ['crane'], excavadora: ['excavator', 'digger'], casco: ['helmet', 'hard hat'],
29
+ martillo: ['hammer'], taladro: ['drill'], llave_inglesa: ['wrench'], desarmador: ['screwdriver'], sierra: ['saw'], pala: ['shovel'], carretilla: ['wheelbarrow'],
30
+ cono: ['traffic cone', 'cone'], barrera: ['barrier'], tunel: ['tunnel'], puente: ['bridge'], tanque: ['tank'], cable: ['cable', 'wire'], tornillo: ['screw', 'bolt'],
31
+ molino: ['windmill', 'building mill', 'mill'], granja: ['farm', 'barn'], granero: ['barn'], pozo: ['well'], muelle: ['dock', 'pier'], mina: ['mine'], castillo_piedra: ['castle'],
32
+ herramienta: ['tool', 'toolbox'], caja_de_herramientas: ['toolbox'], valla: ['fence'], cerca: ['fence'],
33
+ },
34
+ ciudad: {
35
+ edificio: ['building', 'skyscraper'], rascacielos: ['skyscraper'], tienda: ['shop', 'store'], calle: ['road', 'street'], semaforo: ['traffic light'],
36
+ farola: ['street light', 'lamp post'], banca: ['bench'], parada: ['bus stop'], hidrante: ['fire hydrant', 'hydrant'], basura: ['trash can', 'garbage'],
37
+ buzon: ['mailbox'], hospital_edificio: ['hospital'], escuela_edificio: ['school'], iglesia: ['church'], mercado: ['market'], torre: ['tower'], fuente: ['fountain'], letrero: ['sign', 'billboard'], estacion: ['station'], metro: ['subway', 'train'],
38
+ },
39
+ transporte: {
40
+ auto: ['car'], carro: ['car'], coche: ['car'], camion: ['truck'], autobus: ['bus'], ambulancia: ['ambulance'], patrulla: ['police car', 'police'],
41
+ bomberos: ['firetruck', 'fire truck'], taxi: ['taxi'], moto: ['motorcycle', 'motorbike'], bicicleta: ['bicycle', 'bike'], tren: ['train'], avion: ['airplane', 'plane'],
42
+ barco: ['boat', 'ship'], cohete: ['rocket'], helicoptero: ['helicopter'], submarino: ['submarine'],
43
+ },
44
+ naturaleza: {
45
+ arbol: ['tree'], pino: ['pine tree', 'pine'], flor: ['flower'], pasto: ['grass'], roca: ['rock', 'stone'], piedra: ['stone', 'rock'], montana: ['mountain'],
46
+ hongo: ['mushroom'], hoja: ['leaf'], tronco: ['log', 'tree trunk'], arbusto: ['bush', 'shrub'], cactus: ['cactus'], nube: ['cloud'], sol: ['sun'], luna: ['moon'],
47
+ lluvia: ['rain'], rayo: ['lightning', 'thunder'], fuego: ['fire', 'campfire'], agua: ['water'], rio: ['river'], lago: ['lake'], isla: ['island'], volcan: ['volcano'],
48
+ },
49
+ oceano: {
50
+ ballena: ['whale'], tiburon: ['shark'], pez: ['fish'], pulpo: ['octopus'], medusa: ['jellyfish'], delfin: ['dolphin'], tortuga: ['turtle'], cangrejo: ['crab'],
51
+ concha: ['shell', 'seashell'], coral: ['coral'], ancla: ['anchor'], faro: ['lighthouse'], burbuja: ['bubble'],
52
+ },
53
+ animales: {
54
+ perro: ['dog'], gato: ['cat'], oso: ['bear'], leon: ['lion'], tigre: ['tiger'], elefante: ['elephant'], caballo: ['horse'], vaca: ['cow'], cerdo: ['pig'],
55
+ oveja: ['sheep'], gallina: ['chicken'], pato: ['duck'], conejo: ['rabbit', 'bunny'], raton: ['mouse'], mono: ['monkey'], pajaro: ['bird'], buho: ['owl'],
56
+ serpiente: ['snake'], arana: ['spider'], abeja: ['bee'], mariposa: ['butterfly'], dinosaurio: ['dinosaur'], zorro: ['fox'], lobo: ['wolf'], pinguino: ['penguin'],
57
+ },
58
+ comida: {
59
+ manzana: ['apple'], platano: ['banana'], pizza: ['pizza'], hamburguesa: ['burger', 'hamburger'], papas: ['fries', 'french fries'], pan: ['bread'],
60
+ queso: ['cheese'], huevo: ['egg'], pastel: ['cake'], galleta: ['cookie'], helado: ['ice cream'], cafe: ['coffee', 'coffee cup'], taza: ['cup', 'mug'],
61
+ dona: ['donut', 'doughnut'], tacos: ['taco'], sandia: ['watermelon'], uva: ['grapes'], fresa: ['strawberry'], zanahoria: ['carrot'], tomate: ['tomato'],
62
+ carne: ['meat', 'steak'], pollo: ['chicken leg', 'chicken'], pescado: ['fish'], sopa: ['soup', 'bowl'], ramen: ['ramen', 'noodles'], refresco: ['soda', 'can'],
63
+ botella: ['bottle'], vaso: ['glass'], plato: ['plate'], cubiertos: ['cutlery', 'fork'],
64
+ },
65
+ dinero: {
66
+ dinero: ['money', 'cash'], moneda: ['coin'], billete: ['banknote', 'money'], cartera: ['wallet'], banco: ['bank'], tarjeta: ['credit card'], alcancia: ['piggy bank'],
67
+ oro: ['gold', 'gold bar'], diamante: ['diamond', 'gem'], grafica: ['chart', 'graph'], cofre: ['chest', 'treasure chest'], tesoro: ['treasure'], caja_fuerte: ['safe'],
68
+ },
69
+ tecnologia: {
70
+ computadora: ['computer', 'laptop'], laptop: ['laptop'], celular: ['phone', 'smartphone'], telefono: ['phone'], robot: ['robot'], chip: ['chip', 'microchip'],
71
+ bateria: ['battery'], enchufe: ['plug', 'socket'], foco: ['light bulb', 'bulb'], camara: ['camera'], audifonos: ['headphones'], microfono: ['microphone'],
72
+ control: ['gamepad', 'controller'], consola: ['game console', 'console', 'gamepad'], consola_portatil: ['handheld console', 'nintendo switch', 'tablet', 'gamepad'],
73
+ tablet: ['tablet'], monitor: ['monitor', 'screen'], pantalla: ['screen', 'monitor'], disco: ['disc', 'cd', 'compact disc'], cd: ['disc', 'cd', 'compact disc'], dvd: ['disc', 'dvd'], memoria_usb: ['usb', 'flash drive'], antena: ['antenna'], satelite: ['satellite'], dron: ['drone'], servidor: ['server'],
74
+ wifi: ['wifi'], nube_de_datos: ['cloud computing', 'cloud'], codigo: ['code'], teclado: ['keyboard'], raton_pc: ['computer mouse'],
75
+ },
76
+ espacio: {
77
+ planeta: ['planet'], estrella: ['star'], astronauta: ['astronaut'], ovni: ['ufo'], galaxia: ['galaxy'], meteorito: ['meteor', 'asteroid'], telescopio: ['telescope'],
78
+ nave: ['spaceship', 'spacecraft'], tierra: ['earth', 'globe'], marte: ['mars'], cometa: ['comet'],
79
+ },
80
+ cuerpo: {
81
+ corazon: ['heart'], cerebro: ['brain'], pulmon: ['lungs'], hueso: ['bone'], craneo: ['skull'], diente: ['tooth'], ojo: ['eye'], mano: ['hand'], musculo: ['muscle'],
82
+ sangre: ['blood', 'blood drop'], adn: ['dna'], virus: ['virus'], bacteria: ['bacteria'], pastilla: ['pill', 'medicine'], jeringa: ['syringe'], hospital: ['hospital'],
83
+ doctor: ['doctor'], termometro: ['thermometer'], estetoscopio: ['stethoscope'], cama_de_hospital: ['hospital bed'],
84
+ },
85
+ escuela: {
86
+ libro: ['book'], cuaderno: ['notebook'], lapiz: ['pencil'], pluma: ['pen'], mochila: ['backpack'], pizarron: ['blackboard', 'chalkboard'], regla: ['ruler'],
87
+ calculadora: ['calculator'], globo_terraqueo: ['globe'], microscopio: ['microscope'], matraz: ['flask', 'lab flask'], diploma: ['diploma'], birrete: ['graduation cap'],
88
+ },
89
+ deporte: {
90
+ balon: ['ball', 'football'], futbol: ['soccer ball', 'football'], basquetbol: ['basketball'], beisbol: ['baseball'], tenis: ['tennis'], trofeo: ['trophy'],
91
+ medalla: ['medal'], pesa: ['dumbbell'], bicicleta_estatica: ['exercise bike'], caminadora: ['treadmill'], silbato: ['whistle'], cronometro: ['stopwatch'],
92
+ golf: ['golf'], patineta: ['skateboard'], carrera: ['race', 'racing'],
93
+ },
94
+ misterio: {
95
+ fantasma: ['ghost'], calavera: ['skull'], tumba: ['grave', 'tombstone'], ataud: ['coffin'], vela: ['candle'], linterna: ['lantern', 'flashlight'], lupa: ['magnifying glass'],
96
+ huella: ['footprint', 'fingerprint'], cuchillo: ['knife'], arana_red: ['spider web'], murcielago: ['bat'], calabaza: ['pumpkin'], castillo: ['castle'], llave_antigua: ['old key', 'key'],
97
+ },
98
+ juegos: {
99
+ videojuego: ['video game', 'gamepad'], dado: ['dice'], carta: ['playing card', 'card'], ajedrez: ['chess'], moneda_juego: ['coin'], corona: ['crown'], espada: ['sword'],
100
+ escudo: ['shield'], cofre_juego: ['chest'], gema: ['gem', 'crystal'], pocion: ['potion'], bandera: ['flag'], meta: ['finish line', 'flag'],
101
+ cartucho: ['game cartridge', 'cartridge', 'sd card', 'floppy disk'], joystick: ['joystick', 'gamepad'], arcade: ['arcade machine', 'arcade', 'gamepad'], jefe_final: ['monster', 'dragon', 'skull'],
102
+ vida: ['heart', 'life'], nivel: ['level', 'block', 'platform'], bloque: ['block', 'cube', 'brick'], llave_juego: ['key'], hongo: ['mushroom'], estrella_juego: ['star'],
103
+ },
104
+ fiestas: {
105
+ regalo: ['gift', 'present'], globo: ['balloon'], pastel_cumpleanos: ['birthday cake', 'cake'], arbol_navidad: ['christmas tree'], nieve: ['snow', 'snowman'],
106
+ munneco_nieve: ['snowman'], fuegos_artificiales: ['fireworks'], confeti: ['confetti'], mascara: ['mask'],
107
+ },
108
+ };
109
+
110
+ /** Quita acentos, ñ → n, minúsculas y espacios → _ (las claves del diccionario están así). */
111
+ export const clave = (s) =>
112
+ String(s)
113
+ .normalize('NFD')
114
+ .replace(/[̀-ͯ]/g, '')
115
+ .toLowerCase()
116
+ .trim()
117
+ .replace(/\s+/g, '_');
118
+
119
+ const INDICE = new Map();
120
+ for (const [tema, terminos] of Object.entries(TEMAS)) for (const [t, q] of Object.entries(terminos)) if (!INDICE.has(t)) INDICE.set(t, {tema, termino: t, buscar: q});
121
+
122
+ /** Búsquedas en inglés para un término en español (o el término tal cual si no está). */
123
+ export function traducir(termino) {
124
+ const k = clave(termino);
125
+ if (INDICE.has(k)) return INDICE.get(k);
126
+ // plural simple: "tuberías" → "tuberia", "árboles" → "arbol"
127
+ for (const s of [k.replace(/es$/, ''), k.replace(/s$/, '')]) if (INDICE.has(s)) return INDICE.get(s);
128
+ return {tema: null, termino: k, buscar: [String(termino).trim()]};
129
+ }
130
+
131
+ /** Lista de temas con sus términos (para `morfeo recursos temas` y para la IA). */
132
+ export const listaTemas = () => Object.fromEntries(Object.entries(TEMAS).map(([t, v]) => [t, Object.keys(v)]));
package/lib/guion.mjs CHANGED
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import {ErrorMorfeo, MUSICAS} from './config.mjs';
7
+ import {listaTemas} from './diccionario.mjs';
7
8
 
8
9
  export const MODELO_GUION = 'claude-opus-5-5';
9
10
  export const TIPOS_IA = ['title', 'year', 'text', 'icon', 'compare', 'card', 'dialog'];
@@ -87,6 +88,9 @@ export async function esquema() {
87
88
  });
88
89
  }
89
90
 
91
+ // objetos 3D del catálogo de Morfeo (remotion/motor/objetos3d.tsx)
92
+ const OBJETOS = ['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'];
93
+
90
94
  const ESTILOS_MOTOR = ['explicativo', 'documental', 'ilustrado', 'historia', 'mesa', 'minimal'];
91
95
 
92
96
  const INSTRUCCIONES_MOTOR = `Eres guionista y director de arte de videos cortos verticales (YouTube Shorts / TikTok / Reels) de 25 a 60 segundos.
@@ -99,11 +103,19 @@ Tipos de escena:
99
103
  - dato: un número que cuenta hasta "valor" (con "prefijo"/"sufijo", p. ej. sufijo " m"), "texto" corto debajo. "curva": "exponencial" si crece muchísimo.
100
104
  - lista: tarjeta con "titulo" y 2-4 "items"; cada item se palomea cuando la voz dice su palabra "check".
101
105
  - comparar: "a" contra "b" (textos cortos); "at" es la palabra de "say" en la que aparece "b".
102
- - capas: una ilustración. Usa "emoji" (un emoji grande que represente la idea) o, si se pide arte, "imagen_ia" (descripción breve de UN objeto o personaje aislado),
106
+ - capas: una ilustración, en este orden de preferencia:
107
+ 1. "recurso": un objeto concreto de la biblioteca (palabra del diccionario de abajo, según la temática del video), con "recurso_estilo"
108
+ ("3d" low-poly, "realista" o "2d"); usa el mismo estilo en todo el video.
109
+ 2. "objeto": un objeto 3D simbólico del catálogo (corazón, trofeo, cohete...).
110
+ 3. "imagen_ia" si se pide arte (descripción breve de UN objeto o personaje aislado).
111
+ 4. "emoji" SOLO si nada de lo anterior encaja (es el último recurso),
103
112
  más un "chip" opcional (dato corto como "×5" o "230 dB") que aparece en la palabra "en".
104
- - eleccion: final abierto para que el público comente: "pregunta" y 2 "opciones" ({texto, sub, emoji}). Úsala al final en series o historias.
113
+ - eleccion: final abierto para que el público comente: "pregunta" y 2 "opciones" ({texto, sub, objeto}; emoji solo si ningún objeto encaja). Úsala al final en series o historias.
105
114
  Varía los tipos. "at", "check" y "en" deben ser palabras que aparezcan tal cual en "say".
106
- No incluyas la escena final de "sígueme": morfeo la agrega sola.`;
115
+ No incluyas la escena final de "sígueme": morfeo la agrega sola.
116
+
117
+ Diccionario de recursos por tema (usa estas palabras en "recurso"):
118
+ ${Object.entries(listaTemas()).map(([t, v]) => `- ${t}: ${v.join(', ')}`).join('\n')}`;
107
119
 
108
120
  export async function esquemaMotor() {
109
121
  const {z} = await import('zod');
@@ -123,12 +135,15 @@ export async function esquemaMotor() {
123
135
  a: z.string().nullable(),
124
136
  b: z.string().nullable(),
125
137
  at: z.string().nullable(),
138
+ recurso: z.string().nullable(),
139
+ recurso_estilo: z.enum(['3d', 'realista', '2d']).nullable(),
140
+ objeto: z.enum(OBJETOS).nullable(),
126
141
  emoji: z.string().nullable(),
127
142
  imagen_ia: z.string().nullable(),
128
143
  chip: z.string().nullable(),
129
144
  en: z.string().nullable(),
130
145
  pregunta: z.string().nullable(),
131
- opciones: z.array(z.object({texto: z.string(), sub: z.string(), emoji: z.string()})).nullable(),
146
+ opciones: z.array(z.object({texto: z.string(), sub: z.string(), objeto: z.enum(OBJETOS).nullable(), emoji: z.string().nullable()})).nullable(),
132
147
  });
133
148
  return z.object({id: z.string(), title: z.string(), music: z.enum(MUSICAS), scenes: z.array(escena)});
134
149
  }
@@ -160,12 +175,15 @@ function aEscenaMotor(e, conImagenes) {
160
175
  break;
161
176
  case 'eleccion':
162
177
  si('pregunta', e.pregunta);
163
- out.opciones = (e.opciones ?? []).slice(0, 3);
178
+ out.opciones = (e.opciones ?? []).slice(0, 3).map(({texto, sub, objeto, emoji}) => ({texto, sub, ...(objeto ? {objeto} : emoji ? {emoji} : {})}));
164
179
  break;
165
180
  case 'capas': {
166
181
  const capas = [];
167
- if (e.imagen_ia && conImagenes) capas.push({tipo: 'imagen', generar: e.imagen_ia, ancho: 640, x: 540, y: 1000, entrada: 'escala', flotar: true});
168
- else capas.push({tipo: 'emoji', texto: e.emoji || '✨', tam: 420, x: 540, y: 1000, entrada: 'rebote', flotar: 16});
182
+ // recurso de la biblioteca > objeto 3D > imagen con IA > emoji (último recurso)
183
+ if (e.recurso) capas.push({tipo: 'recurso', buscar: e.recurso, estilo: e.recurso_estilo ?? '3d', x: 540, y: 1000, entrada: 'rebote', flotar: 10});
184
+ else if (e.objeto) capas.push({tipo: 'modelo', forma: e.objeto, ancho: 680, alto: 680, x: 540, y: 1000, girar3d: [0, 40, 0], entrada: 'rebote', flotar: 12});
185
+ else if (e.imagen_ia && conImagenes) capas.push({tipo: 'imagen', generar: e.imagen_ia, ancho: 640, x: 540, y: 1000, entrada: 'escala', flotar: true});
186
+ else capas.push({tipo: 'modelo', forma: 'estrella', ancho: 600, alto: 600, x: 540, y: 1000, girar3d: [0, 40, 0], entrada: 'rebote'});
169
187
  if (e.chip) capas.push({tipo: 'chip', texto: e.chip, x: 760, y: 1280, en: e.en ?? 'fin-0.5', entrada: 'rebote', sonido: 'pop'});
170
188
  out.capas = capas;
171
189
  break;