@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.d.mts CHANGED
@@ -2517,12 +2517,6 @@ 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;
2526
2520
  /** Callback fired with each blendshape frame (push mode) */
2527
2521
  onFrame?: (frame: Float32Array) => void;
2528
2522
  /** Error callback */
@@ -2544,11 +2538,12 @@ declare class A2EProcessor {
2544
2538
  private plainQueue;
2545
2539
  private _latestFrame;
2546
2540
  private dripInterval;
2547
- private smoother;
2541
+ private lastPulledFrame;
2548
2542
  private lastDequeuedTime;
2549
- private lastUpdateTime;
2550
- private neutralTriggered;
2551
- private outputBuffer;
2543
+ private decayBuffer;
2544
+ private smoother;
2545
+ private gapDecayStarted;
2546
+ private lastSmootherUpdate;
2552
2547
  private inferenceRunning;
2553
2548
  private pendingChunks;
2554
2549
  private getFrameCallCount;
@@ -2668,6 +2663,15 @@ declare class BlendshapeSmoother {
2668
2663
  * Springs will converge toward these values on subsequent update() calls.
2669
2664
  */
2670
2665
  setTarget(frame: Float32Array): void;
2666
+ /**
2667
+ * Snap current position to a frame without triggering spring physics.
2668
+ * Zeroes velocities so the spring starts from rest at this position.
2669
+ *
2670
+ * Used by A2EProcessor to seed the smoother when entering gap decay —
2671
+ * positions the spring at the last real inference frame before decaying
2672
+ * toward neutral.
2673
+ */
2674
+ setPosition(frame: Float32Array): void;
2671
2675
  /**
2672
2676
  * Advance all 52 springs by `dt` seconds and return the smoothed frame.
2673
2677
  *
package/dist/index.d.ts CHANGED
@@ -2517,12 +2517,6 @@ 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;
2526
2520
  /** Callback fired with each blendshape frame (push mode) */
2527
2521
  onFrame?: (frame: Float32Array) => void;
2528
2522
  /** Error callback */
@@ -2544,11 +2538,12 @@ declare class A2EProcessor {
2544
2538
  private plainQueue;
2545
2539
  private _latestFrame;
2546
2540
  private dripInterval;
2547
- private smoother;
2541
+ private lastPulledFrame;
2548
2542
  private lastDequeuedTime;
2549
- private lastUpdateTime;
2550
- private neutralTriggered;
2551
- private outputBuffer;
2543
+ private decayBuffer;
2544
+ private smoother;
2545
+ private gapDecayStarted;
2546
+ private lastSmootherUpdate;
2552
2547
  private inferenceRunning;
2553
2548
  private pendingChunks;
2554
2549
  private getFrameCallCount;
@@ -2668,6 +2663,15 @@ declare class BlendshapeSmoother {
2668
2663
  * Springs will converge toward these values on subsequent update() calls.
2669
2664
  */
2670
2665
  setTarget(frame: Float32Array): void;
2666
+ /**
2667
+ * Snap current position to a frame without triggering spring physics.
2668
+ * Zeroes velocities so the spring starts from rest at this position.
2669
+ *
2670
+ * Used by A2EProcessor to seed the smoother when entering gap decay —
2671
+ * positions the spring at the last real inference frame before decaying
2672
+ * toward neutral.
2673
+ */
2674
+ setPosition(frame: Float32Array): void;
2671
2675
  /**
2672
2676
  * Advance all 52 springs by `dt` seconds and return the smoothed frame.
2673
2677
  *
package/dist/index.js CHANGED
@@ -1710,6 +1710,19 @@ var BlendshapeSmoother = class {
1710
1710
  this.targets.set(frame);
1711
1711
  this._hasTarget = true;
1712
1712
  }
1713
+ /**
1714
+ * Snap current position to a frame without triggering spring physics.
1715
+ * Zeroes velocities so the spring starts from rest at this position.
1716
+ *
1717
+ * Used by A2EProcessor to seed the smoother when entering gap decay —
1718
+ * positions the spring at the last real inference frame before decaying
1719
+ * toward neutral.
1720
+ */
1721
+ setPosition(frame) {
1722
+ this.values.set(frame);
1723
+ this.velocities.fill(0);
1724
+ this._hasTarget = true;
1725
+ }
1713
1726
  /**
1714
1727
  * Advance all 52 springs by `dt` seconds and return the smoothed frame.
1715
1728
  *
@@ -1765,7 +1778,8 @@ var BlendshapeSmoother = class {
1765
1778
  var logger4 = createLogger("A2EProcessor");
1766
1779
  var FRAME_RATE = 30;
1767
1780
  var DRIP_INTERVAL_MS = 33;
1768
- var NEUTRAL_THRESHOLD_MS = 1500;
1781
+ var HOLD_DURATION_MS = 400;
1782
+ var GAP_DECAY_HALFLIFE_S = 0.08;
1769
1783
  var _A2EProcessor = class _A2EProcessor {
1770
1784
  constructor(config) {
1771
1785
  this.writeOffset = 0;
@@ -1776,10 +1790,12 @@ var _A2EProcessor = class _A2EProcessor {
1776
1790
  // Push mode state
1777
1791
  this._latestFrame = null;
1778
1792
  this.dripInterval = null;
1793
+ // Last-frame-hold for pull mode (prevents avatar freezing between frames)
1794
+ this.lastPulledFrame = null;
1779
1795
  this.lastDequeuedTime = 0;
1780
- this.lastUpdateTime = 0;
1781
- this.neutralTriggered = false;
1782
- this.outputBuffer = null;
1796
+ this.decayBuffer = null;
1797
+ this.gapDecayStarted = false;
1798
+ this.lastSmootherUpdate = 0;
1783
1799
  // Inference serialization
1784
1800
  this.inferenceRunning = false;
1785
1801
  this.pendingChunks = [];
@@ -1792,9 +1808,9 @@ var _A2EProcessor = class _A2EProcessor {
1792
1808
  this.identityIndex = config.identityIndex ?? 0;
1793
1809
  this.onFrame = config.onFrame;
1794
1810
  this.onError = config.onError;
1795
- this.smoother = new BlendshapeSmoother({ halflife: config.smoothingHalflife ?? 0.06 });
1796
1811
  this.bufferCapacity = this.chunkSize * 2;
1797
1812
  this.buffer = new Float32Array(this.bufferCapacity);
1813
+ this.smoother = new BlendshapeSmoother({ halflife: GAP_DECAY_HALFLIFE_S });
1798
1814
  }
1799
1815
  // ═══════════════════════════════════════════════════════════════════════
1800
1816
  // Audio Input
@@ -1869,18 +1885,19 @@ var _A2EProcessor = class _A2EProcessor {
1869
1885
  */
1870
1886
  async flush() {
1871
1887
  if (this.disposed || this.writeOffset === 0) return;
1888
+ const actualSamples = this.writeOffset;
1872
1889
  const padded = new Float32Array(this.chunkSize);
1873
- padded.set(this.buffer.subarray(0, this.writeOffset), 0);
1890
+ padded.set(this.buffer.subarray(0, actualSamples), 0);
1874
1891
  const chunkTimestamp = this.bufferStartTime > 0 ? this.bufferStartTime : void 0;
1875
1892
  logger4.info("flush: routing through drain pipeline", {
1876
- actualSamples: this.writeOffset,
1893
+ actualSamples,
1877
1894
  chunkTimestamp: chunkTimestamp?.toFixed(3),
1878
1895
  pendingChunks: this.pendingChunks.length,
1879
1896
  inferenceRunning: this.inferenceRunning
1880
1897
  });
1881
1898
  this.writeOffset = 0;
1882
1899
  this.bufferStartTime = 0;
1883
- this.pendingChunks.push({ chunk: padded, timestamp: chunkTimestamp });
1900
+ this.pendingChunks.push({ chunk: padded, timestamp: chunkTimestamp, actualSamples });
1884
1901
  this.drainPendingChunks();
1885
1902
  }
1886
1903
  /**
@@ -1892,14 +1909,14 @@ var _A2EProcessor = class _A2EProcessor {
1892
1909
  this.timestampedQueue = [];
1893
1910
  this.plainQueue = [];
1894
1911
  this._latestFrame = null;
1895
- this.smoother.reset();
1912
+ this.lastPulledFrame = null;
1896
1913
  this.lastDequeuedTime = 0;
1897
- this.lastUpdateTime = 0;
1898
- this.neutralTriggered = false;
1899
- this.outputBuffer = null;
1900
1914
  this.pendingChunks = [];
1901
1915
  this.inferenceRunning = false;
1902
1916
  this.getFrameCallCount = 0;
1917
+ this.smoother.reset();
1918
+ this.gapDecayStarted = false;
1919
+ this.lastSmootherUpdate = 0;
1903
1920
  }
1904
1921
  // ═══════════════════════════════════════════════════════════════════════
1905
1922
  // Frame Output — Pull Mode (TTS playback)
@@ -1915,7 +1932,6 @@ var _A2EProcessor = class _A2EProcessor {
1915
1932
  */
1916
1933
  getFrameForTime(currentTime) {
1917
1934
  this.getFrameCallCount++;
1918
- const now = getClock().now();
1919
1935
  const discardWindow = this.backend.backend === "wasm" ? 1.5 : 0.5;
1920
1936
  let discardCount = 0;
1921
1937
  while (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp < currentTime - discardWindow) {
@@ -1931,20 +1947,14 @@ var _A2EProcessor = class _A2EProcessor {
1931
1947
  nextFrameTs: this.timestampedQueue.length > 0 ? this.timestampedQueue[0].timestamp.toFixed(3) : "none"
1932
1948
  });
1933
1949
  }
1934
- let newDequeue = false;
1935
1950
  if (this.timestampedQueue.length > 0 && this.timestampedQueue[0].timestamp <= currentTime) {
1936
1951
  const { frame } = this.timestampedQueue.shift();
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;
1952
+ this.lastPulledFrame = frame;
1953
+ this.lastDequeuedTime = getClock().now();
1954
+ this.gapDecayStarted = false;
1955
+ return frame;
1946
1956
  }
1947
- if (!newDequeue && this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
1957
+ if (this.timestampedQueue.length > 0 && this.getFrameCallCount % 60 === 0) {
1948
1958
  logger4.debug("getFrameForTime: waiting for playback time to reach queued frames", {
1949
1959
  queueLen: this.timestampedQueue.length,
1950
1960
  frontTimestamp: this.timestampedQueue[0].timestamp.toFixed(4),
@@ -1952,24 +1962,34 @@ var _A2EProcessor = class _A2EProcessor {
1952
1962
  delta: (this.timestampedQueue[0].timestamp - currentTime).toFixed(4)
1953
1963
  });
1954
1964
  }
1955
- if (!newDequeue && this.smoother.hasTarget && !this.neutralTriggered) {
1965
+ if (this.lastPulledFrame) {
1966
+ const now = getClock().now();
1956
1967
  const elapsed = now - this.lastDequeuedTime;
1957
- if (elapsed >= NEUTRAL_THRESHOLD_MS) {
1968
+ if (elapsed < HOLD_DURATION_MS) {
1969
+ return this.lastPulledFrame;
1970
+ }
1971
+ if (!this.gapDecayStarted) {
1972
+ this.smoother.setPosition(this.lastPulledFrame);
1958
1973
  this.smoother.decayToNeutral();
1959
- this.neutralTriggered = true;
1974
+ this.gapDecayStarted = true;
1975
+ this.lastSmootherUpdate = now;
1960
1976
  }
1977
+ const dt = Math.min((now - this.lastSmootherUpdate) / 1e3, 0.1);
1978
+ this.lastSmootherUpdate = now;
1979
+ const smoothed = this.smoother.update(dt);
1980
+ let maxVal = 0;
1981
+ for (let i = 0; i < 52; i++) {
1982
+ if (smoothed[i] > maxVal) maxVal = smoothed[i];
1983
+ }
1984
+ if (maxVal < 1e-3) {
1985
+ this.lastPulledFrame = null;
1986
+ return null;
1987
+ }
1988
+ if (!this.decayBuffer) this.decayBuffer = new Float32Array(52);
1989
+ this.decayBuffer.set(smoothed);
1990
+ return this.decayBuffer;
1961
1991
  }
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;
1992
+ return null;
1973
1993
  }
1974
1994
  // ═══════════════════════════════════════════════════════════════════════
1975
1995
  // Frame Output — Push Mode (live mic, game loop)
@@ -2035,14 +2055,15 @@ var _A2EProcessor = class _A2EProcessor {
2035
2055
  logger4.info("drainPendingChunks starting", { pendingChunks: this.pendingChunks.length });
2036
2056
  const processNext = async () => {
2037
2057
  while (this.pendingChunks.length > 0 && !this.disposed) {
2038
- const { chunk, timestamp } = this.pendingChunks.shift();
2058
+ const { chunk, timestamp, actualSamples } = this.pendingChunks.shift();
2039
2059
  try {
2040
2060
  const t0 = getClock().now();
2041
2061
  const result = await this.backend.infer(chunk, this.identityIndex);
2042
2062
  const inferMs = Math.round(getClock().now() - t0);
2043
- const actualDuration = chunk.length / this.sampleRate;
2063
+ const effectiveSamples = actualSamples ?? chunk.length;
2064
+ const actualDuration = effectiveSamples / this.sampleRate;
2044
2065
  const actualFrameCount = Math.ceil(actualDuration * FRAME_RATE);
2045
- const framesToQueue = Math.min(actualFrameCount, result.blendshapes.length);
2066
+ const framesToQueue = Math.min(Math.max(1, actualFrameCount), result.blendshapes.length);
2046
2067
  logger4.info("Inference complete", {
2047
2068
  inferMs,
2048
2069
  modelFrames: result.blendshapes.length,