@omote/core 0.9.3 → 0.9.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.mjs CHANGED
@@ -608,12 +608,85 @@ var AudioChunkCoalescer = class {
608
608
  }
609
609
  };
610
610
 
611
+ // src/inference/BlendshapeSmoother.ts
612
+ var NUM_BLENDSHAPES = 52;
613
+ var BlendshapeSmoother = class {
614
+ constructor(config) {
615
+ /** Whether any target has been set */
616
+ this._hasTarget = false;
617
+ this.halflife = config?.halflife ?? 0.06;
618
+ this.values = new Float32Array(NUM_BLENDSHAPES);
619
+ this.velocities = new Float32Array(NUM_BLENDSHAPES);
620
+ this.targets = new Float32Array(NUM_BLENDSHAPES);
621
+ }
622
+ /** Whether a target frame has been set (false until first setTarget call) */
623
+ get hasTarget() {
624
+ return this._hasTarget;
625
+ }
626
+ /**
627
+ * Set new target frame from inference output.
628
+ * Springs will converge toward these values on subsequent update() calls.
629
+ */
630
+ setTarget(frame) {
631
+ this.targets.set(frame);
632
+ this._hasTarget = true;
633
+ }
634
+ /**
635
+ * Advance all 52 springs by `dt` seconds and return the smoothed frame.
636
+ *
637
+ * Call this every render frame (e.g., inside requestAnimationFrame).
638
+ * Returns the internal values buffer — do NOT mutate the returned array.
639
+ *
640
+ * @param dt - Time step in seconds (e.g., 1/60 for 60fps)
641
+ * @returns Smoothed blendshape values (Float32Array of 52)
642
+ */
643
+ update(dt) {
644
+ if (!this._hasTarget) {
645
+ return this.values;
646
+ }
647
+ if (this.halflife <= 0) {
648
+ this.values.set(this.targets);
649
+ this.velocities.fill(0);
650
+ return this.values;
651
+ }
652
+ const damping = Math.LN2 / this.halflife;
653
+ const eydt = Math.exp(-damping * dt);
654
+ for (let i = 0; i < NUM_BLENDSHAPES; i++) {
655
+ const j0 = this.values[i] - this.targets[i];
656
+ const j1 = this.velocities[i] + j0 * damping;
657
+ this.values[i] = eydt * (j0 + j1 * dt) + this.targets[i];
658
+ this.velocities[i] = eydt * (this.velocities[i] - j1 * damping * dt);
659
+ this.values[i] = Math.max(0, Math.min(1, this.values[i]));
660
+ }
661
+ return this.values;
662
+ }
663
+ /**
664
+ * Decay all spring targets to neutral (0).
665
+ *
666
+ * Call when inference stalls (no new frames for threshold duration).
667
+ * The springs will smoothly close the mouth / relax the face over
668
+ * the halflife period rather than freezing.
669
+ */
670
+ decayToNeutral() {
671
+ this.targets.fill(0);
672
+ }
673
+ /**
674
+ * Reset all state (values, velocities, targets).
675
+ * Call when starting a new playback session.
676
+ */
677
+ reset() {
678
+ this.values.fill(0);
679
+ this.velocities.fill(0);
680
+ this.targets.fill(0);
681
+ this._hasTarget = false;
682
+ }
683
+ };
684
+
611
685
  // src/inference/A2EProcessor.ts
612
686
  var logger4 = createLogger("A2EProcessor");
613
687
  var FRAME_RATE = 30;
614
688
  var DRIP_INTERVAL_MS = 33;
