@sage-rsc/talking-head-react 1.1.3 → 1.1.6

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.3",
3
+ "version": "1.1.6",
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",
@@ -5407,18 +5407,33 @@ class TalkingHead {
5407
5407
  'Spine01': 'Spine1',
5408
5408
  'Spine02': 'Spine2',
5409
5409
  'Spine03': 'Spine2',
5410
+ 'Spine1': 'Spine1',
5411
+ 'Spine2': 'Spine2',
5412
+ 'Spine': 'Spine1',
5413
+
5414
+ // Head and neck
5415
+ 'Head': 'Head',
5416
+ 'Neck': 'Neck',
5417
+ 'Neck1': 'Neck',
5418
+ 'Neck2': 'Neck',
5410
5419
 
5411
5420
  // Left arm mapping
5412
5421
  'L_Upperarm': 'LeftArm',
5413
5422
  'L_Forearm': 'LeftForeArm',
5414
5423
  'L_Hand': 'LeftHand',
5415
5424
  'L_Shoulder': 'LeftShoulder',
5425
+ 'L_Clavicle': 'LeftShoulder',
5426
+ 'L_UpperArm': 'LeftArm',
5427
+ 'L_ForeArm': 'LeftForeArm',
5416
5428
  'L_Index1': 'LeftHandIndex1',
5417
5429
  'L_Index2': 'LeftHandIndex2',
5418
5430
  'L_Index3': 'LeftHandIndex3',
5419
5431
  'L_Middle1': 'LeftHandMiddle1',
5420
5432
  'L_Middle2': 'LeftHandMiddle2',
5421
5433
  'L_Middle3': 'LeftHandMiddle3',
5434
+ 'L_Mid1': 'LeftHandMiddle1',
5435
+ 'L_Mid2': 'LeftHandMiddle2',
5436
+ 'L_Mid3': 'LeftHandMiddle3',
5422
5437
  'L_Ring1': 'LeftHandRing1',
5423
5438
  'L_Ring2': 'LeftHandRing2',
5424
5439
  'L_Ring3': 'LeftHandRing3',
@@ -5434,12 +5449,18 @@ class TalkingHead {
5434
5449
  'R_Forearm': 'RightForeArm',
5435
5450
  'R_Hand': 'RightHand',
5436
5451
  'R_Shoulder': 'RightShoulder',
5452
+ 'R_Clavicle': 'RightShoulder',
5453
+ 'R_UpperArm': 'RightArm',
5454
+ 'R_ForeArm': 'RightForeArm',
5437
5455
  'R_Index1': 'RightHandIndex1',
5438
5456
  'R_Index2': 'RightHandIndex2',
5439
5457
  'R_Index3': 'RightHandIndex3',
5440
5458
  'R_Middle1': 'RightHandMiddle1',
5441
5459
  'R_Middle2': 'RightHandMiddle2',
5442
5460
  'R_Middle3': 'RightHandMiddle3',
5461
+ 'R_Mid1': 'RightHandMiddle1',
5462
+ 'R_Mid2': 'RightHandMiddle2',
5463
+ 'R_Mid3': 'RightHandMiddle3',
5443
5464
  'R_Ring1': 'RightHandRing1',
5444
5465
  'R_Ring2': 'RightHandRing2',
5445
5466
  'R_Ring3': 'RightHandRing3',
@@ -5454,9 +5475,18 @@ class TalkingHead {
5454
5475
  'L_Thigh': 'LeftUpLeg',
5455
5476
  'L_Calf': 'LeftLeg',
5456
5477
  'L_Foot': 'LeftFoot',
5478
+ 'L_UpLeg': 'LeftUpLeg',
5479
+ 'L_Leg': 'LeftLeg',
5457
5480
  'R_Thigh': 'RightUpLeg',
5458
5481
  'R_Calf': 'RightLeg',
5459
5482
  'R_Foot': 'RightFoot',
5483
+ 'R_UpLeg': 'RightUpLeg',
5484
+ 'R_Leg': 'RightLeg',
5485
+
5486
+ // Root/Hips
5487
+ 'Hips': 'Hips',
5488
+ 'Root': 'Hips',
5489
+ 'Pelvis': 'Hips',
5460
5490
  };
5461
5491
 
5462
5492
  // Try mapping
@@ -5504,8 +5534,8 @@ class TalkingHead {
5504
5534
  }
5505
5535
  }
5506
5536
 
5507
- // Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
5508
- const middleMatch = lowerNormalized.match(/^[rl]_middle(\d+)$/);
5537
+ // Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
5538
+ const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
5509
5539
  if (middleMatch) {
5510
5540
  const digit = middleMatch[1];
5511
5541
  const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
@@ -5535,8 +5565,12 @@ class TalkingHead {
5535
5565
  }
5536
5566
  }
5537
5567
 
5538
- // Pattern: R_UpperarmTwist01/02 -> ignore (twist bones)
5539
- if (lowerNormalized.includes('upperarmtwist') || lowerNormalized.includes('forearmtwist')) {
5568
+ // Pattern: R_UpperarmTwist01/02 -> ignore (twist bones and other extra bones)
5569
+ if (lowerNormalized.includes('upperarmtwist') ||
5570
+ lowerNormalized.includes('forearmtwist') ||
5571
+ lowerNormalized.includes('ribstwist') ||
5572
+ lowerNormalized.includes('breast') ||
5573
+ lowerNormalized.includes('twist')) {
5540
5574
  return null;
5541
5575
  }
5542
5576
 
@@ -5557,6 +5591,49 @@ class TalkingHead {
5557
5591
  return candidate;
5558
5592
  }
5559
5593
  }
5594
+
5595
+ // Pattern: R_Clavicle -> RightShoulder
5596
+ if (lowerNormalized.match(/^[rl]_clavicle/)) {
5597
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5598
+ const candidate = `${side}Shoulder`;
5599
+ if (availableBones.has(candidate)) {
5600
+ return candidate;
5601
+ }
5602
+ }
5603
+
5604
+ // Pattern: R_Shoulder -> RightShoulder
5605
+ if (lowerNormalized.match(/^[rl]_shoulder/)) {
5606
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5607
+ const candidate = `${side}Shoulder`;
5608
+ if (availableBones.has(candidate)) {
5609
+ return candidate;
5610
+ }
5611
+ }
5612
+
5613
+ // Pattern: Spine01/02/03 -> Spine1/Spine2
5614
+ if (lowerNormalized.match(/^spine0?(\d+)$/)) {
5615
+ const spineNum = lowerNormalized.match(/^spine0?(\d+)$/)[1];
5616
+ if (spineNum === '1') {
5617
+ if (availableBones.has('Spine1')) return 'Spine1';
5618
+ } else if (spineNum === '2' || spineNum === '3') {
5619
+ if (availableBones.has('Spine2')) return 'Spine2';
5620
+ }
5621
+ }
5622
+
5623
+ // Pattern: Neck1/2 -> Neck
5624
+ if (lowerNormalized.match(/^neck\d*$/)) {
5625
+ if (availableBones.has('Neck')) return 'Neck';
5626
+ }
5627
+
5628
+ // Pattern: Head -> Head
5629
+ if (lowerNormalized === 'head') {
5630
+ if (availableBones.has('Head')) return 'Head';
5631
+ }
5632
+
5633
+ // Pattern: Hips/Pelvis/Root -> Hips
5634
+ if (lowerNormalized.match(/^(hips|pelvis|root)$/)) {
5635
+ if (availableBones.has('Hips')) return 'Hips';
5636
+ }
5560
5637
 
5561
5638
  // Try case-insensitive exact match
5562
5639
  for (const boneName of availableBones) {
@@ -5564,6 +5641,18 @@ class TalkingHead {
5564
5641
  return boneName;
5565
5642
  }
5566
5643
  }
5644
+
5645
+ // Try partial match (contains)
5646
+ for (const boneName of availableBones) {
5647
+ const boneLower = boneName.toLowerCase();
5648
+ if (boneLower.includes(lowerNormalized) || lowerNormalized.includes(boneLower)) {
5649
+ // Prefer exact matches or very close matches
5650
+ if (boneLower === lowerNormalized ||
5651
+ boneLower.replace(/[^a-z0-9]/g, '') === lowerNormalized.replace(/[^a-z0-9]/g, '')) {
5652
+ return boneName;
5653
+ }
5654
+ }
5655
+ }
5567
5656
 
5568
5657
  return null; // No mapping found
5569
5658
  }
