@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.d.mts CHANGED
@@ -2517,6 +2517,12 @@ interface A2EProcessorConfig {
2517
2517
  * different expression intensity across face regions (brows, eyes, cheeks).
2518
2518
  */
2519
2519
  identityIndex?: number;
2520
+ /**
2521
+ * Spring halflife for blendshape smoothing (seconds).
2522
+ * - 0.06 (default): Sweet spot for lip sync
2523
+ * - 0: Bypass (raw frames, no smoothing)
2524
+ */
2525
+ smoothingHalflife?: number;
2520
2526
  /** Callback fired with each blendshape frame (push mode) */
2521
2527
  onFrame?: (frame: Float32Array) => void;
2522
2528
  /** Error callback */
@@ -2538,9 +2544,11 @@ declare class A2EProcessor {
2538
2544
  private plainQueue;
2539
2545
  private _latestFrame;
2540
2546
  private dripInterval;
2541
- private lastPulledFrame;
2547
+ private smoother;
2542
2548
  private lastDequeuedTime;
2543
- private decayBuffer;
2549
+ private lastUpdateTime;
2550
+ private neutralTriggered;
2551
+ private outputBuffer;
2544
2552
  private inferenceRunning;
2545
2553
  private pendingChunks;
2546
2554
  private getFrameCallCount;
package/dist/index.d.ts CHANGED
@@ -2517,6 +2517,12 @@ interface A2EProcessorConfig {
2517
2517
  * different expression intensity across face regions (brows, eyes, cheeks).
2518
2518
  */
2519
2519
  identityIndex?: number;
2520
+ /**
2521
+ * Spring halflife for blendshape smoothing (seconds).
2522
+ * - 0.06 (default): Sweet spot for lip sync
2523
+ * - 0: Bypass (raw frames, no smoothing)
2524
+ */
2525
+ smoothingHalflife?: number;
2520
2526
  /** Callback fired with each blendshape frame (push mode) */
2521
2527
  onFrame?: (frame: Float32Array) => void;
2522
2528
  /** Error callback */
@@ -2538,9 +2544,11 @@ declare class A2EProcessor {
2538
2544
  private plainQueue;
2539
2545
  private _latestFrame;
2540
2546
  private dripInterval;
2541
- private lastPulledFrame;
2547
+ private smoother;
2542
2548
  private lastDequeuedTime;
2543
- private decayBuffer;
2549
+ private lastUpdateTime;
2550
+ private neutralTriggered;
2551
+ private outputBuffer;
2544
2552
  private inferenceRunning;
2545
2553
  private pendingChunks;
2546
2554
  private getFrameCallCount;
package/dist/index.js CHANGED
@@ -1687,12 +1687,85 @@ var AudioChunkCoalescer = class {
1687
1687
  }
1688
1688
  };
1689
1689
 
1690
+ // src/inference/BlendshapeSmoother.ts
1691
+ var NUM_BLENDSHAPES = 52;
1692
+ var BlendshapeSmoother = class {
1693
+ constructor(config) {
1694
+ /** Whether any target has been set */
1695
+ this._hasTarget = false;
1696
+ this.halflife = config?.halflife ?? 0.06;
1697
+ this.values = new Float32Array(NUM_BLENDSHAPES);
1698
+ this.velocities = new Float32Array(NUM_BLENDSHAPES);
1699
+ this.targets = new Float32Array(NUM_BLENDSHAPES);
1700
+ }
1701
+ /** Whether a target frame has been set (false until first setTarget call) */
1702
+ get hasTarget() {
1703
+ return this._hasTarget;
1704
+ }
1705
+ /**
1706
+ * Set new target frame from inference output.
1707
+ * Springs will converge toward these values on subsequent update() calls.
1708
+ */
1709
+ setTarget(frame) {
1710
+ this.targets.set(frame);
1711
+ this._hasTarget = true;
1712
+ }
1713
+ /**
1714
+ * Advance all 52 springs by `dt` seconds and return the smoothed frame.
1715
+ *
1716
+ * Call this every render frame (e.g., inside requestAnimationFrame).
1717
+ * Returns the internal values buffer — do NOT mutate the returned array.
1718
+ *
1719
+ * @param dt - Time step in seconds (e.g., 1/60 for 60fps)
1720
+ * @returns Smoothed blendshape values (Float32Array of 52)
1721
+ */
1722
+ update(dt) {
1723
+ if (!this._hasTarget) {
1724
+ return this.values;
1725
+ }
1726
+ if (this.halflife <= 0) {
1727
+ this.values.set(this.targets);
1728
+ this.velocities.fill(0);
1729
+ return this.values;
1730
+ }
1731
+ const damping = Math.LN2 / this.halflife;
1732
+ const eydt = Math.exp(-damping * dt);
1733
+ for (let i = 0; i < NUM_BLENDSHAPES; i++) {
1734
+ const j0 = this.values[i] - this.targets[i];
1735
+ const j1 = this.velocities[i] + j0 * damping;
1736
+ this.values[i] = eydt * (j0 + j1 * dt) + this.targets[i];
1737
+ this.velocities[i] = eydt * (this.velocities[i] - j1 * damping * dt);
1738
+ this.values[i] = Math.max(0, Math.min(1, this.values[i]));
1739
+ }
1740
+ return this.values;
1741
+ }
1742
+ /**
1743
+ * Decay all spring targets to neutral (0).
1744
+ *
1745
+ * Call when inference stalls (no new frames for threshold duration).
1746
+ * The springs will smoothly close the mouth / relax the face over
1747
+ * the halflife period rather than freezing.
1748
+ */
1749
+ decayToNeutral() {
1750
+ this.targets.fill(0);
1751
+ }
1752
+ /**
1753
+ * Reset all state (values, velocities, targets).
1754
+ * Call when starting a new playback session.
1755
+ */
1756
+ reset() {
1757
+ this.values.fill(0);
1758
+ this.velocities.fill(0);
1759
+ this.targets.fill(0);
1760
+ this._hasTarget = false;
1761
+ }
1762
+ };
1763
+
1690
1764
  // src/inference/A2EProcessor.ts
1691
1765
  var logger4 = createLogger("A2EProcessor");
1692
1766
  var FRAME_RATE = 30;
1693
1767
  var DRIP_INTERVAL_MS = 33;
1694
- var HOLD_DURATION_MS = 400;
1695
- var DECAY_DURATION_MS = 300;
1768
+ var NEUTRAL_THRESHOLD_MS = 1500;
1696
1769
  var _A2EProcessor = class _A2EProcessor {
1697
1770
  constructor(config) {
1698
1771
  this.writeOffset = 0;
@@ -1703,10 +1776,10 @@ var _A2EProcessor = class _A2EProcessor {
1703
1776
  // Push mode state
1704
1777
  this._latestFrame = null;
1705
1778
  this.dripInterval = null;
1706
- // Last-frame-hold for pull mode (prevents avatar freezing between frames)
1707
- this.lastPulledFrame = null;
1708
1779
  this.lastDequeuedTime = 0;
1709
- this.decayBuffer = null;
1780
+ this.lastUpdateTime = 0;
1781
+ this.neutralTriggered = false;
1782
+ this.outputBuffer = null;
1710
1783
  // Inference serialization
1711
1784
  this.inferenceRunning = false;
1712
1785
  this.pendingChunks = [];
@@ -1719,6 +1792,7 @@ var _A2EProcessor = class _A2EProcessor {
1719
1792
  this.identityIndex = config.identityIndex ?? 0;
1720
1793
  this.onFrame = config.onFrame;
1721
1794
  this.onError = config.onError;
1795
+ this.smoother = new BlendshapeSmoother({ halflife: config.smoothingHalflife ?? 0.06 });
1722
1796
  this.bufferCapacity = this.chunkSize * 2;
1723
1797
  this.buffer = new Float32Array(this.bufferCapacity);
1724
1798
  }
@@ -1818,8 +1892,11 @@ var _A2EProcessor = class _A2EProcessor {
1818
1892
  this.timestampedQueue = [];
1819
1893
  this.plainQueue = [];
1820
1894
  this._latestFrame = null;
1821
- this.lastPulledFrame = null;
1895
+ this.smoother.reset();
1822
1896
  this.lastDequeuedTime = 0;
1897
+ this.lastUpdateTime = 0;
1898
+ this.neutralTriggered = false;
1899
+ this.outputBuffer = null;
1823
1900
  this.pendingChunks = [];
1824
1901
  this.inferenceRunning = false;
1825
1902
  this.getFrameCallCount = 0;
@@ -1838,6 +1915,7 @@ var _A2EProcessor = class _A2EProcessor {
1838
1915
  */
1839
1916
  getFrameForTime(currentTime) {
1840
1917
  this.getFrameCallCount++;
1918
+ const now = getClock().now();
1841
1919
  const discardWindow = this.backend.backend === "wasm" ? 1.5 : 0.5;
1842
1920
  let discardCount = 0;
1843
1921
  while (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp < currentTime - discardWindow) {
@@ -1853,13 +1931,20 @@ var _A2EProcessor = class _A2EProcessor {
1853
1931
  nextFrameTs: this.timestampedQueue.length > 0 ? this.timestampedQueue[0].timestamp.toFixed(3) : "none"
1854
1932
  });
1855
1933
  }
1934
+ let newDequeue = false;
1856
1935
  if (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp <= currentTime) {
1857
1936
  const { frame } = this.timestampedQueue.shift();
1858
- this.lastPulledFrame = frame;
1859
- this.lastDequeuedTime = getClock().now();
1860
- return frame;
1937
+ const firstTarget = !this.smoother.hasTarget;
1938
+ this.smoother.setTarget(frame);
1939
+ if (firstTarget) {
1940
+ this.smoother.update(1);
1941
+ this.lastUpdateTime = now;
1942
+ }
1943
+ newDequeue = true;
1944
+ this.neutralTriggered = false;
1945
+ this.lastDequeuedTime = now;
1861
1946
  }
1862
- if (this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
1947
+ if (!newDequeue && this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
1863
1948
  logger4.debug("getFrameForTime: waiting for playback time to reach queued frames", {
1864
1949
  queueLen: this.timestampedQueue.length,
1865
1950
  frontTimestamp: this.timestampedQueue[0].timestamp.toFixed(4),
@@ -1867,25 +1952,24 @@ var _A2EProcessor = class _A2EProcessor {
1867
1952
  delta: (this.timestampedQueue[0].timestamp - currentTime).toFixed(4)
1868
1953
  });
1869
1954
  }
1870
- if (this.lastPulledFrame) {
1871
- const elapsed = getClock().now() - this.lastDequeuedTime;
1872
- if (elapsed < HOLD_DURATION_MS) {
1873
- return this.lastPulledFrame;
1874
- }
1875
- const decayElapsed = elapsed - HOLD_DURATION_MS;
1876
- if (decayElapsed >= DECAY_DURATION_MS) {
1877
- this.lastPulledFrame = null;
1878
- return null;
1879
- }
1880
- const t = decayElapsed / DECAY_DURATION_MS;
1881
- const factor = (1 - t) * (1 - t);
1882
- if (!this.decayBuffer) this.decayBuffer = new Float32Array(52);
1883
- for (let i = 0; i < 52; i++) {
1884
- this.decayBuffer[i] = this.lastPulledFrame[i] * factor;
1955
+ if (!newDequeue && this.smoother.hasTarget && !this.neutralTriggered) {
1956
+ const elapsed = now - this.lastDequeuedTime;
1957
+ if (elapsed >= NEUTRAL_THRESHOLD_MS) {
1958
+ this.smoother.decayToNeutral();
1959
+ this.neutralTriggered = true;
1885
1960
  }
1886
- return this.decayBuffer;
1887
1961
  }
1888
- return null;
1962
+ if (!this.smoother.hasTarget) {
1963
+ return null;
1964
+ }
1965
+ const dt = Math.min((now - this.lastUpdateTime) / 1e3, 0.1);
1966
+ const smoothed = this.smoother.update(dt);
1967
+ this.lastUpdateTime = now;
1968
+ if (newDequeue || !this.outputBuffer) {
1969
+ this.outputBuffer = new Float32Array(52);
1970
+ }
1971
+ this.outputBuffer.set(smoothed);
1972
+ return this.outputBuffer;
1889
1973
  }
1890
1974
  // ═══════════════════════════════════════════════════════════════════════
1891
1975
  // Frame Output — Push Mode (live mic, game loop)
@@ -11216,80 +11300,6 @@ var InterruptionHandler = class extends EventEmitter {
11216
11300
  }
11217
11301
  };
11218
11302
 
11219
- // src/inference/BlendshapeSmoother.ts
11220
- var NUM_BLENDSHAPES = 52;
11221
- var BlendshapeSmoother = class {
11222
- constructor(config) {
11223
- /** Whether any target has been set */
11224
- this._hasTarget = false;
11225
- this.halflife = config?.halflife ?? 0.06;
11226
- this.values = new Float32Array(NUM_BLENDSHAPES);
11227
- this.velocities = new Float32Array(NUM_BLENDSHAPES);
11228
- this.targets = new Float32Array(NUM_BLENDSHAPES);
11229
- }
11230
- /** Whether a target frame has been set (false until first setTarget call) */
11231
- get hasTarget() {
11232
- return this._hasTarget;
11233
- }
11234
- /**
11235
- * Set new target frame from inference output.
11236
- * Springs will converge toward these values on subsequent update() calls.
11237
- */
11238
- setTarget(frame) {
11239
- this.targets.set(frame);
11240
- this._hasTarget = true;
11241
- }
11242
- /**
11243
- * Advance all 52 springs by `dt` seconds and return the smoothed frame.
11244
- *
11245
- * Call this every render frame (e.g., inside requestAnimationFrame).
11246
- * Returns the internal values buffer — do NOT mutate the returned array.
11247
- *
11248
- * @param dt - Time step in seconds (e.g., 1/60 for 60fps)
11249
- * @returns Smoothed blendshape values (Float32Array of 52)
11250
- */
11251
- update(dt) {
11252
- if (!this._hasTarget) {
11253
- return this.values;
11254
- }
11255
- if (this.halflife <= 0) {
11256
- this.values.set(this.targets);
11257
- this.velocities.fill(0);
11258
- return this.values;
11259
- }
11260
- const damping = Math.LN2 / this.halflife;
11261
- const eydt = Math.exp(-damping * dt);
11262
- for (let i = 0; i < NUM_BLENDSHAPES; i++) {
11263
- const j0 = this.values[i] - this.targets[i];
11264
- const j1 = this.velocities[i] + j0 * damping;
11265
- this.values[i] = eydt * (j0 + j1 * dt) + this.targets[i];
11266
- this.velocities[i] = eydt * (this.velocities[i] - j1 * damping * dt);
11267
- this.values[i] = Math.max(0, Math.min(1, this.values[i]));
11268
- }
11269
- return this.values;
11270
- }
11271
- /**
11272
- * Decay all spring targets to neutral (0).
11273
- *
11274
- * Call when inference stalls (no new frames for threshold duration).
11275
- * The springs will smoothly close the mouth / relax the face over
11276
- * the halflife period rather than freezing.
11277
- */
11278
- decayToNeutral() {
11279
- this.targets.fill(0);
11280
- }
11281
- /**
11282
- * Reset all state (values, velocities, targets).
11283
- * Call when starting a new playback session.
11284
- */
11285
- reset() {
11286
- this.values.fill(0);
11287
- this.velocities.fill(0);
11288
- this.targets.fill(0);
11289
- this._hasTarget = false;
11290
- }
11291
- };
11292
-
11293
11303
  // src/inference/SafariSpeechRecognition.ts
11294
11304
  var logger33 = createLogger("SafariSpeech");
11295
11305
  var SafariSpeechRecognition = class _SafariSpeechRecognition {