@sage-rsc/talking-head-react 1.1.8 → 1.2.0

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.1.8",
3
+ "version": "1.2.0",
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",
@@ -5489,15 +5489,65 @@ class TalkingHead {
5489
5489
  return fbxBoneName;
5490
5490
  }
5491
5491
 
5492
- // Remove mixamorig prefix if present
5493
- let normalized = fbxBoneName.replace(/^mixamorig/i, '');
5492
+ // Remove common prefixes (mixamorig, CC_Base_, etc.)
5493
+ let normalized = fbxBoneName
5494
+ .replace(/^mixamorig/i, '')
5495
+ .replace(/^CC_Base_/i, '')
5496
+ .replace(/^RPM_/i, '');
5497
+
5494
5498
  if (availableBones.has(normalized)) {
5495
5499
  return normalized;
5496
5500
  }
5497
5501
 
5498
- // Common bone name mappings for Ready Player Me / Mixamo
5502
+ // Pattern-based matching for Ready Player Me / Mixamo
5503
+ const lowerNormalized = normalized.toLowerCase();
5504
+
5505
+ // Arm bones - pattern matching
5506
+ if (lowerNormalized.includes('left') && lowerNormalized.includes('arm')) {
5507
+ if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
5508
+ if (availableBones.has('LeftForeArm')) return 'LeftForeArm';
5509
+ if (availableBones.has('LeftForearm')) return 'LeftForearm';
5510
+ } else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
5511
+ if (availableBones.has('LeftArm')) return 'LeftArm';
5512
+ }
5513
+ }
5514
+
5515
+ if (lowerNormalized.includes('right') && lowerNormalized.includes('arm')) {
5516
+ if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
5517
+ if (availableBones.has('RightForeArm')) return 'RightForeArm';
5518
+ if (availableBones.has('RightForearm')) return 'RightForearm';
5519
+ } else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
5520
+ if (availableBones.has('RightArm')) return 'RightArm';
5521
+ }
5522
+ }
5523
+
5524
+ // Hand bones
5525
+ if (lowerNormalized.includes('left') && lowerNormalized.includes('hand') &&
5526
+ !lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
5527
+ !lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
5528
+ !lowerNormalized.includes('pinky')) {
5529
+ if (availableBones.has('LeftHand')) return 'LeftHand';
5530
+ }
5531
+
5532
+ if (lowerNormalized.includes('right') && lowerNormalized.includes('hand') &&
5533
+ !lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
5534
+ !lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
5535
+ !lowerNormalized.includes('pinky')) {
5536
+ if (availableBones.has('RightHand')) return 'RightHand';
5537
+ }
5538
+
5539
+ // Shoulder bones
5540
+ if (lowerNormalized.includes('left') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
5541
+ if (availableBones.has('LeftShoulder')) return 'LeftShoulder';
5542
+ }
5543
+
5544
+ if (lowerNormalized.includes('right') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
5545
+ if (availableBones.has('RightShoulder')) return 'RightShoulder';
5546
+ }
5547
+
5548
+ // Common bone name mappings
5499
5549
  const mappings = {
5500
- // Arm bones - handle common variations
5550
+ // Arm bones - exact matches
5501
5551
  'LeftArm': 'LeftArm',
5502
5552
  'leftArm': 'LeftArm',
5503
5553
  'LEFTARM': 'LeftArm',
@@ -5546,11 +5596,26 @@ class TalkingHead {
5546
5596
 
5547
5597
  // Try case-insensitive match
5548
5598
  for (const boneName of availableBones) {
5549
- if (boneName.toLowerCase() === normalized.toLowerCase()) {
5599
+ if (boneName.toLowerCase() === lowerNormalized) {
5550
5600
  return boneName;
5551
5601
  }
5552
5602
  }
5553
5603
 
5604
+ // Try partial match (contains)
5605
+ for (const boneName of availableBones) {
5606
+ const boneLower = boneName.toLowerCase();
5607
+ // Match if normalized contains key parts of bone name
5608
+ if ((lowerNormalized.includes('left') && boneLower.includes('left')) ||
5609
+ (lowerNormalized.includes('right') && boneLower.includes('right'))) {
5610
+ if ((lowerNormalized.includes('arm') && boneLower.includes('arm') && !boneLower.includes('fore')) ||
5611
+ (lowerNormalized.includes('forearm') && boneLower.includes('forearm')) ||
5612
+ (lowerNormalized.includes('hand') && boneLower.includes('hand') && !boneLower.includes('index') && !boneLower.includes('thumb')) ||
5613
+ (lowerNormalized.includes('shoulder') && boneLower.includes('shoulder'))) {
5614
+ return boneName;
5615
+ }
5616
+ }
5617
+ }
5618
+
5554
5619
  return null; // No mapping found
5555
5620
  };
5556
5621
 
@@ -5561,8 +5626,23 @@ class TalkingHead {
5561
5626
  fbxBoneNames.add(trackParts[0]);
5562
5627
  });
5563
5628
 
5564
- console.log('Ready Player Me FBX bone names:', Array.from(fbxBoneNames).sort().join(', '));
5629
+ console.log('=== Ready Player Me Animation Bone Analysis ===');
5630
+ console.log('FBX bone names:', Array.from(fbxBoneNames).sort().join(', '));
5565
5631
  console.log('Avatar skeleton bone names:', Array.from(availableBones).sort().join(', '));
5632
+
5633
+ // Check for arm bones specifically
5634
+ const fbxArmBones = Array.from(fbxBoneNames).filter(b =>
5635
+ b.toLowerCase().includes('arm') ||
5636
+ b.toLowerCase().includes('hand') ||
5637
+ b.toLowerCase().includes('shoulder')
5638
+ );
5639
+ const avatarArmBones = Array.from(availableBones).filter(b =>
5640
+ b.includes('Arm') ||
5641
+ b.includes('Hand') ||
5642
+ b.includes('Shoulder')
5643
+ );
5644
+ console.log('FBX arm/hand/shoulder bones:', fbxArmBones.sort().join(', '));
5645
+ console.log('Avatar arm/hand/shoulder bones:', avatarArmBones.sort().join(', '));
5566
5646
 
5567
5647
  // Filter and map animation tracks
5568
5648
  const mappedTracks = [];
@@ -5582,6 +5662,54 @@ class TalkingHead {
5582
5662
  const newTrackName = `${mappedBoneName}.${property}`;
5583
5663
  const newTrack = track.clone();
5584
5664
  newTrack.name = newTrackName;
5665
+
5666
+ // Fix rotations for Ready Player Me arm bones (coordinate system correction)
5667
+ // Ready Player Me uses a different coordinate system, so we need to adjust rotations
5668
+ const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
5669
+ const isLeftSide = mappedBoneName.includes('Left');
5670
+ const isRightSide = mappedBoneName.includes('Right');
5671
+ const isForearm = mappedBoneName.includes('ForeArm') || mappedBoneName.includes('Forearm');
5672
+ const isHand = mappedBoneName.includes('Hand') && !mappedBoneName.includes('Index') &&
5673
+ !mappedBoneName.includes('Thumb') && !mappedBoneName.includes('Middle') &&
5674
+ !mappedBoneName.includes('Ring') && !mappedBoneName.includes('Pinky');
5675
+
5676
+ if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
5677
+ if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
5678
+ // Adjust quaternion rotations for Ready Player Me coordinate system
5679
+ const numKeyframes = newTrack.times.length;
5680
+ for (let i = 0; i < numKeyframes; i++) {
5681
+ const baseIdx = i * 4;
5682
+ if (baseIdx + 3 < newTrack.values.length) {
5683
+ let x = newTrack.values[baseIdx];
5684
+ let y = newTrack.values[baseIdx + 1];
5685
+ let z = newTrack.values[baseIdx + 2];
5686
+ let w = newTrack.values[baseIdx + 3];
5687
+
5688
+ const q = new THREE.Quaternion(x, y, z, w);
5689
+
5690
+ // For Ready Player Me, hands may need coordinate system correction
5691
+ // Hands folding behind suggests we need to rotate around Y axis
5692
+ if (isHand) {
5693
+ // Rotate 180 degrees around Y axis to fix hands folding behind
5694
+ const flipY = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
5695
+ // Apply rotation: q_result = q_original * q_flip (post-multiply)
5696
+ q.multiply(flipY);
5697
+ } else if (isForearm) {
5698
+ // For forearms, try a smaller correction - rotate 90 degrees around Z axis
5699
+ // This might help with the twisting
5700
+ const adjustZ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
5701
+ q.multiply(adjustZ);
5702
+ }
5703
+
5704
+ newTrack.values[baseIdx] = q.x;
5705
+ newTrack.values[baseIdx + 1] = q.y;
5706
+ newTrack.values[baseIdx + 2] = q.z;
5707
+ newTrack.values[baseIdx + 3] = q.w;
5708
+ }
5709
+ }
5710
+ }
5711
+ }
5712
+
5585
5713
  mappedTracks.push(newTrack);
