@sage-rsc/talking-head-react 1.0.60 → 1.0.61
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 +406 -406
- package/package.json +1 -1
- package/src/components/TalkingHeadAvatar.jsx +56 -32
package/package.json
CHANGED
|
@@ -51,6 +51,7 @@ 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
|
|
54
55
|
const [isLoading, setIsLoading] = useState(true);
|
|
55
56
|
const [error, setError] = useState(null);
|
|
56
57
|
const [isReady, setIsReady] = useState(false);
|
|
@@ -277,7 +278,13 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
277
278
|
const speakText = useCallback(async (textToSpeak, options = {}) => {
|
|
278
279
|
if (talkingHeadRef.current && isReady) {
|
|
279
280
|
try {
|
|
280
|
-
//
|
|
281
|
+
// Stop any existing speech end polling
|
|
282
|
+
if (speechEndIntervalRef.current) {
|
|
283
|
+
clearInterval(speechEndIntervalRef.current);
|
|
284
|
+
speechEndIntervalRef.current = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Store speech for potential pause/resume
|
|
281
288
|
pausedSpeechRef.current = { text: textToSpeak, options };
|
|
282
289
|
setIsPaused(false);
|
|
283
290
|
|
|
@@ -304,13 +311,19 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
304
311
|
checkInterval = setInterval(() => {
|
|
305
312
|
checkCount++;
|
|
306
313
|
|
|
314
|
+
// Don't fire callback if paused
|
|
315
|
+
if (isPaused) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
// Safety timeout - call callback anyway after max time
|
|
308
320
|
if (checkCount > maxChecks) {
|
|
309
321
|
if (checkInterval) {
|
|
310
322
|
clearInterval(checkInterval);
|
|
311
323
|
checkInterval = null;
|
|
324
|
+
speechEndIntervalRef.current = null;
|
|
312
325
|
}
|
|
313
|
-
if (!callbackFired) {
|
|
326
|
+
if (!callbackFired && !isPaused) {
|
|
314
327
|
callbackFired = true;
|
|
315
328
|
try {
|
|
316
329
|
options.onSpeechEnd();
|
|
@@ -335,21 +348,23 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
335
348
|
audioPlaylistEmpty &&
|
|
336
349
|
talkingHead.isAudioPlaying === false;
|
|
337
350
|
|
|
338
|
-
if (isFinished && !callbackFired) {
|
|
351
|
+
if (isFinished && !callbackFired && !isPaused) {
|
|
339
352
|
// Double-check after a small delay to ensure it's really finished
|
|
340
353
|
setTimeout(() => {
|
|
341
|
-
// Re-check one more time to be sure
|
|
354
|
+
// Re-check one more time to be sure, and make sure we're still not paused
|
|
342
355
|
const finalCheck = talkingHead &&
|
|
356
|
+
!isPaused &&
|
|
343
357
|
talkingHead.isSpeaking === false &&
|
|
344
358
|
(!talkingHead.speechQueue || talkingHead.speechQueue.length === 0) &&
|
|
345
359
|
(!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
|
|
346
360
|
talkingHead.isAudioPlaying === false;
|
|
347
361
|
|
|
348
|
-
if (finalCheck && !callbackFired) {
|
|
362
|
+
if (finalCheck && !callbackFired && !isPaused) {
|
|
349
363
|
callbackFired = true;
|
|
350
364
|
if (checkInterval) {
|
|
351
365
|
clearInterval(checkInterval);
|
|
352
366
|
checkInterval = null;
|
|
367
|
+
speechEndIntervalRef.current = null;
|
|
353
368
|
}
|
|
354
369
|
try {
|
|
355
370
|
options.onSpeechEnd();
|
|
@@ -360,6 +375,9 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
360
375
|
}, 100);
|
|
361
376
|
}
|
|
362
377
|
}, 100); // Check every 100ms for better reliability
|
|
378
|
+
|
|
379
|
+
// Store interval ref so we can clear it on pause
|
|
380
|
+
speechEndIntervalRef.current = checkInterval;
|
|
363
381
|
}
|
|
364
382
|
|
|
365
383
|
if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {
|
|
@@ -405,13 +423,19 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
405
423
|
(talkingHead.audioPlaylist && talkingHead.audioPlaylist.length > 0) ||
|
|
406
424
|
(talkingHead.speechQueue && talkingHead.speechQueue.length > 0);
|
|
407
425
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
426
|
+
if (isCurrentlySpeaking) {
|
|
427
|
+
// Stop any onSpeechEnd polling to prevent callbacks from firing
|
|
428
|
+
if (speechEndIntervalRef.current) {
|
|
429
|
+
clearInterval(speechEndIntervalRef.current);
|
|
430
|
+
speechEndIntervalRef.current = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Clear speech queue to prevent next statements from playing
|
|
434
|
+
if (talkingHead.speechQueue) {
|
|
435
|
+
talkingHead.speechQueue.length = 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Pause the speaking
|
|
415
439
|
talkingHeadRef.current.pauseSpeaking();
|
|
416
440
|
setIsPaused(true);
|
|
417
441
|
}
|
|
@@ -419,34 +443,34 @@ const TalkingHeadAvatar = forwardRef(({
|
|
|
419
443
|
}, []);
|
|
420
444
|
|
|
421
445
|
const resumeSpeaking = useCallback(async () => {
|
|
422
|
-
if (talkingHeadRef.current && isPaused) {
|
|
446
|
+
if (talkingHeadRef.current && isPaused && pausedSpeechRef.current && pausedSpeechRef.current.text) {
|
|
447
|
+
const pausedSpeech = pausedSpeechRef.current;
|
|
448
|
+
const pausedOptions = pausedSpeech.options || {};
|
|
449
|
+
|
|
450
|
+
// Clear pause state first
|
|
423
451
|
setIsPaused(false);
|
|
424
452
|
|
|
425
453
|
// Resume audio context
|
|
426
454
|
await resumeAudioContext();
|
|
427
455
|
|
|
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);
|
|
456
|
+
// Re-speak the paused text with original options (including onSpeechEnd callback)
|
|
457
|
+
const speakOptions = {
|
|
458
|
+
...pausedOptions,
|
|
459
|
+
lipsyncLang: pausedOptions.lipsyncLang || defaultAvatarConfig.lipsyncLang || 'en'
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Use speakText method which will set up the onSpeechEnd callback again
|
|
463
|
+
if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {
|
|
464
|
+
if (talkingHeadRef.current.setSlowdownRate) {
|
|
465
|
+
talkingHeadRef.current.setSlowdownRate(1.05);
|
|
443
466
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
pausedSpeechRef.current = null;
|
|
467
|
+
// Call speakText which will handle everything including onSpeechEnd
|
|
468
|
+
await speakText(pausedSpeech.text, speakOptions);
|
|
447
469
|
}
|
|
470
|
+
|
|
471
|
+
// Don't clear pausedSpeechRef here - speakText will handle it
|
|
448
472
|
}
|
|
449
|
-
}, [resumeAudioContext, isPaused]);
|
|
473
|
+
}, [resumeAudioContext, isPaused, speakText]);
|
|
450
474
|
|
|
451
475
|
const setMood = useCallback((mood) => {
|
|
452
476
|
if (talkingHeadRef.current) {
|