@sage-rsc/talking-head-react 1.0.62 → 1.0.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
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",
@@ -53,6 +53,7 @@ const TalkingHeadAvatar = forwardRef(({
53
53
  const pausedSpeechRef = useRef(null); // Track paused speech for resume
54
54
  const speechEndIntervalRef = useRef(null); // Track onSpeechEnd polling interval
55
55
  const isPausedRef = useRef(false); // Track pause state for interval checks
56
+ const speechProgressRef = useRef({ remainingText: null, originalText: null, options: null }); // Track speech progress for resume
56
57
  const [isLoading, setIsLoading] = useState(true);
57
58
  const [error, setError] = useState(null);
58
59
  const [isReady, setIsReady] = useState(false);
@@ -292,7 +293,10 @@ const TalkingHeadAvatar = forwardRef(({
292
293
 
293
294
  // Store speech for potential pause/resume
294
295
  pausedSpeechRef.current = { text: textToSpeak, options };
296
+ // Reset progress tracking when starting new speech
297
+ speechProgressRef.current = { remainingText: null, originalText: null, options: null };
295
298
  setIsPaused(false);
299
+ isPausedRef.current = false;
296
300
 
297
301
  // Always resume audio context first (required for user interaction)
298
302
  await resumeAudioContext();
@@ -436,6 +440,29 @@ const TalkingHeadAvatar = forwardRef(({
436
440
  speechEndIntervalRef.current = null;
437
441
  }
438
442
 
443
+ // Save remaining text from speechQueue before clearing it
444
+ let remainingText = '';
445
+ if (talkingHead.speechQueue && talkingHead.speechQueue.length > 0) {
446
+ // Extract text from remaining queue items
447
+ const remainingParts = talkingHead.speechQueue
448
+ .filter(item => item.text) // Only get items with text
449
+ .map(item => item.text)
450
+ .join(' ');
451
+
452
+ if (remainingParts) {
453
+ remainingText = remainingParts.trim();
454
+ }
455
+ }
456
+
457
+ // If we have remaining text, save it for resume
458
+ if (remainingText && pausedSpeechRef.current) {
459
+ speechProgressRef.current = {
460
+ remainingText: remainingText,
461
+ originalText: pausedSpeechRef.current.text,
462
+ options: pausedSpeechRef.current.options
463
+ };
464
+ }
465
+
439
466
  // Clear speech queue to prevent next statements from playing
440
467
  if (talkingHead.speechQueue) {
441
468
  talkingHead.speechQueue.length = 0;
@@ -450,20 +477,37 @@ const TalkingHeadAvatar = forwardRef(({
450
477
  }, []);
451
478
 
452
479
  const resumeSpeaking = useCallback(async () => {
453
- if (talkingHeadRef.current && isPaused && pausedSpeechRef.current && pausedSpeechRef.current.text) {
454
- const pausedSpeech = pausedSpeechRef.current;
455
- const pausedOptions = pausedSpeech.options || {};
456
-
480
+ if (talkingHeadRef.current && isPaused) {
457
481
  // Clear pause state first
458
482
  setIsPaused(false);
483
+ isPausedRef.current = false;
459
484
 
460
485
  // Resume audio context
461
486
  await resumeAudioContext();
462
487
 
463
- // Re-speak the paused text with original options (including onSpeechEnd callback)
488
+ // Determine what text to speak - use remaining text if available, otherwise full text
489
+ let textToSpeak = '';
490
+ let optionsToUse = {};
491
+
492
+ if (speechProgressRef.current && speechProgressRef.current.remainingText) {
493
+ // Resume from where we paused - speak only the remaining text
494
+ textToSpeak = speechProgressRef.current.remainingText;
495
+ optionsToUse = speechProgressRef.current.options || {};
496
+ // Clear progress after using it
497
+ speechProgressRef.current = { remainingText: null, originalText: null, options: null };
498
+ } else if (pausedSpeechRef.current && pausedSpeechRef.current.text) {
499
+ // Fallback: if no progress tracked, resume from beginning
500
+ textToSpeak = pausedSpeechRef.current.text;
501
+ optionsToUse = pausedSpeechRef.current.options || {};
502
+ } else {
503
+ // Nothing to resume
504
+ return;
505
+ }
506
+
507
+ // Prepare speak options
464
508
  const speakOptions = {
465
- ...pausedOptions,
466
- lipsyncLang: pausedOptions.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
509
+ ...optionsToUse,
510
+ lipsyncLang: optionsToUse.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
467
511
  };
468
512
 
469
513
  // Use speakText method which will set up the onSpeechEnd callback again
@@ -472,10 +516,8 @@ const TalkingHeadAvatar = forwardRef(({
472
516
  talkingHeadRef.current.setSlowdownRate(1.05);
473
517
  }
474
518
  // Call speakText which will handle everything including onSpeechEnd
475
- await speakText(pausedSpeech.text, speakOptions);
519
+ await speakText(textToSpeak, speakOptions);
476
520
  }
477
-
478
- // Don't clear pausedSpeechRef here - speakText will handle it
479
521
  }
480
522
  }, [resumeAudioContext, isPaused, speakText]);
481
523