@sage-rsc/talking-head-react 1.4.2 → 1.4.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.
@@ -31,25 +31,8 @@ import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
31
31
  import Stats from 'three/addons/libs/stats.module.js';
32
32
 
33
33
  import{ DynamicBones } from './dynamicbones.mjs';
34
- import { AudioAnalyzer } from './audioAnalyzer.js';
35
34
  const workletUrl = new URL('./playback-worklet.js', import.meta.url);
36
35
 
37
- // Import lipsync modules statically to ensure they're bundled
38
- import * as LipsyncEn from './lipsync-en.mjs';
39
- import * as LipsyncDe from './lipsync-de.mjs';
40
- import * as LipsyncFr from './lipsync-fr.mjs';
41
- import * as LipsyncFi from './lipsync-fi.mjs';
42
- import * as LipsyncLt from './lipsync-lt.mjs';
43
-
44
- // Lipsync module map for dynamic access
45
- const LIPSYNC_MODULES = {
46
- en: LipsyncEn,
47
- de: LipsyncDe,
48
- fr: LipsyncFr,
49
- fi: LipsyncFi,
50
- lt: LipsyncLt
51
- };
52
-
53
36
  // Temporary objects for animation loop
54
37
  const q = new THREE.Quaternion();
55
38
  const e = new THREE.Euler();
