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

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.45",
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,51 @@ 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
+ // Timeout if audio doesn't start within reasonable time
312
+ if (waitForAudioStartCount * 50 > maxWaitForAudioStart) {
313
+ clearInterval(waitForAudioStart);
314
+ // Check if speech has actually started (isSpeaking should be true)
315
+ // If isSpeaking is false, the speech might have failed or completed very quickly
316
+ if (talkingHead && talkingHead.isSpeaking) {
317
+ // Still waiting for API, but assume it will start soon
318
+ audioStarted = true;
319
+ checkInterval = setInterval(checkSpeechFinished, 50);
320
+ } else {
321
+ // Speech never started or finished immediately, call callback
322
+ try {
323
+ options.onSpeechEnd();
324
+ } catch (e) {
325
+ console.error('Error in onSpeechEnd callback:', e);
326
+ }
327
+ }
328
+ }
329
+ }, 50);
293
330
 
294
331
  const checkSpeechFinished = () => {
295
332
  checkCount++;
@@ -307,31 +344,36 @@ const TalkingHeadAvatar = forwardRef(({
307
344
  return;
308
345
  }
309
346
 
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
-
347
+ // Only check if audio has started playing
348
+ if (!audioStarted) {
349
+ return;
350
+ }
351
+
352
+ // Check if speech has finished:
353
+ // 1. Not speaking OR speech queue is empty
354
+ // 2. Audio playlist is empty (no more audio to play)
355
+ // 3. Not currently playing audio
356
+ const isFinished = talkingHead &&
357
+ (!talkingHead.isSpeaking || talkingHead.isSpeaking === false) &&
358
+ (!talkingHead.audioPlaylist || talkingHead.audioPlaylist.length === 0) &&
359
+ (!talkingHead.isAudioPlaying || talkingHead.isAudioPlaying === false);
360
+
361
+ if (isFinished) {
315
362
  if (checkInterval) {
316
363
  clearInterval(checkInterval);
317
364
  checkInterval = null;
318
365
  }
319
366
 
320
- // Small delay to ensure everything is settled - reduced for faster transitions
367
+ // Small delay to ensure everything is settled
321
368
  setTimeout(() => {
322
369
  try {
323
370
  options.onSpeechEnd();
324
371
  } catch (e) {
325
372
  console.error('Error in onSpeechEnd callback:', e);
326
373
  }
327
- }, 10);
374
+ }, 50);
328
375
  }
329
376
  };
330
-
331
- // Start checking after a minimal delay (to allow speech to start)
332
- setTimeout(() => {
333
- checkInterval = setInterval(checkSpeechFinished, 50);
334
- }, 100);
335
377
  }
336
378
 
337
379
  if (talkingHeadRef.current.lipsync && Object.keys(talkingHeadRef.current.lipsync).length > 0) {