@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 +450 -119
- package/dist/index.cjs +1 -1
- package/dist/index.js +6 -6
- package/package.json +1 -1
- package/src/components/SimpleTalkingAvatar.jsx +7 -7
package/README.md
CHANGED
|
@@ -1,74 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sage-rsc/talking-head-react
|
|
2
2
|
|
|
3
|
-
A
|
|
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** -
|
|
9
|
-
- 🔊 **
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
The simplest way to get started - just pass text and the avatar speaks:
|
|
32
39
|
|
|
33
40
|
```jsx
|
|
34
|
-
import React, { useRef
|
|
35
|
-
import {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
<
|
|
53
|
+
<SimpleTalkingAvatar
|
|
51
54
|
ref={avatarRef}
|
|
52
|
-
avatarUrl="/
|
|
55
|
+
avatarUrl="/avatars/brunette.glb"
|
|
53
56
|
avatarBody="F"
|
|
54
57
|
mood="happy"
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
###
|
|
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="/
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
|
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: "
|
|
125
|
-
|
|
126
|
-
|
|
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: "
|
|
131
|
-
options: ["
|
|
132
|
-
answer: "
|
|
133
|
-
explanation: "
|
|
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: "/
|
|
145
|
+
avatarUrl: "/avatars/brunette.glb",
|
|
151
146
|
avatarBody: "F",
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
172
|
+
**Props:**
|
|
182
173
|
|
|
183
174
|
| Prop | Type | Default | Description |
|
|
184
175
|
|------|------|---------|-------------|
|
|
185
|
-
| `
|
|
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',
|
|
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
|
-
| `
|
|
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` | `
|
|
194
|
-
| `cameraView` | `string` | `"upper"` | Camera view ('upper', 'full'
|
|
195
|
-
| `
|
|
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
|
-
|
|
195
|
+
**Ref Methods:**
|
|
203
196
|
|
|
204
197
|
| Method | Parameters | Description |
|
|
205
198
|
|--------|------------|-------------|
|
|
206
|
-
| `speakText(text)` | `text: string` | Make
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
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
|
|
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
|
-
| `
|
|
269
|
+
| `onCustomAction` | `function` | `() => {}` | Callback for custom actions (interactive mode) |
|
|
232
270
|
|
|
233
|
-
|
|
271
|
+
**Ref Methods:**
|
|
234
272
|
|
|
235
273
|
| Method | Parameters | Description |
|
|
236
274
|
|--------|------------|-------------|
|
|
237
|
-
| `startTeaching()` | - | Start teaching
|
|
275
|
+
| `startTeaching()` | - | Start teaching current lesson |
|
|
238
276
|
| `startQuestions()` | - | Start asking questions |
|
|
239
|
-
| `handleAnswerSelect(answer)` | `answer: string/boolean` |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'").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("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'").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, _])
|
|
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
|
@@ -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;
|