@@ -5579,14 +5668,25 @@ class TalkingHead {
5579
5668
  const validTracks = [];
5580
5669
  const missingBones = new Set();
5581
5670
  const mappedBones = new Map(); // Track mappings for logging
5671
+ const fbxBoneNames = new Set(); // Track all unique FBX bone names
5582
5672
 
5583
5673
  // Debug: Log available bones (first time only)
5584
5674
  if (!this._loggedAvailableBones) {
5585
- console.log('Available avatar bones:', Array.from(availableBones).sort().slice(0, 50).join(', '),
5586
- availableBones.size > 50 ? `... (${availableBones.size} total)` : '');
5675
+ console.log('Available avatar bones:', Array.from(availableBones).sort().join(', '));
5587
5676
  this._loggedAvailableBones = true;
5588
5677
  }
5589
5678
 
5679
+ // First pass: collect all unique FBX bone names
5680
+ clip.tracks.forEach(track => {
5681
+ const trackNameParts = track.name.split('.');
5682
+ const fbxBoneName = trackNameParts[0];
5683
+ fbxBoneNames.add(fbxBoneName);
5684
+ });
5685
+
5686
+ console.log(`\n=== FBX Animation "${clip.name}" Bone Analysis ===`);
5687
+ console.log('FBX bone names in animation:', Array.from(fbxBoneNames).sort().join(', '));
5688
+ console.log(`Total FBX bones: ${fbxBoneNames.size}, Avatar bones: ${availableBones.size}\n`);
5689
+
5590
5690
  clip.tracks.forEach(track => {
5591
5691
  // Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
5592
5692
  const trackNameParts = track.name.split('.');
@@ -5604,6 +5704,72 @@ class TalkingHead {
5604
5704
  const newTrack = track.clone();
5605
5705
  newTrack.name = newTrackName;
5606
5706
 
5707
+ // Fix rotations for arm/hand bones that might be inverted
5708
+ // If hands are folding behind instead of in front, we need to adjust rotations
5709
+ const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
5710
+ const isForearmBone = mappedBoneName.includes('ForeArm');
5711
+
5712
+ if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
5713
+ // For quaternion tracks, we might need to adjust the rotation
5714
+ // Check if this is a quaternion track
5715
+ if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
5716
+ // Quaternion format: [x, y, z, w] per keyframe
5717
+ // For arm bones, we might need to invert Y or Z rotation
5718
+ // Adjust quaternion values to fix hand position
5719
+ const numKeyframes = newTrack.times.length;
5720
+ for (let i = 0; i < numKeyframes; i++) {
5721
+ const baseIdx = i * 4;
5722
+ if (baseIdx + 3 < newTrack.values.length) {
5723
+ // Get quaternion values
5724
+ let x = newTrack.values[baseIdx];
5725
+ let y = newTrack.values[baseIdx + 1];
5726
+ let z = newTrack.values[baseIdx + 2];
5727
+ let w = newTrack.values[baseIdx + 3];
5728
+
5729
+ // For arms, adjust rotation to flip hands from back to front
5730
+ // This is a common fix for FBX animations with different coordinate systems
5731
+ if (isForearmBone || mappedBoneName.includes('Hand')) {
5732
+ // Rotate 180 degrees around X axis to flip hands from behind to in front
5733
+ // Create a 180-degree rotation around X axis
5734
+ const flipAngle = Math.PI;
5735
+ const flipX = Math.cos(flipAngle / 2); // w component
5736
+ const flipY = Math.sin(flipAngle / 2); // x component (axis X = 1,0,0)
5737
+
5738
+ // Multiply quaternions: q_result = q_flip * q_original
5739
+ // For rotation around X axis: q_flip = (sin(θ/2), 0, 0, cos(θ/2))
5740
+ const qw = flipX * w - flipY * x;
5741
+ const qx = flipX * x + flipY * w;
5742
+ const qy = flipX * y - flipY * z;
5743
+ const qz = flipX * z + flipY * y;
5744
+
5745
+ x = qx;
5746
+ y = qy;
5747
+ z = qz;
5748
+ w = qw;
5749
+ }
5750
+
5751
+ newTrack.values[baseIdx] = x;
5752
+ newTrack.values[baseIdx + 1] = y;
5753
+ newTrack.values[baseIdx + 2] = z;
5754
+ newTrack.values[baseIdx + 3] = w;
5755
+ }
5756
+ }
5757
+ } else if (property === 'rotation' && newTrack.values && newTrack.values.length >= 3) {
5758
+ // Euler rotation format: [x, y, z] per keyframe
5759
+ const numKeyframes = newTrack.times.length;
5760
+ for (let i = 0; i < numKeyframes; i++) {
5761
+ const baseIdx = i * 3;
5762
+ if (baseIdx + 2 < newTrack.values.length) {
5763
+ // For arm bones, adjust Y rotation to flip hands
5764
+ if (isForearmBone || mappedBoneName.includes('Hand')) {
5765
+ // Add 180 degrees (PI radians) to Y rotation
5766
+ newTrack.values[baseIdx + 1] += Math.PI;
5767
+ }
5768
+ }
5769
+ }
5770
+ }
5771
+ }
5772
+
5607
5773
  validTracks.push(newTrack);
5608
5774
 
5609
5775
  // Track mappings for logging
@@ -5615,22 +5781,64 @@ class TalkingHead {
5615
5781
  }
5616
5782
  });
