@sage-rsc/talking-head-react 1.5.9 → 1.5.11

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.5.9",
3
+ "version": "1.5.11",
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",
@@ -75,6 +75,8 @@ const SimpleTalkingAvatar = forwardRef(({
75
75
  const [isPaused, setIsPaused] = useState(false);
76
76
  const [loadedAnimations, setLoadedAnimations] = useState(animations);
77
77
  const idleAnimationIntervalRef = useRef(null);
78
+ const isSpeakingRef = useRef(false);
79
+ const currentAnimationGroupRef = useRef(null);
78
80
 
79
81
  // Keep ref in sync with state
80
82
  useEffect(() => {
@@ -321,8 +323,8 @@ const SimpleTalkingAvatar = forwardRef(({
321
323
  return null;
322
324
  }, [loadedAnimations, avatarBody]);
323
325
 
324
- // Helper function to play random animation from a group
325
- const playRandomAnimation = useCallback((groupName, disablePositionLock = false) => {
326
+ // Helper function to play random animation from a group with smooth transitions
327
+ const playRandomAnimation = useCallback((groupName, disablePositionLock = false, onFinished = null) => {
326
328
  if (!talkingHeadRef.current) {
327
329
  console.warn('TalkingHead not initialized yet');
328
330
  return null;
@@ -331,8 +333,23 @@ const SimpleTalkingAvatar = forwardRef(({
331
333
  const animationPath = getRandomAnimation(groupName);
332
334
  if (animationPath) {
333
335
  try {
334
- talkingHeadRef.current.playAnimation(animationPath, null, 10, 0, 0.01, disablePositionLock);
336
+ // Create callback that will play next animation if still speaking
337
+ const animationFinishedCallback = () => {
338
+ // If still speaking and same group, play next random animation
339
+ if (isSpeakingRef.current && currentAnimationGroupRef.current === groupName) {
340
+ // Small delay for smooth transition
341
+ setTimeout(() => {
342
+ playRandomAnimation(groupName, disablePositionLock, onFinished);
343
+ }, 100);
344
+ } else if (onFinished) {
345
+ onFinished();
346
+ }
347
+ };
348
+
349
+ // Play animation with callback - fade transitions are handled in playAnimation
350
+ talkingHeadRef.current.playAnimation(animationPath, null, 10, 0, 0.01, disablePositionLock, animationFinishedCallback);
335
351
  console.log(`✅ Playing random animation from "${groupName}" group:`, animationPath);
352
+
336
353
  return animationPath;
337
354
  } catch (error) {
338
355
  console.error(`❌ Failed to play random animation from "${groupName}" group:`, error);
@@ -365,6 +382,11 @@ const SimpleTalkingAvatar = forwardRef(({
365
382
  if (animationGroup && !options.skipAnimation) {
366
383
  console.log(`🎬 Attempting to play animation from group: "${animationGroup}"`);
367
384
  console.log(`📊 Current avatarBody: "${avatarBody}", loadedAnimations:`, loadedAnimations);
385
+
386
+ // Mark as speaking and track animation group for continuous playback
387
+ isSpeakingRef.current = true;
388
+ currentAnimationGroupRef.current = animationGroup;
389
+
368
390
  playRandomAnimation(animationGroup);
369
391
  } else {
370
392
  console.log(`⏭️ Skipping animation (group: ${animationGroup}, skipAnimation: ${options.skipAnimation})`);
@@ -400,6 +422,10 @@ const SimpleTalkingAvatar = forwardRef(({
400
422
  speechEndIntervalRef.current = null;
401
423
  }
402
424
 
425
+ // Stop continuous animation playback
426
+ isSpeakingRef.current = false;
427
+ currentAnimationGroupRef.current = null;
428
+
403
429
  // Call user's onSpeechEnd callback
404
430
  if (options.onSpeechEnd) {
405
431
  options.onSpeechEnd();
@@ -538,6 +564,11 @@ const SimpleTalkingAvatar = forwardRef(({
538
564
  clearInterval(speechEndIntervalRef.current);
539
565
  speechEndIntervalRef.current = null;
540
566
  }
567
+
568
+ // Stop continuous animation playback
569
+ isSpeakingRef.current = false;
570
+ currentAnimationGroupRef.current = null;
571
+
541
572
  setIsPaused(false);
542
573
  isPausedRef.current = false;
543
574
  }
@@ -1609,8 +1609,8 @@ class TalkingHead {
1609
1609
  }
1610
1610
  }
1611
1611
 
1612
- // Apply shoulder adjustment to lower shoulders
1613
- this.applyShoulderAdjustment();
1612
+ // TEMPORARILY DISABLED - No shoulder adjustments
1613
+ // this.applyShoulderAdjustment();
1614
1614
  }
1615
1615
 
1616
1616
  /**
@@ -1618,6 +1618,9 @@ class TalkingHead {
1618
1618
  * This is called from updatePoseBase for pose-based animations
1619
1619
  */
1620
1620
  applyShoulderAdjustment() {
1621
+ // TEMPORARILY DISABLED - No arm/shoulder adjustments
1622
+ return;
1623
+
1621
1624
  const tempEuler = new THREE.Euler();
1622
1625
  const targetX = 0.6; // Target X rotation for relaxed shoulders - lowered significantly
1623
1626
  const maxX = 0.7; // Maximum X rotation - lowered significantly
@@ -5464,7 +5467,7 @@ class TalkingHead {
5464
5467
  * @param {number} [scale=0.01] Position scale factor
5465
5468
  */
5466
5469
 
5467
- async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false) {
5470
+ async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false, onFinished=null) {
5468
5471
  if ( !this.armature ) return;
5469
5472
 
5470
5473
  // Track whether position was locked for this animation
@@ -5502,7 +5505,20 @@ class TalkingHead {
5502
5505
  } else {
5503
5506
  console.log('Using existing mixer for FBX animation, preserving morph targets');
5504
5507
  }
5505
- this.mixer.addEventListener( 'finished', this.stopAnimation.bind(this), { once: true });
5508
+
5509
+ // Store callback for when animation finishes
5510
+ this.animationFinishedCallback = onFinished;
5511
+
5512
+ // Create handler that calls callback before stopping
5513
+ const finishedHandler = () => {
5514
+ if (this.animationFinishedCallback) {
5515
+ this.animationFinishedCallback();
5516
+ this.animationFinishedCallback = null;
5517
+ }
5518
+ this.stopAnimation();
5519
+ };
5520
+
5521
+ this.mixer.addEventListener( 'finished', finishedHandler, { once: true });
5506
5522
 
5507
5523
  // Play action with error handling
5508
5524
  const repeat = Math.ceil(dur / item.clip.duration);
@@ -5510,6 +5526,23 @@ class TalkingHead {
5510
5526
  action.setLoop( THREE.LoopRepeat, repeat );
5511
5527
  action.clampWhenFinished = true;
5512
5528
 
5529
+ // Fade out previous action smoothly if one exists and is running
5530
+ if (this.currentFBXAction && this.currentFBXAction.isRunning()) {
5531
+ this.currentFBXAction.fadeOut(0.3);
5532
+ // Small delay for smooth transition
5533
+ setTimeout(() => {
5534
+ this.currentFBXAction = action;
5535
+ try {
5536
+ action.fadeIn(0.5).play();
5537
+ console.log('FBX animation started successfully (with fade transition):', url);
5538
+ } catch (error) {
5539
+ console.warn('FBX animation failed to start:', error);
5540
+ this.stopAnimation();
5541
+ }
5542
+ }, 300);
5543
+ return;
5544
+ }
5545
+
5513
5546
  // Store the current FBX action for proper cleanup
5514
5547
  this.currentFBXAction = action;
5515
5548