@sage-rsc/talking-head-react 1.0.60 → 1.0.62
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 +1044 -1042
- package/package.json +1 -1
- package/src/components/TalkingHeadAvatar.jsx +63 -32
package/package.json
CHANGED
|
@@ -51,11 +51,18 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
51
51
|
const talkingHeadRef = useRef(null);
|
|
52
52
|
const showFullAvatarRef = useRef(showFullAvatar);
|
|
53
53
|
const pausedSpeechRef = useRef(null); // Track paused speech for resume
|
|
54
|
+
const speechEndIntervalRef = useRef(null); // Track onSpeechEnd polling interval
|
|
55
|
+
const isPausedRef = useRef(false); // Track pause state for interval checks
|
|
54
56
|
const [isLoading, setIsLoading] = useState(true);
|
|
55
57
|
const [error, setError] = useState(null);
|
|
56
58
|
const [isReady, setIsReady] = useState(false);
|
|
57
59
|
const [isPaused, setIsPaused] = useState(false);
|
|
58
60
|
|
|
61
|
+
// Keep ref in sync with state
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
isPausedRef.current = isPaused;
|
|
64
|
+
}, [isPaused]);
|
|
65
|
+
|
|
59
66
|
// Update ref when prop changes
|
|
60
67
|
useEffect(() => {
|
|
61
68
|
showFullAvatarRef.current = showFullAvatar;
|
|
@@ -277,7 +284,13 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
277
284
|
const speakText = useCallback(async (textToSpeak, options = {}) => {
|
|
278
285
|
if (talkingHeadRef.current && isReady) {
|
|
279
286
|
try {
|
|
280
|
-
//
|
|
287
|
+
// Stop any existing speech end polling
|
|
288
|
+
if (speechEndIntervalRef.current) {
|
|
289
|
+
clearInterval(speechEndIntervalRef.current);
|
|
290
|
+
speechEndIntervalRef.current = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Store speech for potential pause/resume
|
|
281
294
|
pausedSpeechRef.current = { text: textToSpeak, options };
|
|
282
295
|
setIsPaused(false);
|
|
283
296
|
|
|
@@ -304,13 +317,19 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
304
317
|
checkInterval = setInterval(() => {
|
|
305
318
|
checkCount++;
|
|
306
319
|
|
|
320
|
+
// Don't fire callback if paused (check ref for current value)
|
|
321
|
+
if (isPausedRef.current) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
307
325
|
// Safety timeout - call callback anyway after max time
|
|
308
326
|
if (checkCount > maxChecks) {
|
|
309
327
|
if (checkInterval) {
|
|
310
328
|
clearInterval(checkInterval);
|
|
311
329
|
checkInterval = null;
|
|
330
|
+
speechEndIntervalRef.current = null;
|
|
312
331
|
}
|
|
313
|
-
if (!callbackFired) {
|
|
332
|
+
if (!callbackFired && !isPausedRef.current) {
|
|
314
333
|
callbackFired = true;
|
|
315
334
|
try {
|
|
316
335
|
options.onSpeechEnd();
|
|
@@ -335,21 +354,23 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
335
354
|
audioPlaylistEmpty &&
|
|
336
355
|
talkingHead.isAudioPlaying === false;
|
|
337
356
|
|
|
338
|
-
if (isFinished && !callbackFired) {
|
|
357
|
+
if (isFinished && !callbackFired && !isPausedRef.current) {
|
|
339
358
|
// Double-check after a small delay to ensure it's really finished
|
|
340
359
|
setTimeout(() => {
|
|
341
|
-
// Re-check one more time to be sure
|
|
360
|
+
// Re-check one more time to be sure, and make sure we're still not paused
|
|
342
361
|
const finalCheck = talkingHead &&
|
|
362
|
+
!isPausedRef.current &&
|
|
343
363
|
talkingHead.isSpeaking === false &&
|
|
344
364
|
(!talkingHead.speechQueue || talkingHead.speechQueue.length === 0) &&
|
|
345
365
|
(!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
|
|
346
366
|
talkingHead.isAudioPlaying === false;
|
|
347
367
|
|
|
348
|
-
if (finalCheck && !callbackFired) {
|
|
368
|
+
if (finalCheck && !callbackFired && !isPausedRef.current) {
|
|
349
369
|
callbackFired = true;
|
|
350
370
|
if (checkInterval) {
|
|
351
371
|
clearInterval(checkInterval);
|
|
352
372
|
checkInterval = null;
|
|
373
|
+
speechEndIntervalRef.current = null;
|
|
353
374
|
}
|
|
354
375
|
try {
|
|
355
376
|
options.onSpeechEnd();
|
|
@@ -360,6 +381,9 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
360
381
|
}, 100);
|
|
361
382
|
}
|
|
362
383
|
}, 100); // Check every 100ms for better reliability
|
|
384
|
+
|
|
385
|
+
// Store interval ref so we can clear it on pause
|
|
386
|
+
speechEndIntervalRef.current = checkInterval;
|
|
363
387
|
}
|
|
364
388
|
|
|
365
389
|
if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {
|
|
@@ -405,48 +429,55 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
405
429
|
(talkingHead.audioPlaylist && talkingHead.audioPlaylist.length > 0) ||
|
|
406
430
|
(talkingHead.speechQueue && talkingHead.speechQueue.length > 0);
|
|
407
431
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
432
|
+
if (isCurrentlySpeaking) {
|
|
433
|
+
// Stop any onSpeechEnd polling to prevent callbacks from firing
|
|
434
|
+
if (speechEndIntervalRef.current) {
|
|
435
|
+
clearInterval(speechEndIntervalRef.current);
|
|
436
|
+
speechEndIntervalRef.current = null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Clear speech queue to prevent next statements from playing
|
|
440
|
+
if (talkingHead.speechQueue) {
|
|
441
|
+
talkingHead.speechQueue.length = 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Pause the speaking
|
|
415
445
|
talkingHeadRef.current.pauseSpeaking();
|
|
446
|
+
isPausedRef.current = true;
|
|
416
447
|
setIsPaused(true);
|
|
417
448
|
}
|
|
418
449
|
}
|
|
419
450
|
}, []);
|
|
420
451
|
|
|
421
452
|
const resumeSpeaking = useCallback(async () => {
|
|
422
|
-
if (talkingHeadRef.current && isPaused) {
|
|
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
|
|
423
458
|
setIsPaused(false);
|
|
424
459
|
|
|
425
460
|
// Resume audio context
|
|
426
461
|
await resumeAudioContext();
|
|
427
462
|
|
|
428
|
-
// Re-speak the paused text
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {
|
|
439
|
-
if (talkingHeadRef.current.setSlowdownRate) {
|
|
440
|
-
talkingHeadRef.current.setSlowdownRate(1.05);
|
|
441
|
-
}
|
|
442
|
-
talkingHeadRef.current.speakText(pausedSpeech.text, speakOptions);
|
|
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);
|
|
443
473
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
pausedSpeechRef.current = null;
|
|
474
|
+
// Call speakText which will handle everything including onSpeechEnd
|
|
475
|
+
await speakText(pausedSpeech.text, speakOptions);
|
|
447
476
|
}
|
|
477
|
+
|
|
478
|
+
// Don't clear pausedSpeechRef here - speakText will handle it
|
|
448
479
|
}
|
|
449
|
-
}, [resumeAudioContext, isPaused]);
|
|
480
|
+
}, [resumeAudioContext, isPaused, speakText]);
|
|
450
481
|
|
|
451
482
|
const setMood = useCallback((mood) => {
|
|
452
483
|
if (talkingHeadRef.current) {
|