@sage-rsc/talking-head-react 1.4.9 → 1.5.0

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsxs as Pe, jsx as ye } from "react/jsx-runtime";
2
- import { forwardRef as Me, useRef as W, useState as pe, useEffect as ce, useCallback as O, useImperativeHandle as Fe, useLayoutEffect as Xe } from "react";
2
+ import { forwardRef as Me, useRef as W, useState as pe, useEffect as ce, useCallback as O, useImperativeHandle as Fe, useLayoutEffect as Ze } from "react";
3
3
  import * as y from "three";
4
4
  import { OrbitControls as je } from "three/addons/controls/OrbitControls.js";
5
5
  import { GLTFLoader as Ye } from "three/addons/loaders/GLTFLoader.js";
@@ -2629,7 +2629,7 @@ const ct = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2629
2629
  fr: rt,
2630
2630
  fi: ut,
2631
2631
  lt: ct
2632
- }, $ = new y.Quaternion(), Z = new y.Euler(), ve = new y.Vector3(), Re = new y.Vector3(), We = new y.Box3();
2632
+ }, $ = new y.Quaternion(), X = new y.Euler(), ve = new y.Vector3(), Re = new y.Vector3(), We = new y.Box3();
2633
2633
  new y.Matrix4();
2634
2634
  new y.Matrix4();
2635
2635
  new y.Vector3();
@@ -4418,8 +4418,17 @@ class Be {
4418
4418
  if (!this.armature) return;
4419
4419
  const t = this.armature.getObjectByName("LeftShoulder"), e = this.armature.getObjectByName("RightShoulder");
4420
4420
  if (!t || !e) return;
4421
- const n = new y.Euler(), i = new y.Quaternion(), s = this.mixer && this.currentFBXAction && this.currentFBXAction.isRunning(), o = 0.5, l = 0.6;
4422
- t.quaternion && (n.setFromQuaternion(t.quaternion, "XYZ"), n.x > l ? n.x = o : n.x > o && (s ? n.x = o : n.x = o + (n.x - o) * 0.1), i.setFromEuler(n, "XYZ"), t.quaternion.copy(i), t.updateMatrixWorld(!0)), e.quaternion && (n.setFromQuaternion(e.quaternion, "XYZ"), n.x > l ? n.x = o : n.x > o && (s ? n.x = o : n.x = o + (n.x - o) * 0.1), i.setFromEuler(n, "XYZ"), e.quaternion.copy(i), e.updateMatrixWorld(!0));
4421
+ const n = new y.Euler(), i = new y.Quaternion(), o = this.mixer && this.currentFBXAction && this.currentFBXAction.isRunning() ? 0.5 : 0.6, l = 0.7;
4422
+ if (t.quaternion) {
4423
+ n.setFromQuaternion(t.quaternion, "XYZ");
4424
+ const h = n.x;
4425
+ n.x > l ? n.x = l + (n.x - l) * (1 - o) : n.x > 0.5 && (n.x = n.x * (1 - o * 0.5)), Math.abs(n.x - h) > 0.01 && (i.setFromEuler(n, "XYZ"), t.quaternion.copy(i), t.updateMatrixWorld(!0));
4426
+ }
4427
+ if (e.quaternion) {
4428
+ n.setFromQuaternion(e.quaternion, "XYZ");
4429
+ const h = n.x;
4430
+ n.x > l ? n.x = l + (n.x - l) * (1 - o) : n.x > 0.5 && (n.x = n.x * (1 - o * 0.5)), Math.abs(n.x - h) > 0.01 && (i.setFromEuler(n, "XYZ"), e.quaternion.copy(i), e.updateMatrixWorld(!0));
4431
+ }
4423
4432
  }
