@sage-rsc/talking-head-react 1.0.71 → 1.0.73

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
@@ -1,74 +1,76 @@
1
- # Talking Head React
1
+ # @sage-rsc/talking-head-react
2
2
 
3
- A reusable React component library for 3D talking avatars with lip-sync and text-to-speech capabilities. Perfect for creating interactive learning experiences, virtual assistants, and animated presentations.
3
+ A powerful React component library for creating interactive 3D talking avatars with realistic lip-sync, multiple text-to-speech services, and curriculum-based learning capabilities.
4
4
 
5
- ## Features
5
+ ## Features
6
6
 
7
- - 🎭 **3D Avatar Rendering** - Support for GLB/GLTF avatar models
8
- - 🎤 **Lip-Sync** - Real-time lip synchronization with audio
9
- - 🔊 **Text-to-Speech** - Multiple TTS services (Edge TTS, ElevenLabs, Google Cloud, Azure, Browser)
10
- - 🎬 **Animation Support** - FBX animation support and code-based animations
11
- - 🎯 **Programmatic Control** - Full control via props and ref methods
12
- - 🎨 **Zero UI** - Pure component, no built-in UI elements
13
- - 📦 **NPM Ready** - Easy to install and use
7
+ - 🎭 **3D Avatar Rendering** - Support for GLB/GLTF avatar models with full body or head-only modes
8
+ - 🎤 **Real-time Lip-Sync** - Automatic lip synchronization with audio using viseme-based animation
9
+ - 🔊 **Multiple TTS Services** - Edge TTS, ElevenLabs, Deepgram, Google Cloud, Azure, and Browser TTS
10
+ - 📚 **Curriculum Learning** - Built-in curriculum system with lessons, questions, and code examples
11
+ - 🎬 **Animation Support** - FBX animation support and code-based body movements
12
+ - ⏯️ **Playback Control** - Pause, resume, and stop speech functionality
13
+ - 🎯 **Interactive Mode** - Manual control over curriculum progression
14
+ - 💻 **Code IDE Integration** - Simulate code typing and execution in an IDE
15
+ - 🎨 **Zero UI** - Pure components, no built-in UI elements - full control over styling
14
16
 
15
- ## Installation
17
+ ## 📦 Installation
16
18
 
17
19
  ```bash
18
- npm install talking-head-react
20
+ npm install @sage-rsc/talking-head-react
19
21
  ```
20
22
 
21
- ## Peer Dependencies
22
-
23
- This package requires the following peer dependencies:
23
+ ### Peer Dependencies
24
24
 
25
25
  ```bash
26
26
  npm install react react-dom three
27
27
  ```
28
28
 
29
- ## Quick Start
29
+ **Requirements:**
30
+ - React 18.0.0+ or 19.0.0+
31
+ - React DOM 18.0.0+ or 19.0.0+
32
+ - Three.js 0.150.0+
33
+
34
+ ## 🚀 Quick Start
35
+
36
+ ### Simple Talking Avatar
30
37
 
31
- ### Basic Usage
38
+ The simplest way to get started - just pass text and the avatar speaks:
32
39
 
33
40
  ```jsx
34
- import React, { useRef, useEffect } from 'react';
35
- import { TalkingHeadAvatar } from 'talking-head-react';
41
+ import React, { useRef } from 'react';
42
+ import { SimpleTalkingAvatar } from '@sage-rsc/talking-head-react';
36
43
 
37
44
  function App() {
38
45
  const avatarRef = useRef(null);
39
46
 
40
- useEffect(() => {
41
- // Wait for avatar to be ready
42
- if (avatarRef.current?.isReady) {
43
- // Make the avatar speak
44
- avatarRef.current.speakText("Hello! I'm your talking avatar.");
45
- }
46
- }, []);
47
+ const handleSpeak = () => {
48
+ avatarRef.current?.speakText("Hello! I'm a talking avatar.");
49
+ };
47
50
 
48
51
  return (
49
52
  <div style={{ width: '100vw', height: '100vh' }}>
50
- <TalkingHeadAvatar
53
+ <SimpleTalkingAvatar
51
54
  ref={avatarRef}
52
- avatarUrl="/path/to/your/avatar.glb"
55
+ avatarUrl="/avatars/brunette.glb"
53
56
  avatarBody="F"
54
57
  mood="happy"
55
- ttsService="edge"
56
- ttsVoice="en-US-AriaNeural"
57
- showFullAvatar={true}
58
- onReady={(talkingHead) => {
59
- console.log('Avatar is ready!', talkingHead);
60
- }}
58
+ showFullAvatar={false}
59
+ onReady={() => console.log('Avatar ready!')}
61
60
  />
61
+ <button onClick={handleSpeak}>Speak</button>
62
62
  </div>
63
63
  );
64
64
  }
65
65
  ```
66
66
 
67
- ### With FBX Animations
67
+ ### Full-Featured Avatar
68
+
69
+ For advanced control with animations and custom configurations:
68
70
 
69
71
  ```jsx
70
72
  import React, { useRef } from 'react';
71
- import { TalkingHeadAvatar } from 'talking-head-react';
73
+ import { TalkingHeadAvatar } from '@sage-rsc/talking-head-react';
72
74
 
73
75
  function App() {
74
76
  const avatarRef = useRef(null);
@@ -76,61 +78,60 @@ function App() {
76
78
  const animations = {
77
79
  teaching: "/animations/Arguing.fbx",
78
80
  correct: "/animations/Happy.fbx",
79
- incorrect: "/animations/Disappointed.fbx",
80
- lessonComplete: "/animations/Step.fbx"
81
- };
82
-
83
- const handleSpeak = () => {
84
- if (avatarRef.current?.isReady) {
85
- // Play animation while speaking
86
- avatarRef.current.playAnimation(animations.teaching, true);
87
- avatarRef.current.speakText("Let me explain this concept to you.");
88
- }
81
+ incorrect: "/animations/Disappointed.fbx"
89
82
  };
90
83
 
91
84
  return (
92
85
  <div style={{ width: '100vw', height: '100vh' }}>
93
86
  <TalkingHeadAvatar
94
87
  ref={avatarRef}
95
- avatarUrl="/path/to/your/avatar.glb"
88
+ avatarUrl="/avatars/brunette.glb"
89
+ avatarBody="F"
90
+ mood="happy"
91
+ ttsService="elevenlabs"
92
+ ttsApiKey="your-api-key"
93
+ ttsVoice="21m00Tcm4TlvDq8ikWAM"
94
+ showFullAvatar={false}
95
+ bodyMovement="gesturing"
96
96
  animations={animations}
97
97
  onReady={() => {
98
- console.log('Avatar ready!');
98
+ avatarRef.current?.speakText("Welcome!");
99
99
  }}
100
100
  />
101
- <button onClick={handleSpeak}>Start Teaching</button>
102
101
  </div>
103
102
  );
104
103
  }
105
104
  ```
106
105
 
107
- ### With Curriculum Data
106
+ ### Curriculum Learning
107
+
108
+ Complete learning system with lessons, questions, and automatic progression:
108
109
 
109
110
  ```jsx
110
111
  import React, { useRef } from 'react';
111
- import { CurriculumLearning } from 'talking-head-react';
112
+ import { CurriculumLearning } from '@sage-rsc/talking-head-react';
112
113
 
113
114
  function App() {
114
- const curriculumRef = useRef(null);
115
-
116
115
  const curriculumData = {
117
116
  curriculum: {
118
- title: "Introduction to Computer Science",
117
+ title: "Introduction to Programming",
118
+ description: "Learn the basics of programming",
119
+ language: "en",
119
120
  modules: [
120
121
  {
121
- title: "Module 1",
122
+ title: "Module 1: Variables",
122
123
  lessons: [
123
124
  {
124
- title: "Lesson 1",
125
- body: "This is lesson content...",
126
- avatar_script: "Welcome to this lesson!",
125
+ title: "JavaScript Variables",
126
+ avatar_script: "Let's learn about variables.",
127
+ body: "Variables store data values in your program.",
127
128
  questions: [
128
129
  {
129
130
  type: "multiple_choice",
130
- question: "What is a variable?",
131
- options: ["Option A", "Option B", "Option C"],
132
- answer: "Option A",
133
- explanation: "A variable is..."
131
+ question: "Which keyword creates a constant variable?",
132
+ options: ["let", "const", "var"],
133
+ answer: "const",
134
+ explanation: "The 'const' keyword creates a constant variable."
134
135
  }
135
136
  ]
136
137
  }
@@ -140,122 +141,297 @@ function App() {
140
141
  }
141
142
  };
142
143
 
143
- const animations = {
144
- teaching: "/animations/Arguing.fbx",
145
- correct: "/animations/Happy.fbx",
146
- incorrect: "/animations/Disappointed.fbx"
147
- };
148
-
149
144
  const avatarConfig = {
150
- avatarUrl: "/path/to/your/avatar.glb",
145
+ avatarUrl: "/avatars/brunette.glb",
151
146
  avatarBody: "F",
152
- ttsService: "edge",
153
- ttsVoice: "en-US-AriaNeural"
147
+ mood: "happy",
148
+ showFullAvatar: false
154
149
  };
155
150
 
156
151
  return (
157
152
  <div style={{ width: '100vw', height: '100vh' }}>
158
153
  <CurriculumLearning
159
- ref={curriculumRef}
160
154
  curriculumData={curriculumData}
161
155
  avatarConfig={avatarConfig}
162
- animations={animations}
163
156
  autoStart={true}
164
- onLessonStart={(data) => {
165
- console.log('Lesson started:', data);
166
- }}
167
157
  onLessonComplete={(data) => {
168
158
  console.log('Lesson completed:', data);
169
159
  }}
170
- onQuestionAnswer={(data) => {
171
- console.log('Question answered:', data);
172
- }}
173
160
  />
174
161
  </div>
175
162
  );
176
163
  }
177
164
  ```
178
165
 
179
- ## API Reference
166
+ ## 📖 Components
167
+
168
+ ### SimpleTalkingAvatar
169
+
170
+ A lightweight component for simple text-to-speech scenarios. Perfect when you just need an avatar to speak text.
180
171
 
181
- ### TalkingHeadAvatar Props
172
+ **Props:**
182
173
 
183
174
  | Prop | Type | Default | Description |
184
175
  |------|------|---------|-------------|
185
- | `avatarUrl` | `string` | `"/avatars/brunette.glb"` | URL/path to the GLB avatar file |
176
+ | `text` | `string` | `null` | Text to speak (optional, can use `speakText` method) |
177
+ | `avatarUrl` | `string` | `"/avatars/brunette.glb"` | URL/path to GLB avatar file |
186
178
  | `avatarBody` | `string` | `"F"` | Avatar body type ('M' or 'F') |
187
- | `mood` | `string` | `"neutral"` | Initial mood ('happy', 'sad', 'neutral', etc.) |
179
+ | `mood` | `string` | `"neutral"` | Initial mood ('happy', 'sad', 'neutral', 'excited') |
188
180
  | `ttsLang` | `string` | `"en"` | Text-to-speech language code |
189
- | `ttsService` | `string` | `null` | TTS service ('edge', 'elevenlabs', 'google', 'azure', 'browser') |
181
+ | `ttsService` | `string` | `null` | TTS service ('edge', 'elevenlabs', 'deepgram', 'google', 'azure', 'browser') |
190
182
  | `ttsVoice` | `string` | `null` | TTS voice ID |
