@sage-rsc/talking-head-react 1.8.2 → 1.8.4

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 ae } from "react/jsx-runtime";
2
- import { forwardRef as Ke, useRef as V, useState as pe, useEffect as Se, useCallback as U, useImperativeHandle as Je, useLayoutEffect as pt } from "react";
2
+ import { forwardRef as Ke, useRef as V, useState as pe, useEffect as Se, useCallback as U, useImperativeHandle as $e, useLayoutEffect as pt } from "react";
3
3
  import * as b from "three";
4
4
  import { OrbitControls as gt } from "three/addons/controls/OrbitControls.js";
5
5
  import { GLTFLoader as yt } from "three/addons/loaders/GLTFLoader.js";
@@ -5200,9 +5200,9 @@ class nt {
5200
5200
  eyeLookOutRight: [null, 0],
5201
5201
  eyeContact: [0]
5202
5202
  }
5203
- })))), e > 2 * this.animFrameDur && (e = 2 * this.animFrameDur), (this.viewName !== "full" || this.isAvatarOnly) && (t = this.mtRandomized[Math.floor(Math.random() * this.mtRandomized.length)], o = this.mtAvatar[t], o.needsUpdate || Object.assign(o, { base: (this.mood.baseline[t] || 0) + (1 + a / 255) * Math.random() / 5, needsUpdate: !0 })), this.updatePoseBase(this.animClock), this.mixer && (this.mixer.update(e / 1e3 * this.mixer.timeScale), this.currentFBXActionForCallback && this.currentFBXActionForCallback.isRunning() && this.currentFBXActionClipDuration)) {
5204
- const r = this.currentFBXActionForCallback.time, h = this.currentFBXActionClipDuration;
5205
- (r >= h - 0.05 || r > 0 && !this.currentFBXActionForCallback.paused && r >= h - 0.1) && (this.currentFBXActionCallback && (this.currentFBXActionCallback(), this.currentFBXActionCallback = null), this.currentFBXActionForCallback = null, this.currentFBXActionStartTime = null, this.currentFBXActionClipDuration = null);
5203
+ })))), e > 2 * this.animFrameDur && (e = 2 * this.animFrameDur), (this.viewName !== "full" || this.isAvatarOnly) && (t = this.mtRandomized[Math.floor(Math.random() * this.mtRandomized.length)], o = this.mtAvatar[t], o.needsUpdate || Object.assign(o, { base: (this.mood.baseline[t] || 0) + (1 + a / 255) * Math.random() / 5, needsUpdate: !0 })), this.updatePoseBase(this.animClock), this.mixer && (this.mixer.update(e / 1e3 * this.mixer.timeScale), this.currentFBXActionForCallback && this.currentFBXActionClipDuration)) {
5204
+ const r = this.currentFBXActionForCallback, h = r.time, d = this.currentFBXActionClipDuration;
5205
+ (!r.isRunning() || h >= d - 0.1 || r.getEffectiveTimeScale() === 0 && h > 0) && (console.log(`🎬 Manual check: Animation finished (time: ${h.toFixed(3)}/${d.toFixed(3)}, running: ${r.isRunning()})`), this.currentFBXActionCallback && (this.currentFBXActionCallback(), this.currentFBXActionCallback = null), this.currentFBXActionForCallback = null, this.currentFBXActionStartTime = null, this.currentFBXActionClipDuration = null);
5206
5206
  }
5207
5207
  if (this.updatePoseDelta(), (this.isSpeaking || this.isListening) && u ? a > this.volumeMax ? (this.volumeHeadBase = 0.05, Math.random() > 0.6 && (this.volumeHeadTarget = -0.05 - Math.random() / 15), this.volumeMax = a) : (this.volumeMax *= 0.92, this.volumeHeadTarget = this.volumeHeadBase - 0.9 * (this.volumeHeadBase - this.volumeHeadTarget)) : (this.volumeHeadTarget = 0, this.volumeMax = 0), t = this.volumeHeadTarget - this.volumeHeadCurrent, o = Math.abs(t), o > 1e-4 && (i = o * (this.volumeHeadEasing(Math.min(1, this.volumeHeadVelocity * e / 1e3 / o) / 2 + 0.5) - 0.5), this.volumeHeadCurrent += Math.sign(t) * Math.min(o, i)), Math.abs(this.volumeHeadCurrent) > 1e-4 && (ue.setFromAxisAngle(Bt, this.volumeHeadCurrent), this.objectNeck.quaternion.multiply(ue)), at.setFromObject(this.armature), this.objectLeftToeBase.getWorldPosition(Ue), Ue.sub(this.armature.position), this.objectRightToeBase.getWorldPosition(Xe), Xe.sub(this.armature.position), this.objectHips.position.y -= at.min.y / 2, this.objectHips.position.x -= (Ue.x + Xe.x) / 4, this.objectHips.position.z -= (Ue.z + Xe.z) / 2, this.dynamicbones.update(e), this.fbxAnimationLoader && this.fbxAnimationLoader.update(), this.opt.update && this.opt.update(e), this.updateMorphTargets(e), this.isAvatarOnly)
