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

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.64",
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 && pausedSpeechRef.current) {
446
+ // Extract text from remaining queue items
447
+ const remainingParts = talkingHead.speechQueue
448
+ .filter(item => item && item.text) // Only get items with text
449
+ .map(item => item.text)
450
+ .join(' ');
451
+
452
+ if (remainingParts && remainingParts.trim()) {
453
+ remainingText = remainingParts.trim();
454
+ }
455
+ }
456
+
457
+ // Always save progress for resume (even if no remaining text, we'll resume from beginning)
458
+ if (pausedSpeechRef.current) {
459
+ speechProgressRef.current = {
460
+ remainingText: remainingText || null,
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,34 +477,55 @@ 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
-
457
- // Clear pause state first
480
+ if (!talkingHeadRef.current || !isPaused) {
481
+ return;
482
+ }
483
+
484
+ // Determine what text to speak - use remaining text if available, otherwise full text
485
+ let textToSpeak = '';
486
+ let optionsToUse = {};
487
+
488
+ if (speechProgressRef.current && speechProgressRef.current.remainingText) {
489
+ // Resume from where we paused - speak only the remaining text
490
+ textToSpeak = speechProgressRef.current.remainingText;
491
+ optionsToUse = speechProgressRef.current.options || {};
492
+ // Clear progress after using it
493
+ speechProgressRef.current = { remainingText: null, originalText: null, options: null };
494
+ } else if (pausedSpeechRef.current && pausedSpeechRef.current.text) {
495
+ // Fallback: if no progress tracked, resume from beginning
496
+ textToSpeak = pausedSpeechRef.current.text;
497
+ optionsToUse = pausedSpeechRef.current.options || {};
498
+ } else {
499
+ // Nothing to resume
500
+ console.warn('Resume called but no paused speech found');
458
501
  setIsPaused(false);
459
-
460
- // Resume audio context
461
- await resumeAudioContext();
462
-
463
- // Re-speak the paused text with original options (including onSpeechEnd callback)
464
- const speakOptions = {
465
- ...pausedOptions,
466
- lipsyncLang: pausedOptions.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
467
- };
468
-
469
- // Use speakText method which will set up the onSpeechEnd callback again
470
- if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {
471
- if (talkingHeadRef.current.setSlowdownRate) {
472
- talkingHeadRef.current.setSlowdownRate(1.05);
473
- }
474
- // Call speakText which will handle everything including onSpeechEnd
475
- await speakText(pausedSpeech.text, speakOptions);
476
- }
477
-
478
- // Don't clear pausedSpeechRef here - speakText will handle it
502
+ isPausedRef.current = false;
503
+ return;
504
+ }
505
+
506
+ // Clear pause state before speaking
507
+ setIsPaused(false);
508
+ isPausedRef.current = false;
509
+
510
+ // Resume audio context
511
+ await resumeAudioContext();
512
+
513
+ // Prepare speak options
514
+ const speakOptions = {
515
+ ...optionsToUse,
516
+ lipsyncLang: optionsToUse.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
517
+ };
518
+
519
+ // Use speakText method which will set up the onSpeechEnd callback again
520
+ try {
521
+ await speakText(textToSpeak, speakOptions);
522
+ } catch (error) {
523
+ console.error('Error resuming speech:', error);
524
+ // Reset pause state on error
525
+ setIsPaused(false);
526
+ isPausedRef.current = false;
479
527
  }
480
- }, [resumeAudioContext, isPaused, speakText]);
528
+ }, [resumeAudioContext, isPaused, speakText, defaultAvatarConfig]);
481
529
 
482
530
  const setMood = useCallback((mood) => {
483
531
  if (talkingHeadRef.current) {