@sage-rsc/talking-head-react 1.0.44 → 1.0.46

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.44",
3
+ "version": "1.0.46",
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",
@@ -282,14 +282,70 @@ const TalkingHeadAvatar = forwardRef(({
282
282
  if (options.onSpeechEnd && talkingHeadRef.current) {
283
283
  const talkingHead = talkingHeadRef.current;
284
284
 
285
- // Store original onAudioEnd if it exists
286
- const originalOnAudioEnd = talkingHead.onAudioEnd;
287
-
288
285
  // Set up a polling mechanism to detect when speech finishes
289
- // This is because onAudioEnd is only for streaming mode
286
+ // Wait for audio to actually start playing before checking if it's finished
290
287
  let checkInterval = null;
291
288
  let checkCount = 0;
292
- const maxChecks = 600; // 60 seconds max (50ms intervals for faster detection)
289
+ let audioStarted = false;
290
+ const maxChecks = 1200; // 60 seconds max (50ms intervals)
291
+ const maxWaitForAudioStart = 10000; // 10 seconds max to wait for audio to start
292
+
293
+ // First, wait for audio to actually start playing (API call completes and audio is added to playlist)
294
+ let waitForAudioStartCount = 0;
295
+ const waitForAudioStart = setInterval(() => {
296
+ waitForAudioStartCount++;
297
+
298
+ // Check if audio has started playing (audioPlaylist has items OR isAudioPlaying is true)
299
+ // Also check if isSpeaking is true (indicating API call has started processing)
300
+ if (talkingHead && talkingHead.isSpeaking && (
301
+ (talkingHead.audioPlaylist && talkingHead.audioPlaylist.length > 0) ||
302
+ (talkingHead.isAudioPlaying === true)
303
+ )) {
304
+ audioStarted = true;
305
+ clearInterval(waitForAudioStart);
306
+
307
+ // Now start checking if speech has finished
308
+ checkInterval = setInterval(checkSpeechFinished, 50);
309
+ }
310
+
311
+ // Also check if speech queue is empty and not speaking (meaning all sentences processed)
312
+ // This handles the case where text was split into sentences but all are processed
313
+ const speechQueueEmpty = !talkingHead.speechQueue || talkingHead.speechQueue.length === 0;
314
+
315
+ if (talkingHead && !talkingHead.isSpeaking && speechQueueEmpty &&
316
+ (!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
317
+ (!talkingHead.isAudioPlaying || talkingHead.isAudioPlaying === false)) {
318
+ // All speech has finished (all sentences processed and audio finished)
319
+ clearInterval(waitForAudioStart);
320
+ try {
321
+ options.onSpeechEnd();
322
+ } catch (e) {
323
+ console.error('Error in onSpeechEnd callback:', e);
324
+ }
325
+ return;
326
+ }
327
+
328
+ // Timeout if audio doesn't start within reasonable time
329
+ if (waitForAudioStartCount * 50 > maxWaitForAudioStart) {
330
+ clearInterval(waitForAudioStart);
331
+ // Check if speech has actually started (isSpeaking should be true)
332
+ // If isSpeaking is false, the speech might have failed or completed very quickly
333
+ if (talkingHead && talkingHead.isSpeaking) {
334
+ // Still waiting for API, but assume it will start soon
335
+ audioStarted = true;
336
+ checkInterval = setInterval(checkSpeechFinished, 50);
337
+ } else if (talkingHead && !talkingHead.isSpeaking && speechQueueEmpty &&
338
+ (!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
339
+ (!talkingHead.isAudioPlaying || talkingHead.isAudioPlaying === false)) {
340
+ // Speech never started or finished immediately, call callback
341
+ try {
342
+ options.onSpeechEnd();
343
+ } catch (e) {
344
+ console.error('Error in onSpeechEnd callback:', e);
345
+ }
346
+ }
347
+ }
348
+ }, 50);
293
349
 
294
350
  const checkSpeechFinished = () => {
295
351
  checkCount++;
@@ -307,31 +363,40 @@ const TalkingHeadAvatar = forwardRef(({
307
363
  return;
308
364
  }
309
365
 
310
- // Check if speech has finished (not speaking and audio playlist is empty)
311
- if (talkingHead && (!talkingHead.isSpeaking || talkingHead.isSpeaking === false) &&
312
- (!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
313
- (!talkingHead.isAudioPlaying || talkingHead.isAudioPlaying === false)) {
314
-
366
+ // Only check if audio has started playing
367
+ if (!audioStarted) {
368
+ return;
369
+ }
370
+
371
+ // Check if speech has finished:
372
+ // 1. Not speaking OR speech queue is empty
373
+ // 2. Audio playlist is empty (no more audio to play)
374
+ // 3. Not currently playing audio
375
+ // 4. Speech queue is empty (all sentences have been processed)
376
+ const speechQueueEmpty = !talkingHead.speechQueue || talkingHead.speechQueue.length === 0;
377
+
378
+ const isFinished = talkingHead &&
379
+ (!talkingHead.isSpeaking || talkingHead.isSpeaking === false) &&
380
+ (!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
381
+ (!talkingHead.isAudioPlaying || talkingHead.isAudioPlaying === false) &&
382
+ speechQueueEmpty;
383
+
384
+ if (isFinished) {
315
385
  if (checkInterval) {
316
386
  clearInterval(checkInterval);
317
387
  checkInterval = null;
318
388
  }
319
389
 
320
- // Small delay to ensure everything is settled - reduced for faster transitions
390
+ // Small delay to ensure everything is settled
321
391
  setTimeout(() => {
322
392
  try {
323
393
  options.onSpeechEnd();
324
394
  } catch (e) {
325
395
  console.error('Error in onSpeechEnd callback:', e);
326
396
  }
327
- }, 10);
397
+ }, 50);
328
398
  }
329
399
  };
330
-
331
- // Start checking after a minimal delay (to allow speech to start)
332
- setTimeout(() => {
333
- checkInterval = setInterval(checkSpeechFinished, 50);
334
- }, 100);
335
400
  }
336
401
 
337
402
  if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {