@omote/core 0.5.3 → 0.5.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/README.md +222 -443
- package/dist/index.d.mts +95 -810
- package/dist/index.d.ts +95 -810
- package/dist/index.js +105 -1220
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +105 -1220
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -510,6 +510,7 @@ var A2EProcessor = class {
|
|
|
510
510
|
this.backend = config.backend;
|
|
511
511
|
this.sampleRate = config.sampleRate ?? 16e3;
|
|
512
512
|
this.chunkSize = config.chunkSize ?? config.backend.chunkSize ?? 16e3;
|
|
513
|
+
this.identityIndex = config.identityIndex ?? 0;
|
|
513
514
|
this.onFrame = config.onFrame;
|
|
514
515
|
this.onError = config.onError;
|
|
515
516
|
this.bufferCapacity = this.chunkSize * 2;
|
|
@@ -717,7 +718,7 @@ var A2EProcessor = class {
|
|
|
717
718
|
const { chunk, timestamp } = this.pendingChunks.shift();
|
|
718
719
|
try {
|
|
719
720
|
const t0 = performance.now();
|
|
720
|
-
const result = await this.backend.infer(chunk);
|
|
721
|
+
const result = await this.backend.infer(chunk, this.identityIndex);
|
|
721
722
|
const inferMs = Math.round(performance.now() - t0);
|
|
722
723
|
const actualDuration = chunk.length / this.sampleRate;
|
|
723
724
|
const actualFrameCount = Math.ceil(actualDuration * FRAME_RATE);
|
|
@@ -2778,13 +2779,6 @@ function pcm16ToFloat32(buffer) {
|
|
|
2778
2779
|
}
|
|
2779
2780
|
return float32;
|
|
2780
2781
|
}
|
|
2781
|
-
function int16ToFloat32(int16) {
|
|
2782
|
-
const float32 = new Float32Array(int16.length);
|
|
2783
|
-
for (let i = 0; i < int16.length; i++) {
|
|
2784
|
-
float32[i] = int16[i] / 32768;
|
|
2785
|
-
}
|
|
2786
|
-
return float32;
|
|
2787
|
-
}
|
|
2788
2782
|
|
|
2789
2783
|
// src/audio/FullFacePipeline.ts
|
|
2790
2784
|
var logger4 = createLogger("FullFacePipeline");
|
|
@@ -2848,6 +2842,7 @@ var FullFacePipeline = class extends EventEmitter {
|
|
|
2848
2842
|
backend: options.lam,
|
|
2849
2843
|
sampleRate,
|
|
2850
2844
|
chunkSize,
|
|
2845
|
+
identityIndex: options.identityIndex,
|
|
2851
2846
|
onError: (error) => {
|
|
2852
2847
|
logger4.error("A2E inference error", { message: error.message, stack: error.stack });
|
|
2853
2848
|
this.emit("error", error);
|
|
@@ -3072,6 +3067,108 @@ var FullFacePipeline = class extends EventEmitter {
|
|
|
3072
3067
|
}
|
|
3073
3068
|
};
|
|
3074
3069
|
|
|
3070
|
+
// src/audio/InterruptionHandler.ts
|
|
3071
|
+
var InterruptionHandler = class extends EventEmitter {
|
|
3072
|
+
constructor(config = {}) {
|
|
3073
|
+
super();
|
|
3074
|
+
this.isSpeaking = false;
|
|
3075
|
+
this.speechStartTime = 0;
|
|
3076
|
+
this.lastSpeechTime = 0;
|
|
3077
|
+
this.silenceTimer = null;
|
|
3078
|
+
this.aiIsSpeaking = false;
|
|
3079
|
+
// Debouncing: only emit one interruption per speech session
|
|
3080
|
+
this.interruptionTriggeredThisSession = false;
|
|
3081
|
+
this.config = {
|
|
3082
|
+
vadThreshold: 0.5,
|
|
3083
|
+
// Silero VAD default
|
|
3084
|
+
minSpeechDurationMs: 200,
|
|
3085
|
+
// Google/Amazon barge-in standard
|
|
3086
|
+
silenceTimeoutMs: 500,
|
|
3087
|
+
// OpenAI Realtime API standard
|
|
3088
|
+
enabled: true,
|
|
3089
|
+
...config
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
/**
|
|
3093
|
+
* Process VAD result for interruption detection
|
|
3094
|
+
* @param vadProbability - Speech probability from VAD (0-1)
|
|
3095
|
+
* @param audioEnergy - Optional RMS energy for logging (default: 0)
|
|
3096
|
+
*/
|
|
3097
|
+
processVADResult(vadProbability, audioEnergy = 0) {
|
|
3098
|
+
if (!this.config.enabled) return;
|
|
3099
|
+
if (vadProbability > this.config.vadThreshold) {
|
|
3100
|
+
this.onSpeechDetected(audioEnergy || vadProbability);
|
|
3101
|
+
} else {
|
|
3102
|
+
this.onSilenceDetected();
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
/** Notify that AI started/stopped speaking */
|
|
3106
|
+
setAISpeaking(speaking) {
|
|
3107
|
+
this.aiIsSpeaking = speaking;
|
|
3108
|
+
}
|
|
3109
|
+
/** Enable/disable interruption detection */
|
|
3110
|
+
setEnabled(enabled) {
|
|
3111
|
+
this.config.enabled = enabled;
|
|
3112
|
+
if (!enabled) {
|
|
3113
|
+
this.reset();
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
/** Update configuration */
|
|
3117
|
+
updateConfig(config) {
|
|
3118
|
+
this.config = { ...this.config, ...config };
|
|
3119
|
+
}
|
|
3120
|
+
/** Reset state */
|
|
3121
|
+
reset() {
|
|
3122
|
+
this.isSpeaking = false;
|
|
3123
|
+
this.speechStartTime = 0;
|
|
3124
|
+
this.lastSpeechTime = 0;
|
|
3125
|
+
this.interruptionTriggeredThisSession = false;
|
|
3126
|
+
if (this.silenceTimer) {
|
|
3127
|
+
clearTimeout(this.silenceTimer);
|
|
3128
|
+
this.silenceTimer = null;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
/** Get current state */
|
|
3132
|
+
getState() {
|
|
3133
|
+
return {
|
|
3134
|
+
isSpeaking: this.isSpeaking,
|
|
3135
|
+
speechDurationMs: this.isSpeaking ? Date.now() - this.speechStartTime : 0
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
onSpeechDetected(rms) {
|
|
3139
|
+
const now = Date.now();
|
|
3140
|
+
this.lastSpeechTime = now;
|
|
3141
|
+
if (this.silenceTimer) {
|
|
3142
|
+
clearTimeout(this.silenceTimer);
|
|
3143
|
+
this.silenceTimer = null;
|
|
3144
|
+
}
|
|
3145
|
+
if (!this.isSpeaking) {
|
|
3146
|
+
this.isSpeaking = true;
|
|
3147
|
+
this.speechStartTime = now;
|
|
3148
|
+
this.emit("speech.detected", { rms });
|
|
3149
|
+
}
|
|
3150
|
+
if (this.aiIsSpeaking && !this.interruptionTriggeredThisSession) {
|
|
3151
|
+
const speechDuration = now - this.speechStartTime;
|
|
3152
|
+
if (speechDuration >= this.config.minSpeechDurationMs) {
|
|
3153
|
+
this.interruptionTriggeredThisSession = true;
|
|
3154
|
+
this.emit("interruption.triggered", { rms, durationMs: speechDuration });
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
onSilenceDetected() {
|
|
3159
|
+
if (!this.isSpeaking) return;
|
|
3160
|
+
if (!this.silenceTimer) {
|
|
3161
|
+
this.silenceTimer = setTimeout(() => {
|
|
3162
|
+
const durationMs = this.lastSpeechTime - this.speechStartTime;
|
|
3163
|
+
this.isSpeaking = false;
|
|
3164
|
+
this.silenceTimer = null;
|
|
3165
|
+
this.interruptionTriggeredThisSession = false;
|
|
3166
|
+
this.emit("speech.ended", { durationMs });
|
|
3167
|
+
}, this.config.silenceTimeoutMs);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3171
|
+
|
|
3075
3172
|
// src/inference/kaldiFbank.ts
|
|
3076
3173
|
function fft(re, im) {
|
|
3077
3174
|
const n = re.length;
|
|
@@ -8778,1214 +8875,6 @@ var EmotionController = class {
|
|
|
8778
8875
|
}
|
|
8779
8876
|
};
|
|
8780
8877
|
|
|
8781
|
-
// src/ai/adapters/AgentCoreAdapter.ts
|
|
8782
|
-
var AgentCoreAdapter = class extends EventEmitter {
|
|
8783
|
-
constructor(config) {
|
|
8784
|
-
super();
|
|
8785
|
-
this.name = "AgentCore";
|
|
8786
|
-
this._state = "disconnected";
|
|
8787
|
-
this._sessionId = null;
|
|
8788
|
-
this._isConnected = false;
|
|
8789
|
-
// Sub-components
|
|
8790
|
-
this.asr = null;
|
|
8791
|
-
this.vad = null;
|
|
8792
|
-
this.lam = null;
|
|
8793
|
-
this.pipeline = null;
|
|
8794
|
-
// WebSocket connection to AgentCore
|
|
8795
|
-
this.ws = null;
|
|
8796
|
-
this.wsReconnectAttempts = 0;
|
|
8797
|
-
this.maxReconnectAttempts = 5;
|
|
8798
|
-
// Audio buffers
|
|
8799
|
-
this.audioBuffer = [];
|
|
8800
|
-
// Conversation state
|
|
8801
|
-
this.history = [];
|
|
8802
|
-
this.currentConfig = null;
|
|
8803
|
-
// Interruption handling
|
|
8804
|
-
this.isSpeaking = false;
|
|
8805
|
-
this.currentTtsAbortController = null;
|
|
8806
|
-
// Auth token cache per tenant
|
|
8807
|
-
this.tokenCache = /* @__PURE__ */ new Map();
|
|
8808
|
-
this.agentCoreConfig = config;
|
|
8809
|
-
this.emotionController = new EmotionController();
|
|
8810
|
-
}
|
|
8811
|
-
get state() {
|
|
8812
|
-
return this._state;
|
|
8813
|
-
}
|
|
8814
|
-
get sessionId() {
|
|
8815
|
-
return this._sessionId;
|
|
8816
|
-
}
|
|
8817
|
-
get isConnected() {
|
|
8818
|
-
return this._isConnected;
|
|
8819
|
-
}
|
|
8820
|
-
/**
|
|
8821
|
-
* Connect to AgentCore with session configuration
|
|
8822
|
-
*/
|
|
8823
|
-
async connect(config) {
|
|
8824
|
-
this.currentConfig = config;
|
|
8825
|
-
this._sessionId = config.sessionId;
|
|
8826
|
-
try {
|
|
8827
|
-
const authToken = await this.getAuthToken(config.tenant);
|
|
8828
|
-
await Promise.all([
|
|
8829
|
-
this.initASR(),
|
|
8830
|
-
this.initLAM()
|
|
8831
|
-
]);
|
|
8832
|
-
await this.connectWebSocket(authToken, config);
|
|
8833
|
-
this._isConnected = true;
|
|
8834
|
-
this.setState("idle");
|
|
8835
|
-
this.emit("connection.opened", { sessionId: this._sessionId, adapter: this.name });
|
|
8836
|
-
} catch (error) {
|
|
8837
|
-
this.setState("error");
|
|
8838
|
-
this.emit("connection.error", {
|
|
8839
|
-
error,
|
|
8840
|
-
recoverable: true
|
|
8841
|
-
});
|
|
8842
|
-
throw error;
|
|
8843
|
-
}
|
|
8844
|
-
}
|
|
8845
|
-
/**
|
|
8846
|
-
* Disconnect and cleanup
|
|
8847
|
-
*/
|
|
8848
|
-
async disconnect() {
|
|
8849
|
-
this.currentTtsAbortController?.abort();
|
|
8850
|
-
if (this.pipeline) {
|
|
8851
|
-
this.pipeline.dispose();
|
|
8852
|
-
this.pipeline = null;
|
|
8853
|
-
}
|
|
8854
|
-
if (this.ws) {
|
|
8855
|
-
this.ws.close(1e3, "Client disconnect");
|
|
8856
|
-
this.ws = null;
|
|
8857
|
-
}
|
|
8858
|
-
await Promise.all([
|
|
8859
|
-
this.asr?.dispose(),
|
|
8860
|
-
this.vad?.dispose(),
|
|
8861
|
-
this.lam?.dispose()
|
|
8862
|
-
]);
|
|
8863
|
-
this._isConnected = false;
|
|
8864
|
-
this.setState("disconnected");
|
|
8865
|
-
this.emit("connection.closed", { reason: "Client disconnect" });
|
|
8866
|
-
}
|
|
8867
|
-
/**
|
|
8868
|
-
* Push user audio for processing
|
|
8869
|
-
*/
|
|
8870
|
-
pushAudio(audio) {
|
|
8871
|
-
if (!this._isConnected) return;
|
|
8872
|
-
if (this.isSpeaking) {
|
|
8873
|
-
this.detectVoiceActivity(audio).then((hasVoiceActivity) => {
|
|
8874
|
-
if (hasVoiceActivity) {
|
|
8875
|
-
this.interrupt();
|
|
8876
|
-
}
|
|
8877
|
-
}).catch((error) => {
|
|
8878
|
-
console.error("[AgentCore] VAD error during interruption detection:", error);
|
|
8879
|
-
});
|
|
8880
|
-
}
|
|
8881
|
-
const float32 = audio instanceof Float32Array ? audio : int16ToFloat32(audio);
|
|
8882
|
-
this.audioBuffer.push(float32);
|
|
8883
|
-
this.scheduleTranscription();
|
|
8884
|
-
}
|
|
8885
|
-
/**
|
|
8886
|
-
* Send text directly to AgentCore
|
|
8887
|
-
*/
|
|
8888
|
-
async sendText(text) {
|
|
8889
|
-
if (!this._isConnected || !this.ws) {
|
|
8890
|
-
throw new Error("Not connected to AgentCore");
|
|
8891
|
-
}
|
|
8892
|
-
this.addToHistory({
|
|
8893
|
-
role: "user",
|
|
8894
|
-
content: text,
|
|
8895
|
-
timestamp: Date.now()
|
|
8896
|
-
});
|
|
8897
|
-
this.setState("thinking");
|
|
8898
|
-
this.emit("ai.thinking.start", { timestamp: Date.now() });
|
|
8899
|
-
this.ws.send(JSON.stringify({
|
|
8900
|
-
type: "user_message",
|
|
8901
|
-
sessionId: this._sessionId,
|
|
8902
|
-
content: text,
|
|
8903
|
-
context: {
|
|
8904
|
-
history: this.history.slice(-10),
|
|
8905
|
-
// Last 10 messages
|
|
8906
|
-
emotion: Array.from(this.emotionController.emotion)
|
|
8907
|
-
}
|
|
8908
|
-
}));
|
|
8909
|
-
}
|
|
8910
|
-
/**
|
|
8911
|
-
* Interrupt current AI response
|
|
8912
|
-
*/
|
|
8913
|
-
interrupt() {
|
|
8914
|
-
if (!this.isSpeaking) return;
|
|
8915
|
-
this.emit("interruption.detected", { timestamp: Date.now() });
|
|
8916
|
-
this.currentTtsAbortController?.abort();
|
|
8917
|
-
this.currentTtsAbortController = null;
|
|
8918
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
8919
|
-
this.ws.send(JSON.stringify({
|
|
8920
|
-
type: "interrupt",
|
|
8921
|
-
sessionId: this._sessionId,
|
|
8922
|
-
timestamp: Date.now()
|
|
8923
|
-
}));
|
|
8924
|
-
}
|
|
8925
|
-
this.isSpeaking = false;
|
|
8926
|
-
this.setState("listening");
|
|
8927
|
-
this.emit("interruption.handled", { timestamp: Date.now(), action: "stop" });
|
|
8928
|
-
}
|
|
8929
|
-
getHistory() {
|
|
8930
|
-
return [...this.history];
|
|
8931
|
-
}
|
|
8932
|
-
clearHistory() {
|
|
8933
|
-
this.history = [];
|
|
8934
|
-
this.emit("memory.updated", { messageCount: 0 });
|
|
8935
|
-
}
|
|
8936
|
-
async healthCheck() {
|
|
8937
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
8938
|
-
return false;
|
|
8939
|
-
}
|
|
8940
|
-
return new Promise((resolve) => {
|
|
8941
|
-
const timeout = setTimeout(() => resolve(false), 5e3);
|
|
8942
|
-
const handler = (event) => {
|
|
8943
|
-
try {
|
|
8944
|
-
const data = JSON.parse(event.data);
|
|
8945
|
-
if (data.type === "pong") {
|
|
8946
|
-
clearTimeout(timeout);
|
|
8947
|
-
this.ws?.removeEventListener("message", handler);
|
|
8948
|
-
resolve(true);
|
|
8949
|
-
}
|
|
8950
|
-
} catch {
|
|
8951
|
-
}
|
|
8952
|
-
};
|
|
8953
|
-
this.ws?.addEventListener("message", handler);
|
|
8954
|
-
this.ws?.send(JSON.stringify({ type: "ping" }));
|
|
8955
|
-
});
|
|
8956
|
-
}
|
|
8957
|
-
// ==================== Private Methods ====================
|
|
8958
|
-
setState(state) {
|
|
8959
|
-
const previousState = this._state;
|
|
8960
|
-
this._state = state;
|
|
8961
|
-
this.emit("state.change", { state, previousState });
|
|
8962
|
-
}
|
|
8963
|
-
async getAuthToken(tenant) {
|
|
8964
|
-
const cached = this.tokenCache.get(tenant.tenantId);
|
|
8965
|
-
if (cached && cached.expiresAt > Date.now() + 6e4) {
|
|
8966
|
-
return cached.token;
|
|
8967
|
-
}
|
|
8968
|
-
if (tenant.credentials.authToken) {
|
|
8969
|
-
return tenant.credentials.authToken;
|
|
8970
|
-
}
|
|
8971
|
-
const endpoint = this.agentCoreConfig.endpoint;
|
|
8972
|
-
if (endpoint.startsWith("ws://") || endpoint.includes("localhost")) {
|
|
8973
|
-
return "local-dev-token";
|
|
8974
|
-
}
|
|
8975
|
-
const httpEndpoint = endpoint.replace("wss://", "https://").replace("ws://", "http://");
|
|
8976
|
-
const response = await fetch(`${httpEndpoint}/auth/token`, {
|
|
8977
|
-
method: "POST",
|
|
8978
|
-
headers: { "Content-Type": "application/json" },
|
|
8979
|
-
body: JSON.stringify({
|
|
8980
|
-
tenantId: tenant.tenantId,
|
|
8981
|
-
apiKey: tenant.credentials.apiKey
|
|
8982
|
-
})
|
|
8983
|
-
});
|
|
8984
|
-
if (!response.ok) {
|
|
8985
|
-
throw new Error(`Auth failed: ${response.statusText}`);
|
|
8986
|
-
}
|
|
8987
|
-
const { token, expiresIn } = await response.json();
|
|
8988
|
-
this.tokenCache.set(tenant.tenantId, {
|
|
8989
|
-
token,
|
|
8990
|
-
expiresAt: Date.now() + expiresIn * 1e3
|
|
8991
|
-
});
|
|
8992
|
-
return token;
|
|
8993
|
-
}
|
|
8994
|
-
async initASR() {
|
|
8995
|
-
await Promise.all([
|
|
8996
|
-
// SenseVoice ASR
|
|
8997
|
-
(async () => {
|
|
8998
|
-
this.asr = new SenseVoiceInference({
|
|
8999
|
-
modelUrl: "/models/sensevoice/model.int8.onnx",
|
|
9000
|
-
language: "auto"
|
|
9001
|
-
});
|
|
9002
|
-
await this.asr.load();
|
|
9003
|
-
})(),
|
|
9004
|
-
// Silero VAD for accurate voice activity detection
|
|
9005
|
-
(async () => {
|
|
9006
|
-
this.vad = new SileroVADInference({
|
|
9007
|
-
modelUrl: "/models/silero-vad.onnx",
|
|
9008
|
-
backend: "webgpu",
|
|
9009
|
-
sampleRate: 16e3,
|
|
9010
|
-
threshold: 0.5
|
|
9011
|
-
});
|
|
9012
|
-
await this.vad.load();
|
|
9013
|
-
})()
|
|
9014
|
-
]);
|
|
9015
|
-
}
|
|
9016
|
-
async initLAM() {
|
|
9017
|
-
const lamUrl = this.agentCoreConfig.models?.lamUrl || "/models/unified_wav2vec2_asr_a2e.onnx";
|
|
9018
|
-
this.lam = new Wav2Vec2Inference({
|
|
9019
|
-
modelUrl: lamUrl,
|
|
9020
|
-
backend: "auto"
|
|
9021
|
-
});
|
|
9022
|
-
await this.lam.load();
|
|
9023
|
-
await this.initPipeline();
|
|
9024
|
-
}
|
|
9025
|
-
async initPipeline() {
|
|
9026
|
-
if (!this.lam) {
|
|
9027
|
-
throw new Error("LAM must be initialized before pipeline");
|
|
9028
|
-
}
|
|
9029
|
-
this.pipeline = new FullFacePipeline({
|
|
9030
|
-
lam: this.lam,
|
|
9031
|
-
sampleRate: 16e3,
|
|
9032
|
-
chunkTargetMs: 200
|
|
9033
|
-
});
|
|
9034
|
-
await this.pipeline.initialize();
|
|
9035
|
-
this.pipeline.on("full_frame_ready", (fullFrame) => {
|
|
9036
|
-
const frame = fullFrame.blendshapes;
|
|
9037
|
-
this.emit("animation", {
|
|
9038
|
-
blendshapes: frame,
|
|
9039
|
-
get: (name) => {
|
|
9040
|
-
const idx = LAM_BLENDSHAPES.indexOf(name);
|
|
9041
|
-
return idx >= 0 ? frame[idx] : 0;
|
|
9042
|
-
},
|
|
9043
|
-
timestamp: Date.now(),
|
|
9044
|
-
// Wall clock for client-side logging only
|
|
9045
|
-
inferenceMs: 0
|
|
9046
|
-
// Pipeline handles LAM inference asynchronously
|
|
9047
|
-
});
|
|
9048
|
-
});
|
|
9049
|
-
this.pipeline.on("playback_complete", () => {
|
|
9050
|
-
this.isSpeaking = false;
|
|
9051
|
-
this.setState("idle");
|
|
9052
|
-
this.emit("audio.output.end", { durationMs: 0 });
|
|
9053
|
-
});
|
|
9054
|
-
this.pipeline.on("error", (error) => {
|
|
9055
|
-
console.error("[AgentCore] Pipeline error:", error);
|
|
9056
|
-
this.emit("connection.error", {
|
|
9057
|
-
error,
|
|
9058
|
-
recoverable: true
|
|
9059
|
-
});
|
|
9060
|
-
});
|
|
9061
|
-
}
|
|
9062
|
-
async connectWebSocket(authToken, config) {
|
|
9063
|
-
return new Promise((resolve, reject) => {
|
|
9064
|
-
const wsUrl = new URL(`${this.agentCoreConfig.endpoint.replace("http", "ws")}/ws`);
|
|
9065
|
-
wsUrl.searchParams.set("sessionId", config.sessionId);
|
|
9066
|
-
wsUrl.searchParams.set("characterId", config.tenant.characterId);
|
|
9067
|
-
this.ws = new WebSocket(wsUrl.toString());
|
|
9068
|
-
this.ws.onopen = () => {
|
|
9069
|
-
this.ws?.send(JSON.stringify({
|
|
9070
|
-
type: "auth",
|
|
9071
|
-
token: authToken,
|
|
9072
|
-
tenantId: config.tenant.tenantId,
|
|
9073
|
-
systemPrompt: config.systemPrompt
|
|
9074
|
-
}));
|
|
9075
|
-
};
|
|
9076
|
-
this.ws.onmessage = (event) => {
|
|
9077
|
-
try {
|
|
9078
|
-
this.handleAgentCoreMessage(JSON.parse(event.data));
|
|
9079
|
-
} catch {
|
|
9080
|
-
}
|
|
9081
|
-
};
|
|
9082
|
-
this.ws.onerror = () => {
|
|
9083
|
-
reject(new Error("WebSocket connection failed"));
|
|
9084
|
-
};
|
|
9085
|
-
this.ws.onclose = (event) => {
|
|
9086
|
-
this.handleDisconnect(event);
|
|
9087
|
-
};
|
|
9088
|
-
const authTimeout = setTimeout(() => {
|
|
9089
|
-
reject(new Error("Auth timeout"));
|
|
9090
|
-
}, 1e4);
|
|
9091
|
-
const authHandler = (event) => {
|
|
9092
|
-
try {
|
|
9093
|
-
const data = JSON.parse(event.data);
|
|
9094
|
-
if (data.type === "auth_success") {
|
|
9095
|
-
clearTimeout(authTimeout);
|
|
9096
|
-
this.ws?.removeEventListener("message", authHandler);
|
|
9097
|
-
resolve();
|
|
9098
|
-
} else if (data.type === "auth_failed") {
|
|
9099
|
-
clearTimeout(authTimeout);
|
|
9100
|
-
reject(new Error(data.message));
|
|
9101
|
-
}
|
|
9102
|
-
} catch {
|
|
9103
|
-
}
|
|
9104
|
-
};
|
|
9105
|
-
this.ws.addEventListener("message", authHandler);
|
|
9106
|
-
});
|
|
9107
|
-
}
|
|
9108
|
-
handleAgentCoreMessage(data) {
|
|
9109
|
-
switch (data.type) {
|
|
9110
|
-
case "response_start":
|
|
9111
|
-
this.setState("speaking");
|
|
9112
|
-
this.isSpeaking = true;
|
|
9113
|
-
this.emit("ai.response.start", {
|
|
9114
|
-
text: data.text,
|
|
9115
|
-
emotion: data.emotion
|
|
9116
|
-
});
|
|
9117
|
-
if (data.emotion) {
|
|
9118
|
-
this.emotionController.transitionTo(
|
|
9119
|
-
{ [data.emotion]: 0.7 },
|
|
9120
|
-
300
|
|
9121
|
-
);
|
|
9122
|
-
}
|
|
9123
|
-
if (this.pipeline) {
|
|
9124
|
-
this.pipeline.start();
|
|
9125
|
-
}
|
|
9126
|
-
break;
|
|
9127
|
-
case "response_chunk":
|
|
9128
|
-
this.emit("ai.response.chunk", {
|
|
9129
|
-
text: data.text,
|
|
9130
|
-
isLast: data.isLast
|
|
9131
|
-
});
|
|
9132
|
-
break;
|
|
9133
|
-
case "audio_chunk":
|
|
9134
|
-
if (data.audio && this.pipeline) {
|
|
9135
|
-
const audioData = this.base64ToArrayBuffer(data.audio);
|
|
9136
|
-
const uint8 = new Uint8Array(audioData);
|
|
9137
|
-
this.pipeline.onAudioChunk(uint8).catch((error) => {
|
|
9138
|
-
console.error("[AgentCore] Pipeline chunk error:", error);
|
|
9139
|
-
});
|
|
9140
|
-
}
|
|
9141
|
-
break;
|
|
9142
|
-
case "audio_end":
|
|
9143
|
-
if (this.pipeline) {
|
|
9144
|
-
this.pipeline.end().catch((error) => {
|
|
9145
|
-
console.error("[AgentCore] Pipeline end error:", error);
|
|
9146
|
-
});
|
|
9147
|
-
}
|
|
9148
|
-
break;
|
|
9149
|
-
case "response_end":
|
|
9150
|
-
this.addToHistory({
|
|
9151
|
-
role: "assistant",
|
|
9152
|
-
content: data.fullText,
|
|
9153
|
-
timestamp: Date.now(),
|
|
9154
|
-
emotion: data.emotion
|
|
9155
|
-
});
|
|
9156
|
-
this.emit("ai.response.end", {
|
|
9157
|
-
fullText: data.fullText,
|
|
9158
|
-
durationMs: data.durationMs || 0
|
|
9159
|
-
});
|
|
9160
|
-
break;
|
|
9161
|
-
case "memory_updated":
|
|
9162
|
-
this.emit("memory.updated", {
|
|
9163
|
-
messageCount: data.messageCount,
|
|
9164
|
-
tokenCount: data.tokenCount
|
|
9165
|
-
});
|
|
9166
|
-
break;
|
|
9167
|
-
case "error":
|
|
9168
|
-
this.emit("connection.error", {
|
|
9169
|
-
error: new Error(data.message),
|
|
9170
|
-
recoverable: data.recoverable ?? false
|
|
9171
|
-
});
|
|
9172
|
-
break;
|
|
9173
|
-
}
|
|
9174
|
-
}
|
|
9175
|
-
scheduleTranscription() {
|
|
9176
|
-
if (this.audioBuffer.length === 0) return;
|
|
9177
|
-
const totalLength = this.audioBuffer.reduce((sum2, buf) => sum2 + buf.length, 0);
|
|
9178
|
-
if (totalLength < 4e3) return;
|
|
9179
|
-
const audio = new Float32Array(totalLength);
|
|
9180
|
-
let offset = 0;
|
|
9181
|
-
for (const buf of this.audioBuffer) {
|
|
9182
|
-
audio.set(buf, offset);
|
|
9183
|
-
offset += buf.length;
|
|
9184
|
-
}
|
|
9185
|
-
this.audioBuffer = [];
|
|
9186
|
-
let sum = 0;
|
|
9187
|
-
for (let i = 0; i < audio.length; i++) {
|
|
9188
|
-
sum += audio[i] * audio[i];
|
|
9189
|
-
}
|
|
9190
|
-
const rms = Math.sqrt(sum / audio.length);
|
|
9191
|
-
if (rms < 0.01) {
|
|
9192
|
-
console.debug("[AgentCore] Skipping silent audio", { rms, samples: audio.length });
|
|
9193
|
-
return;
|
|
9194
|
-
}
|
|
9195
|
-
if (this.asr) {
|
|
9196
|
-
this.setState("listening");
|
|
9197
|
-
this.emit("user.speech.start", { timestamp: Date.now() });
|
|
9198
|
-
this.asr.transcribe(audio).then((result) => {
|
|
9199
|
-
this.emit("user.transcript.final", {
|
|
9200
|
-
text: result.text,
|
|
9201
|
-
confidence: 1
|
|
9202
|
-
});
|
|
9203
|
-
this.emit("user.speech.end", { timestamp: Date.now(), durationMs: result.inferenceTimeMs });
|
|
9204
|
-
const cleanText = result.text.trim();
|
|
9205
|
-
if (cleanText) {
|
|
9206
|
-
this.sendText(cleanText).catch((error) => {
|
|
9207
|
-
console.error("[AgentCore] Send text error:", error);
|
|
9208
|
-
});
|
|
9209
|
-
}
|
|
9210
|
-
}).catch((error) => {
|
|
9211
|
-
console.error("[AgentCore] Transcription error:", error);
|
|
9212
|
-
});
|
|
9213
|
-
}
|
|
9214
|
-
}
|
|
9215
|
-
// REMOVED: processAudioForAnimation() - now handled by FullFacePipeline
|
|
9216
|
-
// The pipeline manages audio scheduling, LAM inference, and frame synchronization
|
|
9217
|
-
// Frames are emitted via pipeline.on('full_frame_ready') event (see initPipeline())
|
|
9218
|
-
/**
|
|
9219
|
-
* Detect voice activity using Silero VAD
|
|
9220
|
-
* Falls back to simple RMS if VAD not available
|
|
9221
|
-
*/
|
|
9222
|
-
async detectVoiceActivity(audio) {
|
|
9223
|
-
const float32 = audio instanceof Float32Array ? audio : int16ToFloat32(audio);
|
|
9224
|
-
if (this.vad) {
|
|
9225
|
-
const chunkSize = this.vad.getChunkSize();
|
|
9226
|
-
for (let i = 0; i + chunkSize <= float32.length; i += chunkSize) {
|
|
9227
|
-
const chunk = float32.slice(i, i + chunkSize);
|
|
9228
|
-
const result = await this.vad.process(chunk);
|
|
9229
|
-
if (result.isSpeech) {
|
|
9230
|
-
return true;
|
|
9231
|
-
}
|
|
9232
|
-
}
|
|
9233
|
-
return false;
|
|
9234
|
-
}
|
|
9235
|
-
let sum = 0;
|
|
9236
|
-
for (let i = 0; i < float32.length; i++) {
|
|
9237
|
-
sum += float32[i] * float32[i];
|
|
9238
|
-
}
|
|
9239
|
-
const rms = Math.sqrt(sum / float32.length);
|
|
9240
|
-
return rms > 0.02;
|
|
9241
|
-
}
|
|
9242
|
-
base64ToArrayBuffer(base64) {
|
|
9243
|
-
const binaryString = atob(base64);
|
|
9244
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
9245
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
9246
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
9247
|
-
}
|
|
9248
|
-
return bytes.buffer;
|
|
9249
|
-
}
|
|
9250
|
-
addToHistory(message) {
|
|
9251
|
-
this.history.push(message);
|
|
9252
|
-
this.emit("memory.updated", { messageCount: this.history.length });
|
|
9253
|
-
}
|
|
9254
|
-
handleDisconnect(event) {
|
|
9255
|
-
this._isConnected = false;
|
|
9256
|
-
if (event.code !== 1e3) {
|
|
9257
|
-
if (this.wsReconnectAttempts < this.maxReconnectAttempts) {
|
|
9258
|
-
this.wsReconnectAttempts++;
|
|
9259
|
-
setTimeout(() => {
|
|
9260
|
-
if (this.currentConfig) {
|
|
9261
|
-
this.connect(this.currentConfig).catch(() => {
|
|
9262
|
-
});
|
|
9263
|
-
}
|
|
9264
|
-
}, Math.pow(2, this.wsReconnectAttempts) * 1e3);
|
|
9265
|
-
} else {
|
|
9266
|
-
this.setState("error");
|
|
9267
|
-
this.emit("connection.error", {
|
|
9268
|
-
error: new Error("Max reconnection attempts reached"),
|
|
9269
|
-
recoverable: false
|
|
9270
|
-
});
|
|
9271
|
-
}
|
|
9272
|
-
}
|
|
9273
|
-
this.emit("connection.closed", { reason: event.reason || "Connection closed" });
|
|
9274
|
-
}
|
|
9275
|
-
};
|
|
9276
|
-
|
|
9277
|
-
// src/ai/orchestration/ConversationOrchestrator.ts
|
|
9278
|
-
var ConversationSessionImpl = class {
|
|
9279
|
-
constructor(config, adapter) {
|
|
9280
|
-
this._history = [];
|
|
9281
|
-
this._context = /* @__PURE__ */ new Map();
|
|
9282
|
-
this.sessionId = config.sessionId;
|
|
9283
|
-
this._config = config;
|
|
9284
|
-
this._adapter = adapter;
|
|
9285
|
-
this.createdAt = Date.now();
|
|
9286
|
-
this._lastActivityAt = Date.now();
|
|
9287
|
-
this._emotionController = new EmotionController();
|
|
9288
|
-
if (config.emotion) {
|
|
9289
|
-
this._emotionController.setPreset(config.emotion);
|
|
9290
|
-
}
|
|
9291
|
-
}
|
|
9292
|
-
get adapter() {
|
|
9293
|
-
return this._adapter;
|
|
9294
|
-
}
|
|
9295
|
-
get config() {
|
|
9296
|
-
return this._config;
|
|
9297
|
-
}
|
|
9298
|
-
get state() {
|
|
9299
|
-
return this._adapter.state;
|
|
9300
|
-
}
|
|
9301
|
-
get history() {
|
|
9302
|
-
return [...this._history];
|
|
9303
|
-
}
|
|
9304
|
-
get emotion() {
|
|
9305
|
-
return {};
|
|
9306
|
-
}
|
|
9307
|
-
get lastActivityAt() {
|
|
9308
|
-
return this._lastActivityAt;
|
|
9309
|
-
}
|
|
9310
|
-
async start() {
|
|
9311
|
-
await this._adapter.connect(this._config);
|
|
9312
|
-
this._lastActivityAt = Date.now();
|
|
9313
|
-
}
|
|
9314
|
-
async end() {
|
|
9315
|
-
await this._adapter.disconnect();
|
|
9316
|
-
}
|
|
9317
|
-
pushAudio(audio) {
|
|
9318
|
-
this._adapter.pushAudio(audio);
|
|
9319
|
-
this._lastActivityAt = Date.now();
|
|
9320
|
-
}
|
|
9321
|
-
async sendText(text) {
|
|
9322
|
-
await this._adapter.sendText(text);
|
|
9323
|
-
this._lastActivityAt = Date.now();
|
|
9324
|
-
}
|
|
9325
|
-
interrupt() {
|
|
9326
|
-
this._adapter.interrupt();
|
|
9327
|
-
this._lastActivityAt = Date.now();
|
|
9328
|
-
}
|
|
9329
|
-
setEmotion(emotion) {
|
|
9330
|
-
this._emotionController.set(emotion);
|
|
9331
|
-
}
|
|
9332
|
-
addContext(key, value) {
|
|
9333
|
-
this._context.set(key, value);
|
|
9334
|
-
}
|
|
9335
|
-
removeContext(key) {
|
|
9336
|
-
this._context.delete(key);
|
|
9337
|
-
}
|
|
9338
|
-
getContext() {
|
|
9339
|
-
return Object.fromEntries(this._context);
|
|
9340
|
-
}
|
|
9341
|
-
export() {
|
|
9342
|
-
return {
|
|
9343
|
-
sessionId: this.sessionId,
|
|
9344
|
-
tenantId: this._config.tenant.tenantId,
|
|
9345
|
-
characterId: this._config.tenant.characterId,
|
|
9346
|
-
history: this._history,
|
|
9347
|
-
context: Object.fromEntries(this._context),
|
|
9348
|
-
emotion: this.emotion,
|
|
9349
|
-
createdAt: this.createdAt,
|
|
9350
|
-
lastActivityAt: this._lastActivityAt
|
|
9351
|
-
};
|
|
9352
|
-
}
|
|
9353
|
-
import(snapshot) {
|
|
9354
|
-
this._history = [...snapshot.history];
|
|
9355
|
-
this._context = new Map(Object.entries(snapshot.context));
|
|
9356
|
-
this._lastActivityAt = snapshot.lastActivityAt;
|
|
9357
|
-
}
|
|
9358
|
-
syncHistory() {
|
|
9359
|
-
this._history = this._adapter.getHistory();
|
|
9360
|
-
}
|
|
9361
|
-
};
|
|
9362
|
-
var ConversationOrchestrator = class extends EventEmitter {
|
|
9363
|
-
constructor(config) {
|
|
9364
|
-
super();
|
|
9365
|
-
// Sessions per tenant
|
|
9366
|
-
this.sessions = /* @__PURE__ */ new Map();
|
|
9367
|
-
// Tenant configurations
|
|
9368
|
-
this.tenants = /* @__PURE__ */ new Map();
|
|
9369
|
-
// Health monitoring
|
|
9370
|
-
this.healthCheckInterval = null;
|
|
9371
|
-
this.HEALTH_CHECK_INTERVAL_MS = 3e4;
|
|
9372
|
-
this.config = {
|
|
9373
|
-
connectionTimeoutMs: 5e3,
|
|
9374
|
-
maxRetries: 3,
|
|
9375
|
-
...config
|
|
9376
|
-
};
|
|
9377
|
-
this.adapter = new AgentCoreAdapter(config.adapter);
|
|
9378
|
-
}
|
|
9379
|
-
/**
|
|
9380
|
-
* Register a tenant
|
|
9381
|
-
*/
|
|
9382
|
-
registerTenant(tenant) {
|
|
9383
|
-
this.tenants.set(tenant.tenantId, tenant);
|
|
9384
|
-
}
|
|
9385
|
-
/**
|
|
9386
|
-
* Unregister a tenant
|
|
9387
|
-
*/
|
|
9388
|
-
unregisterTenant(tenantId) {
|
|
9389
|
-
this.tenants.delete(tenantId);
|
|
9390
|
-
}
|
|
9391
|
-
/**
|
|
9392
|
-
* Get tenant config
|
|
9393
|
-
*/
|
|
9394
|
-
getTenant(tenantId) {
|
|
9395
|
-
return this.tenants.get(tenantId);
|
|
9396
|
-
}
|
|
9397
|
-
/**
|
|
9398
|
-
* Create a new conversation session for a tenant
|
|
9399
|
-
*/
|
|
9400
|
-
async createSession(tenantId, options = {}) {
|
|
9401
|
-
const tenant = this.tenants.get(tenantId);
|
|
9402
|
-
if (!tenant) {
|
|
9403
|
-
throw new Error(`Tenant not found: ${tenantId}`);
|
|
9404
|
-
}
|
|
9405
|
-
const sessionId = options.sessionId || this.generateSessionId();
|
|
9406
|
-
const sessionConfig = {
|
|
9407
|
-
sessionId,
|
|
9408
|
-
tenant,
|
|
9409
|
-
systemPrompt: options.systemPrompt,
|
|
9410
|
-
voice: options.voice,
|
|
9411
|
-
emotion: options.emotion,
|
|
9412
|
-
language: options.language
|
|
9413
|
-
};
|
|
9414
|
-
const session = new ConversationSessionImpl(sessionConfig, this.adapter);
|
|
9415
|
-
this.sessions.set(sessionId, session);
|
|
9416
|
-
this.forwardAdapterEvents(this.adapter, sessionId);
|
|
9417
|
-
await session.start();
|
|
9418
|
-
this.emit("session.created", { sessionId, tenantId });
|
|
9419
|
-
return session;
|
|
9420
|
-
}
|
|
9421
|
-
/**
|
|
9422
|
-
* End a session
|
|
9423
|
-
*/
|
|
9424
|
-
async endSession(sessionId) {
|
|
9425
|
-
const session = this.sessions.get(sessionId);
|
|
9426
|
-
if (session) {
|
|
9427
|
-
await session.end();
|
|
9428
|
-
this.sessions.delete(sessionId);
|
|
9429
|
-
this.emit("session.ended", { sessionId, reason: "Client requested" });
|
|
9430
|
-
}
|
|
9431
|
-
}
|
|
9432
|
-
/**
|
|
9433
|
-
* Get session by ID
|
|
9434
|
-
*/
|
|
9435
|
-
getSession(sessionId) {
|
|
9436
|
-
return this.sessions.get(sessionId);
|
|
9437
|
-
}
|
|
9438
|
-
/**
|
|
9439
|
-
* Get all sessions for a tenant
|
|
9440
|
-
*/
|
|
9441
|
-
getTenantSessions(tenantId) {
|
|
9442
|
-
return Array.from(this.sessions.values()).filter((s) => s.config.tenant.tenantId === tenantId);
|
|
9443
|
-
}
|
|
9444
|
-
/**
|
|
9445
|
-
* Start health monitoring
|
|
9446
|
-
*/
|
|
9447
|
-
startHealthMonitoring() {
|
|
9448
|
-
if (this.healthCheckInterval) return;
|
|
9449
|
-
this.healthCheckInterval = setInterval(async () => {
|
|
9450
|
-
await this.performHealthCheck();
|
|
9451
|
-
}, this.HEALTH_CHECK_INTERVAL_MS);
|
|
9452
|
-
}
|
|
9453
|
-
/**
|
|
9454
|
-
* Stop health monitoring
|
|
9455
|
-
*/
|
|
9456
|
-
stopHealthMonitoring() {
|
|
9457
|
-
if (this.healthCheckInterval) {
|
|
9458
|
-
clearInterval(this.healthCheckInterval);
|
|
9459
|
-
this.healthCheckInterval = null;
|
|
9460
|
-
}
|
|
9461
|
-
}
|
|
9462
|
-
/**
|
|
9463
|
-
* Dispose all resources
|
|
9464
|
-
*/
|
|
9465
|
-
async dispose() {
|
|
9466
|
-
this.stopHealthMonitoring();
|
|
9467
|
-
const endPromises = Array.from(this.sessions.values()).map((s) => s.end());
|
|
9468
|
-
await Promise.all(endPromises);
|
|
9469
|
-
this.sessions.clear();
|
|
9470
|
-
await this.adapter.disconnect();
|
|
9471
|
-
}
|
|
9472
|
-
// ==================== Private Methods ====================
|
|
9473
|
-
generateSessionId() {
|
|
9474
|
-
return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
9475
|
-
}
|
|
9476
|
-
forwardAdapterEvents(adapter, sessionId) {
|
|
9477
|
-
const events = [
|
|
9478
|
-
"state.change",
|
|
9479
|
-
"user.speech.start",
|
|
9480
|
-
"user.speech.end",
|
|
9481
|
-
"user.transcript.partial",
|
|
9482
|
-
"user.transcript.final",
|
|
9483
|
-
"ai.thinking.start",
|
|
9484
|
-
"ai.response.start",
|
|
9485
|
-
"ai.response.chunk",
|
|
9486
|
-
"ai.response.end",
|
|
9487
|
-
"audio.output.chunk",
|
|
9488
|
-
"audio.output.end",
|
|
9489
|
-
"animation",
|
|
9490
|
-
"memory.updated",
|
|
9491
|
-
"connection.error",
|
|
9492
|
-
"interruption.detected",
|
|
9493
|
-
"interruption.handled"
|
|
9494
|
-
];
|
|
9495
|
-
for (const event of events) {
|
|
9496
|
-
adapter.on(event, (data) => {
|
|
9497
|
-
const eventData = data;
|
|
9498
|
-
this.emit(event, { ...eventData, sessionId });
|
|
9499
|
-
});
|
|
9500
|
-
}
|
|
9501
|
-
}
|
|
9502
|
-
async performHealthCheck() {
|
|
9503
|
-
try {
|
|
9504
|
-
await this.adapter.healthCheck();
|
|
9505
|
-
} catch {
|
|
9506
|
-
}
|
|
9507
|
-
}
|
|
9508
|
-
};
|
|
9509
|
-
|
|
9510
|
-
// src/ai/tenancy/TenantManager.ts
|
|
9511
|
-
var _TenantManager = class _TenantManager {
|
|
9512
|
-
constructor() {
|
|
9513
|
-
this.tenants = /* @__PURE__ */ new Map();
|
|
9514
|
-
this.quotas = /* @__PURE__ */ new Map();
|
|
9515
|
-
this.usage = /* @__PURE__ */ new Map();
|
|
9516
|
-
this.tokenRefreshCallbacks = /* @__PURE__ */ new Map();
|
|
9517
|
-
}
|
|
9518
|
-
/**
|
|
9519
|
-
* Register a tenant with quota
|
|
9520
|
-
*/
|
|
9521
|
-
register(tenant, quota = _TenantManager.DEFAULT_QUOTA, tokenRefreshCallback) {
|
|
9522
|
-
this.tenants.set(tenant.tenantId, tenant);
|
|
9523
|
-
this.quotas.set(tenant.tenantId, quota);
|
|
9524
|
-
this.usage.set(tenant.tenantId, {
|
|
9525
|
-
currentSessions: 0,
|
|
9526
|
-
requestsThisMinute: 0,
|
|
9527
|
-
tokensUsed: 0,
|
|
9528
|
-
audioMinutesToday: 0,
|
|
9529
|
-
lastMinuteReset: Date.now(),
|
|
9530
|
-
lastDailyReset: Date.now()
|
|
9531
|
-
});
|
|
9532
|
-
if (tokenRefreshCallback) {
|
|
9533
|
-
this.tokenRefreshCallbacks.set(tenant.tenantId, tokenRefreshCallback);
|
|
9534
|
-
}
|
|
9535
|
-
}
|
|
9536
|
-
/**
|
|
9537
|
-
* Unregister a tenant
|
|
9538
|
-
*/
|
|
9539
|
-
unregister(tenantId) {
|
|
9540
|
-
this.tenants.delete(tenantId);
|
|
9541
|
-
this.quotas.delete(tenantId);
|
|
9542
|
-
this.usage.delete(tenantId);
|
|
9543
|
-
this.tokenRefreshCallbacks.delete(tenantId);
|
|
9544
|
-
}
|
|
9545
|
-
/**
|
|
9546
|
-
* Get tenant config
|
|
9547
|
-
*/
|
|
9548
|
-
get(tenantId) {
|
|
9549
|
-
return this.tenants.get(tenantId);
|
|
9550
|
-
}
|
|
9551
|
-
/**
|
|
9552
|
-
* Check if tenant exists
|
|
9553
|
-
*/
|
|
9554
|
-
has(tenantId) {
|
|
9555
|
-
return this.tenants.has(tenantId);
|
|
9556
|
-
}
|
|
9557
|
-
/**
|
|
9558
|
-
* Get all tenant IDs
|
|
9559
|
-
*/
|
|
9560
|
-
getTenantIds() {
|
|
9561
|
-
return Array.from(this.tenants.keys());
|
|
9562
|
-
}
|
|
9563
|
-
/**
|
|
9564
|
-
* Check if tenant can create new session
|
|
9565
|
-
*/
|
|
9566
|
-
canCreateSession(tenantId) {
|
|
9567
|
-
const quota = this.quotas.get(tenantId);
|
|
9568
|
-
const usage = this.usage.get(tenantId);
|
|
9569
|
-
if (!quota || !usage) return false;
|
|
9570
|
-
return usage.currentSessions < quota.maxSessions;
|
|
9571
|
-
}
|
|
9572
|
-
/**
|
|
9573
|
-
* Check if tenant can make request
|
|
9574
|
-
*/
|
|
9575
|
-
canMakeRequest(tenantId) {
|
|
9576
|
-
const quota = this.quotas.get(tenantId);
|
|
9577
|
-
const usage = this.usage.get(tenantId);
|
|
9578
|
-
if (!quota || !usage) return false;
|
|
9579
|
-
this.checkMinuteReset(tenantId);
|
|
9580
|
-
return usage.requestsThisMinute < quota.requestsPerMinute;
|
|
9581
|
-
}
|
|
9582
|
-
/**
|
|
9583
|
-
* Check if tenant can use audio
|
|
9584
|
-
*/
|
|
9585
|
-
canUseAudio(tenantId, minutes) {
|
|
9586
|
-
const quota = this.quotas.get(tenantId);
|
|
9587
|
-
const usage = this.usage.get(tenantId);
|
|
9588
|
-
if (!quota || !usage) return false;
|
|
9589
|
-
this.checkDailyReset(tenantId);
|
|
9590
|
-
return usage.audioMinutesToday + minutes <= quota.maxAudioMinutesPerDay;
|
|
9591
|
-
}
|
|
9592
|
-
/**
|
|
9593
|
-
* Increment session count
|
|
9594
|
-
*/
|
|
9595
|
-
incrementSessions(tenantId) {
|
|
9596
|
-
const usage = this.usage.get(tenantId);
|
|
9597
|
-
if (usage) {
|
|
9598
|
-
usage.currentSessions++;
|
|
9599
|
-
}
|
|
9600
|
-
}
|
|
9601
|
-
/**
|
|
9602
|
-
* Decrement session count
|
|
9603
|
-
*/
|
|
9604
|
-
decrementSessions(tenantId) {
|
|
9605
|
-
const usage = this.usage.get(tenantId);
|
|
9606
|
-
if (usage && usage.currentSessions > 0) {
|
|
9607
|
-
usage.currentSessions--;
|
|
9608
|
-
}
|
|
9609
|
-
}
|
|
9610
|
-
/**
|
|
9611
|
-
* Record a request
|
|
9612
|
-
*/
|
|
9613
|
-
recordRequest(tenantId) {
|
|
9614
|
-
const usage = this.usage.get(tenantId);
|
|
9615
|
-
if (usage) {
|
|
9616
|
-
this.checkMinuteReset(tenantId);
|
|
9617
|
-
usage.requestsThisMinute++;
|
|
9618
|
-
}
|
|
9619
|
-
}
|
|
9620
|
-
/**
|
|
9621
|
-
* Record token usage
|
|
9622
|
-
*/
|
|
9623
|
-
recordTokens(tenantId, tokens) {
|
|
9624
|
-
const usage = this.usage.get(tenantId);
|
|
9625
|
-
if (usage) {
|
|
9626
|
-
usage.tokensUsed += tokens;
|
|
9627
|
-
}
|
|
9628
|
-
}
|
|
9629
|
-
/**
|
|
9630
|
-
* Record audio usage
|
|
9631
|
-
*/
|
|
9632
|
-
recordAudioMinutes(tenantId, minutes) {
|
|
9633
|
-
const usage = this.usage.get(tenantId);
|
|
9634
|
-
if (usage) {
|
|
9635
|
-
this.checkDailyReset(tenantId);
|
|
9636
|
-
usage.audioMinutesToday += minutes;
|
|
9637
|
-
}
|
|
9638
|
-
}
|
|
9639
|
-
/**
|
|
9640
|
-
* Get fresh auth token for tenant
|
|
9641
|
-
*/
|
|
9642
|
-
async getAuthToken(tenantId) {
|
|
9643
|
-
const tenant = this.tenants.get(tenantId);
|
|
9644
|
-
if (!tenant) {
|
|
9645
|
-
throw new Error(`Tenant not found: ${tenantId}`);
|
|
9646
|
-
}
|
|
9647
|
-
const callback = this.tokenRefreshCallbacks.get(tenantId);
|
|
9648
|
-
if (callback) {
|
|
9649
|
-
const token = await callback();
|
|
9650
|
-
tenant.credentials.authToken = token;
|
|
9651
|
-
return token;
|
|
9652
|
-
}
|
|
9653
|
-
if (tenant.credentials.authToken) {
|
|
9654
|
-
return tenant.credentials.authToken;
|
|
9655
|
-
}
|
|
9656
|
-
throw new Error(`No auth token available for tenant: ${tenantId}`);
|
|
9657
|
-
}
|
|
9658
|
-
/**
|
|
9659
|
-
* Update tenant credentials
|
|
9660
|
-
*/
|
|
9661
|
-
updateCredentials(tenantId, credentials) {
|
|
9662
|
-
const tenant = this.tenants.get(tenantId);
|
|
9663
|
-
if (tenant) {
|
|
9664
|
-
tenant.credentials = { ...tenant.credentials, ...credentials };
|
|
9665
|
-
}
|
|
9666
|
-
}
|
|
9667
|
-
/**
|
|
9668
|
-
* Get usage stats for tenant
|
|
9669
|
-
*/
|
|
9670
|
-
getUsage(tenantId) {
|
|
9671
|
-
return this.usage.get(tenantId);
|
|
9672
|
-
}
|
|
9673
|
-
/**
|
|
9674
|
-
* Get quota for tenant
|
|
9675
|
-
*/
|
|
9676
|
-
getQuota(tenantId) {
|
|
9677
|
-
return this.quotas.get(tenantId);
|
|
9678
|
-
}
|
|
9679
|
-
/**
|
|
9680
|
-
* Update quota for tenant
|
|
9681
|
-
*/
|
|
9682
|
-
updateQuota(tenantId, quota) {
|
|
9683
|
-
const existing = this.quotas.get(tenantId);
|
|
9684
|
-
if (existing) {
|
|
9685
|
-
this.quotas.set(tenantId, { ...existing, ...quota });
|
|
9686
|
-
}
|
|
9687
|
-
}
|
|
9688
|
-
/**
|
|
9689
|
-
* Reset all usage stats for a tenant
|
|
9690
|
-
*/
|
|
9691
|
-
resetUsage(tenantId) {
|
|
9692
|
-
const usage = this.usage.get(tenantId);
|
|
9693
|
-
if (usage) {
|
|
9694
|
-
usage.requestsThisMinute = 0;
|
|
9695
|
-
usage.tokensUsed = 0;
|
|
9696
|
-
usage.audioMinutesToday = 0;
|
|
9697
|
-
usage.lastMinuteReset = Date.now();
|
|
9698
|
-
usage.lastDailyReset = Date.now();
|
|
9699
|
-
}
|
|
9700
|
-
}
|
|
9701
|
-
// ==================== Private Methods ====================
|
|
9702
|
-
checkMinuteReset(tenantId) {
|
|
9703
|
-
const usage = this.usage.get(tenantId);
|
|
9704
|
-
if (!usage) return;
|
|
9705
|
-
const now = Date.now();
|
|
9706
|
-
if (now - usage.lastMinuteReset >= 6e4) {
|
|
9707
|
-
usage.requestsThisMinute = 0;
|
|
9708
|
-
usage.lastMinuteReset = now;
|
|
9709
|
-
}
|
|
9710
|
-
}
|
|
9711
|
-
checkDailyReset(tenantId) {
|
|
9712
|
-
const usage = this.usage.get(tenantId);
|
|
9713
|
-
if (!usage) return;
|
|
9714
|
-
const now = Date.now();
|
|
9715
|
-
const MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
9716
|
-
if (now - usage.lastDailyReset >= MS_PER_DAY) {
|
|
9717
|
-
usage.audioMinutesToday = 0;
|
|
9718
|
-
usage.lastDailyReset = now;
|
|
9719
|
-
}
|
|
9720
|
-
}
|
|
9721
|
-
};
|
|
9722
|
-
/**
|
|
9723
|
-
* Default quota for new tenants
|
|
9724
|
-
*/
|
|
9725
|
-
_TenantManager.DEFAULT_QUOTA = {
|
|
9726
|
-
maxSessions: 10,
|
|
9727
|
-
requestsPerMinute: 60,
|
|
9728
|
-
maxTokensPerConversation: 1e5,
|
|
9729
|
-
maxAudioMinutesPerDay: 60
|
|
9730
|
-
};
|
|
9731
|
-
var TenantManager = _TenantManager;
|
|
9732
|
-
|
|
9733
|
-
// src/ai/utils/AudioSyncManager.ts
|
|
9734
|
-
var AudioSyncManager = class extends EventEmitter {
|
|
9735
|
-
constructor(config = {}) {
|
|
9736
|
-
super();
|
|
9737
|
-
this.bufferPosition = 0;
|
|
9738
|
-
this.playbackQueue = [];
|
|
9739
|
-
this.isPlaying = false;
|
|
9740
|
-
this.audioContext = null;
|
|
9741
|
-
this.playbackStartTime = 0;
|
|
9742
|
-
this.samplesPlayed = 0;
|
|
9743
|
-
this.config = {
|
|
9744
|
-
sampleRate: 16e3,
|
|
9745
|
-
bufferSize: 16640,
|
|
9746
|
-
overlapSize: 4160,
|
|
9747
|
-
maxDriftMs: 100,
|
|
9748
|
-
...config
|
|
9749
|
-
};
|
|
9750
|
-
this.audioBuffer = new Float32Array(this.config.bufferSize);
|
|
9751
|
-
}
|
|
9752
|
-
/**
|
|
9753
|
-
* Initialize audio context
|
|
9754
|
-
*/
|
|
9755
|
-
async initialize() {
|
|
9756
|
-
if (!this.audioContext) {
|
|
9757
|
-
this.audioContext = new AudioContext({ sampleRate: this.config.sampleRate });
|
|
9758
|
-
}
|
|
9759
|
-
if (this.audioContext.state === "suspended") {
|
|
9760
|
-
await this.audioContext.resume();
|
|
9761
|
-
}
|
|
9762
|
-
}
|
|
9763
|
-
/**
|
|
9764
|
-
* Push audio chunk for processing and playback
|
|
9765
|
-
*/
|
|
9766
|
-
pushAudio(audio) {
|
|
9767
|
-
this.playbackQueue.push(audio);
|
|
9768
|
-
this.bufferForInference(audio);
|
|
9769
|
-
if (!this.isPlaying && this.playbackQueue.length > 0) {
|
|
9770
|
-
this.startPlayback();
|
|
9771
|
-
}
|
|
9772
|
-
}
|
|
9773
|
-
/**
|
|
9774
|
-
* Buffer audio for inference
|
|
9775
|
-
*/
|
|
9776
|
-
bufferForInference(audio) {
|
|
9777
|
-
let offset = 0;
|
|
9778
|
-
while (offset < audio.length) {
|
|
9779
|
-
const remaining = this.config.bufferSize - this.bufferPosition;
|
|
9780
|
-
const toCopy = Math.min(remaining, audio.length - offset);
|
|
9781
|
-
this.audioBuffer.set(audio.subarray(offset, offset + toCopy), this.bufferPosition);
|
|
9782
|
-
this.bufferPosition += toCopy;
|
|
9783
|
-
offset += toCopy;
|
|
9784
|
-
if (this.bufferPosition >= this.config.bufferSize) {
|
|
9785
|
-
this.emit("buffer.ready", { audio: new Float32Array(this.audioBuffer) });
|
|
9786
|
-
const overlapStart = this.config.bufferSize - this.config.overlapSize;
|
|
9787
|
-
this.audioBuffer.copyWithin(0, overlapStart);
|
|
9788
|
-
this.bufferPosition = this.config.overlapSize;
|
|
9789
|
-
}
|
|
9790
|
-
}
|
|
9791
|
-
}
|
|
9792
|
-
/**
|
|
9793
|
-
* Start audio playback
|
|
9794
|
-
*/
|
|
9795
|
-
async startPlayback() {
|
|
9796
|
-
if (!this.audioContext || this.isPlaying) return;
|
|
9797
|
-
this.isPlaying = true;
|
|
9798
|
-
this.playbackStartTime = this.audioContext.currentTime;
|
|
9799
|
-
this.samplesPlayed = 0;
|
|
9800
|
-
this.emit("playback.start", {});
|
|
9801
|
-
await this.processPlaybackQueue();
|
|
9802
|
-
}
|
|
9803
|
-
/**
|
|
9804
|
-
* Process playback queue
|
|
9805
|
-
*/
|
|
9806
|
-
async processPlaybackQueue() {
|
|
9807
|
-
if (!this.audioContext) return;
|
|
9808
|
-
while (this.playbackQueue.length > 0) {
|
|
9809
|
-
const audio = this.playbackQueue.shift();
|
|
9810
|
-
const buffer = this.audioContext.createBuffer(1, audio.length, this.config.sampleRate);
|
|
9811
|
-
buffer.copyToChannel(audio, 0);
|
|
9812
|
-
const source = this.audioContext.createBufferSource();
|
|
9813
|
-
source.buffer = buffer;
|
|
9814
|
-
source.connect(this.audioContext.destination);
|
|
9815
|
-
const playTime = this.playbackStartTime + this.samplesPlayed / this.config.sampleRate;
|
|
9816
|
-
source.start(playTime);
|
|
9817
|
-
this.samplesPlayed += audio.length;
|
|
9818
|
-
this.checkDrift();
|
|
9819
|
-
await new Promise((resolve) => {
|
|
9820
|
-
source.onended = resolve;
|
|
9821
|
-
});
|
|
9822
|
-
}
|
|
9823
|
-
this.isPlaying = false;
|
|
9824
|
-
this.emit("playback.end", {});
|
|
9825
|
-
}
|
|
9826
|
-
/**
|
|
9827
|
-
* Check for audio/animation drift
|
|
9828
|
-
*/
|
|
9829
|
-
checkDrift() {
|
|
9830
|
-
if (!this.audioContext) return;
|
|
9831
|
-
const expectedTime = this.playbackStartTime + this.samplesPlayed / this.config.sampleRate;
|
|
9832
|
-
const actualTime = this.audioContext.currentTime;
|
|
9833
|
-
const driftMs = (actualTime - expectedTime) * 1e3;
|
|
9834
|
-
if (Math.abs(driftMs) > this.config.maxDriftMs) {
|
|
9835
|
-
this.emit("sync.drift", { driftMs });
|
|
9836
|
-
}
|
|
9837
|
-
}
|
|
9838
|
-
/**
|
|
9839
|
-
* Clear playback queue
|
|
9840
|
-
*/
|
|
9841
|
-
clearQueue() {
|
|
9842
|
-
this.playbackQueue = [];
|
|
9843
|
-
this.bufferPosition = 0;
|
|
9844
|
-
this.audioBuffer.fill(0);
|
|
9845
|
-
}
|
|
9846
|
-
/**
|
|
9847
|
-
* Stop playback
|
|
9848
|
-
*/
|
|
9849
|
-
stop() {
|
|
9850
|
-
this.clearQueue();
|
|
9851
|
-
this.isPlaying = false;
|
|
9852
|
-
}
|
|
9853
|
-
/**
|
|
9854
|
-
* Get current playback position in seconds
|
|
9855
|
-
*/
|
|
9856
|
-
getPlaybackPosition() {
|
|
9857
|
-
if (!this.audioContext) return 0;
|
|
9858
|
-
return this.audioContext.currentTime - this.playbackStartTime;
|
|
9859
|
-
}
|
|
9860
|
-
/**
|
|
9861
|
-
* Check if currently playing
|
|
9862
|
-
*/
|
|
9863
|
-
getIsPlaying() {
|
|
9864
|
-
return this.isPlaying;
|
|
9865
|
-
}
|
|
9866
|
-
/**
|
|
9867
|
-
* Dispose resources
|
|
9868
|
-
*/
|
|
9869
|
-
dispose() {
|
|
9870
|
-
this.stop();
|
|
9871
|
-
this.audioContext?.close();
|
|
9872
|
-
this.audioContext = null;
|
|
9873
|
-
}
|
|
9874
|
-
};
|
|
9875
|
-
|
|
9876
|
-
// src/ai/utils/InterruptionHandler.ts
|
|
9877
|
-
var InterruptionHandler = class extends EventEmitter {
|
|
9878
|
-
constructor(config = {}) {
|
|
9879
|
-
super();
|
|
9880
|
-
this.isSpeaking = false;
|
|
9881
|
-
this.speechStartTime = 0;
|
|
9882
|
-
this.lastSpeechTime = 0;
|
|
9883
|
-
this.silenceTimer = null;
|
|
9884
|
-
this.aiIsSpeaking = false;
|
|
9885
|
-
// Debouncing: only emit one interruption per speech session
|
|
9886
|
-
this.interruptionTriggeredThisSession = false;
|
|
9887
|
-
this.config = {
|
|
9888
|
-
vadThreshold: 0.5,
|
|
9889
|
-
// Silero VAD default
|
|
9890
|
-
minSpeechDurationMs: 200,
|
|
9891
|
-
// Google/Amazon barge-in standard
|
|
9892
|
-
silenceTimeoutMs: 500,
|
|
9893
|
-
// OpenAI Realtime API standard
|
|
9894
|
-
enabled: true,
|
|
9895
|
-
...config
|
|
9896
|
-
};
|
|
9897
|
-
}
|
|
9898
|
-
/**
|
|
9899
|
-
* Process VAD result for interruption detection
|
|
9900
|
-
* @param vadProbability - Speech probability from VAD (0-1)
|
|
9901
|
-
* @param audioEnergy - Optional RMS energy for logging (default: 0)
|
|
9902
|
-
*/
|
|
9903
|
-
processVADResult(vadProbability, audioEnergy = 0) {
|
|
9904
|
-
if (!this.config.enabled) return;
|
|
9905
|
-
if (vadProbability > this.config.vadThreshold) {
|
|
9906
|
-
this.onSpeechDetected(audioEnergy || vadProbability);
|
|
9907
|
-
} else {
|
|
9908
|
-
this.onSilenceDetected();
|
|
9909
|
-
}
|
|
9910
|
-
}
|
|
9911
|
-
/**
|
|
9912
|
-
* Notify that AI started speaking
|
|
9913
|
-
*/
|
|
9914
|
-
setAISpeaking(speaking) {
|
|
9915
|
-
this.aiIsSpeaking = speaking;
|
|
9916
|
-
}
|
|
9917
|
-
/**
|
|
9918
|
-
* Enable/disable interruption detection
|
|
9919
|
-
*/
|
|
9920
|
-
setEnabled(enabled) {
|
|
9921
|
-
this.config.enabled = enabled;
|
|
9922
|
-
if (!enabled) {
|
|
9923
|
-
this.reset();
|
|
9924
|
-
}
|
|
9925
|
-
}
|
|
9926
|
-
/**
|
|
9927
|
-
* Update configuration
|
|
9928
|
-
*/
|
|
9929
|
-
updateConfig(config) {
|
|
9930
|
-
this.config = { ...this.config, ...config };
|
|
9931
|
-
}
|
|
9932
|
-
/**
|
|
9933
|
-
* Reset state
|
|
9934
|
-
*/
|
|
9935
|
-
reset() {
|
|
9936
|
-
this.isSpeaking = false;
|
|
9937
|
-
this.speechStartTime = 0;
|
|
9938
|
-
this.lastSpeechTime = 0;
|
|
9939
|
-
this.interruptionTriggeredThisSession = false;
|
|
9940
|
-
if (this.silenceTimer) {
|
|
9941
|
-
clearTimeout(this.silenceTimer);
|
|
9942
|
-
this.silenceTimer = null;
|
|
9943
|
-
}
|
|
9944
|
-
}
|
|
9945
|
-
/**
|
|
9946
|
-
* Get current state
|
|
9947
|
-
*/
|
|
9948
|
-
getState() {
|
|
9949
|
-
return {
|
|
9950
|
-
isSpeaking: this.isSpeaking,
|
|
9951
|
-
speechDurationMs: this.isSpeaking ? Date.now() - this.speechStartTime : 0
|
|
9952
|
-
};
|
|
9953
|
-
}
|
|
9954
|
-
// ==================== Private Methods ====================
|
|
9955
|
-
onSpeechDetected(rms) {
|
|
9956
|
-
const now = Date.now();
|
|
9957
|
-
this.lastSpeechTime = now;
|
|
9958
|
-
if (this.silenceTimer) {
|
|
9959
|
-
clearTimeout(this.silenceTimer);
|
|
9960
|
-
this.silenceTimer = null;
|
|
9961
|
-
}
|
|
9962
|
-
if (!this.isSpeaking) {
|
|
9963
|
-
this.isSpeaking = true;
|
|
9964
|
-
this.speechStartTime = now;
|
|
9965
|
-
this.emit("speech.detected", { rms });
|
|
9966
|
-
}
|
|
9967
|
-
if (this.aiIsSpeaking && !this.interruptionTriggeredThisSession) {
|
|
9968
|
-
const speechDuration = now - this.speechStartTime;
|
|
9969
|
-
if (speechDuration >= this.config.minSpeechDurationMs) {
|
|
9970
|
-
this.interruptionTriggeredThisSession = true;
|
|
9971
|
-
this.emit("interruption.triggered", { rms, durationMs: speechDuration });
|
|
9972
|
-
}
|
|
9973
|
-
}
|
|
9974
|
-
}
|
|
9975
|
-
onSilenceDetected() {
|
|
9976
|
-
if (!this.isSpeaking) return;
|
|
9977
|
-
if (!this.silenceTimer) {
|
|
9978
|
-
this.silenceTimer = setTimeout(() => {
|
|
9979
|
-
const durationMs = this.lastSpeechTime - this.speechStartTime;
|
|
9980
|
-
this.isSpeaking = false;
|
|
9981
|
-
this.silenceTimer = null;
|
|
9982
|
-
this.interruptionTriggeredThisSession = false;
|
|
9983
|
-
this.emit("speech.ended", { durationMs });
|
|
9984
|
-
}, this.config.silenceTimeoutMs);
|
|
9985
|
-
}
|
|
9986
|
-
}
|
|
9987
|
-
};
|
|
9988
|
-
|
|
9989
8878
|
// src/animation/types.ts
|
|
9990
8879
|
var DEFAULT_ANIMATION_CONFIG = {
|
|
9991
8880
|
initialState: "idle",
|
|
@@ -11028,17 +9917,14 @@ export {
|
|
|
11028
9917
|
A2EOrchestrator,
|
|
11029
9918
|
A2EProcessor,
|
|
11030
9919
|
ARKIT_BLENDSHAPES,
|
|
11031
|
-
AgentCoreAdapter,
|
|
11032
9920
|
AnimationGraph,
|
|
11033
9921
|
AudioChunkCoalescer,
|
|
11034
9922
|
AudioEnergyAnalyzer,
|
|
11035
9923
|
AudioScheduler,
|
|
11036
|
-
AudioSyncManager,
|
|
11037
9924
|
BLENDSHAPE_TO_GROUP,
|
|
11038
9925
|
BlendshapeSmoother,
|
|
11039
9926
|
CTC_VOCAB,
|
|
11040
9927
|
ConsoleExporter,
|
|
11041
|
-
ConversationOrchestrator,
|
|
11042
9928
|
DEFAULT_ANIMATION_CONFIG,
|
|
11043
9929
|
DEFAULT_LOGGING_CONFIG,
|
|
11044
9930
|
EMOTION_NAMES,
|
|
@@ -11068,7 +9954,6 @@ export {
|
|
|
11068
9954
|
SileroVADInference,
|
|
11069
9955
|
SileroVADUnifiedAdapter,
|
|
11070
9956
|
SileroVADWorker,
|
|
11071
|
-
TenantManager,
|
|
11072
9957
|
UnifiedInferenceWorker,
|
|
11073
9958
|
Wav2ArkitCpuInference,
|
|
11074
9959
|
Wav2ArkitCpuUnifiedAdapter,
|