@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 +14 -10
- package/dist/index.d.ts +14 -10
- package/dist/index.js +62 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
702
|
-
this.
|
|
703
|
-
this.
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
859
|
-
this.
|
|
860
|
-
|
|
861
|
-
|
|
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 (
|
|
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 (
|
|
886
|
+
if (this.lastPulledFrame) {
|
|
887
|
+
const now = getClock().now();
|
|
877
888
|
const elapsed = now - this.lastDequeuedTime;
|
|
878
|
-
if (elapsed
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|