@sage-rsc/talking-head-react 1.0.29 → 1.0.31

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 CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-C3A1_CcE.cjs");exports.CurriculumLearning=i.CurriculumLearning;exports.TalkingHeadAvatar=i.TalkingHeadAvatar;exports.TalkingHeadComponent=i.TalkingHeadComponent;exports.animations=i.animations;exports.getActiveTTSConfig=i.getActiveTTSConfig;exports.getAnimation=i.getAnimation;exports.getVoiceOptions=i.getVoiceOptions;exports.hasAnimation=i.hasAnimation;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("./index-WXvj5jje.cjs");exports.CurriculumLearning=i.CurriculumLearning;exports.TalkingHeadAvatar=i.TalkingHeadAvatar;exports.TalkingHeadComponent=i.TalkingHeadComponent;exports.animations=i.animations;exports.getActiveTTSConfig=i.getActiveTTSConfig;exports.getAnimation=i.getAnimation;exports.getVoiceOptions=i.getVoiceOptions;exports.hasAnimation=i.hasAnimation;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as n, T as s, a as t, d as e, b as o, g, c as m, h as r } from "./index-BvDk1TRw.js";
1
+ import { C as n, T as s, a as t, d as e, b as o, g, c as m, h as r } from "./index-iUfJQ70v.js";
2
2
  export {
3
3
  n as CurriculumLearning,
4
4
  s as TalkingHeadAvatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "A reusable React component for 3D talking avatars with lip-sync and text-to-speech",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -1,4 +1,4 @@
1
- import React, { useRef, useEffect, forwardRef, useImperativeHandle, useCallback } from 'react';
1
+ import React, { useRef, useEffect, useLayoutEffect, forwardRef, useImperativeHandle, useCallback } from 'react';
2
2
  import TalkingHeadAvatar from './TalkingHeadAvatar';
3
3
 
4
4
  /**
@@ -55,45 +55,77 @@ const CurriculumLearning = forwardRef(({
55
55
  const startTeachingRef = useRef(null);
56
56
  const nextLessonRef = useRef(null);
57
57
  const completeLessonRef = useRef(null);
58
-
59
- // Update callbacks ref when they change
60
- useEffect(() => {
61
- callbacksRef.current = {
62
- onLessonStart,
63
- onLessonComplete,
64
- onQuestionAnswer,
65
- onCurriculumComplete,
66
- onCustomAction
67
- };
68
- }, [onLessonStart, onLessonComplete, onQuestionAnswer, onCurriculumComplete, onCustomAction]);
69
-
70
- const curriculum = curriculumData?.curriculum || {
58
+ const nextQuestionRef = useRef(null);
59
+ const completeCurriculumRef = useRef(null);
60
+ const startQuestionsRef = useRef(null);
61
+ const handleAnswerSelectRef = useRef(null);
62
+
63
+ // Store curriculum in ref to avoid dependency issues - initialize early
64
+ const curriculumRef = useRef(curriculumData?.curriculum || {
71
65
  title: "Default Curriculum",
72
66
  description: "No curriculum data provided",
73
67
  language: "en",
74
68
  modules: []
75
- };
76
-
77
- const defaultAvatarConfig = {
69
+ });
70
+
71
+ // Store avatar config in ref to avoid dependency issues - initialize early
72
+ const defaultAvatarConfigRef = useRef({
78
73
  avatarUrl: avatarConfig.avatarUrl || "/avatars/brunette.glb",
79
74
  avatarBody: avatarConfig.avatarBody || "F",
80
75
  mood: avatarConfig.mood || "happy",
81
76
  ttsLang: avatarConfig.ttsLang || "en",
82
- ttsService: avatarConfig.ttsService || null, // Don't default to "edge" - let config decide
83
- ttsVoice: avatarConfig.ttsVoice || null, // Don't default - let config decide
77
+ ttsService: avatarConfig.ttsService || null,
78
+ ttsVoice: avatarConfig.ttsVoice || null,
84
79
  ttsApiKey: avatarConfig.ttsApiKey || null,
85
80
  bodyMovement: avatarConfig.bodyMovement || "gesturing",
86
81
  movementIntensity: avatarConfig.movementIntensity || 0.7,
87
82
  showFullAvatar: avatarConfig.showFullAvatar !== undefined ? avatarConfig.showFullAvatar : true,
88
83
  animations: animations,
89
- lipsyncLang: 'en' // Default lipsync language
90
- };
84
+ lipsyncLang: 'en'
85
+ });
86
+
87
+ // Update callbacks ref when they change
88
+ useEffect(() => {
89
+ callbacksRef.current = {
90
+ onLessonStart,
91
+ onLessonComplete,
92
+ onQuestionAnswer,
93
+ onCurriculumComplete,
94
+ onCustomAction
95
+ };
96
+ }, [onLessonStart, onLessonComplete, onQuestionAnswer, onCurriculumComplete, onCustomAction]);
97
+
98
+ // Update curriculum and config refs when they change
99
+ useEffect(() => {
100
+ curriculumRef.current = curriculumData?.curriculum || {
101
+ title: "Default Curriculum",
102
+ description: "No curriculum data provided",
103
+ language: "en",
104
+ modules: []
105
+ };
106
+
107
+ defaultAvatarConfigRef.current = {
108
+ avatarUrl: avatarConfig.avatarUrl || "/avatars/brunette.glb",
109
+ avatarBody: avatarConfig.avatarBody || "F",
110
+ mood: avatarConfig.mood || "happy",
111
+ ttsLang: avatarConfig.ttsLang || "en",
112
+ ttsService: avatarConfig.ttsService || null,
113
+ ttsVoice: avatarConfig.ttsVoice || null,
114
+ ttsApiKey: avatarConfig.ttsApiKey || null,
115
+ bodyMovement: avatarConfig.bodyMovement || "gesturing",
116
+ movementIntensity: avatarConfig.movementIntensity || 0.7,
117
+ showFullAvatar: avatarConfig.showFullAvatar !== undefined ? avatarConfig.showFullAvatar : true,
118
+ animations: animations,
119
+ lipsyncLang: 'en'
120
+ };
121
+ }, [curriculumData, avatarConfig, animations]);
91
122
 
92
123
  // Helper to get current lesson/question
93
124
  const getCurrentLesson = useCallback(() => {
125
+ const curriculum = curriculumRef.current || { modules: [] };
94
126
  const module = curriculum.modules[stateRef.current.currentModuleIndex];
95
127
  return module?.lessons[stateRef.current.currentLessonIndex];
96
- }, [curriculum]);
128
+ }, []);
97
129
 
98
130
  const getCurrentQuestion = useCallback(() => {
99
131
  const lesson = getCurrentLesson();
@@ -158,23 +190,26 @@ const CurriculumLearning = forwardRef(({
158
190
  });
159
191
 
160
192
  if (avatarRef.current) {
161
- avatarRef.current.setMood("happy");
193
+ avatarRef.current.setMood("happy");
162
194
  if (animations.lessonComplete) {
163
195
  try {
164
196
  avatarRef.current.playAnimation(animations.lessonComplete, true);
165
197
  } catch (error) {
166
- avatarRef.current.playCelebration();
198
+ avatarRef.current.playCelebration();
167
199
  }
168
200
  }
169
201
 
170
202
  // Check if there's a next lesson available
203
+ const curriculum = curriculumRef.current || { modules: [] };
171
204
  const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
172
205
  const hasNextLesson = stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1;
173
206
 
207
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
208
+
174
209
  if (hasNextLesson) {
175
210
  // Wait for speech to finish, then automatically move to next lesson
176
211
  avatarRef.current.speakText(feedbackMessage, {
177
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
212
+ lipsyncLang: config.lipsyncLang,
178
213
  onSpeechEnd: () => {
179
214
  // Add a small delay after speech ends for natural flow
180
215
  setTimeout(() => {
@@ -188,25 +223,25 @@ const CurriculumLearning = forwardRef(({
188
223
  } else {
189
224
  // This is the last lesson, complete curriculum instead
190
225
  avatarRef.current.speakText(feedbackMessage, {
191
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
226
+ lipsyncLang: config.lipsyncLang,
192
227
  onSpeechEnd: () => {
193
228
  // Add a small delay after speech ends for natural flow
194
229
  setTimeout(() => {
195
- completeCurriculum();
230
+ if (completeCurriculumRef.current) {
231
+ completeCurriculumRef.current();
232
+ }
196
233
  }, 1000);
197
234
  }
198
235
  });
199
236
  }
200
237
  }
201
- }, [animations.lessonComplete, curriculum, completeCurriculum, defaultAvatarConfig]);
202
-
203
- // Assign ref immediately after function is defined
204
- completeLessonRef.current = completeLesson;
238
+ }, [animations.lessonComplete]);
205
239
 
206
240
  // Complete entire curriculum
207
241
  const completeCurriculum = useCallback(() => {
208
242
  stateRef.current.curriculumCompleted = true;
209
243
 
244
+ const curriculum = curriculumRef.current || { modules: [] };
210
245
  callbacksRef.current.onCurriculumComplete({
211
246
  modules: curriculum.modules.length,
212
247
  totalLessons: curriculum.modules.reduce((sum, mod) => sum + mod.lessons.length, 0)
@@ -221,9 +256,10 @@ const CurriculumLearning = forwardRef(({
221
256
  avatarRef.current.playCelebration();
222
257
  }
223
258
  }
224
- avatarRef.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
259
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
260
+ avatarRef.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!", { lipsyncLang: config.lipsyncLang });
225
261
  }
226
- }, [animations.curriculumComplete, curriculum, defaultAvatarConfig]);
262
+ }, [animations.curriculumComplete]);
227
263
 
228
264
  // Start asking questions
229
265
  const startQuestions = useCallback(() => {
@@ -258,20 +294,23 @@ const CurriculumLearning = forwardRef(({
258
294
  }
259
295
  }
260
296
 
297
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
298
+
261
299
  // Introduce the first question properly based on type
262
300
  if (firstQuestion.type === 'code_test') {
263
- avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
301
+ avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
264
302
  } else if (firstQuestion.type === 'multiple_choice') {
265
- avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
303
+ avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
266
304
  } else if (firstQuestion.type === 'true_false') {
267
- avatarRef.current.speakText(`Let's start with some true or false questions. First question: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
305
+ avatarRef.current.speakText(`Let's start with some true or false questions. First question: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
268
306
  } else {
269
- avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
307
+ avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: config.lipsyncLang });
270
308
  }
271
309
  } else if (avatarRef.current) {
272
- avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
310
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
311
+ avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: config.lipsyncLang });
273
312
  }
274
- }, [animations.questionStart, getCurrentLesson, getCurrentQuestion, defaultAvatarConfig]);
313
+ }, [animations.questionStart, getCurrentLesson, getCurrentQuestion]);
275
314
 
276
315
  // Move to next question
277
316
  const nextQuestion = useCallback(() => {
@@ -301,27 +340,29 @@ const CurriculumLearning = forwardRef(({
301
340
  if (animations.nextQuestion) {
302
341
  try {
303
342
  avatarRef.current.playAnimation(animations.nextQuestion, true);
304
- } catch (error) {
343
+ } catch (error) {
305
344
  console.warn('Failed to play nextQuestion animation:', error);
306
345
  }
307
346
  }
308
347
 
348
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
349
+
309
350
  // Speak the question text with proper introduction
310
351
  if (nextQuestionObj.type === 'code_test') {
311
352
  avatarRef.current.speakText(`Great! Now let's move on to your next coding challenge: ${nextQuestionObj.question}`, {
312
- lipsyncLang: defaultAvatarConfig.lipsyncLang
353
+ lipsyncLang: config.lipsyncLang
313
354
  });
314
355
  } else if (nextQuestionObj.type === 'multiple_choice') {
315
356
  avatarRef.current.speakText(`Alright! Here's your next question: ${nextQuestionObj.question}`, {
316
- lipsyncLang: defaultAvatarConfig.lipsyncLang
357
+ lipsyncLang: config.lipsyncLang
317
358
  });
318
359
  } else if (nextQuestionObj.type === 'true_false') {
319
360
  avatarRef.current.speakText(`Now let's try this one: ${nextQuestionObj.question}`, {
320
- lipsyncLang: defaultAvatarConfig.lipsyncLang
361
+ lipsyncLang: config.lipsyncLang
321
362
  });
322
363
  } else {
323
364
  avatarRef.current.speakText(`Here's the next question: ${nextQuestionObj.question}`, {
324
- lipsyncLang: defaultAvatarConfig.lipsyncLang
365
+ lipsyncLang: config.lipsyncLang
325
366
  });
326
367
  }
327
368
  }
@@ -331,10 +372,11 @@ const CurriculumLearning = forwardRef(({
331
372
  completeLessonRef.current();
332
373
  }
333
374
  }
334
- }, [animations.nextQuestion, getCurrentLesson, getCurrentQuestion, defaultAvatarConfig]);
375
+ }, [animations.nextQuestion, getCurrentLesson, getCurrentQuestion]);
335
376
 
336
377
  // Move to next lesson
337
378
  const nextLesson = useCallback(() => {
379
+ const curriculum = curriculumRef.current || { modules: [] };
338
380
  const currentModule = curriculum.modules[stateRef.current.currentModuleIndex];
339
381
  if (stateRef.current.currentLessonIndex < (currentModule?.lessons?.length || 0) - 1) {
340
382
  stateRef.current.currentLessonIndex += 1;
@@ -353,7 +395,7 @@ const CurriculumLearning = forwardRef(({
353
395
  });
354
396
 
355
397
  if (avatarRef.current) {
356
- avatarRef.current.setMood("happy");
398
+ avatarRef.current.setMood("happy");
357
399
  avatarRef.current.setBodyMovement("idle");
358
400
 
359
401
  // Automatically start teaching the next lesson after a brief pause
@@ -364,12 +406,11 @@ const CurriculumLearning = forwardRef(({
364
406
  }, 500);
365
407
  }
366
408
  } else {
367
- completeCurriculum();
409
+ if (completeCurriculumRef.current) {
410
+ completeCurriculumRef.current();
411
+ }
368
412
  }
369
- }, [curriculum, completeCurriculum]);
370
-
371
- // Assign ref immediately after function is defined
372
- nextLessonRef.current = nextLesson;
413
+ }, []);
373
414
 
374
415
  // Start teaching the lesson
375
416
  const startTeaching = useCallback(() => {
@@ -383,10 +424,10 @@ const CurriculumLearning = forwardRef(({
383
424
  // Play animation if available (can be overridden via custom action)
384
425
  let animationPlayed = false;
385
426
  if (animations.teaching) {
386
- try {
427
+ try {
387
428
  avatarRef.current.playAnimation(animations.teaching, true);
388
429
  animationPlayed = true;
389
- } catch (error) {
430
+ } catch (error) {
390
431
  console.warn('Failed to play teaching animation:', error);
391
432
  }
392
433
  }
@@ -395,7 +436,8 @@ const CurriculumLearning = forwardRef(({
395
436
  avatarRef.current.setBodyMovement("gesturing");
396
437
  }
397
438
 
398
- avatarRef.current.speakText(currentLesson.avatar_script, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
439
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
440
+ avatarRef.current.speakText(currentLesson.avatar_script, { lipsyncLang: config.lipsyncLang });
399
441
 
400
442
  callbacksRef.current.onLessonStart({
401
443
  moduleIndex: stateRef.current.currentModuleIndex,
@@ -415,8 +457,11 @@ const CurriculumLearning = forwardRef(({
415
457
  setTimeout(() => {
416
458
  stateRef.current.isTeaching = false;
417
459
  if (currentLesson.questions && currentLesson.questions.length > 0) {
418
- startQuestions();
419
- } else {
460
+ // Use ref to avoid circular dependency
461
+ if (startQuestionsRef.current) {
462
+ startQuestionsRef.current();
463
+ }
464
+ } else {
420
465
  // No questions, complete the lesson using ref to avoid circular dependency
421
466
  if (completeLessonRef.current) {
422
467
  completeLessonRef.current();
@@ -424,10 +469,7 @@ const CurriculumLearning = forwardRef(({
424
469
  }
425
470
  }, 8000);
426
471
  }
427
- }, [animations.teaching, getCurrentLesson, startQuestions, defaultAvatarConfig]);
428
-
429
- // Assign ref immediately after function is defined
430
- startTeachingRef.current = startTeaching;
472
+ }, [animations.teaching, getCurrentLesson]);
431
473
 
432
474
  // Handle answer selection
433
475
  const handleAnswerSelect = useCallback((answer) => {
@@ -449,11 +491,11 @@ const CurriculumLearning = forwardRef(({
449
491
 
450
492
  if (avatarRef.current) {
451
493
  if (isCorrect) {
452
- avatarRef.current.setMood("happy");
494
+ avatarRef.current.setMood("happy");
453
495
  if (animations.correct) {
454
- try {
496
+ try {
455
497
  avatarRef.current.playReaction("happy");
456
- } catch (error) {
498
+ } catch (error) {
457
499
  avatarRef.current.setBodyMovement("happy");
458
500
  }
459
501
  }
@@ -462,22 +504,26 @@ const CurriculumLearning = forwardRef(({
462
504
  ? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
463
505
  : `Excellent! That's correct! ${currentQuestion.explanation || ''}`;
464
506
 
507
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
508
+
465
509
  // Wait for speech to finish before moving to next question
466
510
  avatarRef.current.speakText(successMessage, {
467
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
511
+ lipsyncLang: config.lipsyncLang,
468
512
  onSpeechEnd: () => {
469
513
  // Add a small delay after speech ends for natural flow
470
514
  setTimeout(() => {
471
- nextQuestion();
515
+ if (nextQuestionRef.current) {
516
+ nextQuestionRef.current();
517
+ }
472
518
  }, 500);
473
519
  }
474
520
  });
475
- } else {
521
+ } else {
476
522
  avatarRef.current.setMood("sad");
477
523
  if (animations.incorrect) {
478
524
  try {
479
525
  avatarRef.current.playAnimation(animations.incorrect, true);
480
- } catch (error) {
526
+ } catch (error) {
481
527
  avatarRef.current.setBodyMovement("idle");
482
528
  }
483
529
  }
@@ -486,22 +532,28 @@ const CurriculumLearning = forwardRef(({
486
532
  ? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
487
533
  : `Not quite right, but don't worry! ${currentQuestion.explanation || ''} Let's move on to the next question.`;
488
534
 
535
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
536
+
489
537
  // Wait for speech to finish before moving to next question
490
538
  avatarRef.current.speakText(failureMessage, {
491
- lipsyncLang: defaultAvatarConfig.lipsyncLang,
539
+ lipsyncLang: config.lipsyncLang,
492
540
  onSpeechEnd: () => {
493
541
  // Add a small delay after speech ends for natural flow
494
542
  setTimeout(() => {
495
- nextQuestion();
543
+ if (nextQuestionRef.current) {
544
+ nextQuestionRef.current();
545
+ }
496
546
  }, 500);
497
547
  }
498
548
  });
499
549
  }
500
550
  } else {
501
551
  // If avatar not ready, move to next question immediately
502
- nextQuestion();
552
+ if (nextQuestionRef.current) {
553
+ nextQuestionRef.current();
554
+ }
503
555
  }
504
- }, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer, nextQuestion, defaultAvatarConfig]);
556
+ }, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer]);
505
557
 
506
558
  // Handle code test result submission
507
559
  const handleCodeTestResult = useCallback((testResult) => {
@@ -542,8 +594,10 @@ const CurriculumLearning = forwardRef(({
542
594
  });
543
595
 
544
596
  // Handle answer using the same logic as regular questions
545
- handleAnswerSelect(codeTestAnswer);
546
- }, [getCurrentQuestion, handleAnswerSelect]);
597
+ if (handleAnswerSelectRef.current) {
598
+ handleAnswerSelectRef.current(codeTestAnswer);
599
+ }
600
+ }, [getCurrentQuestion, checkAnswer]);
547
601
 
548
602
  // Reset curriculum
549
603
  const resetCurriculum = useCallback(() => {
@@ -571,6 +625,18 @@ const CurriculumLearning = forwardRef(({
571
625
  }
572
626
  }, [autoStart, getCurrentLesson]);
573
627
 
628
+ // Set refs after all functions are defined - use useLayoutEffect for synchronous execution
629
+ // This ensures refs are set before React commits the render, avoiding initialization errors
630
+ useLayoutEffect(() => {
631
+ startTeachingRef.current = startTeaching;
632
+ nextLessonRef.current = nextLesson;
633
+ completeLessonRef.current = completeLesson;
634
+ nextQuestionRef.current = nextQuestion;
635
+ completeCurriculumRef.current = completeCurriculum;
636
+ startQuestionsRef.current = startQuestions;
637
+ handleAnswerSelectRef.current = handleAnswerSelect;
638
+ });
639
+
574
640
  // Expose methods via ref (for external control)
575
641
  useImperativeHandle(ref, () => ({
576
642
  // Curriculum control methods
@@ -594,7 +660,8 @@ const CurriculumLearning = forwardRef(({
594
660
  speakText: async (text, options = {}) => {
595
661
  // Ensure audio context is resumed before speaking
596
662
  await avatarRef.current?.resumeAudioContext?.();
597
- avatarRef.current?.speakText(text, { ...options, lipsyncLang: options.lipsyncLang || defaultAvatarConfig.lipsyncLang });
663
+ const config = defaultAvatarConfigRef.current || { lipsyncLang: 'en' };
664
+ avatarRef.current?.speakText(text, { ...options, lipsyncLang: options.lipsyncLang || config.lipsyncLang });
598
665
  },
599
666
  resumeAudioContext: async () => {
600
667
  // Try to resume through avatar ref first
@@ -646,6 +713,22 @@ const CurriculumLearning = forwardRef(({
646
713
  isAvatarReady: () => avatarRef.current?.isReady || false
647
714
  }), [startTeaching, startQuestions, handleAnswerSelect, handleCodeTestResult, nextQuestion, nextLesson, completeLesson, completeCurriculum, resetCurriculum, getCurrentQuestion, getCurrentLesson]);
648
715
 
716
+ // Get current config for render
717
+ const defaultAvatarConfig = defaultAvatarConfigRef.current || {
718
+ avatarUrl: "/avatars/brunette.glb",
719
+ avatarBody: "F",
720
+ mood: "happy",
721
+ ttsLang: "en",
722
+ ttsService: null,
723
+ ttsVoice: null,
724
+ ttsApiKey: null,
725
+ bodyMovement: "gesturing",
726
+ movementIntensity: 0.7,
727
+ showFullAvatar: true,
728
+ animations: animations,
729
+ lipsyncLang: 'en'
730
+ };
731
+
649
732
  return (
650
733
  <div style={{ width: '100%', height: '100%' }}>
651
734
  <TalkingHeadAvatar
@@ -676,4 +759,4 @@ const CurriculumLearning = forwardRef(({
676
759
 
677
760
  CurriculumLearning.displayName = 'CurriculumLearning';
678
761
 
679
- export default CurriculumLearning;
762
+ export default CurriculumLearning;
@@ -3248,7 +3248,7 @@ class TalkingHead {
3248
3248
  const module = LIPSYNC_MODULES[langLower];
3249
3249
 
3250
3250
  if (module && module[className]) {
3251
- this.lipsync[lang] = new module[className];
3251
+ this.lipsync[lang] = new module[className];
3252
3252
  console.log(`Loaded lip-sync module for ${lang}`);
3253
3253
  } else {
3254
3254
  console.warn(`Lip-sync module for ${lang} not found. Available modules:`, Object.keys(LIPSYNC_MODULES));