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

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.6",
3
+ "version": "1.1.7",
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",
@@ -5323,532 +5323,6 @@ class TalkingHead {
5323
5323
  * @param {number} [ndx=0] Index of the clip
5324
5324
  * @param {number} [scale=0.01] Position scale factor
5325
5325
  */
5326
- /**
5327
- * Get all bone names from the avatar's armature
5328
- * @returns {Set<string>} Set of bone names
5329
- */
5330
- getAvailableBoneNames() {
5331
- const boneNames = new Set();
5332
- if (!this.armature) return boneNames;
5333
-
5334
- this.armature.traverse((child) => {
5335
- if (child.isBone || child.type === 'Bone') {
5336
- boneNames.add(child.name);
5337
- }
5338
- });
5339
-
5340
- return boneNames;
5341
- }
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
- const originalNormalized = normalized;
5358
-
5359
- // Remove CC_Base prefix (Character Creator)
5360
- if (normalized.startsWith('CC_Base_')) {
5361
- normalized = normalized.replace('CC_Base_', '');
5362
- }
5363
-
5364
- // Remove mixamorig prefix (Mixamo)
5365
- normalized = normalized.replace(/^mixamorig/i, '');
5366
-
5367
- // Try direct match after prefix removal
5368
- if (availableBones.has(normalized)) {
5369
- return normalized;
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
- }
5380
-
5381
- // Handle numbered bones (e.g., Spine01 -> Spine1)
5382
- if (normalized.match(/^Spine\d+$/)) {
5383
- const num = normalized.match(/\d+/)?.[0];
5384
- if (num) {
5385
- const mapped = `Spine${parseInt(num)}`;
5386
- if (availableBones.has(mapped)) {
5387
- return mapped;
5388
- }
5389
- // Try Spine1 if Spine01, Spine2 if Spine02+
5390
- if (num === '01' && availableBones.has('Spine1')) {
5391
- return 'Spine1';
5392
- }
5393
- if (parseInt(num) >= 2 && availableBones.has('Spine2')) {
5394
- return 'Spine2';
5395
- }
5396
- }
5397
- }
5398
-
5399
- // Handle twist bones (ignore them, they're usually not in standard skeletons)
5400
- if (normalized.includes('Twist')) {
5401
- return null;
5402
- }
5403
-
5404
- // Mapping rules for common bone name patterns
5405
- const mappings = {
5406
- // Spine mapping
5407
- 'Spine01': 'Spine1',
5408
- 'Spine02': 'Spine2',
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',
5419
-
5420
- // Left arm mapping
5421
- 'L_Upperarm': 'LeftArm',
5422
- 'L_Forearm': 'LeftForeArm',
5423
- 'L_Hand': 'LeftHand',
5424
- 'L_Shoulder': 'LeftShoulder',
5425
- 'L_Clavicle': 'LeftShoulder',
5426
- 'L_UpperArm': 'LeftArm',
5427
- 'L_ForeArm': 'LeftForeArm',
5428
- 'L_Index1': 'LeftHandIndex1',
5429
- 'L_Index2': 'LeftHandIndex2',
5430
- 'L_Index3': 'LeftHandIndex3',
5431
- 'L_Middle1': 'LeftHandMiddle1',
5432
- 'L_Middle2': 'LeftHandMiddle2',
5433
- 'L_Middle3': 'LeftHandMiddle3',
5434
- 'L_Mid1': 'LeftHandMiddle1',
5435
- 'L_Mid2': 'LeftHandMiddle2',
5436
- 'L_Mid3': 'LeftHandMiddle3',
5437
- 'L_Ring1': 'LeftHandRing1',
5438
- 'L_Ring2': 'LeftHandRing2',
5439
- 'L_Ring3': 'LeftHandRing3',
5440
- 'L_Pinky1': 'LeftHandPinky1',
5441
- 'L_Pinky2': 'LeftHandPinky2',
5442
- 'L_Pinky3': 'LeftHandPinky3',
5443
- 'L_Thumb1': 'LeftHandThumb1',
5444
- 'L_Thumb2': 'LeftHandThumb2',
5445
- 'L_Thumb3': 'LeftHandThumb3',
5446
-
5447
- // Right arm mapping
5448
- 'R_Upperarm': 'RightArm',
5449
- 'R_Forearm': 'RightForeArm',
5450
- 'R_Hand': 'RightHand',
5451
- 'R_Shoulder': 'RightShoulder',
5452
- 'R_Clavicle': 'RightShoulder',
5453
- 'R_UpperArm': 'RightArm',
5454
- 'R_ForeArm': 'RightForeArm',
5455
- 'R_Index1': 'RightHandIndex1',
5456
- 'R_Index2': 'RightHandIndex2',
5457
- 'R_Index3': 'RightHandIndex3',
5458
- 'R_Middle1': 'RightHandMiddle1',
5459
- 'R_Middle2': 'RightHandMiddle2',
5460
- 'R_Middle3': 'RightHandMiddle3',
5461
- 'R_Mid1': 'RightHandMiddle1',
5462
- 'R_Mid2': 'RightHandMiddle2',
5463
- 'R_Mid3': 'RightHandMiddle3',
5464
- 'R_Ring1': 'RightHandRing1',
5465
- 'R_Ring2': 'RightHandRing2',
5466
- 'R_Ring3': 'RightHandRing3',
5467
- 'R_Pinky1': 'RightHandPinky1',
5468
- 'R_Pinky2': 'RightHandPinky2',
5469
- 'R_Pinky3': 'RightHandPinky3',
5470
- 'R_Thumb1': 'RightHandThumb1',
5471
- 'R_Thumb2': 'RightHandThumb2',
5472
- 'R_Thumb3': 'RightHandThumb3',
5473
-
5474
- // Leg mapping
5475
- 'L_Thigh': 'LeftUpLeg',
5476
- 'L_Calf': 'LeftLeg',
5477
- 'L_Foot': 'LeftFoot',
5478
- 'L_UpLeg': 'LeftUpLeg',
5479
- 'L_Leg': 'LeftLeg',
5480
- 'R_Thigh': 'RightUpLeg',
5481
- 'R_Calf': 'RightLeg',
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',
5490
- };
5491
-
5492
- // Try mapping
5493
- if (mappings[normalized]) {
5494
- const mapped = mappings[normalized];
5495
- if (availableBones.has(mapped)) {
5496
- return mapped;
5497
- }
5498
- }
5499
-
5500
- // Pattern-based matching for CC_Base and similar naming conventions
5501
- const lowerNormalized = normalized.toLowerCase();
5502
- const upperFirst = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
5503
-
5504
- // Pattern: R_Index1/2/3 or r_index1/2/3 -> RightHandIndex1/2/3
5505
- const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
5506
- if (indexMatch) {
5507
- const digit = indexMatch[1];
5508
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5509
- const candidate = `${side}HandIndex${digit}`;
5510
- if (availableBones.has(candidate)) {
5511
- return candidate;
5512
- }
5513
- }
5514
-
5515
- // Pattern: R_Pinky1/2/3 -> RightHandPinky1/2/3
5516
- const pinkyMatch = lowerNormalized.match(/^[rl]_pinky(\d+)$/);
5517
- if (pinkyMatch) {
5518
- const digit = pinkyMatch[1];
5519
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5520
- const candidate = `${side}HandPinky${digit}`;
5521
- if (availableBones.has(candidate)) {
5522
- return candidate;
5523
- }
5524
- }
5525
-
5526
- // Pattern: R_Ring1/2/3 -> RightHandRing1/2/3
5527
- const ringMatch = lowerNormalized.match(/^[rl]_ring(\d+)$/);
5528
- if (ringMatch) {
5529
- const digit = ringMatch[1];
5530
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5531
- const candidate = `${side}HandRing${digit}`;
5532
- if (availableBones.has(candidate)) {
5533
- return candidate;
5534
- }
5535
- }
5536
-
5537
- // Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
5538
- const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
5539
- if (middleMatch) {
5540
- const digit = middleMatch[1];
5541
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5542
- const candidate = `${side}HandMiddle${digit}`;
5543
- if (availableBones.has(candidate)) {
5544
- return candidate;
5545
- }
5546
- }
5547
-
5548
- // Pattern: R_Thumb1/2/3 -> RightHandThumb1/2/3
5549
- const thumbMatch = lowerNormalized.match(/^[rl]_thumb(\d+)$/);
5550
- if (thumbMatch) {
5551
- const digit = thumbMatch[1];
5552
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5553
- const candidate = `${side}HandThumb${digit}`;
5554
- if (availableBones.has(candidate)) {
5555
- return candidate;
5556
- }
5557
- }
5558
-
5559
- // Pattern: R_Upperarm -> RightArm (case insensitive)
5560
- if (lowerNormalized.match(/^[rl]_upperarm/)) {
5561
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5562
- const candidate = `${side}Arm`;
5563
- if (availableBones.has(candidate)) {
5564
- return candidate;
5565
- }
5566
- }
5567
-
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')) {
5574
- return null;
5575
- }
5576
-
5577
- // Pattern: R_Forearm -> RightForeArm
5578
- if (lowerNormalized.match(/^[rl]_forearm/)) {
5579
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5580
- const candidate = `${side}ForeArm`;
5581
- if (availableBones.has(candidate)) {
5582
- return candidate;
5583
- }
5584
- }
5585
-
5586
- // Pattern: R_Hand -> RightHand
5587
- if (lowerNormalized.match(/^[rl]_hand$/)) {
5588
- const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
5589
- const candidate = `${side}Hand`;
5590
- if (availableBones.has(candidate)) {
5591
- return candidate;
5592
- }
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
- }
5637
-
5638
- // Try case-insensitive exact match
5639
- for (const boneName of availableBones) {
5640
- if (boneName.toLowerCase() === lowerNormalized) {
5641
- return boneName;
5642
- }
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
- }
5656
-
5657
- return null; // No mapping found
5658
- }
5659
-
5660
- /**
5661
- * Filter animation tracks to only include bones that exist in the avatar
5662
- * Maps bone names from different naming conventions to avatar bone names
5663
- * @param {THREE.AnimationClip} clip - Animation clip to filter
5664
- * @param {Set<string>} availableBones - Set of available bone names
5665
- * @returns {THREE.AnimationClip} Filtered animation clip with mapped bone names
5666
- */
5667
- filterAnimationTracks(clip, availableBones) {
5668
- const validTracks = [];
5669
- const missingBones = new Set();
5670
- const mappedBones = new Map(); // Track mappings for logging
5671
- const fbxBoneNames = new Set(); // Track all unique FBX bone names
5672
-
5673
- // Debug: Log available bones (first time only)
5674
- if (!this._loggedAvailableBones) {
5675
- console.log('Available avatar bones:', Array.from(availableBones).sort().join(', '));
5676
- this._loggedAvailableBones = true;
5677
- }
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
-
5690
- clip.tracks.forEach(track => {
5691
- // Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
5692
- const trackNameParts = track.name.split('.');
5693
- const fbxBoneName = trackNameParts[0];
5694
- const property = trackNameParts[1]; // position, quaternion, rotation, etc.
5695
-
5696
- // Try to map the bone name
5697
- const mappedBoneName = this.mapBoneName(fbxBoneName, availableBones);
5698
-
5699
- if (mappedBoneName) {
5700
- // Create a new track with the mapped bone name
5701
- const newTrackName = `${mappedBoneName}.${property}`;
5702
-
5703
- // Clone the track and update its name
5704
- const newTrack = track.clone();
5705
- newTrack.name = newTrackName;
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
-
5773
- validTracks.push(newTrack);
5774
-
5775
- // Track mappings for logging
5776
- if (fbxBoneName !== mappedBoneName) {
5777
- mappedBones.set(fbxBoneName, mappedBoneName);
5778
- }
5779
- } else {
5780
- missingBones.add(fbxBoneName);
5781
- }
5782
- });
5783
-
5784
- // Log results with detailed mapping information
5785
- console.log(`\n=== Mapping Results for "${clip.name}" ===`);
5786
-
5787
- // Show all mapped bones
5788
- if (mappedBones.size > 0) {
5789
- console.info(`✓ Successfully mapped ${mappedBones.size} bone(s):`);
5790
- Array.from(mappedBones.entries()).forEach(([from, to]) => {
5791
- console.log(` ${from} → ${to}`);
5792
- });
5793
- }
5794
-
5795
- // Show all missing bones with suggestions
5796
- if (missingBones.size > 0) {
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
- });
5814
- }
5815
-
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) {
5842
- console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
5843
- }
5844
-
5845
- // Create a new clip with only valid tracks
5846
- if (validTracks.length === 0) {
5847
- return null;
5848
- }
5849
-
5850
- return new THREE.AnimationClip(clip.name, clip.duration, validTracks);
5851
- }
5852
5326
 
5853
5327
  async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false) {
5854
5328
  if ( !this.armature ) return;
@@ -5997,20 +5471,6 @@ class TalkingHead {
5997
5471
  if ( fbx && fbx.animations && fbx.animations[ndx] ) {
5998
5472
  let anim = fbx.animations[ndx];
5999
5473
 
6000
- // Get available bone names from avatar skeleton
6001
- const availableBones = this.getAvailableBoneNames();
6002
-
6003
- // Filter animation tracks to only include bones that exist
6004
- const filteredAnim = this.filterAnimationTracks(anim, availableBones);
6005
-
6006
- if (!filteredAnim) {
6007
- console.error(`Cannot play FBX animation "${url}": No compatible bones found.`);
6008
- return;
6009
- }
6010
-
6011
- // Use the filtered animation instead of the original
6012
- anim = filteredAnim;
6013
-
6014
5474
  // Rename and scale Mixamo tracks, create a pose
6015
5475
  const props = {};
6016
5476
  anim.tracks.forEach( t => {