@omote/core 0.9.4 → 0.9.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.
package/dist/index.mjs CHANGED
@@ -631,6 +631,19 @@ var BlendshapeSmoother = class {
631
631
  this.targets.set(frame);
632
632
  this._hasTarget = true;
633
633
  }
634
+ /**
635
+ * Snap current position to a frame without triggering spring physics.
636
+ * Zeroes velocities so the spring starts from rest at this position.
637
+ *
638
+ * Used by A2EProcessor to seed the smoother when entering gap decay —
639
+ * positions the spring at the last real inference frame before decaying
640
+ * toward neutral.
641
+ */
642
+ setPosition(frame) {
643
+ this.values.set(frame);
644
+ this.velocities.fill(0);
645
+ this._hasTarget = true;
646
+ }
634
647
  /**
635
648
  * Advance all 52 springs by `dt` seconds and return the smoothed frame.
636
649
  *
@@ -686,7 +699,8 @@ var BlendshapeSmoother = class {
686
699
  var logger4 = createLogger("A2EProcessor");
687
700
  var FRAME_RATE = 30;
688
701
  var DRIP_INTERVAL_MS = 33;
689
- var NEUTRAL_THRESHOLD_MS = 1500;
702
+ var HOLD_DURATION_MS = 400;
703
+ var GAP_DECAY_HALFLIFE_S = 0.08;
690
704
  var _A2EProcessor = class _A2EProcessor {
691
705
  constructor(config) {
692
706
  this.writeOffset = 0;
@@ -697,10 +711,12 @@ var _A2EProcessor = class _A2EProcessor {
697
711
  // Push mode state
698
712
  this._latestFrame = null;
699
713
  this.dripInterval = null;
714
+ // Last-frame-hold for pull mode (prevents avatar freezing between frames)
715
+ this.lastPulledFrame = null;
700
716
  this.lastDequeuedTime = 0;
701
- this.lastUpdateTime = 0;
702
- this.neutralTriggered = false;
703
- this.outputBuffer = null;
717
+ this.decayBuffer = null;
718
+ this.gapDecayStarted = false;
719
+ this.lastSmootherUpdate = 0;
704
720
  // Inference serialization
705
721
  this.inferenceRunning = false;
706
722
  this.pendingChunks = [];
@@ -713,9 +729,9 @@ var _A2EProcessor = class _A2EProcessor {
713
729
  this.identityIndex = config.identityIndex ?? 0;
714
730
  this.onFrame = config.onFrame;
715
731
  this.onError = config.onError;
716
- this.smoother = new BlendshapeSmoother({ halflife: config.smoothingHalflife ?? 0.06 });
717
732
  this.bufferCapacity = this.chunkSize * 2;
718
733
  this.buffer = new Float32Array(this.bufferCapacity);
734
+ this.smoother = new BlendshapeSmoother({ halflife: GAP_DECAY_HALFLIFE_S });
719
735
  }
720
736
  // ═══════════════════════════════════════════════════════════════════════
721
737
  // Audio Input
@@ -813,14 +829,14 @@ var _A2EProcessor = class _A2EProcessor {
813
829
  this.timestampedQueue = [];
814
830
  this.plainQueue = [];
815
831
  this._latestFrame = null;
816
- this.smoother.reset();
832
+ this.lastPulledFrame = null;
817
833
  this.lastDequeuedTime = 0;
818
- this.lastUpdateTime = 0;
819
- this.neutralTriggered = false;
820
- this.outputBuffer = null;
821
834
  this.pendingChunks = [];
822
835
  this.inferenceRunning = false;
823
836
  this.getFrameCallCount = 0;
837
+ this.smoother.reset();
838
+ this.gapDecayStarted = false;
839
+ this.lastSmootherUpdate = 0;
824
840
  }
825
841
  // ═══════════════════════════════════════════════════════════════════════
826
842
  // Frame Output — Pull Mode (TTS playback)
@@ -836,7 +852,6 @@ var _A2EProcessor = class _A2EProcessor {
836
852
  */
