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

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.2",
3
+ "version": "1.1.5",
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",
@@ -2253,11 +2253,11 @@ class TalkingHead {
2253
2253
  if (this.lockedPosition && this.armature) {
2254
2254
  // Enforce the locked position - keep avatar exactly where it was locked
2255
2255
  // This prevents FBX animations from moving the avatar
2256
- this.armature.position.set(
2257
- this.lockedPosition.x,
2256
+ this.armature.position.set(
2257
+ this.lockedPosition.x,
2258
2258
  this.lockedPosition.y,
2259
- this.lockedPosition.z
2260
- );
2259
+ this.lockedPosition.z
2260
+ );
2261
2261
  }
2262
2262
  }
2263
2263
 
@@ -5354,6 +5354,7 @@ class TalkingHead {
5354
5354
 
5355
5355
  // Remove common prefixes
5356
5356
  let normalized = fbxBoneName;
5357
+ const originalNormalized = normalized;
5357
5358
 
5358
5359
  // Remove CC_Base prefix (Character Creator)
5359
5360
  if (normalized.startsWith('CC_Base_')) {
@@ -5367,6 +5368,15 @@ class TalkingHead {
5367
5368
  if (availableBones.has(normalized)) {
5368
5369
  return normalized;
5369
5370
  }
5371
+
5372
+ // Debug: Log first few failed mappings to help identify patterns
5373
+ if (!this._mappingDebugLog) {
5374
+ this._mappingDebugLog = new Set();
5375
+ }
5376
+ if (this._mappingDebugLog.size < 5 && !this._mappingDebugLog.has(fbxBoneName)) {
5377
+ this._mappingDebugLog.add(fbxBoneName);
5378
+ console.debug(`Mapping attempt: "${fbxBoneName}" -> "${normalized}" (not found in available bones)`);
5379
+ }
5370
5380
 
5371
5381
  // Handle numbered bones (e.g., Spine01 -> Spine1)
5372
5382
  if (normalized.match(/^Spine\d+$/)) {
@@ -5409,6 +5419,9 @@ class TalkingHead {
5409
5419
  'L_Middle1': 'LeftHandMiddle1',
5410
5420
  'L_Middle2': 'LeftHandMiddle2',
5411
5421
  'L_Middle3': 'LeftHandMiddle3',
5422
+ 'L_Mid1': 'LeftHandMiddle1',
5423
+ 'L_Mid2': 'LeftHandMiddle2',
5424
+ 'L_Mid3': 'LeftHandMiddle3',
5412
5425
  'L_Ring1': 'LeftHandRing1',
5413
5426
  'L_Ring2': 'LeftHandRing2',
5414
5427
  'L_Ring3': 'LeftHandRing3',
@@ -5430,6 +5443,9 @@ class TalkingHead {
5430
5443
  'R_Middle1': 'RightHandMiddle1',
5431
5444
  'R_Middle2': 'RightHandMiddle2',
5432
5445
  'R_Middle3': 'RightHandMiddle3',
5446
+ 'R_Mid1': 'RightHandMiddle1',
5447
+ 'R_Mid2': 'RightHandMiddle2',
5448
+ 'R_Mid3': 'RightHandMiddle3',
5433
5449
  'R_Ring1': 'RightHandRing1',
5434
5450
  'R_Ring2': 'RightHandRing2',
5435
5451
  'R_Ring3': 'RightHandRing3',
@@ -5459,8 +5475,9 @@ class TalkingHead {
5459
5475
 
5460
5476
  // Pattern-based matching for CC_Base and similar naming conventions
5461
5477
  const lowerNormalized = normalized.toLowerCase();
5478
+ const upperFirst = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
5462
5479
 
5463
- // Pattern: R_Index1/2/3 -> RightHandIndex1/2/3
5480
+ // Pattern: R_Index1/2/3 or r_index1/2/3 -> RightHandIndex1/2/3
5464
5481
  const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
5465
5482
  if (indexMatch) {
5466
5483
  const digit = indexMatch[1];
@@ -5493,8 +5510,8 @@ class TalkingHead {
5493
5510
  }
5494
5511
  }
5495
5512
 
5496
- // Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
5497
- const middleMatch = lowerNormalized.match(/^[rl]_middle(\d+)$/);
5513
+ // Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
5514
+ const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
5498
5515
  if (middleMatch) {
5499
5516
  const digit = middleMatch[1];
5500
5517
  const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
@@ -5515,7 +5532,7 @@ class TalkingHead {
5515
5532
  }
5516
5533
  }
5517
5534
 
5518
- // Pattern: R_Upperarm -> RightArm
5535
+ // Pattern: R_Upperarm -> RightArm (case insensitive)
5519
5536
  if (lowerNormalized.match(/^[rl]_upperarm/)) {
5520
5537
  const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5521
5538
  const candidate = `${side}Arm`;
@@ -5524,6 +5541,15 @@ class TalkingHead {
5524
5541
  }
5525
5542
  }
5526
5543
 
5544
+ // Pattern: R_UpperarmTwist01/02 -> ignore (twist bones and other extra bones)
5545
+ if (lowerNormalized.includes('upperarmtwist') ||
5546
+ lowerNormalized.includes('forearmtwist') ||
5547
+ lowerNormalized.includes('ribstwist') ||
5548
+ lowerNormalized.includes('breast') ||
5549
+ lowerNormalized.includes('twist')) {
5550
+ return null;
5551
+ }
5552
+
5527
5553
  // Pattern: R_Forearm -> RightForeArm
5528
5554
  if (lowerNormalized.match(/^[rl]_forearm/)) {
5529
5555
  const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
@@ -5564,6 +5590,13 @@ class TalkingHead {
5564
5590
  const missingBones = new Set();
5565
5591
  const mappedBones = new Map(); // Track mappings for logging
5566
5592
 
5593
+ // Debug: Log available bones (first time only)
5594
+ if (!this._loggedAvailableBones) {
5595
+ console.log('Available avatar bones:', Array.from(availableBones).sort().slice(0, 50).join(', '),
5596
+ availableBones.size > 50 ? `... (${availableBones.size} total)` : '');
5597
+ this._loggedAvailableBones = true;
5598
+ }
5599
+
5567
5600
  clip.tracks.forEach(track => {
5568
5601
  // Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
5569
5602
  const trackNameParts = track.name.split('.');
@@ -5581,6 +5614,72 @@ class TalkingHead {
5581
5614
  const newTrack = track.clone();
5582
5615
  newTrack.name = newTrackName;
5583
5616
 
5617
+ // Fix rotations for arm/hand bones that might be inverted
5618
+ // If hands are folding behind instead of in front, we need to adjust rotations
5619
+ const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
5620
+ const isForearmBone = mappedBoneName.includes('ForeArm');
5621
+
5622
+ if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
5623
+ // For quaternion tracks, we might need to adjust the rotation
5624
+ // Check if this is a quaternion track
5625
+ if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
5626
+ // Quaternion format: [x, y, z, w] per keyframe
5627
+ // For arm bones, we might need to invert Y or Z rotation
5628
+ // Adjust quaternion values to fix hand position
5629
+ const numKeyframes = newTrack.times.length;
5630
+ for (let i = 0; i < numKeyframes; i++) {
5631
+ const baseIdx = i * 4;
5632
+ if (baseIdx + 3 < newTrack.values.length) {
5633
+ // Get quaternion values
5634
+ let x = newTrack.values[baseIdx];
5635
+ let y = newTrack.values[baseIdx + 1];
5636
+ let z = newTrack.values[baseIdx + 2];
5637
+ let w = newTrack.values[baseIdx + 3];
5638
+
5639
+ // For arms, adjust rotation to flip hands from back to front
5640
+ // This is a common fix for FBX animations with different coordinate systems
5641
+ if (isForearmBone || mappedBoneName.includes('Hand')) {
5642
+ // Rotate 180 degrees around X axis to flip hands from behind to in front
5643
+ // Create a 180-degree rotation around X axis
5644
+ const flipAngle = Math.PI;
5645
+ const flipX = Math.cos(flipAngle / 2); // w component
5646
+ const flipY = Math.sin(flipAngle / 2); // x component (axis X = 1,0,0)
5647
+
5648
+ // Multiply quaternions: q_result = q_flip * q_original
5649
+ // For rotation around X axis: q_flip = (sin(θ/2), 0, 0, cos(θ/2))
5650
+ const qw = flipX * w - flipY * x;
5651
+ const qx = flipX * x + flipY * w;
5652
+ const qy = flipX * y - flipY * z;
5653
+ const qz = flipX * z + flipY * y;
5654
+
5655
+ x = qx;
5656
+ y = qy;
5657
+ z = qz;
5658
+ w = qw;
5659
+ }
5660
+
5661
+ newTrack.values[baseIdx] = x;
5662
+ newTrack.values[baseIdx + 1] = y;
5663
+ newTrack.values[baseIdx + 2] = z;
5664
+ newTrack.values[baseIdx + 3] = w;
5665
+ }
5666
+ }
5667
+ } else if (property === 'rotation' && newTrack.values && newTrack.values.length >= 3) {
5668
+ // Euler rotation format: [x, y, z] per keyframe
5669
+ const numKeyframes = newTrack.times.length;
5670
+ for (let i = 0; i < numKeyframes; i++) {
5671
+ const baseIdx = i * 3;
5672
+ if (baseIdx + 2 < newTrack.values.length) {
5673
+ // For arm bones, adjust Y rotation to flip hands
5674
+ if (isForearmBone || mappedBoneName.includes('Hand')) {
5675
+ // Add 180 degrees (PI radians) to Y rotation
5676
+ newTrack.values[baseIdx + 1] += Math.PI;
5677
+ }
5678
+ }
5679
+ }
5680
+ }
5681
+ }
5682
+
5584
5683
  validTracks.push(newTrack);
5585
5684
 
5586
5685
  // Track mappings for logging