@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/dist/index.cjs +2 -2
- package/dist/index.js +468 -444
- package/package.json +1 -1
- package/src/components/TalkingHeadAvatar.jsx +74 -26
package/package.json
CHANGED
|
@@ -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
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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) {
|