4424
4433
  /**
4425
4434
  * Update avatar pose deltas
@@ -4427,9 +4436,9 @@ class Be {
4427
4436
  updatePoseDelta() {
4428
4437
  for (const [t, e] of Object.entries(this.poseDelta.props)) {
4429
4438
  if (e.x === 0 && e.y === 0 && e.z === 0) continue;
4430
- Z.set(e.x, e.y, e.z);
4439
+ X.set(e.x, e.y, e.z);
4431
4440
  const n = this.poseAvatar.props[t];
4432
- n.isQuaternion ? ($.setFromEuler(Z), n.multiply($)) : n.isVector3 && n.add(Z);
4441
+ n.isQuaternion ? ($.setFromEuler(X), n.multiply($)) : n.isVector3 && n.add(X);
4433
4442
  }
4434
4443
  }
4435
4444
  /**
@@ -5201,7 +5210,7 @@ class Be {
5201
5210
  }, i.x ? new y.Vector3(i.x, i.y, i.z) : null, !0, i.d);
5202
5211
  break;
5203
5212
  }
5204
- if ((h || r) && (Z.setFromQuaternion(this.poseAvatar.props["Head.quaternion"]), Z.x = Math.max(-0.9, Math.min(0.9, 2 * Z.x - 0.5)), Z.y = Math.max(-0.9, Math.min(0.9, -2.5 * Z.y)), h ? (Object.assign(this.mtAvatar.eyesLookDown, { system: Z.x < 0 ? -Z.x : 0, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyesLookUp, { system: Z.x < 0 ? 0 : Z.x, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookInLeft, { system: Z.y < 0 ? -Z.y : 0, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookOutLeft, { system: Z.y < 0 ? 0 : Z.y, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookInRight, { system: Z.y < 0 ? 0 : Z.y, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookOutRight, { system: Z.y < 0 ? -Z.y : 0, needsUpdate: !0 }), r && (n = -this.mtAvatar.bodyRotateY.value, i = this.gaussianRandom(-0.2, 0.2), this.animQueue.push(this.animFactory({
5213
+ if ((h || r) && (X.setFromQuaternion(this.poseAvatar.props["Head.quaternion"]), X.x = Math.max(-0.9, Math.min(0.9, 2 * X.x - 0.5)), X.y = Math.max(-0.9, Math.min(0.9, -2.5 * X.y)), h ? (Object.assign(this.mtAvatar.eyesLookDown, { system: X.x < 0 ? -X.x : 0, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyesLookUp, { system: X.x < 0 ? 0 : X.x, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookInLeft, { system: X.y < 0 ? -X.y : 0, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookOutLeft, { system: X.y < 0 ? 0 : X.y, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookInRight, { system: X.y < 0 ? 0 : X.y, needsUpdate: !0 }), Object.assign(this.mtAvatar.eyeLookOutRight, { system: X.y < 0 ? -X.y : 0, needsUpdate: !0 }), r && (n = -this.mtAvatar.bodyRotateY.value, i = this.gaussianRandom(-0.2, 0.2), this.animQueue.push(this.animFactory({
5205
5214
  name: "headmove",
5206
5215
  dt: [[1e3, 2e3], [1e3, 2e3, 1, 2], [1e3, 2e3], [1e3, 2e3, 1, 2]],
5207
5216
  vs: {
@@ -6190,10 +6199,10 @@ class Be {
6190
6199
  }
6191
6200
  this.objectLeftEye.updateMatrixWorld(!0), this.objectRightEye.updateMatrixWorld(!0), ve.setFromMatrixPosition(this.objectLeftEye.matrixWorld), Re.setFromMatrixPosition(this.objectRightEye.matrixWorld), ve.add(Re).divideScalar(2), $.copy(this.armature.quaternion), $.multiply(this.poseTarget.props["Hips.quaternion"]), $.multiply(this.poseTarget.props["Spine.quaternion"]), $.multiply(this.poseTarget.props["Spine1.quaternion"]), $.multiply(this.poseTarget.props["Spine2.quaternion"]), $.multiply(this.poseTarget.props["Neck.quaternion"]), $.multiply(this.poseTarget.props["Head.quaternion"]);
6192
6201
  const n = new y.Vector3().subVectors(e, ve).normalize(), i = Math.atan2(n.x, n.z), s = Math.asin(-n.y);
6193
- Z.set(s, i, 0, "YXZ");
6194
- const l = new y.Quaternion().setFromEuler(Z), h = new y.Quaternion().copy(l).multiply($.clone().invert());
6195
- Z.setFromQuaternion(h, "YXZ");
6196
- let r = Z.x / (40 / 24) + 0.2, u = Z.y / (9 / 4), a = Math.min(0.6, Math.max(-0.3, r)), d = Math.min(0.8, Math.max(-0.8, u)), c = (Math.random() - 0.5) / 4, g = (Math.random() - 0.5) / 4;
6202
+ X.set(s, i, 0, "YXZ");
6203
+ const l = new y.Quaternion().setFromEuler(X), h = new y.Quaternion().copy(l).multiply($.clone().invert());
6204
+ X.setFromQuaternion(h, "YXZ");
6205
+ let r = X.x / (40 / 24) + 0.2, u = X.y / (9 / 4), a = Math.min(0.6, Math.max(-0.3, r)), d = Math.min(0.8, Math.max(-0.8, u)), c = (Math.random() - 0.5) / 4, g = (Math.random() - 0.5) / 4;
6197
6206
  if (t) {
6198
6207
  let x = this.animQueue.findIndex((k) => k.template.name === "lookat");
6199
6208
  x !== -1 && this.animQueue.splice(x, 1);
@@ -6228,8 +6237,8 @@ class Be {
6228
6237
  const s = new y.Vector3().setFromMatrixPosition(this.objectLeftEye.matrixWorld), o = new y.Vector3().setFromMatrixPosition(this.objectRightEye.matrixWorld), l = new y.Vector3().addVectors(s, o).divideScalar(2);
6229
6238
  l.project(this.camera);
6230
6239
  let h = (l.x + 1) / 2 * i.width + i.left, r = -(l.y - 1) / 2 * i.height + i.top;
6231
- t === null && (t = h), e === null && (e = r), $.copy(this.armature.quaternion), $.multiply(this.poseTarget.props["Hips.quaternion"]), $.multiply(this.poseTarget.props["Spine.quaternion"]), $.multiply(this.poseTarget.props["Spine1.quaternion"]), $.multiply(this.poseTarget.props["Spine2.quaternion"]), $.multiply(this.poseTarget.props["Neck.quaternion"]), $.multiply(this.poseTarget.props["Head.quaternion"]), Z.setFromQuaternion($);
6232
- let u = Z.x / (40 / 24), a = Z.y / (9 / 4), d = Math.min(0.4, Math.max(-0.4, this.camera.rotation.x)), c = Math.min(0.4, Math.max(-0.4, this.camera.rotation.y)), g = Math.max(window.innerWidth - h, h), x = Math.max(window.innerHeight - r, r), f = this.convertRange(e, [r - x, r + x], [-0.3, 0.6]) - u + d, k = this.convertRange(t, [h - g, h + g], [-0.8, 0.8]) - a + c;
6240
+ t === null && (t = h), e === null && (e = r), $.copy(this.armature.quaternion), $.multiply(this.poseTarget.props["Hips.quaternion"]), $.multiply(this.poseTarget.props["Spine.quaternion"]), $.multiply(this.poseTarget.props["Spine1.quaternion"]), $.multiply(this.poseTarget.props["Spine2.quaternion"]), $.multiply(this.poseTarget.props["Neck.quaternion"]), $.multiply(this.poseTarget.props["Head.quaternion"]), X.setFromQuaternion($);
6241
+ let u = X.x / (40 / 24), a = X.y / (9 / 4), d = Math.min(0.4, Math.max(-0.4, this.camera.rotation.x)), c = Math.min(0.4, Math.max(-0.4, this.camera.rotation.y)), g = Math.max(window.innerWidth - h, h), x = Math.max(window.innerHeight - r, r), f = this.convertRange(e, [r - x, r + x], [-0.3, 0.6]) - u + d, k = this.convertRange(t, [h - g, h + g], [-0.8, 0.8]) - a + c;
6233
6242
  f = Math.min(0.6, Math.max(-0.3, f)), k = Math.min(0.8, Math.max(-0.8, k));
6234
6243
  let D = (Math.random() - 0.5) / 4, p = (Math.random() - 0.5) / 4;
6235
6244
  if (n) {
@@ -6572,8 +6581,8 @@ class Be {
6572
6581
  if (d.tracks.forEach((L) => {
6573
6582
  const z = L.name.replaceAll("mixamorig", "").split("."), Y = z[0], S = z[1], E = x(Y);
6574
6583
  if (E && S) {
6575
- const j = `${E}.${S}`, X = L.clone();
6576
- X.name = j, p.push(X), Y !== E && g.set(Y, E);
6584
+ const j = `${E}.${S}`, Z = L.clone();
6585
+ Z.name = j, p.push(Z), Y !== E && g.set(Y, E);
6577
6586
  } else
6578
6587
  B.add(Y), (Y.toLowerCase().includes("arm") || Y.toLowerCase().includes("hand") || Y.toLowerCase().includes("shoulder")) && console.warn(`⚠️ Arm bone "${Y}" could not be mapped to avatar skeleton`);
6579
6588
  }), B.size > 0 && console.warn(`⚠️ ${B.size} bone(s) could not be mapped:`, Array.from(B).sort().join(", ")), p.length > 0) {
@@ -6844,7 +6853,7 @@ const Ve = Me(({
6844
6853
  style: x = {},
6845
6854
  animations: f = {}
6846
6855
  }, k) => {
6847
- const D = W(null), p = W(null), B = W(r), T = W(null), R = W(null), L = W(!1), I = W({ remainingText: null, originalText: null, options: null }), z = W([]), Y = W(0), [S, E] = pe(!0), [j, X] = pe(null), [oe, le] = pe(!1), [ge, de] = pe(!1);
6856
+ const D = W(null), p = W(null), B = W(r), T = W(null), R = W(null), L = W(!1), I = W({ remainingText: null, originalText: null, options: null }), z = W([]), Y = W(0), [S, E] = pe(!0), [j, Z] = pe(null), [oe, le] = pe(!1), [ge, de] = pe(!1);
6848
6857
  ce(() => {
6849
6858
  L.current = ge;
6850
6859
  }, [ge]), ce(() => {
@@ -6893,7 +6902,7 @@ const Ve = Me(({
6893
6902
  }, F = O(async () => {
6894
6903
  if (!(!D.current || p.current))
6895
6904
  try {
6896
- if (E(!0), X(null), p.current = new Be(D.current, v), p.current.controls && (p.current.controls.enableRotate = !1, p.current.controls.enableZoom = !1, p.current.controls.enablePan = !1, p.current.controls.enableDamping = !1), f && Object.keys(f).length > 0 && (p.current.customAnimations = f), await p.current.showAvatar(b, (K) => {
6905
+ if (E(!0), Z(null), p.current = new Be(D.current, v), p.current.controls && (p.current.controls.enableRotate = !1, p.current.controls.enableZoom = !1, p.current.controls.enablePan = !1, p.current.controls.enableDamping = !1), f && Object.keys(f).length > 0 && (p.current.customAnimations = f), await p.current.showAvatar(b, (K) => {
6897
6906
  if (K.lengthComputable) {
6898
6907
  const ne = Math.min(100, Math.round(K.loaded / K.total * 100));
6899
6908
  d(ne);
@@ -6917,7 +6926,7 @@ const Ve = Me(({
6917
6926
  document.removeEventListener("visibilitychange", U);
6918
6927
  };
6919
6928
  } catch (w) {
6920
- console.error("Error initializing TalkingHead:", w), X(w.message || "Failed to initialize avatar"), E(!1), c(w);
6929
+ console.error("Error initializing TalkingHead:", w), Z(w.message || "Failed to initialize avatar"), E(!1), c(w);
6921
6930
  }
6922
6931
  }, [V, t, e, n, i, s, o, r, l, h, u]);
6923
6932
  ce(() => (F(), () => {
@@ -6978,8 +6987,8 @@ const Ve = Me(({
6978
6987
  ie = !0, H && (clearInterval(H), H = null, R.current = null);
6979
6988
  try {
6980
6989
  U.onSpeechEnd();
6981
- } catch (Ze) {
6982
- console.error("Error in onSpeechEnd callback:", Ze);
6990
+ } catch (Xe) {
6991
+ console.error("Error in onSpeechEnd callback:", Xe);
6983
6992
  }
6984
6993
  }
6985
6994
  }, 100);
@@ -6989,7 +6998,7 @@ const Ve = Me(({
6989
6998
  await P(), p.current && p.current.lipsync && (p.current.setSlowdownRate && p.current.setSlowdownRate(1.05), p.current.speakText(w, me));
6990
6999
  }, 100);
6991
7000
  } catch (K) {
6992
- console.error("Error speaking text:", K), X(K.message || "Failed to speak text");
7001
+ console.error("Error speaking text:", K), Z(K.message || "Failed to speak text");
6993
7002
  }
6994
7003
  }, [oe, P, b.lipsyncLang]), te = O(() => {
6995
7004
  p.current && (p.current.stopSpeaking(), p.current.setSlowdownRate && p.current.setSlowdownRate(1), T.current = null, de(!1));
@@ -7246,20 +7255,20 @@ const pt = Me(({
7246
7255
  try {
7247
7256
  if (a(!0), c(null), r.current = new Be(h.current, B), await r.current.showAvatar(p, (j) => {
7248
7257
  if (j.lengthComputable) {
7249
- const X = Math.min(100, Math.round(j.loaded / j.total * 100));
7250
- t(X);
7258
+ const Z = Math.min(100, Math.round(j.loaded / j.total * 100));
7259
+ t(Z);
7251
7260
  }
7252
7261
  }), r.current.morphs && r.current.morphs.length > 0) {
7253
7262
  const j = r.current.morphs[0].morphTargetDictionary;
7254
7263
  console.log("Available morph targets:", Object.keys(j));
7255
- const X = Object.keys(j).filter((oe) => oe.startsWith("viseme_"));
7256
- console.log("Viseme morph targets found:", X), X.length === 0 && (console.warn("No viseme morph targets found! Lip-sync will not work properly."), console.log("Expected viseme targets: viseme_aa, viseme_E, viseme_I, viseme_O, viseme_U, viseme_PP, viseme_SS, viseme_TH, viseme_DD, viseme_FF, viseme_kk, viseme_nn, viseme_RR, viseme_CH, viseme_sil"));
7264
+ const Z = Object.keys(j).filter((oe) => oe.startsWith("viseme_"));
7265
+ console.log("Viseme morph targets found:", Z), Z.length === 0 && (console.warn("No viseme morph targets found! Lip-sync will not work properly."), console.log("Expected viseme targets: viseme_aa, viseme_E, viseme_I, viseme_O, viseme_U, viseme_PP, viseme_SS, viseme_TH, viseme_DD, viseme_FF, viseme_kk, viseme_nn, viseme_RR, viseme_CH, viseme_sil"));
7257
7266
  }
7258
7267
  if (await new Promise((j) => {
7259
- const X = () => {
7260
- r.current.lipsync && Object.keys(r.current.lipsync).length > 0 ? (console.log("Lip-sync modules loaded:", Object.keys(r.current.lipsync)), j()) : (console.log("Waiting for lip-sync modules to load..."), setTimeout(X, 100));
7268
+ const Z = () => {
7269
+ r.current.lipsync && Object.keys(r.current.lipsync).length > 0 ? (console.log("Lip-sync modules loaded:", Object.keys(r.current.lipsync)), j()) : (console.log("Waiting for lip-sync modules to load..."), setTimeout(Z, 100));
7261
7270
  };
7262
- X();
7271
+ Z();
7263
7272
  }), r.current && r.current.setShowFullAvatar)
7264
7273
  try {
7265
7274
  r.current.setShowFullAvatar(!0), console.log("Avatar initialized in full body mode");
@@ -7302,14 +7311,14 @@ const pt = Me(({
7302
7311
  if (r.current.setShowFullAvatar)
7303
7312
  try {
7304
7313
  r.current.setShowFullAvatar(!0);
7305
- } catch (X) {
7306
- console.warn("Error setting full body mode:", X);
7314
+ } catch (Z) {
7315
+ console.warn("Error setting full body mode:", Z);
7307
7316
  }
7308
7317
  if (S.includes("."))
7309
7318
  try {
7310
7319
  r.current.playAnimation(S, null, 10, 0, 0.01, E), console.log("Playing animation:", S);
7311
- } catch (X) {
7312
- console.log(`Failed to play ${S}:`, X);
7320
+ } catch (Z) {
7321
+ console.log(`Failed to play ${S}:`, Z);
7313
7322
  try {
7314
7323
  r.current.setBodyMovement("idle"), console.log("Fallback to idle animation");
7315
7324
  } catch (oe) {
@@ -7317,9 +7326,9 @@ const pt = Me(({
7317
7326
  }
7318
7327
  }
7319
7328
  else {
7320
- const X = [".fbx", ".glb", ".gltf"];
7329
+ const Z = [".fbx", ".glb", ".gltf"];
7321
7330
  let oe = !1;
7322
- for (const le of X)
7331
+ for (const le of Z)
7323
7332
  try {
7324
7333
  r.current.playAnimation(S + le, null, 10, 0, 0.01, E), console.log("Playing animation:", S + le), oe = !0;
7325
7334
  break;
@@ -7488,7 +7497,7 @@ const yt = Me(({
7488
7497
  // e.g., "idle" - will randomly select from this group when idle
7489
7498
  autoSpeak: T = !1
7490
7499
  }, R) => {
7491
- const L = W(null), I = W(null), z = W(u), Y = W(null), S = W(null), E = W(!1), j = W({ remainingText: null, originalText: null, options: null }), X = W([]), [oe, le] = pe(!0), [ge, de] = pe(null), [ee, be] = pe(!1), [Q, b] = pe(!1), [v, F] = pe(D), P = W(null);
7500
+ const L = W(null), I = W(null), z = W(u), Y = W(null), S = W(null), E = W(!1), j = W({ remainingText: null, originalText: null, options: null }), Z = W([]), [oe, le] = pe(!0), [ge, de] = pe(null), [ee, be] = pe(!1), [Q, b] = pe(!1), [v, F] = pe(D), P = W(null);
7492
7501
  ce(() => {
7493
7502
  E.current = Q;
7494
7503
  }, [Q]), ce(() => {
@@ -7628,9 +7637,9 @@ const yt = Me(({
7628
7637
  }
7629
7638
  await Le();
7630
7639
  const G = H.animationGroup || p;
7631
- G && !H.skipAnimation ? (console.log(`🎬 Attempting to play animation from group: "${G}"`), console.log(`📊 Current avatarBody: "${e}", loadedAnimations:`, v), w(G)) : console.log(`⏭️ Skipping animation (group: ${G}, skipAnimation: ${H.skipAnimation})`), j.current = { remainingText: null, originalText: null, options: null }, X.current = [], Y.current = { text: A, options: H }, S.current && (clearInterval(S.current), S.current = null), b(!1), E.current = !1;
7640
+ G && !H.skipAnimation ? (console.log(`🎬 Attempting to play animation from group: "${G}"`), console.log(`📊 Current avatarBody: "${e}", loadedAnimations:`, v), w(G)) : console.log(`⏭️ Skipping animation (group: ${G}, skipAnimation: ${H.skipAnimation})`), j.current = { remainingText: null, originalText: null, options: null }, Z.current = [], Y.current = { text: A, options: H }, S.current && (clearInterval(S.current), S.current = null), b(!1), E.current = !1;
7632
7641
  const J = A.split(/[.!?]+/).filter((se) => se.trim().length > 0);
7633
- X.current = J;
7642
+ Z.current = J;
7634
7643
  const ie = {
7635
7644
  lipsyncLang: H.lipsyncLang || "en",
7636
7645
  onSpeechEnd: () => {
@@ -8012,7 +8021,7 @@ const ft = Me(({
8012
8021
  }), u.current && (u.current.setMood("happy"), u.current.setBodyMovement("idle"));
8013
8022
  } else
8014
8023
  k.current && k.current();
8015
- }, []), X = O(() => {
8024
+ }, []), Z = O(() => {
8016
8025
  const b = R();
8017
8026
  let v = null;
8018
8027
  if (b?.avatar_script && b?.body) {
@@ -8233,11 +8242,11 @@ const ft = Me(({
8233
8242
  c.current && c.current();
8234
8243
  }, 10);
8235
8244
  }, [h, R]);
8236
- Xe(() => {
8237
- c.current = X, g.current = j, x.current = z, f.current = E, k.current = Y, D.current = S, p.current = oe;
8245
+ Ze(() => {
8246
+ c.current = Z, g.current = j, x.current = z, f.current = E, k.current = Y, D.current = S, p.current = oe;
8238
8247
  }), Fe(r, () => ({
8239
8248
  // Curriculum control methods
8240
- startTeaching: X,
8249
+ startTeaching: Z,
8241
8250
  startQuestions: S,
8242
8251
  handleAnswerSelect: oe,
8243
8252
  handleCodeTestResult: le,
@@ -8301,7 +8310,7 @@ const ft = Me(({
8301
8310
  handleResize: () => u.current?.handleResize(),
8302
8311
  // Avatar readiness check (always returns current value)
8303
8312
  isAvatarReady: () => u.current?.isReady || !1
8304
- }), [X, S, oe, le, E, j, z, Y, ee, L, R]);
8313
+ }), [Z, S, oe, le, E, j, z, Y, ee, L, R]);
8305
8314
  const Q = T.current || {
8306
8315
  avatarUrl: "/avatars/brunette.glb",
8307
8316
  avatarBody: "F",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.4.9",
3
+ "version": "1.5.0",
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",
@@ -1667,66 +1667,59 @@ class TalkingHead {
1667
1667
  const tempEuler = new THREE.Euler();
1668
1668
  const tempQuaternion = new THREE.Quaternion();
1669
1669
 
1670
- // Check if FBX animation is playing - if so, be even more aggressive
1670
+ // Check if FBX animation is playing
1671
1671
  const isFBXPlaying = this.mixer && this.currentFBXAction && this.currentFBXAction.isRunning();
1672
1672
 
1673
- // Research-based relaxed shoulder rotation values
1674
- // Natural relaxed shoulders: X rotation ~0.4-0.5 radians
1675
- // High/stiff shoulders: X rotation ~1.5-1.8 radians
1676
- // We ALWAYS clamp to relaxed position, especially during FBX animations
1677
- const targetX = 0.5; // Target X rotation for relaxed, natural shoulders (radians)
1678
- const maxX = 0.6; // Maximum allowed X rotation - very strict
1673
+ // Instead of forcing to a fixed value, reduce X rotation proportionally
1674
+ // This maintains the relative relationship between shoulders and arms
1675
+ // Natural relaxed shoulders: X rotation typically 0.3-0.6 radians
1676
+ // High shoulders: X rotation > 0.8 radians
1677
+ // We reduce high rotations by 40-50% to bring them down while maintaining arm relationships
1678
+ const reductionFactor = isFBXPlaying ? 0.5 : 0.6; // Reduce by 50% during FBX, 40% otherwise
1679
+ const maxAllowedX = 0.7; // Maximum allowed X rotation
1679
1680
 
1680
1681
  // Adjust left shoulder bone directly
1681
1682
  if (leftShoulderBone.quaternion) {
1682
1683
  tempEuler.setFromQuaternion(leftShoulderBone.quaternion, 'XYZ');
1684
+ const originalX = tempEuler.x;
1683
1685
 
1684
- // ALWAYS clamp X rotation to relaxed position (no threshold check)
1685
- // During FBX animations, be even more aggressive
1686
- if (tempEuler.x > maxX) {
1687
- // Force to target relaxed position immediately
1688
- tempEuler.x = targetX;
1689
- } else if (tempEuler.x > targetX) {
1690
- // If slightly above target, reduce aggressively
1691
- if (isFBXPlaying) {
1692
- // During FBX animations, force to target immediately
1693
- tempEuler.x = targetX;
1694
- } else {
1695
- // Otherwise, reduce smoothly but still aggressively
1696
- tempEuler.x = targetX + (tempEuler.x - targetX) * 0.1;
1697
- }
1686
+ // Reduce X rotation proportionally if it's too high
1687
+ if (tempEuler.x > maxAllowedX) {
1688
+ // Reduce by percentage to maintain arm-shoulder relationship
1689
+ tempEuler.x = maxAllowedX + (tempEuler.x - maxAllowedX) * (1 - reductionFactor);
1690
+ } else if (tempEuler.x > 0.5) {
1691
+ // Slight reduction for moderately high shoulders
1692
+ tempEuler.x = tempEuler.x * (1 - reductionFactor * 0.5);
1698
1693
  }
1699
1694
 
1700
- // Always apply the adjustment (no change detection)
1701
- tempQuaternion.setFromEuler(tempEuler, 'XYZ');
1702
- leftShoulderBone.quaternion.copy(tempQuaternion);
1703
- leftShoulderBone.updateMatrixWorld(true);
1695
+ // Only update if we changed something significantly
1696
+ if (Math.abs(tempEuler.x - originalX) > 0.01) {
1697
+ tempQuaternion.setFromEuler(tempEuler, 'XYZ');
1698
+ leftShoulderBone.quaternion.copy(tempQuaternion);
1699
+ leftShoulderBone.updateMatrixWorld(true);
1700
+ }
1704
1701
  }
1705
1702
 
1706
1703
  // Adjust right shoulder bone directly
1707
1704
  if (rightShoulderBone.quaternion) {
1708
1705
  tempEuler.setFromQuaternion(rightShoulderBone.quaternion, 'XYZ');
1706
+ const originalX = tempEuler.x;
1709
1707
 
1710
- // ALWAYS clamp X rotation to relaxed position (no threshold check)
1711
- // During FBX animations, be even more aggressive
1712
- if (tempEuler.x > maxX) {
1713
- // Force to target relaxed position immediately
1714
- tempEuler.x = targetX;
1715
- } else if (tempEuler.x > targetX) {
1716
- // If slightly above target, reduce aggressively
1717
- if (isFBXPlaying) {
1718
- // During FBX animations, force to target immediately
1719
- tempEuler.x = targetX;
1720
- } else {
1721
- // Otherwise, reduce smoothly but still aggressively
1722
- tempEuler.x = targetX + (tempEuler.x - targetX) * 0.1;
1723
- }
1708
+ // Reduce X rotation proportionally if it's too high
1709
+ if (tempEuler.x > maxAllowedX) {
1710
+ // Reduce by percentage to maintain arm-shoulder relationship
1711
+ tempEuler.x = maxAllowedX + (tempEuler.x - maxAllowedX) * (1 - reductionFactor);
1712
+ } else if (tempEuler.x > 0.5) {
1713
+ // Slight reduction for moderately high shoulders
1714
+ tempEuler.x = tempEuler.x * (1 - reductionFactor * 0.5);
1724
1715
  }
1725
1716
 
1726
- // Always apply the adjustment (no change detection)
1727
- tempQuaternion.setFromEuler(tempEuler, 'XYZ');
1728
- rightShoulderBone.quaternion.copy(tempQuaternion);
1729
- rightShoulderBone.updateMatrixWorld(true);
1717
+ // Only update if we changed something significantly
1718
+ if (Math.abs(tempEuler.x - originalX) > 0.01) {
1719
+ tempQuaternion.setFromEuler(tempEuler, 'XYZ');
1720
+ rightShoulderBone.quaternion.copy(tempQuaternion);
1721
+ rightShoulderBone.updateMatrixWorld(true);
1722
+ }
1730
1723
  }
1731
1724
  }
1732
1725