@sage-rsc/talking-head-react 1.0.21 → 1.0.23

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-BeFzGp0g.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-Bwi0p5fF.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-3_k5n4iL.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-CiNyTJ1A.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.21",
3
+ "version": "1.0.23",
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",
@@ -80,7 +80,8 @@ const CurriculumLearning = forwardRef(({
80
80
  bodyMovement: avatarConfig.bodyMovement || "gesturing",
81
81
  movementIntensity: avatarConfig.movementIntensity || 0.7,
82
82
  showFullAvatar: avatarConfig.showFullAvatar !== undefined ? avatarConfig.showFullAvatar : true,
83
- animations: animations
83
+ animations: animations,
84
+ lipsyncLang: 'en' // Default lipsync language
84
85
  };
85
86
 
86
87
  // Helper to get current lesson/question
@@ -143,9 +144,9 @@ const CurriculumLearning = forwardRef(({
143
144
  avatarRef.current.playCelebration();
144
145
  }
145
146
  }
146
- avatarRef.current.speakText(feedbackMessage);
147
+ avatarRef.current.speakText(feedbackMessage, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
147
148
  }
148
- }, [animations.lessonComplete]);
149
+ }, [animations.lessonComplete, defaultAvatarConfig]);
149
150
 
150
151
  // Complete entire curriculum
151
152
  const completeCurriculum = useCallback(() => {
@@ -165,9 +166,9 @@ const CurriculumLearning = forwardRef(({
165
166
  avatarRef.current.playCelebration();
166
167
  }
167
168
  }
168
- avatarRef.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!");
169
+ 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 });
169
170
  }
170
- }, [animations.curriculumComplete, curriculum]);
171
+ }, [animations.curriculumComplete, curriculum, defaultAvatarConfig]);
171
172
 
172
173
  // Start asking questions
