@sage-rsc/talking-head-react 1.1.1 → 1.1.2

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.1",
3
+ "version": "1.1.2",
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",
@@ -5340,38 +5340,279 @@ class TalkingHead {
5340
5340
  return boneNames;
5341
5341
  }
5342
5342
 
5343
+ /**
5344
+ * Map bone names from different naming conventions to avatar bone names
5345
+ * @param {string} fbxBoneName - Bone name from FBX animation
5346
+ * @param {Set<string>} availableBones - Set of available bone names in avatar
5347
+ * @returns {string|null} Mapped bone name or null if no match found
5348
+ */
5349
+ mapBoneName(fbxBoneName, availableBones) {
5350
+ // Direct match first
5351
+ if (availableBones.has(fbxBoneName)) {
5352
+ return fbxBoneName;
5353
+ }
5354
+
5355
+ // Remove common prefixes
5356
+ let normalized = fbxBoneName;
5357
+
5358
+ // Remove CC_Base prefix (Character Creator)
5359
+ if (normalized.startsWith('CC_Base_')) {
5360
+ normalized = normalized.replace('CC_Base_', '');
5361
+ }
5362
+
5363
+ // Remove mixamorig prefix (Mixamo)
5364
+ normalized = normalized.replace(/^mixamorig/i, '');
5365
+
5366
+ // Try direct match after prefix removal
5367
+ if (availableBones.has(normalized)) {
5368
+ return normalized;
5369
+ }
5370
+
5371
+ // Handle numbered bones (e.g., Spine01 -> Spine1)
5372
+ if (normalized.match(/^Spine\d+$/)) {
5373
+ const num = normalized.match(/\d+/)?.[0];
5374
+ if (num) {
5375
+ const mapped = `Spine${parseInt(num)}`;
5376
+ if (availableBones.has(mapped)) {
5377
+ return mapped;
5378
+ }
5379
+ // Try Spine1 if Spine01, Spine2 if Spine02+
5380
+ if (num === '01' && availableBones.has('Spine1')) {
5381
+ return 'Spine1';
5382
+ }
5383
+ if (parseInt(num) >= 2 && availableBones.has('Spine2')) {
5384
+ return 'Spine2';
5385
+ }
5386
+ }
5387
+ }
5388
+
5389
+ // Handle twist bones (ignore them, they're usually not in standard skeletons)
5390
+ if (normalized.includes('Twist')) {
5391
+ return null;
5392
+ }
5393
+
5394
+ // Mapping rules for common bone name patterns
5395
+ const mappings = {
5396
+ // Spine mapping
5397
+ 'Spine01': 'Spine1',
5398
+ 'Spine02': 'Spine2',
5399
+ 'Spine03': 'Spine2',
5400
+
5401
+ // Left arm mapping
5402
+ 'L_Upperarm': 'LeftArm',
5403
+ 'L_Forearm': 'LeftForeArm',
5404
+ 'L_Hand': 'LeftHand',
5405
+ 'L_Shoulder': 'LeftShoulder',
5406
+ 'L_Index1': 'LeftHandIndex1',
5407
+ 'L_Index2': 'LeftHandIndex2',
5408
+ 'L_Index3': 'LeftHandIndex3',
5409
+ 'L_Middle1': 'LeftHandMiddle1',
5410
+ 'L_Middle2': 'LeftHandMiddle2',
5411
+ 'L_Middle3': 'LeftHandMiddle3',
5412
+ 'L_Ring1': 'LeftHandRing1',
5413
+ 'L_Ring2': 'LeftHandRing2',
5414
+ 'L_Ring3': 'LeftHandRing3',
5415
+ 'L_Pinky1': 'LeftHandPinky1',
5416
+ 'L_Pinky2': 'LeftHandPinky2',
5417
+ 'L_Pinky3': 'LeftHandPinky3',
5418
+ 'L_Thumb1': 'LeftHandThumb1',
5419
+ 'L_Thumb2': 'LeftHandThumb2',
5420
+ 'L_Thumb3': 'LeftHandThumb3',
5421
+
5422
+ // Right arm mapping
5423
+ 'R_Upperarm': 'RightArm',
5424
+ 'R_Forearm': 'RightForeArm',
5425
+ 'R_Hand': 'RightHand',
5426
+ 'R_Shoulder': 'RightShoulder',
5427
+ 'R_Index1': 'RightHandIndex1',
5428
+ 'R_Index2': 'RightHandIndex2',
5429
+ 'R_Index3': 'RightHandIndex3',
5430
+ 'R_Middle1': 'RightHandMiddle1',
5431
+ 'R_Middle2': 'RightHandMiddle2',
5432
+ 'R_Middle3': 'RightHandMiddle3',
5433
+ 'R_Ring1': 'RightHandRing1',
5434
+ 'R_Ring2': 'RightHandRing2',
5435
+ 'R_Ring3': 'RightHandRing3',
5436
+ 'R_Pinky1': 'RightHandPinky1',
5437
+ 'R_Pinky2': 'RightHandPinky2',
5438
+ 'R_Pinky3': 'RightHandPinky3',
5439
+ 'R_Thumb1': 'RightHandThumb1',
5440
+ 'R_Thumb2': 'RightHandThumb2',
5441
+ 'R_Thumb3': 'RightHandThumb3',
5442
+
5443
+ // Leg mapping
5444
+ 'L_Thigh': 'LeftUpLeg',
5445
+ 'L_Calf': 'LeftLeg',
5446
+ 'L_Foot': 'LeftFoot',
5447
+ 'R_Thigh': 'RightUpLeg',
5448
+ 'R_Calf': 'RightLeg',
5449
+ 'R_Foot': 'RightFoot',
5450
+ };
5451
+
5452
+ // Try mapping
5453
+ if (mappings[normalized]) {
5454
+ const mapped = mappings[normalized];
5455
+ if (availableBones.has(mapped)) {
5456
+ return mapped;
5457
+ }
5458
+ }
5459
+
5460
+ // Pattern-based matching for CC_Base and similar naming conventions
5461
+ const lowerNormalized = normalized.toLowerCase();
5462
+
5463
+ // Pattern: R_Index1/2/3 -> RightHandIndex1/2/3
5464
+ const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
5465
+ if (indexMatch) {
5466
+ const digit = indexMatch[1];
5467
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5468
+ const candidate = `${side}HandIndex${digit}`;
5469
+ if (availableBones.has(candidate)) {
5470
+ return candidate;
5471
+ }
5472
+ }
5473
+
5474
+ // Pattern: R_Pinky1/2/3 -> RightHandPinky1/2/3
5475
+ const pinkyMatch = lowerNormalized.match(/^[rl]_pinky(\d+)$/);
5476
+ if (pinkyMatch) {
5477
+ const digit = pinkyMatch[1];
5478
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5479
+ const candidate = `${side}HandPinky${digit}`;
5480
+ if (availableBones.has(candidate)) {
5481
+ return candidate;
5482
+ }
5483
+ }
5484
+
5485
+ // Pattern: R_Ring1/2/3 -> RightHandRing1/2/3
5486
+ const ringMatch = lowerNormalized.match(/^[rl]_ring(\d+)$/);
5487
+ if (ringMatch) {
5488
+ const digit = ringMatch[1];
5489
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5490
+ const candidate = `${side}HandRing${digit}`;
5491
+ if (availableBones.has(candidate)) {
5492
+ return candidate;
5493
+ }
5494
+ }
5495
+
5496
+ // Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
5497
+ const middleMatch = lowerNormalized.match(/^[rl]_middle(\d+)$/);
5498
+ if (middleMatch) {
5499
+ const digit = middleMatch[1];
5500
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5501
+ const candidate = `${side}HandMiddle${digit}`;
5502
+ if (availableBones.has(candidate)) {
5503
+ return candidate;
5504
+ }
5505
+ }
5506
+
5507
+ // Pattern: R_Thumb1/2/3 -> RightHandThumb1/2/3
5508
+ const thumbMatch = lowerNormalized.match(/^[rl]_thumb(\d+)$/);
5509
+ if (thumbMatch) {
5510
+ const digit = thumbMatch[1];
5511
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5512
+ const candidate = `${side}HandThumb${digit}`;
5513
+ if (availableBones.has(candidate)) {
5514
+ return candidate;
5515
+ }
5516
+ }
5517
+
5518
+ // Pattern: R_Upperarm -> RightArm
5519
+ if (lowerNormalized.match(/^[rl]_upperarm/)) {
5520
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5521
+ const candidate = `${side}Arm`;
5522
+ if (availableBones.has(candidate)) {
5523
+ return candidate;
5524
+ }
5525
+ }
5526
+
5527
+ // Pattern: R_Forearm -> RightForeArm
5528
+ if (lowerNormalized.match(/^[rl]_forearm/)) {
5529
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5530
+ const candidate = `${side}ForeArm`;
5531
+ if (availableBones.has(candidate)) {
5532
+ return candidate;
5533
+ }
5534
+ }
5535
+
5536
+ // Pattern: R_Hand -> RightHand
5537
+ if (lowerNormalized.match(/^[rl]_hand$/)) {
5538
+ const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5539
+ const candidate = `${side}Hand`;
5540
+ if (availableBones.has(candidate)) {
5541
+ return candidate;
5542
+ }
5543
+ }
5544
+
5545
+ // Try case-insensitive exact match
5546
+ for (const boneName of availableBones) {
5547
+ if (boneName.toLowerCase() === lowerNormalized) {
5548
+ return boneName;
5549
+ }
5550
+ }
5551
+
5552
+ return null; // No mapping found
5553
+ }
5554
+
5343
5555
  /**
5344
5556
  * Filter animation tracks to only include bones that exist in the avatar
5557
+ * Maps bone names from different naming conventions to avatar bone names
5345
5558
  * @param {THREE.AnimationClip} clip - Animation clip to filter
5346
5559
  * @param {Set<string>} availableBones - Set of available bone names
5347
- * @returns {THREE.AnimationClip} Filtered animation clip
5560
+ * @returns {THREE.AnimationClip} Filtered animation clip with mapped bone names
5348
5561
  */