@@ -146,8 +129,8 @@ class TalkingHead {
146
129
  ttsVoice: "fi-FI-Standard-A",
147
130
  ttsRate: 1,
148
131
  ttsPitch: 0,
149
- ttsVolume: 0.3,
150
- mixerGainSpeech: 1.2,
132
+ ttsVolume: 0,
133
+ mixerGainSpeech: null,
151
134
  mixerGainBackground: null,
152
135
  lipsyncLang: 'fi',
153
136
  lipsyncModules: ['fi','en','lt'],
@@ -168,9 +151,9 @@ class TalkingHead {
168
151
  cameraPanEnable: false,
169
152
  cameraZoomEnable: false,
170
153
  lightAmbientColor: 0xffffff,
171
- lightAmbientIntensity: 1.25,
154
+ lightAmbientIntensity: 2,
172
155
  lightDirectColor: 0x8888aa,
173
- lightDirectIntensity: 12,
156
+ lightDirectIntensity: 30,
174
157
  lightDirectPhi: 1,
175
158
  lightDirectTheta: 2,
176
159
  lightSpotIntensity: 0,
@@ -180,9 +163,9 @@ class TalkingHead {
180
163
  lightSpotDispersion: 1,
181
164
  avatarMood: "neutral",
182
165
  avatarMute: false,
183
- avatarIdleEyeContact: 0.6,
166
+ avatarIdleEyeContact: 0.2,
184
167
  avatarIdleHeadMove: 0.5,
185
- avatarSpeakingEyeContact: 0.8,
168
+ avatarSpeakingEyeContact: 0.5,
186
169
  avatarSpeakingHeadMove: 0.5,
187
170
  avatarIgnoreCamera: false,
188
171
  listeningSilenceThresholdLevel: 40,
@@ -425,20 +408,16 @@ class TalkingHead {
425
408
 
426
409
  this.animMoods = {
427
410
  'neutral' : {
428
- baseline: { eyesLookDown: 0 },
411
+ baseline: { eyesLookDown: 0.1 },
429
412
  speech: { deltaRate: 0, deltaPitch: 0, deltaVolume: 0 },
430
413
  anims: [
431
414
  { name: 'breathing', delay: 1500, dt: [ 1200,500,1000 ], vs: { chestInhale: [0.5,0.5,0] } },
432
415
  { name: 'pose', alt: [
433
- { p: 0.5, delay: [5000,30000], vs: { pose: ['side'] },
434
- 'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
435
- },
416
+ { p: 0.5, delay: [5000,30000], vs: { pose: ['side'] } },
436
417
  { p: 0.3, delay: [5000,30000], vs: { pose: ['hip'] },
437
418
  'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
438
419
  },
439
- { delay: [5000,30000], vs: { pose: ['straight'] },
440
- 'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
441
- }
420
+ { delay: [5000,30000], vs: { pose: ['straight'] } }
442
421
  ]},
443
422
  { name: 'head',
444
423
  idle: { delay: [0,1000], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.3,0.3]], bodyRotateZ: [[-0.08,0.08]] } },
@@ -1237,16 +1216,6 @@ class TalkingHead {
1237
1216
 
1238
1217
  this.stop();
1239
1218
  this.avatar = avatar;
1240
-
1241
- // Initialize custom properties
1242
- this.bodyMovement = avatar.bodyMovement || 'idle';
1243
- this.movementIntensity = avatar.movementIntensity || 0.5;
1244
- this.lockedPosition = null;
1245
- this.originalPosition = null;
1246
- this.positionWasLocked = false;
1247
-
1248
- // Initialize FBX animation loader
1249
- this.fbxAnimationLoader = null;
1250
1219
 
1251
1220
  // Dispose Dynamic Bones
1252
1221
  this.dynamicbones.dispose();
@@ -1420,24 +1389,6 @@ class TalkingHead {
1420
1389
  // Set pose, view and start animation
1421
1390
  if ( !this.viewName ) this.setView( this.opt.cameraView );
1422
1391
  this.setMood( this.avatar.avatarMood || this.moodName || this.opt.avatarMood );
1423
-
1424
- // Set initial gender-appropriate pose for male avatars to avoid feminine appearance
1425
- // Do this BEFORE starting the animation system to prevent initial female pose
1426
- if (this.avatar.body === 'M' && this.poseTemplates['wide']) {
1427
- // Use setPoseFromTemplate which properly handles pose transitions without loops
1428
- this.poseName = 'wide';
1429
- this.setPoseFromTemplate(this.poseTemplates['wide'], 0); // 0ms = instant, no transition
1430
- console.log('Set initial male-appropriate pose: wide');
1431
- }
1432
-
1433
- // Initialize FBX animation loader
1434
- this.initializeFBXAnimationLoader();
1435
-
1436
- // Apply body movement animation if set
1437
- if (this.bodyMovement && this.bodyMovement !== 'idle') {
1438
- this.applyBodyMovementAnimation();
1439
- }
1440
-
1441
1392
  this.start();
1442
1393
 
1443
1394
  }
@@ -1610,9 +1561,6 @@ class TalkingHead {
1610
1561
  }
1611
1562
  }
1612
1563
  }
1613
-
1614
- // Apply shoulder adjustment to lower shoulders
1615
- this.applyShoulderAdjustment();
1616
1564
  }
1617
1565
 
1618
1566
  /**
@@ -1631,68 +1579,6 @@ class TalkingHead {
1631
1579
  }
1632
1580
  }
1633
1581
  }
1634
-
1635
- /**
1636
- * Apply shoulder adjustment to lower shoulders to a more natural position
1637
- * This is called from updatePoseBase for pose-based animations
1638
- */
1639
- applyShoulderAdjustment() {
1640
- // Shoulder adjustment: reduce X-axis rotation by ~0.6 radians (34 degrees) to lower shoulders to a relaxed position
1641
- const shoulderAdjustment = -0.6; // Negative to lower shoulders (increased for more relaxed look)
1642
- const tempEuler = new THREE.Euler();
1643
-
1644
- // Adjust left shoulder
1645
- if (this.poseAvatar.props['LeftShoulder.quaternion']) {
1646
- const leftShoulder = this.poseAvatar.props['LeftShoulder.quaternion'];
1647
- tempEuler.setFromQuaternion(leftShoulder, 'XYZ');
1648
- tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
1649
- leftShoulder.setFromEuler(tempEuler, 'XYZ');
1650
- }
1651
-
1652
- // Adjust right shoulder
1653
- if (this.poseAvatar.props['RightShoulder.quaternion']) {
1654
- const rightShoulder = this.poseAvatar.props['RightShoulder.quaternion'];
1655
- tempEuler.setFromQuaternion(rightShoulder, 'XYZ');
1656
- tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
1657
- rightShoulder.setFromEuler(tempEuler, 'XYZ');
1658
- }
1659
- }
1660
-
1661
- /**
1662
- * Apply shoulder adjustment directly to bone objects
1663
- * This is called AFTER FBX animations update to ensure shoulders stay relaxed
1664
- * regardless of what the animation sets
1665
- */
1666
- applyShoulderAdjustmentToBones() {
1667
- if (!this.armature) return;
1668
-
1669
- // Shoulder adjustment: reduce X-axis rotation by ~0.6 radians (34 degrees) to lower shoulders
1670
- const shoulderAdjustment = -0.6; // Negative to lower shoulders
1671
- const tempEuler = new THREE.Euler();
1672
- const tempQuaternion = new THREE.Quaternion();
1673
-
1674
- // Get shoulder bones directly from armature
1675
- const leftShoulderBone = this.armature.getObjectByName('LeftShoulder');
1676
- const rightShoulderBone = this.armature.getObjectByName('RightShoulder');
1677
-
1678
- // Adjust left shoulder bone directly
1679
- if (leftShoulderBone && leftShoulderBone.quaternion) {
1680
- tempEuler.setFromQuaternion(leftShoulderBone.quaternion, 'XYZ');
1681
- tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
1682
- tempQuaternion.setFromEuler(tempEuler, 'XYZ');
1683
- leftShoulderBone.quaternion.copy(tempQuaternion);
1684
- leftShoulderBone.updateMatrixWorld(true);
1685
- }
1686
-
1687
- // Adjust right shoulder bone directly
1688
- if (rightShoulderBone && rightShoulderBone.quaternion) {
1689
- tempEuler.setFromQuaternion(rightShoulderBone.quaternion, 'XYZ');
1690
- tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
1691
- tempQuaternion.setFromEuler(tempEuler, 'XYZ');
1692
- rightShoulderBone.quaternion.copy(tempQuaternion);
1693
- rightShoulderBone.updateMatrixWorld(true);
1694
- }
1695
- }
1696
1582
 
1697
1583
  /**
1698
1584
  * Update morph target values.
@@ -2466,9 +2352,6 @@ class TalkingHead {
2466
2352
  dt = dt / this.animSlowdownRate;
2467
2353
  this.animClock += dt;
2468
2354
 
2469
- // Maintain locked position if set
2470
- this.maintainLockedPosition();
2471
-
2472
2355
  let i,j,l,k,vol=0;
2473
2356
 
2474
2357
  // Statistics start
@@ -2787,15 +2670,6 @@ class TalkingHead {
2787
2670
 
2788
2671
  // Update Dynamic Bones
2789
2672
  this.dynamicbones.update(dt);
2790
-
2791
- // Update FBX animations
2792
- if (this.fbxAnimationLoader) {
2793
- this.fbxAnimationLoader.update();
2794
- }
2795
-
2796
- // Apply shoulder adjustment AFTER FBX animations to ensure relaxed shoulders
2797
- // This overrides any shoulder positions set by animations
2798
- this.applyShoulderAdjustmentToBones();
2799
2673
 
2800
2674
  // Custom update
2801
2675
  if ( this.opt.update ) {
@@ -4415,20 +4289,9 @@ class TalkingHead {
4415
4289
  * @param {number} [ndx=0] Index of the clip
4416
4290
  * @param {number} [scale=0.01] Position scale factor
4417
4291
  */
4418
- async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false) {
4292
+ async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01) {
4419
4293
  if ( !this.armature ) return;
4420
4294
 
4421
- // Track whether position was locked for this animation
4422
- this.positionWasLocked = !disablePositionLock;
4423
-
4424
- // Lock position IMMEDIATELY to prevent any position changes (unless disabled)
4425
- if (!disablePositionLock) {
4426
- this.lockAvatarPosition();
4427
- console.log('Position locked immediately before FBX animation:', url);
4428
- } else {
4429
- console.log('Position locking disabled for FBX animation:', url);
4430
- }
4431
-
4432
4295
  let item = this.animClips.find( x => x.url === url+'-'+ndx );
4433
4296
  if ( item ) {
4434
4297
 
@@ -4446,40 +4309,27 @@ class TalkingHead {
4446
4309
  this.poseTarget.props[x[0]].d = 1000;
4447
4310
  });
4448
4311
 
4449
- // Use existing mixer or create new one if none exists
4450
- if (!this.mixer) {
4451
- this.mixer = new THREE.AnimationMixer(this.armature);
4452
- console.log('Created new mixer for FBX animation');
4453
- } else {
4454
- console.log('Using existing mixer for FBX animation, preserving morph targets');
4312
+ // Create a new mixer
4313
+ if (this.mixer) {
4314
+ this.mixer.removeEventListener('finished', this._mixerHandler);
4315
+ this.mixer.stopAllAction();
4316
+ this.mixer.uncacheRoot(this.armature);
4317
+ this.mixer = null;
4318
+ this._mixerHandler = null;
4455
4319
  }
4456
- this.mixer.addEventListener( 'finished', this.stopAnimation.bind(this), { once: true });
4320
+ this.mixer = new THREE.AnimationMixer(this.armature);
4321
+ this._mixerHandler = () => {
4322
+ this.stopAnimation();
4323
+ this.mixer?.removeEventListener('finished', this._mixerHandler);
4324
+ };
4325
+ this.mixer.addEventListener('finished', this._mixerHandler);
4457
4326
 
4458
- // Play action with error handling
4327
+ // Play action
4459
4328
  const repeat = Math.ceil(dur / item.clip.duration);
4460
4329
  const action = this.mixer.clipAction(item.clip);
4461
4330
  action.setLoop( THREE.LoopRepeat, repeat );
4462
4331
  action.clampWhenFinished = true;
4463
-
4464
- // Store the current FBX action for proper cleanup
4465
- this.currentFBXAction = action;
4466
-
4467
- try {
4468
4332
  action.fadeIn(0.5).play();
4469
- console.log('FBX animation started successfully:', url);
4470
- } catch (error) {
4471
- console.warn('FBX animation failed to start:', error);
4472
- // Stop the animation and unlock position on error
4473
- this.stopAnimation();
4474
- return;
4475
- }
4476
-
4477
- // Check if the animation actually has valid tracks
4478
- if (action.getClip().tracks.length === 0) {
4479
- console.warn('FBX animation has no valid tracks, stopping');
4480
- this.stopAnimation();
4481
- return;
4482
- }
4483
4333
 
4484
4334
  } else {
4485
4335
 
@@ -4491,200 +4341,10 @@ class TalkingHead {
4491
4341
  if ( fbx && fbx.animations && fbx.animations[ndx] ) {
4492
4342
  let anim = fbx.animations[ndx];
4493
4343
 
4494
- // Get available bone names from avatar skeleton for mapping
4495
- const availableBones = new Set();
4496
- if (this.armature) {
4497
- this.armature.traverse((child) => {
4498
- if (child.isBone || child.type === 'Bone') {
4499
- availableBones.add(child.name);
4500
- }
4501
- });
4502
- }
4503
-
4504
- // Map bone names from FBX to avatar skeleton
4505
- const boneNameMap = new Map();
4506
- const mapBoneName = (fbxBoneName) => {
4507
- // Direct match
4508
- if (availableBones.has(fbxBoneName)) {
4509
- return fbxBoneName;
4510
- }
4511
-
4512
- // Remove common prefixes (mixamorig, CC_Base_, etc.)
4513
- let normalized = fbxBoneName
4514
- .replace(/^mixamorig/i, '')
4515
- .replace(/^CC_Base_/i, '')
4516
- .replace(/^RPM_/i, '');
4517
-
4518
- if (availableBones.has(normalized)) {
4519
- return normalized;
4520
- }
4521
-
4522
- // Pattern-based matching for Ready Player Me / Mixamo
4523
- const lowerNormalized = normalized.toLowerCase();
4524
-
4525
- // Arm bones - pattern matching
4526
- if (lowerNormalized.includes('left') && lowerNormalized.includes('arm')) {
4527
- if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
4528
- if (availableBones.has('LeftForeArm')) return 'LeftForeArm';
4529
- if (availableBones.has('LeftForearm')) return 'LeftForearm';
4530
- } else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
4531
- if (availableBones.has('LeftArm')) return 'LeftArm';
4532
- }
4533
- }
4534
-
4535
- if (lowerNormalized.includes('right') && lowerNormalized.includes('arm')) {
4536
- if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
4537
- if (availableBones.has('RightForeArm')) return 'RightForeArm';
4538
- if (availableBones.has('RightForearm')) return 'RightForearm';
4539
- } else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
4540
- if (availableBones.has('RightArm')) return 'RightArm';
4541
- }
4542
- }
4543
-
4544
- // Hand bones
4545
- if (lowerNormalized.includes('left') && lowerNormalized.includes('hand') &&
4546
- !lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
4547
- !lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
4548
- !lowerNormalized.includes('pinky')) {
4549
- if (availableBones.has('LeftHand')) return 'LeftHand';
4550
- }
4551
-
4552
- if (lowerNormalized.includes('right') && lowerNormalized.includes('hand') &&
4553
- !lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
4554
- !lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
4555
- !lowerNormalized.includes('pinky')) {
4556
- if (availableBones.has('RightHand')) return 'RightHand';
4557
- }
4558
-
4559
- // Shoulder bones
4560
- if (lowerNormalized.includes('left') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
4561
- if (availableBones.has('LeftShoulder')) return 'LeftShoulder';
4562
- }
4563
-
4564
- if (lowerNormalized.includes('right') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
4565
- if (availableBones.has('RightShoulder')) return 'RightShoulder';
4566
- }
4567
-
4568
- // Common bone name mappings
4569
- const mappings = {
4570
- // Arm bones - exact matches
4571
- 'LeftArm': 'LeftArm',
4572
- 'leftArm': 'LeftArm',
4573
- 'LEFTARM': 'LeftArm',
4574
- 'RightArm': 'RightArm',
4575
- 'rightArm': 'RightArm',
4576
- 'RIGHTARM': 'RightArm',
4577
- 'LeftForeArm': 'LeftForeArm',
4578
- 'leftForeArm': 'LeftForeArm',
4579
- 'leftForearm': 'LeftForeArm',
4580
- 'LeftForearm': 'LeftForeArm',
4581
- 'RightForeArm': 'RightForeArm',
4582
- 'rightForeArm': 'RightForeArm',
4583
- 'rightForearm': 'RightForeArm',
4584
- 'RightForearm': 'RightForeArm',
4585
- 'LeftHand': 'LeftHand',
4586
- 'leftHand': 'LeftHand',
4587
- 'RightHand': 'RightHand',
4588
- 'rightHand': 'RightHand',
4589
- 'LeftShoulder': 'LeftShoulder',
4590
- 'leftShoulder': 'LeftShoulder',
4591
- 'RightShoulder': 'RightShoulder',
4592
- 'rightShoulder': 'RightShoulder',
4593
- // Spine
4594
- 'Spine': 'Spine1',
4595
- 'spine': 'Spine1',
4596
- 'Spine1': 'Spine1',
4597
- 'Spine2': 'Spine2',
4598
- // Head/Neck
4599
- 'Head': 'Head',
4600
- 'head': 'Head',
4601
- 'Neck': 'Neck',
4602
- 'neck': 'Neck',
4603
- // Hips
4604
- 'Hips': 'Hips',
4605
- 'hips': 'Hips',
4606
- 'Root': 'Hips',
4607
- 'root': 'Hips',
4608
- };
4609
-
4610
- if (mappings[normalized]) {
4611
- const mapped = mappings[normalized];
4612
- if (availableBones.has(mapped)) {
4613
- return mapped;
4614
- }
4615
- }
4616
-
4617
- // Try case-insensitive match
4618
- for (const boneName of availableBones) {
4619
- if (boneName.toLowerCase() === lowerNormalized) {
4620
- return boneName;
4621
- }
4622
- }
4623
-
4624
- // Try partial match (contains)
4625
- for (const boneName of availableBones) {
4626
- const boneLower = boneName.toLowerCase();
4627
- // Match if normalized contains key parts of bone name
4628
- if ((lowerNormalized.includes('left') && boneLower.includes('left')) ||
4629
- (lowerNormalized.includes('right') && boneLower.includes('right'))) {
4630
- if ((lowerNormalized.includes('arm') && boneLower.includes('arm') && !boneLower.includes('fore')) ||
4631
- (lowerNormalized.includes('forearm') && boneLower.includes('forearm')) ||
4632
- (lowerNormalized.includes('hand') && boneLower.includes('hand') && !boneLower.includes('index') && !boneLower.includes('thumb')) ||
4633
- (lowerNormalized.includes('shoulder') && boneLower.includes('shoulder'))) {
4634
- return boneName;
4635
- }
4636
- }
4637
- }
4638
-
4639
- return null; // No mapping found
4640
- };
4641
-
4642
- // Filter and map animation tracks
4643
- const mappedTracks = [];
4644
- const unmappedBones = new Set();
4645
- anim.tracks.forEach(track => {
4646
- // Remove mixamorig prefix first
4647
- let trackName = track.name.replaceAll('mixamorig', '');
4648
- const trackParts = trackName.split('.');
4649
- const fbxBoneName = trackParts[0];
4650
- const property = trackParts[1];
4651
-
4652
- // Map bone name to avatar skeleton
4653
- const mappedBoneName = mapBoneName(fbxBoneName);
4654
-
4655
- if (mappedBoneName && property) {
4656
- // Create new track with mapped bone name
4657
- const newTrackName = `${mappedBoneName}.${property}`;
4658
- const newTrack = track.clone();
4659
- newTrack.name = newTrackName;
4660
- mappedTracks.push(newTrack);
4661
-
4662
- // Store mapping for logging
4663
- if (fbxBoneName !== mappedBoneName) {
4664
- boneNameMap.set(fbxBoneName, mappedBoneName);
4665
- }
4666
- } else {
4667
- unmappedBones.add(fbxBoneName);
4668
- }
4669
- });
4670
-
4671
- if (unmappedBones.size > 0) {
4672
- console.warn(`⚠️ ${unmappedBones.size} bone(s) could not be mapped:`, Array.from(unmappedBones).sort().join(', '));
4673
- }
4674
-
4675
- // Use mapped tracks if we have any, otherwise use original
4676
- if (mappedTracks.length > 0) {
4677
- anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
4678
- console.log(`✓ Created animation with ${mappedTracks.length} mapped tracks (from ${anim.tracks.length} original tracks)`);
4679
- if (boneNameMap.size > 0) {
4680
- console.log(`✓ Mapped ${boneNameMap.size} bone(s):`,
4681
- Array.from(boneNameMap.entries()).map(([from, to]) => `${from}→${to}`).join(', '));
4682
- }
4683
- }
4684
-
4685
4344
  // Rename and scale Mixamo tracks, create a pose
4686
4345
  const props = {};
4687
4346
  anim.tracks.forEach( t => {
4347
+ t.name = t.name.replaceAll('mixamorig','');
4688
4348
  const ids = t.name.split('.');
4689
4349
  if ( ids[1] === 'position' ) {
4690
4350
  for(let i=0; i<t.values.length; i++ ) {
@@ -4738,14 +4398,6 @@ class TalkingHead {
4738
4398
  this._mixerHandler = null;
4739
4399
  }
4740
4400
 
4741
- // Unlock position if it was locked
4742
- if (this.positionWasLocked) {
4743
- this.unlockAvatarPosition();
4744
- console.log('Position unlocked after FBX animation stopped');
4745
- } else {
4746
- console.log('Position was not locked, no unlock needed');
4747
- }
4748
-
4749
4401
  // Restart gesture
4750
4402
  if ( this.gesture ) {
4751
4403
  for( let [p,v] of Object.entries(this.gesture) ) {
@@ -5096,376 +4748,6 @@ class TalkingHead {
5096
4748
  }
5097
4749
  }
5098
4750
 
5099
- /**
5100
- * Initialize FBX animation loader
5101
- */
5102
- async initializeFBXAnimationLoader() {
5103
- try {
5104
- // Dynamic import to avoid loading issues
5105
- const { FBXAnimationLoader } = await import('./fbxAnimationLoader.js');
5106
- this.fbxAnimationLoader = new FBXAnimationLoader(this.armature);
5107
- console.log('FBX Animation Loader initialized');
5108
- } catch (error) {
5109
- console.warn('FBX Animation Loader not available:', error);
5110
- this.fbxAnimationLoader = null;
5111
- }
5112
- }
5113
-
5114
- /**
5115
- * Set body movement type.
5116
- * @param {string} movement Movement type (idle, walking, prancing, gesturing, dancing, excited).
5117
- */
5118
- setBodyMovement(movement) {
5119
- this.bodyMovement = movement;
5120
-
5121
- // Only set avatar property if avatar exists
5122
- if (this.avatar) {
5123
- this.avatar.bodyMovement = movement;
5124
- }
5125
-
5126
- console.log('Body movement set to:', movement);
5127
-
5128
- // Respect the current showFullAvatar setting instead of forcing it to true
5129
- // Only unlock position when returning to idle
5130
- if (movement === 'idle') {
5131
- // Unlock position when returning to idle
5132
- this.unlockAvatarPosition();
5133
- }
5134
- // Note: We no longer force showFullAvatar to true for body movements
5135
- // The avatar will use whatever showFullAvatar value was set by the user
5136
-
5137
- // Apply body movement animation
5138
- this.applyBodyMovementAnimation();
5139
- }
5140
-
5141
- /**
5142
- * Apply body movement animation based on current movement type.
5143
- */
5144
- async applyBodyMovementAnimation() {
5145
- // Check if avatar is ready
5146
- if (!this.armature || !this.animQueue) {
5147
- console.log('Avatar not ready for body movement animations');
5148
- return;
5149
- }
5150
-
5151
- console.log('Avatar is running:', this.isRunning);
5152
- console.log('Animation queue exists:', !!this.animQueue);
5153
-
5154
- // Remove existing body movement animations
5155
- const beforeLength = this.animQueue.length;
5156
- this.animQueue = this.animQueue.filter(anim => !anim.template.name.startsWith('bodyMovement'));
5157
- const afterLength = this.animQueue.length;
5158
- console.log(`Filtered animation queue: ${beforeLength} -> ${afterLength} animations`);
5159
-
5160
- if (this.bodyMovement === 'idle') {
5161
- // Stop FBX animations if any
5162
- if (this.fbxAnimationLoader) {
5163
- this.fbxAnimationLoader.stopCurrentAnimation();
5164
- }
5165
- return; // No body movement for idle
5166
- }
5167
-
5168
- // Try to use FBX animations first
5169
- if (this.fbxAnimationLoader) {
5170
- try {
5171
- await this.fbxAnimationLoader.playGestureAnimation(this.bodyMovement, this.movementIntensity);
5172
- console.log('Applied FBX body movement animation:', this.bodyMovement);
5173
- return; // Successfully applied FBX animation
5174
- } catch (error) {
5175
- console.warn('FBX animation failed, falling back to code animation:', error);
5176
- }
5177
- }
5178
-
5179
- // Fallback to code-based animations
5180
- const movementAnim = this.createBodyMovementAnimation(this.bodyMovement);
5181
- console.log('Created movement animation:', movementAnim);
5182
- if (movementAnim) {
5183
- try {
5184
- // Use animFactory to create proper animation object
5185
- const animObj = this.animFactory(movementAnim, true); // true for looping
5186
-
5187
- // Validate the animation object before adding
5188
- if (animObj && animObj.ts && animObj.ts.length > 0) {
5189
- this.animQueue.push(animObj);
5190
- console.log('Applied code-based body movement animation:', this.bodyMovement);
5191
- console.log('Animation queue length:', this.animQueue.length);
5192
- console.log('Animation object:', animObj);
5193
- } else {
5194
- console.error('Invalid animation object created for:', this.bodyMovement);
5195
- console.error('Animation object:', animObj);
5196
- }
5197
- } catch (error) {
5198
- console.error('Error creating body movement animation:', error);
5199
- }
5200
- }
5201
- }
5202
-
5203
- /**
5204
- * Lock avatar position to prevent movement during animations.
5205
- */
5206
- lockAvatarPosition() {
5207
- if (!this.armature) {
5208
- console.warn('Cannot lock position: armature not available');
5209
- return;
5210
- }
5211
-
5212
- // Store the original position if not already stored
5213
- if (!this.originalPosition) {
5214
- this.originalPosition = {
5215
- x: this.armature.position.x,
5216
- y: this.armature.position.y,
5217
- z: this.armature.position.z
5218
- };
5219
- console.log('Original position stored:', this.originalPosition);
5220
- }
5221
-
5222
- // Lock the avatar at its CURRENT position (don't move it)
5223
- this.lockedPosition = {
5224
- x: this.armature.position.x,
5225
- y: this.armature.position.y,
5226
- z: this.armature.position.z
5227
- };
5228
-
5229
- console.log('Avatar position locked at current position:', this.lockedPosition);
5230
- }
5231
-
5232
- /**
5233
- * Unlock avatar position and restore original position.
5234
- */
5235
- unlockAvatarPosition() {
5236
- if (this.armature && this.originalPosition) {
5237
- // Restore avatar to its original position before locking
5238
- this.armature.position.set(
5239
- this.originalPosition.x,
5240
- this.originalPosition.y,
5241
- this.originalPosition.z
5242
- );
5243
- console.log('Avatar position restored to original:', this.originalPosition);
5244
- } else if (this.armature) {
5245
- // Fallback: reset to center if no original position was stored
5246
- this.armature.position.set(0, 0, 0);
5247
- console.log('Avatar position reset to center (0,0,0)');
5248
- }
5249
- this.lockedPosition = null;
5250
- this.originalPosition = null; // Clear original position after unlock
5251
- console.log('Avatar position unlocked');
5252
- }
5253
-
5254
- /**
5255
- * Ensure avatar stays at locked position.
5256
- */
5257
- maintainLockedPosition() {
5258
- if (this.lockedPosition && this.armature) {
5259
- // Enforce the locked position - keep avatar exactly where it was locked
5260
- // This prevents FBX animations from moving the avatar
5261
- this.armature.position.set(
5262
- this.lockedPosition.x,
5263
- this.lockedPosition.y,
5264
- this.lockedPosition.z
5265
- );
5266
- }
5267
- }
5268
-
5269
- /**
5270
- * Create body movement animation.
5271
- * @param {string} movementType Movement type.
5272
- * @returns {Object} Animation object.
5273
- */
5274
- createBodyMovementAnimation(movementType) {
5275
- const intensity = this.movementIntensity || 0.5;
5276
-
5277
- const movementAnimations = {
5278
- walking: {
5279
- name: 'bodyMovement_walking',
5280
- delay: [500, 2000],
5281
- dt: [800, 1200],
5282
- vs: {
5283
- bodyRotateY: [-0.1 * intensity, 0.1 * intensity, 0],
5284
- bodyRotateZ: [-0.05 * intensity, 0.05 * intensity, 0],
5285
- bodyRotateX: [-0.02 * intensity, 0.02 * intensity, 0]
5286
- }
5287
- },
5288
- prancing: {
5289
- name: 'bodyMovement_prancing',
5290
- delay: [300, 1000],
5291
- dt: [400, 800],
5292
- vs: {
5293
- bodyRotateY: [-0.15 * intensity, 0.15 * intensity, 0],
5294
- bodyRotateZ: [-0.08 * intensity, 0.08 * intensity, 0],
5295
- bodyRotateX: [-0.05 * intensity, 0.05 * intensity, 0]
5296
- }
5297
- },
5298
- gesturing: {
5299
- name: 'bodyMovement_gesturing',
5300
- delay: [400, 1500],
5301
- dt: [600, 1000],
5302
- vs: {
5303
- bodyRotateY: [-0.08 * intensity, 0.08 * intensity, 0],
5304
- bodyRotateZ: [-0.03 * intensity, 0.03 * intensity, 0]
5305
- }
5306
- },
5307
- dancing: {
5308
- name: 'bodyMovement_dancing',
5309
- delay: [200, 600],
5310
- dt: [400, 800],
5311
- vs: {
5312
- bodyRotateY: [-0.25 * intensity, 0.25 * intensity, 0],
5313
- bodyRotateZ: [-0.15 * intensity, 0.15 * intensity, 0],
5314
- bodyRotateX: [-0.1 * intensity, 0.1 * intensity, 0]
5315
- }
5316
- },
5317
- dancing2: {
5318
- name: 'bodyMovement_dancing2',
5319
- delay: [150, 500],
5320
- dt: [300, 700],
5321
- vs: {
5322
- bodyRotateY: [-0.3 * intensity, 0.3 * intensity, 0],
5323
- bodyRotateZ: [-0.2 * intensity, 0.2 * intensity, 0],
5324
- bodyRotateX: [-0.12 * intensity, 0.12 * intensity, 0]
5325
- }
5326
- },
5327
- dancing3: {
5328
- name: 'bodyMovement_dancing3',
5329
- delay: [100, 400],
5330
- dt: [200, 600],
5331
- vs: {
5332
- bodyRotateY: [-0.35 * intensity, 0.35 * intensity, 0],
5333
- bodyRotateZ: [-0.25 * intensity, 0.25 * intensity, 0],
5334
- bodyRotateX: [-0.15 * intensity, 0.15 * intensity, 0]
5335
- }
5336
- },
5337
- excited: {
5338
- name: 'bodyMovement_excited',
5339
- delay: [200, 600],
5340
- dt: [300, 700],
5341
- vs: {
5342
- bodyRotateY: [-0.12 * intensity, 0.12 * intensity, 0],
5343
- bodyRotateZ: [-0.06 * intensity, 0.06 * intensity, 0],
5344
- bodyRotateX: [-0.04 * intensity, 0.04 * intensity, 0]
5345
- }
5346
- },
5347
- happy: {
5348
- name: 'bodyMovement_happy',
5349
- delay: [300, 800],
5350
- dt: [500, 1000],
5351
- vs: {
5352
- bodyRotateY: [-0.08 * intensity, 0.08 * intensity, 0],
5353
- bodyRotateZ: [-0.04 * intensity, 0.04 * intensity, 0],
5354
- bodyRotateX: [-0.02 * intensity, 0.02 * intensity, 0]
5355
- }
5356
- },
5357
- surprised: {
5358
- name: 'bodyMovement_surprised',
5359
- delay: [100, 300],
5360
- dt: [200, 500],
5361
- vs: {
5362
- bodyRotateY: [-0.05 * intensity, 0.05 * intensity, 0],
5363
- bodyRotateZ: [-0.03 * intensity, 0.03 * intensity, 0],
5364
- bodyRotateX: [-0.01 * intensity, 0.01 * intensity, 0]
5365
- }
5366
- },
5367
- thinking: {
5368
- name: 'bodyMovement_thinking',
5369
- delay: [800, 2000],
5370
- dt: [1000, 1500],
5371
- vs: {
5372
- bodyRotateY: [-0.06 * intensity, 0.06 * intensity, 0],
5373
- bodyRotateZ: [-0.03 * intensity, 0.03 * intensity, 0],
5374
- bodyRotateX: [-0.02 * intensity, 0.02 * intensity, 0]
5375
- }
5376
- },
5377
- nodding: {
5378
- name: 'bodyMovement_nodding',
5379
- delay: [400, 800],
5380
- dt: [300, 600],
5381
- vs: {
5382
- bodyRotateX: [-0.1 * intensity, 0.1 * intensity, 0],
5383
- bodyRotateY: [-0.02 * intensity, 0.02 * intensity, 0]
5384
- }
5385
- },
5386
- shaking: {
5387
- name: 'bodyMovement_shaking',
5388
- delay: [200, 400],
5389
- dt: [150, 300],
5390
- vs: {
5391
- bodyRotateY: [-0.15 * intensity, 0.15 * intensity, 0],
5392
- bodyRotateZ: [-0.05 * intensity, 0.05 * intensity, 0]
5393
- }
5394
- },
5395
- celebration: {
5396
- name: 'bodyMovement_celebration',
5397
- delay: [100, 300],
5398
- dt: [200, 500],
5399
- vs: {
5400
- bodyRotateY: [-0.2 * intensity, 0.2 * intensity, 0],
5401
- bodyRotateZ: [-0.1 * intensity, 0.1 * intensity, 0],
5402
- bodyRotateX: [-0.08 * intensity, 0.08 * intensity, 0]
5403
- }
5404
- },
5405
- energetic: {
5406
- name: 'bodyMovement_energetic',
5407
- delay: [150, 400],
5408
- dt: [250, 500],
5409
- vs: {
5410
- bodyRotateY: [-0.18 * intensity, 0.18 * intensity, 0],
5411
- bodyRotateZ: [-0.12 * intensity, 0.12 * intensity, 0],
5412
- bodyRotateX: [-0.08 * intensity, 0.08 * intensity, 0]
5413
- }
5414
- },
5415
- swaying: {
5416
- name: 'bodyMovement_swaying',
5417
- delay: [600, 1200],
5418
- dt: [800, 1000],
5419
- vs: {
5420
- bodyRotateY: [-0.1 * intensity, 0.1 * intensity, 0],
5421
- bodyRotateZ: [-0.05 * intensity, 0.05 * intensity, 0]
5422
- }
5423
- },
5424
- bouncing: {
5425
- name: 'bodyMovement_bouncing',
5426
- delay: [300, 600],
5427
- dt: [400, 700],
5428
- vs: {
5429
- bodyRotateY: [-0.05 * intensity, 0.05 * intensity, 0]
5430
- }
5431
- }
5432
- };
5433
-
5434
- // Handle dance variations
5435
- if (movementType === 'dancing') {
5436
- const danceVariations = ['dancing', 'dancing2', 'dancing3'];
5437
- const randomDance = danceVariations[Math.floor(Math.random() * danceVariations.length)];
5438
- return movementAnimations[randomDance] || movementAnimations['dancing'];
5439
- }
5440
-
5441
- return movementAnimations[movementType] || null;
5442
- }
5443
-
5444
- /**
5445
- * Set movement intensity.
5446
- * @param {number} intensity Movement intensity (0-1).
5447
- */
5448
- setMovementIntensity(intensity) {
5449
- this.movementIntensity = Math.max(0, Math.min(1, intensity));
5450
-
5451
- // Only set avatar property if avatar exists
5452
- if (this.avatar) {
5453
- this.avatar.movementIntensity = this.movementIntensity;
5454
- }
5455
-
5456
- console.log('Movement intensity set to:', this.movementIntensity);
5457
-
5458
- // Update FBX animation intensity if available
5459
- if (this.fbxAnimationLoader) {
5460
- this.fbxAnimationLoader.setIntensity(this.movementIntensity);
5461
- }
5462
-
5463
- // Reapply body movement animation with new intensity
5464
- if (this.bodyMovement && this.bodyMovement !== 'idle') {
5465
- this.applyBodyMovementAnimation();
5466
- }
5467
- }
5468
-
5469
4751
  /**
5470
4752
  * Dispose the instance.
5471
4753
  */
@@ -5532,12 +4814,6 @@ class TalkingHead {
5532
4814
 
5533
4815
  this.clearThree( this.ikMesh );
5534
4816
  this.dynamicbones.dispose();
5535
-
5536
- // Clean up FBX animation loader
5537
- if (this.fbxAnimationLoader) {
5538
- this.fbxAnimationLoader.stopCurrentAnimation();
5539
- this.fbxAnimationLoader = null;
5540
- }
5541
4817
 
5542
4818
  // DOM
5543
4819
  this.nodeAvatar = null;