morfeo 0.6.3 → 0.6.5

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,7 +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
+ - **Objetos 2D y 3D:** una biblioteca con diccionario temático en español (`{tipo: 'recurso', buscar: 'tubería', estilo: '3d'}`) que busca en Kenney, Quaternius, KayKit, 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.
25
25
  - **Música:** 7 estilos generados sin derechos de autor, o tu propia pista (mp3, wav, flac, ogg). Baja sola cuando habla la voz.
26
26
  - **Guiones:** los escribes tú, o Claude escribe el video completo a partir de un tema o de tu texto.
27
27
  - **Subtítulos:** palabra por palabra; con Whisper quedan sincronizados con la voz real.
package/index.d.ts CHANGED
@@ -27,7 +27,9 @@ interface EscenaBase {
27
27
  export type Estilo = 'datos-curiosos' | 'explicativo' | 'documental' | 'ilustrado' | 'historia' | 'mesa' | 'serie' | 'infografia' | '3d' | 'maqueta' | 'minimal' | (string & {});
28
28
 
29
29
  /** Objetos 3D incluidos para capas {tipo: 'modelo', forma}: mejor opción que un emoji. */
30
- 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';
30
+ 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'
31
+ | 'valvula' | 'grifo' | 'tinaco' | 'calentador' | 'bomba' | 'alcantarilla' | 'codo' | 'interruptor' | 'enchufe' | 'tablero' | 'medidor' | 'transformador' | 'poste' | 'bateria' | 'turbina' | 'torre_electrica' | 'aire' | 'chip' | 'placa' | 'resistor' | 'capacitor' | 'led' | 'disco_duro' | 'tarjeta_grafica' | 'disipador' | 'ram' | 'router' | 'servidor' | 'fuente_poder' | 'engrane' | 'tornillo' | 'tuerca' | 'motor' | 'resorte' | 'piston'
32
+ | 'atomo' | 'molecula' | 'adn' | 'celula' | 'virus' | 'cerebro' | 'neurona' | 'pulmon' | 'ojo' | 'diente' | 'hueso' | 'jeringa' | 'matraz' | 'telescopio' | 'iman' | 'sol' | 'tierra' | 'volcan' | 'tornado' | 'iceberg' | 'agujero_negro' | 'disco' | 'usb' | 'tarjeta_sd' | 'cable_hdmi' | 'microfono' | 'visor_vr' | 'cargador' | 'dron' | 'tanque_gas' | 'calentador_solar' | 'termostato' | 'detector_humo' | 'camara_seguridad' | 'antena_celular' | 'presa' | 'torre_enfriamiento' | 'ascensor' | 'fusible' | 'fibra_optica' | 'buzon' | 'autobus' | 'helicoptero' | 'submarino' | 'bicicleta' | 'lapiz' | 'regla' | 'dado' | 'ancla' | 'paraguas';
31
33
 
32
34
  /** Momento: segundos desde que empieza la escena, una palabra de `say` ("orcas"), "voz", "fin", o "palabra+0.3". */
33
35
  export type Momento = number | string;
package/lib/config.mjs CHANGED
@@ -30,7 +30,9 @@ export const ESTILOS = ['datos-curiosos', 'explicativo', 'documental', 'ilustrad
30
30
  export const TIPOS_CAPA = ['imagen', 'video', 'texto', 'contador', 'chip', 'etiqueta', 'numero', 'tarjeta', 'telefono', 'forma', 'icono', 'emoji', 'marca', 'particulas', 'grupo', 'modelo', 'componente', 'ambiente', 'barra', 'medidor', 'votacion', 'estado', 'noticia', 'chat', 'onda', 'globo', 'seguir', 'personaje', 'escena3d', 'ventana', 'escenario', 'recurso', 'dato', 'medida', 'trazo', 'contorno', 'fuente', 'seccion', 'insignia', 'sello', 'cursor', 'terminal', 'nombre', 'accion', 'reloj', 'cronometro', 'termometro', 'recipientes', 'ficha', 'checklist', 'mapa', 'bloqueo', 'llamada', 'recuento', 'capitulo', 'vigilancia', 'cintillo', 'fecha', 'calendario', 'dividida', 'tierra', 'linea'];
31
31
  export const AMBIENTES = ['cuarto-gamer', 'sala', 'arcade', 'taller', 'oficina', 'tienda', 'calle'];
32
32
  export const ESCENARIOS = ['bosque', 'cuarto', 'departamento', 'tunel', 'pasillo'];
33
- export const FORMAS_3D = ['moneda', 'billete', 'lingote', 'cubo', 'esfera', 'corazon', 'estrella', 'rayo', 'diamante', 'trofeo', 'cohete', 'foco', 'reloj', 'planeta', 'arbol', 'nube', 'regalo', 'llave', 'candado', 'casa', 'libro', 'globo', 'medalla', 'escudo', 'pin', 'tuberia', 'muro', 'viga', 'columna', 'escalera', 'losa', 'tanque', 'suelo'];
33
+ export const FORMAS_3D = ['moneda', 'billete', 'lingote', 'cubo', 'esfera', 'corazon', 'estrella', 'rayo', 'diamante', 'trofeo', 'cohete', 'foco', 'reloj', 'planeta', 'arbol', 'nube', 'regalo', 'llave', 'candado', 'casa', 'libro', 'globo', 'medalla', 'escudo', 'pin', 'tuberia', 'muro', 'viga', 'columna', 'escalera', 'losa', 'tanque', 'suelo',
34
+ 'valvula', 'grifo', 'tinaco', 'calentador', 'bomba', 'alcantarilla', 'codo', 'interruptor', 'enchufe', 'tablero', 'medidor', 'transformador', 'poste', 'bateria', 'turbina', 'torre_electrica', 'aire', 'chip', 'placa', 'resistor', 'capacitor', 'led', 'disco_duro', 'tarjeta_grafica', 'disipador', 'ram', 'router', 'servidor', 'fuente_poder', 'engrane', 'tornillo', 'tuerca', 'motor', 'resorte', 'piston',
35
+ 'atomo', 'molecula', 'adn', 'celula', 'virus', 'cerebro', 'neurona', 'pulmon', 'ojo', 'diente', 'hueso', 'jeringa', 'matraz', 'telescopio', 'iman', 'sol', 'tierra', 'volcan', 'tornado', 'iceberg', 'agujero_negro', 'disco', 'usb', 'tarjeta_sd', 'cable_hdmi', 'microfono', 'visor_vr', 'cargador', 'dron', 'tanque_gas', 'calentador_solar', 'termostato', 'detector_humo', 'camara_seguridad', 'antena_celular', 'presa', 'torre_enfriamiento', 'ascensor', 'fusible', 'fibra_optica', 'buzon', 'autobus', 'helicoptero', 'submarino', 'bicicleta', 'lapiz', 'regla', 'dado', 'ancla', 'paraguas'];
34
36
  export const PROVEEDORES_VOZ = ['kokoro', 'elevenlabs', 'openai'];
35
37
  export const MUSICAS = ['lofi', 'lofi2', 'chillpop', 'synth', 'house', 'ballad', 'trap'];
36
38
 
@@ -13,14 +13,14 @@ export const TEMAS = {
13
13
  vela_aromatica: ['candle'], jarron: ['vase'], maceta: ['pot', 'plant pot'], tetera: ['teapot'], olla: ['pot', 'cooking pot'], sarten: ['pan', 'frying pan'],
14
14
  cuchara: ['spoon'], tenedor: ['fork'], frasco: ['jar'], lata: ['can'], extintor: ['fire extinguisher'], botiquin: ['first aid kit'], escalera_mano: ['ladder'],
15
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'],
16
+ arco: ['bow'], flecha: ['arrow'], bote_basura: ['trashcan', 'dumpster', 'trash can'], bote_de_basura: ['trashcan', 'dumpster'], periodico: ['newspaper'], revista: ['magazine'], boleto: ['ticket'], pasaporte: ['passport'],
17
17
  },
18
18
  hogar: {
19
19
  casa: ['house', 'home'], cuarto: ['bedroom', 'room'], cama: ['bed'], sillon: ['sofa', 'couch', 'armchair'], sofa: ['sofa', 'couch'],
20
20
  silla: ['chair'], mesa: ['table'], escritorio: ['desk'], lampara: ['lamp'], librero: ['bookcase', 'shelf'], refrigerador: ['fridge', 'refrigerator'],
21
21
  estufa: ['stove', 'oven'], horno: ['oven'], lavabo: ['sink', 'bathroom sink'], fregadero: ['kitchen sink', 'sink'], inodoro: ['toilet'], banera: ['bathtub'],
22
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'],
23
+ alfombra: ['rug', 'carpet'], chimenea: ['fireplace'], ropero: ['wardrobe', 'closet'], buro: ['nightstand'], cajon: ['drawer', 'cabinet'], lavadora: ['washer', 'washing machine'], microondas: ['microwave'], licuadora: ['blender'], ventilador: ['fan'], ventilador_de_techo: ['ceiling fan'], generador: ['generator'], bocina: ['speaker'], boton: ['button'], satelite_antena: ['satellite dish'], almohada: ['pillow'], cortina: ['curtain'], reloj: ['clock'], llave: ['key'], candado: ['padlock', 'lock'], cocina: ['kitchen'], bano: ['bathroom'],
24
24
  },
25
25
  construccion: {
26
26
  tuberia: ['pipe', 'pipes'], tubo: ['pipe'], valvula: ['valve'], llave_de_agua: ['faucet', 'tap'], drenaje: ['drain', 'sewer'], manguera: ['hose'],
@@ -29,17 +29,19 @@ export const TEMAS = {
29
29
  martillo: ['hammer'], taladro: ['drill'], llave_inglesa: ['wrench'], desarmador: ['screwdriver'], sierra: ['saw'], pala: ['shovel'], carretilla: ['wheelbarrow'],
30
30
  cono: ['traffic cone', 'cone'], barrera: ['barrier'], tunel: ['tunnel'], puente: ['bridge'], tanque: ['tank'], cable: ['cable', 'wire'], tornillo: ['screw', 'bolt'],
31
31
  molino: ['windmill', 'building mill', 'mill'], granja: ['farm', 'barn'], granero: ['barn'], pozo: ['well'], muelle: ['dock', 'pier'], mina: ['mine'], castillo_piedra: ['castle'],
32
+ fabrica: ['factory machine', 'machine', 'factory'], maquina: ['machine'], banda_transportadora: ['conveyor'], brazo_robotico: ['robot arm'], palanca: ['lever'], tolva: ['hopper'], escaner: ['scanner'], pasarela: ['catwalk'],
33
+ chimenea_industrial: ['chimney'], canaleta: ['gutter'], contenedor: ['container', 'cargo container'], carguero: ['ship cargo'], boya: ['buoy'], via: ['railroad'], riel: ['railroad', 'rail'], vagon: ['train carriage', 'wagon'], locomotora: ['locomotive', 'train'],
32
34
  herramienta: ['tool', 'toolbox'], caja_de_herramientas: ['toolbox'], valla: ['fence'], cerca: ['fence'],
33
35
  },
34
36
  ciudad: {
35
37
  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'],
38
+ farola: ['street light', 'lamp post'], banca: ['bench'], parada: ['bus stop'], hidrante: ['fire hydrant', 'hydrant'], basura: ['trashcan', 'dumpster', 'garbage'], camion_de_basura: ['garbage truck'],
37
39
  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
40
  },
39
41
  transporte: {
40
42
  auto: ['car'], carro: ['car'], coche: ['car'], camion: ['truck'], autobus: ['bus'], ambulancia: ['ambulance'], patrulla: ['police car', 'police'],
41
43
  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'],
44
+ barco: ['boat', 'ship'], velero: ['sail ship', 'boat'], crucero: ['cruise ship'], balsa: ['raft'], bote: ['boat'], cohete: ['rocket'], helicoptero: ['helicopter'], submarino: ['submarine'],
43
45
  },
44
46
  naturaleza: {
45
47
  arbol: ['tree'], pino: ['pine tree', 'pine'], flor: ['flower'], pasto: ['grass'], roca: ['rock', 'stone'], piedra: ['stone', 'rock'], montana: ['mountain'],
@@ -51,9 +53,11 @@ export const TEMAS = {
51
53
  concha: ['shell', 'seashell'], coral: ['coral'], ancla: ['anchor'], faro: ['lighthouse'], burbuja: ['bubble'],
52
54
  },
53
55
  animales: {
54
- perro: ['dog'], gato: ['cat'], oso: ['bear'], leon: ['lion'], tigre: ['tiger'], elefante: ['elephant'], caballo: ['horse'], vaca: ['cow'], cerdo: ['pig'],
56
+ perro: ['husky', 'shiba', 'dog'], gato: ['cat'], oso: ['bear'], leon: ['lion'], tigre: ['tiger'], elefante: ['elephant'], caballo: ['horse'], vaca: ['cow'], cerdo: ['pig'],
55
57
  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'],
58
+ serpiente: ['snake'], arana: ['spider'], abeja: ['bee'], mariposa: ['butterfly'], dinosaurio: ['trex', 'dinosaur'], zorro: ['fox'], lobo: ['wolf'], pinguino: ['penguin'],
59
+ rana: ['frog'], rata: ['rat', 'mouse'], avispa: ['wasp', 'bee'], venado: ['deer', 'stag'], burro: ['donkey'], toro: ['bull'], alpaca: ['alpaca'], llama: ['alpaca'],
60
+ tiranosaurio: ['trex'], triceratops: ['triceratops'], velociraptor: ['velociraptor'], pirana: ['piranha'], pez_payaso: ['clownfish'], pez_globo: ['puffer'], pez_espada: ['swordfish'], atun: ['tuna'],
57
61
  },
58
62
  comida: {
59
63
  manzana: ['apple'], platano: ['banana'], pizza: ['pizza'], hamburguesa: ['burger', 'hamburger'], papas: ['fries', 'french fries'], pan: ['bread'],
@@ -67,7 +71,7 @@ export const TEMAS = {
67
71
  oro: ['gold', 'gold bar'], diamante: ['diamond', 'gem'], grafica: ['chart', 'graph'], cofre: ['chest', 'treasure chest'], tesoro: ['treasure'], caja_fuerte: ['safe'],
68
72
  },
69
73
  tecnologia: {
70
- computadora: ['computer', 'laptop'], laptop: ['laptop'], celular: ['phone', 'smartphone'], telefono: ['phone'], robot: ['robot'], chip: ['chip', 'microchip'],
74
+ computadora: ['computer', 'laptop'], laptop: ['laptop'], celular: ['phone', 'smartphone'], telefono: ['phone'], robot: ['robot', 'mech'], radio: ['radio'], chip: ['chip', 'microchip'],
71
75
  bateria: ['battery'], enchufe: ['plug', 'socket'], foco: ['light bulb', 'bulb'], camara: ['camera'], audifonos: ['headphones'], microfono: ['microphone'],
72
76
  control: ['gamepad', 'controller'], consola: ['game console', 'console', 'gamepad'], consola_portatil: ['handheld console', 'nintendo switch', 'tablet', 'gamepad'],
73
77
  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'],
@@ -75,7 +79,7 @@ export const TEMAS = {
75
79
  },
76
80
  espacio: {
77
81
  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'],
82
+ nave: ['spaceship', 'spacecraft'], base_espacial: ['base module', 'base'], panel_solar: ['solar panel'], modulo_lunar: ['lander'], domo: ['dome'], radar: ['radar'], tierra: ['earth', 'globe'], marte: ['mars'], cometa: ['comet'],
79
83
  },
80
84
  cuerpo: {
81
85
  corazon: ['heart'], cerebro: ['brain'], pulmon: ['lungs'], hueso: ['bone'], craneo: ['skull'], diente: ['tooth'], ojo: ['eye'], mano: ['hand'], musculo: ['muscle'],
@@ -99,6 +103,9 @@ export const TEMAS = {
99
103
  videojuego: ['video game', 'gamepad'], dado: ['dice'], carta: ['playing card', 'card'], ajedrez: ['chess'], moneda_juego: ['coin'], corona: ['crown'], espada: ['sword'],
100
104
  escudo: ['shield'], cofre_juego: ['chest'], gema: ['gem', 'crystal'], pocion: ['potion'], bandera: ['flag'], meta: ['finish line', 'flag'],
101
105
  cartucho: ['game cartridge', 'cartridge', 'sd card', 'floppy disk'], joystick: ['joystick', 'gamepad'], arcade: ['arcade machine', 'arcade', 'gamepad'], jefe_final: ['monster', 'dragon', 'skull'],
106
+ caballero: ['knight'], esqueleto: ['skeleton'], mago: ['mage', 'wizard'], barbaro: ['barbarian'], enemigo: ['enemy', 'monster'], mecha: ['mech', 'robot'], torreta: ['turret'], pistola: ['pistol', 'gun'], escopeta: ['shotgun'],
107
+ armadura: ['armor'], daga: ['dagger'], baston_magico: ['staff', 'wand'], varita: ['wand'], ballesta: ['crossbow'], pergamino: ['parchment', 'scroll'], caliz: ['chalice'], collar: ['necklace'], lingote: ['gold ingots', 'ingot'],
108
+ catapulta: ['catapult'], taberna: ['tavern'], herreria: ['blacksmith'], estandarte: ['banner'], antorcha: ['torch'], botin: ['lootbox', 'chest'],
102
109
  vida: ['heart', 'life'], nivel: ['level', 'block', 'platform'], bloque: ['block', 'cube', 'brick'], llave_juego: ['key'], hongo: ['mushroom'], estrella_juego: ['star'],
103
110
  },
104
111
  fiestas: {
@@ -128,5 +135,58 @@ export function traducir(termino) {
128
135
  return {tema: null, termino: k, buscar: [String(termino).trim()]};
129
136
  }
130
137
 
138
+ /**
139
+ * Piezas de ingeniería hechas en código (remotion/motor/objetos-ingenieria.tsx): forma → términos que la piden.
140
+ * No están en ninguna biblioteca libre, así que el buscador las revisa antes que a las bibliotecas.
141
+ */
142
+ export const PIEZAS = {
143
+ valvula: ['valvula', 'llave_de_paso', 'valvula_de_agua'], grifo: ['grifo', 'llave_de_agua', 'llave_del_agua'], tinaco: ['tinaco', 'tanque_de_agua'],
144
+ calentador: ['calentador', 'boiler', 'caldera', 'calentador_de_agua'], bomba: ['bomba_de_agua', 'bomba_hidraulica'], alcantarilla: ['alcantarilla', 'coladera', 'tapa_de_drenaje'],
145
+ codo: ['codo', 'codo_de_tuberia'], interruptor: ['interruptor', 'apagador', 'switch'], enchufe: ['enchufe', 'contacto', 'tomacorriente'],
146
+ tablero: ['tablero_electrico', 'centro_de_carga', 'caja_de_fusibles', 'pastillas', 'breaker'], medidor: ['medidor', 'medidor_de_luz'], transformador: ['transformador'],
147
+ poste: ['poste_de_luz', 'poste'], bateria: ['bateria', 'pila', 'pila_aa'], turbina: ['turbina', 'aerogenerador', 'turbina_eolica'], torre_electrica: ['torre_electrica', 'torre_de_alta_tension'],
148
+ aire: ['aire_acondicionado', 'minisplit', 'clima'], chip: ['chip', 'microchip', 'procesador', 'cpu'], placa: ['placa_madre', 'tarjeta_madre', 'placa_base', 'motherboard', 'placa'],
149
+ resistor: ['resistor', 'resistencia'], capacitor: ['capacitor', 'condensador'], led: ['led'], disco_duro: ['disco_duro', 'hdd'],
150
+ tarjeta_grafica: ['tarjeta_grafica', 'tarjeta_de_video', 'gpu'], disipador: ['disipador', 'ventilador_de_pc', 'cooler'], ram: ['ram', 'memoria_ram'], router: ['router', 'modem'],
151
+ servidor: ['servidor', 'rack'], fuente_poder: ['fuente_de_poder', 'fuente_de_alimentacion'], engrane: ['engrane', 'engranaje'], tornillo: ['tornillo'], tuerca: ['tuerca'],
152
+ motor: ['motor', 'motor_electrico'], resorte: ['resorte'], piston: ['piston'],
153
+ // ciencia y cuerpo
154
+ atomo: ['atomo'], molecula: ['molecula', 'molecula_de_agua'], adn: ['adn', 'dna', 'genes'], celula: ['celula'], virus: ['virus', 'bacteria', 'microbio'], cerebro: ['cerebro'],
155
+ neurona: ['neurona'], pulmon: ['pulmon', 'pulmones'], ojo: ['ojo'], diente: ['diente', 'muela'], hueso: ['hueso'], jeringa: ['jeringa', 'vacuna', 'inyeccion'],
156
+ matraz: ['matraz', 'quimica', 'tubo_de_ensayo'], telescopio: ['telescopio'], iman: ['iman'], sol: ['sol'], tierra: ['tierra', 'globo_terraqueo', 'mundo', 'planeta_tierra'],
157
+ volcan: ['volcan'], tornado: ['tornado', 'huracan'], iceberg: ['iceberg'], agujero_negro: ['agujero_negro'],
158
+ // consola y tecnología
159
+ disco: ['disco', 'cd', 'dvd', 'disco_de_juego', 'blu_ray'], usb: ['usb', 'memoria_usb'], tarjeta_sd: ['tarjeta_sd', 'memoria_sd', 'tarjeta_de_memoria'], cable_hdmi: ['cable_hdmi', 'hdmi'],
160
+ microfono: ['microfono'], visor_vr: ['visor_vr', 'realidad_virtual', 'lentes_vr', 'vr'], cargador: ['cargador'], dron: ['dron'],
161
+ // casa y ciudad
162
+ tanque_gas: ['tanque_de_gas', 'gas', 'tanque_estacionario'], calentador_solar: ['calentador_solar'], termostato: ['termostato'], detector_humo: ['detector_de_humo', 'alarma_de_humo'],
163
+ camara_seguridad: ['camara_de_seguridad', 'camara_de_vigilancia'], antena_celular: ['antena_celular', 'antena_de_telefonia', 'torre_celular'], presa: ['presa', 'represa'],
164
+ torre_enfriamiento: ['planta_nuclear', 'torre_de_enfriamiento', 'central_nuclear'], ascensor: ['ascensor', 'elevador'], fusible: ['fusible'], fibra_optica: ['fibra_optica'], buzon: ['buzon'],
165
+ // transporte
166
+ autobus: ['autobus', 'camion_de_pasajeros'], helicoptero: ['helicoptero'], submarino: ['submarino'], bicicleta: ['bicicleta', 'bici'],
167
+ // escuela y objetos
168
+ lapiz: ['lapiz'], regla: ['regla'], dado: ['dado'], ancla: ['ancla'], paraguas: ['paraguas', 'sombrilla'],
169
+ };
170
+ const PIEZA_DE = new Map();
171
+ for (const [forma, terminos] of Object.entries(PIEZAS)) for (const t of terminos) PIEZA_DE.set(t, forma);
172
+
173
+ /** Formas básicas de Morfeo (objetos3d.tsx) por término: se usan solo si ninguna biblioteca tiene el objeto. */
174
+ const BASICAS = {corazon: ['corazon'], estrella: ['estrella'], rayo: ['rayo', 'electricidad'], diamante: ['diamante', 'gema'], trofeo: ['trofeo', 'copa'], cohete: ['cohete'], foco: ['foco', 'bombilla', 'foco_led'], reloj: ['reloj'],
175
+ planeta: ['planeta', 'marte', 'saturno'], arbol: ['arbol'], nube: ['nube'], regalo: ['regalo'], llave: ['llave'], candado: ['candado'], casa: ['casa'], libro: ['libro'], globo: ['globo'], medalla: ['medalla'], escudo: ['escudo'], tanque: ['cisterna']};
176
+ const BASICA_DE = new Map();
177
+ for (const [forma, terminos] of Object.entries(BASICAS)) for (const t of terminos) BASICA_DE.set(t, forma);
178
+ export function basicaDe(termino) {
179
+ const k = clave(termino);
180
+ for (const s of [k, k.replace(/es$/, ''), k.replace(/s$/, '')]) if (BASICA_DE.has(s)) return BASICA_DE.get(s);
181
+ return null;
182
+ }
183
+
184
+ /** Forma hecha en código que corresponde a un término ("llave de agua" → "grifo"), o null. */
185
+ export function piezaDe(termino) {
186
+ const k = clave(termino);
187
+ for (const s of [k, k.replace(/es$/, ''), k.replace(/s$/, '')]) if (PIEZA_DE.has(s)) return PIEZA_DE.get(s);
188
+ return null;
189
+ }
190
+
131
191
  /** 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)]));
192
+ export const listaTemas = () => ({...Object.fromEntries(Object.entries(TEMAS).map(([t, v]) => [t, Object.keys(v)])), ingenieria: Object.values(PIEZAS).map((v) => v[0])});
package/lib/recursos.mjs CHANGED
@@ -14,7 +14,7 @@ import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import {ErrorMorfeo} from './config.mjs';
16
16
  import {HOME} from './entorno.mjs';
17
- import {traducir} from './diccionario.mjs';
17
+ import {traducir, piezaDe, basicaDe} from './diccionario.mjs';
18
18
 
19
19
  export const BIBLIOTECA = path.join(HOME, 'biblioteca');
20
20
  const UA = {'user-agent': 'morfeo (https://github.com/UreckChan/morfeo)'};
@@ -136,6 +136,82 @@ async function bajarKenney(r) {
136
136
  return {archivos: [local], principal: local, credito: `${r.nombre} · Kenney (kenney.nl) · CC0`};
137
137
  }
138
138
 
139
+ // ---------------------------------------------------------------- Quaternius y KayKit (3D low-poly, CC0)
140
+ // Las dos viven en GitHub: el índice sale del árbol de cada repositorio y los archivos de raw.githubusercontent.
141
+ // Quaternius no tiene repositorio propio: se usa un espejo que conserva los paquetes y sus licencias (todas CC0).
142
+ const KAYKIT = ['Character-Pack-Adventures', 'Character-Pack-Skeletons', 'City-Builder-Bits', 'Dungeon-Remastered', 'Furniture-Bits', 'Halloween-Bits', 'Medieval-Hexagon-Pack', 'Prototype-Bits', 'Restaurant-Bits', 'Space-Base-Bits'];
143
+ const REPOS = {
144
+ quaternius: [{repo: 'trebeljahr/quaternius-showcase', rama: 'main', filtro: /^public\/glb\/.+\.glb$/i, kit: (ruta) => ruta.split('/')[2].replace(/_/g, ' ')}],
145
+ kaykit: KAYKIT.map((n) => ({repo: `KayKit-Game-Assets/KayKit-${n}-1.0`, rama: 'main', filtro: /\/gltf\/.+\.(gltf|glb)$/i, kit: () => n.replace(/-/g, ' ')})),
146
+ };
147
+ // Kenney completo (49 kits): el espejo de arriba solo trae 12. De este se toman los kits que aquel no tiene
148
+ // (fábrica, ciudad industrial, edificios, trenes, estación espacial, caminos…), para no duplicar resultados.
149
+ const KENNEY_YA = ['car', 'fantasy-town', 'food', 'furniture', 'graveyard', 'hexagon', 'holiday', 'nature', 'platformer', 'racing', 'minigolf', 'tower-defense'];
150
+ REPOS['kenney-mas'] = [{repo: 'shorepine/kenney', rama: 'main', filtro: new RegExp(`^3d/(?!(${KENNEY_YA.join('|')})/)[^/]+/[^/]+\\.glb$`, 'i'), kit: (ruta) => ruta.split('/')[1].replace(/-/g, ' ')}];
151
+ const CREDITO_REPO = {'kenney-mas': 'Kenney (kenney.nl)', quaternius: 'Quaternius (quaternius.com)', kaykit: 'KayKit · Kay Lousberg (kaylousberg.com)'};
152
+
153
+ const repoIndice = (fuente) =>
154
+ indice(fuente, async () => {
155
+ const out = [];
156
+ let fallos = 0;
157
+ for (const r of REPOS[fuente]) {
158
+ try {
159
+ const arbol = await pedirJSON(`https://api.github.com/repos/${r.repo}/git/trees/${r.rama}?recursive=1`);
160
+ for (const n of arbol.tree) if (r.filtro.test(n.path)) out.push({repo: r.repo, rama: r.rama, ruta: n.path, nombre: n.path.split('/').pop().replace(/(\.gltf)?\.(glb|gltf)$/i, '').replace(/-transformed$/, ''), kit: r.kit(n.path)});
161
+ } catch {
162
+ fallos++; // un repositorio que falla no tira a los demás
163
+ }
164
+ }
165
+ if (!out.length) throw new ErrorMorfeo(`No pude leer el índice de ${fuente}`);
166
+ void fallos;
167
+ return out;
168
+ });
169
+
170
+ async function buscarRepo(fuente, consultas) {
171
+ const idx = await repoIndice(fuente);
172
+ const out = [];
173
+ for (const [i, q] of consultas.entries())
174
+ for (const m of idx) {
175
+ // "bed_double_A" / "Tree_03": la letra o el número de variante no es el sustantivo
176
+ const nombre = palabras(m.nombre).filter((w) => w.length > 1);
177
+ // el paquete también cuenta, con menos peso: "dinosaur" trae todo el paquete de dinosaurios aunque se llamen "Trex"
178
+ const p = Math.max(puntaje(nombre, q), puntaje([...nombre, ...palabras(m.kit).filter((w) => !['pack', 'kit', 'bits', 'ultimate', 'modular'].includes(w))], q) - 6);
179
+ if (p > 0) out.push({fuente, id: `${m.repo}/${m.ruta}`, repo: m.repo, rama: m.rama, ruta: m.ruta, nombre: m.nombre, coleccion: m.kit, licencia: 'CC0', puntaje: p - i * 3, tipo: 'modelo'});
180
+ }
181
+ return out;
182
+ }
183
+
184
+ /** El JSON de un .glb (primer bloque del archivo), o null si no se puede leer. */
185
+ function jsonDeGlb(archivo) {
186
+ try {
187
+ const b = fs.readFileSync(archivo);
188
+ if (b.readUInt32LE(0) !== 0x46546c67 || b.readUInt32LE(16) !== 0x4e4f534a) return null;
189
+ return JSON.parse(b.subarray(20, 20 + b.readUInt32LE(12)).toString('utf8'));
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ async function bajarRepo(r) {
196
+ const cruda = (ruta) => `https://raw.githubusercontent.com/${r.repo}/${r.rama}/${ruta.split('/').map(encodeURIComponent).join('/')}`;
197
+ const local = (ruta) => path.join(BIBLIOTECA, r.fuente, r.repo.split('/')[1], ruta.replace(/[^A-Za-z0-9._/-]/g, '_'));
198
+ const principal = await bajar(cruda(r.ruta), local(r.ruta));
199
+ const archivos = [principal];
200
+ // un .gltf trae su geometría (.bin) y sus texturas en archivos aparte, con rutas relativas; un .glb también
201
+ // puede apuntar a una textura de fuera (los de Kenney comparten "Textures/colormap.png" por kit)
202
+ const g = /\.gltf$/i.test(r.ruta) ? JSON.parse(fs.readFileSync(principal, 'utf8')) : jsonDeGlb(principal);
203
+ if (g) {
204
+ const dir = path.posix.dirname(r.ruta);
205
+ for (const x of [...(g.buffers ?? []), ...(g.images ?? [])]) {
206
+ if (!x.uri || x.uri.startsWith('data:')) continue;
207
+ const rel = path.posix.normalize(path.posix.join(dir, decodeURIComponent(x.uri)));
208
+ if (rel.startsWith('..')) continue;
209
+ archivos.push(await bajar(cruda(rel), local(rel)));
210
+ }
211
+ }
212
+ return {archivos, principal, credito: `${r.nombre} · ${r.coleccion} · ${CREDITO_REPO[r.fuente]} · CC0`};
213
+ }
214
+
139
215
  // ---------------------------------------------------------------- Poly Haven (3D realista, CC0)
140
216
  const polyhavenIndice = () => indice('polyhaven', () => pedirJSON('https://api.polyhaven.com/assets?t=models'));
141
217
 
@@ -211,7 +287,7 @@ async function bajarPolypizza(r) {
211
287
 
212
288
  // ---------------------------------------------------------------- búsqueda y descarga
213
289
  /** Orden de fuentes por estilo. */
214
- const FUENTES = {'3d': ['kenney', 'polypizza', 'polyhaven'], realista: ['polyhaven', 'polypizza', 'kenney'], '2d': ['iconify']};
290
+ const FUENTES = {'3d': ['kenney', 'kenney-mas', 'quaternius', 'kaykit', 'polypizza', 'polyhaven'], realista: ['polyhaven', 'polypizza', 'kenney', 'kenney-mas', 'quaternius', 'kaykit'], '2d': ['iconify']};
215
291
 
216
292
  /**
217
293
  * Busca un término en las bibliotecas.
@@ -221,15 +297,25 @@ export async function buscarRecursos(termino, {estilo = '3d', variante, fuente}
221
297
  const {buscar} = traducir(termino);
222
298
  const fuentes = fuente ? [fuente] : FUENTES[estilo] ?? FUENTES['3d'];
223
299
  const todos = [];
300
+ // piezas de ingeniería hechas en código (válvula, interruptor, placa madre…): van primero porque ninguna
301
+ // biblioteca las tiene y lo que devuelven en su lugar no sirve ("tornillo" → unas pinzas)
302
+ const pieza = estilo !== '2d' && (!fuente || fuente === 'morfeo') ? piezaDe(termino) : null;
303
+ if (pieza) todos.push({fuente: 'morfeo', id: pieza, nombre: pieza, coleccion: 'piezas de ingeniería', licencia: 'incluida', puntaje: 1000, tipo: 'forma'});
304
+ if (fuente === 'morfeo') return todos;
224
305
  for (const f of fuentes) {
225
306
  try {
226
- const r = f === 'kenney' ? await buscarKenney(buscar) : f === 'polyhaven' ? await buscarPolyhaven(buscar) : f === 'polypizza' ? await buscarPolypizza(buscar) : await buscarIconify(buscar, variante);
307
+ const r = f === 'kenney' ? await buscarKenney(buscar) : REPOS[f] ? await buscarRepo(f, buscar) : f === 'polyhaven' ? await buscarPolyhaven(buscar) : f === 'polypizza' ? await buscarPolypizza(buscar) : await buscarIconify(buscar, variante);
227
308
  // el orden de las fuentes pesa: la primera fuente con resultados buenos gana
228
309
  todos.push(...r.map((x) => ({...x, puntaje: x.puntaje - fuentes.indexOf(f) * 6})));
229
310
  } catch (e) {
230
311
  if (fuente) throw e; // con fuente explícita, el error importa; si no, se prueba la siguiente
231
312
  }
232
313
  }
314
+ // formas básicas de siempre (corazón, foco, planeta…): solo si ninguna biblioteca tuvo el objeto
315
+ // (en `3d`, un modelo realista de Poly Haven no cuenta: un foco de vidrio oscuro no combina con lo demás)
316
+ const hayLowPoly = todos.some((r) => estilo === 'realista' || r.fuente !== 'polyhaven');
317
+ const basica = estilo !== '2d' && !fuente && !hayLowPoly ? basicaDe(termino) : null;
318
+ if (basica) todos.push({fuente: 'morfeo', id: basica, nombre: basica, coleccion: 'formas básicas', licencia: 'incluida', puntaje: 500, tipo: 'forma'});
233
319
  const vistos = new Set();
234
320
  return todos
235
321
  .sort((a, b) => b.puntaje - a.puntaje)
@@ -239,6 +325,7 @@ export async function buscarRecursos(termino, {estilo = '3d', variante, fuente}
239
325
  /** Descarga un resultado. Devuelve {principal, archivos, base?, credito}. */
240
326
  export async function bajarRecurso(r, {color} = {}) {
241
327
  if (r.fuente === 'kenney') return bajarKenney(r);
328
+ if (REPOS[r.fuente]) return bajarRepo(r);
242
329
  if (r.fuente === 'polyhaven') return bajarPolyhaven(r);
243
330
  if (r.fuente === 'polypizza') return bajarPolypizza(r);
244
331
  return bajarIconify(r, color);
@@ -279,6 +366,12 @@ export async function resolverRecursos(video, assetsPublico, log = console.log)
279
366
  return null;
280
367
  }
281
368
  const r = res[Math.min(n, res.length - 1)];
369
+ if (r.tipo === 'forma') {
370
+ log(` recurso "${buscar}" → ${r.nombre} (pieza de Morfeo)`);
371
+ // quietas y en tres cuartos: varias son planas (placa, chip, tapa) y girando quedaban de canto; las que
372
+ // tienen movimiento propio (engrane, ventiladores, turbina) se siguen moviendo
373
+ return {tipo: 'modelo', forma: r.id, ancho: 620, alto: 620, girar3d: [0, 0, 0], rotacion: [6, -28, 0], reflejos: 0.5, luzAmbiente: 0.42, ...resto};
374
+ }
282
375
  const d = await bajarRecurso(r, {color});
283
376
  creditos.add(d.credito);
284
377
  // se copia a public/assets/biblioteca/<fuente>/... conservando la carpeta (los glTF de Poly Haven traen texturas aparte)
package/lib/render.mjs CHANGED
@@ -80,6 +80,47 @@ const conHuella = (config) => ({
80
80
  resolve: {...config.resolve, modules: [...(config.resolve?.modules ?? ['node_modules']), path.join(PAQUETE, 'node_modules')]},
81
81
  });
82
82
 
83
+ const PREFIJO_BUNDLE = 'remotion-webpack-bundle-';
84
+ const esBundleTemporal = (dir) => path.basename(dir).startsWith(PREFIJO_BUNDLE) && fs.realpathSync(path.dirname(dir)) === fs.realpathSync(os.tmpdir());
85
+
86
+ /** Bundles que dejó un render interrumpido (o versiones anteriores de Morfeo, que no los borraban): fuera si tienen más de un día. */
87
+ function limpiarBundlesViejos() {
88
+ try {
89
+ const limite = Date.now() - 24 * 3600 * 1000;
90
+ for (const n of fs.readdirSync(os.tmpdir())) {
91
+ if (!n.startsWith(PREFIJO_BUNDLE)) continue;
92
+ const dir = path.join(os.tmpdir(), n);
93
+ if (fs.statSync(dir).mtimeMs < limite) fs.rmSync(dir, {recursive: true, force: true});
94
+ }
95
+ } catch {}
96
+ }
97
+
98
+ /**
99
+ * Webpack guarda un caché (~300 MB) por cada huella del motor y de la configuración, y no borra los anteriores:
100
+ * tras varias actualizaciones eran varios GB. Se quedan los dos más recientes.
101
+ */
102
+ function podarCacheWebpack(raiz) {
103
+ try {
104
+ let dir = raiz;
105
+ while (!fs.existsSync(path.join(dir, 'node_modules', '.cache', 'webpack'))) {
106
+ const arriba = path.dirname(dir);
107
+ if (arriba === dir) return;
108
+ dir = arriba;
109
+ }
110
+ const base = path.join(dir, 'node_modules', '.cache', 'webpack');
111
+ for (const grupo of fs.readdirSync(base)) {
112
+ if (!grupo.startsWith('remotion-')) continue;
113
+ const g = path.join(base, grupo);
114
+ const viejos = fs
115
+ .readdirSync(g)
116
+ .map((n) => ({d: path.join(g, n), m: fs.statSync(path.join(g, n)).mtimeMs}))
117
+ .sort((x, y) => y.m - x.m)
118
+ .slice(2);
119
+ for (const v of viejos) fs.rmSync(v.d, {recursive: true, force: true});
120
+ }
121
+ } catch {}
122
+ }
123
+
83
124
  /**
84
125
  * Entrada de Remotion. Sin componentes propios es la del paquete; con ellos, Morfeo escribe
85
126
  * .morfeo/entrada.tsx que los importa y los registra antes de arrancar.
@@ -294,13 +335,17 @@ export async function render({proyecto = process.cwd(), ids, todos = false, camb
294
335
  // cuadros pedazos de texto de cuadros anteriores (rótulos o subtítulos fantasma)
295
336
  const hechos = [];
296
337
  const soltar = await tomarCandado(log);
338
+ let serveUrl;
297
339
  try {
298
340
  log('Empaquetando la animación...');
299
341
  const {bundle} = await import('@remotion/bundler');
300
342
  const {renderMedia, renderStill, selectComposition} = await import('@remotion/renderer');
301
343
  const entrada = entradaDe(p);
302
344
  const tBundle = Date.now();
303
- const serveUrl = await bundle({entryPoint: entrada, publicDir: publico, rootDir: entrada === ENTRADA_REMOTION ? path.dirname(ENTRADA_REMOTION) : p.proyecto, webpackOverride: conHuella});
345
+ limpiarBundlesViejos();
346
+ const raizBundle = entrada === ENTRADA_REMOTION ? path.dirname(ENTRADA_REMOTION) : p.proyecto;
347
+ serveUrl = await bundle({entryPoint: entrada, publicDir: publico, rootDir: raizBundle, webpackOverride: conHuella});
348
+ podarCacheWebpack(raizBundle);
304
349
  const segBundle = (Date.now() - tBundle) / 1000;
305
350
  fs.mkdirSync(p.salida, {recursive: true});
306
351
  const chromiumOptions = {gl: p.render.gl};
@@ -414,6 +459,8 @@ export async function render({proyecto = process.cwd(), ids, todos = false, camb
414
459
  }
415
460
  }
416
461
  } finally {
462
+ // el bundle es una copia de la animación y de public/ (cientos de MB) en la carpeta temporal: Remotion no la borra
463
+ if (serveUrl && esBundleTemporal(serveUrl)) fs.rmSync(serveUrl, {recursive: true, force: true});
417
464
  soltar();
418
465
  }
419
466
  if (previa || parcial) return hechos;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morfeo",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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",
@@ -11,6 +11,7 @@ import React, {useEffect, useLayoutEffect, useMemo, useState} from 'react';
11
11
  import {continueRender, delayRender} from 'remotion';
12
12
  import * as THREE from 'three';
13
13
  import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
14
+ import {clone as clonarConEsqueleto} from 'three/examples/jsm/utils/SkeletonUtils.js';
14
15
  import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment.js';
15
16
  import {clamp, easeOut} from '../estilo';
16
17
  import {cuando, recurso, type CtxEscena} from './util';
@@ -30,7 +31,15 @@ const cargar = (url: string) => cargarGltf(url).then((g) => g.scene);
30
31
  * hilos sueltos (el cable de un control, la cuerda de un globo), encuadra esa zona y no la caja completa.
31
32
  */
32
33
  function cajaPrincipal(obj: THREE.Object3D): THREE.Box3 {
34
+ obj.updateMatrixWorld(true);
33
35
  const completa = new THREE.Box3().setFromObject(obj);
36
+ // con esqueleto (animales, personajes), los vértices crudos están en la pose de reposo y a otra escala que la
37
+ // que se dibuja: la caja buena es la que calcula three con los huesos
38
+ let conHuesos = false;
39
+ obj.traverse((o) => {
40
+ if ((o as THREE.SkinnedMesh).isSkinnedMesh) conHuesos = true;
41
+ });
42
+ if (conHuesos) return completa;
34
43
  const pts: number[] = [];
35
44
  const v = new THREE.Vector3();
36
45
  obj.updateMatrixWorld(true);
@@ -67,7 +76,9 @@ const Glb: React.FC<{src: string; levantar?: boolean}> = ({src, levantar}) => {
67
76
  cargar(url)
68
77
  .then((s) => {
69
78
  // centrado y escalado a ~2 unidades para que cualquier modelo quepa en el lienzo
70
- const copia = s.clone(true);
79
+ // clone() a secas deja la malla atada a los huesos del original: el modelo se dibujaba sin centrar ni escalar
80
+ const copia = clonarConEsqueleto(s) as THREE.Group;
81
+ copia.updateMatrixWorld(true);
71
82
  const caja = levantar ? cajaPrincipal(copia) : new THREE.Box3().setFromObject(copia);
72
83
  const tam = caja.getSize(new THREE.Vector3());
73
84
  const centro = caja.getCenter(new THREE.Vector3());
@@ -244,7 +255,7 @@ const ModeloEscena: React.FC<{c: Record<string, any>; ctx: CtxEscena}> = ({c, ct
244
255
  useEffect(() => {
245
256
  cargarGltf(url)
246
257
  .then((gl) => {
247
- const scene = gl.scene.clone(true);
258
+ const scene = clonarConEsqueleto(gl.scene) as THREE.Group;
248
259
  let mixer: THREE.AnimationMixer | null = null;
249
260
  if (c.animacion && gl.animations.length) {
250
261
  mixer = new THREE.AnimationMixer(scene);