5617
5783
 
5618
- // Log results
5784
+ // Log results with detailed mapping information
5785
+ console.log(`\n=== Mapping Results for "${clip.name}" ===`);
5786
+
5787
+ // Show all mapped bones
5619
5788
  if (mappedBones.size > 0) {
5620
- console.info(`FBX animation "${clip.name}": Mapped ${mappedBones.size} bone(s) to avatar skeleton:`,
5621
- Array.from(mappedBones.entries()).slice(0, 5).map(([from, to]) => `${from} → ${to}`).join(', '),
5622
- mappedBones.size > 5 ? '...' : '');
5789
+ console.info(`✓ Successfully mapped ${mappedBones.size} bone(s):`);
5790
+ Array.from(mappedBones.entries()).forEach(([from, to]) => {
5791
+ console.log(` ${from} ${to}`);
5792
+ });
5623
5793
  }
5624
5794
 
5795
+ // Show all missing bones with suggestions
5625
5796
  if (missingBones.size > 0) {
5626
- console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) that couldn't be mapped:`,
5627
- Array.from(missingBones).slice(0, 10).join(', '),
5628
- missingBones.size > 10 ? '...' : '');
5797
+ console.warn(`\n✗ Could not map ${missingBones.size} bone(s):`);
5798
+ Array.from(missingBones).sort().forEach(boneName => {
5799
+ console.log(` - ${boneName}`);
5800
+ // Try to suggest a potential mapping
5801
+ const normalized = boneName.replace(/^(CC_Base_|mixamorig)/i, '').replace(/^[RL]_/, (match) => match.toLowerCase());
5802
+ const suggestions = [];
5803
+ for (const avatarBone of availableBones) {
5804
+ const avatarLower = avatarBone.toLowerCase();
5805
+ const normalizedLower = normalized.toLowerCase();
5806
+ if (avatarLower.includes(normalizedLower) || normalizedLower.includes(avatarLower)) {
5807
+ suggestions.push(avatarBone);
5808
+ }
5809
+ }
5810
+ if (suggestions.length > 0) {
5811
+ console.log(` → Possible matches: ${suggestions.slice(0, 3).join(', ')}`);
5812
+ }
5813
+ });
5629
5814
  }
