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

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.7",
3
+ "version": "1.1.8",
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",
@@ -5471,6 +5471,147 @@ class TalkingHead {
5471
5471
  if ( fbx && fbx.animations && fbx.animations[ndx] ) {
5472
5472
  let anim = fbx.animations[ndx];
5473
5473
 
5474
+ // Get available bone names from avatar skeleton for mapping
5475
+ const availableBones = new Set();
5476
+ if (this.armature) {
5477
+ this.armature.traverse((child) => {
5478
+ if (child.isBone || child.type === 'Bone') {
5479
+ availableBones.add(child.name);
5480
+ }
5481
+ });
5482
+ }
5483
+
5484
+ // Map bone names from FBX to avatar skeleton
5485
+ const boneNameMap = new Map();
5486
+ const mapBoneName = (fbxBoneName) => {
5487
+ // Direct match
5488
+ if (availableBones.has(fbxBoneName)) {
5489
+ return fbxBoneName;
5490
+ }
5491
+
5492
+ // Remove mixamorig prefix if present
5493
+ let normalized = fbxBoneName.replace(/^mixamorig/i, '');
5494
+ if (availableBones.has(normalized)) {
5495
+ return normalized;
5496
+ }
5497
+
5498
+ // Common bone name mappings for Ready Player Me / Mixamo
5499
+ const mappings = {
5500
+ // Arm bones - handle common variations
5501
+ 'LeftArm': 'LeftArm',
5502
+ 'leftArm': 'LeftArm',
5503
+ 'LEFTARM': 'LeftArm',
5504
+ 'RightArm': 'RightArm',
5505
+ 'rightArm': 'RightArm',
5506
+ 'RIGHTARM': 'RightArm',
5507
+ 'LeftForeArm': 'LeftForeArm',
5508
+ 'leftForeArm': 'LeftForeArm',
5509
+ 'leftForearm': 'LeftForeArm',
5510
+ 'LeftForearm': 'LeftForeArm',
5511
+ 'RightForeArm': 'RightForeArm',
5512
+ 'rightForeArm': 'RightForeArm',
5513
+ 'rightForearm': 'RightForeArm',
5514
+ 'RightForearm': 'RightForeArm',
5515
+ 'LeftHand': 'LeftHand',
5516
+ 'leftHand': 'LeftHand',
5517
+ 'RightHand': 'RightHand',
5518
+ 'rightHand': 'RightHand',
5519
+ 'LeftShoulder': 'LeftShoulder',
5520
+ 'leftShoulder': 'LeftShoulder',
5521
+ 'RightShoulder': 'RightShoulder',
5522
+ 'rightShoulder': 'RightShoulder',
5523
+ // Spine
5524
+ 'Spine': 'Spine1',
5525
+ 'spine': 'Spine1',
5526
+ 'Spine1': 'Spine1',
5527
+ 'Spine2': 'Spine2',
5528
+ // Head/Neck
5529
+ 'Head': 'Head',
5530
+ 'head': 'Head',
5531
+ 'Neck': 'Neck',
5532
+ 'neck': 'Neck',
5533
+ // Hips
5534
+ 'Hips': 'Hips',
5535
+ 'hips': 'Hips',
5536
+ 'Root': 'Hips',
5537
+ 'root': 'Hips',
5538
+ };
5539
+
5540
+ if (mappings[normalized]) {
5541
+ const mapped = mappings[normalized];
5542
+ if (availableBones.has(mapped)) {
5543
+ return mapped;
5544
+ }
5545
+ }
5546
+
5547
+ // Try case-insensitive match
5548
+ for (const boneName of availableBones) {
5549
+ if (boneName.toLowerCase() === normalized.toLowerCase()) {
5550
+ return boneName;
5551
+ }
5552
+ }
5553
+
5554
+ return null; // No mapping found
5555
+ };
5556
+
5557
+ // Debug: Log FBX bone names and avatar bone names for comparison
5558
+ const fbxBoneNames = new Set();
5559
+ anim.tracks.forEach(track => {
5560
+ const trackParts = track.name.split('.');
5561
+ fbxBoneNames.add(trackParts[0]);
5562
+ });
5563
+
5564
+ console.log('Ready Player Me FBX bone names:', Array.from(fbxBoneNames).sort().join(', '));
5565
+ console.log('Avatar skeleton bone names:', Array.from(availableBones).sort().join(', '));
5566
+
5567
+ // Filter and map animation tracks
5568
+ const mappedTracks = [];
5569
+ const unmappedBones = new Set();
5570
+ anim.tracks.forEach(track => {
5571
+ // Remove mixamorig prefix first
5572
+ let trackName = track.name.replaceAll('mixamorig', '');
5573
+ const trackParts = trackName.split('.');
5574
+ const fbxBoneName = trackParts[0];
5575
+ const property = trackParts[1];
5576
+
5577
+ // Map bone name to avatar skeleton
5578
+ const mappedBoneName = mapBoneName(fbxBoneName);
5579
+
5580
+ if (mappedBoneName && property) {
5581
+ // Create new track with mapped bone name
5582
+ const newTrackName = `${mappedBoneName}.${property}`;
5583
+ const newTrack = track.clone();
5584
+ newTrack.name = newTrackName;
5585
+ mappedTracks.push(newTrack);
5586
+
5587
+ // Store mapping for logging
5588
+ if (fbxBoneName !== mappedBoneName) {
5589
+ boneNameMap.set(fbxBoneName, mappedBoneName);
5590
+ }
5591
+ } else {
5592
+ unmappedBones.add(fbxBoneName);
5593
+ // Log unmapped bones (especially arm bones)
5594
+ if (fbxBoneName.toLowerCase().includes('arm') ||
5595
+ fbxBoneName.toLowerCase().includes('hand') ||
5596
+ fbxBoneName.toLowerCase().includes('shoulder')) {
5597
+ console.warn(`⚠️ Arm bone "${fbxBoneName}" could not be mapped to avatar skeleton`);
5598
+ }
5599
+ }
5600
+ });
5601
+
5602
+ if (unmappedBones.size > 0) {
5603
+ console.warn(`⚠️ ${unmappedBones.size} bone(s) could not be mapped:`, Array.from(unmappedBones).sort().join(', '));
5604
+ }
5605
+
5606
+ // Use mapped tracks if we have any, otherwise use original
5607
+ if (mappedTracks.length > 0) {
5608
+ anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
5609
+ 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(', '));
5612
+ }
5613
+ }
5614
+
5474
5615
  // Rename and scale Mixamo tracks, create a pose
5475
5616
  const props = {};
5476
5617
  anim.tracks.forEach( t => {