5208
5208
  this.stats && this.stats.end();
@@ -6449,15 +6449,15 @@ class nt {
6449
6449
  this.poseBase.props[h[0]] = h[1].clone(), this.poseTarget.props[h[0]] = h[1].clone(), this.poseTarget.props[h[0]].t = 0, this.poseTarget.props[h[0]].d = 1e3;
6450
6450
  }), this.mixer || (this.mixer = new b.AnimationMixer(this.armature)), this.animationFinishedCallback = a;
6451
6451
  const c = () => {
6452
- this.animationFinishedCallback && (this.animationFinishedCallback(), this.animationFinishedCallback = null), this.stopAnimation();
6452
+ console.log(`🎬 Mixer 'finished' event fired for animation: ${n}`), this.animationFinishedCallback && (this.animationFinishedCallback(), this.animationFinishedCallback = null);
6453
6453
  }, r = this.mixer.clipAction(u.clip);
6454
6454
  if (t <= 0)
6455
- r.setLoop(b.LoopOnce, 1);
6455
+ r.setLoop(b.LoopOnce, 1), console.log(`🎬 Setting animation to LoopOnce, duration: ${u.clip.duration.toFixed(3)}s`);
6456
6456
  else {
6457
6457
  const h = Math.ceil(t / u.clip.duration);
6458
6458
  r.setLoop(b.LoopRepeat, h);
6459
6459
  }