5630
5815
 
5631
- if (validTracks.length > 0) {
5632
- console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks (${mappedBones.size} mapped)`);
5633
- } else {
5816
+ // Show bones that matched directly (no mapping needed)
5817
+ const directMatches = new Set();
5818
+ fbxBoneNames.forEach(fbxBone => {
5819
+ if (availableBones.has(fbxBone) && !mappedBones.has(fbxBone)) {
5820
+ directMatches.add(fbxBone);
5821
+ }
5822
+ });
5823
+ if (directMatches.size > 0) {
5824
+ console.info(`\n✓ Direct matches (no mapping needed): ${directMatches.size} bone(s)`);
5825
+ Array.from(directMatches).sort().slice(0, 10).forEach(bone => {
5826
+ console.log(` - ${bone}`);
5827
+ });
5828
+ if (directMatches.size > 10) {
5829
+ console.log(` ... and ${directMatches.size - 10} more`);
5830
+ }
5831
+ }
5832
+
5833
+ console.log(`\n=== Summary ===`);
5834
+ console.log(`Total FBX tracks: ${clip.tracks.length}`);
5835
+ console.log(`Valid tracks: ${validTracks.length}`);
5836
+ console.log(`Mapped bones: ${mappedBones.size}`);
5837
+ console.log(`Direct matches: ${directMatches.size}`);
5838
+ console.log(`Missing bones: ${missingBones.size}`);
5839
+ console.log(`========================================\n`);
5840
+
5841
+ if (validTracks.length === 0) {
5634
5842
  console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
5635
5843
  }
5636
5844