@sage-rsc/talking-head-react 1.0.68 → 1.0.70
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/dist/index.cjs +2 -2
- package/dist/index.js +1118 -910
- package/package.json +1 -1
- package/src/components/CurriculumLearning.jsx +10 -2
- package/src/components/SimpleTalkingAvatar.jsx +426 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -689,6 +689,10 @@ const CurriculumLearning = forwardRef(({
|
|
|
689
689
|
const currentLesson = getCurrentLesson();
|
|
690
690
|
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
691
691
|
const isLastQuestion = stateRef.current.currentQuestionIndex >= totalQuestionsInLesson - 1;
|
|
692
|
+
const hasNextQuestion = stateRef.current.currentQuestionIndex < totalQuestionsInLesson - 1;
|
|
693
|
+
|
|
694
|
+
// Debug logging
|
|
695
|
+
console.log('[CurriculumLearning] Answer feedback - questionIndex:', stateRef.current.currentQuestionIndex, 'totalQuestions:', totalQuestionsInLesson, 'hasNextQuestion:', hasNextQuestion);
|
|
692
696
|
|
|
693
697
|
const successMessage = currentQuestion.type === "code_test"
|
|
694
698
|
? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
|
|
@@ -707,7 +711,7 @@ const CurriculumLearning = forwardRef(({
|
|
|
707
711
|
lessonIndex: stateRef.current.currentLessonIndex,
|
|
708
712
|
questionIndex: stateRef.current.currentQuestionIndex,
|
|
709
713
|
isCorrect: true,
|
|
710
|
-
hasNextQuestion:
|
|
714
|
+
hasNextQuestion: hasNextQuestion,
|
|
711
715
|
score: stateRef.current.score,
|
|
712
716
|
totalQuestions: stateRef.current.totalQuestions
|
|
713
717
|
});
|
|
@@ -728,6 +732,10 @@ const CurriculumLearning = forwardRef(({
|
|
|
728
732
|
const currentLesson = getCurrentLesson();
|
|
729
733
|
const totalQuestionsInLesson = currentLesson?.questions?.length || 0;
|
|
730
734
|
const isLastQuestion = stateRef.current.currentQuestionIndex >= totalQuestionsInLesson - 1;
|
|
735
|
+
const hasNextQuestion = stateRef.current.currentQuestionIndex < totalQuestionsInLesson - 1;
|
|
736
|
+
|
|
737
|
+
// Debug logging
|
|
738
|
+
console.log('[CurriculumLearning] Answer feedback (incorrect) - questionIndex:', stateRef.current.currentQuestionIndex, 'totalQuestions:', totalQuestionsInLesson, 'hasNextQuestion:', hasNextQuestion);
|
|
731
739
|
|
|
732
740
|
const failureMessage = currentQuestion.type === "code_test"
|
|
733
741
|
? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
|
|
@@ -746,7 +754,7 @@ const CurriculumLearning = forwardRef(({
|
|
|
746
754
|
lessonIndex: stateRef.current.currentLessonIndex,
|
|
747
755
|
questionIndex: stateRef.current.currentQuestionIndex,
|
|
748
756
|
isCorrect: false,
|
|
749
|
-
hasNextQuestion:
|
|
757
|
+
hasNextQuestion: hasNextQuestion,
|
|
750
758
|
score: stateRef.current.score,
|
|
751
759
|
totalQuestions: stateRef.current.totalQuestions
|
|
752
760
|
});
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import { TalkingHead } from '../lib/talkinghead.mjs';
|
|
3
|
+
import { getActiveTTSConfig, ELEVENLABS_CONFIG, DEEPGRAM_CONFIG } from '../config/ttsConfig';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SimpleTalkingAvatar - A simple React component for 3D talking avatars
|
|
7
|
+
*
|
|
8
|
+
* This component provides all avatar settings and functionality without curriculum features.
|
|
9
|
+
* You can pass text to speak via props or use the ref methods.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} props
|
|
12
|
+
* @param {string} props.text - Text to speak (optional, can also use speakText method)
|
|
13
|
+
* @param {string} props.avatarUrl - URL/path to the GLB avatar file
|
|
14
|
+
* @param {string} props.avatarBody - Avatar body type ('M' or 'F')
|
|
15
|
+
* @param {string} props.mood - Initial mood ('happy', 'sad', 'neutral', etc.)
|
|
16
|
+
* @param {string} props.ttsLang - Text-to-speech language code
|
|
17
|
+
* @param {string} props.ttsService - TTS service ('edge', 'elevenlabs', 'deepgram', 'google', 'azure', 'browser')
|
|
18
|
+
* @param {string} props.ttsVoice - TTS voice ID
|
|
19
|
+
* @param {string} props.ttsApiKey - TTS API key (overrides config for ElevenLabs, Google Cloud, Azure)
|
|
20
|
+
* @param {string} props.bodyMovement - Initial body movement type
|
|
21
|
+
* @param {number} props.movementIntensity - Movement intensity (0-1)
|
|
22
|
+
* @param {boolean} props.showFullAvatar - Whether to show full body avatar
|
|
23
|
+
* @param {string} props.cameraView - Camera view ('upper', 'full', etc.)
|
|
24
|
+
* @param {Function} props.onReady - Callback when avatar is ready
|
|
25
|
+
* @param {Function} props.onLoading - Callback for loading progress
|
|
26
|
+
* @param {Function} props.onError - Callback for errors
|
|
27
|
+
* @param {Function} props.onSpeechEnd - Callback when speech ends
|
|
28
|
+
* @param {string} props.className - Additional CSS classes
|
|
29
|
+
* @param {Object} props.style - Additional inline styles
|
|
30
|
+
* @param {Object} props.animations - Object mapping animation names to FBX file paths
|
|
31
|
+
* @param {boolean} props.autoSpeak - Whether to automatically speak the text prop when ready
|
|
32
|
+
* @param {Object} ref - Ref to access component methods
|
|
33
|
+
*/
|
|
34
|
+
const SimpleTalkingAvatar = forwardRef(({
|
|
35
|
+
text = null,
|
|
36
|
+
avatarUrl = "/avatars/brunette.glb",
|
|
37
|
+
avatarBody = "F",
|
|
38
|
+
mood = "neutral",
|
|
39
|
+
ttsLang = "en",
|
|
40
|
+
ttsService = null,
|
|
41
|
+
ttsVoice = null,
|
|
42
|
+
ttsApiKey = null,
|
|
43
|
+
bodyMovement = "idle",
|
|
44
|
+
movementIntensity = 0.5,
|
|
45
|
+
showFullAvatar = false,
|
|
46
|
+
cameraView = "upper",
|
|
47
|
+
onReady = () => {},
|
|
48
|
+
onLoading = () => {},
|
|
49
|
+
onError = () => {},
|
|
50
|
+
onSpeechEnd = () => {},
|
|
51
|
+
className = "",
|
|
52
|
+
style = {},
|
|
53
|
+
animations = {},
|
|
54
|
+
autoSpeak = false
|
|
55
|
+
}, ref) => {
|
|
56
|
+
const containerRef = useRef(null);
|
|
57
|
+
const talkingHeadRef = useRef(null);
|
|
58
|
+
const showFullAvatarRef = useRef(showFullAvatar);
|
|
59
|
+
const pausedSpeechRef = useRef(null);
|
|
60
|
+
const speechEndIntervalRef = useRef(null);
|
|
61
|
+
const isPausedRef = useRef(false);
|
|
62
|
+
const speechProgressRef = useRef({ remainingText: null, originalText: null, options: null });
|
|
63
|
+
const originalSentencesRef = useRef([]);
|
|
64
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
65
|
+
const [error, setError] = useState(null);
|
|
66
|
+
const [isReady, setIsReady] = useState(false);
|
|
67
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
68
|
+
|
|
69
|
+
// Keep ref in sync with state
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
isPausedRef.current = isPaused;
|
|
72
|
+
}, [isPaused]);
|
|
73
|
+
|
|
74
|
+
// Update ref when prop changes
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
showFullAvatarRef.current = showFullAvatar;
|
|
77
|
+
}, [showFullAvatar]);
|
|
78
|
+
|
|
79
|
+
// Get TTS configuration
|
|
80
|
+
const ttsConfig = getActiveTTSConfig();
|
|
81
|
+
|
|
82
|
+
// Override TTS service if specified in props
|
|
83
|
+
const effectiveTtsService = ttsService || ttsConfig.service;
|
|
84
|
+
|
|
85
|
+
// Build effective TTS config
|
|
86
|
+
let effectiveTtsConfig;
|
|
87
|
+
|
|
88
|
+
if (effectiveTtsService === 'browser') {
|
|
89
|
+
effectiveTtsConfig = {
|
|
90
|
+
service: 'browser',
|
|
91
|
+
endpoint: '',
|
|
92
|
+
apiKey: null,
|
|
93
|
+
defaultVoice: 'Google US English'
|
|
94
|
+
};
|
|
95
|
+
} else if (effectiveTtsService === 'elevenlabs') {
|
|
96
|
+
const apiKey = ttsApiKey || ttsConfig.apiKey;
|
|
97
|
+
effectiveTtsConfig = {
|
|
98
|
+
service: 'elevenlabs',
|
|
99
|
+
endpoint: 'https://api.elevenlabs.io/v1/text-to-speech',
|
|
100
|
+
apiKey: apiKey,
|
|
101
|
+
defaultVoice: ttsVoice || ttsConfig.defaultVoice || ELEVENLABS_CONFIG.defaultVoice,
|
|
102
|
+
voices: ttsConfig.voices || ELEVENLABS_CONFIG.voices
|
|
103
|
+
};
|
|
104
|
+
} else if (effectiveTtsService === 'deepgram') {
|
|
105
|
+
const apiKey = ttsApiKey || ttsConfig.apiKey;
|
|
106
|
+
effectiveTtsConfig = {
|
|
107
|
+
service: 'deepgram',
|
|
108
|
+
endpoint: 'https://api.deepgram.com/v1/speak',
|
|
109
|
+
apiKey: apiKey,
|
|
110
|
+
defaultVoice: ttsVoice || ttsConfig.defaultVoice || DEEPGRAM_CONFIG.defaultVoice,
|
|
111
|
+
voices: ttsConfig.voices || DEEPGRAM_CONFIG.voices
|
|
112
|
+
};
|
|
113
|
+
} else {
|
|
114
|
+
effectiveTtsConfig = {
|
|
115
|
+
...ttsConfig,
|
|
116
|
+
apiKey: ttsApiKey !== null ? ttsApiKey : ttsConfig.apiKey
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const defaultAvatarConfig = {
|
|
121
|
+
url: avatarUrl,
|
|
122
|
+
body: avatarBody,
|
|
123
|
+
avatarMood: mood,
|
|
124
|
+
ttsLang: effectiveTtsService === 'browser' ? "en-US" : ttsLang,
|
|
125
|
+
ttsVoice: ttsVoice || effectiveTtsConfig.defaultVoice,
|
|
126
|
+
lipsyncLang: 'en',
|
|
127
|
+
showFullAvatar: showFullAvatar,
|
|
128
|
+
bodyMovement: bodyMovement,
|
|
129
|
+
movementIntensity: movementIntensity,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const defaultOptions = {
|
|
133
|
+
ttsEndpoint: effectiveTtsConfig.endpoint,
|
|
134
|
+
ttsApikey: effectiveTtsConfig.apiKey,
|
|
135
|
+
ttsService: effectiveTtsService,
|
|
136
|
+
lipsyncModules: ["en"],
|
|
137
|
+
cameraView: cameraView
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const initializeTalkingHead = useCallback(async () => {
|
|
141
|
+
if (!containerRef.current || talkingHeadRef.current) return;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
setIsLoading(true);
|
|
145
|
+
setError(null);
|
|
146
|
+
|
|
147
|
+
talkingHeadRef.current = new TalkingHead(containerRef.current, defaultOptions);
|
|
148
|
+
|
|
149
|
+
await talkingHeadRef.current.showAvatar(defaultAvatarConfig, (ev) => {
|
|
150
|
+
if (ev.lengthComputable) {
|
|
151
|
+
const progress = Math.min(100, Math.round(ev.loaded / ev.total * 100));
|
|
152
|
+
onLoading(progress);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
setIsLoading(false);
|
|
157
|
+
setIsReady(true);
|
|
158
|
+
onReady(talkingHeadRef.current);
|
|
159
|
+
|
|
160
|
+
// Handle visibility change
|
|
161
|
+
const handleVisibilityChange = () => {
|
|
162
|
+
if (document.visibilityState === "visible") {
|
|
163
|
+
talkingHeadRef.current?.start();
|
|
164
|
+
} else {
|
|
165
|
+
talkingHeadRef.current?.stop();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('Error initializing TalkingHead:', err);
|
|
177
|
+
setError(err.message || 'Failed to initialize avatar');
|
|
178
|
+
setIsLoading(false);
|
|
179
|
+
onError(err);
|
|
180
|
+
}
|
|
181
|
+
}, [avatarUrl, avatarBody, mood, ttsLang, effectiveTtsService, ttsVoice, ttsApiKey, bodyMovement, movementIntensity, showFullAvatar, cameraView, onLoading, onReady, onError]);
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
initializeTalkingHead();
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
if (talkingHeadRef.current) {
|
|
188
|
+
talkingHeadRef.current.stop();
|
|
189
|
+
talkingHeadRef.current.dispose();
|
|
190
|
+
talkingHeadRef.current = null;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}, [initializeTalkingHead]);
|
|
194
|
+
|
|
195
|
+
// Auto-speak text when ready and autoSpeak is true
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (isReady && text && autoSpeak && talkingHeadRef.current) {
|
|
198
|
+
speakText(text);
|
|
199
|
+
}
|
|
200
|
+
}, [isReady, text, autoSpeak]);
|
|
201
|
+
|
|
202
|
+
// Speak text with proper callback handling
|
|
203
|
+
const speakText = useCallback((textToSpeak, options = {}) => {
|
|
204
|
+
if (!talkingHeadRef.current || !isReady) {
|
|
205
|
+
console.warn('Avatar not ready for speaking');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!textToSpeak || textToSpeak.trim() === '') {
|
|
210
|
+
console.warn('No text provided to speak');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Reset speech progress tracking
|
|
215
|
+
speechProgressRef.current = { remainingText: null, originalText: null, options: null };
|
|
216
|
+
originalSentencesRef.current = [];
|
|
217
|
+
|
|
218
|
+
// Store for pause/resume
|
|
219
|
+
pausedSpeechRef.current = { text: textToSpeak, options };
|
|
220
|
+
|
|
221
|
+
// Clear any existing speech end interval
|
|
222
|
+
if (speechEndIntervalRef.current) {
|
|
223
|
+
clearInterval(speechEndIntervalRef.current);
|
|
224
|
+
speechEndIntervalRef.current = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Reset pause state
|
|
228
|
+
setIsPaused(false);
|
|
229
|
+
isPausedRef.current = false;
|
|
230
|
+
|
|
231
|
+
// Split text into sentences for tracking
|
|
232
|
+
const sentences = textToSpeak.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
233
|
+
originalSentencesRef.current = sentences;
|
|
234
|
+
|
|
235
|
+
const speakOptions = {
|
|
236
|
+
lipsyncLang: options.lipsyncLang || 'en',
|
|
237
|
+
onSpeechEnd: () => {
|
|
238
|
+
// Clear interval
|
|
239
|
+
if (speechEndIntervalRef.current) {
|
|
240
|
+
clearInterval(speechEndIntervalRef.current);
|
|
241
|
+
speechEndIntervalRef.current = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Call user's onSpeechEnd callback
|
|
245
|
+
if (options.onSpeechEnd) {
|
|
246
|
+
options.onSpeechEnd();
|
|
247
|
+
}
|
|
248
|
+
onSpeechEnd();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
talkingHeadRef.current.speakText(textToSpeak, speakOptions);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error('Error speaking text:', err);
|
|
256
|
+
setError(err.message || 'Failed to speak text');
|
|
257
|
+
}
|
|
258
|
+
}, [isReady, onSpeechEnd]);
|
|
259
|
+
|
|
260
|
+
// Pause speaking
|
|
261
|
+
const pauseSpeaking = useCallback(() => {
|
|
262
|
+
if (!talkingHeadRef.current) return;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Check if currently speaking
|
|
266
|
+
const isSpeaking = talkingHeadRef.current.isSpeaking || false;
|
|
267
|
+
const audioPlaylist = talkingHeadRef.current.audioPlaylist || [];
|
|
268
|
+
const speechQueue = talkingHeadRef.current.speechQueue || [];
|
|
269
|
+
|
|
270
|
+
if (isSpeaking || audioPlaylist.length > 0 || speechQueue.length > 0) {
|
|
271
|
+
// Clear speech end interval
|
|
272
|
+
if (speechEndIntervalRef.current) {
|
|
273
|
+
clearInterval(speechEndIntervalRef.current);
|
|
274
|
+
speechEndIntervalRef.current = null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Extract remaining text from speech queue
|
|
278
|
+
let remainingText = '';
|
|
279
|
+
if (speechQueue.length > 0) {
|
|
280
|
+
remainingText = speechQueue.map(item => {
|
|
281
|
+
if (item.text && Array.isArray(item.text)) {
|
|
282
|
+
return item.text.map(wordObj => wordObj.word).join(' ');
|
|
283
|
+
}
|
|
284
|
+
return item.text || '';
|
|
285
|
+
}).join(' ');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Store progress for resume
|
|
289
|
+
speechProgressRef.current = {
|
|
290
|
+
remainingText: remainingText || null,
|
|
291
|
+
originalText: pausedSpeechRef.current?.text || null,
|
|
292
|
+
options: pausedSpeechRef.current?.options || null
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Clear speech queue and pause
|
|
296
|
+
talkingHeadRef.current.speechQueue.length = 0;
|
|
297
|
+
talkingHeadRef.current.pauseSpeaking();
|
|
298
|
+
setIsPaused(true);
|
|
299
|
+
isPausedRef.current = true;
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.warn('Error pausing speech:', err);
|
|
303
|
+
}
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
// Resume speaking
|
|
307
|
+
const resumeSpeaking = useCallback(async () => {
|
|
308
|
+
if (!talkingHeadRef.current || !isPaused) return;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
// Resume audio context if needed
|
|
312
|
+
if (talkingHeadRef.current.audioContext && talkingHeadRef.current.audioContext.state === 'suspended') {
|
|
313
|
+
await talkingHeadRef.current.audioContext.resume();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setIsPaused(false);
|
|
317
|
+
isPausedRef.current = false;
|
|
318
|
+
|
|
319
|
+
// Determine what text to speak
|
|
320
|
+
const remainingText = speechProgressRef.current?.remainingText;
|
|
321
|
+
const originalText = speechProgressRef.current?.originalText || pausedSpeechRef.current?.text;
|
|
322
|
+
const originalOptions = speechProgressRef.current?.options || pausedSpeechRef.current?.options || {};
|
|
323
|
+
|
|
324
|
+
const textToSpeak = remainingText || originalText;
|
|
325
|
+
|
|
326
|
+
if (textToSpeak) {
|
|
327
|
+
speakText(textToSpeak, originalOptions);
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.warn('Error resuming speech:', err);
|
|
331
|
+
setIsPaused(false);
|
|
332
|
+
isPausedRef.current = false;
|
|
333
|
+
}
|
|
334
|
+
}, [isPaused, speakText]);
|
|
335
|
+
|
|
336
|
+
// Stop speaking
|
|
337
|
+
const stopSpeaking = useCallback(() => {
|
|
338
|
+
if (talkingHeadRef.current) {
|
|
339
|
+
talkingHeadRef.current.stopSpeaking();
|
|
340
|
+
if (speechEndIntervalRef.current) {
|
|
341
|
+
clearInterval(speechEndIntervalRef.current);
|
|
342
|
+
speechEndIntervalRef.current = null;
|
|
343
|
+
}
|
|
344
|
+
setIsPaused(false);
|
|
345
|
+
isPausedRef.current = false;
|
|
346
|
+
}
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
349
|
+
// Expose methods via ref
|
|
350
|
+
useImperativeHandle(ref, () => ({
|
|
351
|
+
speakText,
|
|
352
|
+
pauseSpeaking,
|
|
353
|
+
resumeSpeaking,
|
|
354
|
+
stopSpeaking,
|
|
355
|
+
isPaused: () => isPaused,
|
|
356
|
+
setMood: (mood) => talkingHeadRef.current?.setMood(mood),
|
|
357
|
+
setBodyMovement: (movement) => {
|
|
358
|
+
if (talkingHeadRef.current) {
|
|
359
|
+
talkingHeadRef.current.setBodyMovement(movement);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
playAnimation: (animationName, disablePositionLock = false) => {
|
|
363
|
+
if (talkingHeadRef.current && talkingHeadRef.current.playAnimation) {
|
|
364
|
+
talkingHeadRef.current.playAnimation(animationName, null, 10, 0, 0.01, disablePositionLock);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
playReaction: (reactionType) => talkingHeadRef.current?.playReaction(reactionType),
|
|
368
|
+
playCelebration: () => talkingHeadRef.current?.playCelebration(),
|
|
369
|
+
setShowFullAvatar: (show) => {
|
|
370
|
+
if (talkingHeadRef.current) {
|
|
371
|
+
showFullAvatarRef.current = show;
|
|
372
|
+
talkingHeadRef.current.setShowFullAvatar(show);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
isReady,
|
|
376
|
+
talkingHead: talkingHeadRef.current
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className={`simple-talking-avatar-container ${className}`} style={style}>
|
|
381
|
+
<div
|
|
382
|
+
ref={containerRef}
|
|
383
|
+
className="talking-head-viewer"
|
|
384
|
+
style={{
|
|
385
|
+
width: '100%',
|
|
386
|
+
height: '100%',
|
|
387
|
+
minHeight: '400px',
|
|
388
|
+
}}
|
|
389
|
+
/>
|
|
390
|
+
{isLoading && (
|
|
391
|
+
<div className="loading-overlay" style={{
|
|
392
|
+
position: 'absolute',
|
|
393
|
+
top: '50%',
|
|
394
|
+
left: '50%',
|
|
395
|
+
transform: 'translate(-50%, -50%)',
|
|
396
|
+
color: 'white',
|
|
397
|
+
fontSize: '18px',
|
|
398
|
+
zIndex: 10
|
|
399
|
+
}}>
|
|
400
|
+
Loading avatar...
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
{error && (
|
|
404
|
+
<div className="error-overlay" style={{
|
|
405
|
+
position: 'absolute',
|
|
406
|
+
top: '50%',
|
|
407
|
+
left: '50%',
|
|
408
|
+
transform: 'translate(-50%, -50%)',
|
|
409
|
+
color: '#ff6b6b',
|
|
410
|
+
fontSize: '16px',
|
|
411
|
+
textAlign: 'center',
|
|
412
|
+
zIndex: 10,
|
|
413
|
+
padding: '20px',
|
|
414
|
+
borderRadius: '8px'
|
|
415
|
+
}}>
|
|
416
|
+
{error}
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
SimpleTalkingAvatar.displayName = 'SimpleTalkingAvatar';
|
|
424
|
+
|
|
425
|
+
export default SimpleTalkingAvatar;
|
|
426
|
+
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export { default as TalkingHeadAvatar } from './components/TalkingHeadAvatar';
|
|
8
8
|
export { default as TalkingHeadComponent } from './components/TalkingHeadComponent';
|
|
9
|
+
export { default as SimpleTalkingAvatar } from './components/SimpleTalkingAvatar';
|
|
9
10
|
export { default as CurriculumLearning } from './components/CurriculumLearning';
|
|
10
11
|
export { getActiveTTSConfig, getVoiceOptions } from './config/ttsConfig';
|
|
11
12
|
export { animations, getAnimation, hasAnimation } from './config/animations';
|