@sage-rsc/talking-head-react 1.0.61 → 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/dist/index.cjs +2 -2
- package/dist/index.js +1064 -1044
- package/package.json +1 -1
- package/src/components/TalkingHeadAvatar.jsx +65 -16
package/package.json
CHANGED
|
@@ -52,11 +52,18 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
52
52
|
const showFullAvatarRef = useRef(showFullAvatar);
|
|
53
53
|
const pausedSpeechRef = useRef(null); // Track paused speech for resume
|
|
54
54
|
const speechEndIntervalRef = useRef(null); // Track onSpeechEnd polling interval
|
|
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
|
|
55
57
|
const [isLoading, setIsLoading] = useState(true);
|
|
56
58
|
const [error, setError] = useState(null);
|
|
57
59
|
const [isReady, setIsReady] = useState(false);
|
|
58
60
|
const [isPaused, setIsPaused] = useState(false);
|
|
59
61
|
|
|
62
|
+
// Keep ref in sync with state
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
isPausedRef.current = isPaused;
|
|
65
|
+
}, [isPaused]);
|
|
66
|
+
|
|
60
67
|
// Update ref when prop changes
|
|
61
68
|
useEffect(() => {
|
|
62
69
|
showFullAvatarRef.current = showFullAvatar;
|
|
@@ -286,7 +293,10 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
286
293
|
|
|
287
294
|
// Store speech for potential pause/resume
|
|
288
295
|
pausedSpeechRef.current = { text: textToSpeak, options };
|
|
296
|
+
// Reset progress tracking when starting new speech
|
|
297
|
+
speechProgressRef.current = { remainingText: null, originalText: null, options: null };
|
|
289
298
|
setIsPaused(false);
|
|
299
|
+
isPausedRef.current = false;
|
|
290
300
|
|
|
291
301
|
// Always resume audio context first (required for user interaction)
|
|
292
302
|
await resumeAudioContext();
|
|
@@ -311,8 +321,8 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
311
321
|
checkInterval = setInterval(() => {
|
|
312
322
|
checkCount++;
|
|
313
323
|
|
|
314
|
-
// Don't fire callback if paused
|
|
315
|
-
if (
|
|
324
|
+
// Don't fire callback if paused (check ref for current value)
|
|
325
|
+
if (isPausedRef.current) {
|
|
316
326
|
return;
|
|
317
327
|
}
|
|
318
328
|
|
|
@@ -323,7 +333,7 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
323
333
|
checkInterval = null;
|
|
324
334
|
speechEndIntervalRef.current = null;
|
|
325
335
|
}
|
|
326
|
-
if (!callbackFired && !
|
|
336
|
+
if (!callbackFired && !isPausedRef.current) {
|
|
327
337
|
callbackFired = true;
|
|
328
338
|
try {
|
|
329
339
|
options.onSpeechEnd();
|
|
@@ -348,18 +358,18 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
348
358
|
audioPlaylistEmpty &&
|
|
349
359
|
talkingHead.isAudioPlaying === false;
|
|
350
360
|
|
|
351
|
-
if (isFinished && !callbackFired && !
|
|
361
|
+
if (isFinished && !callbackFired && !isPausedRef.current) {
|
|
352
362
|
// Double-check after a small delay to ensure it's really finished
|
|
353
363
|
setTimeout(() => {
|
|
354
364
|
// Re-check one more time to be sure, and make sure we're still not paused
|
|
355
365
|
const finalCheck = talkingHead &&
|
|
356
|
-
!
|
|
366
|
+
!isPausedRef.current &&
|
|
357
367
|
talkingHead.isSpeaking === false &&
|
|
358
368
|
(!talkingHead.speechQueue || talkingHead.speechQueue.length === 0) &&
|
|
359
369
|
(!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
|
|
360
370
|
talkingHead.isAudioPlaying === false;
|
|
361
371
|
|
|
362
|
-
if (finalCheck && !callbackFired && !
|
|
372
|
+
if (finalCheck && !callbackFired && !isPausedRef.current) {
|
|
363
373
|
callbackFired = true;
|
|
364
374
|
if (checkInterval) {
|
|
365
375
|
clearInterval(checkInterval);
|
|
@@ -430,6 +440,29 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
430
440
|
speechEndIntervalRef.current = null;
|
|
431
441
|
}
|
|
432
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
|
+
|
|
433
466
|
// Clear speech queue to prevent next statements from playing
|
|
434
467
|
if (talkingHead.speechQueue) {
|
|
435
468
|
talkingHead.speechQueue.length = 0;
|
|
@@ -437,26 +470,44 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
437
470
|
|
|
438
471
|
// Pause the speaking
|
|
439
472
|
talkingHeadRef.current.pauseSpeaking();
|
|
473
|
+
isPausedRef.current = true;
|
|
440
474
|
setIsPaused(true);
|
|
441
475
|
}
|
|
442
476
|
}
|
|
443
477
|
}, []);
|
|
444
478
|
|
|
445
479
|
const resumeSpeaking = useCallback(async () => {
|
|
446
|
-
if (talkingHeadRef.current && isPaused
|
|
447
|
-
const pausedSpeech = pausedSpeechRef.current;
|
|
448
|
-
const pausedOptions = pausedSpeech.options || {};
|
|
449
|
-
|
|
480
|
+
if (talkingHeadRef.current && isPaused) {
|
|
450
481
|
// Clear pause state first
|
|
451
482
|
setIsPaused(false);
|
|
483
|
+
isPausedRef.current = false;
|
|
452
484
|
|
|
453
485
|
// Resume audio context
|
|
454
486
|
await resumeAudioContext();
|
|
455
487
|
|
|
456
|
-
//
|
|
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
|
|
457
508
|
const speakOptions = {
|
|
458
|
-
...
|
|
459
|
-
lipsyncLang:
|
|
509
|
+
...optionsToUse,
|
|
510
|
+
lipsyncLang: optionsToUse.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
|
|
460
511
|
};
|
|
461
512
|
|
|
462
513
|
// Use speakText method which will set up the onSpeechEnd callback again
|
|
@@ -465,10 +516,8 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
465
516
|
talkingHeadRef.current.setSlowdownRate(1.05);
|
|
466
517
|
}
|
|
467
518
|
// Call speakText which will handle everything including onSpeechEnd
|
|
468
|
-
await speakText(
|
|
519
|
+
await speakText(textToSpeak, speakOptions);
|
|
469
520
|
}
|
|
470
|
-
|
|
471
|
-
// Don't clear pausedSpeechRef here - speakText will handle it
|
|
472
521
|
}
|
|
473
522
|
}, [resumeAudioContext, isPaused, speakText]);
|
|
474
523
|
|