@sage-rsc/talking-head-react 1.4.0 → 1.4.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.4.0",
3
+ "version": "1.4.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",
@@ -1128,7 +1128,7 @@ class TalkingHead {
1128
1128
  });
1129
1129
  } else {
1130
1130
  if (obj.material.map) obj.material.map.dispose();
1131
- obj.material.dispose();
1131
+ obj.material.dispose();
1132
1132
  }
1133
1133
  }
1134
1134
  }
@@ -1244,7 +1244,7 @@ class TalkingHead {
1244
1244
  this.lockedPosition = null;
1245
1245
  this.originalPosition = null;
1246
1246
  this.positionWasLocked = false;
1247
-
1247
+
1248
1248
  // Initialize FBX animation loader
1249
1249
  this.fbxAnimationLoader = null;
1250
1250
 
@@ -1256,7 +1256,7 @@ class TalkingHead {
1256
1256
  this.mixer.removeEventListener('finished', this._mixerHandler);
1257
1257
  this.mixer.stopAllAction();
1258
1258
  this.mixer.uncacheRoot(this.armature);
1259
- this.mixer = null;
1259
+ this.mixer = null;
1260
1260
  this._mixerHandler = null;
1261
1261
  }
1262
1262
  if ( this.isAvatarOnly ) {
@@ -1512,6 +1512,11 @@ class TalkingHead {
1512
1512
  this.controlsEnd = new THREE.Vector3(x, y, 0);
1513
1513
  this.cameraEnd = new THREE.Vector3(x, y, z).applyEuler( new THREE.Euler( cameraRotateX, cameraRotateY, 0 ) );
1514
1514
 
1515
+ // Guard against null controls (e.g., in avatarOnly mode or before initialization)
1516
+ if ( !this.controls ) {
1517
+ return;
1518
+ }
1519
+
1515
1520
  if ( this.cameraClock === null ) {
1516
1521
  this.controls.target.copy( this.controlsEnd );
1517
1522
  this.camera.position.copy( this.cameraEnd );
@@ -1585,7 +1590,9 @@ class TalkingHead {
1585
1590
  this.camera.aspect = this.nodeAvatar.clientWidth / this.nodeAvatar.clientHeight;
1586
1591
  this.camera.updateProjectionMatrix();
1587
1592
  this.renderer.setSize( this.nodeAvatar.clientWidth, this.nodeAvatar.clientHeight );
1593
+ if ( this.controls ) {
1588
1594
  this.controls.update();
1595
+ }
1589
1596
  this.render();
1590
1597
  }
1591
1598
  }
@@ -2040,6 +2047,11 @@ class TalkingHead {
2040
2047
  */
2041
2048
  setPoseFromTemplate(template, ms=2000) {
2042
2049
 
2050
+ // Guard against disposal: check if required objects exist
2051
+ if (!this.poseFactory || !this.poseTemplates) {
2052
+ return;
2053
+ }
2054
+
2043
2055
  // Special cases
2044
2056
  const isIntermediate = template && this.poseTarget && this.poseTarget.template && ((this.poseTarget.template.standing && template.lying) || (this.poseTarget.template.lying && template.standing));
2045
2057
  const isSameTemplate = template && (template === this.poseCurrentTemplate);
@@ -2053,13 +2065,18 @@ class TalkingHead {
2053
2065
  this.setPoseFromTemplate(template,ms);
2054
2066
  }, duration);
2055
2067
  } else {
2056
- this.poseCurrentTemplate = template || this.poseCurrentTemplate;
2068
+ this.poseCurrentTemplate = template || this.poseCurrentTemplate;
2057
2069
  }
2058
2070
 
2059
2071
  // Set target
2060
2072
  this.poseTarget = this.poseFactory(this.poseCurrentTemplate, duration);
2061
2073
  this.poseWeightOnLeft = true;
2062
2074
 
2075
+ // Guard: ensure poseTarget was created successfully
2076
+ if (!this.poseTarget || !this.poseTarget.props) {
2077
+ return;
2078
+ }
2079
+
2063
2080
  // Mirror properties, if necessary
2064
2081
  if ( (!isSameTemplate && !isWeightOnLeft) || (isSameTemplate && isWeightOnLeft ) ) {
2065
2082
  this.poseTarget.props = this.mirrorPose(this.poseTarget.props);
@@ -2078,13 +2095,16 @@ class TalkingHead {
2078
2095
  }
2079
2096
 
2080
2097
  // Make sure deltas are included in the target
2098
+ // Guard against disposal: check if poseBase and its props exist
2099
+ if (this.poseBase && this.poseBase.props && this.poseDelta && this.poseDelta.props) {
2081
2100
  Object.keys(this.poseDelta.props).forEach( key => {
2082
- if ( !this.poseTarget.props.hasOwnProperty(key) ) {
2101
+ if ( !this.poseTarget.props.hasOwnProperty(key) && this.poseBase.props[key] ) {
2083
2102
  this.poseTarget.props[key] = this.poseBase.props[key].clone();
2084
2103
  this.poseTarget.props[key].t = this.animClock;
2085
2104
  this.poseTarget.props[key].d = duration;
2086
2105
  }
2087
2106
  });
2107
+ }
2088
2108
 
2089
2109
  }
2090
2110
 
@@ -2816,7 +2836,7 @@ class TalkingHead {
2816
2836
  } else {
2817
2837
 
2818
2838
  // Camera
2819
- if ( this.cameraClock !== null && this.cameraClock < 1000 ) {
2839
+ if ( this.controls && this.cameraClock !== null && this.cameraClock < 1000 ) {
2820
2840
  this.cameraClock += dt;
2821
2841
  if ( this.cameraClock > 1000 ) this.cameraClock = 1000;
2822
2842
  let s = new THREE.Spherical().setFromVector3(this.cameraStart);
@@ -2841,7 +2861,7 @@ class TalkingHead {
2841
2861
  }
2842
2862
 
2843
2863
  // Autorotate
2844
- if ( this.controls.autoRotate ) this.controls.update();
2864
+ if ( this.controls && this.controls.autoRotate ) this.controls.update();
2845
2865
 
2846
2866
  // Statistics end
2847
2867
  if ( this.stats ) {
@@ -2868,17 +2888,24 @@ class TalkingHead {
2868
2888
  }
2869
2889
 
2870
2890
  /**
2871
- * Get lip-sync processor based on language. Import module dynamically.
2891
+ * Get lip-sync processor based on language. Use statically imported modules.
2872
2892
  * @param {string} lang Language
2873
- * @param {string} [path="./"] Module path
2893
+ * @param {string} [path="./"] Module path (ignored, using static imports)
2874
2894
  */
2875
2895
  lipsyncGetProcessor(lang, path="./") {
2876
2896
  if ( !this.lipsync.hasOwnProperty(lang) ) {
2877
- const moduleName = path + 'lipsync-' + lang.toLowerCase() + '.mjs';
2897
+ const langLower = lang.toLowerCase();
2898
+ // Use statically imported modules from LIPSYNC_MODULES
2899
+ if (LIPSYNC_MODULES[langLower]) {
2878
2900
  const className = 'Lipsync' + lang.charAt(0).toUpperCase() + lang.slice(1);
2879
- import(moduleName).then( module => {
2880
- this.lipsync[lang] = new module[className];
2881
- });
2901
+ if (LIPSYNC_MODULES[langLower][className]) {
2902
+ this.lipsync[lang] = new LIPSYNC_MODULES[langLower][className];
2903
+ } else {
2904
+ console.warn(`Lipsync class ${className} not found in module for language ${lang}`);
2905
+ }
2906
+ } else {
2907
+ console.warn(`Lipsync module for language ${lang} not found in static imports`);
2908
+ }
2882
2909
  }
2883
2910
  }
2884
2911
 
@@ -4326,10 +4353,10 @@ class TalkingHead {
4326
4353
  setSlowdownRate(k) {
4327
4354
  this.animSlowdownRate = k;
4328
4355
  if ( this.audioSpeechSource ) {
4329
- this.audioSpeechSource.playbackRate.value = 1 / this.animSlowdownRate;
4356
+ this.audioSpeechSource.playbackRate.value = 1 / this.animSlowdownRate;
4330
4357
  }
4331
4358
  if ( this.audioBackgroundSource ) {
4332
- this.audioBackgroundSource.playbackRate.value = 1 / this.animSlowdownRate;
4359
+ this.audioBackgroundSource.playbackRate.value = 1 / this.animSlowdownRate;
4333
4360
  }
4334
4361
  }
4335
4362
 
@@ -4338,7 +4365,7 @@ class TalkingHead {
4338
4365
  * @return {numeric} Autorotate speed.
4339
4366
  */
4340
4367
  getAutoRotateSpeed(k) {
4341
- return this.controls.autoRotateSpeed;
4368
+ return this.controls ? this.controls.autoRotateSpeed : 0;
4342
4369
  }
4343
4370
 
4344
4371
  /**
@@ -4346,8 +4373,10 @@ class TalkingHead {
4346
4373
  * @param {numeric} speed Autorotate speed, e.g. value 2 = 30 secs per orbit at 60fps.
4347
4374
  */
4348
4375
  setAutoRotateSpeed(speed) {
4349
- this.controls.autoRotateSpeed = speed;
4350
- this.controls.autoRotate = (speed > 0);
4376
+ if ( this.controls ) {
4377
+ this.controls.autoRotateSpeed = speed;
4378
+ this.controls.autoRotate = (speed > 0);
4379
+ }
4351
4380
  }
4352
4381
 
4353
4382
  /**
@@ -4737,7 +4766,7 @@ class TalkingHead {
4737
4766
  this.mixer = null;
4738
4767
  this._mixerHandler = null;
4739
4768
  }
4740
-
4769
+
4741
4770
  // Unlock position if it was locked
4742
4771
  if (this.positionWasLocked) {
4743
4772
  this.unlockAvatarPosition();
@@ -4747,7 +4776,7 @@ class TalkingHead {
4747
4776
  }
4748
4777
 
4749
4778
  // Restart gesture
4750
- if ( this.gesture ) {
4779
+ if ( this.gesture && this.poseTarget && this.poseTarget.props ) {
4751
4780
  for( let [p,v] of Object.entries(this.gesture) ) {
4752
4781
  v.t = this.animClock;
4753
4782
  v.d = 1000;
@@ -4760,11 +4789,17 @@ class TalkingHead {
4760
4789
  }
4761
4790
 
4762
4791
  // Restart pose animation
4792
+ if ( this.animQueue ) {
4763
4793
  let anim = this.animQueue.find( x => x.template.name === 'pose' );
4764
4794
  if ( anim ) {
4765
4795
  anim.ts[0] = this.animClock;
4766
4796
  }
4797
+ }
4798
+
4799
+ // Only call setPoseFromTemplate if poseFactory exists (not disposed)
4800
+ if ( this.poseFactory ) {
4767
4801
  this.setPoseFromTemplate( null );
4802
+ }
4768
4803
 
4769
4804
  }
4770
4805
 
@@ -4799,7 +4834,7 @@ class TalkingHead {
4799
4834
  this.mixer.removeEventListener('finished', this._mixerHandler);
4800
4835
  this.mixer.stopAllAction();
4801
4836
  this.mixer.uncacheRoot(this.armature);
4802
- this.mixer = null;
4837
+ this.mixer = null;
4803
4838
  this._mixerHandler = null;
4804
4839
  }
4805
4840
  let anim = this.animQueue.find( x => x.template.name === 'pose' );
@@ -5514,7 +5549,7 @@ class TalkingHead {
5514
5549
  this.clearThree(this.scene);
5515
5550
  this.resizeobserver.disconnect();
5516
5551
  this.resizeobserver = null;
5517
-
5552
+
5518
5553
  if ( this.renderer ) {
5519
5554
  this.renderer.dispose();
5520
5555
  const gl = this.renderer.getContext();
@@ -5527,12 +5562,12 @@ class TalkingHead {
5527
5562
  if ( this.controls ) {
5528
5563
  this.controls.dispose();
5529
5564
  this.controls = null;
5530
- }
5565
+ }
5531
5566
  }
5532
5567
 
5533
5568
  this.clearThree( this.ikMesh );
5534
5569
  this.dynamicbones.dispose();
5535
-
5570
+
5536
5571
  // Clean up FBX animation loader
5537
5572
  if (this.fbxAnimationLoader) {
5538
5573
  this.fbxAnimationLoader.stopCurrentAnimation();
@@ -4448,7 +4448,7 @@ class TalkingHead {
4448
4448
 
4449
4449
  // Use existing mixer or create new one if none exists
4450
4450
  if (!this.mixer) {
4451
- this.mixer = new THREE.AnimationMixer(this.armature);
4451
+ this.mixer = new THREE.AnimationMixer(this.armature);
4452
4452
  console.log('Created new mixer for FBX animation');
4453
4453
  } else {
4454
4454
  console.log('Using existing mixer for FBX animation, preserving morph targets');
@@ -4465,7 +4465,7 @@ class TalkingHead {
4465
4465
  this.currentFBXAction = action;
4466
4466
 
4467
4467
  try {
4468
- action.fadeIn(0.5).play();
4468
+ action.fadeIn(0.5).play();
4469
4469
  console.log('FBX animation started successfully:', url);
4470
4470
  } catch (error) {
4471
4471
  console.warn('FBX animation failed to start:', error);