@olo-studio/studio-mcp 1.0.0 → 1.0.1
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 -2
- package/bin/olo-studio-mcp.js +10 -3
- package/package.json +5 -2
- package/src/compiler.js +60 -13
- package/src/director.js +1098 -0
- package/src/renderer.js +256 -35
- package/src/server.js +33 -6
package/src/director.js
ADDED
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
/* Olo Studio — Director Engine v3.0 (21 Cycles 3D States + Canonical character.js POP/Fade + motion.js Choreography)
|
|
2
|
+
* Pure, deterministic compiler & frame evaluator for Browser (window.OloDirector) and Node.js (CommonJS).
|
|
3
|
+
* Built strictly on Olomni's canonical principles (`presence-widget/src/character.js`, `motion.js`, `minis.js`, `tools/olo3d/render.py`):
|
|
4
|
+
* - All 21 Cycles-rendered 3D states (including 3D props: book, notepad+pencil, magnifying glass, globe, clipboard, gear, mini-Olos, heart, stars, and the 5×3 3D `look` gaze grid)
|
|
5
|
+
* - Cohesive 1:1 sprite + mask + wear registration (zero fake 2D face overlays, zero holdout detachment)
|
|
6
|
+
* - Canonical `POP` anticipation squash/rebound curves anchored at Olo's feet (`FEET = 0.955`)
|
|
7
|
+
* - Canonical `motion.js` events per beat (`orbStep`, `paper`, `memory`, `orbBurst`, `goldAura`)
|
|
8
|
+
*/
|
|
9
|
+
(function (root, factory) {
|
|
10
|
+
if (typeof module === 'object' && module.exports) module.exports = factory()
|
|
11
|
+
else root.OloDirector = factory()
|
|
12
|
+
})(typeof self !== 'undefined' ? self : typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
13
|
+
'use strict'
|
|
14
|
+
|
|
15
|
+
// Los 21 estados 3D reales renderizados en Blender Cycles (`presence-widget/src-tauri/characters/olo/manifest.json`)
|
|
16
|
+
const OLO_STATES = {
|
|
17
|
+
idle: { row: 0, frames: 16, fps: 8, loop: true, screen: '#3b6cff', label: 'En calma', category: 'base' },
|
|
18
|
+
sleeping: { row: 1, frames: 12, fps: 5, loop: true, screen: '#4d86ff', label: 'Dormido (zZ)', category: 'base' },
|
|
19
|
+
thinking: { row: 2, frames: 12, fps: 8, loop: true, screen: '#4d86ff', label: 'Pensando (...)', category: 'base' },
|
|
20
|
+
working: { row: 3, frames: 12, fps: 12, loop: true, screen: '#3b6cff', label: 'Trabajando (>_)', category: 'base' },
|
|
21
|
+
needs_approval: { row: 4, frames: 12, fps: 10, loop: true, screen: '#f2c14e', label: '¿Aprobación? (?)', category: 'base' },
|
|
22
|
+
done: { row: 5, frames: 10, fps: 12, loop: false, screen: '#3ddc97', label: 'Terminado (✓)', category: 'base' },
|
|
23
|
+
error: { row: 6, frames: 12, fps: 12, loop: false, screen: '#ff5d7a', label: 'Error (X)', category: 'base' },
|
|
24
|
+
alert: { row: 7, frames: 12, fps: 9, loop: true, screen: '#f2c14e', label: 'Alerta (!)', category: 'base' },
|
|
25
|
+
reading: { row: 8, frames: 16, fps: 8, loop: true, screen: '#4d86ff', label: 'Leyendo Libro 3D', category: 'expr' },
|
|
26
|
+
writing: { row: 9, frames: 12, fps: 10, loop: true, screen: '#3b6cff', label: 'Libreta y Lápiz', category: 'expr' },
|
|
27
|
+
searching: { row: 10, frames: 12, fps: 8, loop: true, screen: '#4d86ff', label: 'Lupa 3D', category: 'expr' },
|
|
28
|
+
running: { row: 11, frames: 15, fps: 10, loop: true, screen: '#3b6cff', label: 'Terminal en Vivo', category: 'expr' },
|
|
29
|
+
browsing: { row: 12, frames: 12, fps: 8, loop: true, screen: '#4d86ff', label: 'Globo Terráqueo', category: 'expr' },
|
|
30
|
+
planning: { row: 13, frames: 12, fps: 5, loop: true, screen: '#3ddc97', label: 'Checklist (✓)', category: 'expr' },
|
|
31
|
+
delegating: { row: 14, frames: 12, fps: 8, loop: true, screen: '#3b6cff', label: 'Mini-Olos 3D', category: 'expr' },
|
|
32
|
+
tool: { row: 15, frames: 10, fps: 10, loop: true, screen: '#3b6cff', label: 'Engrane 3D', category: 'expr' },
|
|
33
|
+
curious: { row: 16, frames: 10, fps: 10, loop: false, screen: '#f2c14e', label: 'Curioso', category: 'react' },
|
|
34
|
+
boop: { row: 17, frames: 10, fps: 16, loop: false, screen: '#ff5d7a', label: 'Boop (Corazón)', category: 'react' },
|
|
35
|
+
proud: { row: 18, frames: 12, fps: 12, loop: false, screen: '#3ddc97', label: 'Orgulloso (★)', category: 'react' },
|
|
36
|
+
yawn: { row: 19, frames: 12, fps: 8, loop: false, screen: '#4d86ff', label: 'Bostezo', category: 'react' },
|
|
37
|
+
look: { row: 20, frames: 16, fps: 1, loop: false, screen: '#3b6cff', label: 'Mirada 3D (5×3)', category: 'gaze' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Anticipación canónica de `character.js` (línea 44): [escalaX, escalaY] al entrar en un estado
|
|
41
|
+
const STATE_POP = {
|
|
42
|
+
needs_approval: [1.06, 0.92],
|
|
43
|
+
done: [1.05, 0.93],
|
|
44
|
+
error: [1.07, 0.90],
|
|
45
|
+
alert: [1.06, 0.92],
|
|
46
|
+
proud: [1.05, 0.94],
|
|
47
|
+
curious: [1.04, 0.95],
|
|
48
|
+
boop: [1.12, 0.86],
|
|
49
|
+
searching: [1.03, 0.96],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const OLO_TINTS = {
|
|
53
|
+
bone: { hex: null, name: 'Vinilo Hueso' },
|
|
54
|
+
mint: { hex: '#c4f2dc', name: 'Menta Neón' },
|
|
55
|
+
peach: { hex: '#ffd6bf', name: 'Durazno' },
|
|
56
|
+
lilac: { hex: '#ddd1ff', name: 'Lavanda' },
|
|
57
|
+
sky: { hex: '#c6e3ff', name: 'Cielo' },
|
|
58
|
+
butter: { hex: '#fff1b0', name: 'Oro Mantequilla' },
|
|
59
|
+
rose: { hex: '#ffcfdc', name: 'Rosa Drama' },
|
|
60
|
+
graphite: { hex: '#8e8e9c', name: 'Grafito' },
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const OLO_WEARS = [
|
|
64
|
+
'none', 'corona', 'gafas_sol', 'auriculares', 'mago', 'laurel',
|
|
65
|
+
'boina', 'casco', 'gafas', 'bufanda', 'aureola', 'flor', 'lazo'
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// 6 Visemas Fonéticos para Síntesis de Formantes (F1, F2) y pulso corporal silábico
|
|
69
|
+
const OLO_VISEMES = {
|
|
70
|
+
REST: { id: 'REST', open: 0.00, width: 1.00, round: 0.00, teeth: 0.00, f1: 300, f2: 1200, label: 'Reposo' },
|
|
71
|
+
A_AH: { id: 'A_AH', open: 0.92, width: 1.16, round: 0.22, teeth: 0.38, f1: 750, f2: 1180, label: 'Vocal Abierta (A)' },
|
|
72
|
+
E_EE: { id: 'E_EE', open: 0.50, width: 1.38, round: 0.05, teeth: 0.78, f1: 410, f2: 2150, label: 'Palatal (E/I)' },
|
|
73
|
+
O_U: { id: 'O_U', open: 0.78, width: 0.60, round: 0.96, teeth: 0.00, f1: 370, f2: 760, label: 'Redondeada (O/U)' },
|
|
74
|
+
M_B_P: { id: 'M_B_P', open: 0.08, width: 1.25, round: 0.00, teeth: 0.00, f1: 250, f2: 880, label: 'Bilabial (M/B/P)' },
|
|
75
|
+
F_V: { id: 'F_V', open: 0.28, width: 1.10, round: 0.12, teeth: 0.88, f1: 340, f2: 1680, label: 'Fricativa (F/S/T)' },
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const OLO_CAMERA_MODES = {
|
|
79
|
+
auto: { id: 'auto', name: 'Auto DOP (Inteligente por Beat)', yawDeg: 0 },
|
|
80
|
+
crash_zoom: { id: 'crash_zoom', name: 'Crash Zoom Dramático', yawDeg: 0 },
|
|
81
|
+
three_quarter_left: { id: 'three_quarter_left', name: 'Perspectiva 3/4 Izquierda', yawDeg: -14 },
|
|
82
|
+
three_quarter_right: { id: 'three_quarter_right', name: 'Perspectiva 3/4 Derecha', yawDeg: 14 },
|
|
83
|
+
orbit_turntable: { id: 'orbit_turntable', name: 'Vaivén 3D Suave', yawDeg: 0 },
|
|
84
|
+
snorricam: { id: 'snorricam', name: 'Snorricam (Anclada a Olo)', yawDeg: 0 },
|
|
85
|
+
hero_low: { id: 'hero_low', name: 'Contrapicado Héroe', yawDeg: -6 },
|
|
86
|
+
handheld_doc: { id: 'handheld_doc', name: 'Cámara en Mano (Perlin DOP)', yawDeg: 5 },
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Presets de alto impacto que aprovechan los 21 estados con utilería 3D de Olo
|
|
90
|
+
const PRESET_SHOWS = {
|
|
91
|
+
whatsapp_ex: {
|
|
92
|
+
id: 'whatsapp_ex',
|
|
93
|
+
title: 'El audio de las 3:14 AM',
|
|
94
|
+
category: 'Drama WhatsApp',
|
|
95
|
+
wear: 'gafas_sol',
|
|
96
|
+
tint: 'rose',
|
|
97
|
+
vibe: 'drama',
|
|
98
|
+
script: [
|
|
99
|
+
'Yo: Olo, mi ex me escribió "hola perdido" a las 3:14 de la mañana.',
|
|
100
|
+
'Olo: ¡Alerta roja! Bloquea el teclado y suelta ese teléfono ahora mismo.',
|
|
101
|
+
'Yo: Es que solo le voy a contestar "quién eres" para verme indignado.',
|
|
102
|
+
'Olo: ¡Mentira! En 4 minutos le estás escribiendo un testamento de 90 líneas.',
|
|
103
|
+
'Olo: Dignidad verificada en cero. Te decomiso el WhatsApp por 24 horas.'
|
|
104
|
+
].join('\n')
|
|
105
|
+
},
|
|
106
|
+
jefe_viernes: {
|
|
107
|
+
id: 'jefe_viernes',
|
|
108
|
+
title: 'Viernes 6:58 PM en la oficina',
|
|
109
|
+
category: 'Rugido de Oficina',
|
|
110
|
+
wear: 'casco',
|
|
111
|
+
tint: 'butter',
|
|
112
|
+
vibe: 'hype',
|
|
113
|
+
script: [
|
|
114
|
+
'Jefe: Equipo, ¿quién me apoya con un cambiecito súper rápido de 5 minutos?',
|
|
115
|
+
'Olo: Buscando en el diccionario: significa que vas a cenar frente al monitor.',
|
|
116
|
+
'Yo: Jefe, justo iba saliendo porque tengo una urgencia familiar.',
|
|
117
|
+
'Jefe: Pero si te vi subiendo historias en los tacos hace diez minutos.',
|
|
118
|
+
'Olo: ¡Impacto crítico! Cierra la terminal y corre sin mirar atrás.'
|
|
119
|
+
].join('\n')
|
|
120
|
+
},
|
|
121
|
+
cliente_caro: {
|
|
122
|
+
id: 'cliente_caro',
|
|
123
|
+
title: 'Mi sobrino lo hace más barato',
|
|
124
|
+
category: 'Cierre de Ventas',
|
|
125
|
+
wear: 'corona',
|
|
126
|
+
tint: 'mint',
|
|
127
|
+
vibe: 'victory',
|
|
128
|
+
script: [
|
|
129
|
+
'Cliente: Me encanta tu propuesta, pero mi sobrino me cobra 500 pesos en Canva.',
|
|
130
|
+
'Olo: Revisando el plan: respira hondo y no le lances el escritorio todavía.',
|
|
131
|
+
'Yo: Perfecto, encárgaselo a tu sobrino y nos vemos en un mes cuando falle.',
|
|
132
|
+
'Cliente: Bueno, espera, ¿si te pago hoy me incluyes los videos para TikTok?',
|
|
133
|
+
'Olo: ¡Jaque mate! Factura emitida con 30 por ciento extra por daño psicológico.'
|
|
134
|
+
].join('\n')
|
|
135
|
+
},
|
|
136
|
+
espejo_roast: {
|
|
137
|
+
id: 'espejo_roast',
|
|
138
|
+
title: 'Lo que tu IA calla de ti',
|
|
139
|
+
category: 'Espejo de IA',
|
|
140
|
+
wear: 'mago',
|
|
141
|
+
tint: 'lilac',
|
|
142
|
+
vibe: 'roast',
|
|
143
|
+
script: [
|
|
144
|
+
'Olo: Escaneé con lupa tus últimos 400 chats de inteligencia artificial.',
|
|
145
|
+
'Olo: Leyendo tu historial: el 68 por ciento son a las 2 de la mañana.',
|
|
146
|
+
'Yo: Es que soy un estratega global optimizando mi tiempo con mi equipo.',
|
|
147
|
+
'Olo: Le dijiste "por favor y gracias" 94 veces por miedo a que te conquiste.',
|
|
148
|
+
'Olo: ¡Nivel legendario! Top 3 por ciento de Latinoamérica. Reta a tu grupo.'
|
|
149
|
+
].join('\n')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const SPARRING_MISSIONS = {
|
|
154
|
+
sueldo_25: {
|
|
155
|
+
id: 'sueldo_25',
|
|
156
|
+
title: 'Súbeme el sueldo +25%',
|
|
157
|
+
subtitle: 'Negociación bajo presión · 60s',
|
|
158
|
+
lang: 'es-MX',
|
|
159
|
+
wear: 'corona',
|
|
160
|
+
tint: 'rose',
|
|
161
|
+
openingLine: 'No hay presupuesto este trimestre. Dime en 45 segundos por qué debería subirte el sueldo 25% hoy y no el próximo año.',
|
|
162
|
+
powerKeywords: ['resultados', 'ventas', 'ingresos', 'ahorro', 'clientes', 'entregué', 'meta', 'valor', 'mercado', 'oferta', 'retorno', 'lideré', 'producción', 'porcentaje', 'doble'],
|
|
163
|
+
weakKeywords: ['creo', 'tal vez', 'quizás', 'perdón', 'disculpa', 'necesito', 'gastos', 'renta', 'favor', 'ojalá'],
|
|
164
|
+
fillers: ['ehh', 'este', 'mmm', 'osea', 'bueno', 'pues'],
|
|
165
|
+
rebuttals: [
|
|
166
|
+
{ atScore: 35, state: 'needs_approval', text: 'Todos trabajan duro aquí. Dame un número concreto que hayas movido.' },
|
|
167
|
+
{ atScore: 65, state: 'planning', text: 'Okay, ese resultado sí me duele perderlo. ¿Qué propones cerrar este mes si te lo apruebo?' },
|
|
168
|
+
{ atScore: 85, state: 'proud', text: '¡Trato cerrado! Tienes tu 25% a partir del día primero.' }
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
cliente_objecion: {
|
|
172
|
+
id: 'cliente_objecion',
|
|
173
|
+
title: 'Rebate: "Está muy caro"',
|
|
174
|
+
subtitle: 'Cierre de ventas en frío · 60s',
|
|
175
|
+
lang: 'es-MX',
|
|
176
|
+
wear: 'gafas_sol',
|
|
177
|
+
tint: 'butter',
|
|
178
|
+
openingLine: 'Me gusta lo que haces, pero tu competencia cobra la mitad. Convénceme en 45 segundos sin bajar tu precio.',
|
|
179
|
+
powerKeywords: ['garantía', 'retorno', 'riesgo', 'tiempo', 'calidad', 'clientes', 'resultados', 'costo', 'inversión', 'barato', 'caro', 'mes', 'hoy', 'implementación'],
|
|
180
|
+
weakKeywords: ['descuento', 'rebaja', 'barato', 'mitad', 'bueno te bajo', 'perdón', 'quizás'],
|
|
181
|
+
fillers: ['ehh', 'este', 'mmm', 'osea', 'digamos'],
|
|
182
|
+
rebuttals: [
|
|
183
|
+
{ atScore: 35, state: 'alert', text: 'No me hables de características. ¿Cuánto dinero pierdo si me voy con el barato?' },
|
|
184
|
+
{ atScore: 65, state: 'searching', text: 'Tienes un punto con el costo del retrabajo. ¿En cuántos días veo el primer entregable?' },
|
|
185
|
+
{ atScore: 85, state: 'proud', text: 'Mándame la liga de pago ahora mismo. Cerrado a precio completo.' }
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
english_interview: {
|
|
189
|
+
id: 'english_interview',
|
|
190
|
+
title: 'Silicon Valley Interview',
|
|
191
|
+
subtitle: 'English Under Pressure · 60s',
|
|
192
|
+
lang: 'en-US',
|
|
193
|
+
wear: 'auriculares',
|
|
194
|
+
tint: 'sky',
|
|
195
|
+
openingLine: 'We have 200 applicants for this remote role. In 45 seconds, tell me the hardest problem you solved and why we should hire you today.',
|
|
196
|
+
powerKeywords: ['built', 'shipped', 'scaled', 'reduced', 'increased', 'users', 'revenue', 'latency', 'production', 'team', 'automated', 'solved', 'impact', 'percent', 'days'],
|
|
197
|
+
weakKeywords: ['maybe', 'sorry', 'try', 'little', 'hoping', 'guess', 'dont know'],
|
|
198
|
+
fillers: ['um', 'uh', 'like', 'basically', 'literally'],
|
|
199
|
+
rebuttals: [
|
|
200
|
+
{ atScore: 35, state: 'needs_approval', text: 'Skip the resume buzzwords. What actually broke and how did you fix it?' },
|
|
201
|
+
{ atScore: 65, state: 'running', text: 'Strong execution. How fast can you ship your first feature if you start Monday?' },
|
|
202
|
+
{ atScore: 85, state: 'proud', text: 'You are hired. Expect the offer letter in your inbox tonight!' }
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fnv1aHex(str) {
|
|
208
|
+
let h1 = 0xdeadbeef ^ str.length
|
|
209
|
+
let h2 = 0x41c6ce57 ^ str.length
|
|
210
|
+
for (let i = 0; i < str.length; i++) {
|
|
211
|
+
const ch = str.charCodeAt(i)
|
|
212
|
+
h1 = Math.imul(h1 ^ ch, 2654435761)
|
|
213
|
+
h2 = Math.imul(h2 ^ ch, 1597334677)
|
|
214
|
+
}
|
|
215
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
|
216
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
|
217
|
+
return (h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hashFloat(n) {
|
|
221
|
+
const x = Math.sin(n * 127.1 + 311.7) * 43758.5453123
|
|
222
|
+
return x - Math.floor(x)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function noise1D(x) {
|
|
226
|
+
const i = Math.floor(x)
|
|
227
|
+
const f = x - i
|
|
228
|
+
const u = f * f * (3 - 2 * f)
|
|
229
|
+
return (hashFloat(i) * (1 - u) + hashFloat(i + 1) * u) * 2 - 1
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fractalNoise1D(x, seed = 0) {
|
|
233
|
+
const s = x + seed * 19.37
|
|
234
|
+
return 0.58 * noise1D(s) + 0.28 * noise1D(s * 2.13 + 7.1) + 0.14 * noise1D(s * 4.41 + 13.7)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function computeVisemesForWord(cleanWord) {
|
|
238
|
+
const w = (cleanWord || '').toLowerCase()
|
|
239
|
+
if (!w) return [{ viseme: 'A_AH', p0: 0, p1: 1 }]
|
|
240
|
+
|
|
241
|
+
const tokens = []
|
|
242
|
+
let i = 0
|
|
243
|
+
while (i < w.length) {
|
|
244
|
+
const ch = w[i]
|
|
245
|
+
const next = w[i + 1] || ''
|
|
246
|
+
if ('mbp'.includes(ch)) {
|
|
247
|
+
tokens.push('M_B_P')
|
|
248
|
+
i += next === ch ? 2 : 1
|
|
249
|
+
} else if ('fvszcjxtdlrnñ'.includes(ch)) {
|
|
250
|
+
tokens.push('F_V')
|
|
251
|
+
while (i + 1 < w.length && 'fvszcjxtdlrnñh'.includes(w[i + 1])) i++
|
|
252
|
+
i++
|
|
253
|
+
} else if ('aáä'.includes(ch)) {
|
|
254
|
+
tokens.push('A_AH')
|
|
255
|
+
i++
|
|
256
|
+
} else if ('eéëiíïy'.includes(ch)) {
|
|
257
|
+
tokens.push('E_EE')
|
|
258
|
+
i++
|
|
259
|
+
} else if ('oóöuúüw'.includes(ch)) {
|
|
260
|
+
tokens.push('O_U')
|
|
261
|
+
i++
|
|
262
|
+
} else if (/\d/.test(ch)) {
|
|
263
|
+
tokens.push((ch.charCodeAt(0) % 2 === 0) ? 'O_U' : 'E_EE')
|
|
264
|
+
i++
|
|
265
|
+
} else {
|
|
266
|
+
i++
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!tokens.length) tokens.push('A_AH')
|
|
271
|
+
const compact = []
|
|
272
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
273
|
+
if (compact[compact.length - 1] !== tokens[k]) compact.push(tokens[k])
|
|
274
|
+
if (compact.length >= 6) break
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const weights = compact.map((v) => (v === 'M_B_P' ? 0.55 : v === 'F_V' ? 0.65 : 1.35))
|
|
278
|
+
const totalW = weights.reduce((a, b) => a + b, 0)
|
|
279
|
+
let cursor = 0
|
|
280
|
+
return compact.map((v, idx) => {
|
|
281
|
+
const span = weights[idx] / totalW
|
|
282
|
+
const seg = {
|
|
283
|
+
viseme: v,
|
|
284
|
+
p0: Number(cursor.toFixed(3)),
|
|
285
|
+
p1: Number(Math.min(1, cursor + span).toFixed(3)),
|
|
286
|
+
f1: OLO_VISEMES[v].f1,
|
|
287
|
+
f2: OLO_VISEMES[v].f2,
|
|
288
|
+
}
|
|
289
|
+
cursor += span
|
|
290
|
+
return seg
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Clasifica cada línea del guion usando los 21 estados 3D reales de Olo (`manifest.json`)
|
|
296
|
+
* y asigna los efectos de `motion.js` (`orb`, `paper`, `memory`, `burst`, `gold`).
|
|
297
|
+
*/
|
|
298
|
+
function classifyBeat(lineText, speaker, idx, total) {
|
|
299
|
+
const low = lineText.toLowerCase()
|
|
300
|
+
const isExclaim = lineText.includes('!') || lineText.includes('¡')
|
|
301
|
+
const isQuestion = lineText.includes('?') || lineText.includes('¿')
|
|
302
|
+
|
|
303
|
+
let state = 'idle'
|
|
304
|
+
let cam = 'medium'
|
|
305
|
+
let sfx = 'blip'
|
|
306
|
+
let highlightColor = '#f2c14e'
|
|
307
|
+
let turntableYaw = 0
|
|
308
|
+
let prosody = 'neutral'
|
|
309
|
+
let motionEvent = 'step' // 'step' | 'paper' | 'memory' | 'burst' | 'error' | 'gold'
|
|
310
|
+
|
|
311
|
+
if (/alerta|bloquea|peligro|cuidado|urgencia|impacto/.test(low)) {
|
|
312
|
+
state = 'alert'
|
|
313
|
+
cam = 'crash_zoom'
|
|
314
|
+
sfx = 'impact'
|
|
315
|
+
highlightColor = '#ff5d7a'
|
|
316
|
+
turntableYaw = -10
|
|
317
|
+
prosody = 'excited'
|
|
318
|
+
motionEvent = 'step'
|
|
319
|
+
} else if (/mentira|colapso|error|cero|decomiso|daño|falle|miedo|quiebra|crisis|fatal/.test(low)) {
|
|
320
|
+
state = 'error'
|
|
321
|
+
cam = 'shake'
|
|
322
|
+
sfx = 'thunk'
|
|
323
|
+
highlightColor = '#ff5d7a'
|
|
324
|
+
turntableYaw = 12
|
|
325
|
+
prosody = 'tense'
|
|
326
|
+
motionEvent = 'error'
|
|
327
|
+
} else if (/increíble|increible|wow|épico|epico|brutal|celebrar|victoria|ganamos|celebrate|amazing|legendary/.test(low)) {
|
|
328
|
+
state = 'proud'
|
|
329
|
+
cam = 'hero_low'
|
|
330
|
+
sfx = 'bell'
|
|
331
|
+
highlightColor = '#3ddc97'
|
|
332
|
+
turntableYaw = -12
|
|
333
|
+
prosody = 'confident'
|
|
334
|
+
motionEvent = 'gold'
|
|
335
|
+
} else if (/jaque mate|nivel legendario|top |coronando|ganaste|contratado|hired/.test(low)) {
|
|
336
|
+
state = 'proud'
|
|
337
|
+
cam = 'hero_low'
|
|
338
|
+
sfx = 'bell'
|
|
339
|
+
highlightColor = '#3ddc97'
|
|
340
|
+
turntableYaw = -12
|
|
341
|
+
prosody = 'confident'
|
|
342
|
+
motionEvent = 'gold'
|
|
343
|
+
} else if (/cerrado|trato|éxito|perfecto|terminada/.test(low)) {
|
|
344
|
+
state = 'done'
|
|
345
|
+
cam = 'hero_low'
|
|
346
|
+
sfx = 'bell'
|
|
347
|
+
highlightColor = '#3ddc97'
|
|
348
|
+
turntableYaw = -8
|
|
349
|
+
prosody = 'confident'
|
|
350
|
+
motionEvent = 'burst'
|
|
351
|
+
} else if (/boop|corazón|corazon|te quiero|abracito|cariño|amor|love/.test(low)) {
|
|
352
|
+
state = 'boop'
|
|
353
|
+
cam = 'closeup'
|
|
354
|
+
sfx = 'bell'
|
|
355
|
+
highlightColor = '#ff5d7a'
|
|
356
|
+
turntableYaw = 0
|
|
357
|
+
prosody = 'confident'
|
|
358
|
+
motionEvent = 'burst'
|
|
359
|
+
} else if (/bostezo|bostezar|aburrido|flojera|sueñito|yawn|boring/.test(low)) {
|
|
360
|
+
state = 'yawn'
|
|
361
|
+
cam = 'medium'
|
|
362
|
+
sfx = 'whoosh'
|
|
363
|
+
highlightColor = '#4d86ff'
|
|
364
|
+
turntableYaw = 6
|
|
365
|
+
prosody = 'neutral'
|
|
366
|
+
motionEvent = 'step'
|
|
367
|
+
} else if (/dormir|durmiendo|a la cama|buenas noches|sleep|sleeping|zzz/.test(low)) {
|
|
368
|
+
state = 'sleeping'
|
|
369
|
+
cam = 'wide_in'
|
|
370
|
+
sfx = 'whoosh'
|
|
371
|
+
highlightColor = '#4d86ff'
|
|
372
|
+
turntableYaw = 4
|
|
373
|
+
prosody = 'neutral'
|
|
374
|
+
motionEvent = 'step'
|
|
375
|
+
} else if (/escaneé|lupa|buscando|diccionario|auditoría|investigué/.test(low)) {
|
|
376
|
+
state = 'searching'
|
|
377
|
+
cam = 'closeup'
|
|
378
|
+
sfx = 'whoosh'
|
|
379
|
+
highlightColor = '#4d86ff'
|
|
380
|
+
turntableYaw = 10
|
|
381
|
+
prosody = 'neutral'
|
|
382
|
+
motionEvent = 'memory'
|
|
383
|
+
} else if (/leyendo|historial|libro|leí|mensajes/.test(low)) {
|
|
384
|
+
state = 'reading'
|
|
385
|
+
cam = 'dutch_right'
|
|
386
|
+
sfx = 'pluck'
|
|
387
|
+
highlightColor = '#8fb3ff'
|
|
388
|
+
turntableYaw = -8
|
|
389
|
+
prosody = 'neutral'
|
|
390
|
+
motionEvent = 'paper'
|
|
391
|
+
} else if (/escribiendo|testamento|redacten|correos|libreta/.test(low)) {
|
|
392
|
+
state = 'writing'
|
|
393
|
+
cam = 'closeup'
|
|
394
|
+
sfx = 'blip'
|
|
395
|
+
highlightColor = '#4d86ff'
|
|
396
|
+
turntableYaw = 12
|
|
397
|
+
prosody = 'excited'
|
|
398
|
+
motionEvent = 'paper'
|
|
399
|
+
} else if (/revisando el plan|propuesta|factura|checklist|meta/.test(low)) {
|
|
400
|
+
state = 'planning'
|
|
401
|
+
cam = 'dutch_right'
|
|
402
|
+
sfx = 'pluck'
|
|
403
|
+
highlightColor = '#3ddc97'
|
|
404
|
+
turntableYaw = -10
|
|
405
|
+
prosody = 'confident'
|
|
406
|
+
motionEvent = 'paper'
|
|
407
|
+
} else if (/terminal|código|monitor|sistema|pipeline/.test(low)) {
|
|
408
|
+
state = 'running'
|
|
409
|
+
cam = 'crash_zoom'
|
|
410
|
+
sfx = 'whoosh'
|
|
411
|
+
highlightColor = '#3b6cff'
|
|
412
|
+
turntableYaw = 8
|
|
413
|
+
prosody = 'excited'
|
|
414
|
+
motionEvent = 'step'
|
|
415
|
+
} else if (/global|latinoamérica|mundo|internacional|tacos/.test(low)) {
|
|
416
|
+
state = 'browsing'
|
|
417
|
+
cam = 'medium'
|
|
418
|
+
sfx = 'whoosh'
|
|
419
|
+
highlightColor = '#4d86ff'
|
|
420
|
+
turntableYaw = -9
|
|
421
|
+
prosody = 'neutral'
|
|
422
|
+
motionEvent = 'memory'
|
|
423
|
+
} else if (/equipo|grupo|subagentes|apoya|todos/.test(low)) {
|
|
424
|
+
state = 'delegating'
|
|
425
|
+
cam = 'wide_in'
|
|
426
|
+
sfx = 'pluck'
|
|
427
|
+
highlightColor = '#8fb3ff'
|
|
428
|
+
turntableYaw = 8
|
|
429
|
+
prosody = 'neutral'
|
|
430
|
+
motionEvent = 'step'
|
|
431
|
+
} else if (/optimizando|engrane|herramienta|producción/.test(low)) {
|
|
432
|
+
state = 'tool'
|
|
433
|
+
cam = 'dutch_right'
|
|
434
|
+
sfx = 'blip'
|
|
435
|
+
highlightColor = '#3b6cff'
|
|
436
|
+
turntableYaw = 10
|
|
437
|
+
prosody = 'neutral'
|
|
438
|
+
motionEvent = 'step'
|
|
439
|
+
} else if (/quién eres|indignado|hola perdido/.test(low)) {
|
|
440
|
+
state = 'curious'
|
|
441
|
+
cam = 'closeup'
|
|
442
|
+
sfx = 'pluck'
|
|
443
|
+
highlightColor = '#f2c14e'
|
|
444
|
+
turntableYaw = -12
|
|
445
|
+
prosody = 'question'
|
|
446
|
+
motionEvent = 'memory'
|
|
447
|
+
} else if (isQuestion && /mira|ves|viste|fíjate|fijate|ojos|look|see/.test(low)) {
|
|
448
|
+
state = 'look'
|
|
449
|
+
cam = 'closeup'
|
|
450
|
+
sfx = 'pluck'
|
|
451
|
+
highlightColor = '#3b6cff'
|
|
452
|
+
turntableYaw = -8
|
|
453
|
+
prosody = 'question'
|
|
454
|
+
motionEvent = 'memory'
|
|
455
|
+
} else if (isQuestion && /quién|quien|cómo|como|por qué|por que|cuál|cual|dónde|donde|cuándo|cuando|who|how|why|what|where/.test(low)) {
|
|
456
|
+
state = 'curious'
|
|
457
|
+
cam = 'closeup'
|
|
458
|
+
sfx = 'pluck'
|
|
459
|
+
highlightColor = '#f2c14e'
|
|
460
|
+
turntableYaw = -10
|
|
461
|
+
prosody = 'question'
|
|
462
|
+
motionEvent = 'memory'
|
|
463
|
+
} else if (isQuestion || /cambiecito|sobrino|espera|seguro/.test(low)) {
|
|
464
|
+
state = 'needs_approval'
|
|
465
|
+
cam = 'closeup'
|
|
466
|
+
sfx = 'pluck'
|
|
467
|
+
highlightColor = '#f2c14e'
|
|
468
|
+
turntableYaw = -10
|
|
469
|
+
prosody = 'question'
|
|
470
|
+
motionEvent = 'step'
|
|
471
|
+
} else if (idx === 0) {
|
|
472
|
+
state = 'thinking'
|
|
473
|
+
cam = 'wide_in'
|
|
474
|
+
sfx = 'whoosh'
|
|
475
|
+
highlightColor = '#8fb3ff'
|
|
476
|
+
turntableYaw = 6
|
|
477
|
+
prosody = 'neutral'
|
|
478
|
+
motionEvent = 'memory'
|
|
479
|
+
} else if (isExclaim) {
|
|
480
|
+
state = 'alert'
|
|
481
|
+
cam = 'closeup'
|
|
482
|
+
sfx = 'impact'
|
|
483
|
+
highlightColor = '#f2c14e'
|
|
484
|
+
turntableYaw = -8
|
|
485
|
+
prosody = 'excited'
|
|
486
|
+
motionEvent = 'step'
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (idx === total - 1 && (state === 'idle' || state === 'working')) {
|
|
490
|
+
state = 'proud'
|
|
491
|
+
cam = 'crash_zoom'
|
|
492
|
+
sfx = 'bell'
|
|
493
|
+
highlightColor = '#3ddc97'
|
|
494
|
+
turntableYaw = 0
|
|
495
|
+
prosody = 'confident'
|
|
496
|
+
motionEvent = 'burst'
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const speakerLow = (speaker || '').toLowerCase()
|
|
500
|
+
let voicePitch = 290
|
|
501
|
+
if (speakerLow.includes('olo')) voicePitch = 340
|
|
502
|
+
else if (speakerLow.includes('jefe') || speakerLow.includes('cliente') || speakerLow.includes('ex')) voicePitch = 195
|
|
503
|
+
else voicePitch = 250
|
|
504
|
+
|
|
505
|
+
if (prosody === 'excited') voicePitch = Math.round(voicePitch * Math.pow(2, 3.0 / 12))
|
|
506
|
+
else if (prosody === 'tense') voicePitch = Math.round(voicePitch * Math.pow(2, 1.5 / 12))
|
|
507
|
+
else if (prosody === 'confident') voicePitch = Math.round(voicePitch * Math.pow(2, -1.0 / 12))
|
|
508
|
+
|
|
509
|
+
return { state, cam, sfx, highlightColor, voicePitch, turntableYaw, prosody, motionEvent }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function buildWordTimings(text, tStart, tEnd) {
|
|
513
|
+
const rawWords = text.trim().split(/\s+/).filter(Boolean)
|
|
514
|
+
if (!rawWords.length) return []
|
|
515
|
+
const span = Math.max(0.6, tEnd - tStart - 0.18)
|
|
516
|
+
const weights = rawWords.map((w) => {
|
|
517
|
+
let wt = Math.max(1.2, Math.min(4.2, w.length * 0.38))
|
|
518
|
+
if (/[,.:;!?¡¿]/.test(w)) wt += 0.65
|
|
519
|
+
return wt
|
|
520
|
+
})
|
|
521
|
+
const totalW = weights.reduce((a, b) => a + b, 0)
|
|
522
|
+
let cursor = tStart + 0.06
|
|
523
|
+
return rawWords.map((w, i) => {
|
|
524
|
+
const dur = (weights[i] / totalW) * span
|
|
525
|
+
const clean = w.replace(/[^\p{L}\p{N}%]/gu, '')
|
|
526
|
+
const visemes = computeVisemesForWord(clean)
|
|
527
|
+
const item = {
|
|
528
|
+
word: w,
|
|
529
|
+
clean,
|
|
530
|
+
t0: Number(cursor.toFixed(3)),
|
|
531
|
+
t1: Number((cursor + dur).toFixed(3)),
|
|
532
|
+
emphasis: /[A-ZÁÉÍÓÚ]{3,}|[!¡?¿%0-9]/.test(w) || w.length >= 8,
|
|
533
|
+
isQuestionWord: /[?¿]/.test(w),
|
|
534
|
+
isExclaimWord: /[!¡]/.test(w),
|
|
535
|
+
visemes,
|
|
536
|
+
}
|
|
537
|
+
cursor += dur
|
|
538
|
+
return item
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const VALID_SCENARIOS = [
|
|
543
|
+
'studio', 'lofi', 'gamer', 'podcast', 'news', 'car', 'court', 'comedy', 'sunset'
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
function normalizeScenario(rawScenario) {
|
|
547
|
+
if (!rawScenario || typeof rawScenario !== 'string') return 'lofi'
|
|
548
|
+
const clean = rawScenario.trim().toLowerCase()
|
|
549
|
+
if (clean === 'lofi_night') return 'lofi'
|
|
550
|
+
return VALID_SCENARIOS.includes(clean) ? clean : 'lofi'
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Compila un guion o conversación en un `SceneSpec v1` determinista con los 21 estados 3D de Olo.
|
|
555
|
+
*/
|
|
556
|
+
function compileScene(opts = {}) {
|
|
557
|
+
const rawScript = String(opts.script || opts.scriptText || PRESET_SHOWS.whatsapp_ex.script).trim()
|
|
558
|
+
const requestedMaxLines = Number(opts.maxLines)
|
|
559
|
+
const maxLines = Number.isFinite(requestedMaxLines) && requestedMaxLines > 0
|
|
560
|
+
? Math.max(1, Math.min(30, Math.floor(requestedMaxLines)))
|
|
561
|
+
: 10
|
|
562
|
+
const rawLines = rawScript
|
|
563
|
+
.split(/\r?\n/)
|
|
564
|
+
.map((l) => l.trim())
|
|
565
|
+
.filter(Boolean)
|
|
566
|
+
.slice(0, maxLines)
|
|
567
|
+
|
|
568
|
+
const wear = OLO_WEARS.includes(opts.wear) ? opts.wear : 'gafas_sol'
|
|
569
|
+
const tint = OLO_TINTS[opts.tint] ? opts.tint : 'rose'
|
|
570
|
+
const finish = opts.finish || opts.collectorFinish || 'vinyl'
|
|
571
|
+
const title = (opts.title || 'Olo Studio Show').slice(0, 60)
|
|
572
|
+
const category = (opts.category || 'Show 9:16').slice(0, 32)
|
|
573
|
+
const vibe = opts.vibe || 'drama'
|
|
574
|
+
const scenario = normalizeScenario(opts.scenario)
|
|
575
|
+
const cameraOverride = OLO_CAMERA_MODES[opts.cameraOverride] ? opts.cameraOverride : 'auto'
|
|
576
|
+
const stateOverride = opts.stateOverride && OLO_STATES[opts.stateOverride] ? opts.stateOverride : null
|
|
577
|
+
const duelConfig = (opts.duelConfig && typeof opts.duelConfig === 'object') ? Object.assign({}, opts.duelConfig) : null
|
|
578
|
+
const voiceFx = opts.voiceFx
|
|
579
|
+
? (typeof opts.voiceFx === 'object' ? Object.assign({}, opts.voiceFx) : String(opts.voiceFx))
|
|
580
|
+
: 'none'
|
|
581
|
+
const drillBeat = opts.drillBeat
|
|
582
|
+
? (typeof opts.drillBeat === 'object' ? Object.assign({}, opts.drillBeat) : opts.drillBeat)
|
|
583
|
+
: null
|
|
584
|
+
const customOutro = (opts.customOutro && typeof opts.customOutro === 'object') ? Object.assign({}, opts.customOutro) : null
|
|
585
|
+
const pipMeta = (opts.pipMeta && typeof opts.pipMeta === 'object') ? Object.assign({}, opts.pipMeta) : null
|
|
586
|
+
|
|
587
|
+
const beats = []
|
|
588
|
+
let tCursor = 0.45
|
|
589
|
+
|
|
590
|
+
rawLines.forEach((raw, idx) => {
|
|
591
|
+
let speaker = 'Olo'
|
|
592
|
+
let text = raw
|
|
593
|
+
const colonIdx = raw.indexOf(':')
|
|
594
|
+
if (colonIdx > 0 && colonIdx < 18) {
|
|
595
|
+
speaker = raw.slice(0, colonIdx).trim()
|
|
596
|
+
text = raw.slice(colonIdx + 1).trim()
|
|
597
|
+
}
|
|
598
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length
|
|
599
|
+
const duration = Math.max(2.1, Math.min(4.4, 0.75 + wordCount * 0.27))
|
|
600
|
+
const t0 = Number(tCursor.toFixed(3))
|
|
601
|
+
const t1 = Number((tCursor + duration).toFixed(3))
|
|
602
|
+
const meta = classifyBeat(text, speaker, idx, rawLines.length)
|
|
603
|
+
const words = buildWordTimings(text, t0, t1)
|
|
604
|
+
|
|
605
|
+
beats.push({
|
|
606
|
+
index: idx,
|
|
607
|
+
speaker,
|
|
608
|
+
isOlo: speaker.toLowerCase().includes('olo'),
|
|
609
|
+
text,
|
|
610
|
+
t0,
|
|
611
|
+
t1,
|
|
612
|
+
duration: Number(duration.toFixed(3)),
|
|
613
|
+
state: stateOverride || meta.state,
|
|
614
|
+
cam: cameraOverride !== 'auto' ? cameraOverride : meta.cam,
|
|
615
|
+
sfx: meta.sfx,
|
|
616
|
+
highlightColor: meta.highlightColor,
|
|
617
|
+
voicePitch: meta.voicePitch,
|
|
618
|
+
turntableYaw: meta.turntableYaw,
|
|
619
|
+
prosody: meta.prosody,
|
|
620
|
+
motionEvent: meta.motionEvent,
|
|
621
|
+
words,
|
|
622
|
+
})
|
|
623
|
+
tCursor = t1 + 0.12
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
const outroDuration = 1.35
|
|
627
|
+
const totalDuration = Number((tCursor + outroDuration).toFixed(3))
|
|
628
|
+
const canonicalPayload = JSON.stringify({
|
|
629
|
+
title,
|
|
630
|
+
category,
|
|
631
|
+
wear,
|
|
632
|
+
tint,
|
|
633
|
+
finish,
|
|
634
|
+
scenario,
|
|
635
|
+
cameraOverride,
|
|
636
|
+
stateOverride,
|
|
637
|
+
beats: beats.map((b) => [b.speaker, b.text, b.state, b.cam])
|
|
638
|
+
})
|
|
639
|
+
const sceneHash = fnv1aHex(canonicalPayload)
|
|
640
|
+
|
|
641
|
+
const sceneSpec = {
|
|
642
|
+
schema: 'olomni.studio.scene.v1',
|
|
643
|
+
sceneHash,
|
|
644
|
+
title,
|
|
645
|
+
category,
|
|
646
|
+
wear: wear === 'none' ? null : wear,
|
|
647
|
+
tint,
|
|
648
|
+
finish,
|
|
649
|
+
vibe,
|
|
650
|
+
scenario,
|
|
651
|
+
cameraOverride,
|
|
652
|
+
stateOverride,
|
|
653
|
+
duelConfig,
|
|
654
|
+
voiceFx,
|
|
655
|
+
drillBeat,
|
|
656
|
+
customOutro,
|
|
657
|
+
pipMeta,
|
|
658
|
+
duration: totalDuration,
|
|
659
|
+
outroStart: Number(tCursor.toFixed(3)),
|
|
660
|
+
beats,
|
|
661
|
+
}
|
|
662
|
+
if (typeof opts.watermark === 'boolean') {
|
|
663
|
+
sceneSpec.watermark = opts.watermark
|
|
664
|
+
}
|
|
665
|
+
return sceneSpec
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Wrapper canónico para compilación desde texto libre (`script` o `scriptText`).
|
|
670
|
+
*/
|
|
671
|
+
function compileSceneFromScript(opts = {}) {
|
|
672
|
+
const safeOpts = opts && typeof opts === 'object' ? opts : {}
|
|
673
|
+
return compileScene(Object.assign({}, safeOpts, {
|
|
674
|
+
script: safeOpts.script || safeOpts.scriptText,
|
|
675
|
+
}))
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Evalúa la curva de anticipación canónica `POP` de `character.js` (duración 340ms al cambiar de estado).
|
|
680
|
+
*/
|
|
681
|
+
function evalCanonicalPop(stateName, elapsedSec) {
|
|
682
|
+
const p = STATE_POP[stateName]
|
|
683
|
+
if (!p || elapsedSec < 0 || elapsedSec >= 0.34) return [1, 1]
|
|
684
|
+
const u = elapsedSec / 0.34
|
|
685
|
+
const [sx0, sy0] = p
|
|
686
|
+
const sx1 = 2 - sx0
|
|
687
|
+
const sy1 = 2 - sy0
|
|
688
|
+
if (u < 0.35) {
|
|
689
|
+
const k = u / 0.35
|
|
690
|
+
return [1 + (sx0 - 1) * k, 1 + (sy0 - 1) * k]
|
|
691
|
+
} else if (u < 0.70) {
|
|
692
|
+
const k = (u - 0.35) / 0.35
|
|
693
|
+
return [sx0 + (sx1 - sx0) * k, sy0 + (sy1 - sy0) * k]
|
|
694
|
+
} else {
|
|
695
|
+
const k = (u - 0.70) / 0.30
|
|
696
|
+
return [sx1 + (1 - sx1) * k, sy1 + (1 - sy1) * k]
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function computeBodyKinematicsAt(scene, tau) {
|
|
701
|
+
let activeBeat = null
|
|
702
|
+
let prevBeat = null
|
|
703
|
+
for (let i = 0; i < scene.beats.length; i++) {
|
|
704
|
+
const b = scene.beats[i]
|
|
705
|
+
if (tau >= b.t0 && tau <= b.t1 + 0.12) {
|
|
706
|
+
activeBeat = b
|
|
707
|
+
prevBeat = i > 0 ? scene.beats[i - 1] : null
|
|
708
|
+
break
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!activeBeat && scene.beats.length && tau < scene.outroStart) {
|
|
712
|
+
activeBeat = scene.beats[0]
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let activeWord = null
|
|
716
|
+
let wordIndex = -1
|
|
717
|
+
if (activeBeat) {
|
|
718
|
+
for (let i = 0; i < activeBeat.words.length; i++) {
|
|
719
|
+
const w = activeBeat.words[i]
|
|
720
|
+
if (tau >= w.t0 && tau <= w.t1) {
|
|
721
|
+
activeWord = w
|
|
722
|
+
wordIndex = i
|
|
723
|
+
break
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const stateName = tau >= scene.outroStart ? 'proud' : (activeBeat ? activeBeat.state : 'idle')
|
|
729
|
+
const beatElapsed = activeBeat ? Math.max(0, tau - activeBeat.t0) : tau
|
|
730
|
+
|
|
731
|
+
// 1. Curva `POP` canónica de `character.js` al entrar a un nuevo estado
|
|
732
|
+
const [popX, popY] = evalCanonicalPop(stateName, beatElapsed)
|
|
733
|
+
let squashX = popX
|
|
734
|
+
let stretchY = 1 / squashX
|
|
735
|
+
let headTiltDeg = 0
|
|
736
|
+
let bodyOffsetY = 0
|
|
737
|
+
|
|
738
|
+
// 2. Pulso silábico sutil y cohesivo en todo el cuerpo 3D (anclado en los pies `FEET = 0.955`)
|
|
739
|
+
let mouthOpen = 0
|
|
740
|
+
let visemeId = 'REST'
|
|
741
|
+
let visemeShape = OLO_VISEMES.REST
|
|
742
|
+
|
|
743
|
+
if (activeWord && activeBeat) {
|
|
744
|
+
const wordDur = Math.max(0.05, activeWord.t1 - activeWord.t0)
|
|
745
|
+
const wordProgress = Math.max(0, Math.min(1, (tau - activeWord.t0) / wordDur))
|
|
746
|
+
|
|
747
|
+
const segs = activeWord.visemes || [{ viseme: 'A_AH', p0: 0, p1: 1 }]
|
|
748
|
+
let chosenSeg = segs[0]
|
|
749
|
+
for (let s = 0; s < segs.length; s++) {
|
|
750
|
+
if (wordProgress >= segs[s].p0 && wordProgress <= segs[s].p1) {
|
|
751
|
+
chosenSeg = segs[s]
|
|
752
|
+
break
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
visemeId = chosenSeg.viseme
|
|
756
|
+
const targetV = OLO_VISEMES[visemeId] || OLO_VISEMES.A_AH
|
|
757
|
+
|
|
758
|
+
const segSpan = Math.max(0.08, chosenSeg.p1 - chosenSeg.p0)
|
|
759
|
+
const localP = Math.max(0, Math.min(1, (wordProgress - chosenSeg.p0) / segSpan))
|
|
760
|
+
const segEnv = Math.sin(localP * Math.PI)
|
|
761
|
+
const emphasisBoost = activeWord.emphasis ? 1.2 : 1.0
|
|
762
|
+
|
|
763
|
+
mouthOpen = Math.min(1, (0.35 + 0.65 * segEnv) * targetV.open * emphasisBoost)
|
|
764
|
+
visemeShape = {
|
|
765
|
+
id: visemeId,
|
|
766
|
+
open: Number(mouthOpen.toFixed(3)),
|
|
767
|
+
width: Number((1.0 + (targetV.width - 1.0) * (0.4 + 0.6 * segEnv)).toFixed(3)),
|
|
768
|
+
round: Number((targetV.round * (0.45 + 0.55 * segEnv)).toFixed(3)),
|
|
769
|
+
teeth: Number((targetV.teeth * segEnv).toFixed(3)),
|
|
770
|
+
f1: targetV.f1,
|
|
771
|
+
f2: targetV.f2,
|
|
772
|
+
label: targetV.label,
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// El pulso de voz comprime/estira suavemente a Olo conservando el área (squashX * stretchY = 1.0)
|
|
776
|
+
const roleFactor = activeBeat.isOlo ? 1.0 : 0.45
|
|
777
|
+
const vocalSquash = 1 + mouthOpen * 0.034 * roleFactor
|
|
778
|
+
squashX *= vocalSquash
|
|
779
|
+
stretchY = 1 / squashX
|
|
780
|
+
headTiltDeg = Math.sin(tau * 6.8 + wordIndex * 1.1) * (activeWord.emphasis ? 2.4 : 1.1) * roleFactor
|
|
781
|
+
bodyOffsetY = -mouthOpen * 5.0 * roleFactor
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
activeBeat,
|
|
786
|
+
prevBeat,
|
|
787
|
+
activeWord,
|
|
788
|
+
wordIndex,
|
|
789
|
+
stateName,
|
|
790
|
+
beatElapsed,
|
|
791
|
+
mouthOpen,
|
|
792
|
+
visemeId,
|
|
793
|
+
visemeShape,
|
|
794
|
+
squashX,
|
|
795
|
+
stretchY,
|
|
796
|
+
headTiltDeg,
|
|
797
|
+
bodyOffsetY,
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Evalúa el estado completo de la escena en el instante `t` (segundos).
|
|
803
|
+
* Incluye transiciones `FADE = 170ms` entre estados (como `character.js`), telemetría de `motion.js` y cámara DOP.
|
|
804
|
+
*/
|
|
805
|
+
function sampleSceneAt(scene, t) {
|
|
806
|
+
const clampedT = Math.max(0, Math.min(scene.duration, Number(t) || 0))
|
|
807
|
+
const isIntro = clampedT < (scene.beats[0] ? scene.beats[0].t0 : 0.45)
|
|
808
|
+
const isOutro = clampedT >= scene.outroStart
|
|
809
|
+
|
|
810
|
+
const nowKin = computeBodyKinematicsAt(scene, clampedT)
|
|
811
|
+
const lagKin = computeBodyKinematicsAt(scene, Math.max(0, clampedT - 0.068))
|
|
812
|
+
|
|
813
|
+
const activeBeat = nowKin.activeBeat
|
|
814
|
+
const activeWord = nowKin.activeWord
|
|
815
|
+
const wordIndex = nowKin.wordIndex
|
|
816
|
+
const stateName = nowKin.stateName
|
|
817
|
+
const stateDef = OLO_STATES[stateName] || OLO_STATES.idle
|
|
818
|
+
const beatElapsed = nowKin.beatElapsed
|
|
819
|
+
const rawFrame = Math.floor(beatElapsed * stateDef.fps)
|
|
820
|
+
const frameIndex = stateDef.loop ? (rawFrame % stateDef.frames) : Math.min(stateDef.frames - 1, rawFrame)
|
|
821
|
+
|
|
822
|
+
// Transición suave de 170ms (`FADE = 0.17s` de `character.js`) con el estado del beat anterior
|
|
823
|
+
let prevStateDef = null
|
|
824
|
+
let prevFrameIndex = 0
|
|
825
|
+
let fadeAlpha = 0
|
|
826
|
+
if (activeBeat && beatElapsed < 0.17 && nowKin.prevBeat && nowKin.prevBeat.state !== stateName) {
|
|
827
|
+
prevStateDef = OLO_STATES[nowKin.prevBeat.state] || OLO_STATES.idle
|
|
828
|
+
const prevDur = nowKin.prevBeat.duration || 2.0
|
|
829
|
+
const pRaw = Math.floor(prevDur * prevStateDef.fps)
|
|
830
|
+
prevFrameIndex = prevStateDef.loop ? (pRaw % prevStateDef.frames) : (prevStateDef.frames - 1)
|
|
831
|
+
const k = 1 - beatElapsed / 0.17
|
|
832
|
+
fadeAlpha = Number((k * k).toFixed(3))
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const velY = nowKin.bodyOffsetY - lagKin.bodyOffsetY
|
|
836
|
+
const velTilt = nowKin.headTiltDeg - lagKin.headTiltDeg
|
|
837
|
+
const wearSpring = {
|
|
838
|
+
offsetX: Number((velTilt * -1.2).toFixed(2)),
|
|
839
|
+
offsetY: Number((-velY * 1.1).toFixed(2)),
|
|
840
|
+
tiltDeg: Number((-velTilt * 0.9).toFixed(2)),
|
|
841
|
+
scaleY: Number((1.0 + (lagKin.stretchY - nowKin.stretchY) * 0.5).toFixed(4)),
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const blinkCycle = (clampedT + Math.abs(fractalNoise1D(clampedT * 0.25, 41)) * 0.9) % 3.4
|
|
845
|
+
const blinkAmount = blinkCycle < 0.13 ? Math.sin((blinkCycle / 0.13) * Math.PI) : 0
|
|
846
|
+
const microExpressions = {
|
|
847
|
+
blinkAmount: Number(blinkAmount.toFixed(3)),
|
|
848
|
+
saccadeX: Number((fractalNoise1D(clampedT * 0.9, 17) * 3.5).toFixed(2)),
|
|
849
|
+
saccadeY: Number((fractalNoise1D(clampedT * 0.8, 31) * 2.5).toFixed(2)),
|
|
850
|
+
breathOffsetY: Number(nowKin.bodyOffsetY.toFixed(2)),
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const handheldDriftX = fractalNoise1D(clampedT * 0.95, 101) * 5.5
|
|
854
|
+
const handheldDriftY = fractalNoise1D(clampedT * 0.88, 202) * 4.2
|
|
855
|
+
const handheldRot = fractalNoise1D(clampedT * 0.72, 303) * 0.42
|
|
856
|
+
|
|
857
|
+
let camZoom = 1.0
|
|
858
|
+
let camOffsetX = handheldDriftX
|
|
859
|
+
let camOffsetY = handheldDriftY
|
|
860
|
+
let camRotateDeg = handheldRot
|
|
861
|
+
let turntableYawDeg = 0
|
|
862
|
+
let chromaticAberration = 0
|
|
863
|
+
|
|
864
|
+
if (activeBeat) {
|
|
865
|
+
const bp = Math.max(0, Math.min(1, (clampedT - activeBeat.t0) / Math.max(0.1, activeBeat.duration)))
|
|
866
|
+
const dtBeat = Math.max(0, clampedT - activeBeat.t0)
|
|
867
|
+
const entryPop = Math.exp(-bp * 8) * Math.sin(bp * 24)
|
|
868
|
+
const targetYaw = activeBeat.turntableYaw || 0
|
|
869
|
+
turntableYawDeg = targetYaw * Math.min(1, dtBeat * 4.5)
|
|
870
|
+
|
|
871
|
+
switch (activeBeat.cam) {
|
|
872
|
+
case 'crash_zoom':
|
|
873
|
+
camZoom = 1.0 + Math.min(1, bp * 3.2) * 0.18 + entryPop * 0.03
|
|
874
|
+
camOffsetY = -22 + handheldDriftY * 0.5
|
|
875
|
+
chromaticAberration = Math.exp(-dtBeat * 5.5) * 0.75
|
|
876
|
+
break
|
|
877
|
+
case 'closeup':
|
|
878
|
+
camZoom = 1.12 + Math.sin(bp * Math.PI) * 0.035
|
|
879
|
+
camOffsetY = -14 + handheldDriftY * 0.5
|
|
880
|
+
break
|
|
881
|
+
case 'shake':
|
|
882
|
+
camZoom = 1.10
|
|
883
|
+
camOffsetX = fractalNoise1D(clampedT * 26, 11) * 18 * Math.exp(-bp * 2.4)
|
|
884
|
+
camOffsetY = fractalNoise1D(clampedT * 24, 22) * 14 * Math.exp(-bp * 2.4)
|
|
885
|
+
camRotateDeg = fractalNoise1D(clampedT * 20, 33) * 2.2 * Math.exp(-bp * 2.4)
|
|
886
|
+
chromaticAberration = Math.exp(-dtBeat * 3.8) * 0.7
|
|
887
|
+
break
|
|
888
|
+
case 'dutch_right':
|
|
889
|
+
camZoom = 1.07
|
|
890
|
+
camRotateDeg = 2.0 + handheldRot
|
|
891
|
+
camOffsetX = 10 + handheldDriftX
|
|
892
|
+
break
|
|
893
|
+
case 'hero_low':
|
|
894
|
+
camZoom = 1.15
|
|
895
|
+
camOffsetY = -28 + handheldDriftY * 0.5
|
|
896
|
+
chromaticAberration = Math.exp(-dtBeat * 6.0) * 0.35
|
|
897
|
+
break
|
|
898
|
+
case 'three_quarter_left':
|
|
899
|
+
camZoom = 1.08
|
|
900
|
+
turntableYawDeg = -14
|
|
901
|
+
camOffsetX = -10 + handheldDriftX
|
|
902
|
+
break
|
|
903
|
+
case 'three_quarter_right':
|
|
904
|
+
camZoom = 1.08
|
|
905
|
+
turntableYawDeg = 14
|
|
906
|
+
camOffsetX = 10 + handheldDriftX
|
|
907
|
+
break
|
|
908
|
+
case 'orbit_turntable':
|
|
909
|
+
camZoom = 1.07
|
|
910
|
+
turntableYawDeg = Math.sin(clampedT * 1.25) * 18
|
|
911
|
+
break
|
|
912
|
+
case 'snorricam':
|
|
913
|
+
camZoom = 1.14
|
|
914
|
+
camOffsetX = -nowKin.headTiltDeg * 2.8
|
|
915
|
+
camOffsetY = -18 - nowKin.bodyOffsetY * 0.85
|
|
916
|
+
camRotateDeg = -nowKin.headTiltDeg * 0.55
|
|
917
|
+
chromaticAberration = 0.18 + nowKin.mouthOpen * 0.25
|
|
918
|
+
break
|
|
919
|
+
case 'handheld_doc':
|
|
920
|
+
camZoom = 1.05 + fractalNoise1D(clampedT * 1.4, 77) * 0.035
|
|
921
|
+
camOffsetX = handheldDriftX * 2.1
|
|
922
|
+
camOffsetY = handheldDriftY * 2.1
|
|
923
|
+
camRotateDeg = handheldRot * 2.0
|
|
924
|
+
turntableYawDeg = fractalNoise1D(clampedT * 0.8, 88) * 12
|
|
925
|
+
break
|
|
926
|
+
default:
|
|
927
|
+
camZoom = 1.0 + bp * 0.04
|
|
928
|
+
}
|
|
929
|
+
} else if (isOutro) {
|
|
930
|
+
camZoom = 1.08
|
|
931
|
+
turntableYawDeg = Math.sin(clampedT * 1.4) * 6
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return {
|
|
935
|
+
t: clampedT,
|
|
936
|
+
isIntro,
|
|
937
|
+
isOutro,
|
|
938
|
+
state: stateName,
|
|
939
|
+
stateDef,
|
|
940
|
+
frameIndex,
|
|
941
|
+
prevStateDef,
|
|
942
|
+
prevFrameIndex,
|
|
943
|
+
fadeAlpha,
|
|
944
|
+
activeBeat,
|
|
945
|
+
activeWord,
|
|
946
|
+
wordIndex,
|
|
947
|
+
mouthOpen: Number(nowKin.mouthOpen.toFixed(3)),
|
|
948
|
+
viseme: nowKin.visemeId,
|
|
949
|
+
visemeShape: nowKin.visemeShape,
|
|
950
|
+
squashX: Number(nowKin.squashX.toFixed(4)),
|
|
951
|
+
stretchY: Number(nowKin.stretchY.toFixed(4)),
|
|
952
|
+
headTiltDeg: Number(nowKin.headTiltDeg.toFixed(2)),
|
|
953
|
+
wearSpring,
|
|
954
|
+
microExpressions,
|
|
955
|
+
postFx: {
|
|
956
|
+
chromaticAberration: Number(chromaticAberration.toFixed(3)),
|
|
957
|
+
rimIntensity: Number((0.42 + nowKin.mouthOpen * 0.52).toFixed(3)),
|
|
958
|
+
turntableYawDeg: Number(turntableYawDeg.toFixed(2)),
|
|
959
|
+
},
|
|
960
|
+
camera: {
|
|
961
|
+
zoom: Number(camZoom.toFixed(4)),
|
|
962
|
+
x: Number(camOffsetX.toFixed(2)),
|
|
963
|
+
y: Number(camOffsetY.toFixed(2)),
|
|
964
|
+
rotateDeg: Number(camRotateDeg.toFixed(2)),
|
|
965
|
+
turntableYawDeg: Number(turntableYawDeg.toFixed(2)),
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function evaluateSparringTurn(missionId, transcriptText, elapsedSec = 15) {
|
|
971
|
+
const mission = SPARRING_MISSIONS[missionId] || SPARRING_MISSIONS.sueldo_25
|
|
972
|
+
const clean = (transcriptText || '').trim()
|
|
973
|
+
const words = clean.split(/\s+/).filter(Boolean)
|
|
974
|
+
const low = clean.toLowerCase()
|
|
975
|
+
|
|
976
|
+
const matchedPower = mission.powerKeywords.filter((kw) => low.includes(kw))
|
|
977
|
+
const matchedWeak = mission.weakKeywords.filter((kw) => low.includes(kw))
|
|
978
|
+
const matchedFillers = []
|
|
979
|
+
mission.fillers.forEach((f) => {
|
|
980
|
+
const re = new RegExp(`\\b${f}\\b`, 'gi')
|
|
981
|
+
const m = low.match(re)
|
|
982
|
+
if (m) matchedFillers.push(...m)
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
const hasNumbers = /\d+%?|\b(veinte|treinta|cincuenta|cien|doble|mitad|mil|millones)\b/i.test(clean)
|
|
986
|
+
const wpm = elapsedSec > 0 ? Math.round((words.length / elapsedSec) * 60) : 110
|
|
987
|
+
|
|
988
|
+
let rawScore = 28
|
|
989
|
+
rawScore += matchedPower.length * 14
|
|
990
|
+
if (hasNumbers) rawScore += 18
|
|
991
|
+
if (words.length >= 12) rawScore += 12
|
|
992
|
+
if (wpm >= 95 && wpm <= 175) rawScore += 10
|
|
993
|
+
rawScore -= matchedWeak.length * 11
|
|
994
|
+
rawScore -= matchedFillers.length * 6
|
|
995
|
+
|
|
996
|
+
const score = Math.max(5, Math.min(99, Math.round(rawScore)))
|
|
997
|
+
let tier = 'En Lona'
|
|
998
|
+
let oloState = 'error'
|
|
999
|
+
let oloReply = mission.rebuttals[0].text
|
|
1000
|
+
|
|
1001
|
+
if (score >= 80) {
|
|
1002
|
+
tier = 'Tiburón Implacable'
|
|
1003
|
+
oloState = 'done'
|
|
1004
|
+
oloReply = mission.rebuttals[2].text
|
|
1005
|
+
} else if (score >= 55) {
|
|
1006
|
+
tier = 'Contendiente Sólido'
|
|
1007
|
+
oloState = 'thinking'
|
|
1008
|
+
oloReply = mission.rebuttals[1].text
|
|
1009
|
+
} else {
|
|
1010
|
+
tier = 'Congelado Bajo Presión'
|
|
1011
|
+
oloState = 'needs_approval'
|
|
1012
|
+
oloReply = mission.rebuttals[0].text
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const replayScene = compileScene({
|
|
1016
|
+
title: `${mission.title} · ${score}/100`,
|
|
1017
|
+
category: `Ring Olo · ${tier}`,
|
|
1018
|
+
wear: mission.wear,
|
|
1019
|
+
tint: mission.tint,
|
|
1020
|
+
vibe: score >= 80 ? 'victory' : 'drama',
|
|
1021
|
+
script: [
|
|
1022
|
+
`Olo: ${mission.openingLine}`,
|
|
1023
|
+
`Yo: ${clean || 'Eh... este... pues quería ver si se podía revisar.'}`,
|
|
1024
|
+
`Olo: ${oloReply}`,
|
|
1025
|
+
`Olo: Puntaje de presión: ${score} de 100. Rango: ${tier}.`
|
|
1026
|
+
].join('\n')
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
return {
|
|
1030
|
+
missionId: mission.id,
|
|
1031
|
+
score,
|
|
1032
|
+
tier,
|
|
1033
|
+
oloState,
|
|
1034
|
+
oloReply,
|
|
1035
|
+
metrics: {
|
|
1036
|
+
wordCount: words.length,
|
|
1037
|
+
wpm,
|
|
1038
|
+
hasNumbers,
|
|
1039
|
+
powerHits: matchedPower,
|
|
1040
|
+
weakHits: matchedWeak,
|
|
1041
|
+
fillerCount: matchedFillers.length,
|
|
1042
|
+
},
|
|
1043
|
+
replayScene,
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function buildMirrorRoastScene(profile = {}) {
|
|
1048
|
+
const archetype = profile.archetype || 'Arquitecto Nocturno de Prompts'
|
|
1049
|
+
const topTopic = profile.topTopic || 'Negocio y Código'
|
|
1050
|
+
const peakHour = profile.peakHour != null ? `${profile.peakHour}:00` : '02:00 AM'
|
|
1051
|
+
const politeness = profile.politeness || 'Ultra educado con las máquinas'
|
|
1052
|
+
const score = profile.score || 88
|
|
1053
|
+
|
|
1054
|
+
return compileScene({
|
|
1055
|
+
title: `Mi Espejo IA: ${archetype}`,
|
|
1056
|
+
category: 'Roast Espejo IA',
|
|
1057
|
+
wear: 'mago',
|
|
1058
|
+
tint: 'lilac',
|
|
1059
|
+
vibe: 'roast',
|
|
1060
|
+
script: [
|
|
1061
|
+
`Olo: Escaneé con lupa tus chats de inteligencia artificial.`,
|
|
1062
|
+
`Olo: Leyendo tu historial: eres "${archetype}" con hora pico a las ${peakHour}.`,
|
|
1063
|
+
`Yo: Te juro que solo uso la IA para temas serios de ${topTopic}.`,
|
|
1064
|
+
`Olo: ¡Claro que sí! Y tu nivel de cortesía es "${politeness}".`,
|
|
1065
|
+
`Olo: ¡Nivel legendario! Índice de intensidad: ${score} de 100.`
|
|
1066
|
+
].join('\n')
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function evalStatePop(stateName, elapsedMs) {
|
|
1071
|
+
const [sx, sy] = evalCanonicalPop(stateName, (Number(elapsedMs) || 0) / 1000)
|
|
1072
|
+
return { sx, sy }
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
OLO_STATES,
|
|
1077
|
+
STATE_POP,
|
|
1078
|
+
evalStatePop,
|
|
1079
|
+
OLO_TINTS,
|
|
1080
|
+
OLO_WEARS,
|
|
1081
|
+
OLO_VISEMES,
|
|
1082
|
+
OLO_CAMERA_MODES,
|
|
1083
|
+
VALID_SCENARIOS,
|
|
1084
|
+
normalizeScenario,
|
|
1085
|
+
PRESET_SHOWS,
|
|
1086
|
+
SPARRING_MISSIONS,
|
|
1087
|
+
fnv1aHex,
|
|
1088
|
+
fractalNoise1D,
|
|
1089
|
+
computeVisemesForWord,
|
|
1090
|
+
buildWordTimings,
|
|
1091
|
+
classifyBeat,
|
|
1092
|
+
compileScene,
|
|
1093
|
+
compileSceneFromScript,
|
|
1094
|
+
sampleSceneAt,
|
|
1095
|
+
evaluateSparringTurn,
|
|
1096
|
+
buildMirrorRoastScene,
|
|
1097
|
+
}
|
|
1098
|
+
})
|