morfeo 0.1.0 → 0.1.2
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 +2 -1
- package/bin/morfeo.mjs +26 -1
- package/index.d.ts +17 -4
- package/lib/config.mjs +84 -6
- package/lib/diccionario.mjs +129 -0
- package/lib/guion.mjs +34 -12
- package/lib/recursos.mjs +299 -0
- package/lib/render.mjs +87 -27
- package/package.json +2 -2
- package/plantilla/videos.mjs +1 -0
- package/remotion/Short.tsx +6 -6
- package/remotion/motor/Video.tsx +8 -6
- package/remotion/motor/capas.tsx +88 -3
- package/remotion/motor/escenarios/Bosque.tsx +157 -0
- package/remotion/motor/escenarios/Cuarto.tsx +103 -0
- package/remotion/motor/escenarios/Noche.tsx +163 -0
- package/remotion/motor/escenarios/index.tsx +26 -0
- package/remotion/motor/fondo.tsx +18 -7
- package/remotion/motor/modelo3d.tsx +4 -1
- package/remotion/motor/objetos3d.tsx +548 -0
- package/remotion/motor/plantillas.tsx +10 -4
- package/remotion/motor/tema.ts +14 -8
- package/remotion/motor/widgets.tsx +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Morfeo
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **Documentación: [ureckchan.github.io/morfeo](https://ureckchan.github.io/morfeo/)** · Ejemplos: [github.com/UreckChan/morfeo](https://github.com/UreckChan/morfeo)
|
|
4
4
|
|
|
5
5
|
Genera videos verticales animados (Shorts, TikTok, Reels) desde código: escribes las escenas y Morfeo agrega
|
|
6
6
|
la voz, la música, los efectos de sonido, los subtítulos, las animaciones y la miniatura.
|
|
@@ -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; /** 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,13 +90,20 @@ 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'; 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}
|
|
95
|
+
| {tipo: 'ventana'; diametro?: number; src?: string; capas?: Capa[]; borde?: 'metal' | 'ninguno' | (string & {}); grosor?: number; remaches?: number | false; zoom?: number | [number, number]; fondo?: string}
|
|
90
96
|
);
|
|
91
97
|
|
|
92
98
|
export type FondoEscena =
|
|
93
99
|
| {tipo: 'color'; color: string}
|
|
94
100
|
| {tipo: 'degradado'; colores: string[]; angulo?: number}
|
|
95
101
|
| {tipo: 'imagen'; src?: string; generar?: string; oscurecer?: number; desenfoque?: number}
|
|
96
|
-
| {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';
|
|
97
107
|
|
|
98
108
|
interface EscenaMotorBase extends EscenaBase {
|
|
99
109
|
capas?: Capa[];
|
|
@@ -109,11 +119,11 @@ interface EscenaMotorBase extends EscenaBase {
|
|
|
109
119
|
export type EscenaMotor =
|
|
110
120
|
| (EscenaMotorBase & {type?: 'capas'})
|
|
111
121
|
| (EscenaMotorBase & {type: 'titular'; texto: string; etiqueta?: string; numero?: number | string; sub?: string; y?: number; tam?: number})
|
|
112
|
-
| (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})
|
|
113
123
|
| (EscenaMotorBase & {type: 'lista'; titulo?: string; cabecera?: string; items: (string | {texto: string; check?: Momento | boolean})[]; ancho?: number; y?: number})
|
|
114
124
|
| (EscenaMotorBase & {type: 'telefono'; src?: string; capasPantalla?: Capa[]; texto?: string; ancho?: number; y?: number})
|
|
115
125
|
| (EscenaMotorBase & {type: 'comparar'; a: string | {texto: string; imagen?: string; color?: string}; b: string | {texto: string; imagen?: string; color?: string}; at?: string})
|
|
116
|
-
| (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})
|
|
117
127
|
| (Partial<EscenaMotorBase> & {type: 'cta'});
|
|
118
128
|
|
|
119
129
|
export interface Tema {
|
|
@@ -182,6 +192,8 @@ export interface Video {
|
|
|
182
192
|
/** Idioma de Kokoro: "es", "en-us", "en-gb", "fr-fr", "it", "pt-br", "ja", "cmn", "hi". */
|
|
183
193
|
lang?: string;
|
|
184
194
|
scenes: Escena[];
|
|
195
|
+
/** Segundo del video para la miniatura (por defecto, cuando la primera escena ya está completa). Sale sin subtítulos. */
|
|
196
|
+
miniaturaEn?: number;
|
|
185
197
|
/** Datos libres que se copian a salida/resumen.json. */
|
|
186
198
|
meta?: Record<string, unknown>;
|
|
187
199
|
}
|
|
@@ -221,6 +233,7 @@ export interface Config {
|
|
|
221
233
|
musicas?: Record<string, Musica>;
|
|
222
234
|
/** Carpeta de los .mp4 (por defecto ./salida). */
|
|
223
235
|
salida?: string;
|
|
236
|
+
/** gl: por defecto 'angle' (GPU) en Mac con Apple Silicon y 'swangle' (CPU) en lo demás; si la GPU falla, reintenta con CPU. */
|
|
224
237
|
render?: {crf?: number; gl?: 'swangle' | 'angle' | 'egl' | 'swiftshader' | 'vulkan'; navegador?: string | null; concurrencia?: number | null; miniatura?: boolean};
|
|
225
238
|
}
|
|
226
239
|
|
package/lib/config.mjs
CHANGED
|
@@ -5,16 +5,39 @@ import {pathToFileURL} from 'node:url';
|
|
|
5
5
|
|
|
6
6
|
export const NOMBRES_CONFIG = ['morfeo.config.mjs', 'morfeo.config.js'];
|
|
7
7
|
|
|
8
|
+
// Voces de Kokoro v1.0 (archivo voices-v1.0.bin)
|
|
9
|
+
export const VOCES_KOKORO = 'af_alloy af_aoede af_bella af_heart af_jessica af_kore af_nicole af_nova af_river af_sarah af_sky am_adam am_echo am_eric am_fenrir am_liam am_michael am_onyx am_puck am_santa bf_alice bf_emma bf_isabella bf_lily bm_daniel bm_fable bm_george bm_lewis ef_dora em_alex em_santa ff_siwis hf_alpha hf_beta hm_omega hm_psi if_sara im_nicola jf_alpha jf_gongitsune jf_nezumi jf_tebukuro jm_kumo pf_dora pm_alex pm_santa zf_xiaobei zf_xiaoni zf_xiaoxiao zf_xiaoyi zm_yunjian zm_yunxi zm_yunxia zm_yunyang'.split(' ');
|
|
10
|
+
|
|
11
|
+
/** Las voces más parecidas a un nombre mal escrito (para sugerir). */
|
|
12
|
+
const parecidas = (nombre, lista, n = 3) => {
|
|
13
|
+
const d = (a, b) => {
|
|
14
|
+
const m = Array.from({length: a.length + 1}, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
15
|
+
for (let j = 1; j <= b.length; j++) m[0][j] = j;
|
|
16
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
17
|
+
return m[a.length][b.length];
|
|
18
|
+
};
|
|
19
|
+
return [...lista].sort((a, b) => d(nombre, a) - d(nombre, b)).slice(0, n);
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
export const TIPOS_CLASICOS = ['title', 'year', 'text', 'icon', 'compare', 'card', 'dialog', 'aliens', 'memory', 'morph', 'cta'];
|
|
9
23
|
export const TIPOS_MOTOR = ['capas', 'titular', 'dato', 'lista', 'telefono', 'comparar', 'eleccion', 'cta'];
|
|
10
24
|
export const TIPOS_ESCENA = [...new Set([...TIPOS_CLASICOS, ...TIPOS_MOTOR])];
|
|
11
25
|
export const ESTILOS = ['datos-curiosos', 'explicativo', 'documental', 'ilustrado', 'historia', 'mesa', 'serie', 'infografia', '3d', 'minimal'];
|
|
12
|
-
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'];
|
|
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'];
|
|
13
29
|
export const PROVEEDORES_VOZ = ['kokoro', 'elevenlabs', 'openai'];
|
|
14
30
|
export const MUSICAS = ['lofi', 'lofi2', 'chillpop', 'synth', 'house', 'ballad', 'trap'];
|
|
15
31
|
|
|
16
32
|
export class ErrorMorfeo extends Error {}
|
|
17
33
|
|
|
34
|
+
/**
|
|
35
|
+
* WebGL por defecto: en Mac con Apple Silicon la tarjeta de video (angle) es estable y ~4 veces más rápida;
|
|
36
|
+
* en lo demás, por software (swangle), que funciona en cualquier computadora. Si angle falla, el render
|
|
37
|
+
* reintenta solo con swangle.
|
|
38
|
+
*/
|
|
39
|
+
export const glPorDefecto = () => (process.platform === 'darwin' && process.arch === 'arm64' ? 'angle' : 'swangle');
|
|
40
|
+
|
|
18
41
|
/** Una carpeta de guiones: cada <id>.json o <id>.mjs es un video. */
|
|
19
42
|
async function leerCarpeta(dir) {
|
|
20
43
|
const out = {};
|
|
@@ -84,7 +107,8 @@ export async function cargarProyecto(proyecto = process.cwd()) {
|
|
|
84
107
|
const errores = [];
|
|
85
108
|
const estilosPropios = cfg.estilos ?? {};
|
|
86
109
|
const estilo = cfg.estilo ?? 'datos-curiosos';
|
|
87
|
-
const
|
|
110
|
+
const assetsDir = path.resolve(proyecto, cfg.assets ?? 'assets');
|
|
111
|
+
const ctx = {proyecto, marca, voces: cfg.voces ?? {}, musicas: cfg.musicas ?? {}, errores, estilo, estilosPropios, assetsDir, personajes: cfg.personajes ?? {}};
|
|
88
112
|
if (!ESTILOS.includes(estilo) && !estilosPropios[estilo]) errores.push(`estilo "${estilo}" no existe. Opciones: ${[...ESTILOS, ...Object.keys(estilosPropios)].join(', ')}`);
|
|
89
113
|
const componentes = {};
|
|
90
114
|
for (const [nombre, ruta] of Object.entries(cfg.componentes ?? {})) {
|
|
@@ -125,14 +149,53 @@ export async function cargarProyecto(proyecto = process.cwd()) {
|
|
|
125
149
|
componentes,
|
|
126
150
|
render: {
|
|
127
151
|
crf: r.crf ?? 18,
|
|
128
|
-
gl: process.env.MORFEO_GL || process.env.REMOTION_GL || r.gl ||
|
|
152
|
+
gl: process.env.MORFEO_GL || process.env.REMOTION_GL || r.gl || glPorDefecto(),
|
|
129
153
|
navegador: process.env.MORFEO_NAVEGADOR || r.navegador || null,
|
|
130
|
-
concurrencia: r.concurrencia ?? null,
|
|
154
|
+
concurrencia: r.concurrencia ?? null, // null = Morfeo elige según la máquina y el modo (ver render.mjs)
|
|
131
155
|
miniatura: r.miniatura ?? true,
|
|
132
156
|
},
|
|
133
157
|
};
|
|
134
158
|
}
|
|
135
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
|
+
|
|
186
|
+
const CAMPOS_ARCHIVO = new Set(['src', 'imagen']);
|
|
187
|
+
/** Busca `src`/`imagen` que apunten a assets/ y avisa de los que no existen. */
|
|
188
|
+
function revisarArchivos(x, assetsDir, err, ruta = '') {
|
|
189
|
+
if (Array.isArray(x)) return x.forEach((v, i) => revisarArchivos(v, assetsDir, err, `${ruta}[${i}]`));
|
|
190
|
+
if (!x || typeof x !== 'object') return;
|
|
191
|
+
for (const [k, v] of Object.entries(x)) {
|
|
192
|
+
if (CAMPOS_ARCHIVO.has(k) && typeof v === 'string' && !/^(https?:|data:)/.test(v)) {
|
|
193
|
+
const rel = v.replace(/^\.?\/?(assets\/)?/, '');
|
|
194
|
+
if (!fs.existsSync(path.join(assetsDir, rel))) err(`no existe assets/${rel} (lo pide ${ruta.replace(/^\./, '') || 'una capa'}.${k}). Ponlo en la carpeta assets/ o usa generar: 'descripción' para la IA`);
|
|
195
|
+
} else if (v && typeof v === 'object') revisarArchivos(v, assetsDir, err, `${ruta}.${k}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
136
199
|
function validarCapas(capas, donde, err) {
|
|
137
200
|
if (!Array.isArray(capas)) return err(`${donde}: \`capas\` debe ser una lista`);
|
|
138
201
|
capas.forEach((c, i) => {
|
|
@@ -140,6 +203,10 @@ function validarCapas(capas, donde, err) {
|
|
|
140
203
|
if (!c || typeof c !== 'object') return err(`${d} debe ser un objeto`);
|
|
141
204
|
if (!TIPOS_CAPA.includes(c.tipo)) return err(`${d}: tipo "${c.tipo}" no existe. Opciones: ${TIPOS_CAPA.join(', ')}`);
|
|
142
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
|
+
}
|
|
143
210
|
if (c.capas) validarCapas(c.capas, d, err);
|
|
144
211
|
if (c.capa) validarCapas([c.capa], d, err);
|
|
145
212
|
});
|
|
@@ -178,7 +245,13 @@ function normalizarVoz(voz, ctx, err, profundidad = 0) {
|
|
|
178
245
|
if (!/\.npy$/i.test(abs)) err(`la voz ${ref} debe ser un archivo .npy (estilo de voz Kokoro, forma 510×1×256)`);
|
|
179
246
|
else if (!fs.existsSync(abs)) err(`no existe el archivo de voz ${ref}`);
|
|
180
247
|
out.push([abs, peso]);
|
|
181
|
-
} else
|
|
248
|
+
} else {
|
|
249
|
+
if (!VOCES_KOKORO.includes(ref)) {
|
|
250
|
+
const alias = Object.keys(ctx.voces);
|
|
251
|
+
err(`la voz "${ref}" no existe. ¿Quisiste decir ${parecidas(ref, [...alias, ...VOCES_KOKORO]).map((x) => `"${x}"`).join(', ')}? En español: em_alex, ef_dora, em_santa`);
|
|
252
|
+
}
|
|
253
|
+
out.push([ref, peso]);
|
|
254
|
+
}
|
|
182
255
|
}
|
|
183
256
|
return out;
|
|
184
257
|
}
|
|
@@ -211,7 +284,8 @@ function validarVideo(id, v, ctx) {
|
|
|
211
284
|
const scenes = v.scenes.map((s0, i) => {
|
|
212
285
|
const n = `escena ${i + 1}`;
|
|
213
286
|
if (!s0 || typeof s0 !== 'object') return err(`${n} debe ser un objeto`), s0;
|
|
214
|
-
|
|
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'};
|
|
215
289
|
if (!permitidos.includes(s.type))
|
|
216
290
|
err(
|
|
217
291
|
TIPOS_CLASICOS.includes(s.type)
|
|
@@ -237,5 +311,9 @@ function validarVideo(id, v, ctx) {
|
|
|
237
311
|
if (v.voice != null) voice = normalizarVoz(v.voice, ctx, err);
|
|
238
312
|
else if (necesitaVoz) err('falta `voice` (p. ej. "em_alex", una mezcla [["em_alex", 0.6], ["am_michael", 0.4]], un .npy, o `audio` en cada escena)');
|
|
239
313
|
if (v.hud != null) validarCapas(v.hud, 'hud', err);
|
|
314
|
+
// archivos de assets/ que piden las escenas y el HUD: mejor avisar ahora que a mitad del render
|
|
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);
|
|
240
318
|
return {...v, estilo, voice, music: normalizarMusica(v.music, ctx, err), scenes};
|
|
241
319
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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'], 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: ['console', 'game console'], antena: ['antenna'], satelite: ['satellite'], dron: ['drone'], servidor: ['server'],
|
|
73
|
+
wifi: ['wifi'], nube_de_datos: ['cloud computing', 'cloud'], codigo: ['code'], teclado: ['keyboard'], raton_pc: ['computer mouse'],
|
|
74
|
+
},
|
|
75
|
+
espacio: {
|
|
76
|
+
planeta: ['planet'], estrella: ['star'], astronauta: ['astronaut'], ovni: ['ufo'], galaxia: ['galaxy'], meteorito: ['meteor', 'asteroid'], telescopio: ['telescope'],
|
|
77
|
+
nave: ['spaceship', 'spacecraft'], tierra: ['earth', 'globe'], marte: ['mars'], cometa: ['comet'],
|
|
78
|
+
},
|
|
79
|
+
cuerpo: {
|
|
80
|
+
corazon: ['heart'], cerebro: ['brain'], pulmon: ['lungs'], hueso: ['bone'], craneo: ['skull'], diente: ['tooth'], ojo: ['eye'], mano: ['hand'], musculo: ['muscle'],
|
|
81
|
+
sangre: ['blood', 'blood drop'], adn: ['dna'], virus: ['virus'], bacteria: ['bacteria'], pastilla: ['pill', 'medicine'], jeringa: ['syringe'], hospital: ['hospital'],
|
|
82
|
+
doctor: ['doctor'], termometro: ['thermometer'], estetoscopio: ['stethoscope'], cama_de_hospital: ['hospital bed'],
|
|
83
|
+
},
|
|
84
|
+
escuela: {
|
|
85
|
+
libro: ['book'], cuaderno: ['notebook'], lapiz: ['pencil'], pluma: ['pen'], mochila: ['backpack'], pizarron: ['blackboard', 'chalkboard'], regla: ['ruler'],
|
|
86
|
+
calculadora: ['calculator'], globo_terraqueo: ['globe'], microscopio: ['microscope'], matraz: ['flask', 'lab flask'], diploma: ['diploma'], birrete: ['graduation cap'],
|
|
87
|
+
},
|
|
88
|
+
deporte: {
|
|
89
|
+
balon: ['ball', 'football'], futbol: ['soccer ball', 'football'], basquetbol: ['basketball'], beisbol: ['baseball'], tenis: ['tennis'], trofeo: ['trophy'],
|
|
90
|
+
medalla: ['medal'], pesa: ['dumbbell'], bicicleta_estatica: ['exercise bike'], caminadora: ['treadmill'], silbato: ['whistle'], cronometro: ['stopwatch'],
|
|
91
|
+
golf: ['golf'], patineta: ['skateboard'], carrera: ['race', 'racing'],
|
|
92
|
+
},
|
|
93
|
+
misterio: {
|
|
94
|
+
fantasma: ['ghost'], calavera: ['skull'], tumba: ['grave', 'tombstone'], ataud: ['coffin'], vela: ['candle'], linterna: ['lantern', 'flashlight'], lupa: ['magnifying glass'],
|
|
95
|
+
huella: ['footprint', 'fingerprint'], cuchillo: ['knife'], arana_red: ['spider web'], murcielago: ['bat'], calabaza: ['pumpkin'], castillo: ['castle'], llave_antigua: ['old key', 'key'],
|
|
96
|
+
},
|
|
97
|
+
juegos: {
|
|
98
|
+
videojuego: ['video game', 'gamepad'], dado: ['dice'], carta: ['playing card', 'card'], ajedrez: ['chess'], moneda_juego: ['coin'], corona: ['crown'], espada: ['sword'],
|
|
99
|
+
escudo: ['shield'], cofre_juego: ['chest'], gema: ['gem', 'crystal'], pocion: ['potion'], bandera: ['flag'], meta: ['finish line', 'flag'],
|
|
100
|
+
},
|
|
101
|
+
fiestas: {
|
|
102
|
+
regalo: ['gift', 'present'], globo: ['balloon'], pastel_cumpleanos: ['birthday cake', 'cake'], arbol_navidad: ['christmas tree'], nieve: ['snow', 'snowman'],
|
|
103
|
+
munneco_nieve: ['snowman'], fuegos_artificiales: ['fireworks'], confeti: ['confetti'], mascara: ['mask'],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Quita acentos, ñ → n, minúsculas y espacios → _ (las claves del diccionario están así). */
|
|
108
|
+
export const clave = (s) =>
|
|
109
|
+
String(s)
|
|
110
|
+
.normalize('NFD')
|
|
111
|
+
.replace(/[̀-ͯ]/g, '')
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.trim()
|
|
114
|
+
.replace(/\s+/g, '_');
|
|
115
|
+
|
|
116
|
+
const INDICE = new Map();
|
|
117
|
+
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});
|
|
118
|
+
|
|
119
|
+
/** Búsquedas en inglés para un término en español (o el término tal cual si no está). */
|
|
120
|
+
export function traducir(termino) {
|
|
121
|
+
const k = clave(termino);
|
|
122
|
+
if (INDICE.has(k)) return INDICE.get(k);
|
|
123
|
+
// plural simple: "tuberías" → "tuberia", "árboles" → "arbol"
|
|
124
|
+
for (const s of [k.replace(/es$/, ''), k.replace(/s$/, '')]) if (INDICE.has(s)) return INDICE.get(s);
|
|
125
|
+
return {tema: null, termino: k, buscar: [String(termino).trim()]};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Lista de temas con sus términos (para `morfeo recursos temas` y para la IA). */
|
|
129
|
+
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'];
|
|
@@ -13,14 +14,18 @@ export const ICONOS = [
|
|
|
13
14
|
];
|
|
14
15
|
const COLORES = {blanco: '#FFFFFF', amarillo: '#FFD400', rojo: '#FF4050', verde: '#5ADC6E', cian: '#50DCFF'};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/** id corto para el archivo: sin acentos, en minúsculas, hasta ~38 letras y sin cortar palabras. */
|
|
18
|
+
export const slug = (s) => {
|
|
19
|
+
const base = s
|
|
18
20
|
.normalize('NFD')
|
|
19
|
-
.replace(/[
|
|
21
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
20
22
|
.toLowerCase()
|
|
21
23
|
.replace(/[^a-z0-9]+/g, '-')
|
|
22
|
-
.replace(/^-+|-+$/g, '')
|
|
23
|
-
|
|
24
|
+
.replace(/^-+|-+$/g, '');
|
|
25
|
+
if (base.length <= 38) return base || 'video';
|
|
26
|
+
const corto = base.slice(0, 39);
|
|
27
|
+
return corto.slice(0, Math.max(corto.lastIndexOf('-'), 12)).replace(/-+$/, '') || 'video';
|
|
28
|
+
};
|
|
24
29
|
|
|
25
30
|
const INSTRUCCIONES = `Eres guionista de videos cortos verticales (YouTube Shorts / TikTok / Reels) de 25 a 45 segundos.
|
|
26
31
|
Escribes en el idioma que se te pida, con frases cortas, naturales y fáciles de decir en voz alta.
|
|
@@ -83,6 +88,9 @@ export async function esquema() {
|
|
|
83
88
|
});
|
|
84
89
|
}
|
|
85
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
|
+
|
|
86
94
|
const ESTILOS_MOTOR = ['explicativo', 'documental', 'ilustrado', 'historia', 'mesa', 'minimal'];
|
|
87
95
|
|
|
88
96
|
const INSTRUCCIONES_MOTOR = `Eres guionista y director de arte de videos cortos verticales (YouTube Shorts / TikTok / Reels) de 25 a 60 segundos.
|
|
@@ -95,11 +103,19 @@ Tipos de escena:
|
|
|
95
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.
|
|
96
104
|
- lista: tarjeta con "titulo" y 2-4 "items"; cada item se palomea cuando la voz dice su palabra "check".
|
|
97
105
|
- comparar: "a" contra "b" (textos cortos); "at" es la palabra de "say" en la que aparece "b".
|
|
98
|
-
- capas: una ilustración
|
|
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),
|
|
99
112
|
más un "chip" opcional (dato corto como "×5" o "230 dB") que aparece en la palabra "en".
|
|
100
|
-
- eleccion: final abierto para que el público comente: "pregunta" y 2 "opciones" ({texto, sub, emoji
|
|
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.
|
|
101
114
|
Varía los tipos. "at", "check" y "en" deben ser palabras que aparezcan tal cual en "say".
|
|
102
|
-
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')}`;
|
|
103
119
|
|
|
104
120
|
export async function esquemaMotor() {
|
|
105
121
|
const {z} = await import('zod');
|
|
@@ -119,12 +135,15 @@ export async function esquemaMotor() {
|
|
|
119
135
|
a: z.string().nullable(),
|
|
120
136
|
b: z.string().nullable(),
|
|
121
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(),
|
|
122
141
|
emoji: z.string().nullable(),
|
|
123
142
|
imagen_ia: z.string().nullable(),
|
|
124
143
|
chip: z.string().nullable(),
|
|
125
144
|
en: z.string().nullable(),
|
|
126
145
|
pregunta: z.string().nullable(),
|
|
127
|
-
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(),
|
|
128
147
|
});
|
|
129
148
|
return z.object({id: z.string(), title: z.string(), music: z.enum(MUSICAS), scenes: z.array(escena)});
|
|
130
149
|
}
|
|
@@ -156,12 +175,15 @@ function aEscenaMotor(e, conImagenes) {
|
|
|
156
175
|
break;
|
|
157
176
|
case 'eleccion':
|
|
158
177
|
si('pregunta', e.pregunta);
|
|
159
|
-
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} : {})}));
|
|
160
179
|
break;
|
|
161
180
|
case 'capas': {
|
|
162
181
|
const capas = [];
|
|
163
|
-
|
|
164
|
-
|
|
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'});
|
|
165
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'});
|
|
166
188
|
out.capas = capas;
|
|
167
189
|
break;
|