5349
5562
  filterAnimationTracks(clip, availableBones) {
5350
5563
  const validTracks = [];
5351
5564
  const missingBones = new Set();
5565
+ const mappedBones = new Map(); // Track mappings for logging
5352
5566
 
5353
5567
  clip.tracks.forEach(track => {
5354
5568
  // Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
5355
5569
  const trackNameParts = track.name.split('.');
5356
- const boneName = trackNameParts[0];
5570
+ const fbxBoneName = trackNameParts[0];
5571
+ const property = trackNameParts[1]; // position, quaternion, rotation, etc.
5357
5572
 
5358
- if (availableBones.has(boneName)) {
5359
- validTracks.push(track);
5573
+ // Try to map the bone name
5574
+ const mappedBoneName = this.mapBoneName(fbxBoneName, availableBones);
5575
+
5576
+ if (mappedBoneName) {
5577
+ // Create a new track with the mapped bone name
5578
+ const newTrackName = `${mappedBoneName}.${property}`;
5579
+
5580
+ // Clone the track and update its name
5581
+ const newTrack = track.clone();
5582
+ newTrack.name = newTrackName;
5583
+
5584
+ validTracks.push(newTrack);
5585
+
5586
+ // Track mappings for logging
5587
+ if (fbxBoneName !== mappedBoneName) {
5588
+ mappedBones.set(fbxBoneName, mappedBoneName);
5589
+ }
5360
5590
  } else {
5361
- missingBones.add(boneName);
5591
+ missingBones.add(fbxBoneName);
5362
5592
  }
5363
5593
  });
5364
5594
 
5595
+ // Log results
5596
+ if (mappedBones.size > 0) {
5597
+ console.info(`FBX animation "${clip.name}": Mapped ${mappedBones.size} bone(s) to avatar skeleton:`,
5598
+ Array.from(mappedBones.entries()).slice(0, 5).map(([from, to]) => `${from} → ${to}`).join(', '),
5599
+ mappedBones.size > 5 ? '...' : '');
5600
+ }
5601
+
5365
5602
  if (missingBones.size > 0) {
5366
- console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) not found in avatar skeleton:`, Array.from(missingBones).slice(0, 10).join(', '), missingBones.size > 10 ? '...' : '');
5367
- console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks`);
5368
- } else if (validTracks.length > 0) {
5369
- console.info(`FBX animation "${clip.name}" is fully compatible: all ${validTracks.length} tracks match avatar skeleton`);
5603
+ console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) that couldn't be mapped:`,
5604
+ Array.from(missingBones).slice(0, 10).join(', '),
5605
+ missingBones.size > 10 ? '...' : '');
5606
+ }
5607
+
5608
+ if (validTracks.length > 0) {
5609
+ console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks (${mappedBones.size} mapped)`);
5610
+ } else {
5611
+ console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
5370
5612
  }
5371
5613
 
5372
5614
  // Create a new clip with only valid tracks
5373
5615
  if (validTracks.length === 0) {
5374
- console.error(`No valid tracks found for animation "${clip.name}". All bones are missing from avatar skeleton.`);
5375
5616
  return null;
5376
5617
  }
5377
5618