837
853
  getFrameForTime(currentTime) {
838
854
  this.getFrameCallCount++;
839
- const now = getClock().now();
840
855
  const discardWindow = this.backend.backend === "wasm" ? 1.5 : 0.5;
841
856
  let discardCount = 0;
842
857
  while (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp < currentTime - discardWindow) {
@@ -852,20 +867,14 @@ var _A2EProcessor = class _A2EProcessor {
852
867
  nextFrameTs: this.timestampedQueue.length > 0 ? this.timestampedQueue[0].timestamp.toFixed(3) : "none"
853
868
  });
854
869
  }
855
- let newDequeue = false;
856
870
  if (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp <= currentTime) {
857
871
  const { frame } = this.timestampedQueue.shift();
858
- const firstTarget = !this.smoother.hasTarget;
859
- this.smoother.setTarget(frame);
860
- if (firstTarget) {
861
- this.smoother.update(1);
862
- this.lastUpdateTime = now;
863
- }
864
- newDequeue = true;
865
- this.neutralTriggered = false;
866
- this.lastDequeuedTime = now;
872
+ this.lastPulledFrame = frame;
873
+ this.lastDequeuedTime = getClock().now();
874
+ this.gapDecayStarted = false;
875
+ return frame;
867
876
  }
868
- if (!newDequeue && this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
877
+ if (this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
869
878
  logger4.debug("getFrameForTime: waiting for playback time to reach queued frames", {
870
879
  queueLen: this.timestampedQueue.length,
871
880
  frontTimestamp: this.timestampedQueue[0].timestamp.toFixed(4),
@@ -873,24 +882,34 @@ var _A2EProcessor = class _A2EProcessor {
873
882
  delta: (this.timestampedQueue[0].timestamp - currentTime).toFixed(4)
874
883
  });
875
884
  }
876
- if (!newDequeue && this.smoother.hasTarget && !this.neutralTriggered) {
885
+ if (this.lastPulledFrame) {
886
+ const now = getClock().now();
877
887
  const elapsed = now - this.lastDequeuedTime;
878
- if (elapsed >= NEUTRAL_THRESHOLD_MS) {
888
+ if (elapsed < HOLD_DURATION_MS) {
889
+ return this.lastPulledFrame;
890
+ }
891
+ if (!this.gapDecayStarted) {
892
+ this.smoother.setPosition(this.lastPulledFrame);
879
893
  this.smoother.decayToNeutral();
880
- this.neutralTriggered = true;
894
+ this.gapDecayStarted = true;
895
+ this.lastSmootherUpdate = now;
881
896
  }
897
+ const dt = Math.min((now - this.lastSmootherUpdate) / 1e3, 0.1);
898
+ this.lastSmootherUpdate = now;
899
+ const smoothed = this.smoother.update(dt);
900
+ let maxVal = 0;
901
+ for (let i = 0; i < 52; i++) {
902
+ if (smoothed[i] > maxVal) maxVal = smoothed[i];
903
+ }
904
+ if (maxVal < 1e-3) {
905
+ this.lastPulledFrame = null;
906
+ return null;
907
+ }
908
+ if (!this.decayBuffer) this.decayBuffer = new Float32Array(52);
909
+ this.decayBuffer.set(smoothed);
910
+ return this.decayBuffer;
882
911
  }
883
- if (!this.smoother.hasTarget) {
884
- return null;
885
- }
886
- const dt = Math.min((now - this.lastUpdateTime) / 1e3, 0.1);
887
- const smoothed = this.smoother.update(dt);
888
- this.lastUpdateTime = now;
889
- if (newDequeue || !this.outputBuffer) {
890
- this.outputBuffer = new Float32Array(52);
891
- }
892
- this.outputBuffer.set(smoothed);
893
- return this.outputBuffer;
912
+ return null;
894
913
  }
895
914
  // ═══════════════════════════════════════════════════════════════════════
896
915
  // Frame Output — Push Mode (live mic, game loop)