5586
5714
 
5587
5715
  // Store mapping for logging
@@ -5606,10 +5734,23 @@ class TalkingHead {
5606
5734
  // Use mapped tracks if we have any, otherwise use original
5607
5735
  if (mappedTracks.length > 0) {
5608
5736
  anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
5737
+ console.log(`✓ Created animation with ${mappedTracks.length} mapped tracks (from ${anim.tracks.length} original tracks)`);
5609
5738
  if (boneNameMap.size > 0) {
5610
- console.log(`Mapped ${boneNameMap.size} bone(s) for Ready Player Me animation:`,
5611
- Array.from(boneNameMap.entries()).slice(0, 5).map(([from, to]) => `${from}→${to}`).join(', '));
5739
+ console.log(`✓ Mapped ${boneNameMap.size} bone(s):`,
5740
+ Array.from(boneNameMap.entries()).map(([from, to]) => `${from}→${to}`).join(', '));
5612
5741
  }
5742
+
5743
+ // Check if arm bones were mapped
5744
+ const mappedArmBones = Array.from(boneNameMap.values()).filter(b =>
5745
+ b.includes('Arm') || b.includes('Hand') || b.includes('Shoulder')
5746
+ );
5747
+ if (mappedArmBones.length > 0) {
5748
+ console.log(`✓ Arm bones mapped: ${mappedArmBones.join(', ')}`);
5749
+ } else {
5750
+ console.warn('⚠️ No arm bones were mapped! This may cause arm rigging issues.');
5751
+ }
5752
+ } else {
5753
+ console.error('❌ No tracks could be mapped! Animation may not work correctly.');
5613
5754
  }
5614
5755
 
5615
5756
  // Rename and scale Mixamo tracks, create a pose