615
- var HOLD_DURATION_MS = 400;
616
- var DECAY_DURATION_MS = 300;
689
+ var NEUTRAL_THRESHOLD_MS = 1500;
617
690
  var _A2EProcessor = class _A2EProcessor {
618
691
  constructor(config) {
619
692
  this.writeOffset = 0;
@@ -624,10 +697,10 @@ var _A2EProcessor = class _A2EProcessor {
624
697
  // Push mode state
625
698
  this._latestFrame = null;
626
699
  this.dripInterval = null;
627
- // Last-frame-hold for pull mode (prevents avatar freezing between frames)
628
- this.lastPulledFrame = null;
629
700
  this.lastDequeuedTime = 0;
630
- this.decayBuffer = null;
701
+ this.lastUpdateTime = 0;
702
+ this.neutralTriggered = false;
703
+ this.outputBuffer = null;
631
704
  // Inference serialization
632
705
  this.inferenceRunning = false;
633
706
  this.pendingChunks = [];
@@ -640,6 +713,7 @@ var _A2EProcessor = class _A2EProcessor {
640
713
  this.identityIndex = config.identityIndex ?? 0;
641
714
  this.onFrame = config.onFrame;
642
715
  this.onError = config.onError;
716
+ this.smoother = new BlendshapeSmoother({ halflife: config.smoothingHalflife ?? 0.06 });
643
717
  this.bufferCapacity = this.chunkSize * 2;
644
718
  this.buffer = new Float32Array(this.bufferCapacity);
645
719
  }
@@ -739,8 +813,11 @@ var _A2EProcessor = class _A2EProcessor {
739
813
  this.timestampedQueue = [];
740
814
  this.plainQueue = [];
741
815
  this._latestFrame = null;
742
- this.lastPulledFrame = null;
816
+ this.smoother.reset();
743
817
  this.lastDequeuedTime = 0;
818
+ this.lastUpdateTime = 0;
819
+ this.neutralTriggered = false;
820
+ this.outputBuffer = null;
744
821
  this.pendingChunks = [];
745
822
  this.inferenceRunning = false;
746
823
  this.getFrameCallCount = 0;
@@ -759,6 +836,7 @@ var _A2EProcessor = class _A2EProcessor {
759
836
  */
760
837
  getFrameForTime(currentTime) {
761
838
  this.getFrameCallCount++;
839
+ const now = getClock().now();
762
840
  const discardWindow = this.backend.backend === "wasm" ? 1.5 : 0.5;
763
841
  let discardCount = 0;
764
842
  while (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp < currentTime - discardWindow) {
@@ -774,13 +852,20 @@ var _A2EProcessor = class _A2EProcessor {
774
852
  nextFrameTs: this.timestampedQueue.length > 0 ? this.timestampedQueue[0].timestamp.toFixed(3) : "none"
775
853
  });
776
854
  }
855
+ let newDequeue = false;
777
856
  if (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp <= currentTime) {
778
857
  const { frame } = this.timestampedQueue.shift();
779
- this.lastPulledFrame = frame;
780
- this.lastDequeuedTime = getClock().now();
781
- return frame;
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;
782
867
  }
783
- if (this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
868
+ if (!newDequeue && this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
784
869
  logger4.debug("getFrameForTime: waiting for playback time to reach queued frames", {
785
870
  queueLen: this.timestampedQueue.length,
786
871
  frontTimestamp: this.timestampedQueue[0].timestamp.toFixed(4),
@@ -788,25 +873,24 @@ var _A2EProcessor = class _A2EProcessor {
788
873
  delta: (this.timestampedQueue[0].timestamp - currentTime).toFixed(4)
789
874
  });
790
875
  }
791
- if (this.lastPulledFrame) {
792
- const elapsed = getClock().now() - this.lastDequeuedTime;
793
- if (elapsed < HOLD_DURATION_MS) {
794
- return this.lastPulledFrame;
795
- }
796
- const decayElapsed = elapsed - HOLD_DURATION_MS;
797
- if (decayElapsed >= DECAY_DURATION_MS) {
798
- this.lastPulledFrame = null;
799
- return null;
800
- }
801
- const t = decayElapsed / DECAY_DURATION_MS;
802
- const factor = (1 - t) * (1 - t);
803
- if (!this.decayBuffer) this.decayBuffer = new Float32Array(52);
804
- for (let i = 0; i < 52; i++) {
805
- this.decayBuffer[i] = this.lastPulledFrame[i] * factor;
876
+ if (!newDequeue && this.smoother.hasTarget && !this.neutralTriggered) {
877
+ const elapsed = now - this.lastDequeuedTime;
878
+ if (elapsed >= NEUTRAL_THRESHOLD_MS) {
879
+ this.smoother.decayToNeutral();
880
+ this.neutralTriggered = true;
806
881
  }
807
- return this.decayBuffer;
808
882
  }
809
- return null;
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;
810
894
  }
811
895
  // ═══════════════════════════════════════════════════════════════════════
812
896
  // Frame Output — Push Mode (live mic, game loop)
@@ -10137,80 +10221,6 @@ var InterruptionHandler = class extends EventEmitter {
10137
10221
  }
10138
10222
  };
10139
10223
 
10140
- // src/inference/BlendshapeSmoother.ts
10141
- var NUM_BLENDSHAPES = 52;
10142
- var BlendshapeSmoother = class {
10143
- constructor(config) {
10144
- /** Whether any target has been set */
10145
- this._hasTarget = false;
10146
- this.halflife = config?.halflife ?? 0.06;
10147
- this.values = new Float32Array(NUM_BLENDSHAPES);
10148
- this.velocities = new Float32Array(NUM_BLENDSHAPES);
10149
- this.targets = new Float32Array(NUM_BLENDSHAPES);
10150
- }
10151
- /** Whether a target frame has been set (false until first setTarget call) */
10152
- get hasTarget() {
10153
- return this._hasTarget;
10154
- }
10155
- /**
10156
- * Set new target frame from inference output.
10157
- * Springs will converge toward these values on subsequent update() calls.
10158
- */
10159
- setTarget(frame) {
10160
- this.targets.set(frame);
10161
- this._hasTarget = true;
10162
- }
10163
- /**
10164
- * Advance all 52 springs by `dt` seconds and return the smoothed frame.
10165
- *
10166
- * Call this every render frame (e.g., inside requestAnimationFrame).
10167
- * Returns the internal values buffer — do NOT mutate the returned array.
10168
- *
10169
- * @param dt - Time step in seconds (e.g., 1/60 for 60fps)
10170
- * @returns Smoothed blendshape values (Float32Array of 52)
10171
- */
10172
- update(dt) {
10173
- if (!this._hasTarget) {
10174
- return this.values;
10175
- }
10176
- if (this.halflife <= 0) {
10177
- this.values.set(this.targets);
10178
- this.velocities.fill(0);
10179
- return this.values;
10180
- }
10181
- const damping = Math.LN2 / this.halflife;
10182
- const eydt = Math.exp(-damping * dt);
10183
- for (let i = 0; i < NUM_BLENDSHAPES; i++) {
10184
- const j0 = this.values[i] - this.targets[i];
10185
- const j1 = this.velocities[i] + j0 * damping;
10186
- this.values[i] = eydt * (j0 + j1 * dt) + this.targets[i];
10187
- this.velocities[i] = eydt * (this.velocities[i] - j1 * damping * dt);
10188
- this.values[i] = Math.max(0, Math.min(1, this.values[i]));
10189
- }
10190
- return this.values;
10191
- }
10192
- /**
10193
- * Decay all spring targets to neutral (0).
10194
- *
10195
- * Call when inference stalls (no new frames for threshold duration).
10196
- * The springs will smoothly close the mouth / relax the face over
10197
- * the halflife period rather than freezing.
10198
- */
10199
- decayToNeutral() {
10200
- this.targets.fill(0);
10201
- }
10202
- /**
10203
- * Reset all state (values, velocities, targets).
10204
- * Call when starting a new playback session.
10205
- */
10206
- reset() {
10207
- this.values.fill(0);
10208
- this.velocities.fill(0);
10209
- this.targets.fill(0);
10210
- this._hasTarget = false;
10211
- }
10212
- };
10213
-
10214
10224
  // src/inference/SafariSpeechRecognition.ts
10215
10225
  var logger33 = createLogger("SafariSpeech");
10216
10226
  var SafariSpeechRecognition = class _SafariSpeechRecognition {