@omote/core 0.9.3 → 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
@@ -608,12 +608,99 @@ 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
+ * 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
+ }
647
+ /**
648
+ * Advance all 52 springs by `dt` seconds and return the smoothed frame.
649
+ *
650
+ * Call this every render frame (e.g., inside requestAnimationFrame).
651
+ * Returns the internal values buffer — do NOT mutate the returned array.
652
+ *
653
+ * @param dt - Time step in seconds (e.g., 1/60 for 60fps)
654
+ * @returns Smoothed blendshape values (Float32Array of 52)
655
+ */
656
+ update(dt) {
657
+ if (!this._hasTarget) {
658
+ return this.values;
659
+ }
660
+ if (this.halflife <= 0) {
661
+ this.values.set(this.targets);
662
+ this.velocities.fill(0);
663
+ return this.values;
664
+ }
665
+ const damping = Math.LN2 / this.halflife;
666
+ const eydt = Math.exp(-damping * dt);
667
+ for (let i = 0; i < NUM_BLENDSHAPES; i++) {
668
+ const j0 = this.values[i] - this.targets[i];
669
+ const j1 = this.velocities[i] + j0 * damping;
670
+ this.values[i] = eydt * (j0 + j1 * dt) + this.targets[i];
671
+ this.velocities[i] = eydt * (this.velocities[i] - j1 * damping * dt);
672
+ this.values[i] = Math.max(0, Math.min(1, this.values[i]));
673
+ }
674
+ return this.values;
675
+ }
676
+ /**
677
+ * Decay all spring targets to neutral (0).
678
+ *
679
+ * Call when inference stalls (no new frames for threshold duration).
680
+ * The springs will smoothly close the mouth / relax the face over
681
+ * the halflife period rather than freezing.
682
+ */
683
+ decayToNeutral() {
684
+ this.targets.fill(0);
685
+ }
686
+ /**
687
+ * Reset all state (values, velocities, targets).
688
+ * Call when starting a new playback session.
689
+ */
690
+ reset() {
691
+ this.values.fill(0);
692
+ this.velocities.fill(0);
693
+ this.targets.fill(0);
694
+ this._hasTarget = false;
695
+ }
696
+ };
697
+
611
698
  // src/inference/A2EProcessor.ts
612
699
  var logger4 = createLogger("A2EProcessor");
613
700
  var FRAME_RATE = 30;
614
701
  var DRIP_INTERVAL_MS = 33;
615
702
  var HOLD_DURATION_MS = 400;
616
- var DECAY_DURATION_MS = 300;
703
+ var GAP_DECAY_HALFLIFE_S = 0.08;
617
704
  var _A2EProcessor = class _A2EProcessor {
618
705
  constructor(config) {
619
706
  this.writeOffset = 0;
@@ -628,6 +715,8 @@ var _A2EProcessor = class _A2EProcessor {
628
715
  this.lastPulledFrame = null;
629
716
  this.lastDequeuedTime = 0;
630
717
  this.decayBuffer = null;
718
+ this.gapDecayStarted = false;
719
+ this.lastSmootherUpdate = 0;
631
720
  // Inference serialization
632
721
  this.inferenceRunning = false;
633
722
  this.pendingChunks = [];
@@ -642,6 +731,7 @@ var _A2EProcessor = class _A2EProcessor {
642
731
  this.onError = config.onError;
643
732
  this.bufferCapacity = this.chunkSize * 2;
644
733
  this.buffer = new Float32Array(this.bufferCapacity);
734
+ this.smoother = new BlendshapeSmoother({ halflife: GAP_DECAY_HALFLIFE_S });
645
735
  }
646
736
  // ═══════════════════════════════════════════════════════════════════════
647
737
  // Audio Input
@@ -744,6 +834,9 @@ var _A2EProcessor = class _A2EProcessor {
744
834
  this.pendingChunks = [];
745
835
  this.inferenceRunning = false;
746
836
  this.getFrameCallCount = 0;
837
+ this.smoother.reset();
838
+ this.gapDecayStarted = false;
839
+ this.lastSmootherUpdate = 0;
747
840
  }
748
841
  // ═══════════════════════════════════════════════════════════════════════
749
842
  // Frame Output — Pull Mode (TTS playback)
@@ -778,6 +871,7 @@ var _A2EProcessor = class _A2EProcessor {
778
871
  const { frame } = this.timestampedQueue.shift();
779
872
  this.lastPulledFrame = frame;
780
873
  this.lastDequeuedTime = getClock().now();
874
+ this.gapDecayStarted = false;
781
875
  return frame;
782
876
  }
783
877
  if (this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
@@ -789,21 +883,30 @@ var _A2EProcessor = class _A2EProcessor {
789
883
  });
790
884
  }
791
885
  if (this.lastPulledFrame) {
792
- const elapsed = getClock().now() - this.lastDequeuedTime;
886
+ const now = getClock().now();
887
+ const elapsed = now - this.lastDequeuedTime;
793
888
  if (elapsed < HOLD_DURATION_MS) {
794
889
  return this.lastPulledFrame;
795
890
  }
796
- const decayElapsed = elapsed - HOLD_DURATION_MS;
797
- if (decayElapsed >= DECAY_DURATION_MS) {
891
+ if (!this.gapDecayStarted) {
892
+ this.smoother.setPosition(this.lastPulledFrame);
893
+ this.smoother.decayToNeutral();
894
+ this.gapDecayStarted = true;
895
+ this.lastSmootherUpdate = now;
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) {
798
905
  this.lastPulledFrame = null;
799
906
  return null;
800
907
  }
801
- const t = decayElapsed / DECAY_DURATION_MS;
802
- const factor = (1 - t) * (1 - t);
803
908
  if (!this.decayBuffer) this.decayBuffer = new Float32Array(52);
804
- for (let i = 0; i < 52; i++) {
805
- this.decayBuffer[i] = this.lastPulledFrame[i] * factor;
806
- }
909
+ this.decayBuffer.set(smoothed);
807
910
  return this.decayBuffer;
808
911
  }
809
912
  return null;
@@ -10137,80 +10240,6 @@ var InterruptionHandler = class extends EventEmitter {
10137
10240
  }
10138
10241
  };
10139
10242
 
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
10243
  // src/inference/SafariSpeechRecognition.ts
10215
10244
  var logger33 = createLogger("SafariSpeech");
10216
10245
  var SafariSpeechRecognition = class _SafariSpeechRecognition {