173
174
  const startQuestions = useCallback(() => {
@@ -176,7 +177,17 @@ const CurriculumLearning = forwardRef(({
176
177
  stateRef.current.currentQuestionIndex = 0;
177
178
  stateRef.current.totalQuestions = currentLesson?.questions?.length || 0;
178
179
 
179
- if (avatarRef.current) {
180
+ // Trigger custom action immediately (before avatar speaks) - includes first question
181
+ const firstQuestion = getCurrentQuestion();
182
+ callbacksRef.current.onCustomAction({
183
+ type: 'questionStart',
184
+ moduleIndex: stateRef.current.currentModuleIndex,
185
+ lessonIndex: stateRef.current.currentLessonIndex,
186
+ totalQuestions: stateRef.current.totalQuestions,
187
+ question: firstQuestion // Include first question in event
188
+ });
189
+
190
+ if (avatarRef.current) {
180
191
  avatarRef.current.setMood("curious");
181
192
 
182
193
  // Play custom animation if available
@@ -188,17 +199,18 @@ const CurriculumLearning = forwardRef(({
188
199
  }
189
200
  }
190
201
 
191
- avatarRef.current.speakText("Now let me ask you some questions to test your understanding.");
192
-
193
- // Trigger custom action
194
- callbacksRef.current.onCustomAction({
195
- type: 'questionStart',
196
- moduleIndex: stateRef.current.currentModuleIndex,
197
- lessonIndex: stateRef.current.currentLessonIndex,
198
- totalQuestions: stateRef.current.totalQuestions
199
- });
202
+ if (firstQuestion) {
203
+ // If there's a question, introduce it directly
204
+ if (firstQuestion.type === 'code_test') {
205
+ avatarRef.current.speakText(`Let's test your coding skills! Here's your first challenge: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
206
+ } else {
207
+ avatarRef.current.speakText(`Now let me ask you some questions. Here's the first one: ${firstQuestion.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
208
+ }
209
+ } else {
210
+ avatarRef.current.speakText("Now let me ask you some questions to test your understanding.", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
211
+ }
200
212
  }
201
- }, [animations.questionStart, getCurrentLesson]);
213
+ }, [animations.questionStart, getCurrentLesson, getCurrentQuestion, defaultAvatarConfig]);
202
214
 
203
215
  // Move to next question
204
216
  const nextQuestion = useCallback(() => {
@@ -206,33 +218,44 @@ const CurriculumLearning = forwardRef(({
206
218
  if (stateRef.current.currentQuestionIndex < (currentLesson?.questions?.length || 0) - 1) {
207
219
  stateRef.current.currentQuestionIndex += 1;
208
220
 
209
- if (avatarRef.current) {
210
- avatarRef.current.setMood("happy");
221
+ // Trigger custom action BEFORE speaking (so UI can update immediately)
222
+ const nextQuestionObj = getCurrentQuestion();
223
+ callbacksRef.current.onCustomAction({
224
+ type: 'nextQuestion',
225
+ moduleIndex: stateRef.current.currentModuleIndex,
226
+ lessonIndex: stateRef.current.currentLessonIndex,
227
+ questionIndex: stateRef.current.currentQuestionIndex,
228
+ question: nextQuestionObj
229
+ });
230
+
231
+ if (avatarRef.current) {
232
+ avatarRef.current.setMood("happy");
211
233
  avatarRef.current.setBodyMovement("idle");
212
234
 
213
235
  // Play custom animation if available
214
236
  if (animations.nextQuestion) {
215
- try {
237
+ try {
216
238
  avatarRef.current.playAnimation(animations.nextQuestion, true);
217
- } catch (error) {
239
+ } catch (error) {
218
240
  console.warn('Failed to play nextQuestion animation:', error);
219
241
  }
220
242
  }
221
243
 
222
- avatarRef.current.speakText("Here's the next question.");
223
-
224
- // Trigger custom action
225
- callbacksRef.current.onCustomAction({
226
- type: 'nextQuestion',
227
- moduleIndex: stateRef.current.currentModuleIndex,
228
- lessonIndex: stateRef.current.currentLessonIndex,
229
- questionIndex: stateRef.current.currentQuestionIndex
230
- });
244
+ if (nextQuestionObj) {
245
+ // Speak the question text directly if it's a code test, otherwise introduce it
246
+ if (nextQuestionObj.type === 'code_test') {
247
+ avatarRef.current.speakText(`Here's your next coding challenge: ${nextQuestionObj.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
248
+ } else {
249
+ avatarRef.current.speakText(`Here's the next question: ${nextQuestionObj.question}`, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
250
+ }
251
+ } else {
252
+ avatarRef.current.speakText("Here's the next question.", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
253
+ }
231
254
  }
232
255
  } else {
233
256
  completeLesson();
234
257
  }
235
- }, [animations.nextQuestion, getCurrentLesson, completeLesson]);
258
+ }, [animations.nextQuestion, getCurrentLesson, completeLesson, getCurrentQuestion, defaultAvatarConfig]);
236
259
 
237
260
  // Move to next lesson
238
261
  const nextLesson = useCallback(() => {
@@ -245,12 +268,12 @@ const CurriculumLearning = forwardRef(({
245
268
  stateRef.current.totalQuestions = 0;
246
269
 
247
270
  if (avatarRef.current) {
248
- avatarRef.current.speakText("Let's move on to the next lesson!");
271
+ avatarRef.current.speakText("Let's move on to the next lesson!", { lipsyncLang: defaultAvatarConfig.lipsyncLang });
249
272
  }
250
273
  } else {
251
274
  completeCurriculum();
252
275
  }
253
- }, [curriculum, completeCurriculum]);
276
+ }, [curriculum, completeCurriculum, defaultAvatarConfig]);
254
277
 
255
278
  // Start teaching the lesson
256
279
  const startTeaching = useCallback(() => {
@@ -276,7 +299,7 @@ const CurriculumLearning = forwardRef(({
276
299
  avatarRef.current.setBodyMovement("gesturing");
277
300
  }
278
301
 
279
- avatarRef.current.speakText(currentLesson.avatar_script);
302
+ avatarRef.current.speakText(currentLesson.avatar_script, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
280
303
 
281
304
  callbacksRef.current.onLessonStart({
282
305
  moduleIndex: stateRef.current.currentModuleIndex,
@@ -302,7 +325,7 @@ const CurriculumLearning = forwardRef(({
302
325
  }
303
326
  }, 8000);
304
327
  }
305
- }, [animations.teaching, getCurrentLesson, startQuestions, completeLesson]);
328
+ }, [animations.teaching, getCurrentLesson, startQuestions, completeLesson, defaultAvatarConfig]);
306
329
 
307
330
  // Handle answer selection
308
331
  const handleAnswerSelect = useCallback((answer) => {
@@ -336,7 +359,7 @@ const CurriculumLearning = forwardRef(({
336
359
  const successMessage = currentQuestion.type === "code_test"
337
360
  ? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
338
361
  : `Excellent! That's correct! ${currentQuestion.explanation || ''}`;
339
- avatarRef.current.speakText(successMessage);
362
+ avatarRef.current.speakText(successMessage, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
340
363
  } else {
341
364
  avatarRef.current.setMood("sad");
342
365
  if (animations.incorrect) {
@@ -350,10 +373,28 @@ const CurriculumLearning = forwardRef(({
350
373
  const failureMessage = currentQuestion.type === "code_test"
351
374
  ? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
352
375
  : `Not quite right, but don't worry! ${currentQuestion.explanation || ''} Let's move on to the next question.`;
353
- avatarRef.current.speakText(failureMessage);
376
+ avatarRef.current.speakText(failureMessage, { lipsyncLang: defaultAvatarConfig.lipsyncLang });
354
377
  }
378
+
379
+ // Automatically move to next question after avatar finishes speaking
380
+ // Estimate speaking time: ~150 characters per second, minimum 2 seconds
381
+ const messageLength = (isCorrect
382
+ ? (currentQuestion.type === "code_test"
383
+ ? `Great job! Your code passed all the tests! ${currentQuestion.explanation || ''}`
384
+ : `Excellent! That's correct! ${currentQuestion.explanation || ''}`)
385
+ : (currentQuestion.type === "code_test"
386
+ ? `Your code didn't pass all the tests. ${currentQuestion.explanation || 'Try again!'}`
387
+ : `Not quite right, but don't worry! ${currentQuestion.explanation || ''} Let's move on to the next question.`)).length;
388
+ const estimatedTime = Math.max(2000, (messageLength / 150) * 1000) + 500; // Add 500ms buffer
389
+
390
+ setTimeout(() => {
391
+ nextQuestion();
392
+ }, estimatedTime);
393
+ } else {
394
+ // If avatar not ready, move to next question immediately
395
+ nextQuestion();
355
396
  }
356
- }, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer]);
397
+ }, [animations.correct, animations.incorrect, getCurrentQuestion, checkAnswer, nextQuestion, defaultAvatarConfig]);
357
398
 
358
399
  // Handle code test result submission
359
400
  const handleCodeTestResult = useCallback((testResult) => {
@@ -441,10 +482,10 @@ const CurriculumLearning = forwardRef(({
441
482
  getAvatarRef: () => avatarRef.current,
442
483
 
443
484
  // Convenience methods that delegate to avatar (always check current ref)
444
- speakText: async (text) => {
485
+ speakText: async (text, options = {}) => {
445
486
  // Ensure audio context is resumed before speaking
446
487
  await avatarRef.current?.resumeAudioContext?.();
447
- avatarRef.current?.speakText(text);
488
+ avatarRef.current?.speakText(text, { ...options, lipsyncLang: options.lipsyncLang || defaultAvatarConfig.lipsyncLang });
448
489
  },
449
490
  resumeAudioContext: async () => {
450
491
  // Try to resume through avatar ref first
@@ -256,17 +256,23 @@ const TalkingHeadAvatar = forwardRef(({
256
256
  }
257
257
  }, []);
258
258
 
259
- const speakText = useCallback(async (textToSpeak) => {
259
+ const speakText = useCallback(async (textToSpeak, options = {}) => {
260
260
  if (talkingHeadRef.current && isReady) {
261
261
  try {
262
262
  // Always resume audio context first (required for user interaction)
263
263
  await resumeAudioContext();
264
264
 
265
+ // Ensure lipsyncLang is in options if not provided
266
+ const speakOptions = {
267
+ ...options,
268
+ lipsyncLang: options.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
269
+ };
270
+
265
271
  if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {
266
272
  if (talkingHeadRef.current.setSlowdownRate) {
267
273
  talkingHeadRef.current.setSlowdownRate(1.05);
268
274
  }
269
- talkingHeadRef.current.speakText(textToSpeak);
275
+ talkingHeadRef.current.speakText(textToSpeak, speakOptions);
270
276
  } else {
271
277
  setTimeout(async () => {
272
278
  await resumeAudioContext();
@@ -274,7 +280,7 @@ const TalkingHeadAvatar = forwardRef(({
274
280
  if (talkingHeadRef.current.setSlowdownRate) {
275
281
  talkingHeadRef.current.setSlowdownRate(1.05);
276
282
  }
277
- talkingHeadRef.current.speakText(textToSpeak);
283
+ talkingHeadRef.current.speakText(textToSpeak, speakOptions);
278
284
  }
279
285
  }, 500);
280
286
  }
@@ -283,7 +289,7 @@ const TalkingHeadAvatar = forwardRef(({
283
289
  setError(err.message || 'Failed to speak text');
284
290
  }
285
291
  }
286
- }, [isReady, resumeAudioContext]);
292
+ }, [isReady, resumeAudioContext, defaultAvatarConfig.lipsyncLang]);
287
293
 
288
294
  const stopSpeaking = useCallback(() => {
289
295
  if (talkingHeadRef.current) {