@omote/core 0.9.4 → 0.9.6

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
@@ -790,18 +806,19 @@ var _A2EProcessor = class _A2EProcessor {
790
806
  */
791
807
  async flush() {
792
808
  if (this.disposed || this.writeOffset === 0) return;
809
+ const actualSamples = this.writeOffset;
793
810
  const padded = new Float32Array(this.chunkSize);
794
- padded.set(this.buffer.subarray(0, this.writeOffset), 0);
811
+ padded.set(this.buffer.subarray(0, actualSamples), 0);
795
812
  const chunkTimestamp = this.bufferStartTime > 0 ? this.bufferStartTime : void 0;
796
813
  logger4.info("flush: routing through drain pipeline", {
797
- actualSamples: this.writeOffset,
814
+ actualSamples,
798
815
  chunkTimestamp: chunkTimestamp?.toFixed(3),
799
816
  pendingChunks: this.pendingChunks.length,
800
817
  inferenceRunning: this.inferenceRunning
801
818
  });
802
819
  this.writeOffset = 0;
803
820
  this.bufferStartTime = 0;
804
- this.pendingChunks.push({ chunk: padded, timestamp: chunkTimestamp });
821
+ this.pendingChunks.push({ chunk: padded, timestamp: chunkTimestamp, actualSamples });
805
822
  this.drainPendingChunks();
806
823
  }
807
824
  /**
@@ -813,14 +830,14 @@ var _A2EProcessor = class _A2EProcessor {
813
830
  this.timestampedQueue = [];
814
831
  this.plainQueue = [];
815
832
  this._latestFrame = null;
816
- this.smoother.reset();
833
+ this.lastPulledFrame = null;
817
834
  this.lastDequeuedTime = 0;
818
- this.lastUpdateTime = 0;
819
- this.neutralTriggered = false;
820
- this.outputBuffer = null;
821
835
  this.pendingChunks = [];
822
836
  this.inferenceRunning = false;
823
837
  this.getFrameCallCount = 0;
838
+ this.smoother.reset();
839
+ this.gapDecayStarted = false;
840
+ this.lastSmootherUpdate = 0;
824
841
  }
825
842
  // ═══════════════════════════════════════════════════════════════════════
826
843
  // Frame Output — Pull Mode (TTS playback)
@@ -836,7 +853,6 @@ var _A2EProcessor = class _A2EProcessor {
836
853
  */
837
854
  getFrameForTime(currentTime) {
838
855
  this.getFrameCallCount++;
839
- const now = getClock().now();
840
856
  const discardWindow = this.backend.backend === "wasm" ? 1.5 : 0.5;
841
857
  let discardCount = 0;
842
858
  while (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp < currentTime - discardWindow) {
@@ -852,20 +868,14 @@ var _A2EProcessor = class _A2EProcessor {
852
868
  nextFrameTs: this.timestampedQueue.length > 0 ? this.timestampedQueue[0].timestamp.toFixed(3) : "none"
853
869
  });
854
870
  }
855
- let newDequeue = false;
856
871
  if (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp <= currentTime) {
857
872
  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;
873
+ this.lastPulledFrame = frame;
874
+ this.lastDequeuedTime = getClock().now();
875
+ this.gapDecayStarted = false;
876
+ return frame;
867
877
  }
868
- if (!newDequeue && this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
878
+ if (this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
869
879
  logger4.debug("getFrameForTime: waiting for playback time to reach queued frames", {
870
880
  queueLen: this.timestampedQueue.length,
871
881
  frontTimestamp: this.timestampedQueue[0].timestamp.toFixed(4),
@@ -873,24 +883,34 @@ var _A2EProcessor = class _A2EProcessor {
873
883
  delta: (this.timestampedQueue[0].timestamp - currentTime).toFixed(4)
874
884
  });
875
885
  }
876
- if (!newDequeue && this.smoother.hasTarget && !this.neutralTriggered) {
886
+ if (this.lastPulledFrame) {
887
+ const now = getClock().now();
877
888
  const elapsed = now - this.lastDequeuedTime;
878
- if (elapsed >= NEUTRAL_THRESHOLD_MS) {
889
+ if (elapsed < HOLD_DURATION_MS) {
890
+ return this.lastPulledFrame;
891
+ }
892
+ if (!this.gapDecayStarted) {
893
+ this.smoother.setPosition(this.lastPulledFrame);
879
894
  this.smoother.decayToNeutral();
880
- this.neutralTriggered = true;
895
+ this.gapDecayStarted = true;
896
+ this.lastSmootherUpdate = now;
881
897
  }
898
+ const dt = Math.min((now - this.lastSmootherUpdate) / 1e3, 0.1);
899
+ this.lastSmootherUpdate = now;
900
+ const smoothed = this.smoother.update(dt);
901
+ let maxVal = 0;
902
+ for (let i = 0; i < 52; i++) {
903
+ if (smoothed[i] > maxVal) maxVal = smoothed[i];
904
+ }
905
+ if (maxVal < 1e-3) {
906
+ this.lastPulledFrame = null;
907
+ return null;
908
+ }
909
+ if (!this.decayBuffer) this.decayBuffer = new Float32Array(52);
910
+ this.decayBuffer.set(smoothed);
911
+ return this.decayBuffer;
882
912
  }
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;
913
+ return null;
894
914
  }
895
915
  // ═══════════════════════════════════════════════════════════════════════
896
916
  // Frame Output — Push Mode (live mic, game loop)
@@ -956,14 +976,15 @@ var _A2EProcessor = class _A2EProcessor {
956
976
  logger4.info("drainPendingChunks starting", { pendingChunks: this.pendingChunks.length });
957
977
  const processNext = async () => {
958
978
  while (this.pendingChunks.length > 0 && !this.disposed) {
959
- const { chunk, timestamp } = this.pendingChunks.shift();
979
+ const { chunk, timestamp, actualSamples } = this.pendingChunks.shift();
960
980
  try {
961
981
  const t0 = getClock().now();
962
982
  const result = await this.backend.infer(chunk, this.identityIndex);
963
983
  const inferMs = Math.round(getClock().now() - t0);
964
- const actualDuration = chunk.length / this.sampleRate;
984
+ const effectiveSamples = actualSamples ?? chunk.length;
985
+ const actualDuration = effectiveSamples / this.sampleRate;
965
986
  const actualFrameCount = Math.ceil(actualDuration * FRAME_RATE);
966
- const framesToQueue = Math.min(actualFrameCount, result.blendshapes.length);
987
+ const framesToQueue = Math.min(Math.max(1, actualFrameCount), result.blendshapes.length);
967
988
  logger4.info("Inference complete", {
968
989
  inferMs,
969
990
  modelFrames: result.blendshapes.length,