191
- | `bodyMovement` | `string` | `"idle"` | Initial body movement type |
183
+ | `ttsApiKey` | `string` | `null` | TTS API key (ElevenLabs, Deepgram, Google, or Azure |
184
+ | `bodyMovement` | `string` | `"idle"` | Body movement type ('idle', 'gesturing', 'dancing') |
192
185
  | `movementIntensity` | `number` | `0.5` | Movement intensity (0-1) |
193
- | `showFullAvatar` | `boolean` | `true` | Whether to show full body avatar |
194
- | `cameraView` | `string` | `"upper"` | Camera view ('upper', 'full', etc.) |
195
- | `animations` | `object` | `{}` | Object mapping animation names to FBX file paths |
186
+ | `showFullAvatar` | `boolean` | `false` | Whether to show full body avatar |
187
+ | `cameraView` | `string` | `"upper"` | Camera view ('upper', 'full') |
188
+ | `autoSpeak` | `boolean` | `false` | Automatically speak `text` prop when ready |
196
189
  | `onReady` | `function` | `() => {}` | Callback when avatar is ready |
197
- | `onLoading` | `function` | `() => {}` | Callback for loading progress |
198
190
  | `onError` | `function` | `() => {}` | Callback for errors |
191
+ | `onSpeechEnd` | `function` | `() => {}` | Callback when speech ends |
199
192
  | `className` | `string` | `""` | Additional CSS classes |
200
193
  | `style` | `object` | `{}` | Additional inline styles |
201
194
 
202
- ### TalkingHeadAvatar Ref Methods
195
+ **Ref Methods:**
203
196
 
204
197
  | Method | Parameters | Description |
205
198
  |--------|------------|-------------|
206
- | `speakText(text)` | `text: string` | Make the avatar speak the given text |
207
- | `stopSpeaking()` | - | Stop current speech |
208
- | `setMood(mood)` | `mood: string` | Set avatar mood |
209
- | `setTimingAdjustment(rate)` | `rate: number` | Adjust animation timing (e.g., 1.05 for 5% slower) |
210
- | `playAnimation(name, disablePositionLock)` | `name: string`, `disablePositionLock: boolean` | Play an FBX animation |
211
- | `setBodyMovement(movement)` | `movement: string` | Set body movement type |
212
- | `setMovementIntensity(intensity)` | `intensity: number` | Set movement intensity (0-1) |
213
- | `playRandomDance()` | - | Play a random dance animation |
214
- | `playReaction(reactionType)` | `reactionType: string` | Play a reaction animation |
199
+ | `speakText(text, options)` | `text: string`, `options: object` | Make avatar speak text |
200
+ | `pauseSpeaking()` | - | Pause current speech |
201
+ | `resumeSpeaking()` | - | Resume paused speech |
202
+ | `stopSpeaking()` | - | Stop all speech |
203
+ | `resumeAudioContext()` | - | Resume audio context (for user interaction) |
204
+ | `isPaused()` | - | Check if currently paused |
205
+ | `setMood(mood)` | `mood: string` | Change avatar mood |
206
+ | `setBodyMovement(movement)` | `movement: string` | Change body movement |
207
+ | `playAnimation(name)` | `name: string` | Play FBX animation |
208
+ | `playReaction(type)` | `type: string` | Play reaction animation |
215
209
  | `playCelebration()` | - | Play celebration animation |
216
210
  | `setShowFullAvatar(show)` | `show: boolean` | Toggle full body mode |
211
+ | `isReady` | - | Boolean indicating if avatar is ready |
212
+
213
+ **Example:**
214
+
215
+ ```jsx
216
+ const avatarRef = useRef(null);
217
+
218
+ // Speak text
219
+ avatarRef.current?.speakText("Hello world!", {
220
+ lipsyncLang: 'en',
221
+ onSpeechEnd: () => console.log('Done speaking')
222
+ });
223
+
224
+ // Pause/Resume
225
+ avatarRef.current?.pauseSpeaking();
226
+ avatarRef.current?.resumeSpeaking();
227
+
228
+ // Change mood
229
+ avatarRef.current?.setMood("happy");
230
+ ```
231
+
232
+ ### TalkingHeadAvatar
233
+
234
+ Full-featured avatar component with advanced controls, animations, and TTS configuration.
235
+
236
+ **Props:** Same as `SimpleTalkingAvatar` plus:
237
+
238
+ | Prop | Type | Default | Description |
239
+ |------|------|---------|-------------|
240
+ | `animations` | `object` | `{}` | Object mapping animation names to FBX file paths |
241
+ | `onLoading` | `function` | `() => {}` | Callback for loading progress |
242
+
243
+ **Ref Methods:** Same as `SimpleTalkingAvatar` plus:
244
+
245
+ | Method | Parameters | Description |
246
+ |--------|------------|-------------|
247
+ | `setTimingAdjustment(rate)` | `rate: number` | Adjust animation timing (e.g., 1.05 for 5% slower) |
248
+ | `setMovementIntensity(intensity)` | `intensity: number` | Set movement intensity (0-1) |
249
+ | `playRandomDance()` | - | Play random dance animation |
217
250
  | `lockAvatarPosition()` | - | Lock avatar position |
218
251
  | `unlockAvatarPosition()` | - | Unlock avatar position |
219
252
 
220
- ### CurriculumLearning Props
253
+ ### CurriculumLearning
254
+
255
+ Complete learning system with curriculum management, questions, and code examples.
256
+
257
+ **Props:**
221
258
 
222
259
  | Prop | Type | Default | Description |
223
260
  |------|------|---------|-------------|
224
- | `curriculumData` | `object` | `null` | Curriculum data object |
225
- | `avatarConfig` | `object` | `{}` | Avatar configuration |
261
+ | `curriculumData` | `object` | `null` | Curriculum data object (see structure below) |
262
+ | `avatarConfig` | `object` | `{}` | Avatar configuration (same as `SimpleTalkingAvatar` props) |
226
263
  | `animations` | `object` | `{}` | Animation files mapping |
264
+ | `autoStart` | `boolean` | `false` | Automatically start teaching when ready |
227
265
  | `onLessonStart` | `function` | `() => {}` | Callback when lesson starts |
228
266
  | `onLessonComplete` | `function` | `() => {}` | Callback when lesson completes |
229
267
  | `onQuestionAnswer` | `function` | `() => {}` | Callback when question is answered |
230
268
  | `onCurriculumComplete` | `function` | `() => {}` | Callback when curriculum completes |
231
- | `autoStart` | `boolean` | `false` | Whether to auto-start the curriculum |
269
+ | `onCustomAction` | `function` | `() => {}` | Callback for custom actions (interactive mode) |
232
270
 
233
- ### CurriculumLearning Ref Methods
271
+ **Ref Methods:**
234
272
 
235
273
  | Method | Parameters | Description |
236
274
  |--------|------------|-------------|
237
- | `startTeaching()` | - | Start teaching the current lesson |
275
+ | `startTeaching()` | - | Start teaching current lesson |
238
276
  | `startQuestions()` | - | Start asking questions |
239
- | `handleAnswerSelect(answer)` | `answer: string/boolean` | Handle answer selection |
277
+ | `handleAnswerSelect(answer)` | `answer: string/boolean` | Submit answer to current question |
240
278
  | `nextQuestion()` | - | Move to next question |
279
+ | `previousQuestion()` | - | Move to previous question |
241
280
  | `nextLesson()` | - | Move to next lesson |
281
+ | `previousLesson()` | - | Move to previous lesson |
242
282
  | `completeLesson()` | - | Complete current lesson |
243
283
  | `completeCurriculum()` | - | Complete entire curriculum |
244
284
  | `resetCurriculum()` | - | Reset curriculum to beginning |
245
285
  | `getState()` | - | Get current curriculum state |
286
+ | `pauseSpeaking()` | - | Pause avatar speech |
287
+ | `resumeSpeaking()` | - | Resume avatar speech |
288
+ | `isPaused()` | - | Check if paused |
289
+ | `speakText(text, options)` | `text: string`, `options: object` | Make avatar speak text |
246
290
 
247
- ## TTS Configuration
248
-
249
- The package includes a TTS configuration system. You can configure TTS services in `src/config/ttsConfig.js`:
291
+ **Curriculum Data Structure:**
250
292
 
251
293
  ```javascript
252
- import { getActiveTTSConfig } from 'talking-head-react';
294
+ {
295
+ curriculum: {
296
+ title: "Course Title",
297
+ description: "Course description",
298
+ language: "en",
299
+ modules: [
300
+ {
301
+ title: "Module Title",
302
+ lessons: [
303
+ {
304
+ title: "Lesson Title",
305
+ avatar_script: "What the avatar will say",
306
+ body: "Lesson content text",
307
+ code_example: { // Optional
308
+ code: "console.log('Hello');",
309
+ language: "javascript",
310
+ description: "Code example description",
311
+ autoRun: true,
312
+ typingSpeed: 50
313
+ },
314
+ questions: [ // Optional
315
+ {
316
+ type: "multiple_choice", // or "true_false" or "code_test"
317
+ question: "Question text?",
318
+ options: ["Option 1", "Option 2", "Option 3"], // For multiple_choice
319
+ answer: "Option 1", // or true/false for true_false
320
+ explanation: "Why this answer is correct"
321
+ }
322
+ ]
323
+ }
324
+ ]
325
+ }
326
+ ]
327
+ }
328
+ }
329
+ ```
253
330
 
254
- const config = getActiveTTSConfig();
255
- console.log(config.service); // 'edge', 'elevenlabs', 'google', 'azure', or 'browser'
331
+ **Interactive Mode:**
332
+
333
+ For manual control over curriculum progression:
334
+
335
+ ```jsx
336
+ const curriculumRef = useRef(null);
337
+
338
+ // Handle custom actions
339
+ const handleCustomAction = (action) => {
340
+ switch (action.type) {
341
+ case 'teachingComplete':
342
+ // Teaching finished, enable "Start Questions" button
343
+ break;
344
+ case 'questionStart':
345
+ // Question started, show question UI
346
+ break;
347
+ case 'answerFeedbackComplete':
348
+ // Answer feedback finished, enable "Next Question" button
349
+ break;
350
+ case 'allQuestionsComplete':
351
+ // All questions done, enable "Complete Lesson" button
352
+ break;
353
+ case 'lessonCompleteFeedbackDone':
354
+ // Lesson completion feedback done, enable "Next Lesson" button
355
+ break;
356
+ case 'codeExampleReady':
357
+ // Code example ready, trigger IDE typing animation
358
+ break;
359
+ }
360
+ };
361
+
362
+ <CurriculumLearning
363
+ ref={curriculumRef}
364
+ curriculumData={curriculumData}
365
+ avatarConfig={avatarConfig}
366
+ autoStart={false} // Manual control
367
+ onCustomAction={handleCustomAction}
368
+ />
369
+
370
+ // Control progression manually
371
+ curriculumRef.current?.startTeaching();
372
+ curriculumRef.current?.startQuestions();
373
+ curriculumRef.current?.nextQuestion();
374
+ curriculumRef.current?.completeLesson();
375
+ ```
376
+
377
+ ## 🎤 Text-to-Speech Services
378
+
379
+ ### Edge TTS (Default)
380
+
381
+ Free, no API key required:
382
+
383
+ ```jsx
384
+ <SimpleTalkingAvatar
385
+ ttsService="edge"
386
+ ttsVoice="en-US-AriaNeural"
387
+ />
388
+ ```
389
+
390
+ ### ElevenLabs
391
+
392
+ High-quality voices, requires API key:
393
+
394
+ ```jsx
395
+ <SimpleTalkingAvatar
396
+ ttsService="elevenlabs"
397
+ ttsApiKey="your-api-key"
398
+ ttsVoice="21m00Tcm4TlvDq8ikWAM"
399
+ />
256
400
  ```
257
401
 
258
- ## Animation Support
402
+ ### Deepgram
403
+
404
+ Fast, high-quality TTS, requires API key:
405
+
406
+ ```jsx
407
+ <SimpleTalkingAvatar
408
+ ttsService="deepgram"
409
+ ttsApiKey="your-api-key"
410
+ ttsVoice="aura-asteria-en" // Options: aura-thalia-en, aura-asteria-en, aura-orion-en, etc.
411
+ />
412
+ ```
413
+
414
+ ### Browser TTS
415
+
416
+ Uses browser's built-in speech synthesis:
417
+
418
+ ```jsx
419
+ <SimpleTalkingAvatar
420
+ ttsService="browser"
421
+ />
422
+ ```
423
+
424
+ ### Google Cloud / Azure
425
+
426
+ ```jsx
427
+ <SimpleTalkingAvatar
428
+ ttsService="google" // or "azure"
429
+ ttsApiKey="your-api-key"
430
+ ttsVoice="en-US-Wavenet-D"
431
+ />
432
+ ```
433
+
434
+ ## 🎬 Animations
259
435
 
260
436
  ### FBX Animations
261
437
 
@@ -265,7 +441,8 @@ Provide animation mappings via the `animations` prop:
265
441
  const animations = {
266
442
  teaching: "/animations/Arguing.fbx",
267
443
  correct: "/animations/Happy.fbx",
268
- incorrect: "/animations/Disappointed.fbx"
444
+ incorrect: "/animations/Disappointed.fbx",
445
+ lessonComplete: "/animations/Step.fbx"
269
446
  };
270
447
 
271
448
  <TalkingHeadAvatar animations={animations} />
@@ -273,19 +450,173 @@ const animations = {
273
450
 
274
451
  ### Code-Based Animations
275
452
 
276
- The package also supports code-based animations:
453
+ Built-in body movements:
277
454
 
278
455
  - `idle` - Idle animation
279
456
  - `gesturing` - Teaching gestures
457
+ - `dancing` - Dance animation
280
458
  - `happy` - Happy mood animation
281
459
  - `sad` - Sad mood animation
282
- - `dancing` - Dance animation
283
- - And more...
284
460
 
285
- ## License
461
+ ```jsx
462
+ avatarRef.current?.setBodyMovement("gesturing");
463
+ avatarRef.current?.playReaction("happy");
464
+ avatarRef.current?.playCelebration();
465
+ ```
466
+
467
+ ## 📚 Question Types
468
+
469
+ ### Multiple Choice
470
+
471
+ ```javascript
472
+ {
473
+ type: "multiple_choice",
474
+ question: "What is a variable?",
475
+ options: ["A container", "A function", "A loop"],
476
+ answer: "A container",
477
+ explanation: "A variable is a container that stores data."
478
+ }
479
+ ```
480
+
481
+ ### True/False
482
+
483
+ ```javascript
484
+ {
485
+ type: "true_false",
486
+ question: "JavaScript is a compiled language.",
487
+ answer: false,
488
+ explanation: "JavaScript is an interpreted language."
489
+ }
490
+ ```
491
+
492
+ ### Code Test
493
+
494
+ ```javascript
495
+ {
496
+ type: "code_test",
497
+ question: "Write a function that adds two numbers.",
498
+ testCriteria: {
499
+ type: "function",
500
+ functionName: "add",
501
+ testCases: [
502
+ { input: [2, 3], expectedOutput: 5 },
503
+ { input: [10, 20], expectedOutput: 30 }
504
+ ]
505
+ },
506
+ explanation: "The function should return the sum of two numbers."
507
+ }
508
+ ```
509
+
510
+ ## 💻 Code Examples
511
+
512
+ Include code examples in lessons for IDE integration:
513
+
514
+ ```javascript
515
+ {
516
+ title: "JavaScript Variables",
517
+ avatar_script: "Let's learn about variables.",
518
+ body: "Variables store data values.",
519
+ code_example: {
520
+ code: "let name = 'Alice';\nconst age = 25;\nconsole.log(name);",
521
+ language: "javascript", // "javascript", "python", "java", "html"
522
+ description: "Basic variable declarations",
523
+ autoRun: true, // Automatically run after typing
524
+ typingSpeed: 50 // Characters per second
525
+ }
526
+ }
527
+ ```
528
+
529
+ Listen for `codeExampleReady` event in interactive mode:
530
+
531
+ ```jsx
532
+ const handleCustomAction = (action) => {
533
+ if (action.type === 'codeExampleReady') {
534
+ // Trigger IDE typing animation
535
+ handleCodeExample(action.codeExample);
536
+ }
537
+ };
538
+ ```
539
+
540
+ ## 🎯 Use Cases
541
+
542
+ - **Educational Platforms** - Interactive learning with curriculum management
543
+ - **Virtual Assistants** - Conversational avatars with TTS
544
+ - **Code Tutorials** - Step-by-step coding lessons with IDE integration
545
+ - **Training Simulations** - Interactive training with questions and feedback
546
+ - **Presentation Tools** - Animated presentations with talking avatars
547
+
548
+ ## 🔧 Configuration
549
+
550
+ ### TTS Configuration
551
+
552
+ Configure default TTS service in your app:
553
+
554
+ ```javascript
555
+ import { getActiveTTSConfig } from '@sage-rsc/talking-head-react';
556
+
557
+ const config = getActiveTTSConfig();
558
+ // Returns current TTS configuration
559
+ ```
560
+
561
+ ### Avatar Configuration
562
+
563
+ Common avatar settings:
564
+
565
+ ```javascript
566
+ const avatarConfig = {
567
+ avatarUrl: "/avatars/brunette.glb",
568
+ avatarBody: "F", // "M" or "F"
569
+ mood: "happy",
570
+ ttsService: "elevenlabs",
571
+ ttsApiKey: "your-key",
572
+ ttsVoice: "voice-id",
573
+ showFullAvatar: false, // false = head only, true = full body
574
+ bodyMovement: "gesturing",
575
+ movementIntensity: 0.7
576
+ };
577
+ ```
578
+
579
+ ## 📝 Examples
580
+
581
+ Check the `example-*.jsx` files in the repository for complete examples:
582
+
583
+ - `example-simple-avatar.jsx` - Simple text-to-speech usage
584
+ - `example-interactive-mode.jsx` - Manual curriculum control
585
+ - `example-with-code-ide.jsx` - Code IDE integration
586
+ - `example-with-api-key.jsx` - TTS service configuration
587
+
588
+ ## 🐛 Troubleshooting
589
+
590
+ ### Audio Not Playing
591
+
592
+ If you see "Web Audio API suspended" error:
593
+
594
+ ```jsx
595
+ // The component automatically resumes audio context on user interaction
596
+ // But you can also manually resume:
597
+ avatarRef.current?.resumeAudioContext();
598
+ ```
599
+
600
+ ### Avatar Not Loading
601
+
602
+ - Ensure the avatar file path is correct
603
+ - Check browser console for loading errors
604
+ - Verify GLB file is valid
605
+
606
+ ### Lip-Sync Not Working
607
+
608
+ - Ensure avatar model has viseme morph targets
609
+ - Check that `lipsyncLang` matches your TTS language
610
+ - Verify TTS service is working correctly
611
+
612
+ ## 📄 License
286
613
 
287
614
  MIT
288
615
 
289
- ## Contributing
616
+ ## 🤝 Contributing
290
617
 
291
618
  Contributions are welcome! Please feel free to submit a Pull Request.
619
+
620
+ ## 📞 Support
621
+
622
+ For issues, questions, or feature requests, please open an issue on GitHub.
package/dist/index.cjs CHANGED
@@ -4,4 +4,4 @@
4
4
  ${e}
5
5
  </voice>
6
6
  </speak>
7
- `,s=await fetch(this.opt.ttsEndpoint,{method:"POST",headers:{"Ocp-Apim-Subscription-Key":this.opt.ttsApikey,"Content-Type":"application/ssml+xml","X-Microsoft-OutputFormat":"audio-16khz-128kbitrate-mono-mp3"},body:i});if(!s.ok)throw new Error(`Azure TTS error: ${s.status} ${s.statusText}`);const o=await s.arrayBuffer(),l=await this.audioCtx.decodeAudioData(o);console.log("Analyzing audio for precise lip-sync...");const h=await this.audioAnalyzer.analyzeAudio(l,e);console.log("Azure TTS Audio Analysis:",{text:e,audioDuration:l.duration,visemeCount:h.visemes.length,wordCount:h.words.length,features:{onsets:h.features.onsets.length,boundaries:h.features.phonemeBoundaries.length}});const r=[];for(let a=0;a<h.visemes.length;a++){const c=h.visemes[a],d=c.startTime*1e3,g=c.duration*1e3,y=c.intensity;r.push({template:{name:"viseme"},ts:[d-Math.min(60,2*g/3),d+Math.min(25,g/2),d+g+Math.min(60,g/2)],vs:{["viseme_"+c.viseme]:[null,y,0]}})}const u=[...t.anim,...r];this.audioPlaylist.push({anim:u,audio:l}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}async synthesizeWithExternalTTS(t){let e="<speak>";t.text.forEach((o,l)=>{l>0&&(e+=" <mark name='"+o.mark+"'/>"),e+=o.word.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;").replace(new RegExp("^\\p{Dash_Punctuation}$","ug"),'<break time="750ms"/>')}),e+="</speak>";const n={method:"POST",headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify({input:{ssml:e},voice:{languageCode:t.lang||this.avatar.ttsLang||this.opt.ttsLang,name:t.voice||this.avatar.ttsVoice||this.opt.ttsVoice},audioConfig:{audioEncoding:this.ttsAudioEncoding,speakingRate:(t.rate||this.avatar.ttsRate||this.opt.ttsRate)+this.mood.speech.deltaRate,pitch:(t.pitch||this.avatar.ttsPitch||this.opt.ttsPitch)+this.mood.speech.deltaPitch,volumeGainDb:(t.volume||this.avatar.ttsVolume||this.opt.ttsVolume)+this.mood.speech.deltaVolume},enableTimePointing:[1]})};this.opt.jwtGet&&typeof this.opt.jwtGet=="function"&&(n.headers.Authorization="Bearer "+await this.opt.jwtGet());const i=await fetch(this.opt.ttsEndpoint+(this.opt.ttsApikey?"?key="+this.opt.ttsApikey:""),n),s=await i.json();if(i.status===200&&s&&s.audioContent){const o=this.b64ToArrayBuffer(s.audioContent),l=await this.audioCtx.decodeAudioData(o);this.speakWithHands();const h=[0];let r=0;t.text.forEach((c,d)=>{if(d>0){let g=h[h.length-1];s.timepoints[r]&&(g=s.timepoints[r].timeSeconds*1e3,s.timepoints[r].markName===""+c.mark&&r++),h.push(g)}});const u=[{mark:0,time:0}];h.forEach((c,d)=>{if(d>0){let g=c-h[d-1];u[d-1].duration=g,u.push({mark:d,time:c})}});let a=1e3*l.duration;a>this.opt.ttsTrimEnd&&(a=a-this.opt.ttsTrimEnd),u[u.length-1].duration=a-u[u.length-1].time,t.anim.forEach(c=>{const d=u[c.mark];if(d)for(let g=0;g<c.ts.length;g++)c.ts[g]=d.time+c.ts[g]*d.duration+this.opt.ttsTrimStart}),this.audioPlaylist.push({anim:t.anim,audio:l}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}else this.startSpeaking(!0)}async startSpeaking(t=!1){if(!(!this.armature||this.isSpeaking&&!t))if(this.stateName="speaking",this.isSpeaking=!0,this.speechQueue.length){let e=this.speechQueue.shift();if(e.emoji){this.lookAtCamera(500);let n=e.emoji.dt.reduce((i,s)=>i+s,0);this.animQueue.push(this.animFactory(e.emoji)),setTimeout(this.startSpeaking.bind(this),n,!0)}else if(e.break)setTimeout(this.startSpeaking.bind(this),e.break,!0);else if(e.audio)e.isRaw||(this.lookAtCamera(500),this.speakWithHands(),this.resetLips()),this.audioPlaylist.push({anim:e.anim,audio:e.audio,isRaw:e.isRaw}),this.onSubtitles=e.onSubtitles||null,e.mood&&this.setMood(e.mood),this.playAudio();else if(e.text){this.lookAtCamera(500);try{!this.opt.ttsEndpoint||this.opt.ttsEndpoint===""?await this.synthesizeWithBrowserTTS(e):this.opt.ttsService==="elevenlabs"?await this.synthesizeWithElevenLabsTTS(e):this.opt.ttsService==="deepgram"?await this.synthesizeWithDeepgramTTS(e):this.opt.ttsService==="azure"?await this.synthesizeWithAzureTTS(e):await this.synthesizeWithExternalTTS(e)}catch(n){console.error("Error:",n),this.startSpeaking(!0)}}else e.anim?(this.onSubtitles=e.onSubtitles||null,this.resetLips(),e.mood&&this.setMood(e.mood),e.anim.forEach((n,i)=>{for(let s=0;s<n.ts.length;s++)n.ts[s]=this.animClock+10*i;this.animQueue.push(n)}),setTimeout(this.startSpeaking.bind(this),10*e.anim.length,!0)):e.marker?(typeof e.marker=="function"&&e.marker(),this.startSpeaking(!0)):this.startSpeaking(!0)}else this.stateName="idle",this.isSpeaking=!1}pauseSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render())}stopSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.speechQueue.length=0,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.armature&&(this.resetLips(),this.render())}async streamStart(t={},e=null,n=null,i=null,s=null){if(this.stopSpeaking(),this.isStreaming=!0,t.waitForAudioChunks!==void 0&&(this.streamWaitForAudioChunks=t.waitForAudioChunks),this.streamWaitForAudioChunks||(this.streamAudioStartTime=this.animClock),this.streamLipsyncQueue=[],this.streamLipsyncType=t.lipsyncType||this.streamLipsyncType||"visemes",this.streamLipsyncLang=t.lipsyncLang||this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,this.onAudioStart=e,this.onAudioEnd=n,this.onMetrics=s,t.sampleRate!==void 0){const l=t.sampleRate;typeof l=="number"&&l>=8e3&&l<=96e3?l!==this.audioCtx.sampleRate&&this.initAudioGraph(l):console.warn("Invalid sampleRate provided. It must be a number between 8000 and 96000 Hz.")}if(t.gain!==void 0&&(this.audioStreamGainNode.gain.value=t.gain),!this.streamWorkletNode||!this.streamWorkletNode.port||this.streamWorkletNode.numberOfOutputs===0||this.streamWorkletNode.context!==this.audioCtx){if(this.streamWorkletNode)try{this.streamWorkletNode.disconnect(),this.streamWorkletNode=null}catch{}if(!this.workletLoaded)try{const l=this.audioCtx.audioWorklet.addModule(ct.href),h=new Promise((r,u)=>setTimeout(()=>u(new Error("Worklet loading timed out")),5e3));await Promise.race([l,h]),this.workletLoaded=!0}catch(l){throw console.error("Failed to load audio worklet:",l),new Error("Failed to initialize streaming speech")}this.streamWorkletNode=new AudioWorkletNode(this.audioCtx,"playback-worklet",{processorOptions:{sampleRate:this.audioCtx.sampleRate,metrics:t.metrics||{enabled:!1}}}),this.streamWorkletNode.connect(this.audioStreamGainNode),this.streamWorkletNode.connect(this.audioAnalyzerNode),this.streamWorkletNode.port.onmessage=l=>{if(l.data.type==="playback-started"&&(this.isSpeaking=!0,this.stateName="speaking",this.streamWaitForAudioChunks&&(this.streamAudioStartTime=this.animClock),this._processStreamLipsyncQueue(),this.speakWithHands(),this.onAudioStart))try{this.onAudioStart?.()}catch(h){console.error(h)}if(l.data.type==="playback-ended"&&(this._streamPause(),this.onAudioEnd))try{this.onAudioEnd()}catch{}if(this.onMetrics&&l.data.type==="metrics")try{this.onMetrics(l.data)}catch{}}}if(t.metrics)try{this.streamWorkletNode.port.postMessage({type:"config-metrics",data:t.metrics})}catch{}if(this.resetLips(),this.lookAtCamera(500),t.mood&&this.setMood(t.mood),this.onSubtitles=i||null,this.audioCtx.state==="suspended"||this.audioCtx.state==="interrupted"){const l=this.audioCtx.resume(),h=new Promise((r,u)=>setTimeout(()=>u("p2"),1e3));try{await Promise.race([l,h])}catch{console.warn("Can't play audio. Web Audio API suspended. This is often due to calling some speak method before the first user action, which is typically prevented by the browser.");return}}}streamNotifyEnd(){!this.isStreaming||!this.streamWorkletNode||this.streamWorkletNode.port.postMessage({type:"no-more-data"})}streamInterrupt(){if(!this.isStreaming)return;const t=this.isSpeaking;if(this.streamWorkletNode)try{this.streamWorkletNode.port.postMessage({type:"stop"})}catch{}if(this._streamPause(!0),t&&this.onAudioEnd)try{this.onAudioEnd()}catch{}}streamStop(){if(this.isStreaming){if(this.streamInterrupt(),this.streamWorkletNode){try{this.streamWorkletNode.disconnect()}catch{}this.streamWorkletNode=null}this.isStreaming=!1}}_streamPause(t=!1){this.isSpeaking=!1,this.stateName="idle",t&&(this.streamWaitForAudioChunks&&(this.streamAudioStartTime=null),this.streamLipsyncQueue=[],this.animQueue=this.animQueue.filter(e=>e.template.name!=="viseme"&&e.template.name!=="subtitles"&&e.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render()))}_processStreamLipsyncQueue(){if(this.isStreaming)for(;this.streamLipsyncQueue.length>0;){const t=this.streamLipsyncQueue.shift();this._processLipsyncData(t,this.streamAudioStartTime)}}_processLipsyncData(t,e){if(this.isStreaming){if(t.visemes&&this.streamLipsyncType=="visemes")for(let n=0;n<t.visemes.length;n++){const i=t.visemes[n],s=e+t.vtimes[n],o=t.vdurations[n],l={template:{name:"viseme"},ts:[s-2*o/3,s+o/2,s+o+o/2],vs:{["viseme_"+i]:[null,i==="PP"||i==="FF"?.9:.6,0]}};this.animQueue.push(l)}if(t.words&&(this.onSubtitles||this.streamLipsyncType=="words"))for(let n=0;n<t.words.length;n++){const i=t.words[n],s=t.wtimes[n];let o=t.wdurations[n];if(i.length&&(this.onSubtitles&&this.animQueue.push({template:{name:"subtitles"},ts:[e+s],vs:{subtitles:[" "+i]}}),this.streamLipsyncType=="words")){const l=this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,h=this.lipsyncPreProcessText(i,l),r=this.lipsyncWordsToVisemes(h,l);if(r&&r.visemes&&r.visemes.length){const u=r.times[r.visemes.length-1]+r.durations[r.visemes.length-1],a=Math.min(o,Math.max(0,o-r.visemes.length*150));let c=.6+this.convertRange(a,[0,o],[0,.4]);if(o=Math.min(o,r.visemes.length*200),u>0)for(let d=0;d<r.visemes.length;d++){const g=e+s+r.times[d]/u*o,y=r.durations[d]/u*o;this.animQueue.push({template:{name:"viseme"},ts:[g-Math.min(60,2*y/3),g+Math.min(25,y/2),g+y+Math.min(60,y/2)],vs:{["viseme_"+r.visemes[d]]:[null,r.visemes[d]==="PP"||r.visemes[d]==="FF"?.9:c,0]}})}}}}if(t.anims&&this.streamLipsyncType=="blendshapes")for(let n=0;n<t.anims.length;n++){let i=t.anims[n];i.delay+=e;let s=this.animFactory(i,!1,1,1,!0);this.animQueue.push(s)}}}streamAudio(t){if(!(!this.isStreaming||!this.streamWorkletNode)){if(this.isSpeaking||(this.streamLipsyncQueue=[],this.streamAudioStartTime=null),this.isSpeaking=!0,this.stateName="speaking",t.audio!==void 0){const e={type:"audioData",data:null};if(t.audio instanceof ArrayBuffer)e.data=t.audio,this.streamWorkletNode.port.postMessage(e,[e.data]);else if(t.audio instanceof Int16Array||t.audio instanceof Uint8Array){const n=t.audio.buffer.slice(t.audio.byteOffset,t.audio.byteOffset+t.audio.byteLength);e.data=n,this.streamWorkletNode.port.postMessage(e,[e.data])}else if(t.audio instanceof Float32Array){const n=new Int16Array(t.audio.length);for(let i=0;i<t.audio.length;i++){let s=Math.max(-1,Math.min(1,t.audio[i]));n[i]=s<0?s*32768:s*32767}e.data=n.buffer,this.streamWorkletNode.port.postMessage(e,[e.data])}else console.error("r.audio is not a supported type. Must be ArrayBuffer, Int16Array, Uint8Array, or Float32Array:",t.audio)}if(t.visemes||t.anims||t.words){if(this.streamWaitForAudioChunks&&!this.streamAudioStartTime){this.streamLipsyncQueue.length>=200&&this.streamLipsyncQueue.shift(),this.streamLipsyncQueue.push(t);return}else!this.streamWaitForAudioChunks&&!this.streamAudioStartTime&&(this.streamAudioStartTime=this.animClock);this._processLipsyncData(t,this.streamAudioStartTime)}}}makeEyeContact(t){this.animQueue.push(this.animFactory({name:"eyecontact",dt:[0,t],vs:{eyeContact:[1]}}))}lookAhead(t){if(t){let e=(Math.random()-.5)/4,n=(Math.random()-.5)/4,i=this.animQueue.findIndex(o=>o.template.name==="lookat");i!==-1&&this.animQueue.splice(i,1);const s={name:"lookat",dt:[750,t],vs:{bodyRotateX:[e],bodyRotateY:[n],eyesRotateX:[-3*e+.1],eyesRotateY:[-5*n],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(s))}}lookAtCamera(t){let e;if(this.speakTo&&(e=new x.Vector3,this.speakTo.objectLeftEye?.isObject3D?(this.speakTo.armature.objectHead,this.speakTo.objectLeftEye.updateMatrixWorld(!0),this.speakTo.objectRightEye.updateMatrixWorld(!0),fe.setFromMatrixPosition(this.speakTo.objectLeftEye.matrixWorld),xe.setFromMatrixPosition(this.speakTo.objectRightEye.matrixWorld),e.addVectors(fe,xe).divideScalar(2)):this.speakTo.isObject3D?this.speakTo.getWorldPosition(e):this.speakTo.isVector3?e.set(this.speakTo):this.speakTo.x&&this.speakTo.y&&this.speakTo.z&&e.set(this.speakTo.x,this.speakTo.y,this.speakTo.z)),!e){if(this.avatar.hasOwnProperty("avatarIgnoreCamera")){if(this.avatar.avatarIgnoreCamera){this.lookAhead(t);return}}else if(this.opt.avatarIgnoreCamera){this.lookAhead(t);return}this.lookAt(null,null,t);return}this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0),fe.setFromMatrixPosition(this.objectLeftEye.matrixWorld),xe.setFromMatrixPosition(this.objectRightEye.matrixWorld),fe.add(xe).divideScalar(2),Y.copy(this.armature.quaternion),Y.multiply(this.poseTarget.props["Hips.quaternion"]),Y.multiply(this.poseTarget.props["Spine.quaternion"]),Y.multiply(this.poseTarget.props["Spine1.quaternion"]),Y.multiply(this.poseTarget.props["Spine2.quaternion"]),Y.multiply(this.poseTarget.props["Neck.quaternion"]),Y.multiply(this.poseTarget.props["Head.quaternion"]);const n=new x.Vector3().subVectors(e,fe).normalize(),i=Math.atan2(n.x,n.z),s=Math.asin(-n.y);W.set(s,i,0,"YXZ");const l=new x.Quaternion().setFromEuler(W),h=new x.Quaternion().copy(l).multiply(Y.clone().invert());W.setFromQuaternion(h,"YXZ");let r=W.x/(40/24)+.2,u=W.y/(9/4),a=Math.min(.6,Math.max(-.3,r)),c=Math.min(.8,Math.max(-.8,u)),d=(Math.random()-.5)/4,g=(Math.random()-.5)/4;if(t){let y=this.animQueue.findIndex(L=>L.template.name==="lookat");y!==-1&&this.animQueue.splice(y,1);const b={name:"lookat",dt:[750,t],vs:{bodyRotateX:[a+d],bodyRotateY:[c+g],eyesRotateX:[-3*d+.1],eyesRotateY:[-5*g],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(b))}}lookAt(t,e,n){if(!this.camera)return;const i=this.nodeAvatar.getBoundingClientRect();this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0);const s=new x.Vector3().setFromMatrixPosition(this.objectLeftEye.matrixWorld),o=new x.Vector3().setFromMatrixPosition(this.objectRightEye.matrixWorld),l=new x.Vector3().addVectors(s,o).divideScalar(2);l.project(this.camera);let h=(l.x+1)/2*i.width+i.left,r=-(l.y-1)/2*i.height+i.top;t===null&&(t=h),e===null&&(e=r),Y.copy(this.armature.quaternion),Y.multiply(this.poseTarget.props["Hips.quaternion"]),Y.multiply(this.poseTarget.props["Spine.quaternion"]),Y.multiply(this.poseTarget.props["Spine1.quaternion"]),Y.multiply(this.poseTarget.props["Spine2.quaternion"]),Y.multiply(this.poseTarget.props["Neck.quaternion"]),Y.multiply(this.poseTarget.props["Head.quaternion"]),W.setFromQuaternion(Y);let u=W.x/(40/24),a=W.y/(9/4),c=Math.min(.4,Math.max(-.4,this.camera.rotation.x)),d=Math.min(.4,Math.max(-.4,this.camera.rotation.y)),g=Math.max(window.innerWidth-h,h),y=Math.max(window.innerHeight-r,r),b=this.convertRange(e,[r-y,r+y],[-.3,.6])-u+c,L=this.convertRange(t,[h-g,h+g],[-.8,.8])-a+d;b=Math.min(.6,Math.max(-.3,b)),L=Math.min(.8,Math.max(-.8,L));let V=(Math.random()-.5)/4,p=(Math.random()-.5)/4;if(n){let M=this.animQueue.findIndex(f=>f.template.name==="lookat");M!==-1&&this.animQueue.splice(M,1);const C={name:"lookat",dt:[750,n],vs:{bodyRotateX:[b+V],bodyRotateY:[L+p],eyesRotateX:[-3*V+.1],eyesRotateY:[-5*p],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(C))}}touchAt(t,e){if(!this.camera)return;const n=this.nodeAvatar.getBoundingClientRect(),i=new x.Vector2((t-n.left)/n.width*2-1,-((e-n.top)/n.height)*2+1),s=new x.Raycaster;s.setFromCamera(i,this.camera);const o=s.intersectObject(this.armature);if(o.length>0){const l=o[0].point,h=new x.Vector3,r=new x.Vector3;this.objectLeftArm.getWorldPosition(h),this.objectRightArm.getWorldPosition(r);const u=h.distanceToSquared(l),a=r.distanceToSquared(l);u<a?(this.ikSolve({iterations:20,root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3,maxAngle:.2},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},l,!1,1e3),this.setValue("handFistLeft",0)):(this.ikSolve({iterations:20,root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5,maxAngle:.2},{link:"RightArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},l,!1,1e3),this.setValue("handFistRight",0))}else["LeftArm","LeftForeArm","LeftHand","RightArm","RightForeArm","RightHand"].forEach(l=>{let h=l+".quaternion";this.poseTarget.props[h].copy(this.getPoseTemplateProp(h)),this.poseTarget.props[h].t=this.animClock,this.poseTarget.props[h].d=1e3});return o.length>0}speakWithHands(t=0,e=.5){if(this.mixer||this.gesture||!this.poseTarget.template.standing||this.poseTarget.template.bend||Math.random()>e)return;this.ikSolve({root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-1,maxz:3}]},new x.Vector3(this.gaussianRandom(0,.5),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0),this.ikSolve({root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5},{link:"RightArm"}]},new x.Vector3(this.gaussianRandom(-.5,0),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0);const n=[],i=[];n.push(100+Math.round(Math.random()*500)),i.push({duration:1e3,props:{"LeftHand.quaternion":new x.Quaternion().setFromEuler(new x.Euler(0,-1-Math.random(),0)),"RightHand.quaternion":new x.Quaternion().setFromEuler(new x.Euler(0,1+Math.random(),0))}}),["LeftArm","LeftForeArm","RightArm","RightForeArm"].forEach(o=>{i[0].props[o+".quaternion"]=this.ikMesh.getObjectByName(o).quaternion.clone()}),n.push(1e3+Math.round(Math.random()*500)),i.push({duration:2e3,props:{}}),["LeftArm","LeftForeArm","RightArm","RightForeArm","LeftHand","RightHand"].forEach(o=>{i[1].props[o+".quaternion"]=null});const s=this.animFactory({name:"talkinghands",delay:t,dt:n,vs:{moveto:i}});this.animQueue.push(s)}getSlowdownRate(t){return this.animSlowdownRate}setSlowdownRate(t){this.animSlowdownRate=t,this.audioSpeechSource.playbackRate.value=1/this.animSlowdownRate,this.audioBackgroundSource.playbackRate.value=1/this.animSlowdownRate}getAutoRotateSpeed(t){return this.controls.autoRotateSpeed}setAutoRotateSpeed(t){this.controls.autoRotateSpeed=t,this.controls.autoRotate=t>0}start(){this.armature&&this.isRunning===!1&&(this.audioCtx.resume(),this.animTimeLast=performance.now(),this.isRunning=!0,this.isAvatarOnly||requestAnimationFrame(this.animate.bind(this)))}stop(){this.isRunning=!1,this.audioCtx.suspend()}startListening(t,e={},n=null){this.listeningAnalyzer=t,this.listeningAnalyzer.fftSize=256,this.listeningAnalyzer.smoothingTimeConstant=.1,this.listeningAnalyzer.minDecibels=-70,this.listeningAnalyzer.maxDecibels=-10,this.listeningOnchange=n&&typeof n=="function"?n:null,this.listeningSilenceThresholdLevel=e?.hasOwnProperty("listeningSilenceThresholdLevel")?e.listeningSilenceThresholdLevel:this.opt.listeningSilenceThresholdLevel,this.listeningSilenceThresholdMs=e?.hasOwnProperty("listeningSilenceThresholdMs")?e.listeningSilenceThresholdMs:this.opt.listeningSilenceThresholdMs,this.listeningSilenceDurationMax=e?.hasOwnProperty("listeningSilenceDurationMax")?e.listeningSilenceDurationMax:this.opt.listeningSilenceDurationMax,this.listeningActiveThresholdLevel=e?.hasOwnProperty("listeningActiveThresholdLevel")?e.listeningActiveThresholdLevel:this.opt.listeningActiveThresholdLevel,this.listeningActiveThresholdMs=e?.hasOwnProperty("listeningActiveThresholdMs")?e.listeningActiveThresholdMs:this.opt.listeningActiveThresholdMs,this.listeningActiveDurationMax=e?.hasOwnProperty("listeningActiveDurationMax")?e.listeningActiveDurationMax:this.opt.listeningActiveDurationMax,this.listeningActive=!1,this.listeningVolume=0,this.listeningTimer=0,this.listeningTimerTotal=0,this.isListening=!0}stopListening(){this.isListening=!1}async playAnimation(t,e=null,n=10,i=0,s=.01,o=!1){if(!this.armature)return;this.positionWasLocked=!o,o?console.log("Position locking disabled for FBX animation:",t):(this.lockAvatarPosition(),console.log("Position locked immediately before FBX animation:",t));let l=this.animClips.find(h=>h.url===t+"-"+i);if(l){let h=this.animQueue.find(a=>a.template.name==="pose");h&&(h.ts[0]=1/0),Object.entries(l.pose.props).forEach(a=>{this.poseBase.props[a[0]]=a[1].clone(),this.poseTarget.props[a[0]]=a[1].clone(),this.poseTarget.props[a[0]].t=0,this.poseTarget.props[a[0]].d=1e3}),this.mixer?console.log("Using existing mixer for FBX animation, preserving morph targets"):(this.mixer=new x.AnimationMixer(this.armature),console.log("Created new mixer for FBX animation")),this.mixer.addEventListener("finished",this.stopAnimation.bind(this),{once:!0});const r=Math.ceil(n/l.clip.duration),u=this.mixer.clipAction(l.clip);u.setLoop(x.LoopRepeat,r),u.clampWhenFinished=!0,this.currentFBXAction=u;try{u.fadeIn(.5).play(),console.log("FBX animation started successfully:",t)}catch(a){console.warn("FBX animation failed to start:",a),this.stopAnimation();return}if(u.getClip().tracks.length===0){console.warn("FBX animation has no valid tracks, stopping"),this.stopAnimation();return}}else{if(t.split(".").pop().toLowerCase()!=="fbx"){console.error(`Invalid file type for FBX animation: ${t}. Expected .fbx file.`);return}let r=!1;try{const c=await fetch(t,{method:"HEAD"});if(r=c.ok,!r){console.error(`FBX file not found at ${t}. Status: ${c.status}`),console.error("Please check:"),console.error("1. File path is correct (note: path is case-sensitive)"),console.error("2. File exists in your public folder"),console.error("3. File is accessible (not blocked by server)");return}}catch(c){console.warn(`Could not verify file existence for ${t}, attempting to load anyway:`,c)}const u=new Pe.FBXLoader;let a;try{a=await u.loadAsync(t,e)}catch(c){console.error(`Failed to load FBX animation from ${t}:`,c),console.error("Error details:",{message:c.message,url:t,suggestion:"Make sure the file is a valid FBX file and the path is correct"}),c.message&&c.message.includes("version number")&&(console.error("FBX Loader Error: Cannot find version number"),console.error("This error usually means:"),console.error("1. The file is not a valid FBX file (might be GLB, corrupted, or wrong format)"),console.error("2. The file might be corrupted"),console.error("3. The file path might be incorrect"),console.error("4. The server returned an HTML error page instead of the FBX file"),console.error("5. The file might not exist at that path"),console.error(""),console.error("Solution: Please verify:"),console.error(` - File exists at: ${t}`),console.error(" - File is a valid FBX binary file"),console.error(" - File path matches your public folder structure"),console.error(" - File is not corrupted"));try{const d=await fetch(t),g=d.headers.get("content-type"),y=await d.text();console.error("Response details:",{status:d.status,contentType:g,firstBytes:y.substring(0,100),isHTML:y.trim().startsWith("<!DOCTYPE")||y.trim().startsWith("<html")}),(y.trim().startsWith("<!DOCTYPE")||y.trim().startsWith("<html"))&&console.error("The server returned an HTML page instead of an FBX file. The file path is likely incorrect.")}catch(d){console.error("Could not fetch file for debugging:",d)}return}if(a&&a.animations&&a.animations[i]){let c=a.animations[i];const d={};c.tracks.forEach(y=>{y.name=y.name.replaceAll("mixamorig","");const b=y.name.split(".");if(b[1]==="position"){for(let L=0;L<y.values.length;L++)y.values[L]=y.values[L]*s;d[y.name]=new x.Vector3(y.values[0],y.values[1],y.values[2])}else b[1]==="quaternion"?d[y.name]=new x.Quaternion(y.values[0],y.values[1],y.values[2],y.values[3]):b[1]==="rotation"&&(d[b[0]+".quaternion"]=new x.Quaternion().setFromEuler(new x.Euler(y.values[0],y.values[1],y.values[2],"XYZ")).normalize())});const g={props:d};d["Hips.position"]&&(d["Hips.position"].y<.5?g.lying=!0:g.standing=!0),this.animClips.push({url:t+"-"+i,clip:c,pose:g}),this.playAnimation(t,e,n,i,s)}else{const c="Animation "+t+" (ndx="+i+") not found";console.error(c),a&&a.animations?console.error(`FBX file loaded but has ${a.animations.length} animation(s), requested index ${i}`):console.error(a?"FBX file loaded but contains no animations":"FBX file failed to load or is invalid")}}}stopAnimation(){if(this.currentFBXAction&&(this.currentFBXAction.stop(),this.currentFBXAction=null,console.log("FBX animation action stopped, mixer preserved for lip-sync")),this.mixer&&this.mixer._actions.length===0&&(this.mixer=null,console.log("Mixer destroyed as no actions remain")),this.positionWasLocked?(this.unlockAvatarPosition(),console.log("Position unlocked after FBX animation stopped")):console.log("Position was not locked, no unlock needed"),this.gesture)for(let[e,n]of Object.entries(this.gesture))n.t=this.animClock,n.d=1e3,this.poseTarget.props.hasOwnProperty(e)&&(this.poseTarget.props[e].copy(n),this.poseTarget.props[e].t=this.animClock,this.poseTarget.props[e].d=1e3);let t=this.animQueue.find(e=>e.template.name==="pose");t&&(t.ts[0]=this.animClock),this.setPoseFromTemplate(null)}async playPose(t,e=null,n=5,i=0,s=.01){if(!this.armature)return;let o=this.poseTemplates[t];if(!o){const l=this.animPoses.find(h=>h.url===t+"-"+i);l&&(o=l.pose)}if(o){this.poseName=t,this.mixer=null;let l=this.animQueue.find(h=>h.template.name==="pose");l&&(l.ts[0]=this.animClock+n*1e3+2e3),this.setPoseFromTemplate(o)}else{let h=await new Pe.FBXLoader().loadAsync(t,e);if(h&&h.animations&&h.animations[i]){let r=h.animations[i];const u={};r.tracks.forEach(c=>{c.name=c.name.replaceAll("mixamorig","");const d=c.name.split(".");d[1]==="position"?u[c.name]=new x.Vector3(c.values[0]*s,c.values[1]*s,c.values[2]*s):d[1]==="quaternion"?u[c.name]=new x.Quaternion(c.values[0],c.values[1],c.values[2],c.values[3]):d[1]==="rotation"&&(u[d[0]+".quaternion"]=new x.Quaternion().setFromEuler(new x.Euler(c.values[0],c.values[1],c.values[2],"XYZ")).normalize())});const a={props:u};u["Hips.position"]&&(u["Hips.position"].y<.5?a.lying=!0:a.standing=!0),this.animPoses.push({url:t+"-"+i,pose:a}),this.playPose(t,e,n,i,s)}else{const r="Pose "+t+" (ndx="+i+") not found";console.error(r)}}}stopPose(){this.stopAnimation()}playGesture(t,e=3,n=!1,i=1e3){if(!this.armature)return;let s=this.gestureTemplates[t];if(s){this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null);let l=this.animQueue.findIndex(h=>h.template.name==="talkinghands");l!==-1&&(this.animQueue[l].ts=this.animQueue[l].ts.map(h=>0)),this.gesture=this.propsToThreeObjects(s),n&&(this.gesture=this.mirrorPose(this.gesture)),t==="namaste"&&this.avatar.body==="M"&&(this.gesture["RightArm.quaternion"].rotateTowards(new x.Quaternion(0,1,0,0),-.25),this.gesture["LeftArm.quaternion"].rotateTowards(new x.Quaternion(0,1,0,0),-.25));for(let[h,r]of Object.entries(this.gesture))r.t=this.animClock,r.d=i,this.poseTarget.props.hasOwnProperty(h)&&(this.poseTarget.props[h].copy(r),this.poseTarget.props[h].t=this.animClock,this.poseTarget.props[h].d=i);e&&Number.isFinite(e)&&(this.gestureTimeout=setTimeout(this.stopGesture.bind(this,i),1e3*e))}let o=this.animEmojis[t];if(o&&(o&&o.link&&(o=this.animEmojis[o.link]),o)){this.lookAtCamera(500);const l=this.animFactory(o);if(l.gesture=!0,e&&Number.isFinite(e)){const h=l.ts[0],u=l.ts[l.ts.length-1]-h;if(e*1e3-u>0){const c=[];for(let y=1;y<l.ts.length;y++)c.push(l.ts[y]-l.ts[y-1]);const d=o.template?.rescale||c.map(y=>y/u),g=e*1e3-u;l.ts=l.ts.map((y,b,L)=>b===0?h:L[b-1]+c[b-1]+d[b-1]*g)}else{const c=e*1e3/u;l.ts=l.ts.map(d=>h+c*(d-h))}}this.animQueue.push(l)}}stopGesture(t=1e3){if(this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null),this.gesture){const n=Object.entries(this.gesture);this.gesture=null;for(const[i,s]of n)this.poseTarget.props.hasOwnProperty(i)&&(this.poseTarget.props[i].copy(this.getPoseTemplateProp(i)),this.poseTarget.props[i].t=this.animClock,this.poseTarget.props[i].d=t)}let e=this.animQueue.findIndex(n=>n.gesture);e!==-1&&this.animQueue.splice(e,1)}ikSolve(t,e=null,n=!1,i=null){const s=new x.Vector3,o=new x.Vector3,l=new x.Vector3,h=new x.Vector3,r=new x.Quaternion,u=new x.Vector3,a=new x.Vector3,c=new x.Vector3,d=this.ikMesh.getObjectByName(t.root);d.position.setFromMatrixPosition(this.armature.getObjectByName(t.root).matrixWorld),d.quaternion.setFromRotationMatrix(this.armature.getObjectByName(t.root).matrixWorld),e&&n&&e.applyQuaternion(this.armature.quaternion).add(d.position);const g=this.ikMesh.getObjectByName(t.effector),y=t.links;y.forEach(L=>{L.bone=this.ikMesh.getObjectByName(L.link),L.bone.quaternion.copy(this.getPoseTemplateProp(L.link+".quaternion"))}),d.updateMatrixWorld(!0);const b=t.iterations||10;if(e)for(let L=0;L<b;L++){let V=!1;for(let p=0,M=y.length;p<M;p++){const C=y[p].bone;C.matrixWorld.decompose(h,r,u),r.invert(),o.setFromMatrixPosition(g.matrixWorld),l.subVectors(o,h),l.applyQuaternion(r),l.normalize(),s.subVectors(e,h),s.applyQuaternion(r),s.normalize();let f=s.dot(l);f>1?f=1:f<-1&&(f=-1),f=Math.acos(f),!(f<1e-5)&&(y[p].minAngle!==void 0&&f<y[p].minAngle&&(f=y[p].minAngle),y[p].maxAngle!==void 0&&f>y[p].maxAngle&&(f=y[p].maxAngle),a.crossVectors(l,s),a.normalize(),Y.setFromAxisAngle(a,f),C.quaternion.multiply(Y),C.rotation.setFromVector3(c.setFromEuler(C.rotation).clamp(new x.Vector3(y[p].minx!==void 0?y[p].minx:-1/0,y[p].miny!==void 0?y[p].miny:-1/0,y[p].minz!==void 0?y[p].minz:-1/0),new x.Vector3(y[p].maxx!==void 0?y[p].maxx:1/0,y[p].maxy!==void 0?y[p].maxy:1/0,y[p].maxz!==void 0?y[p].maxz:1/0))),C.updateMatrixWorld(!0),V=!0)}if(!V)break}i&&y.forEach(L=>{this.poseTarget.props[L.link+".quaternion"].copy(L.bone.quaternion),this.poseTarget.props[L.link+".quaternion"].t=this.animClock,this.poseTarget.props[L.link+".quaternion"].d=i})}dispose(){this.isRunning=!1,this.stop(),this.stopSpeaking(),this.streamStop(),this.isAvatarOnly?this.armature&&(this.armature.parent&&this.armature.parent.remove(this.armature),this.clearThree(this.armature)):(this.clearThree(this.scene),this.resizeobserver.disconnect(),this.renderer&&(this.renderer.dispose(),this.renderer.domElement&&this.renderer.domElement.parentNode&&this.renderer.domElement.parentNode.removeChild(this.renderer.domElement),this.renderer=null)),this.clearThree(this.ikMesh),this.dynamicbones.dispose()}}const be={apiKey:"sk_ace57ef3ef65a92b9d3bee2a00183b78ca790bc3e10964f2",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",defaultVoice:"21m00Tcm4TlvDq8ikWAM",voices:{rachel:"21m00Tcm4TlvDq8ikWAM",drew:"29vD33N1CtxCmqQRPOHJ",bella:"EXAVITQu4vr4xnSDxMaL",antoni:"ErXwobaYiN019PkySvjV",elli:"MF3mGyEYCl7XYWbV9V6O",josh:"VR6AewLTigWG4xSOukaG"}},ze={defaultVoice:"aura-2-thalia-en",voices:{thalia:"aura-2-thalia-en",asteria:"aura-2-asteria-en",orion:"aura-2-orion-en",stella:"aura-2-stella-en",athena:"aura-2-athena-en",hera:"aura-2-hera-en",zeus:"aura-2-zeus-en"}};function Ae(){return{service:"elevenlabs",endpoint:be.endpoint,apiKey:be.apiKey,defaultVoice:be.defaultVoice,voices:be.voices}}function mt(){const B=Ae(),t=[];return Object.entries(B.voices).forEach(([e,n])=>{t.push({value:n,label:`${e.charAt(0).toUpperCase()+e.slice(1)} (${B.service})`})}),t}const Me=R.forwardRef(({avatarUrl:B="/avatars/brunette.glb",avatarBody:t="F",mood:e="neutral",ttsLang:n="en",ttsService:i=null,ttsVoice:s=null,ttsApiKey:o=null,bodyMovement:l="idle",movementIntensity:h=.5,showFullAvatar:r=!0,cameraView:u="upper",onReady:a=()=>{},onLoading:c=()=>{},onError:d=()=>{},className:g="",style:y={},animations:b={}},L)=>{const V=R.useRef(null),p=R.useRef(null),M=R.useRef(r),C=R.useRef(null),f=R.useRef(null),E=R.useRef(!1),P=R.useRef({remainingText:null,originalText:null,options:null}),U=R.useRef([]),ie=R.useRef(0),[S,G]=R.useState(!0),[q,Z]=R.useState(null),[J,oe]=R.useState(!1),[se,ce]=R.useState(!1);R.useEffect(()=>{E.current=se},[se]),R.useEffect(()=>{M.current=r},[r]);const $=Ae(),le=i||$.service;let D;le==="browser"?D={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:le==="elevenlabs"?D={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:o||$.apiKey,defaultVoice:s||$.defaultVoice||be.defaultVoice,voices:$.voices||be.voices}:le==="deepgram"?D={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:o||$.apiKey,defaultVoice:s||$.defaultVoice||ze.defaultVoice,voices:$.voices||ze.voices}:D={...$,apiKey:o!==null?o:$.apiKey};const v={url:B,body:t,avatarMood:e,ttsLang:le==="browser"?"en-US":n,ttsVoice:s||D.defaultVoice,lipsyncLang:"en",showFullAvatar:r,bodyMovement:l,movementIntensity:h},I={ttsEndpoint:D.endpoint,ttsApikey:D.apiKey,ttsService:le,lipsyncModules:["en"],cameraView:u},z=R.useCallback(async()=>{if(!(!V.current||p.current))try{if(G(!0),Z(null),p.current=new Te(V.current,I),p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1),b&&Object.keys(b).length>0&&(p.current.customAnimations=b),await p.current.showAvatar(v,O=>{if(O.lengthComputable){const K=Math.min(100,Math.round(O.loaded/O.total*100));c(K)}}),await new Promise(O=>{const K=()=>{p.current.lipsync&&Object.keys(p.current.lipsync).length>0?O():setTimeout(K,100)};K()}),p.current&&p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(r)}catch(O){console.warn("Error setting full body mode on initialization:",O)}p.current&&p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1,p.current.controls.update()),G(!1),oe(!0),a(p.current);const F=()=>{document.visibilityState==="visible"?p.current?.start():p.current?.stop()};return document.addEventListener("visibilitychange",F),()=>{document.removeEventListener("visibilitychange",F)}}catch(A){console.error("Error initializing TalkingHead:",A),Z(A.message||"Failed to initialize avatar"),G(!1),d(A)}},[B,t,e,n,i,s,o,r,l,h,u]);R.useEffect(()=>(z(),()=>{p.current&&(p.current.stop(),p.current.dispose(),p.current=null)}),[z]),R.useEffect(()=>{if(!V.current||!p.current)return;const A=new ResizeObserver(O=>{for(const K of O)p.current&&p.current.onResize&&p.current.onResize()});A.observe(V.current);const F=()=>{p.current&&p.current.onResize&&p.current.onResize()};return window.addEventListener("resize",F),()=>{A.disconnect(),window.removeEventListener("resize",F)}},[J]);const T=R.useCallback(async()=>{if(p.current&&p.current.audioCtx)try{(p.current.audioCtx.state==="suspended"||p.current.audioCtx.state==="interrupted")&&(await p.current.audioCtx.resume(),console.log("Audio context resumed"))}catch(A){console.warn("Failed to resume audio context:",A)}},[]),N=R.useCallback(async(A,F={})=>{if(p.current&&J)try{f.current&&(clearInterval(f.current),f.current=null),C.current={text:A,options:F},P.current={remainingText:null,originalText:null,options:null};const O=/[!\.\?\n\p{Extended_Pictographic}]/ug,K=A.split(O).map(X=>X.trim()).filter(X=>X.length>0);U.current=K,ie.current=0,ce(!1),E.current=!1,await T();const de={...F,lipsyncLang:F.lipsyncLang||v.lipsyncLang||"en"};if(F.onSpeechEnd&&p.current){const X=p.current;let he=null,Ie=0;const Re=1200;let ye=!1;he=setInterval(()=>{if(Ie++,E.current)return;if(Ie>Re){if(he&&(clearInterval(he),he=null,f.current=null),!ye&&!E.current){ye=!0;try{F.onSpeechEnd()}catch(Fe){console.error("Error in onSpeechEnd callback (timeout):",Fe)}}return}const me=!X.speechQueue||X.speechQueue.length===0,Le=!X.audioPlaylist||X.audioPlaylist.length===0;X&&X.isSpeaking===!1&&me&&Le&&X.isAudioPlaying===!1&&!ye&&!E.current&&setTimeout(()=>{if(X&&!E.current&&X.isSpeaking===!1&&(!X.speechQueue||X.speechQueue.length===0)&&(!X.audioPlaylist||X.audioPlaylist.length===0)&&X.isAudioPlaying===!1&&!ye&&!E.current){ye=!0,he&&(clearInterval(he),he=null,f.current=null);try{F.onSpeechEnd()}catch(Ve){console.error("Error in onSpeechEnd callback:",Ve)}}},100)},100),f.current=he}p.current.lipsync&&Object.keys(p.current.lipsync).length>0?(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(A,de)):setTimeout(async()=>{await T(),p.current&&p.current.lipsync&&(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(A,de))},100)}catch(O){console.error("Error speaking text:",O),Z(O.message||"Failed to speak text")}},[J,T,v.lipsyncLang]),_=R.useCallback(()=>{p.current&&(p.current.stopSpeaking(),p.current.setSlowdownRate&&p.current.setSlowdownRate(1),C.current=null,ce(!1))},[]),j=R.useCallback(()=>{if(p.current&&p.current.pauseSpeaking){const A=p.current;if(A.isSpeaking||A.audioPlaylist&&A.audioPlaylist.length>0||A.speechQueue&&A.speechQueue.length>0){f.current&&(clearInterval(f.current),f.current=null);let O="";if(C.current&&U.current.length>0){const K=U.current.length,de=A.speechQueue?A.speechQueue.filter(Re=>Re&&Re.text&&Array.isArray(Re.text)&&Re.text.length>0).length:0,X=A.audioPlaylist&&A.audioPlaylist.length>0,he=de+(X?1:0),Ie=K-he;if(he>0&&Ie<K&&(O=U.current.slice(Ie).join(". ").trim(),!O&&de>0&&A.speechQueue)){const ye=A.speechQueue.filter(me=>me&&me.text&&Array.isArray(me.text)&&me.text.length>0).map(me=>me.text.map(Le=>Le.word||"").filter(Le=>Le.length>0).join(" ")).filter(me=>me.length>0).join(" ");ye&&ye.trim()&&(O=ye.trim())}}C.current&&(P.current={remainingText:O||null,originalText:C.current.text,options:C.current.options}),A.speechQueue&&(A.speechQueue.length=0),p.current.pauseSpeaking(),E.current=!0,ce(!0)}}},[]),Q=R.useCallback(async()=>{if(!p.current||!se)return;let A="",F={};if(P.current&&P.current.remainingText)A=P.current.remainingText,F=P.current.options||{},P.current={remainingText:null,originalText:null,options:null};else if(C.current&&C.current.text)A=C.current.text,F=C.current.options||{};else{console.warn("Resume called but no paused speech found"),ce(!1),E.current=!1;return}ce(!1),E.current=!1,await T();const O={...F,lipsyncLang:F.lipsyncLang||v.lipsyncLang||"en"};try{await N(A,O)}catch(K){console.error("Error resuming speech:",K),ce(!1),E.current=!1}},[T,se,N,v]),ve=R.useCallback(A=>{p.current&&p.current.setMood(A)},[]),ke=R.useCallback(A=>{p.current&&p.current.setSlowdownRate&&p.current.setSlowdownRate(A)},[]),H=R.useCallback((A,F=!1)=>{if(p.current&&p.current.playAnimation){if(b&&b[A]&&(A=b[A]),p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(M.current)}catch(K){console.warn("Error setting full body mode:",K)}if(A.includes("."))try{p.current.playAnimation(A,null,10,0,.01,F)}catch(K){console.warn(`Failed to play ${A}:`,K);try{p.current.setBodyMovement("idle")}catch(de){console.warn("Fallback animation also failed:",de)}}else{const K=[".fbx",".glb",".gltf"];let de=!1;for(const X of K)try{p.current.playAnimation(A+X,null,10,0,.01,F),de=!0;break}catch{}if(!de){console.warn("Animation not found:",A);try{p.current.setBodyMovement("idle")}catch(X){console.warn("Fallback animation also failed:",X)}}}}},[b]),ee=R.useCallback(()=>{p.current&&p.current.onResize&&p.current.onResize()},[]);return R.useImperativeHandle(L,()=>({speakText:N,stopSpeaking:_,pauseSpeaking:j,resumeSpeaking:Q,resumeAudioContext:T,setMood:ve,setTimingAdjustment:ke,playAnimation:H,isReady:J,isPaused:se,talkingHead:p.current,handleResize:ee,setBodyMovement:A=>{if(p.current&&p.current.setShowFullAvatar&&p.current.setBodyMovement)try{p.current.setShowFullAvatar(M.current),p.current.setBodyMovement(A)}catch(F){console.warn("Error setting body movement:",F)}},setMovementIntensity:A=>p.current?.setMovementIntensity(A),playRandomDance:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playRandomDance)try{p.current.setShowFullAvatar(M.current),p.current.playRandomDance()}catch(A){console.warn("Error playing random dance:",A)}},playReaction:A=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playReaction)try{p.current.setShowFullAvatar(M.current),p.current.playReaction(A)}catch(F){console.warn("Error playing reaction:",F)}},playCelebration:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playCelebration)try{p.current.setShowFullAvatar(M.current),p.current.playCelebration()}catch(A){console.warn("Error playing celebration:",A)}},setShowFullAvatar:A=>{if(p.current&&p.current.setShowFullAvatar)try{M.current=A,p.current.setShowFullAvatar(A)}catch(F){console.warn("Error setting showFullAvatar:",F)}},lockAvatarPosition:()=>{if(p.current&&p.current.lockAvatarPosition)try{p.current.lockAvatarPosition()}catch(A){console.warn("Error locking avatar position:",A)}},unlockAvatarPosition:()=>{if(p.current&&p.current.unlockAvatarPosition)try{p.current.unlockAvatarPosition()}catch(A){console.warn("Error unlocking avatar position:",A)}}})),re.jsxs("div",{className:`talking-head-avatar ${g}`,style:{width:"100%",height:"100%",position:"relative",...y},children:[re.jsx("div",{ref:V,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),S&&re.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),q&&re.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:q})]})});Me.displayName="TalkingHeadAvatar";const Ne=R.forwardRef(({text:B="Hello! I'm a talking avatar. How are you today?",onLoading:t=()=>{},onError:e=()=>{},onReady:n=()=>{},className:i="",style:s={},avatarConfig:o={}},l)=>{const h=R.useRef(null),r=R.useRef(null),[u,a]=R.useState(!0),[c,d]=R.useState(null),[g,y]=R.useState(!1),b=Ae(),L=o.ttsService||b.service,V=L==="browser"?{endpoint:"",apiKey:null,defaultVoice:"Google US English"}:{...b,apiKey:o.ttsApiKey!==void 0&&o.ttsApiKey!==null?o.ttsApiKey:b.apiKey,endpoint:L==="elevenlabs"&&o.ttsApiKey?"https://api.elevenlabs.io/v1/text-to-speech":b.endpoint},p={url:"/avatars/brunette.glb",body:"F",avatarMood:"neutral",ttsLang:L==="browser"?"en-US":"en",ttsVoice:o.ttsVoice||V.defaultVoice,lipsyncLang:"en",showFullAvatar:!0,bodyMovement:"idle",movementIntensity:.5,...o},M={ttsEndpoint:V.endpoint,ttsApikey:V.apiKey,ttsService:L,lipsyncModules:["en"],cameraView:"upper"},C=R.useCallback(async()=>{if(!(!h.current||r.current))try{if(a(!0),d(null),r.current=new Te(h.current,M),await r.current.showAvatar(p,q=>{if(q.lengthComputable){const Z=Math.min(100,Math.round(q.loaded/q.total*100));t(Z)}}),r.current.morphs&&r.current.morphs.length>0){const q=r.current.morphs[0].morphTargetDictionary;console.log("Available morph targets:",Object.keys(q));const Z=Object.keys(q).filter(J=>J.startsWith("viseme_"));console.log("Viseme morph targets found:",Z),Z.length===0&&(console.warn("No viseme morph targets found! Lip-sync will not work properly."),console.log("Expected viseme targets: viseme_aa, viseme_E, viseme_I, viseme_O, viseme_U, viseme_PP, viseme_SS, viseme_TH, viseme_DD, viseme_FF, viseme_kk, viseme_nn, viseme_RR, viseme_CH, viseme_sil"))}if(await new Promise(q=>{const Z=()=>{r.current.lipsync&&Object.keys(r.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(r.current.lipsync)),q()):(console.log("Waiting for lip-sync modules to load..."),setTimeout(Z,100))};Z()}),r.current&&r.current.setShowFullAvatar)try{r.current.setShowFullAvatar(!0),console.log("Avatar initialized in full body mode")}catch(q){console.warn("Error setting full body mode on initialization:",q)}a(!1),y(!0),n(r.current);const G=()=>{document.visibilityState==="visible"?r.current?.start():r.current?.stop()};return document.addEventListener("visibilitychange",G),()=>{document.removeEventListener("visibilitychange",G)}}catch(S){console.error("Error initializing TalkingHead:",S),d(S.message||"Failed to initialize avatar"),a(!1),e(S)}},[]);R.useEffect(()=>(C(),()=>{r.current&&(r.current.stop(),r.current.dispose(),r.current=null)}),[C]);const f=R.useCallback(S=>{if(r.current&&g)try{console.log("Speaking text:",S),console.log("Avatar config:",p),console.log("TalkingHead instance:",r.current),r.current.lipsync&&Object.keys(r.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(r.current.lipsync)),r.current.setSlowdownRate&&(r.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),r.current.speakText(S)):(console.warn("Lip-sync modules not ready, waiting..."),setTimeout(()=>{r.current&&r.current.lipsync?(console.log("Lip-sync now ready, speaking..."),r.current.setSlowdownRate&&(r.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),r.current.speakText(S)):console.error("Lip-sync still not ready after waiting")},500))}catch(G){console.error("Error speaking text:",G),d(G.message||"Failed to speak text")}else console.warn("Avatar not ready for speaking. isReady:",g,"talkingHeadRef:",!!r.current)},[g,p]),E=R.useCallback(()=>{r.current&&(r.current.stopSpeaking(),r.current.setSlowdownRate&&(r.current.setSlowdownRate(1),console.log("Reset timing to normal")))},[]),P=R.useCallback(S=>{r.current&&r.current.setMood(S)},[]),U=R.useCallback(S=>{r.current&&r.current.setSlowdownRate&&(r.current.setSlowdownRate(S),console.log("Timing adjustment set to:",S))},[]),ie=R.useCallback((S,G=!1)=>{if(r.current&&r.current.playAnimation){if(r.current.setShowFullAvatar)try{r.current.setShowFullAvatar(!0)}catch(Z){console.warn("Error setting full body mode:",Z)}if(S.includes("."))try{r.current.playAnimation(S,null,10,0,.01,G),console.log("Playing animation:",S)}catch(Z){console.log(`Failed to play ${S}:`,Z);try{r.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(J){console.warn("Fallback animation also failed:",J)}}else{const Z=[".fbx",".glb",".gltf"];let J=!1;for(const oe of Z)try{r.current.playAnimation(S+oe,null,10,0,.01,G),console.log("Playing animation:",S+oe),J=!0;break}catch{console.log(`Failed to play ${S}${oe}, trying next format...`)}if(!J){console.warn("Animation system not available or animation not found:",S);try{r.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(oe){console.warn("Fallback animation also failed:",oe)}}}}else console.warn("Animation system not available or animation not found:",S)},[]);return R.useImperativeHandle(l,()=>({speakText:f,stopSpeaking:E,setMood:P,setTimingAdjustment:U,playAnimation:ie,isReady:g,talkingHead:r.current,setBodyMovement:S=>{if(r.current&&r.current.setShowFullAvatar&&r.current.setBodyMovement)try{r.current.setShowFullAvatar(!0),r.current.setBodyMovement(S),console.log("Body movement set with full body mode:",S)}catch(G){console.warn("Error setting body movement:",G)}},setMovementIntensity:S=>r.current?.setMovementIntensity(S),playRandomDance:()=>{if(r.current&&r.current.setShowFullAvatar&&r.current.playRandomDance)try{r.current.setShowFullAvatar(!0),r.current.playRandomDance(),console.log("Random dance played with full body mode")}catch(S){console.warn("Error playing random dance:",S)}},playReaction:S=>{if(r.current&&r.current.setShowFullAvatar&&r.current.playReaction)try{r.current.setShowFullAvatar(!0),r.current.playReaction(S),console.log("Reaction played with full body mode:",S)}catch(G){console.warn("Error playing reaction:",G)}},playCelebration:()=>{if(r.current&&r.current.setShowFullAvatar&&r.current.playCelebration)try{r.current.setShowFullAvatar(!0),r.current.playCelebration(),console.log("Celebration played with full body mode")}catch(S){console.warn("Error playing celebration:",S)}},setShowFullAvatar:S=>{if(r.current&&r.current.setShowFullAvatar)try{r.current.setShowFullAvatar(S),console.log("Show full avatar set to:",S)}catch(G){console.warn("Error setting showFullAvatar:",G)}},lockAvatarPosition:()=>{if(r.current&&r.current.lockAvatarPosition)try{r.current.lockAvatarPosition()}catch(S){console.warn("Error locking avatar position:",S)}},unlockAvatarPosition:()=>{if(r.current&&r.current.unlockAvatarPosition)try{r.current.unlockAvatarPosition()}catch(S){console.warn("Error unlocking avatar position:",S)}}})),re.jsxs("div",{className:`talking-head-container ${i}`,style:s,children:[re.jsx("div",{ref:h,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),u&&re.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),c&&re.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:c})]})});Ne.displayName="TalkingHeadComponent";const Ue=R.forwardRef(({text:B=null,avatarUrl:t="/avatars/brunette.glb",avatarBody:e="F",mood:n="neutral",ttsLang:i="en",ttsService:s=null,ttsVoice:o=null,ttsApiKey:l=null,bodyMovement:h="idle",movementIntensity:r=.5,showFullAvatar:u=!1,cameraView:a="upper",onReady:c=()=>{},onLoading:d=()=>{},onError:g=()=>{},onSpeechEnd:y=()=>{},className:b="",style:L={},animations:V={},autoSpeak:p=!1},M)=>{const C=R.useRef(null),f=R.useRef(null),E=R.useRef(u),P=R.useRef(null),U=R.useRef(null),ie=R.useRef(!1),S=R.useRef({remainingText:null,originalText:null,options:null}),G=R.useRef([]),[q,Z]=R.useState(!0),[J,oe]=R.useState(null),[se,ce]=R.useState(!1),[$,le]=R.useState(!1);R.useEffect(()=>{ie.current=$},[$]),R.useEffect(()=>{E.current=u},[u]);const D=Ae(),v=s||D.service;let I;v==="browser"?I={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:v==="elevenlabs"?I={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:l||D.apiKey,defaultVoice:o||D.defaultVoice||be.defaultVoice,voices:D.voices||be.voices}:v==="deepgram"?I={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:l||D.apiKey,defaultVoice:o||D.defaultVoice||ze.defaultVoice,voices:D.voices||ze.voices}:I={...D,apiKey:l!==null?l:D.apiKey};const z={url:t,body:e,avatarMood:n,ttsLang:v==="browser"?"en-US":i,ttsVoice:o||I.defaultVoice,lipsyncLang:"en",showFullAvatar:u,bodyMovement:h,movementIntensity:r},T={ttsEndpoint:I.endpoint,ttsApikey:I.apiKey,ttsService:v,lipsyncModules:["en"],cameraView:a},N=R.useCallback(async()=>{if(!(!C.current||f.current))try{Z(!0),oe(null),f.current=new Te(C.current,T),await f.current.showAvatar(z,ee=>{if(ee.lengthComputable){const A=Math.min(100,Math.round(ee.loaded/ee.total*100));d(A)}}),Z(!1),ce(!0),c(f.current);const H=()=>{document.visibilityState==="visible"?f.current?.start():f.current?.stop()};return document.addEventListener("visibilitychange",H),()=>{document.removeEventListener("visibilitychange",H)}}catch(H){console.error("Error initializing TalkingHead:",H),oe(H.message||"Failed to initialize avatar"),Z(!1),g(H)}},[]);R.useEffect(()=>(N(),()=>{f.current&&(f.current.stop(),f.current.dispose(),f.current=null)}),[N]);const _=R.useCallback(async()=>{if(f.current)try{const H=f.current.audioCtx||f.current.audioContext;H&&(H.state==="suspended"||H.state==="interrupted")&&(await H.resume(),console.log("Audio context resumed"))}catch(H){console.warn("Failed to resume audio context:",H)}},[]);R.useEffect(()=>{se&&B&&p&&f.current&&j(B)},[se,B,p,j]);const j=R.useCallback(async(H,ee={})=>{if(!f.current||!se){console.warn("Avatar not ready for speaking");return}if(!H||H.trim()===""){console.warn("No text provided to speak");return}await _(),S.current={remainingText:null,originalText:null,options:null},G.current=[],P.current={text:H,options:ee},U.current&&(clearInterval(U.current),U.current=null),le(!1),ie.current=!1;const A=H.split(/[.!?]+/).filter(O=>O.trim().length>0);G.current=A;const F={lipsyncLang:ee.lipsyncLang||"en",onSpeechEnd:()=>{U.current&&(clearInterval(U.current),U.current=null),ee.onSpeechEnd&&ee.onSpeechEnd(),y()}};try{f.current.speakText(H,F)}catch(O){console.error("Error speaking text:",O),oe(O.message||"Failed to speak text")}},[se,y,_]),Q=R.useCallback(()=>{if(f.current)try{const H=f.current.isSpeaking||!1,ee=f.current.audioPlaylist||[],A=f.current.speechQueue||[];if(H||ee.length>0||A.length>0){U.current&&(clearInterval(U.current),U.current=null);let F="";A.length>0&&(F=A.map(O=>O.text&&Array.isArray(O.text)?O.text.map(K=>K.word).join(" "):O.text||"").join(" ")),S.current={remainingText:F||null,originalText:P.current?.text||null,options:P.current?.options||null},f.current.speechQueue.length=0,f.current.pauseSpeaking(),le(!0),ie.current=!0}}catch(H){console.warn("Error pausing speech:",H)}},[]),ve=R.useCallback(async()=>{if(!(!f.current||!$))try{await _(),le(!1),ie.current=!1;const H=S.current?.remainingText,ee=S.current?.originalText||P.current?.text,A=S.current?.options||P.current?.options||{},F=H||ee;F&&j(F,A)}catch(H){console.warn("Error resuming speech:",H),le(!1),ie.current=!1}},[$,j,_]),ke=R.useCallback(()=>{f.current&&(f.current.stopSpeaking(),U.current&&(clearInterval(U.current),U.current=null),le(!1),ie.current=!1)},[]);return R.useImperativeHandle(M,()=>({speakText:j,pauseSpeaking:Q,resumeSpeaking:ve,stopSpeaking:ke,resumeAudioContext:_,isPaused:()=>$,setMood:H=>f.current?.setMood(H),setBodyMovement:H=>{f.current&&f.current.setBodyMovement(H)},playAnimation:(H,ee=!1)=>{f.current&&f.current.playAnimation&&f.current.playAnimation(H,null,10,0,.01,ee)},playReaction:H=>f.current?.playReaction(H),playCelebration:()=>f.current?.playCelebration(),setShowFullAvatar:H=>{f.current&&(E.current=H,f.current.setShowFullAvatar(H))},isReady:se,talkingHead:f.current})),re.jsxs("div",{className:`simple-talking-avatar-container ${b}`,style:L,children:[re.jsx("div",{ref:C,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),q&&re.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),J&&re.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:J})]})});Ue.displayName="SimpleTalkingAvatar";const We=R.forwardRef(({curriculumData:B=null,avatarConfig:t={},animations:e={},onLessonStart:n=()=>{},onLessonComplete:i=()=>{},onQuestionAnswer:s=()=>{},onCurriculumComplete:o=()=>{},onCustomAction:l=()=>{},autoStart:h=!1},r)=>{const u=R.useRef(null),a=R.useRef({currentModuleIndex:0,currentLessonIndex:0,currentQuestionIndex:0,isTeaching:!1,isQuestionMode:!1,lessonCompleted:!1,curriculumCompleted:!1,score:0,totalQuestions:0}),c=R.useRef({onLessonStart:n,onLessonComplete:i,onQuestionAnswer:s,onCurriculumComplete:o,onCustomAction:l}),d=R.useRef(null),g=R.useRef(null),y=R.useRef(null),b=R.useRef(null),L=R.useRef(null),V=R.useRef(null),p=R.useRef(null),M=R.useRef(B?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]}),C=R.useRef({avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"});R.useEffect(()=>{c.current={onLessonStart:n,onLessonComplete:i,onQuestionAnswer:s,onCurriculumComplete:o,onCustomAction:l}},[n,i,s,o,l]),R.useEffect(()=>{M.current=B?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]},C.current={avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"}},[B,t,e]);const f=R.useCallback(()=>(M.current||{modules:[]}).modules[a.current.currentModuleIndex]?.lessons[a.current.currentLessonIndex],[]),E=R.useCallback(()=>f()?.questions[a.current.currentQuestionIndex],[f]),P=R.useCallback((v,I)=>I.type==="multiple_choice"||I.type==="true_false"?v===I.answer:I.type==="code_test"&&typeof v=="object"&&v!==null?v.passed===!0:!1,[]),U=R.useCallback(()=>{a.current.lessonCompleted=!0,a.current.isQuestionMode=!1;const v=a.current.totalQuestions>0?Math.round(a.current.score/a.current.totalQuestions*100):100;let I="Congratulations! You've completed this lesson";if(a.current.totalQuestions>0?I+=` You got ${a.current.score} correct out of ${a.current.totalQuestions} question${a.current.totalQuestions===1?"":"s"}, achieving a score of ${v} percent. `:I+="! ",v>=80?I+="Excellent work! You have a great understanding of this topic.":v>=60?I+="Good job! You understand most of the concepts.":I+="Keep practicing! You're making progress.",c.current.onLessonComplete({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,score:a.current.score,totalQuestions:a.current.totalQuestions,percentage:v}),c.current.onCustomAction({type:"lessonComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,score:a.current.score,totalQuestions:a.current.totalQuestions,percentage:v}),u.current){if(u.current.setMood("happy"),e.lessonComplete)try{u.current.playAnimation(e.lessonComplete,!0)}catch{u.current.playCelebration()}const z=M.current||{modules:[]},T=z.modules[a.current.currentModuleIndex],N=a.current.currentLessonIndex<(T?.lessons?.length||0)-1,_=a.current.currentModuleIndex<(z.modules?.length||0)-1,j=N||_,Q=C.current||{lipsyncLang:"en"};u.current.speakText(I,{lipsyncLang:Q.lipsyncLang,onSpeechEnd:()=>{c.current.onCustomAction({type:"lessonCompleteFeedbackDone",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,score:a.current.score,totalQuestions:a.current.totalQuestions,percentage:v,hasNextLesson:j})}})}},[e.lessonComplete]),ie=R.useCallback(()=>{a.current.curriculumCompleted=!0;const v=M.current||{modules:[]};if(c.current.onCurriculumComplete({modules:v.modules.length,totalLessons:v.modules.reduce((I,z)=>I+z.lessons.length,0)}),u.current){if(u.current.setMood("celebrating"),e.curriculumComplete)try{u.current.playAnimation(e.curriculumComplete,!0)}catch{u.current.playCelebration()}const I=C.current||{lipsyncLang:"en"};u.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!",{lipsyncLang:I.lipsyncLang})}},[e.curriculumComplete]),S=R.useCallback(()=>{const v=f();a.current.isQuestionMode=!0,a.current.currentQuestionIndex=0,a.current.totalQuestions=v?.questions?.length||0,a.current.score=0;const I=E();I&&c.current.onCustomAction({type:"questionStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,totalQuestions:a.current.totalQuestions,question:I,score:a.current.score});const z=()=>{if(!u.current||!I)return;if(u.current.setMood("happy"),e.questionStart)try{u.current.playAnimation(e.questionStart,!0)}catch(N){console.warn("Failed to play questionStart animation:",N)}const T=C.current||{lipsyncLang:"en"};I.type==="code_test"?u.current.speakText(`Let's test your coding skills! Here's your first challenge: ${I.question}`,{lipsyncLang:T.lipsyncLang}):I.type==="multiple_choice"?u.current.speakText(`Now let me ask you some questions. Here's the first one: ${I.question}`,{lipsyncLang:T.lipsyncLang}):I.type==="true_false"?u.current.speakText(`Let's start with some true or false questions. First question: ${I.question}`,{lipsyncLang:T.lipsyncLang}):u.current.speakText(`Now let me ask you some questions. Here's the first one: ${I.question}`,{lipsyncLang:T.lipsyncLang})};if(u.current&&u.current.isReady&&I)z();else if(u.current&&u.current.isReady){const T=C.current||{lipsyncLang:"en"};u.current.speakText("Now let me ask you some questions to test your understanding.",{lipsyncLang:T.lipsyncLang})}else{const T=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(T),I&&z())},100);setTimeout(()=>{clearInterval(T)},5e3)}},[e.questionStart,f,E]),G=R.useCallback(()=>{const v=f();if(a.current.currentQuestionIndex<(v?.questions?.length||0)-1){u.current&&u.current.stopSpeaking&&u.current.stopSpeaking(),a.current.currentQuestionIndex+=1;const I=E();I&&c.current.onCustomAction({type:"nextQuestion",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,totalQuestions:a.current.totalQuestions,question:I,score:a.current.score});const z=()=>{if(!u.current||!I)return;if(u.current.setMood("happy"),u.current.setBodyMovement("idle"),e.nextQuestion)try{u.current.playAnimation(e.nextQuestion,!0)}catch(Q){console.warn("Failed to play nextQuestion animation:",Q)}const T=C.current||{lipsyncLang:"en"},_=f()?.questions?.length||0,j=a.current.currentQuestionIndex>=_-1;if(I.type==="code_test"){const Q=j?`Great! Here's your final coding challenge: ${I.question}`:`Great! Now let's move on to your next coding challenge: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}else if(I.type==="multiple_choice"){const Q=j?`Alright! Here's your final question: ${I.question}`:`Alright! Here's your next question: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}else if(I.type==="true_false"){const Q=j?`Now let's try this final one: ${I.question}`:`Now let's try this one: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}else{const Q=j?`Here's your final question: ${I.question}`:`Here's the next question: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}};if(u.current&&u.current.isReady&&I)z();else if(I){const T=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(T),z())},100);setTimeout(()=>{clearInterval(T)},5e3)}}else c.current.onCustomAction({type:"allQuestionsComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,totalQuestions:a.current.totalQuestions,score:a.current.score})},[e.nextQuestion,f,E]),q=R.useCallback(()=>{const v=M.current||{modules:[]},I=v.modules[a.current.currentModuleIndex];if(a.current.currentLessonIndex<(I?.lessons?.length||0)-1){a.current.currentLessonIndex+=1,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0;const T=v.modules[a.current.currentModuleIndex],N=a.current.currentLessonIndex<(T?.lessons?.length||0)-1,_=a.current.currentModuleIndex<(v.modules?.length||0)-1,j=N||_;c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,hasNextLesson:j}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else if(a.current.currentModuleIndex<(v.modules?.length||0)-1){a.current.currentModuleIndex+=1,a.current.currentLessonIndex=0,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0;const N=v.modules[a.current.currentModuleIndex],_=a.current.currentLessonIndex<(N?.lessons?.length||0)-1,j=a.current.currentModuleIndex<(v.modules?.length||0)-1,Q=_||j;c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,hasNextLesson:Q}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else L.current&&L.current()},[]),Z=R.useCallback(()=>{const v=f();let I=null;if(v?.avatar_script&&v?.body){const z=v.avatar_script.trim(),T=v.body.trim(),N=z.match(/[.!?]$/)?" ":". ";I=`${z}${N}${T}`}else I=v?.avatar_script||v?.body||null;if(u.current&&u.current.isReady&&I){a.current.isTeaching=!0,a.current.isQuestionMode=!1,a.current.score=0,a.current.totalQuestions=0,u.current.setMood("happy");let z=!1;if(e.teaching)try{u.current.playAnimation(e.teaching,!0),z=!0}catch(N){console.warn("Failed to play teaching animation:",N)}z||u.current.setBodyMovement("gesturing");const T=C.current||{lipsyncLang:"en"};c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v}),c.current.onCustomAction({type:"teachingStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v}),u.current.speakText(I,{lipsyncLang:T.lipsyncLang,onSpeechEnd:()=>{a.current.isTeaching=!1,c.current.onCustomAction({type:"teachingComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v,hasQuestions:v.questions&&v.questions.length>0}),v?.code_example&&c.current.onCustomAction({type:"codeExampleReady",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v,codeExample:v.code_example})}})}},[e.teaching,f]),J=R.useCallback(v=>{const I=E(),z=P(v,I);if(z&&(a.current.score+=1),c.current.onQuestionAnswer({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,answer:v,isCorrect:z,question:I}),u.current)if(z){if(u.current.setMood("happy"),e.correct)try{u.current.playReaction("happy")}catch{u.current.setBodyMovement("happy")}u.current.setBodyMovement("gesturing");const N=f()?.questions?.length||0;a.current.currentQuestionIndex>=N-1;const _=a.current.currentQuestionIndex<N-1;console.log("[CurriculumLearning] Answer feedback - questionIndex:",a.current.currentQuestionIndex,"totalQuestions:",N,"hasNextQuestion:",_);const j=I.type==="code_test"?`Great job! Your code passed all the tests! ${I.explanation||""}`:`Excellent! That's correct! ${I.explanation||""}`,Q=C.current||{lipsyncLang:"en"};u.current.speakText(j,{lipsyncLang:Q.lipsyncLang,onSpeechEnd:()=>{c.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,isCorrect:!0,hasNextQuestion:_,score:a.current.score,totalQuestions:a.current.totalQuestions})}})}else{if(u.current.setMood("sad"),e.incorrect)try{u.current.playAnimation(e.incorrect,!0)}catch{u.current.setBodyMovement("idle")}u.current.setBodyMovement("gesturing");const N=f()?.questions?.length||0,_=a.current.currentQuestionIndex>=N-1,j=a.current.currentQuestionIndex<N-1;console.log("[CurriculumLearning] Answer feedback (incorrect) - questionIndex:",a.current.currentQuestionIndex,"totalQuestions:",N,"hasNextQuestion:",j);const Q=I.type==="code_test"?`Your code didn't pass all the tests. ${I.explanation||"Try again!"}`:`Not quite right, but don't worry! ${I.explanation||""}${_?"":" Let's move on to the next question."}`,ve=C.current||{lipsyncLang:"en"};u.current.speakText(Q,{lipsyncLang:ve.lipsyncLang,onSpeechEnd:()=>{c.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,isCorrect:!1,hasNextQuestion:j,score:a.current.score,totalQuestions:a.current.totalQuestions})}})}else{const N=f()?.questions?.length||0;c.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,isCorrect:z,hasNextQuestion:a.current.currentQuestionIndex<N-1,score:a.current.score,totalQuestions:a.current.totalQuestions,avatarNotReady:!0})}},[e.correct,e.incorrect,E,f,P]),oe=R.useCallback(v=>{const I=E();if(!v||typeof v!="object"){console.error("Invalid code test result format. Expected object with {passed: boolean, ...}");return}if(I?.type!=="code_test"){console.warn("Current question is not a code test. Use handleAnswerSelect for other question types.");return}const z={passed:v.passed===!0,results:v.results||[],output:v.output||"",error:v.error||null,executionTime:v.executionTime||null,testCount:v.testCount||0,passedCount:v.passedCount||0,failedCount:v.failedCount||0};c.current.onCustomAction({type:"codeTestSubmitted",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,testResult:z,question:I}),p.current&&p.current(z)},[E,P]),se=R.useCallback(()=>{if(a.current.currentQuestionIndex>0){a.current.currentQuestionIndex-=1;const v=E();v&&c.current.onCustomAction({type:"questionStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,totalQuestions:a.current.totalQuestions,question:v,score:a.current.score});const I=()=>{if(!u.current||!v)return;u.current.setMood("happy"),u.current.setBodyMovement("idle");const z=C.current||{lipsyncLang:"en"};v.type==="code_test"?u.current.speakText(`Let's go back to this coding challenge: ${v.question}`,{lipsyncLang:z.lipsyncLang}):u.current.speakText(`Going back to: ${v.question}`,{lipsyncLang:z.lipsyncLang})};if(u.current&&u.current.isReady&&v)I();else if(v){const z=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(z),I())},100);setTimeout(()=>{clearInterval(z)},5e3)}}},[E]),ce=R.useCallback(()=>{const v=M.current||{modules:[]};if(v.modules[a.current.currentModuleIndex],a.current.currentLessonIndex>0)a.current.currentLessonIndex-=1,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0,c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"));else if(a.current.currentModuleIndex>0){const T=v.modules[a.current.currentModuleIndex-1];a.current.currentModuleIndex-=1,a.current.currentLessonIndex=(T?.lessons?.length||1)-1,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0,c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}},[f]),$=R.useCallback(()=>{a.current.currentModuleIndex=0,a.current.currentLessonIndex=0,a.current.currentQuestionIndex=0,a.current.isTeaching=!1,a.current.isQuestionMode=!1,a.current.lessonCompleted=!1,a.current.curriculumCompleted=!1,a.current.score=0,a.current.totalQuestions=0},[]),le=R.useCallback(v=>{console.log("Avatar is ready!",v);const I=f(),z=I?.avatar_script||I?.body;h&&z&&setTimeout(()=>{d.current&&d.current()},10)},[h,f]);R.useLayoutEffect(()=>{d.current=Z,g.current=q,y.current=U,b.current=G,L.current=ie,V.current=S,p.current=J}),R.useImperativeHandle(r,()=>({startTeaching:Z,startQuestions:S,handleAnswerSelect:J,handleCodeTestResult:oe,nextQuestion:G,previousQuestion:se,nextLesson:q,previousLesson:ce,completeLesson:U,completeCurriculum:ie,resetCurriculum:$,getState:()=>({...a.current}),getCurrentQuestion:()=>E(),getCurrentLesson:()=>f(),getAvatarRef:()=>u.current,speakText:async(v,I={})=>{await u.current?.resumeAudioContext?.();const z=C.current||{lipsyncLang:"en"};u.current?.speakText(v,{...I,lipsyncLang:I.lipsyncLang||z.lipsyncLang})},resumeAudioContext:async()=>{if(u.current?.resumeAudioContext)return await u.current.resumeAudioContext();const v=u.current?.talkingHead;if(v?.audioCtx){const I=v.audioCtx;if(I.state==="suspended"||I.state==="interrupted")try{await I.resume(),console.log("Audio context resumed via talkingHead")}catch(z){console.warn("Failed to resume audio context:",z)}}else console.warn("Audio context not available yet")},stopSpeaking:()=>u.current?.stopSpeaking(),pauseSpeaking:()=>u.current?.pauseSpeaking(),resumeSpeaking:async()=>await u.current?.resumeSpeaking(),isPaused:()=>u.current&&typeof u.current.isPaused<"u"?u.current.isPaused:!1,setMood:v=>u.current?.setMood(v),playAnimation:(v,I)=>u.current?.playAnimation(v,I),setBodyMovement:v=>u.current?.setBodyMovement(v),setMovementIntensity:v=>u.current?.setMovementIntensity(v),playRandomDance:()=>u.current?.playRandomDance(),playReaction:v=>u.current?.playReaction(v),playCelebration:()=>u.current?.playCelebration(),setShowFullAvatar:v=>u.current?.setShowFullAvatar(v),setTimingAdjustment:v=>u.current?.setTimingAdjustment(v),lockAvatarPosition:()=>u.current?.lockAvatarPosition(),unlockAvatarPosition:()=>u.current?.unlockAvatarPosition(),triggerCustomAction:(v,I)=>{c.current.onCustomAction({type:v,...I,state:{...a.current}})},handleResize:()=>u.current?.handleResize(),isAvatarReady:()=>u.current?.isReady||!1}),[Z,S,J,oe,G,q,U,ie,$,E,f]);const D=C.current||{avatarUrl:"/avatars/brunette.glb",avatarBody:"F",mood:"happy",ttsLang:"en",ttsService:null,ttsVoice:null,ttsApiKey:null,bodyMovement:"gesturing",movementIntensity:.7,showFullAvatar:!1,animations:e};return re.jsx("div",{style:{width:"100%",height:"100%"},children:re.jsx(Me,{ref:u,avatarUrl:D.avatarUrl,avatarBody:D.avatarBody,mood:D.mood,ttsLang:D.ttsLang,ttsService:D.ttsService,ttsVoice:D.ttsVoice,ttsApiKey:D.ttsApiKey,bodyMovement:D.bodyMovement,movementIntensity:D.movementIntensity,showFullAvatar:D.showFullAvatar,cameraView:"upper",animations:D.animations,onReady:le,onLoading:()=>{},onError:v=>{console.error("Avatar error:",v)}})})});We.displayName="CurriculumLearning";const Ee={dance:{name:"dance",type:"code-based",variations:["dancing","dancing2","dancing3"],loop:!0,duration:1e4,description:"Celebration dance animation with multiple variations"},happy:{name:"happy",type:"code-based",loop:!0,duration:5e3,description:"Happy, upbeat body movement"},surprised:{name:"surprised",type:"code-based",loop:!1,duration:3e3,description:"Surprised reaction with quick movements"},thinking:{name:"thinking",type:"code-based",loop:!0,duration:8e3,description:"Thoughtful, contemplative movement"},nodding:{name:"nodding",type:"code-based",loop:!1,duration:2e3,description:"Nodding agreement gesture"},shaking:{name:"shaking",type:"code-based",loop:!1,duration:2e3,description:"Shaking head disagreement gesture"},celebration:{name:"celebration",type:"code-based",loop:!1,duration:3e3,description:"Celebration animation for achievements"},energetic:{name:"energetic",type:"code-based",loop:!0,duration:6e3,description:"High-energy, enthusiastic movement"},swaying:{name:"swaying",type:"code-based",loop:!0,duration:8e3,description:"Gentle swaying motion"},bouncing:{name:"bouncing",type:"code-based",loop:!0,duration:4e3,description:"Bouncy, playful movement"},gesturing:{name:"gesturing",type:"code-based",loop:!0,duration:5e3,description:"Teaching gesture animation"},walking:{name:"walking",type:"code-based",loop:!0,duration:6e3,description:"Walking motion"},prancing:{name:"prancing",type:"code-based",loop:!0,duration:4e3,description:"Playful prancing movement"},excited:{name:"excited",type:"code-based",loop:!0,duration:5e3,description:"Excited, energetic movement"}},pt=B=>Ee[B]||null,gt=B=>Ee.hasOwnProperty(B);exports.CurriculumLearning=We;exports.SimpleTalkingAvatar=Ue;exports.TalkingHeadAvatar=Me;exports.TalkingHeadComponent=Ne;exports.animations=Ee;exports.getActiveTTSConfig=Ae;exports.getAnimation=pt;exports.getVoiceOptions=mt;exports.hasAnimation=gt;
7
+ `,s=await fetch(this.opt.ttsEndpoint,{method:"POST",headers:{"Ocp-Apim-Subscription-Key":this.opt.ttsApikey,"Content-Type":"application/ssml+xml","X-Microsoft-OutputFormat":"audio-16khz-128kbitrate-mono-mp3"},body:i});if(!s.ok)throw new Error(`Azure TTS error: ${s.status} ${s.statusText}`);const o=await s.arrayBuffer(),l=await this.audioCtx.decodeAudioData(o);console.log("Analyzing audio for precise lip-sync...");const h=await this.audioAnalyzer.analyzeAudio(l,e);console.log("Azure TTS Audio Analysis:",{text:e,audioDuration:l.duration,visemeCount:h.visemes.length,wordCount:h.words.length,features:{onsets:h.features.onsets.length,boundaries:h.features.phonemeBoundaries.length}});const r=[];for(let a=0;a<h.visemes.length;a++){const c=h.visemes[a],d=c.startTime*1e3,g=c.duration*1e3,y=c.intensity;r.push({template:{name:"viseme"},ts:[d-Math.min(60,2*g/3),d+Math.min(25,g/2),d+g+Math.min(60,g/2)],vs:{["viseme_"+c.viseme]:[null,y,0]}})}const u=[...t.anim,...r];this.audioPlaylist.push({anim:u,audio:l}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}async synthesizeWithExternalTTS(t){let e="<speak>";t.text.forEach((o,l)=>{l>0&&(e+=" <mark name='"+o.mark+"'/>"),e+=o.word.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;").replace(new RegExp("^\\p{Dash_Punctuation}$","ug"),'<break time="750ms"/>')}),e+="</speak>";const n={method:"POST",headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify({input:{ssml:e},voice:{languageCode:t.lang||this.avatar.ttsLang||this.opt.ttsLang,name:t.voice||this.avatar.ttsVoice||this.opt.ttsVoice},audioConfig:{audioEncoding:this.ttsAudioEncoding,speakingRate:(t.rate||this.avatar.ttsRate||this.opt.ttsRate)+this.mood.speech.deltaRate,pitch:(t.pitch||this.avatar.ttsPitch||this.opt.ttsPitch)+this.mood.speech.deltaPitch,volumeGainDb:(t.volume||this.avatar.ttsVolume||this.opt.ttsVolume)+this.mood.speech.deltaVolume},enableTimePointing:[1]})};this.opt.jwtGet&&typeof this.opt.jwtGet=="function"&&(n.headers.Authorization="Bearer "+await this.opt.jwtGet());const i=await fetch(this.opt.ttsEndpoint+(this.opt.ttsApikey?"?key="+this.opt.ttsApikey:""),n),s=await i.json();if(i.status===200&&s&&s.audioContent){const o=this.b64ToArrayBuffer(s.audioContent),l=await this.audioCtx.decodeAudioData(o);this.speakWithHands();const h=[0];let r=0;t.text.forEach((c,d)=>{if(d>0){let g=h[h.length-1];s.timepoints[r]&&(g=s.timepoints[r].timeSeconds*1e3,s.timepoints[r].markName===""+c.mark&&r++),h.push(g)}});const u=[{mark:0,time:0}];h.forEach((c,d)=>{if(d>0){let g=c-h[d-1];u[d-1].duration=g,u.push({mark:d,time:c})}});let a=1e3*l.duration;a>this.opt.ttsTrimEnd&&(a=a-this.opt.ttsTrimEnd),u[u.length-1].duration=a-u[u.length-1].time,t.anim.forEach(c=>{const d=u[c.mark];if(d)for(let g=0;g<c.ts.length;g++)c.ts[g]=d.time+c.ts[g]*d.duration+this.opt.ttsTrimStart}),this.audioPlaylist.push({anim:t.anim,audio:l}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}else this.startSpeaking(!0)}async startSpeaking(t=!1){if(!(!this.armature||this.isSpeaking&&!t))if(this.stateName="speaking",this.isSpeaking=!0,this.speechQueue.length){let e=this.speechQueue.shift();if(e.emoji){this.lookAtCamera(500);let n=e.emoji.dt.reduce((i,s)=>i+s,0);this.animQueue.push(this.animFactory(e.emoji)),setTimeout(this.startSpeaking.bind(this),n,!0)}else if(e.break)setTimeout(this.startSpeaking.bind(this),e.break,!0);else if(e.audio)e.isRaw||(this.lookAtCamera(500),this.speakWithHands(),this.resetLips()),this.audioPlaylist.push({anim:e.anim,audio:e.audio,isRaw:e.isRaw}),this.onSubtitles=e.onSubtitles||null,e.mood&&this.setMood(e.mood),this.playAudio();else if(e.text){this.lookAtCamera(500);try{!this.opt.ttsEndpoint||this.opt.ttsEndpoint===""?await this.synthesizeWithBrowserTTS(e):this.opt.ttsService==="elevenlabs"?await this.synthesizeWithElevenLabsTTS(e):this.opt.ttsService==="deepgram"?await this.synthesizeWithDeepgramTTS(e):this.opt.ttsService==="azure"?await this.synthesizeWithAzureTTS(e):await this.synthesizeWithExternalTTS(e)}catch(n){console.error("Error:",n),this.startSpeaking(!0)}}else e.anim?(this.onSubtitles=e.onSubtitles||null,this.resetLips(),e.mood&&this.setMood(e.mood),e.anim.forEach((n,i)=>{for(let s=0;s<n.ts.length;s++)n.ts[s]=this.animClock+10*i;this.animQueue.push(n)}),setTimeout(this.startSpeaking.bind(this),10*e.anim.length,!0)):e.marker?(typeof e.marker=="function"&&e.marker(),this.startSpeaking(!0)):this.startSpeaking(!0)}else this.stateName="idle",this.isSpeaking=!1}pauseSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render())}stopSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.speechQueue.length=0,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.armature&&(this.resetLips(),this.render())}async streamStart(t={},e=null,n=null,i=null,s=null){if(this.stopSpeaking(),this.isStreaming=!0,t.waitForAudioChunks!==void 0&&(this.streamWaitForAudioChunks=t.waitForAudioChunks),this.streamWaitForAudioChunks||(this.streamAudioStartTime=this.animClock),this.streamLipsyncQueue=[],this.streamLipsyncType=t.lipsyncType||this.streamLipsyncType||"visemes",this.streamLipsyncLang=t.lipsyncLang||this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,this.onAudioStart=e,this.onAudioEnd=n,this.onMetrics=s,t.sampleRate!==void 0){const l=t.sampleRate;typeof l=="number"&&l>=8e3&&l<=96e3?l!==this.audioCtx.sampleRate&&this.initAudioGraph(l):console.warn("Invalid sampleRate provided. It must be a number between 8000 and 96000 Hz.")}if(t.gain!==void 0&&(this.audioStreamGainNode.gain.value=t.gain),!this.streamWorkletNode||!this.streamWorkletNode.port||this.streamWorkletNode.numberOfOutputs===0||this.streamWorkletNode.context!==this.audioCtx){if(this.streamWorkletNode)try{this.streamWorkletNode.disconnect(),this.streamWorkletNode=null}catch{}if(!this.workletLoaded)try{const l=this.audioCtx.audioWorklet.addModule(ct.href),h=new Promise((r,u)=>setTimeout(()=>u(new Error("Worklet loading timed out")),5e3));await Promise.race([l,h]),this.workletLoaded=!0}catch(l){throw console.error("Failed to load audio worklet:",l),new Error("Failed to initialize streaming speech")}this.streamWorkletNode=new AudioWorkletNode(this.audioCtx,"playback-worklet",{processorOptions:{sampleRate:this.audioCtx.sampleRate,metrics:t.metrics||{enabled:!1}}}),this.streamWorkletNode.connect(this.audioStreamGainNode),this.streamWorkletNode.connect(this.audioAnalyzerNode),this.streamWorkletNode.port.onmessage=l=>{if(l.data.type==="playback-started"&&(this.isSpeaking=!0,this.stateName="speaking",this.streamWaitForAudioChunks&&(this.streamAudioStartTime=this.animClock),this._processStreamLipsyncQueue(),this.speakWithHands(),this.onAudioStart))try{this.onAudioStart?.()}catch(h){console.error(h)}if(l.data.type==="playback-ended"&&(this._streamPause(),this.onAudioEnd))try{this.onAudioEnd()}catch{}if(this.onMetrics&&l.data.type==="metrics")try{this.onMetrics(l.data)}catch{}}}if(t.metrics)try{this.streamWorkletNode.port.postMessage({type:"config-metrics",data:t.metrics})}catch{}if(this.resetLips(),this.lookAtCamera(500),t.mood&&this.setMood(t.mood),this.onSubtitles=i||null,this.audioCtx.state==="suspended"||this.audioCtx.state==="interrupted"){const l=this.audioCtx.resume(),h=new Promise((r,u)=>setTimeout(()=>u("p2"),1e3));try{await Promise.race([l,h])}catch{console.warn("Can't play audio. Web Audio API suspended. This is often due to calling some speak method before the first user action, which is typically prevented by the browser.");return}}}streamNotifyEnd(){!this.isStreaming||!this.streamWorkletNode||this.streamWorkletNode.port.postMessage({type:"no-more-data"})}streamInterrupt(){if(!this.isStreaming)return;const t=this.isSpeaking;if(this.streamWorkletNode)try{this.streamWorkletNode.port.postMessage({type:"stop"})}catch{}if(this._streamPause(!0),t&&this.onAudioEnd)try{this.onAudioEnd()}catch{}}streamStop(){if(this.isStreaming){if(this.streamInterrupt(),this.streamWorkletNode){try{this.streamWorkletNode.disconnect()}catch{}this.streamWorkletNode=null}this.isStreaming=!1}}_streamPause(t=!1){this.isSpeaking=!1,this.stateName="idle",t&&(this.streamWaitForAudioChunks&&(this.streamAudioStartTime=null),this.streamLipsyncQueue=[],this.animQueue=this.animQueue.filter(e=>e.template.name!=="viseme"&&e.template.name!=="subtitles"&&e.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render()))}_processStreamLipsyncQueue(){if(this.isStreaming)for(;this.streamLipsyncQueue.length>0;){const t=this.streamLipsyncQueue.shift();this._processLipsyncData(t,this.streamAudioStartTime)}}_processLipsyncData(t,e){if(this.isStreaming){if(t.visemes&&this.streamLipsyncType=="visemes")for(let n=0;n<t.visemes.length;n++){const i=t.visemes[n],s=e+t.vtimes[n],o=t.vdurations[n],l={template:{name:"viseme"},ts:[s-2*o/3,s+o/2,s+o+o/2],vs:{["viseme_"+i]:[null,i==="PP"||i==="FF"?.9:.6,0]}};this.animQueue.push(l)}if(t.words&&(this.onSubtitles||this.streamLipsyncType=="words"))for(let n=0;n<t.words.length;n++){const i=t.words[n],s=t.wtimes[n];let o=t.wdurations[n];if(i.length&&(this.onSubtitles&&this.animQueue.push({template:{name:"subtitles"},ts:[e+s],vs:{subtitles:[" "+i]}}),this.streamLipsyncType=="words")){const l=this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,h=this.lipsyncPreProcessText(i,l),r=this.lipsyncWordsToVisemes(h,l);if(r&&r.visemes&&r.visemes.length){const u=r.times[r.visemes.length-1]+r.durations[r.visemes.length-1],a=Math.min(o,Math.max(0,o-r.visemes.length*150));let c=.6+this.convertRange(a,[0,o],[0,.4]);if(o=Math.min(o,r.visemes.length*200),u>0)for(let d=0;d<r.visemes.length;d++){const g=e+s+r.times[d]/u*o,y=r.durations[d]/u*o;this.animQueue.push({template:{name:"viseme"},ts:[g-Math.min(60,2*y/3),g+Math.min(25,y/2),g+y+Math.min(60,y/2)],vs:{["viseme_"+r.visemes[d]]:[null,r.visemes[d]==="PP"||r.visemes[d]==="FF"?.9:c,0]}})}}}}if(t.anims&&this.streamLipsyncType=="blendshapes")for(let n=0;n<t.anims.length;n++){let i=t.anims[n];i.delay+=e;let s=this.animFactory(i,!1,1,1,!0);this.animQueue.push(s)}}}streamAudio(t){if(!(!this.isStreaming||!this.streamWorkletNode)){if(this.isSpeaking||(this.streamLipsyncQueue=[],this.streamAudioStartTime=null),this.isSpeaking=!0,this.stateName="speaking",t.audio!==void 0){const e={type:"audioData",data:null};if(t.audio instanceof ArrayBuffer)e.data=t.audio,this.streamWorkletNode.port.postMessage(e,[e.data]);else if(t.audio instanceof Int16Array||t.audio instanceof Uint8Array){const n=t.audio.buffer.slice(t.audio.byteOffset,t.audio.byteOffset+t.audio.byteLength);e.data=n,this.streamWorkletNode.port.postMessage(e,[e.data])}else if(t.audio instanceof Float32Array){const n=new Int16Array(t.audio.length);for(let i=0;i<t.audio.length;i++){let s=Math.max(-1,Math.min(1,t.audio[i]));n[i]=s<0?s*32768:s*32767}e.data=n.buffer,this.streamWorkletNode.port.postMessage(e,[e.data])}else console.error("r.audio is not a supported type. Must be ArrayBuffer, Int16Array, Uint8Array, or Float32Array:",t.audio)}if(t.visemes||t.anims||t.words){if(this.streamWaitForAudioChunks&&!this.streamAudioStartTime){this.streamLipsyncQueue.length>=200&&this.streamLipsyncQueue.shift(),this.streamLipsyncQueue.push(t);return}else!this.streamWaitForAudioChunks&&!this.streamAudioStartTime&&(this.streamAudioStartTime=this.animClock);this._processLipsyncData(t,this.streamAudioStartTime)}}}makeEyeContact(t){this.animQueue.push(this.animFactory({name:"eyecontact",dt:[0,t],vs:{eyeContact:[1]}}))}lookAhead(t){if(t){let e=(Math.random()-.5)/4,n=(Math.random()-.5)/4,i=this.animQueue.findIndex(o=>o.template.name==="lookat");i!==-1&&this.animQueue.splice(i,1);const s={name:"lookat",dt:[750,t],vs:{bodyRotateX:[e],bodyRotateY:[n],eyesRotateX:[-3*e+.1],eyesRotateY:[-5*n],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(s))}}lookAtCamera(t){let e;if(this.speakTo&&(e=new x.Vector3,this.speakTo.objectLeftEye?.isObject3D?(this.speakTo.armature.objectHead,this.speakTo.objectLeftEye.updateMatrixWorld(!0),this.speakTo.objectRightEye.updateMatrixWorld(!0),fe.setFromMatrixPosition(this.speakTo.objectLeftEye.matrixWorld),xe.setFromMatrixPosition(this.speakTo.objectRightEye.matrixWorld),e.addVectors(fe,xe).divideScalar(2)):this.speakTo.isObject3D?this.speakTo.getWorldPosition(e):this.speakTo.isVector3?e.set(this.speakTo):this.speakTo.x&&this.speakTo.y&&this.speakTo.z&&e.set(this.speakTo.x,this.speakTo.y,this.speakTo.z)),!e){if(this.avatar.hasOwnProperty("avatarIgnoreCamera")){if(this.avatar.avatarIgnoreCamera){this.lookAhead(t);return}}else if(this.opt.avatarIgnoreCamera){this.lookAhead(t);return}this.lookAt(null,null,t);return}this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0),fe.setFromMatrixPosition(this.objectLeftEye.matrixWorld),xe.setFromMatrixPosition(this.objectRightEye.matrixWorld),fe.add(xe).divideScalar(2),Y.copy(this.armature.quaternion),Y.multiply(this.poseTarget.props["Hips.quaternion"]),Y.multiply(this.poseTarget.props["Spine.quaternion"]),Y.multiply(this.poseTarget.props["Spine1.quaternion"]),Y.multiply(this.poseTarget.props["Spine2.quaternion"]),Y.multiply(this.poseTarget.props["Neck.quaternion"]),Y.multiply(this.poseTarget.props["Head.quaternion"]);const n=new x.Vector3().subVectors(e,fe).normalize(),i=Math.atan2(n.x,n.z),s=Math.asin(-n.y);W.set(s,i,0,"YXZ");const l=new x.Quaternion().setFromEuler(W),h=new x.Quaternion().copy(l).multiply(Y.clone().invert());W.setFromQuaternion(h,"YXZ");let r=W.x/(40/24)+.2,u=W.y/(9/4),a=Math.min(.6,Math.max(-.3,r)),c=Math.min(.8,Math.max(-.8,u)),d=(Math.random()-.5)/4,g=(Math.random()-.5)/4;if(t){let y=this.animQueue.findIndex(L=>L.template.name==="lookat");y!==-1&&this.animQueue.splice(y,1);const b={name:"lookat",dt:[750,t],vs:{bodyRotateX:[a+d],bodyRotateY:[c+g],eyesRotateX:[-3*d+.1],eyesRotateY:[-5*g],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(b))}}lookAt(t,e,n){if(!this.camera)return;const i=this.nodeAvatar.getBoundingClientRect();this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0);const s=new x.Vector3().setFromMatrixPosition(this.objectLeftEye.matrixWorld),o=new x.Vector3().setFromMatrixPosition(this.objectRightEye.matrixWorld),l=new x.Vector3().addVectors(s,o).divideScalar(2);l.project(this.camera);let h=(l.x+1)/2*i.width+i.left,r=-(l.y-1)/2*i.height+i.top;t===null&&(t=h),e===null&&(e=r),Y.copy(this.armature.quaternion),Y.multiply(this.poseTarget.props["Hips.quaternion"]),Y.multiply(this.poseTarget.props["Spine.quaternion"]),Y.multiply(this.poseTarget.props["Spine1.quaternion"]),Y.multiply(this.poseTarget.props["Spine2.quaternion"]),Y.multiply(this.poseTarget.props["Neck.quaternion"]),Y.multiply(this.poseTarget.props["Head.quaternion"]),W.setFromQuaternion(Y);let u=W.x/(40/24),a=W.y/(9/4),c=Math.min(.4,Math.max(-.4,this.camera.rotation.x)),d=Math.min(.4,Math.max(-.4,this.camera.rotation.y)),g=Math.max(window.innerWidth-h,h),y=Math.max(window.innerHeight-r,r),b=this.convertRange(e,[r-y,r+y],[-.3,.6])-u+c,L=this.convertRange(t,[h-g,h+g],[-.8,.8])-a+d;b=Math.min(.6,Math.max(-.3,b)),L=Math.min(.8,Math.max(-.8,L));let V=(Math.random()-.5)/4,p=(Math.random()-.5)/4;if(n){let M=this.animQueue.findIndex(f=>f.template.name==="lookat");M!==-1&&this.animQueue.splice(M,1);const C={name:"lookat",dt:[750,n],vs:{bodyRotateX:[b+V],bodyRotateY:[L+p],eyesRotateX:[-3*V+.1],eyesRotateY:[-5*p],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(C))}}touchAt(t,e){if(!this.camera)return;const n=this.nodeAvatar.getBoundingClientRect(),i=new x.Vector2((t-n.left)/n.width*2-1,-((e-n.top)/n.height)*2+1),s=new x.Raycaster;s.setFromCamera(i,this.camera);const o=s.intersectObject(this.armature);if(o.length>0){const l=o[0].point,h=new x.Vector3,r=new x.Vector3;this.objectLeftArm.getWorldPosition(h),this.objectRightArm.getWorldPosition(r);const u=h.distanceToSquared(l),a=r.distanceToSquared(l);u<a?(this.ikSolve({iterations:20,root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3,maxAngle:.2},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},l,!1,1e3),this.setValue("handFistLeft",0)):(this.ikSolve({iterations:20,root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5,maxAngle:.2},{link:"RightArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},l,!1,1e3),this.setValue("handFistRight",0))}else["LeftArm","LeftForeArm","LeftHand","RightArm","RightForeArm","RightHand"].forEach(l=>{let h=l+".quaternion";this.poseTarget.props[h].copy(this.getPoseTemplateProp(h)),this.poseTarget.props[h].t=this.animClock,this.poseTarget.props[h].d=1e3});return o.length>0}speakWithHands(t=0,e=.5){if(this.mixer||this.gesture||!this.poseTarget.template.standing||this.poseTarget.template.bend||Math.random()>e)return;this.ikSolve({root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-1,maxz:3}]},new x.Vector3(this.gaussianRandom(0,.5),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0),this.ikSolve({root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5},{link:"RightArm"}]},new x.Vector3(this.gaussianRandom(-.5,0),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0);const n=[],i=[];n.push(100+Math.round(Math.random()*500)),i.push({duration:1e3,props:{"LeftHand.quaternion":new x.Quaternion().setFromEuler(new x.Euler(0,-1-Math.random(),0)),"RightHand.quaternion":new x.Quaternion().setFromEuler(new x.Euler(0,1+Math.random(),0))}}),["LeftArm","LeftForeArm","RightArm","RightForeArm"].forEach(o=>{i[0].props[o+".quaternion"]=this.ikMesh.getObjectByName(o).quaternion.clone()}),n.push(1e3+Math.round(Math.random()*500)),i.push({duration:2e3,props:{}}),["LeftArm","LeftForeArm","RightArm","RightForeArm","LeftHand","RightHand"].forEach(o=>{i[1].props[o+".quaternion"]=null});const s=this.animFactory({name:"talkinghands",delay:t,dt:n,vs:{moveto:i}});this.animQueue.push(s)}getSlowdownRate(t){return this.animSlowdownRate}setSlowdownRate(t){this.animSlowdownRate=t,this.audioSpeechSource.playbackRate.value=1/this.animSlowdownRate,this.audioBackgroundSource.playbackRate.value=1/this.animSlowdownRate}getAutoRotateSpeed(t){return this.controls.autoRotateSpeed}setAutoRotateSpeed(t){this.controls.autoRotateSpeed=t,this.controls.autoRotate=t>0}start(){this.armature&&this.isRunning===!1&&(this.audioCtx.resume(),this.animTimeLast=performance.now(),this.isRunning=!0,this.isAvatarOnly||requestAnimationFrame(this.animate.bind(this)))}stop(){this.isRunning=!1,this.audioCtx.suspend()}startListening(t,e={},n=null){this.listeningAnalyzer=t,this.listeningAnalyzer.fftSize=256,this.listeningAnalyzer.smoothingTimeConstant=.1,this.listeningAnalyzer.minDecibels=-70,this.listeningAnalyzer.maxDecibels=-10,this.listeningOnchange=n&&typeof n=="function"?n:null,this.listeningSilenceThresholdLevel=e?.hasOwnProperty("listeningSilenceThresholdLevel")?e.listeningSilenceThresholdLevel:this.opt.listeningSilenceThresholdLevel,this.listeningSilenceThresholdMs=e?.hasOwnProperty("listeningSilenceThresholdMs")?e.listeningSilenceThresholdMs:this.opt.listeningSilenceThresholdMs,this.listeningSilenceDurationMax=e?.hasOwnProperty("listeningSilenceDurationMax")?e.listeningSilenceDurationMax:this.opt.listeningSilenceDurationMax,this.listeningActiveThresholdLevel=e?.hasOwnProperty("listeningActiveThresholdLevel")?e.listeningActiveThresholdLevel:this.opt.listeningActiveThresholdLevel,this.listeningActiveThresholdMs=e?.hasOwnProperty("listeningActiveThresholdMs")?e.listeningActiveThresholdMs:this.opt.listeningActiveThresholdMs,this.listeningActiveDurationMax=e?.hasOwnProperty("listeningActiveDurationMax")?e.listeningActiveDurationMax:this.opt.listeningActiveDurationMax,this.listeningActive=!1,this.listeningVolume=0,this.listeningTimer=0,this.listeningTimerTotal=0,this.isListening=!0}stopListening(){this.isListening=!1}async playAnimation(t,e=null,n=10,i=0,s=.01,o=!1){if(!this.armature)return;this.positionWasLocked=!o,o?console.log("Position locking disabled for FBX animation:",t):(this.lockAvatarPosition(),console.log("Position locked immediately before FBX animation:",t));let l=this.animClips.find(h=>h.url===t+"-"+i);if(l){let h=this.animQueue.find(a=>a.template.name==="pose");h&&(h.ts[0]=1/0),Object.entries(l.pose.props).forEach(a=>{this.poseBase.props[a[0]]=a[1].clone(),this.poseTarget.props[a[0]]=a[1].clone(),this.poseTarget.props[a[0]].t=0,this.poseTarget.props[a[0]].d=1e3}),this.mixer?console.log("Using existing mixer for FBX animation, preserving morph targets"):(this.mixer=new x.AnimationMixer(this.armature),console.log("Created new mixer for FBX animation")),this.mixer.addEventListener("finished",this.stopAnimation.bind(this),{once:!0});const r=Math.ceil(n/l.clip.duration),u=this.mixer.clipAction(l.clip);u.setLoop(x.LoopRepeat,r),u.clampWhenFinished=!0,this.currentFBXAction=u;try{u.fadeIn(.5).play(),console.log("FBX animation started successfully:",t)}catch(a){console.warn("FBX animation failed to start:",a),this.stopAnimation();return}if(u.getClip().tracks.length===0){console.warn("FBX animation has no valid tracks, stopping"),this.stopAnimation();return}}else{if(t.split(".").pop().toLowerCase()!=="fbx"){console.error(`Invalid file type for FBX animation: ${t}. Expected .fbx file.`);return}let r=!1;try{const c=await fetch(t,{method:"HEAD"});if(r=c.ok,!r){console.error(`FBX file not found at ${t}. Status: ${c.status}`),console.error("Please check:"),console.error("1. File path is correct (note: path is case-sensitive)"),console.error("2. File exists in your public folder"),console.error("3. File is accessible (not blocked by server)");return}}catch(c){console.warn(`Could not verify file existence for ${t}, attempting to load anyway:`,c)}const u=new Pe.FBXLoader;let a;try{a=await u.loadAsync(t,e)}catch(c){console.error(`Failed to load FBX animation from ${t}:`,c),console.error("Error details:",{message:c.message,url:t,suggestion:"Make sure the file is a valid FBX file and the path is correct"}),c.message&&c.message.includes("version number")&&(console.error("FBX Loader Error: Cannot find version number"),console.error("This error usually means:"),console.error("1. The file is not a valid FBX file (might be GLB, corrupted, or wrong format)"),console.error("2. The file might be corrupted"),console.error("3. The file path might be incorrect"),console.error("4. The server returned an HTML error page instead of the FBX file"),console.error("5. The file might not exist at that path"),console.error(""),console.error("Solution: Please verify:"),console.error(` - File exists at: ${t}`),console.error(" - File is a valid FBX binary file"),console.error(" - File path matches your public folder structure"),console.error(" - File is not corrupted"));try{const d=await fetch(t),g=d.headers.get("content-type"),y=await d.text();console.error("Response details:",{status:d.status,contentType:g,firstBytes:y.substring(0,100),isHTML:y.trim().startsWith("<!DOCTYPE")||y.trim().startsWith("<html")}),(y.trim().startsWith("<!DOCTYPE")||y.trim().startsWith("<html"))&&console.error("The server returned an HTML page instead of an FBX file. The file path is likely incorrect.")}catch(d){console.error("Could not fetch file for debugging:",d)}return}if(a&&a.animations&&a.animations[i]){let c=a.animations[i];const d={};c.tracks.forEach(y=>{y.name=y.name.replaceAll("mixamorig","");const b=y.name.split(".");if(b[1]==="position"){for(let L=0;L<y.values.length;L++)y.values[L]=y.values[L]*s;d[y.name]=new x.Vector3(y.values[0],y.values[1],y.values[2])}else b[1]==="quaternion"?d[y.name]=new x.Quaternion(y.values[0],y.values[1],y.values[2],y.values[3]):b[1]==="rotation"&&(d[b[0]+".quaternion"]=new x.Quaternion().setFromEuler(new x.Euler(y.values[0],y.values[1],y.values[2],"XYZ")).normalize())});const g={props:d};d["Hips.position"]&&(d["Hips.position"].y<.5?g.lying=!0:g.standing=!0),this.animClips.push({url:t+"-"+i,clip:c,pose:g}),this.playAnimation(t,e,n,i,s)}else{const c="Animation "+t+" (ndx="+i+") not found";console.error(c),a&&a.animations?console.error(`FBX file loaded but has ${a.animations.length} animation(s), requested index ${i}`):console.error(a?"FBX file loaded but contains no animations":"FBX file failed to load or is invalid")}}}stopAnimation(){if(this.currentFBXAction&&(this.currentFBXAction.stop(),this.currentFBXAction=null,console.log("FBX animation action stopped, mixer preserved for lip-sync")),this.mixer&&this.mixer._actions.length===0&&(this.mixer=null,console.log("Mixer destroyed as no actions remain")),this.positionWasLocked?(this.unlockAvatarPosition(),console.log("Position unlocked after FBX animation stopped")):console.log("Position was not locked, no unlock needed"),this.gesture)for(let[e,n]of Object.entries(this.gesture))n.t=this.animClock,n.d=1e3,this.poseTarget.props.hasOwnProperty(e)&&(this.poseTarget.props[e].copy(n),this.poseTarget.props[e].t=this.animClock,this.poseTarget.props[e].d=1e3);let t=this.animQueue.find(e=>e.template.name==="pose");t&&(t.ts[0]=this.animClock),this.setPoseFromTemplate(null)}async playPose(t,e=null,n=5,i=0,s=.01){if(!this.armature)return;let o=this.poseTemplates[t];if(!o){const l=this.animPoses.find(h=>h.url===t+"-"+i);l&&(o=l.pose)}if(o){this.poseName=t,this.mixer=null;let l=this.animQueue.find(h=>h.template.name==="pose");l&&(l.ts[0]=this.animClock+n*1e3+2e3),this.setPoseFromTemplate(o)}else{let h=await new Pe.FBXLoader().loadAsync(t,e);if(h&&h.animations&&h.animations[i]){let r=h.animations[i];const u={};r.tracks.forEach(c=>{c.name=c.name.replaceAll("mixamorig","");const d=c.name.split(".");d[1]==="position"?u[c.name]=new x.Vector3(c.values[0]*s,c.values[1]*s,c.values[2]*s):d[1]==="quaternion"?u[c.name]=new x.Quaternion(c.values[0],c.values[1],c.values[2],c.values[3]):d[1]==="rotation"&&(u[d[0]+".quaternion"]=new x.Quaternion().setFromEuler(new x.Euler(c.values[0],c.values[1],c.values[2],"XYZ")).normalize())});const a={props:u};u["Hips.position"]&&(u["Hips.position"].y<.5?a.lying=!0:a.standing=!0),this.animPoses.push({url:t+"-"+i,pose:a}),this.playPose(t,e,n,i,s)}else{const r="Pose "+t+" (ndx="+i+") not found";console.error(r)}}}stopPose(){this.stopAnimation()}playGesture(t,e=3,n=!1,i=1e3){if(!this.armature)return;let s=this.gestureTemplates[t];if(s){this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null);let l=this.animQueue.findIndex(h=>h.template.name==="talkinghands");l!==-1&&(this.animQueue[l].ts=this.animQueue[l].ts.map(h=>0)),this.gesture=this.propsToThreeObjects(s),n&&(this.gesture=this.mirrorPose(this.gesture)),t==="namaste"&&this.avatar.body==="M"&&(this.gesture["RightArm.quaternion"].rotateTowards(new x.Quaternion(0,1,0,0),-.25),this.gesture["LeftArm.quaternion"].rotateTowards(new x.Quaternion(0,1,0,0),-.25));for(let[h,r]of Object.entries(this.gesture))r.t=this.animClock,r.d=i,this.poseTarget.props.hasOwnProperty(h)&&(this.poseTarget.props[h].copy(r),this.poseTarget.props[h].t=this.animClock,this.poseTarget.props[h].d=i);e&&Number.isFinite(e)&&(this.gestureTimeout=setTimeout(this.stopGesture.bind(this,i),1e3*e))}let o=this.animEmojis[t];if(o&&(o&&o.link&&(o=this.animEmojis[o.link]),o)){this.lookAtCamera(500);const l=this.animFactory(o);if(l.gesture=!0,e&&Number.isFinite(e)){const h=l.ts[0],u=l.ts[l.ts.length-1]-h;if(e*1e3-u>0){const c=[];for(let y=1;y<l.ts.length;y++)c.push(l.ts[y]-l.ts[y-1]);const d=o.template?.rescale||c.map(y=>y/u),g=e*1e3-u;l.ts=l.ts.map((y,b,L)=>b===0?h:L[b-1]+c[b-1]+d[b-1]*g)}else{const c=e*1e3/u;l.ts=l.ts.map(d=>h+c*(d-h))}}this.animQueue.push(l)}}stopGesture(t=1e3){if(this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null),this.gesture){const n=Object.entries(this.gesture);this.gesture=null;for(const[i,s]of n)this.poseTarget.props.hasOwnProperty(i)&&(this.poseTarget.props[i].copy(this.getPoseTemplateProp(i)),this.poseTarget.props[i].t=this.animClock,this.poseTarget.props[i].d=t)}let e=this.animQueue.findIndex(n=>n.gesture);e!==-1&&this.animQueue.splice(e,1)}ikSolve(t,e=null,n=!1,i=null){const s=new x.Vector3,o=new x.Vector3,l=new x.Vector3,h=new x.Vector3,r=new x.Quaternion,u=new x.Vector3,a=new x.Vector3,c=new x.Vector3,d=this.ikMesh.getObjectByName(t.root);d.position.setFromMatrixPosition(this.armature.getObjectByName(t.root).matrixWorld),d.quaternion.setFromRotationMatrix(this.armature.getObjectByName(t.root).matrixWorld),e&&n&&e.applyQuaternion(this.armature.quaternion).add(d.position);const g=this.ikMesh.getObjectByName(t.effector),y=t.links;y.forEach(L=>{L.bone=this.ikMesh.getObjectByName(L.link),L.bone.quaternion.copy(this.getPoseTemplateProp(L.link+".quaternion"))}),d.updateMatrixWorld(!0);const b=t.iterations||10;if(e)for(let L=0;L<b;L++){let V=!1;for(let p=0,M=y.length;p<M;p++){const C=y[p].bone;C.matrixWorld.decompose(h,r,u),r.invert(),o.setFromMatrixPosition(g.matrixWorld),l.subVectors(o,h),l.applyQuaternion(r),l.normalize(),s.subVectors(e,h),s.applyQuaternion(r),s.normalize();let f=s.dot(l);f>1?f=1:f<-1&&(f=-1),f=Math.acos(f),!(f<1e-5)&&(y[p].minAngle!==void 0&&f<y[p].minAngle&&(f=y[p].minAngle),y[p].maxAngle!==void 0&&f>y[p].maxAngle&&(f=y[p].maxAngle),a.crossVectors(l,s),a.normalize(),Y.setFromAxisAngle(a,f),C.quaternion.multiply(Y),C.rotation.setFromVector3(c.setFromEuler(C.rotation).clamp(new x.Vector3(y[p].minx!==void 0?y[p].minx:-1/0,y[p].miny!==void 0?y[p].miny:-1/0,y[p].minz!==void 0?y[p].minz:-1/0),new x.Vector3(y[p].maxx!==void 0?y[p].maxx:1/0,y[p].maxy!==void 0?y[p].maxy:1/0,y[p].maxz!==void 0?y[p].maxz:1/0))),C.updateMatrixWorld(!0),V=!0)}if(!V)break}i&&y.forEach(L=>{this.poseTarget.props[L.link+".quaternion"].copy(L.bone.quaternion),this.poseTarget.props[L.link+".quaternion"].t=this.animClock,this.poseTarget.props[L.link+".quaternion"].d=i})}dispose(){this.isRunning=!1,this.stop(),this.stopSpeaking(),this.streamStop(),this.isAvatarOnly?this.armature&&(this.armature.parent&&this.armature.parent.remove(this.armature),this.clearThree(this.armature)):(this.clearThree(this.scene),this.resizeobserver.disconnect(),this.renderer&&(this.renderer.dispose(),this.renderer.domElement&&this.renderer.domElement.parentNode&&this.renderer.domElement.parentNode.removeChild(this.renderer.domElement),this.renderer=null)),this.clearThree(this.ikMesh),this.dynamicbones.dispose()}}const be={apiKey:"sk_ace57ef3ef65a92b9d3bee2a00183b78ca790bc3e10964f2",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",defaultVoice:"21m00Tcm4TlvDq8ikWAM",voices:{rachel:"21m00Tcm4TlvDq8ikWAM",drew:"29vD33N1CtxCmqQRPOHJ",bella:"EXAVITQu4vr4xnSDxMaL",antoni:"ErXwobaYiN019PkySvjV",elli:"MF3mGyEYCl7XYWbV9V6O",josh:"VR6AewLTigWG4xSOukaG"}},ze={defaultVoice:"aura-2-thalia-en",voices:{thalia:"aura-2-thalia-en",asteria:"aura-2-asteria-en",orion:"aura-2-orion-en",stella:"aura-2-stella-en",athena:"aura-2-athena-en",hera:"aura-2-hera-en",zeus:"aura-2-zeus-en"}};function Ae(){return{service:"elevenlabs",endpoint:be.endpoint,apiKey:be.apiKey,defaultVoice:be.defaultVoice,voices:be.voices}}function mt(){const B=Ae(),t=[];return Object.entries(B.voices).forEach(([e,n])=>{t.push({value:n,label:`${e.charAt(0).toUpperCase()+e.slice(1)} (${B.service})`})}),t}const Me=R.forwardRef(({avatarUrl:B="/avatars/brunette.glb",avatarBody:t="F",mood:e="neutral",ttsLang:n="en",ttsService:i=null,ttsVoice:s=null,ttsApiKey:o=null,bodyMovement:l="idle",movementIntensity:h=.5,showFullAvatar:r=!0,cameraView:u="upper",onReady:a=()=>{},onLoading:c=()=>{},onError:d=()=>{},className:g="",style:y={},animations:b={}},L)=>{const V=R.useRef(null),p=R.useRef(null),M=R.useRef(r),C=R.useRef(null),f=R.useRef(null),E=R.useRef(!1),P=R.useRef({remainingText:null,originalText:null,options:null}),U=R.useRef([]),ie=R.useRef(0),[S,G]=R.useState(!0),[q,Z]=R.useState(null),[J,oe]=R.useState(!1),[se,ce]=R.useState(!1);R.useEffect(()=>{E.current=se},[se]),R.useEffect(()=>{M.current=r},[r]);const $=Ae(),le=i||$.service;let D;le==="browser"?D={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:le==="elevenlabs"?D={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:o||$.apiKey,defaultVoice:s||$.defaultVoice||be.defaultVoice,voices:$.voices||be.voices}:le==="deepgram"?D={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:o||$.apiKey,defaultVoice:s||$.defaultVoice||ze.defaultVoice,voices:$.voices||ze.voices}:D={...$,apiKey:o!==null?o:$.apiKey};const v={url:B,body:t,avatarMood:e,ttsLang:le==="browser"?"en-US":n,ttsVoice:s||D.defaultVoice,lipsyncLang:"en",showFullAvatar:r,bodyMovement:l,movementIntensity:h},I={ttsEndpoint:D.endpoint,ttsApikey:D.apiKey,ttsService:le,lipsyncModules:["en"],cameraView:u},z=R.useCallback(async()=>{if(!(!V.current||p.current))try{if(G(!0),Z(null),p.current=new Te(V.current,I),p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1),b&&Object.keys(b).length>0&&(p.current.customAnimations=b),await p.current.showAvatar(v,O=>{if(O.lengthComputable){const K=Math.min(100,Math.round(O.loaded/O.total*100));c(K)}}),await new Promise(O=>{const K=()=>{p.current.lipsync&&Object.keys(p.current.lipsync).length>0?O():setTimeout(K,100)};K()}),p.current&&p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(r)}catch(O){console.warn("Error setting full body mode on initialization:",O)}p.current&&p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1,p.current.controls.update()),G(!1),oe(!0),a(p.current);const F=()=>{document.visibilityState==="visible"?p.current?.start():p.current?.stop()};return document.addEventListener("visibilitychange",F),()=>{document.removeEventListener("visibilitychange",F)}}catch(A){console.error("Error initializing TalkingHead:",A),Z(A.message||"Failed to initialize avatar"),G(!1),d(A)}},[B,t,e,n,i,s,o,r,l,h,u]);R.useEffect(()=>(z(),()=>{p.current&&(p.current.stop(),p.current.dispose(),p.current=null)}),[z]),R.useEffect(()=>{if(!V.current||!p.current)return;const A=new ResizeObserver(O=>{for(const K of O)p.current&&p.current.onResize&&p.current.onResize()});A.observe(V.current);const F=()=>{p.current&&p.current.onResize&&p.current.onResize()};return window.addEventListener("resize",F),()=>{A.disconnect(),window.removeEventListener("resize",F)}},[J]);const T=R.useCallback(async()=>{if(p.current&&p.current.audioCtx)try{(p.current.audioCtx.state==="suspended"||p.current.audioCtx.state==="interrupted")&&(await p.current.audioCtx.resume(),console.log("Audio context resumed"))}catch(A){console.warn("Failed to resume audio context:",A)}},[]),N=R.useCallback(async(A,F={})=>{if(p.current&&J)try{f.current&&(clearInterval(f.current),f.current=null),C.current={text:A,options:F},P.current={remainingText:null,originalText:null,options:null};const O=/[!\.\?\n\p{Extended_Pictographic}]/ug,K=A.split(O).map(X=>X.trim()).filter(X=>X.length>0);U.current=K,ie.current=0,ce(!1),E.current=!1,await T();const de={...F,lipsyncLang:F.lipsyncLang||v.lipsyncLang||"en"};if(F.onSpeechEnd&&p.current){const X=p.current;let he=null,Ie=0;const Re=1200;let ye=!1;he=setInterval(()=>{if(Ie++,E.current)return;if(Ie>Re){if(he&&(clearInterval(he),he=null,f.current=null),!ye&&!E.current){ye=!0;try{F.onSpeechEnd()}catch(Fe){console.error("Error in onSpeechEnd callback (timeout):",Fe)}}return}const me=!X.speechQueue||X.speechQueue.length===0,Le=!X.audioPlaylist||X.audioPlaylist.length===0;X&&X.isSpeaking===!1&&me&&Le&&X.isAudioPlaying===!1&&!ye&&!E.current&&setTimeout(()=>{if(X&&!E.current&&X.isSpeaking===!1&&(!X.speechQueue||X.speechQueue.length===0)&&(!X.audioPlaylist||X.audioPlaylist.length===0)&&X.isAudioPlaying===!1&&!ye&&!E.current){ye=!0,he&&(clearInterval(he),he=null,f.current=null);try{F.onSpeechEnd()}catch(Ve){console.error("Error in onSpeechEnd callback:",Ve)}}},100)},100),f.current=he}p.current.lipsync&&Object.keys(p.current.lipsync).length>0?(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(A,de)):setTimeout(async()=>{await T(),p.current&&p.current.lipsync&&(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(A,de))},100)}catch(O){console.error("Error speaking text:",O),Z(O.message||"Failed to speak text")}},[J,T,v.lipsyncLang]),_=R.useCallback(()=>{p.current&&(p.current.stopSpeaking(),p.current.setSlowdownRate&&p.current.setSlowdownRate(1),C.current=null,ce(!1))},[]),j=R.useCallback(()=>{if(p.current&&p.current.pauseSpeaking){const A=p.current;if(A.isSpeaking||A.audioPlaylist&&A.audioPlaylist.length>0||A.speechQueue&&A.speechQueue.length>0){f.current&&(clearInterval(f.current),f.current=null);let O="";if(C.current&&U.current.length>0){const K=U.current.length,de=A.speechQueue?A.speechQueue.filter(Re=>Re&&Re.text&&Array.isArray(Re.text)&&Re.text.length>0).length:0,X=A.audioPlaylist&&A.audioPlaylist.length>0,he=de+(X?1:0),Ie=K-he;if(he>0&&Ie<K&&(O=U.current.slice(Ie).join(". ").trim(),!O&&de>0&&A.speechQueue)){const ye=A.speechQueue.filter(me=>me&&me.text&&Array.isArray(me.text)&&me.text.length>0).map(me=>me.text.map(Le=>Le.word||"").filter(Le=>Le.length>0).join(" ")).filter(me=>me.length>0).join(" ");ye&&ye.trim()&&(O=ye.trim())}}C.current&&(P.current={remainingText:O||null,originalText:C.current.text,options:C.current.options}),A.speechQueue&&(A.speechQueue.length=0),p.current.pauseSpeaking(),E.current=!0,ce(!0)}}},[]),Q=R.useCallback(async()=>{if(!p.current||!se)return;let A="",F={};if(P.current&&P.current.remainingText)A=P.current.remainingText,F=P.current.options||{},P.current={remainingText:null,originalText:null,options:null};else if(C.current&&C.current.text)A=C.current.text,F=C.current.options||{};else{console.warn("Resume called but no paused speech found"),ce(!1),E.current=!1;return}ce(!1),E.current=!1,await T();const O={...F,lipsyncLang:F.lipsyncLang||v.lipsyncLang||"en"};try{await N(A,O)}catch(K){console.error("Error resuming speech:",K),ce(!1),E.current=!1}},[T,se,N,v]),ve=R.useCallback(A=>{p.current&&p.current.setMood(A)},[]),ke=R.useCallback(A=>{p.current&&p.current.setSlowdownRate&&p.current.setSlowdownRate(A)},[]),H=R.useCallback((A,F=!1)=>{if(p.current&&p.current.playAnimation){if(b&&b[A]&&(A=b[A]),p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(M.current)}catch(K){console.warn("Error setting full body mode:",K)}if(A.includes("."))try{p.current.playAnimation(A,null,10,0,.01,F)}catch(K){console.warn(`Failed to play ${A}:`,K);try{p.current.setBodyMovement("idle")}catch(de){console.warn("Fallback animation also failed:",de)}}else{const K=[".fbx",".glb",".gltf"];let de=!1;for(const X of K)try{p.current.playAnimation(A+X,null,10,0,.01,F),de=!0;break}catch{}if(!de){console.warn("Animation not found:",A);try{p.current.setBodyMovement("idle")}catch(X){console.warn("Fallback animation also failed:",X)}}}}},[b]),ee=R.useCallback(()=>{p.current&&p.current.onResize&&p.current.onResize()},[]);return R.useImperativeHandle(L,()=>({speakText:N,stopSpeaking:_,pauseSpeaking:j,resumeSpeaking:Q,resumeAudioContext:T,setMood:ve,setTimingAdjustment:ke,playAnimation:H,isReady:J,isPaused:se,talkingHead:p.current,handleResize:ee,setBodyMovement:A=>{if(p.current&&p.current.setShowFullAvatar&&p.current.setBodyMovement)try{p.current.setShowFullAvatar(M.current),p.current.setBodyMovement(A)}catch(F){console.warn("Error setting body movement:",F)}},setMovementIntensity:A=>p.current?.setMovementIntensity(A),playRandomDance:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playRandomDance)try{p.current.setShowFullAvatar(M.current),p.current.playRandomDance()}catch(A){console.warn("Error playing random dance:",A)}},playReaction:A=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playReaction)try{p.current.setShowFullAvatar(M.current),p.current.playReaction(A)}catch(F){console.warn("Error playing reaction:",F)}},playCelebration:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playCelebration)try{p.current.setShowFullAvatar(M.current),p.current.playCelebration()}catch(A){console.warn("Error playing celebration:",A)}},setShowFullAvatar:A=>{if(p.current&&p.current.setShowFullAvatar)try{M.current=A,p.current.setShowFullAvatar(A)}catch(F){console.warn("Error setting showFullAvatar:",F)}},lockAvatarPosition:()=>{if(p.current&&p.current.lockAvatarPosition)try{p.current.lockAvatarPosition()}catch(A){console.warn("Error locking avatar position:",A)}},unlockAvatarPosition:()=>{if(p.current&&p.current.unlockAvatarPosition)try{p.current.unlockAvatarPosition()}catch(A){console.warn("Error unlocking avatar position:",A)}}})),re.jsxs("div",{className:`talking-head-avatar ${g}`,style:{width:"100%",height:"100%",position:"relative",...y},children:[re.jsx("div",{ref:V,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),S&&re.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),q&&re.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:q})]})});Me.displayName="TalkingHeadAvatar";const Ne=R.forwardRef(({text:B="Hello! I'm a talking avatar. How are you today?",onLoading:t=()=>{},onError:e=()=>{},onReady:n=()=>{},className:i="",style:s={},avatarConfig:o={}},l)=>{const h=R.useRef(null),r=R.useRef(null),[u,a]=R.useState(!0),[c,d]=R.useState(null),[g,y]=R.useState(!1),b=Ae(),L=o.ttsService||b.service,V=L==="browser"?{endpoint:"",apiKey:null,defaultVoice:"Google US English"}:{...b,apiKey:o.ttsApiKey!==void 0&&o.ttsApiKey!==null?o.ttsApiKey:b.apiKey,endpoint:L==="elevenlabs"&&o.ttsApiKey?"https://api.elevenlabs.io/v1/text-to-speech":b.endpoint},p={url:"/avatars/brunette.glb",body:"F",avatarMood:"neutral",ttsLang:L==="browser"?"en-US":"en",ttsVoice:o.ttsVoice||V.defaultVoice,lipsyncLang:"en",showFullAvatar:!0,bodyMovement:"idle",movementIntensity:.5,...o},M={ttsEndpoint:V.endpoint,ttsApikey:V.apiKey,ttsService:L,lipsyncModules:["en"],cameraView:"upper"},C=R.useCallback(async()=>{if(!(!h.current||r.current))try{if(a(!0),d(null),r.current=new Te(h.current,M),await r.current.showAvatar(p,q=>{if(q.lengthComputable){const Z=Math.min(100,Math.round(q.loaded/q.total*100));t(Z)}}),r.current.morphs&&r.current.morphs.length>0){const q=r.current.morphs[0].morphTargetDictionary;console.log("Available morph targets:",Object.keys(q));const Z=Object.keys(q).filter(J=>J.startsWith("viseme_"));console.log("Viseme morph targets found:",Z),Z.length===0&&(console.warn("No viseme morph targets found! Lip-sync will not work properly."),console.log("Expected viseme targets: viseme_aa, viseme_E, viseme_I, viseme_O, viseme_U, viseme_PP, viseme_SS, viseme_TH, viseme_DD, viseme_FF, viseme_kk, viseme_nn, viseme_RR, viseme_CH, viseme_sil"))}if(await new Promise(q=>{const Z=()=>{r.current.lipsync&&Object.keys(r.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(r.current.lipsync)),q()):(console.log("Waiting for lip-sync modules to load..."),setTimeout(Z,100))};Z()}),r.current&&r.current.setShowFullAvatar)try{r.current.setShowFullAvatar(!0),console.log("Avatar initialized in full body mode")}catch(q){console.warn("Error setting full body mode on initialization:",q)}a(!1),y(!0),n(r.current);const G=()=>{document.visibilityState==="visible"?r.current?.start():r.current?.stop()};return document.addEventListener("visibilitychange",G),()=>{document.removeEventListener("visibilitychange",G)}}catch(S){console.error("Error initializing TalkingHead:",S),d(S.message||"Failed to initialize avatar"),a(!1),e(S)}},[]);R.useEffect(()=>(C(),()=>{r.current&&(r.current.stop(),r.current.dispose(),r.current=null)}),[C]);const f=R.useCallback(S=>{if(r.current&&g)try{console.log("Speaking text:",S),console.log("Avatar config:",p),console.log("TalkingHead instance:",r.current),r.current.lipsync&&Object.keys(r.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(r.current.lipsync)),r.current.setSlowdownRate&&(r.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),r.current.speakText(S)):(console.warn("Lip-sync modules not ready, waiting..."),setTimeout(()=>{r.current&&r.current.lipsync?(console.log("Lip-sync now ready, speaking..."),r.current.setSlowdownRate&&(r.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),r.current.speakText(S)):console.error("Lip-sync still not ready after waiting")},500))}catch(G){console.error("Error speaking text:",G),d(G.message||"Failed to speak text")}else console.warn("Avatar not ready for speaking. isReady:",g,"talkingHeadRef:",!!r.current)},[g,p]),E=R.useCallback(()=>{r.current&&(r.current.stopSpeaking(),r.current.setSlowdownRate&&(r.current.setSlowdownRate(1),console.log("Reset timing to normal")))},[]),P=R.useCallback(S=>{r.current&&r.current.setMood(S)},[]),U=R.useCallback(S=>{r.current&&r.current.setSlowdownRate&&(r.current.setSlowdownRate(S),console.log("Timing adjustment set to:",S))},[]),ie=R.useCallback((S,G=!1)=>{if(r.current&&r.current.playAnimation){if(r.current.setShowFullAvatar)try{r.current.setShowFullAvatar(!0)}catch(Z){console.warn("Error setting full body mode:",Z)}if(S.includes("."))try{r.current.playAnimation(S,null,10,0,.01,G),console.log("Playing animation:",S)}catch(Z){console.log(`Failed to play ${S}:`,Z);try{r.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(J){console.warn("Fallback animation also failed:",J)}}else{const Z=[".fbx",".glb",".gltf"];let J=!1;for(const oe of Z)try{r.current.playAnimation(S+oe,null,10,0,.01,G),console.log("Playing animation:",S+oe),J=!0;break}catch{console.log(`Failed to play ${S}${oe}, trying next format...`)}if(!J){console.warn("Animation system not available or animation not found:",S);try{r.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(oe){console.warn("Fallback animation also failed:",oe)}}}}else console.warn("Animation system not available or animation not found:",S)},[]);return R.useImperativeHandle(l,()=>({speakText:f,stopSpeaking:E,setMood:P,setTimingAdjustment:U,playAnimation:ie,isReady:g,talkingHead:r.current,setBodyMovement:S=>{if(r.current&&r.current.setShowFullAvatar&&r.current.setBodyMovement)try{r.current.setShowFullAvatar(!0),r.current.setBodyMovement(S),console.log("Body movement set with full body mode:",S)}catch(G){console.warn("Error setting body movement:",G)}},setMovementIntensity:S=>r.current?.setMovementIntensity(S),playRandomDance:()=>{if(r.current&&r.current.setShowFullAvatar&&r.current.playRandomDance)try{r.current.setShowFullAvatar(!0),r.current.playRandomDance(),console.log("Random dance played with full body mode")}catch(S){console.warn("Error playing random dance:",S)}},playReaction:S=>{if(r.current&&r.current.setShowFullAvatar&&r.current.playReaction)try{r.current.setShowFullAvatar(!0),r.current.playReaction(S),console.log("Reaction played with full body mode:",S)}catch(G){console.warn("Error playing reaction:",G)}},playCelebration:()=>{if(r.current&&r.current.setShowFullAvatar&&r.current.playCelebration)try{r.current.setShowFullAvatar(!0),r.current.playCelebration(),console.log("Celebration played with full body mode")}catch(S){console.warn("Error playing celebration:",S)}},setShowFullAvatar:S=>{if(r.current&&r.current.setShowFullAvatar)try{r.current.setShowFullAvatar(S),console.log("Show full avatar set to:",S)}catch(G){console.warn("Error setting showFullAvatar:",G)}},lockAvatarPosition:()=>{if(r.current&&r.current.lockAvatarPosition)try{r.current.lockAvatarPosition()}catch(S){console.warn("Error locking avatar position:",S)}},unlockAvatarPosition:()=>{if(r.current&&r.current.unlockAvatarPosition)try{r.current.unlockAvatarPosition()}catch(S){console.warn("Error unlocking avatar position:",S)}}})),re.jsxs("div",{className:`talking-head-container ${i}`,style:s,children:[re.jsx("div",{ref:h,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),u&&re.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),c&&re.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:c})]})});Ne.displayName="TalkingHeadComponent";const Ue=R.forwardRef(({text:B=null,avatarUrl:t="/avatars/brunette.glb",avatarBody:e="F",mood:n="neutral",ttsLang:i="en",ttsService:s=null,ttsVoice:o=null,ttsApiKey:l=null,bodyMovement:h="idle",movementIntensity:r=.5,showFullAvatar:u=!1,cameraView:a="upper",onReady:c=()=>{},onLoading:d=()=>{},onError:g=()=>{},onSpeechEnd:y=()=>{},className:b="",style:L={},animations:V={},autoSpeak:p=!1},M)=>{const C=R.useRef(null),f=R.useRef(null),E=R.useRef(u),P=R.useRef(null),U=R.useRef(null),ie=R.useRef(!1),S=R.useRef({remainingText:null,originalText:null,options:null}),G=R.useRef([]),[q,Z]=R.useState(!0),[J,oe]=R.useState(null),[se,ce]=R.useState(!1),[$,le]=R.useState(!1);R.useEffect(()=>{ie.current=$},[$]),R.useEffect(()=>{E.current=u},[u]);const D=Ae(),v=s||D.service;let I;v==="browser"?I={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:v==="elevenlabs"?I={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:l||D.apiKey,defaultVoice:o||D.defaultVoice||be.defaultVoice,voices:D.voices||be.voices}:v==="deepgram"?I={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:l||D.apiKey,defaultVoice:o||D.defaultVoice||ze.defaultVoice,voices:D.voices||ze.voices}:I={...D,apiKey:l!==null?l:D.apiKey};const z={url:t,body:e,avatarMood:n,ttsLang:v==="browser"?"en-US":i,ttsVoice:o||I.defaultVoice,lipsyncLang:"en",showFullAvatar:u,bodyMovement:h,movementIntensity:r},T={ttsEndpoint:I.endpoint,ttsApikey:I.apiKey,ttsService:v,lipsyncModules:["en"],cameraView:a},N=R.useCallback(async()=>{if(!(!C.current||f.current))try{Z(!0),oe(null),f.current=new Te(C.current,T),await f.current.showAvatar(z,ee=>{if(ee.lengthComputable){const A=Math.min(100,Math.round(ee.loaded/ee.total*100));d(A)}}),Z(!1),ce(!0),c(f.current);const H=()=>{document.visibilityState==="visible"?f.current?.start():f.current?.stop()};return document.addEventListener("visibilitychange",H),()=>{document.removeEventListener("visibilitychange",H)}}catch(H){console.error("Error initializing TalkingHead:",H),oe(H.message||"Failed to initialize avatar"),Z(!1),g(H)}},[]);R.useEffect(()=>(N(),()=>{f.current&&(f.current.stop(),f.current.dispose(),f.current=null)}),[N]);const _=R.useCallback(async()=>{if(f.current)try{const H=f.current.audioCtx||f.current.audioContext;H&&(H.state==="suspended"||H.state==="interrupted")&&(await H.resume(),console.log("Audio context resumed"))}catch(H){console.warn("Failed to resume audio context:",H)}},[]),j=R.useCallback(async(H,ee={})=>{if(!f.current||!se){console.warn("Avatar not ready for speaking");return}if(!H||H.trim()===""){console.warn("No text provided to speak");return}await _(),S.current={remainingText:null,originalText:null,options:null},G.current=[],P.current={text:H,options:ee},U.current&&(clearInterval(U.current),U.current=null),le(!1),ie.current=!1;const A=H.split(/[.!?]+/).filter(O=>O.trim().length>0);G.current=A;const F={lipsyncLang:ee.lipsyncLang||"en",onSpeechEnd:()=>{U.current&&(clearInterval(U.current),U.current=null),ee.onSpeechEnd&&ee.onSpeechEnd(),y()}};try{f.current.speakText(H,F)}catch(O){console.error("Error speaking text:",O),oe(O.message||"Failed to speak text")}},[se,y,_]);R.useEffect(()=>{se&&B&&p&&f.current&&j(B)},[se,B,p,j]);const Q=R.useCallback(()=>{if(f.current)try{const H=f.current.isSpeaking||!1,ee=f.current.audioPlaylist||[],A=f.current.speechQueue||[];if(H||ee.length>0||A.length>0){U.current&&(clearInterval(U.current),U.current=null);let F="";A.length>0&&(F=A.map(O=>O.text&&Array.isArray(O.text)?O.text.map(K=>K.word).join(" "):O.text||"").join(" ")),S.current={remainingText:F||null,originalText:P.current?.text||null,options:P.current?.options||null},f.current.speechQueue.length=0,f.current.pauseSpeaking(),le(!0),ie.current=!0}}catch(H){console.warn("Error pausing speech:",H)}},[]),ve=R.useCallback(async()=>{if(!(!f.current||!$))try{await _(),le(!1),ie.current=!1;const H=S.current?.remainingText,ee=S.current?.originalText||P.current?.text,A=S.current?.options||P.current?.options||{},F=H||ee;F&&j(F,A)}catch(H){console.warn("Error resuming speech:",H),le(!1),ie.current=!1}},[$,j,_]),ke=R.useCallback(()=>{f.current&&(f.current.stopSpeaking(),U.current&&(clearInterval(U.current),U.current=null),le(!1),ie.current=!1)},[]);return R.useImperativeHandle(M,()=>({speakText:j,pauseSpeaking:Q,resumeSpeaking:ve,stopSpeaking:ke,resumeAudioContext:_,isPaused:()=>$,setMood:H=>f.current?.setMood(H),setBodyMovement:H=>{f.current&&f.current.setBodyMovement(H)},playAnimation:(H,ee=!1)=>{f.current&&f.current.playAnimation&&f.current.playAnimation(H,null,10,0,.01,ee)},playReaction:H=>f.current?.playReaction(H),playCelebration:()=>f.current?.playCelebration(),setShowFullAvatar:H=>{f.current&&(E.current=H,f.current.setShowFullAvatar(H))},isReady:se,talkingHead:f.current})),re.jsxs("div",{className:`simple-talking-avatar-container ${b}`,style:L,children:[re.jsx("div",{ref:C,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),q&&re.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),J&&re.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:J})]})});Ue.displayName="SimpleTalkingAvatar";const We=R.forwardRef(({curriculumData:B=null,avatarConfig:t={},animations:e={},onLessonStart:n=()=>{},onLessonComplete:i=()=>{},onQuestionAnswer:s=()=>{},onCurriculumComplete:o=()=>{},onCustomAction:l=()=>{},autoStart:h=!1},r)=>{const u=R.useRef(null),a=R.useRef({currentModuleIndex:0,currentLessonIndex:0,currentQuestionIndex:0,isTeaching:!1,isQuestionMode:!1,lessonCompleted:!1,curriculumCompleted:!1,score:0,totalQuestions:0}),c=R.useRef({onLessonStart:n,onLessonComplete:i,onQuestionAnswer:s,onCurriculumComplete:o,onCustomAction:l}),d=R.useRef(null),g=R.useRef(null),y=R.useRef(null),b=R.useRef(null),L=R.useRef(null),V=R.useRef(null),p=R.useRef(null),M=R.useRef(B?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]}),C=R.useRef({avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"});R.useEffect(()=>{c.current={onLessonStart:n,onLessonComplete:i,onQuestionAnswer:s,onCurriculumComplete:o,onCustomAction:l}},[n,i,s,o,l]),R.useEffect(()=>{M.current=B?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]},C.current={avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"}},[B,t,e]);const f=R.useCallback(()=>(M.current||{modules:[]}).modules[a.current.currentModuleIndex]?.lessons[a.current.currentLessonIndex],[]),E=R.useCallback(()=>f()?.questions[a.current.currentQuestionIndex],[f]),P=R.useCallback((v,I)=>I.type==="multiple_choice"||I.type==="true_false"?v===I.answer:I.type==="code_test"&&typeof v=="object"&&v!==null?v.passed===!0:!1,[]),U=R.useCallback(()=>{a.current.lessonCompleted=!0,a.current.isQuestionMode=!1;const v=a.current.totalQuestions>0?Math.round(a.current.score/a.current.totalQuestions*100):100;let I="Congratulations! You've completed this lesson";if(a.current.totalQuestions>0?I+=` You got ${a.current.score} correct out of ${a.current.totalQuestions} question${a.current.totalQuestions===1?"":"s"}, achieving a score of ${v} percent. `:I+="! ",v>=80?I+="Excellent work! You have a great understanding of this topic.":v>=60?I+="Good job! You understand most of the concepts.":I+="Keep practicing! You're making progress.",c.current.onLessonComplete({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,score:a.current.score,totalQuestions:a.current.totalQuestions,percentage:v}),c.current.onCustomAction({type:"lessonComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,score:a.current.score,totalQuestions:a.current.totalQuestions,percentage:v}),u.current){if(u.current.setMood("happy"),e.lessonComplete)try{u.current.playAnimation(e.lessonComplete,!0)}catch{u.current.playCelebration()}const z=M.current||{modules:[]},T=z.modules[a.current.currentModuleIndex],N=a.current.currentLessonIndex<(T?.lessons?.length||0)-1,_=a.current.currentModuleIndex<(z.modules?.length||0)-1,j=N||_,Q=C.current||{lipsyncLang:"en"};u.current.speakText(I,{lipsyncLang:Q.lipsyncLang,onSpeechEnd:()=>{c.current.onCustomAction({type:"lessonCompleteFeedbackDone",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,score:a.current.score,totalQuestions:a.current.totalQuestions,percentage:v,hasNextLesson:j})}})}},[e.lessonComplete]),ie=R.useCallback(()=>{a.current.curriculumCompleted=!0;const v=M.current||{modules:[]};if(c.current.onCurriculumComplete({modules:v.modules.length,totalLessons:v.modules.reduce((I,z)=>I+z.lessons.length,0)}),u.current){if(u.current.setMood("celebrating"),e.curriculumComplete)try{u.current.playAnimation(e.curriculumComplete,!0)}catch{u.current.playCelebration()}const I=C.current||{lipsyncLang:"en"};u.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!",{lipsyncLang:I.lipsyncLang})}},[e.curriculumComplete]),S=R.useCallback(()=>{const v=f();a.current.isQuestionMode=!0,a.current.currentQuestionIndex=0,a.current.totalQuestions=v?.questions?.length||0,a.current.score=0;const I=E();I&&c.current.onCustomAction({type:"questionStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,totalQuestions:a.current.totalQuestions,question:I,score:a.current.score});const z=()=>{if(!u.current||!I)return;if(u.current.setMood("happy"),e.questionStart)try{u.current.playAnimation(e.questionStart,!0)}catch(N){console.warn("Failed to play questionStart animation:",N)}const T=C.current||{lipsyncLang:"en"};I.type==="code_test"?u.current.speakText(`Let's test your coding skills! Here's your first challenge: ${I.question}`,{lipsyncLang:T.lipsyncLang}):I.type==="multiple_choice"?u.current.speakText(`Now let me ask you some questions. Here's the first one: ${I.question}`,{lipsyncLang:T.lipsyncLang}):I.type==="true_false"?u.current.speakText(`Let's start with some true or false questions. First question: ${I.question}`,{lipsyncLang:T.lipsyncLang}):u.current.speakText(`Now let me ask you some questions. Here's the first one: ${I.question}`,{lipsyncLang:T.lipsyncLang})};if(u.current&&u.current.isReady&&I)z();else if(u.current&&u.current.isReady){const T=C.current||{lipsyncLang:"en"};u.current.speakText("Now let me ask you some questions to test your understanding.",{lipsyncLang:T.lipsyncLang})}else{const T=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(T),I&&z())},100);setTimeout(()=>{clearInterval(T)},5e3)}},[e.questionStart,f,E]),G=R.useCallback(()=>{const v=f();if(a.current.currentQuestionIndex<(v?.questions?.length||0)-1){u.current&&u.current.stopSpeaking&&u.current.stopSpeaking(),a.current.currentQuestionIndex+=1;const I=E();I&&c.current.onCustomAction({type:"nextQuestion",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,totalQuestions:a.current.totalQuestions,question:I,score:a.current.score});const z=()=>{if(!u.current||!I)return;if(u.current.setMood("happy"),u.current.setBodyMovement("idle"),e.nextQuestion)try{u.current.playAnimation(e.nextQuestion,!0)}catch(Q){console.warn("Failed to play nextQuestion animation:",Q)}const T=C.current||{lipsyncLang:"en"},_=f()?.questions?.length||0,j=a.current.currentQuestionIndex>=_-1;if(I.type==="code_test"){const Q=j?`Great! Here's your final coding challenge: ${I.question}`:`Great! Now let's move on to your next coding challenge: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}else if(I.type==="multiple_choice"){const Q=j?`Alright! Here's your final question: ${I.question}`:`Alright! Here's your next question: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}else if(I.type==="true_false"){const Q=j?`Now let's try this final one: ${I.question}`:`Now let's try this one: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}else{const Q=j?`Here's your final question: ${I.question}`:`Here's the next question: ${I.question}`;u.current.speakText(Q,{lipsyncLang:T.lipsyncLang})}};if(u.current&&u.current.isReady&&I)z();else if(I){const T=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(T),z())},100);setTimeout(()=>{clearInterval(T)},5e3)}}else c.current.onCustomAction({type:"allQuestionsComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,totalQuestions:a.current.totalQuestions,score:a.current.score})},[e.nextQuestion,f,E]),q=R.useCallback(()=>{const v=M.current||{modules:[]},I=v.modules[a.current.currentModuleIndex];if(a.current.currentLessonIndex<(I?.lessons?.length||0)-1){a.current.currentLessonIndex+=1,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0;const T=v.modules[a.current.currentModuleIndex],N=a.current.currentLessonIndex<(T?.lessons?.length||0)-1,_=a.current.currentModuleIndex<(v.modules?.length||0)-1,j=N||_;c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,hasNextLesson:j}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else if(a.current.currentModuleIndex<(v.modules?.length||0)-1){a.current.currentModuleIndex+=1,a.current.currentLessonIndex=0,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0;const N=v.modules[a.current.currentModuleIndex],_=a.current.currentLessonIndex<(N?.lessons?.length||0)-1,j=a.current.currentModuleIndex<(v.modules?.length||0)-1,Q=_||j;c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,hasNextLesson:Q}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else L.current&&L.current()},[]),Z=R.useCallback(()=>{const v=f();let I=null;if(v?.avatar_script&&v?.body){const z=v.avatar_script.trim(),T=v.body.trim(),N=z.match(/[.!?]$/)?" ":". ";I=`${z}${N}${T}`}else I=v?.avatar_script||v?.body||null;if(u.current&&u.current.isReady&&I){a.current.isTeaching=!0,a.current.isQuestionMode=!1,a.current.score=0,a.current.totalQuestions=0,u.current.setMood("happy");let z=!1;if(e.teaching)try{u.current.playAnimation(e.teaching,!0),z=!0}catch(N){console.warn("Failed to play teaching animation:",N)}z||u.current.setBodyMovement("gesturing");const T=C.current||{lipsyncLang:"en"};c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v}),c.current.onCustomAction({type:"teachingStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v}),u.current.speakText(I,{lipsyncLang:T.lipsyncLang,onSpeechEnd:()=>{a.current.isTeaching=!1,c.current.onCustomAction({type:"teachingComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v,hasQuestions:v.questions&&v.questions.length>0}),v?.code_example&&c.current.onCustomAction({type:"codeExampleReady",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:v,codeExample:v.code_example})}})}},[e.teaching,f]),J=R.useCallback(v=>{const I=E(),z=P(v,I);if(z&&(a.current.score+=1),c.current.onQuestionAnswer({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,answer:v,isCorrect:z,question:I}),u.current)if(z){if(u.current.setMood("happy"),e.correct)try{u.current.playReaction("happy")}catch{u.current.setBodyMovement("happy")}u.current.setBodyMovement("gesturing");const N=f()?.questions?.length||0;a.current.currentQuestionIndex>=N-1;const _=a.current.currentQuestionIndex<N-1;console.log("[CurriculumLearning] Answer feedback - questionIndex:",a.current.currentQuestionIndex,"totalQuestions:",N,"hasNextQuestion:",_);const j=I.type==="code_test"?`Great job! Your code passed all the tests! ${I.explanation||""}`:`Excellent! That's correct! ${I.explanation||""}`,Q=C.current||{lipsyncLang:"en"};u.current.speakText(j,{lipsyncLang:Q.lipsyncLang,onSpeechEnd:()=>{c.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,isCorrect:!0,hasNextQuestion:_,score:a.current.score,totalQuestions:a.current.totalQuestions})}})}else{if(u.current.setMood("sad"),e.incorrect)try{u.current.playAnimation(e.incorrect,!0)}catch{u.current.setBodyMovement("idle")}u.current.setBodyMovement("gesturing");const N=f()?.questions?.length||0,_=a.current.currentQuestionIndex>=N-1,j=a.current.currentQuestionIndex<N-1;console.log("[CurriculumLearning] Answer feedback (incorrect) - questionIndex:",a.current.currentQuestionIndex,"totalQuestions:",N,"hasNextQuestion:",j);const Q=I.type==="code_test"?`Your code didn't pass all the tests. ${I.explanation||"Try again!"}`:`Not quite right, but don't worry! ${I.explanation||""}${_?"":" Let's move on to the next question."}`,ve=C.current||{lipsyncLang:"en"};u.current.speakText(Q,{lipsyncLang:ve.lipsyncLang,onSpeechEnd:()=>{c.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,isCorrect:!1,hasNextQuestion:j,score:a.current.score,totalQuestions:a.current.totalQuestions})}})}else{const N=f()?.questions?.length||0;c.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,isCorrect:z,hasNextQuestion:a.current.currentQuestionIndex<N-1,score:a.current.score,totalQuestions:a.current.totalQuestions,avatarNotReady:!0})}},[e.correct,e.incorrect,E,f,P]),oe=R.useCallback(v=>{const I=E();if(!v||typeof v!="object"){console.error("Invalid code test result format. Expected object with {passed: boolean, ...}");return}if(I?.type!=="code_test"){console.warn("Current question is not a code test. Use handleAnswerSelect for other question types.");return}const z={passed:v.passed===!0,results:v.results||[],output:v.output||"",error:v.error||null,executionTime:v.executionTime||null,testCount:v.testCount||0,passedCount:v.passedCount||0,failedCount:v.failedCount||0};c.current.onCustomAction({type:"codeTestSubmitted",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,testResult:z,question:I}),p.current&&p.current(z)},[E,P]),se=R.useCallback(()=>{if(a.current.currentQuestionIndex>0){a.current.currentQuestionIndex-=1;const v=E();v&&c.current.onCustomAction({type:"questionStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,questionIndex:a.current.currentQuestionIndex,totalQuestions:a.current.totalQuestions,question:v,score:a.current.score});const I=()=>{if(!u.current||!v)return;u.current.setMood("happy"),u.current.setBodyMovement("idle");const z=C.current||{lipsyncLang:"en"};v.type==="code_test"?u.current.speakText(`Let's go back to this coding challenge: ${v.question}`,{lipsyncLang:z.lipsyncLang}):u.current.speakText(`Going back to: ${v.question}`,{lipsyncLang:z.lipsyncLang})};if(u.current&&u.current.isReady&&v)I();else if(v){const z=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(z),I())},100);setTimeout(()=>{clearInterval(z)},5e3)}}},[E]),ce=R.useCallback(()=>{const v=M.current||{modules:[]};if(v.modules[a.current.currentModuleIndex],a.current.currentLessonIndex>0)a.current.currentLessonIndex-=1,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0,c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"));else if(a.current.currentModuleIndex>0){const T=v.modules[a.current.currentModuleIndex-1];a.current.currentModuleIndex-=1,a.current.currentLessonIndex=(T?.lessons?.length||1)-1,a.current.currentQuestionIndex=0,a.current.lessonCompleted=!1,a.current.isQuestionMode=!1,a.current.isTeaching=!1,a.current.score=0,a.current.totalQuestions=0,c.current.onCustomAction({type:"lessonStart",moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex}),c.current.onLessonStart({moduleIndex:a.current.currentModuleIndex,lessonIndex:a.current.currentLessonIndex,lesson:f()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}},[f]),$=R.useCallback(()=>{a.current.currentModuleIndex=0,a.current.currentLessonIndex=0,a.current.currentQuestionIndex=0,a.current.isTeaching=!1,a.current.isQuestionMode=!1,a.current.lessonCompleted=!1,a.current.curriculumCompleted=!1,a.current.score=0,a.current.totalQuestions=0},[]),le=R.useCallback(v=>{console.log("Avatar is ready!",v);const I=f(),z=I?.avatar_script||I?.body;h&&z&&setTimeout(()=>{d.current&&d.current()},10)},[h,f]);R.useLayoutEffect(()=>{d.current=Z,g.current=q,y.current=U,b.current=G,L.current=ie,V.current=S,p.current=J}),R.useImperativeHandle(r,()=>({startTeaching:Z,startQuestions:S,handleAnswerSelect:J,handleCodeTestResult:oe,nextQuestion:G,previousQuestion:se,nextLesson:q,previousLesson:ce,completeLesson:U,completeCurriculum:ie,resetCurriculum:$,getState:()=>({...a.current}),getCurrentQuestion:()=>E(),getCurrentLesson:()=>f(),getAvatarRef:()=>u.current,speakText:async(v,I={})=>{await u.current?.resumeAudioContext?.();const z=C.current||{lipsyncLang:"en"};u.current?.speakText(v,{...I,lipsyncLang:I.lipsyncLang||z.lipsyncLang})},resumeAudioContext:async()=>{if(u.current?.resumeAudioContext)return await u.current.resumeAudioContext();const v=u.current?.talkingHead;if(v?.audioCtx){const I=v.audioCtx;if(I.state==="suspended"||I.state==="interrupted")try{await I.resume(),console.log("Audio context resumed via talkingHead")}catch(z){console.warn("Failed to resume audio context:",z)}}else console.warn("Audio context not available yet")},stopSpeaking:()=>u.current?.stopSpeaking(),pauseSpeaking:()=>u.current?.pauseSpeaking(),resumeSpeaking:async()=>await u.current?.resumeSpeaking(),isPaused:()=>u.current&&typeof u.current.isPaused<"u"?u.current.isPaused:!1,setMood:v=>u.current?.setMood(v),playAnimation:(v,I)=>u.current?.playAnimation(v,I),setBodyMovement:v=>u.current?.setBodyMovement(v),setMovementIntensity:v=>u.current?.setMovementIntensity(v),playRandomDance:()=>u.current?.playRandomDance(),playReaction:v=>u.current?.playReaction(v),playCelebration:()=>u.current?.playCelebration(),setShowFullAvatar:v=>u.current?.setShowFullAvatar(v),setTimingAdjustment:v=>u.current?.setTimingAdjustment(v),lockAvatarPosition:()=>u.current?.lockAvatarPosition(),unlockAvatarPosition:()=>u.current?.unlockAvatarPosition(),triggerCustomAction:(v,I)=>{c.current.onCustomAction({type:v,...I,state:{...a.current}})},handleResize:()=>u.current?.handleResize(),isAvatarReady:()=>u.current?.isReady||!1}),[Z,S,J,oe,G,q,U,ie,$,E,f]);const D=C.current||{avatarUrl:"/avatars/brunette.glb",avatarBody:"F",mood:"happy",ttsLang:"en",ttsService:null,ttsVoice:null,ttsApiKey:null,bodyMovement:"gesturing",movementIntensity:.7,showFullAvatar:!1,animations:e};return re.jsx("div",{style:{width:"100%",height:"100%"},children:re.jsx(Me,{ref:u,avatarUrl:D.avatarUrl,avatarBody:D.avatarBody,mood:D.mood,ttsLang:D.ttsLang,ttsService:D.ttsService,ttsVoice:D.ttsVoice,ttsApiKey:D.ttsApiKey,bodyMovement:D.bodyMovement,movementIntensity:D.movementIntensity,showFullAvatar:D.showFullAvatar,cameraView:"upper",animations:D.animations,onReady:le,onLoading:()=>{},onError:v=>{console.error("Avatar error:",v)}})})});We.displayName="CurriculumLearning";const Ee={dance:{name:"dance",type:"code-based",variations:["dancing","dancing2","dancing3"],loop:!0,duration:1e4,description:"Celebration dance animation with multiple variations"},happy:{name:"happy",type:"code-based",loop:!0,duration:5e3,description:"Happy, upbeat body movement"},surprised:{name:"surprised",type:"code-based",loop:!1,duration:3e3,description:"Surprised reaction with quick movements"},thinking:{name:"thinking",type:"code-based",loop:!0,duration:8e3,description:"Thoughtful, contemplative movement"},nodding:{name:"nodding",type:"code-based",loop:!1,duration:2e3,description:"Nodding agreement gesture"},shaking:{name:"shaking",type:"code-based",loop:!1,duration:2e3,description:"Shaking head disagreement gesture"},celebration:{name:"celebration",type:"code-based",loop:!1,duration:3e3,description:"Celebration animation for achievements"},energetic:{name:"energetic",type:"code-based",loop:!0,duration:6e3,description:"High-energy, enthusiastic movement"},swaying:{name:"swaying",type:"code-based",loop:!0,duration:8e3,description:"Gentle swaying motion"},bouncing:{name:"bouncing",type:"code-based",loop:!0,duration:4e3,description:"Bouncy, playful movement"},gesturing:{name:"gesturing",type:"code-based",loop:!0,duration:5e3,description:"Teaching gesture animation"},walking:{name:"walking",type:"code-based",loop:!0,duration:6e3,description:"Walking motion"},prancing:{name:"prancing",type:"code-based",loop:!0,duration:4e3,description:"Playful prancing movement"},excited:{name:"excited",type:"code-based",loop:!0,duration:5e3,description:"Excited, energetic movement"}},pt=B=>Ee[B]||null,gt=B=>Ee.hasOwnProperty(B);exports.CurriculumLearning=We;exports.SimpleTalkingAvatar=Ue;exports.TalkingHeadAvatar=Me;exports.TalkingHeadComponent=Ne;exports.animations=Ee;exports.getActiveTTSConfig=Ae;exports.getAnimation=pt;exports.getVoiceOptions=mt;exports.hasAnimation=gt;
package/dist/index.js CHANGED
@@ -7382,11 +7382,7 @@ const gt = Me(({
7382
7382
  } catch (C) {
7383
7383
  console.warn("Failed to resume audio context:", C);
7384
7384
  }
7385
- }, []);
7386
- de(() => {
7387
- ae && G && p && f.current && j(G);
7388
- }, [ae, G, p, j]);
7389
- const j = T(async (C, te = {}) => {
7385
+ }, []), j = T(async (C, te = {}) => {
7390
7386
  if (!f.current || !ae) {
7391
7387
  console.warn("Avatar not ready for speaking");
7392
7388
  return;
@@ -7409,7 +7405,11 @@ const gt = Me(({
7409
7405
  } catch (B) {
7410
7406
  console.error("Error speaking text:", B), se(B.message || "Failed to speak text");
7411
7407
  }
7412
- }, [ae, y, _]), q = T(() => {
7408
+ }, [ae, y, _]);
7409
+ de(() => {
7410
+ ae && G && p && f.current && j(G);
7411
+ }, [ae, G, p, j]);
7412
+ const q = T(() => {
7413
7413
  if (f.current)
7414
7414
  try {
7415
7415
  const C = f.current.isSpeaking || !1, te = f.current.audioPlaylist || [], L = f.current.speechQueue || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.0.71",
3
+ "version": "1.0.73",
4
4
  "description": "A reusable React component for 3D talking avatars with lip-sync and text-to-speech",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -208,13 +208,6 @@ const SimpleTalkingAvatar = forwardRef(({
208
208
  }
209
209
  }, []);
210
210
 
211
- // Auto-speak text when ready and autoSpeak is true
212
- useEffect(() => {
213
- if (isReady && text && autoSpeak && talkingHeadRef.current) {
214
- speakText(text);
215
- }
216
- }, [isReady, text, autoSpeak, speakText]);
217
-
218
211
  // Speak text with proper callback handling
219
212
  const speakText = useCallback(async (textToSpeak, options = {}) => {
220
213
  if (!talkingHeadRef.current || !isReady) {
@@ -276,6 +269,13 @@ const SimpleTalkingAvatar = forwardRef(({
276
269
  }
277
270
  }, [isReady, onSpeechEnd, resumeAudioContext]);
278
271
 
272
+ // Auto-speak text when ready and autoSpeak is true
273
+ useEffect(() => {
274
+ if (isReady && text && autoSpeak && talkingHeadRef.current) {
275
+ speakText(text);
276
+ }
277
+ }, [isReady, text, autoSpeak, speakText]);
278
+
279
279
  // Pause speaking
280
280
  const pauseSpeaking = useCallback(() => {
281
281
  if (!talkingHeadRef.current) return;