6460
- if (r.clampWhenFinished = !0, this.mixer.addEventListener("finished", c, { once: !0 }), this.currentFBXActionForCallback = r, this.currentFBXActionCallback = c, this.currentFBXActionClipDuration = u.clip.duration, this.currentFBXActionStartTime = null, this.currentFBXAction && this.currentFBXAction.isRunning()) {
6460
+ if (r.clampWhenFinished = !0, r.time = 0, this.mixer.addEventListener("finished", c, { once: !0 }), this.currentFBXActionForCallback = r, this.currentFBXActionCallback = c, this.currentFBXActionClipDuration = u.clip.duration, this.currentFBXActionStartTime = null, this.currentFBXAction && this.currentFBXAction.isRunning()) {
6461
6461
  this.currentFBXAction.fadeOut(0.3), setTimeout(() => {
6462
6462
  this.currentFBXAction = r, this.currentFBXActionForCallback = r, this.currentFBXActionCallback = c, this.currentFBXActionClipDuration = u.clip.duration, this.currentFBXActionStartTime = this.mixer.time;
6463
6463
  try {
@@ -6842,7 +6842,7 @@ const Ze = {
6842
6842
  // Male, English - Powerful
6843
6843
  }
6844
6844
  };
6845
- function $e() {
6845
+ function Je() {
6846
6846
  return {
6847
6847
  service: "elevenlabs",
6848
6848
  endpoint: Ze.endpoint,
@@ -6852,7 +6852,7 @@ function $e() {
6852
6852
  };
6853
6853
  }
6854
6854
  function qt() {
6855
- const Z = $e(), n = [];
6855
+ const Z = Je(), n = [];
6856
6856
  return Object.entries(Z.voices).forEach(([e, t]) => {
6857
6857
  n.push({
6858
6858
  value: t,
@@ -6888,7 +6888,7 @@ const ct = Ke(({
6888
6888
  }, [me]), Se(() => {
6889
6889
  D.current = l;
6890
6890
  }, [l]);
6891
- const ye = $e(), Re = o || ye.service;
6891
+ const ye = Je(), Re = o || ye.service;
6892
6892
  let he;
6893
6893
  Re === "browser" ? he = {
6894
6894
  service: "browser",
@@ -7118,7 +7118,7 @@ const ct = Ke(({
7118
7118
  }, [x]), ke = U(() => {
7119
7119
  m.current && m.current.onResize && m.current.onResize();
7120
7120
  }, []);
7121
- return Je(k, () => ({
7121
+ return $e(k, () => ({
7122
7122
  speakText: Y,
7123
7123
  stopSpeaking: K,
7124
7124
  pauseSpeaking: q,
@@ -7249,7 +7249,7 @@ const Ot = Ke(({
7249
7249
  style: s = {},
7250
7250
  avatarConfig: i = {}
7251
7251
  }, a) => {
7252
- const u = V(null), l = V(null), [c, r] = pe(!0), [h, d] = pe(null), [g, R] = pe(!1), x = $e(), k = i.ttsService || x.service, G = k === "browser" ? {
7252
+ const u = V(null), l = V(null), [c, r] = pe(!0), [h, d] = pe(null), [g, R] = pe(!1), x = Je(), k = i.ttsService || x.service, G = k === "browser" ? {
7253
7253
  endpoint: "",
7254
7254
  apiKey: null,
7255
7255
  defaultVoice: "Google US English"
@@ -7376,7 +7376,7 @@ const Ot = Ke(({
7376
7376
  } else
7377
7377
  console.warn("Animation system not available or animation not found:", I);
7378
7378
  }, []);
7379
- return Je(a, () => ({
7379
+ return $e(a, () => ({
7380
7380
  speakText: y,
7381
7381
  stopSpeaking: P,
7382
7382
  setMood: L,
@@ -7615,12 +7615,12 @@ const Nt = Ke(({
7615
7615
  } catch {
7616
7616
  }
7617
7617
  if (typeof m.auto == "string") {
7618
- const H = m.auto, J = {
7618
+ const H = m.auto, $ = {
7619
7619
  talking: `${H}/talking`,
7620
7620
  idle: `${H}/idle`
7621
7621
  }, W = C(e);
7622
- J[`${W}_talking`] = `${H}/${W}/talking`, J[`${W}_idle`] = `${H}/${W}/idle`, J.shared_talking = `${H}/shared/talking`, J.shared_idle = `${H}/shared/idle`;
7623
- const ie = await lt(J, e);
7622
+ $[`${W}_talking`] = `${H}/${W}/talking`, $[`${W}_idle`] = `${H}/${W}/idle`, $.shared_talking = `${H}/shared/talking`, $.shared_idle = `${H}/shared/idle`;
7623
+ const ie = await lt($, e);
7624
7624
  if (!Object.values(ie).some((j) => Array.isArray(j) && j.length > 0) && w) {
7625
7625
  O(w);
7626
7626
  return;
@@ -7631,22 +7631,22 @@ const Nt = Ke(({
7631
7631
  shared: {}
7632
7632
  }
7633
7633
  };
7634
- Object.entries(ie).forEach(([j, $]) => {
7634
+ Object.entries(ie).forEach(([j, J]) => {
7635
7635
  if (j.includes("_")) {
7636
7636
  const [ee, ...ge] = j.split("_"), de = ge.join("_");
7637
- ee === "shared" ? (ce._genderSpecific.shared[de] || (ce._genderSpecific.shared[de] = []), ce._genderSpecific.shared[de].push(...$)) : ee === W && (ce._genderSpecific[W][de] || (ce._genderSpecific[W][de] = []), ce._genderSpecific[W][de].push(...$));
7637
+ ee === "shared" ? (ce._genderSpecific.shared[de] || (ce._genderSpecific.shared[de] = []), ce._genderSpecific.shared[de].push(...J)) : ee === W && (ce._genderSpecific[W][de] || (ce._genderSpecific[W][de] = []), ce._genderSpecific[W][de].push(...J));
7638
7638
  } else
7639
- ce[j] = $;
7639
+ ce[j] = J;
7640
7640
  }), w && (w._genderSpecific && Object.keys(w._genderSpecific).forEach((j) => {
7641
- ce._genderSpecific[j] || (ce._genderSpecific[j] = {}), Object.entries(w._genderSpecific[j]).forEach(([$, ee]) => {
7642
- ce._genderSpecific[j][$] || (ce._genderSpecific[j][$] = ee);
7641
+ ce._genderSpecific[j] || (ce._genderSpecific[j] = {}), Object.entries(w._genderSpecific[j]).forEach(([J, ee]) => {
7642
+ ce._genderSpecific[j][J] || (ce._genderSpecific[j][J] = ee);
7643
7643
  });
7644
- }), Object.entries(w).forEach(([j, $]) => {
7645
- j !== "_genderSpecific" && !ce[j] && (ce[j] = $);
7644
+ }), Object.entries(w).forEach(([j, J]) => {
7645
+ j !== "_genderSpecific" && !ce[j] && (ce[j] = J);
7646
7646
  })), O(ce);
7647
7647
  } else if (typeof m.auto == "object") {
7648
- const H = await lt(m.auto, e), J = Object.values(H).some((W) => Array.isArray(W) && W.length > 0);
7649
- O(!J && w ? w : H);
7648
+ const H = await lt(m.auto, e), $ = Object.values(H).some((W) => Array.isArray(W) && W.length > 0);
7649
+ O(!$ && w ? w : H);
7650
7650
  }
7651
7651
  } catch (w) {
7652
7652
  if (console.error("Failed to auto-discover animations:", w), m.manifest)
@@ -7665,7 +7665,7 @@ const Nt = Ke(({
7665
7665
  }, [m, e, C]), Se(() => {
7666
7666
  M.current = c;
7667
7667
  }, [c]);
7668
- const X = $e(), ne = s || X.service;
7668
+ const X = Je(), ne = s || X.service;
7669
7669
  let se;
7670
7670
  ne === "browser" ? se = {
7671
7671
  service: "browser",
@@ -7739,8 +7739,8 @@ const Nt = Ke(({
7739
7739
  return [];
7740
7740
  let w = null;
7741
7741
  if (T._genderSpecific) {
7742
- const H = C(e), J = T._genderSpecific[H];
7743
- if (J && J[S] && Array.isArray(J[S]) && J[S].length > 0 && (w = J[S]), !w && T._genderSpecific.shared && T._genderSpecific.shared[S]) {
7742
+ const H = C(e), $ = T._genderSpecific[H];
7743
+ if ($ && $[S] && Array.isArray($[S]) && $[S].length > 0 && (w = $[S]), !w && T._genderSpecific.shared && T._genderSpecific.shared[S]) {
7744
7744
  const W = T._genderSpecific.shared[S];
7745
7745
  Array.isArray(W) && W.length > 0 && (w = W);
7746
7746
  }
@@ -7753,8 +7753,8 @@ const Nt = Ke(({
7753
7753
  }, [T, e, C]), He = U((S) => {
7754
7754
  const w = [...S];
7755
7755
  for (let H = w.length - 1; H > 0; H--) {
7756
- const J = Math.floor(Math.random() * (H + 1));
7757
- [w[H], w[J]] = [w[J], w[H]];
7756
+ const $ = Math.floor(Math.random() * (H + 1));
7757
+ [w[H], w[$]] = [w[$], w[H]];
7758
7758
  }
7759
7759
  return w;
7760
7760
  }, []), we = U((S) => {
@@ -7769,10 +7769,10 @@ const Nt = Ke(({
7769
7769
  }, [Be, He]), We = U((S) => S ? S.split("/").pop().replace(".fbx", "").replace(/[-_]/g, " ") : "Unknown", []), Oe = U((S, w = !1, H = null) => {
7770
7770
  if (!f.current || !K.current || q.current !== S)
7771
7771
  return null;
7772
- const J = we(S);
7773
- if (J)
7772
+ const $ = we(S);
7773
+ if ($)
7774
7774
  try {
7775
- const W = We(J);
7775
+ const W = We($);
7776
7776
  console.log(`🎬 Playing animation: "${W}"`);
7777
7777
  const ie = () => {
7778
7778
  console.log("🎬 Animation finished, checking if should continue...");
@@ -7783,11 +7783,11 @@ const Nt = Ke(({
7783
7783
  return console.log("🎬 isSpeakingRef is false, stopping animations"), !1;
7784
7784
  if (q.current !== S)
7785
7785
  return console.log(`🎬 Animation group mismatch (${q.current} !== ${S}), stopping animations`), !1;
7786
- const j = f.current, $ = j.isAudioPlaying === !0, ee = j.audioPlaylist && j.audioPlaylist.length > 0, ge = j.speechQueue && j.speechQueue.length > 0, de = j.isSpeaking === !0, Ge = de || $ || ee || ge;
7786
+ const j = f.current, J = j.isAudioPlaying === !0, ee = j.audioPlaylist && j.audioPlaylist.length > 0, ge = j.speechQueue && j.speechQueue.length > 0, de = j.isSpeaking === !0, Ge = de || J || ee || ge;
7787
7787
  return console.log("🎬 Still speaking check:", {
7788
7788
  isSpeakingRef: K.current,
7789
7789
  isTalkingHeadSpeaking: de,
7790
- hasAudioPlaying: $,
7790
+ hasAudioPlaying: J,
7791
7791
  hasAudioInPlaylist: ee,
7792
7792
  hasItemsInQueue: ge,
7793
7793
  shouldContinue: Ge
@@ -7797,7 +7797,7 @@ const Nt = Ke(({
7797
7797
  le() ? (console.log("🎬 Playing next animation..."), Oe(S, w, H)) : (console.log("🎬 Speech ended, stopping animations"), K.current = !1, q.current = null, H && H());
7798
7798
  })) : (console.log("🎬 Speech has ended, stopping animations"), K.current = !1, q.current = null, H && H());
7799
7799
  };
7800
- return f.current.playAnimation(J, null, 0, 0, 0.01, w, ie), J;
7800
+ return f.current.playAnimation($, null, 0, 0, 0.01, w, ie), $;
7801
7801
  } catch (W) {
7802
7802
  return console.error("Failed to play animation:", W), null;
7803
7803
  }
@@ -7805,8 +7805,8 @@ const Nt = Ke(({
7805
7805
  const W = () => {
7806
7806
  if (!f.current || !K.current || q.current !== S)
7807
7807
  return !1;
7808
- const le = f.current, ce = le.isAudioPlaying === !0, j = le.audioPlaylist && le.audioPlaylist.length > 0, $ = le.speechQueue && le.speechQueue.length > 0;
7809
- return le.isSpeaking === !0 || ce || j || $;
7808
+ const le = f.current, ce = le.isAudioPlaying === !0, j = le.audioPlaylist && le.audioPlaylist.length > 0, J = le.speechQueue && le.speechQueue.length > 0;
7809
+ return le.isSpeaking === !0 || ce || j || J;
7810
7810
  };
7811
7811
  W() && (Ie.current = [], fe.current = [], W() ? Oe(S, w, H) : (K.current = !1, q.current = null));
7812
7812
  }
@@ -7823,8 +7823,8 @@ const Nt = Ke(({
7823
7823
  console.warn("Error in onSpeechStart callback:", ie);
7824
7824
  }
7825
7825
  Q.current = { remainingText: null, originalText: null, options: null }, _.current = [], I.current = { text: S, options: w }, F.current && (clearInterval(F.current), F.current = null), v(!1), B.current = !1, te.current = !1;
7826
- const J = S.split(/[.!?]+/).filter((ie) => ie.trim().length > 0);
7827
- _.current = J, et.current = 0;
7826
+ const $ = S.split(/[.!?]+/).filter((ie) => ie.trim().length > 0);
7827
+ _.current = $, et.current = 0;
7828
7828
  const W = {
7829
7829
  lipsyncLang: w.lipsyncLang || "en"
7830
7830
  };
@@ -7833,11 +7833,11 @@ const Nt = Ke(({
7833
7833
  let le = 0;
7834
7834
  const ce = 1200;
7835
7835
  F.current && (clearInterval(F.current), F.current = null);
7836
- const j = { current: !1 }, $ = setInterval(() => {
7836
+ const j = { current: !1 }, J = setInterval(() => {
7837
7837
  if (le++, B.current)
7838
7838
  return;
7839
7839
  if (le > ce) {
7840
- if ($ && (clearInterval($), F.current = null), !j.current && !B.current) {
7840
+ if (J && (clearInterval(J), F.current = null), !j.current && !B.current) {
7841
7841
  j.current = !0, K.current = !1, q.current = null, Ie.current = [], fe.current = [], f.current && (f.current.isSpeaking = !1);
7842
7842
  try {
7843
7843
  w.onSpeechEnd && w.onSpeechEnd(), x();
@@ -7852,7 +7852,7 @@ const Nt = Ke(({
7852
7852
  const Te = f.current;
7853
7853
  if (!Te)
7854
7854
  return;
7855
- !B.current && (!Te.speechQueue || Te.speechQueue.length === 0) && (!Te.audioPlaylist || Te.audioPlaylist.length === 0) && Te.isAudioPlaying === !1 && Te.isSpeaking === !1 && !j.current && !B.current && (j.current = !0, $ && (clearInterval($), F.current = null), K.current = !1, q.current = null, Ie.current = [], fe.current = [], te.current = !0, Te && (Te.isSpeaking = !1), setTimeout(() => {
7855
+ !B.current && (!Te.speechQueue || Te.speechQueue.length === 0) && (!Te.audioPlaylist || Te.audioPlaylist.length === 0) && Te.isAudioPlaying === !1 && Te.isSpeaking === !1 && !j.current && !B.current && (j.current = !0, J && (clearInterval(J), F.current = null), K.current = !1, q.current = null, Ie.current = [], fe.current = [], te.current = !0, Te && (Te.isSpeaking = !1), setTimeout(() => {
7856
7856
  try {
7857
7857
  w.onSpeechEnd && w.onSpeechEnd(), x(), setTimeout(() => {
7858
7858
  te.current = !1;
@@ -7863,7 +7863,7 @@ const Nt = Ke(({
7863
7863
  }, 100));
7864
7864
  }, 200);
7865
7865
  }, 50);
7866
- F.current = $;
7866
+ F.current = J;
7867
7867
  }
7868
7868
  try {
7869
7869
  f.current.speakText(S, W);
@@ -7892,12 +7892,12 @@ const Nt = Ke(({
7892
7892
  const S = f.current.isSpeaking || !1, w = [...f.current.audioPlaylist || []], H = [...f.current.speechQueue || []];
7893
7893
  if (S || w.length > 0 || H.length > 0) {
7894
7894
  F.current && (clearInterval(F.current), F.current = null);
7895
- const J = _.current;
7895
+ const $ = _.current;
7896
7896
  let W = [];
7897
7897
  const ie = f.current.isAudioPlaying || !1;
7898
- if (J.length > 0) {
7899
- const ee = w.length + H.length, ge = ie ? 1 : 0, de = J.length - ee - ge, Ge = ie ? de : de + ge;
7900
- Ge < J.length && (W = J.slice(Ge));
7898
+ if ($.length > 0) {
7899
+ const ee = w.length + H.length, ge = ie ? 1 : 0, de = $.length - ee - ge, Ge = ie ? de : de + ge;
7900
+ Ge < $.length && (W = $.slice(Ge));
7901
7901
  } else
7902
7902
  w.length > 0 && w.forEach((ee) => {
7903
7903
  if (ee.text)
@@ -7922,8 +7922,8 @@ const Nt = Ke(({
7922
7922
  };
7923
7923
  const j = f.current.isAudioPlaying || !1 ? [...f.current.speechQueue || []] : null;
7924
7924
  f.current.speechQueue.length = 0;
7925
- const $ = f.current.pauseSpeaking();
7926
- f.current.isSpeaking = !1, K.current = !1, q.current = null, $ && $.audio && j ? (ke.current = $, ke.current.savedSpeechQueue = j) : ke.current = $, v(!0), B.current = !0;
7925
+ const J = f.current.pauseSpeaking();
7926
+ f.current.isSpeaking = !1, K.current = !1, q.current = null, J && J.audio && j ? (ke.current = J, ke.current.savedSpeechQueue = j) : ke.current = J, v(!0), B.current = !0;
7927
7927
  }
7928
7928
  } catch (S) {
7929
7929
  console.warn("Error pausing speech:", S);
@@ -7937,14 +7937,14 @@ const Nt = Ke(({
7937
7937
  if (ie && (q.current = ie), ke.current.savedSpeechQueue && (f.current.speechQueue.length = 0, f.current.speechQueue.push(...ke.current.savedSpeechQueue)), f.current.isSpeaking = !0, f.current) {
7938
7938
  const le = f.current;
7939
7939
  let ce = 0;
7940
- const j = 1200, $ = { current: !1 };
7940
+ const j = 1200, J = { current: !1 };
7941
7941
  F.current && (clearInterval(F.current), F.current = null);
7942
7942
  const ee = setInterval(() => {
7943
7943
  if (ce++, B.current)
7944
7944
  return;
7945
7945
  if (ce > j) {
7946
- if (ee && (clearInterval(ee), F.current = null), !$.current && !B.current) {
7947
- $.current = !0, K.current = !1, q.current = null, Ie.current = [], fe.current = [], f.current && (f.current.isSpeaking = !1);
7946
+ if (ee && (clearInterval(ee), F.current = null), !J.current && !B.current) {
7947
+ J.current = !0, K.current = !1, q.current = null, Ie.current = [], fe.current = [], f.current && (f.current.isSpeaking = !1);
7948
7948
  try {
7949
7949
  W.onSpeechEnd && W.onSpeechEnd(), x();
7950
7950
  } catch (Me) {
@@ -7954,10 +7954,10 @@ const Nt = Ke(({
7954
7954
  return;
7955
7955
  }
7956
7956
  const ge = !le.speechQueue || le.speechQueue.length === 0, de = !le.audioPlaylist || le.audioPlaylist.length === 0, Ge = le.isAudioPlaying === !1;
7957
- le && !B.current && ge && de && Ge && le.isSpeaking === !1 && !$.current && !B.current && setTimeout(() => {
7957
+ le && !B.current && ge && de && Ge && le.isSpeaking === !1 && !J.current && !B.current && setTimeout(() => {
7958
7958
  const Me = f.current;
7959
7959
  if (!Me) return;
7960
- !B.current && (!Me.speechQueue || Me.speechQueue.length === 0) && (!Me.audioPlaylist || Me.audioPlaylist.length === 0) && Me.isAudioPlaying === !1 && Me.isSpeaking === !1 && !$.current && !B.current && ($.current = !0, ee && (clearInterval(ee), F.current = null), K.current = !1, q.current = null, Ie.current = [], fe.current = [], te.current = !0, Me && (Me.isSpeaking = !1), setTimeout(() => {
7960
+ !B.current && (!Me.speechQueue || Me.speechQueue.length === 0) && (!Me.audioPlaylist || Me.audioPlaylist.length === 0) && Me.isAudioPlaying === !1 && Me.isSpeaking === !1 && !J.current && !B.current && (J.current = !0, ee && (clearInterval(ee), F.current = null), K.current = !1, q.current = null, Ie.current = [], fe.current = [], te.current = !0, Me && (Me.isSpeaking = !1), setTimeout(() => {
7961
7961
  try {
7962
7962
  W.onSpeechEnd && W.onSpeechEnd(), x(), setTimeout(() => {
7963
7963
  te.current = !1;
@@ -7973,8 +7973,8 @@ const Nt = Ke(({
7973
7973
  await f.current.playAudio(!1, ke.current), ie && !W.skipAnimation && (Ie.current = [], fe.current = [], Oe(ie)), ke.current = null;
7974
7974
  return;
7975
7975
  }
7976
- const S = Q.current?.remainingText, w = Q.current?.originalText || I.current?.text, H = Q.current?.options || I.current?.options || {}, J = S || w;
7977
- J && Ve(J, H), ke.current = null;
7976
+ const S = Q.current?.remainingText, w = Q.current?.originalText || I.current?.text, H = Q.current?.options || I.current?.options || {}, $ = S || w;
7977
+ $ && Ve($, H), ke.current = null;
7978
7978
  } catch (S) {
7979
7979
  console.warn("Error resuming speech:", S), v(!1), B.current = !1, ke.current = null;
7980
7980
  }
@@ -7988,7 +7988,7 @@ const Nt = Ke(({
7988
7988
  }
7989
7989
  }
7990
7990
  }, [x]);
7991
- return Je(P, () => ({
7991
+ return $e(P, () => ({
7992
7992
  speakText: Ve,
7993
7993
  pauseSpeaking: tt,
7994
7994
  resumeSpeaking: ht,
@@ -8537,7 +8537,7 @@ const Ut = Ke(({
8537
8537
  }, [u, y]);
8538
8538
  pt(() => {
8539
8539
  d.current = Q, g.current = B, R.current = f, x.current = F, k.current = M, G.current = I, m.current = _;
8540
- }), Je(l, () => ({
8540
+ }), $e(l, () => ({
8541
8541
  // Curriculum control methods
8542
8542
  startTeaching: Q,
8543
8543
  startQuestions: I,
@@ -9029,7 +9029,7 @@ const ut = {
9029
9029
  duration: 5e3,
9030
9030
  description: "Excited, energetic movement"
9031
9031
  }
9032
- }, Kt = (Z) => ut[Z] || null, Jt = (Z) => ut.hasOwnProperty(Z);
9032
+ }, Kt = (Z) => ut[Z] || null, $t = (Z) => ut.hasOwnProperty(Z);
9033
9033
  export {
9034
9034
  _t as AnimationSelector,
9035
9035
  Ut as CurriculumLearning,
@@ -9037,8 +9037,8 @@ export {
9037
9037
  ct as TalkingHeadAvatar,
9038
9038
  Ot as TalkingHeadComponent,
9039
9039
  ut as animations,
9040
- $e as getActiveTTSConfig,
9040
+ Je as getActiveTTSConfig,
9041
9041
  Kt as getAnimation,
9042
9042
  qt as getVoiceOptions,
9043
- Jt as hasAnimation
9043
+ $t as hasAnimation
9044
9044
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
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",
@@ -3174,15 +3174,20 @@ class TalkingHead {
3174
3174
 
3175
3175
  // Manually check if current FBX animation has finished (backup to mixer 'finished' event)
3176
3176
  // This ensures the callback fires reliably even if the mixer event doesn't
3177
- if (this.currentFBXActionForCallback && this.currentFBXActionForCallback.isRunning() && this.currentFBXActionClipDuration) {
3178
- const actionTime = this.currentFBXActionForCallback.time;
3177
+ if (this.currentFBXActionForCallback && this.currentFBXActionClipDuration) {
3178
+ const action = this.currentFBXActionForCallback;
3179
+ const actionTime = action.time;
3179
3180
  const clipDuration = this.currentFBXActionClipDuration;
3180
3181
 
3181
- // For LoopOnce, action.time goes from 0 to clipDuration
3182
- // Check if animation has reached the end (with small tolerance for timing)
3183
- // Also check if action is paused (time doesn't advance when paused)
3184
- if (actionTime >= clipDuration - 0.05 || (actionTime > 0 && !this.currentFBXActionForCallback.paused && actionTime >= clipDuration - 0.1)) {
3182
+ // Check if animation is no longer running (finished or stopped)
3183
+ // For LoopOnce with clampWhenFinished, the action stops when it reaches the end
3184
+ const isFinished = !action.isRunning() ||
3185
+ (actionTime >= clipDuration - 0.1) ||
3186
+ (action.getEffectiveTimeScale() === 0 && actionTime > 0);
3187
+
3188
+ if (isFinished) {
3185
3189
  // Animation finished - call callback manually
3190
+ console.log(`🎬 Manual check: Animation finished (time: ${actionTime.toFixed(3)}/${clipDuration.toFixed(3)}, running: ${action.isRunning()})`);
3186
3191
  if (this.currentFBXActionCallback) {
3187
3192
  this.currentFBXActionCallback();
3188
3193
  this.currentFBXActionCallback = null;
@@ -5570,25 +5575,29 @@ class TalkingHead {
5570
5575
 
5571
5576
  // Create handler that calls callback before stopping
5572
5577
  const finishedHandler = () => {
5578
+ console.log(`🎬 Mixer 'finished' event fired for animation: ${url}`);
5573
5579
  if (this.animationFinishedCallback) {
5574
5580
  this.animationFinishedCallback();
5575
5581
  this.animationFinishedCallback = null;
5576
5582
  }
5577
- this.stopAnimation();
5583
+ // Don't call stopAnimation() here - let the callback handle continuation
5584
+ // The callback will decide whether to play next animation or stop
5578
5585
  };
5579
-
5586
+
5580
5587
  // Play action with error handling
5581
5588
  // If dur is 0 or negative, play animation once (use clip's natural duration)
5582
5589
  const action = this.mixer.clipAction(item.clip);
5583
5590
  if (dur <= 0) {
5584
5591
  // Play once - use LoopOnce to ensure finished event fires
5585
5592
  action.setLoop( THREE.LoopOnce, 1 );
5593
+ console.log(`🎬 Setting animation to LoopOnce, duration: ${item.clip.duration.toFixed(3)}s`);
5586
5594
  } else {
5587
5595
  // Play for specified duration
5588
5596
  const repeat = Math.ceil(dur / item.clip.duration);
5589
- action.setLoop( THREE.LoopRepeat, repeat );
5597
+ action.setLoop( THREE.LoopRepeat, repeat );
5590
5598
  }
5591
5599
  action.clampWhenFinished = true;
5600
+ action.time = 0; // Reset to start
5592
5601
 
5593
5602
  // Set up callback to check when animation finishes
5594
5603
  // Use both mixer 'finished' event and manual time checking for reliability