@omote/core 0.5.3 → 0.5.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.js CHANGED
@@ -33,17 +33,14 @@ __export(index_exports, {
33
33
  A2EOrchestrator: () => A2EOrchestrator,
34
34
  A2EProcessor: () => A2EProcessor,
35
35
  ARKIT_BLENDSHAPES: () => ARKIT_BLENDSHAPES,
36
- AgentCoreAdapter: () => AgentCoreAdapter,
37
36
  AnimationGraph: () => AnimationGraph,
38
37
  AudioChunkCoalescer: () => AudioChunkCoalescer,
39
38
  AudioEnergyAnalyzer: () => AudioEnergyAnalyzer,
40
39
  AudioScheduler: () => AudioScheduler,
41
- AudioSyncManager: () => AudioSyncManager,
42
40
  BLENDSHAPE_TO_GROUP: () => BLENDSHAPE_TO_GROUP,
43
41
  BlendshapeSmoother: () => BlendshapeSmoother,
44
42
  CTC_VOCAB: () => CTC_VOCAB,
45
43
  ConsoleExporter: () => ConsoleExporter,
46
- ConversationOrchestrator: () => ConversationOrchestrator,
47
44
  DEFAULT_ANIMATION_CONFIG: () => DEFAULT_ANIMATION_CONFIG,
48
45
  DEFAULT_LOGGING_CONFIG: () => DEFAULT_LOGGING_CONFIG,
49
46
  EMOTION_NAMES: () => EMOTION_NAMES,
@@ -73,7 +70,6 @@ __export(index_exports, {
73
70
  SileroVADInference: () => SileroVADInference,
74
71
  SileroVADUnifiedAdapter: () => SileroVADUnifiedAdapter,
75
72
  SileroVADWorker: () => SileroVADWorker,
76
- TenantManager: () => TenantManager,
77
73
  UnifiedInferenceWorker: () => UnifiedInferenceWorker,
78
74
  Wav2ArkitCpuInference: () => Wav2ArkitCpuInference,
79
75
  Wav2ArkitCpuUnifiedAdapter: () => Wav2ArkitCpuUnifiedAdapter,
@@ -3188,13 +3184,6 @@ function pcm16ToFloat32(buffer) {
3188
3184
  }
3189
3185
  return float32;
3190
3186
  }
3191
- function int16ToFloat32(int16) {
3192
- const float32 = new Float32Array(int16.length);
3193
- for (let i = 0; i < int16.length; i++) {
3194
- float32[i] = int16[i] / 32768;
3195
- }
3196
- return float32;
3197
- }
3198
3187
 
3199
3188
  // src/audio/FullFacePipeline.ts
3200
3189
  var logger4 = createLogger("FullFacePipeline");
@@ -3482,6 +3471,108 @@ var FullFacePipeline = class extends EventEmitter {
3482
3471
  }
3483
3472
  };
3484
3473
 
3474
+ // src/audio/InterruptionHandler.ts
3475
+ var InterruptionHandler = class extends EventEmitter {
3476
+ constructor(config = {}) {
3477
+ super();
3478
+ this.isSpeaking = false;
3479
+ this.speechStartTime = 0;
3480
+ this.lastSpeechTime = 0;
3481
+ this.silenceTimer = null;
3482
+ this.aiIsSpeaking = false;
3483
+ // Debouncing: only emit one interruption per speech session
3484
+ this.interruptionTriggeredThisSession = false;
3485
+ this.config = {
3486
+ vadThreshold: 0.5,
3487
+ // Silero VAD default
3488
+ minSpeechDurationMs: 200,
3489
+ // Google/Amazon barge-in standard
3490
+ silenceTimeoutMs: 500,
3491
+ // OpenAI Realtime API standard
3492
+ enabled: true,
3493
+ ...config
3494
+ };
3495
+ }
3496
+ /**
3497
+ * Process VAD result for interruption detection
3498
+ * @param vadProbability - Speech probability from VAD (0-1)
3499
+ * @param audioEnergy - Optional RMS energy for logging (default: 0)
3500
+ */
3501
+ processVADResult(vadProbability, audioEnergy = 0) {
3502
+ if (!this.config.enabled) return;
3503
+ if (vadProbability > this.config.vadThreshold) {
3504
+ this.onSpeechDetected(audioEnergy || vadProbability);
3505
+ } else {
3506
+ this.onSilenceDetected();
3507
+ }
3508
+ }
3509
+ /** Notify that AI started/stopped speaking */
3510
+ setAISpeaking(speaking) {
3511
+ this.aiIsSpeaking = speaking;
3512
+ }
3513
+ /** Enable/disable interruption detection */
3514
+ setEnabled(enabled) {
3515
+ this.config.enabled = enabled;
3516
+ if (!enabled) {
3517
+ this.reset();
3518
+ }
3519
+ }
3520
+ /** Update configuration */
3521
+ updateConfig(config) {
3522
+ this.config = { ...this.config, ...config };
3523
+ }
3524
+ /** Reset state */
3525
+ reset() {
3526
+ this.isSpeaking = false;
3527
+ this.speechStartTime = 0;
3528
+ this.lastSpeechTime = 0;
3529
+ this.interruptionTriggeredThisSession = false;
3530
+ if (this.silenceTimer) {
3531
+ clearTimeout(this.silenceTimer);
3532
+ this.silenceTimer = null;
3533
+ }
3534
+ }
3535
+ /** Get current state */
3536
+ getState() {
3537
+ return {
3538
+ isSpeaking: this.isSpeaking,
3539
+ speechDurationMs: this.isSpeaking ? Date.now() - this.speechStartTime : 0
3540
+ };
3541
+ }
3542
+ onSpeechDetected(rms) {
3543
+ const now = Date.now();
3544
+ this.lastSpeechTime = now;
3545
+ if (this.silenceTimer) {
3546
+ clearTimeout(this.silenceTimer);
3547
+ this.silenceTimer = null;
3548
+ }
3549
+ if (!this.isSpeaking) {
3550
+ this.isSpeaking = true;
3551
+ this.speechStartTime = now;
3552
+ this.emit("speech.detected", { rms });
3553
+ }
3554
+ if (this.aiIsSpeaking && !this.interruptionTriggeredThisSession) {
3555
+ const speechDuration = now - this.speechStartTime;
3556
+ if (speechDuration >= this.config.minSpeechDurationMs) {
3557
+ this.interruptionTriggeredThisSession = true;
3558
+ this.emit("interruption.triggered", { rms, durationMs: speechDuration });
3559
+ }
3560
+ }
3561
+ }
3562
+ onSilenceDetected() {
3563
+ if (!this.isSpeaking) return;
3564
+ if (!this.silenceTimer) {
3565
+ this.silenceTimer = setTimeout(() => {
3566
+ const durationMs = this.lastSpeechTime - this.speechStartTime;
3567
+ this.isSpeaking = false;
3568
+ this.silenceTimer = null;
3569
+ this.interruptionTriggeredThisSession = false;
3570
+ this.emit("speech.ended", { durationMs });
3571
+ }, this.config.silenceTimeoutMs);
3572
+ }
3573
+ }
3574
+ };
3575
+
3485
3576
  // src/inference/kaldiFbank.ts
3486
3577
  function fft(re, im) {
3487
3578
  const n = re.length;
@@ -9188,1214 +9279,6 @@ var EmotionController = class {
9188
9279
  }
9189
9280
  };
9190
9281
 
9191
- // src/ai/adapters/AgentCoreAdapter.ts
9192
- var AgentCoreAdapter = class extends EventEmitter {
9193
- constructor(config) {
9194
- super();
9195
- this.name = "AgentCore";
9196
- this._state = "disconnected";
9197
- this._sessionId = null;
9198
- this._isConnected = false;
9199
- // Sub-components
9200
- this.asr = null;
9201
- this.vad = null;
9202
- this.lam = null;
9203
- this.pipeline = null;
9204
- // WebSocket connection to AgentCore
9205
- this.ws = null;
9206
- this.wsReconnectAttempts = 0;
9207
- this.maxReconnectAttempts = 5;
9208
- // Audio buffers
9209
- this.audioBuffer = [];
9210
- // Conversation state
9211
- this.history = [];
9212
- this.currentConfig = null;
9213
- // Interruption handling
9214
- this.isSpeaking = false;
9215
- this.currentTtsAbortController = null;
9216
- // Auth token cache per tenant
9217
- this.tokenCache = /* @__PURE__ */ new Map();
9218
- this.agentCoreConfig = config;
9219
- this.emotionController = new EmotionController();
9220
- }
9221
- get state() {
9222
- return this._state;
9223
- }
9224
- get sessionId() {
9225
- return this._sessionId;
9226
- }
9227
- get isConnected() {
9228
- return this._isConnected;
9229
- }
9230
- /**
9231
- * Connect to AgentCore with session configuration
9232
- */
9233
- async connect(config) {
9234
- this.currentConfig = config;
9235
- this._sessionId = config.sessionId;
9236
- try {
9237
- const authToken = await this.getAuthToken(config.tenant);
9238
- await Promise.all([
9239
- this.initASR(),
9240
- this.initLAM()
9241
- ]);
9242
- await this.connectWebSocket(authToken, config);
9243
- this._isConnected = true;
9244
- this.setState("idle");
9245
- this.emit("connection.opened", { sessionId: this._sessionId, adapter: this.name });
9246
- } catch (error) {
9247
- this.setState("error");
9248
- this.emit("connection.error", {
9249
- error,
9250
- recoverable: true
9251
- });
9252
- throw error;
9253
- }
9254
- }
9255
- /**
9256
- * Disconnect and cleanup
9257
- */
9258
- async disconnect() {
9259
- this.currentTtsAbortController?.abort();
9260
- if (this.pipeline) {
9261
- this.pipeline.dispose();
9262
- this.pipeline = null;
9263
- }
9264
- if (this.ws) {
9265
- this.ws.close(1e3, "Client disconnect");
9266
- this.ws = null;
9267
- }
9268
- await Promise.all([
9269
- this.asr?.dispose(),
9270
- this.vad?.dispose(),
9271
- this.lam?.dispose()
9272
- ]);
9273
- this._isConnected = false;
9274
- this.setState("disconnected");
9275
- this.emit("connection.closed", { reason: "Client disconnect" });
9276
- }
9277
- /**
9278
- * Push user audio for processing
9279
- */
9280
- pushAudio(audio) {
9281
- if (!this._isConnected) return;
9282
- if (this.isSpeaking) {
9283
- this.detectVoiceActivity(audio).then((hasVoiceActivity) => {
9284
- if (hasVoiceActivity) {
9285
- this.interrupt();
9286
- }
9287
- }).catch((error) => {
9288
- console.error("[AgentCore] VAD error during interruption detection:", error);
9289
- });
9290
- }
9291
- const float32 = audio instanceof Float32Array ? audio : int16ToFloat32(audio);
9292
- this.audioBuffer.push(float32);
9293
- this.scheduleTranscription();
9294
- }
9295
- /**
9296
- * Send text directly to AgentCore
9297
- */
9298
- async sendText(text) {
9299
- if (!this._isConnected || !this.ws) {
9300
- throw new Error("Not connected to AgentCore");
9301
- }
9302
- this.addToHistory({
9303
- role: "user",
9304
- content: text,
9305
- timestamp: Date.now()
9306
- });
9307
- this.setState("thinking");
9308
- this.emit("ai.thinking.start", { timestamp: Date.now() });
9309
- this.ws.send(JSON.stringify({
9310
- type: "user_message",
9311
- sessionId: this._sessionId,
9312
- content: text,
9313
- context: {
9314
- history: this.history.slice(-10),
9315
- // Last 10 messages
9316
- emotion: Array.from(this.emotionController.emotion)
9317
- }
9318
- }));
9319
- }
9320
- /**
9321
- * Interrupt current AI response
9322
- */
9323
- interrupt() {
9324
- if (!this.isSpeaking) return;
9325
- this.emit("interruption.detected", { timestamp: Date.now() });
9326
- this.currentTtsAbortController?.abort();
9327
- this.currentTtsAbortController = null;
9328
- if (this.ws?.readyState === WebSocket.OPEN) {
9329
- this.ws.send(JSON.stringify({
9330
- type: "interrupt",
9331
- sessionId: this._sessionId,
9332
- timestamp: Date.now()
9333
- }));
9334
- }
9335
- this.isSpeaking = false;
9336
- this.setState("listening");
9337
- this.emit("interruption.handled", { timestamp: Date.now(), action: "stop" });
9338
- }
9339
- getHistory() {
9340
- return [...this.history];
9341
- }
9342
- clearHistory() {
9343
- this.history = [];
9344
- this.emit("memory.updated", { messageCount: 0 });
9345
- }
9346
- async healthCheck() {
9347
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
9348
- return false;
9349
- }
9350
- return new Promise((resolve) => {
9351
- const timeout = setTimeout(() => resolve(false), 5e3);
9352
- const handler = (event) => {
9353
- try {
9354
- const data = JSON.parse(event.data);
9355
- if (data.type === "pong") {
9356
- clearTimeout(timeout);
9357
- this.ws?.removeEventListener("message", handler);
9358
- resolve(true);
9359
- }
9360
- } catch {
9361
- }
9362
- };
9363
- this.ws?.addEventListener("message", handler);
9364
- this.ws?.send(JSON.stringify({ type: "ping" }));
9365
- });
9366
- }
9367
- // ==================== Private Methods ====================
9368
- setState(state) {
9369
- const previousState = this._state;
9370
- this._state = state;
9371
- this.emit("state.change", { state, previousState });
9372
- }
9373
- async getAuthToken(tenant) {
9374
- const cached = this.tokenCache.get(tenant.tenantId);
9375
- if (cached && cached.expiresAt > Date.now() + 6e4) {
9376
- return cached.token;
9377
- }
9378
- if (tenant.credentials.authToken) {
9379
- return tenant.credentials.authToken;
9380
- }
9381
- const endpoint = this.agentCoreConfig.endpoint;
9382
- if (endpoint.startsWith("ws://") || endpoint.includes("localhost")) {
9383
- return "local-dev-token";
9384
- }
9385
- const httpEndpoint = endpoint.replace("wss://", "https://").replace("ws://", "http://");
9386
- const response = await fetch(`${httpEndpoint}/auth/token`, {
9387
- method: "POST",
9388
- headers: { "Content-Type": "application/json" },
9389
- body: JSON.stringify({
9390
- tenantId: tenant.tenantId,
9391
- apiKey: tenant.credentials.apiKey
9392
- })
9393
- });
9394
- if (!response.ok) {
9395
- throw new Error(`Auth failed: ${response.statusText}`);
9396
- }
9397
- const { token, expiresIn } = await response.json();
9398
- this.tokenCache.set(tenant.tenantId, {
9399
- token,
9400
- expiresAt: Date.now() + expiresIn * 1e3
9401
- });
9402
- return token;
9403
- }
9404
- async initASR() {
9405
- await Promise.all([
9406
- // SenseVoice ASR
9407
- (async () => {
9408
- this.asr = new SenseVoiceInference({
9409
- modelUrl: "/models/sensevoice/model.int8.onnx",
9410
- language: "auto"
9411
- });
9412
- await this.asr.load();
9413
- })(),
9414
- // Silero VAD for accurate voice activity detection
9415
- (async () => {
9416
- this.vad = new SileroVADInference({
9417
- modelUrl: "/models/silero-vad.onnx",
9418
- backend: "webgpu",
9419
- sampleRate: 16e3,
9420
- threshold: 0.5
9421
- });
9422
- await this.vad.load();
9423
- })()
9424
- ]);
9425
- }
9426
- async initLAM() {
9427
- const lamUrl = this.agentCoreConfig.models?.lamUrl || "/models/unified_wav2vec2_asr_a2e.onnx";
9428
- this.lam = new Wav2Vec2Inference({
9429
- modelUrl: lamUrl,
9430
- backend: "auto"
9431
- });
9432
- await this.lam.load();
9433
- await this.initPipeline();
9434
- }
9435
- async initPipeline() {
9436
- if (!this.lam) {
9437
- throw new Error("LAM must be initialized before pipeline");
9438
- }
9439
- this.pipeline = new FullFacePipeline({
9440
- lam: this.lam,
9441
- sampleRate: 16e3,
9442
- chunkTargetMs: 200
9443
- });
9444
- await this.pipeline.initialize();
9445
- this.pipeline.on("full_frame_ready", (fullFrame) => {
9446
- const frame = fullFrame.blendshapes;
9447
- this.emit("animation", {
9448
- blendshapes: frame,
9449
- get: (name) => {
9450
- const idx = LAM_BLENDSHAPES.indexOf(name);
9451
- return idx >= 0 ? frame[idx] : 0;
9452
- },
9453
- timestamp: Date.now(),
9454
- // Wall clock for client-side logging only
9455
- inferenceMs: 0
9456
- // Pipeline handles LAM inference asynchronously
9457
- });
9458
- });
9459
- this.pipeline.on("playback_complete", () => {
9460
- this.isSpeaking = false;
9461
- this.setState("idle");
9462
- this.emit("audio.output.end", { durationMs: 0 });
9463
- });
9464
- this.pipeline.on("error", (error) => {
9465
- console.error("[AgentCore] Pipeline error:", error);
9466
- this.emit("connection.error", {
9467
- error,
9468
- recoverable: true
9469
- });
9470
- });
9471
- }
9472
- async connectWebSocket(authToken, config) {
9473
- return new Promise((resolve, reject) => {
9474
- const wsUrl = new URL(`${this.agentCoreConfig.endpoint.replace("http", "ws")}/ws`);
9475
- wsUrl.searchParams.set("sessionId", config.sessionId);
9476
- wsUrl.searchParams.set("characterId", config.tenant.characterId);
9477
- this.ws = new WebSocket(wsUrl.toString());
9478
- this.ws.onopen = () => {
9479
- this.ws?.send(JSON.stringify({
9480
- type: "auth",
9481
- token: authToken,
9482
- tenantId: config.tenant.tenantId,
9483
- systemPrompt: config.systemPrompt
9484
- }));
9485
- };
9486
- this.ws.onmessage = (event) => {
9487
- try {
9488
- this.handleAgentCoreMessage(JSON.parse(event.data));
9489
- } catch {
9490
- }
9491
- };
9492
- this.ws.onerror = () => {
9493
- reject(new Error("WebSocket connection failed"));
9494
- };
9495
- this.ws.onclose = (event) => {
9496
- this.handleDisconnect(event);
9497
- };
9498
- const authTimeout = setTimeout(() => {
9499
- reject(new Error("Auth timeout"));
9500
- }, 1e4);
9501
- const authHandler = (event) => {
9502
- try {
9503
- const data = JSON.parse(event.data);
9504
- if (data.type === "auth_success") {
9505
- clearTimeout(authTimeout);
9506
- this.ws?.removeEventListener("message", authHandler);
9507
- resolve();
9508
- } else if (data.type === "auth_failed") {
9509
- clearTimeout(authTimeout);
9510
- reject(new Error(data.message));
9511
- }
9512
- } catch {
9513
- }
9514
- };
9515
- this.ws.addEventListener("message", authHandler);
9516
- });
9517
- }
9518
- handleAgentCoreMessage(data) {
9519
- switch (data.type) {
9520
- case "response_start":
9521
- this.setState("speaking");
9522
- this.isSpeaking = true;
9523
- this.emit("ai.response.start", {
9524
- text: data.text,
9525
- emotion: data.emotion
9526
- });
9527
- if (data.emotion) {
9528
- this.emotionController.transitionTo(
9529
- { [data.emotion]: 0.7 },
9530
- 300
9531
- );
9532
- }
9533
- if (this.pipeline) {
9534
- this.pipeline.start();
9535
- }
9536
- break;
9537
- case "response_chunk":
9538
- this.emit("ai.response.chunk", {
9539
- text: data.text,
9540
- isLast: data.isLast
9541
- });
9542
- break;
9543
- case "audio_chunk":
9544
- if (data.audio && this.pipeline) {
9545
- const audioData = this.base64ToArrayBuffer(data.audio);
9546
- const uint8 = new Uint8Array(audioData);
9547
- this.pipeline.onAudioChunk(uint8).catch((error) => {
9548
- console.error("[AgentCore] Pipeline chunk error:", error);
9549
- });
9550
- }
9551
- break;
9552
- case "audio_end":
9553
- if (this.pipeline) {
9554
- this.pipeline.end().catch((error) => {
9555
- console.error("[AgentCore] Pipeline end error:", error);
9556
- });
9557
- }
9558
- break;
9559
- case "response_end":
9560
- this.addToHistory({
9561
- role: "assistant",
9562
- content: data.fullText,
9563
- timestamp: Date.now(),
9564
- emotion: data.emotion
9565
- });
9566
- this.emit("ai.response.end", {
9567
- fullText: data.fullText,
9568
- durationMs: data.durationMs || 0
9569
- });
9570
- break;
9571
- case "memory_updated":
9572
- this.emit("memory.updated", {
9573
- messageCount: data.messageCount,
9574
- tokenCount: data.tokenCount
9575
- });
9576
- break;
9577
- case "error":
9578
- this.emit("connection.error", {
9579
- error: new Error(data.message),
9580
- recoverable: data.recoverable ?? false
9581
- });
9582
- break;
9583
- }
9584
- }
9585
- scheduleTranscription() {
9586
- if (this.audioBuffer.length === 0) return;
9587
- const totalLength = this.audioBuffer.reduce((sum2, buf) => sum2 + buf.length, 0);
9588
- if (totalLength < 4e3) return;
9589
- const audio = new Float32Array(totalLength);
9590
- let offset = 0;
9591
- for (const buf of this.audioBuffer) {
9592
- audio.set(buf, offset);
9593
- offset += buf.length;
9594
- }
9595
- this.audioBuffer = [];
9596
- let sum = 0;
9597
- for (let i = 0; i < audio.length; i++) {
9598
- sum += audio[i] * audio[i];
9599
- }
9600
- const rms = Math.sqrt(sum / audio.length);
9601
- if (rms < 0.01) {
9602
- console.debug("[AgentCore] Skipping silent audio", { rms, samples: audio.length });
9603
- return;
9604
- }
9605
- if (this.asr) {
9606
- this.setState("listening");
9607
- this.emit("user.speech.start", { timestamp: Date.now() });
9608
- this.asr.transcribe(audio).then((result) => {
9609
- this.emit("user.transcript.final", {
9610
- text: result.text,
9611
- confidence: 1
9612
- });
9613
- this.emit("user.speech.end", { timestamp: Date.now(), durationMs: result.inferenceTimeMs });
9614
- const cleanText = result.text.trim();
9615
- if (cleanText) {
9616
- this.sendText(cleanText).catch((error) => {
9617
- console.error("[AgentCore] Send text error:", error);
9618
- });
9619
- }
9620
- }).catch((error) => {
9621
- console.error("[AgentCore] Transcription error:", error);
9622
- });
9623
- }
9624
- }
9625
- // REMOVED: processAudioForAnimation() - now handled by FullFacePipeline
9626
- // The pipeline manages audio scheduling, LAM inference, and frame synchronization
9627
- // Frames are emitted via pipeline.on('full_frame_ready') event (see initPipeline())
9628
- /**
9629
- * Detect voice activity using Silero VAD
9630
- * Falls back to simple RMS if VAD not available
9631
- */
9632
- async detectVoiceActivity(audio) {
9633
- const float32 = audio instanceof Float32Array ? audio : int16ToFloat32(audio);
9634
- if (this.vad) {
9635
- const chunkSize = this.vad.getChunkSize();
9636
- for (let i = 0; i + chunkSize <= float32.length; i += chunkSize) {
9637
- const chunk = float32.slice(i, i + chunkSize);
9638
- const result = await this.vad.process(chunk);
9639
- if (result.isSpeech) {
9640
- return true;
9641
- }
9642
- }
9643
- return false;
9644
- }
9645
- let sum = 0;
9646
- for (let i = 0; i < float32.length; i++) {
9647
- sum += float32[i] * float32[i];
9648
- }
9649
- const rms = Math.sqrt(sum / float32.length);
9650
- return rms > 0.02;
9651
- }
9652
- base64ToArrayBuffer(base64) {
9653
- const binaryString = atob(base64);
9654
- const bytes = new Uint8Array(binaryString.length);
9655
- for (let i = 0; i < binaryString.length; i++) {
9656
- bytes[i] = binaryString.charCodeAt(i);
9657
- }
9658
- return bytes.buffer;
9659
- }
9660
- addToHistory(message) {
9661
- this.history.push(message);
9662
- this.emit("memory.updated", { messageCount: this.history.length });
9663
- }
9664
- handleDisconnect(event) {
9665
- this._isConnected = false;
9666
- if (event.code !== 1e3) {
9667
- if (this.wsReconnectAttempts < this.maxReconnectAttempts) {
9668
- this.wsReconnectAttempts++;
9669
- setTimeout(() => {
9670
- if (this.currentConfig) {
9671
- this.connect(this.currentConfig).catch(() => {
9672
- });
9673
- }
9674
- }, Math.pow(2, this.wsReconnectAttempts) * 1e3);
9675
- } else {
9676
- this.setState("error");
9677
- this.emit("connection.error", {
9678
- error: new Error("Max reconnection attempts reached"),
9679
- recoverable: false
9680
- });
9681
- }
9682
- }
9683
- this.emit("connection.closed", { reason: event.reason || "Connection closed" });
9684
- }
9685
- };
9686
-
9687
- // src/ai/orchestration/ConversationOrchestrator.ts
9688
- var ConversationSessionImpl = class {
9689
- constructor(config, adapter) {
9690
- this._history = [];
9691
- this._context = /* @__PURE__ */ new Map();
9692
- this.sessionId = config.sessionId;
9693
- this._config = config;
9694
- this._adapter = adapter;
9695
- this.createdAt = Date.now();
9696
- this._lastActivityAt = Date.now();
9697
- this._emotionController = new EmotionController();
9698
- if (config.emotion) {
9699
- this._emotionController.setPreset(config.emotion);
9700
- }
9701
- }
9702
- get adapter() {
9703
- return this._adapter;
9704
- }
9705
- get config() {
9706
- return this._config;
9707
- }
9708
- get state() {
9709
- return this._adapter.state;
9710
- }
9711
- get history() {
9712
- return [...this._history];
9713
- }
9714
- get emotion() {
9715
- return {};
9716
- }
9717
- get lastActivityAt() {
9718
- return this._lastActivityAt;
9719
- }
9720
- async start() {
9721
- await this._adapter.connect(this._config);
9722
- this._lastActivityAt = Date.now();
9723
- }
9724
- async end() {
9725
- await this._adapter.disconnect();
9726
- }
9727
- pushAudio(audio) {
9728
- this._adapter.pushAudio(audio);
9729
- this._lastActivityAt = Date.now();
9730
- }
9731
- async sendText(text) {
9732
- await this._adapter.sendText(text);
9733
- this._lastActivityAt = Date.now();
9734
- }
9735
- interrupt() {
9736
- this._adapter.interrupt();
9737
- this._lastActivityAt = Date.now();
9738
- }
9739
- setEmotion(emotion) {
9740
- this._emotionController.set(emotion);
9741
- }
9742
- addContext(key, value) {
9743
- this._context.set(key, value);
9744
- }
9745
- removeContext(key) {
9746
- this._context.delete(key);
9747
- }
9748
- getContext() {
9749
- return Object.fromEntries(this._context);
9750
- }
9751
- export() {
9752
- return {
9753
- sessionId: this.sessionId,
9754
- tenantId: this._config.tenant.tenantId,
9755
- characterId: this._config.tenant.characterId,
9756
- history: this._history,
9757
- context: Object.fromEntries(this._context),
9758
- emotion: this.emotion,
9759
- createdAt: this.createdAt,
9760
- lastActivityAt: this._lastActivityAt
9761
- };
9762
- }
9763
- import(snapshot) {
9764
- this._history = [...snapshot.history];
9765
- this._context = new Map(Object.entries(snapshot.context));
9766
- this._lastActivityAt = snapshot.lastActivityAt;
9767
- }
9768
- syncHistory() {
9769
- this._history = this._adapter.getHistory();
9770
- }
9771
- };
9772
- var ConversationOrchestrator = class extends EventEmitter {
9773
- constructor(config) {
9774
- super();
9775
- // Sessions per tenant
9776
- this.sessions = /* @__PURE__ */ new Map();
9777
- // Tenant configurations
9778
- this.tenants = /* @__PURE__ */ new Map();
9779
- // Health monitoring
9780
- this.healthCheckInterval = null;
9781
- this.HEALTH_CHECK_INTERVAL_MS = 3e4;
9782
- this.config = {
9783
- connectionTimeoutMs: 5e3,
9784
- maxRetries: 3,
9785
- ...config
9786
- };
9787
- this.adapter = new AgentCoreAdapter(config.adapter);
9788
- }
9789
- /**
9790
- * Register a tenant
9791
- */
9792
- registerTenant(tenant) {
9793
- this.tenants.set(tenant.tenantId, tenant);
9794
- }
9795
- /**
9796
- * Unregister a tenant
9797
- */
9798
- unregisterTenant(tenantId) {
9799
- this.tenants.delete(tenantId);
9800
- }
9801
- /**
9802
- * Get tenant config
9803
- */
9804
- getTenant(tenantId) {
9805
- return this.tenants.get(tenantId);
9806
- }
9807
- /**
9808
- * Create a new conversation session for a tenant
9809
- */
9810
- async createSession(tenantId, options = {}) {
9811
- const tenant = this.tenants.get(tenantId);
9812
- if (!tenant) {
9813
- throw new Error(`Tenant not found: ${tenantId}`);
9814
- }
9815
- const sessionId = options.sessionId || this.generateSessionId();
9816
- const sessionConfig = {
9817
- sessionId,
9818
- tenant,
9819
- systemPrompt: options.systemPrompt,
9820
- voice: options.voice,
9821
- emotion: options.emotion,
9822
- language: options.language
9823
- };
9824
- const session = new ConversationSessionImpl(sessionConfig, this.adapter);
9825
- this.sessions.set(sessionId, session);
9826
- this.forwardAdapterEvents(this.adapter, sessionId);
9827
- await session.start();
9828
- this.emit("session.created", { sessionId, tenantId });
9829
- return session;
9830
- }
9831
- /**
9832
- * End a session
9833
- */
9834
- async endSession(sessionId) {
9835
- const session = this.sessions.get(sessionId);
9836
- if (session) {
9837
- await session.end();
9838
- this.sessions.delete(sessionId);
9839
- this.emit("session.ended", { sessionId, reason: "Client requested" });
9840
- }
9841
- }
9842
- /**
9843
- * Get session by ID
9844
- */
9845
- getSession(sessionId) {
9846
- return this.sessions.get(sessionId);
9847
- }
9848
- /**
9849
- * Get all sessions for a tenant
9850
- */
9851
- getTenantSessions(tenantId) {
9852
- return Array.from(this.sessions.values()).filter((s) => s.config.tenant.tenantId === tenantId);
9853
- }
9854
- /**
9855
- * Start health monitoring
9856
- */
9857
- startHealthMonitoring() {
9858
- if (this.healthCheckInterval) return;
9859
- this.healthCheckInterval = setInterval(async () => {
9860
- await this.performHealthCheck();
9861
- }, this.HEALTH_CHECK_INTERVAL_MS);
9862
- }
9863
- /**
9864
- * Stop health monitoring
9865
- */
9866
- stopHealthMonitoring() {
9867
- if (this.healthCheckInterval) {
9868
- clearInterval(this.healthCheckInterval);
9869
- this.healthCheckInterval = null;
9870
- }
9871
- }
9872
- /**
9873
- * Dispose all resources
9874
- */
9875
- async dispose() {
9876
- this.stopHealthMonitoring();
9877
- const endPromises = Array.from(this.sessions.values()).map((s) => s.end());
9878
- await Promise.all(endPromises);
9879
- this.sessions.clear();
9880
- await this.adapter.disconnect();
9881
- }
9882
- // ==================== Private Methods ====================
9883
- generateSessionId() {
9884
- return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
9885
- }
9886
- forwardAdapterEvents(adapter, sessionId) {
9887
- const events = [
9888
- "state.change",
9889
- "user.speech.start",
9890
- "user.speech.end",
9891
- "user.transcript.partial",
9892
- "user.transcript.final",
9893
- "ai.thinking.start",
9894
- "ai.response.start",
9895
- "ai.response.chunk",
9896
- "ai.response.end",
9897
- "audio.output.chunk",
9898
- "audio.output.end",
9899
- "animation",
9900
- "memory.updated",
9901
- "connection.error",
9902
- "interruption.detected",
9903
- "interruption.handled"
9904
- ];
9905
- for (const event of events) {
9906
- adapter.on(event, (data) => {
9907
- const eventData = data;
9908
- this.emit(event, { ...eventData, sessionId });
9909
- });
9910
- }
9911
- }
9912
- async performHealthCheck() {
9913
- try {
9914
- await this.adapter.healthCheck();
9915
- } catch {
9916
- }
9917
- }
9918
- };
9919
-
9920
- // src/ai/tenancy/TenantManager.ts
9921
- var _TenantManager = class _TenantManager {
9922
- constructor() {
9923
- this.tenants = /* @__PURE__ */ new Map();
9924
- this.quotas = /* @__PURE__ */ new Map();
9925
- this.usage = /* @__PURE__ */ new Map();
9926
- this.tokenRefreshCallbacks = /* @__PURE__ */ new Map();
9927
- }
9928
- /**
9929
- * Register a tenant with quota
9930
- */
9931
- register(tenant, quota = _TenantManager.DEFAULT_QUOTA, tokenRefreshCallback) {
9932
- this.tenants.set(tenant.tenantId, tenant);
9933
- this.quotas.set(tenant.tenantId, quota);
9934
- this.usage.set(tenant.tenantId, {
9935
- currentSessions: 0,
9936
- requestsThisMinute: 0,
9937
- tokensUsed: 0,
9938
- audioMinutesToday: 0,
9939
- lastMinuteReset: Date.now(),
9940
- lastDailyReset: Date.now()
9941
- });
9942
- if (tokenRefreshCallback) {
9943
- this.tokenRefreshCallbacks.set(tenant.tenantId, tokenRefreshCallback);
9944
- }
9945
- }
9946
- /**
9947
- * Unregister a tenant
9948
- */
9949
- unregister(tenantId) {
9950
- this.tenants.delete(tenantId);
9951
- this.quotas.delete(tenantId);
9952
- this.usage.delete(tenantId);
9953
- this.tokenRefreshCallbacks.delete(tenantId);
9954
- }
9955
- /**
9956
- * Get tenant config
9957
- */
9958
- get(tenantId) {
9959
- return this.tenants.get(tenantId);
9960
- }
9961
- /**
9962
- * Check if tenant exists
9963
- */
9964
- has(tenantId) {
9965
- return this.tenants.has(tenantId);
9966
- }
9967
- /**
9968
- * Get all tenant IDs
9969
- */
9970
- getTenantIds() {
9971
- return Array.from(this.tenants.keys());
9972
- }
9973
- /**
9974
- * Check if tenant can create new session
9975
- */
9976
- canCreateSession(tenantId) {
9977
- const quota = this.quotas.get(tenantId);
9978
- const usage = this.usage.get(tenantId);
9979
- if (!quota || !usage) return false;
9980
- return usage.currentSessions < quota.maxSessions;
9981
- }
9982
- /**
9983
- * Check if tenant can make request
9984
- */
9985
- canMakeRequest(tenantId) {
9986
- const quota = this.quotas.get(tenantId);
9987
- const usage = this.usage.get(tenantId);
9988
- if (!quota || !usage) return false;
9989
- this.checkMinuteReset(tenantId);
9990
- return usage.requestsThisMinute < quota.requestsPerMinute;
9991
- }
9992
- /**
9993
- * Check if tenant can use audio
9994
- */
9995
- canUseAudio(tenantId, minutes) {
9996
- const quota = this.quotas.get(tenantId);
9997
- const usage = this.usage.get(tenantId);
9998
- if (!quota || !usage) return false;
9999
- this.checkDailyReset(tenantId);
10000
- return usage.audioMinutesToday + minutes <= quota.maxAudioMinutesPerDay;
10001
- }
10002
- /**
10003
- * Increment session count
10004
- */
10005
- incrementSessions(tenantId) {
10006
- const usage = this.usage.get(tenantId);
10007
- if (usage) {
10008
- usage.currentSessions++;
10009
- }
10010
- }
10011
- /**
10012
- * Decrement session count
10013
- */
10014
- decrementSessions(tenantId) {
10015
- const usage = this.usage.get(tenantId);
10016
- if (usage && usage.currentSessions > 0) {
10017
- usage.currentSessions--;
10018
- }
10019
- }
10020
- /**
10021
- * Record a request
10022
- */
10023
- recordRequest(tenantId) {
10024
- const usage = this.usage.get(tenantId);
10025
- if (usage) {
10026
- this.checkMinuteReset(tenantId);
10027
- usage.requestsThisMinute++;
10028
- }
10029
- }
10030
- /**
10031
- * Record token usage
10032
- */
10033
- recordTokens(tenantId, tokens) {
10034
- const usage = this.usage.get(tenantId);
10035
- if (usage) {
10036
- usage.tokensUsed += tokens;
10037
- }
10038
- }
10039
- /**
10040
- * Record audio usage
10041
- */
10042
- recordAudioMinutes(tenantId, minutes) {
10043
- const usage = this.usage.get(tenantId);
10044
- if (usage) {
10045
- this.checkDailyReset(tenantId);
10046
- usage.audioMinutesToday += minutes;
10047
- }
10048
- }
10049
- /**
10050
- * Get fresh auth token for tenant
10051
- */
10052
- async getAuthToken(tenantId) {
10053
- const tenant = this.tenants.get(tenantId);
10054
- if (!tenant) {
10055
- throw new Error(`Tenant not found: ${tenantId}`);
10056
- }
10057
- const callback = this.tokenRefreshCallbacks.get(tenantId);
10058
- if (callback) {
10059
- const token = await callback();
10060
- tenant.credentials.authToken = token;
10061
- return token;
10062
- }
10063
- if (tenant.credentials.authToken) {
10064
- return tenant.credentials.authToken;
10065
- }
10066
- throw new Error(`No auth token available for tenant: ${tenantId}`);
10067
- }
10068
- /**
10069
- * Update tenant credentials
10070
- */
10071
- updateCredentials(tenantId, credentials) {
10072
- const tenant = this.tenants.get(tenantId);
10073
- if (tenant) {
10074
- tenant.credentials = { ...tenant.credentials, ...credentials };
10075
- }
10076
- }
10077
- /**
10078
- * Get usage stats for tenant
10079
- */
10080
- getUsage(tenantId) {
10081
- return this.usage.get(tenantId);
10082
- }
10083
- /**
10084
- * Get quota for tenant
10085
- */
10086
- getQuota(tenantId) {
10087
- return this.quotas.get(tenantId);
10088
- }
10089
- /**
10090
- * Update quota for tenant
10091
- */
10092
- updateQuota(tenantId, quota) {
10093
- const existing = this.quotas.get(tenantId);
10094
- if (existing) {
10095
- this.quotas.set(tenantId, { ...existing, ...quota });
10096
- }
10097
- }
10098
- /**
10099
- * Reset all usage stats for a tenant
10100
- */
10101
- resetUsage(tenantId) {
10102
- const usage = this.usage.get(tenantId);
10103
- if (usage) {
10104
- usage.requestsThisMinute = 0;
10105
- usage.tokensUsed = 0;
10106
- usage.audioMinutesToday = 0;
10107
- usage.lastMinuteReset = Date.now();
10108
- usage.lastDailyReset = Date.now();
10109
- }
10110
- }
10111
- // ==================== Private Methods ====================
10112
- checkMinuteReset(tenantId) {
10113
- const usage = this.usage.get(tenantId);
10114
- if (!usage) return;
10115
- const now = Date.now();
10116
- if (now - usage.lastMinuteReset >= 6e4) {
10117
- usage.requestsThisMinute = 0;
10118
- usage.lastMinuteReset = now;
10119
- }
10120
- }
10121
- checkDailyReset(tenantId) {
10122
- const usage = this.usage.get(tenantId);
10123
- if (!usage) return;
10124
- const now = Date.now();
10125
- const MS_PER_DAY = 24 * 60 * 60 * 1e3;
10126
- if (now - usage.lastDailyReset >= MS_PER_DAY) {
10127
- usage.audioMinutesToday = 0;
10128
- usage.lastDailyReset = now;
10129
- }
10130
- }
10131
- };
10132
- /**
10133
- * Default quota for new tenants
10134
- */
10135
- _TenantManager.DEFAULT_QUOTA = {
10136
- maxSessions: 10,
10137
- requestsPerMinute: 60,
10138
- maxTokensPerConversation: 1e5,
10139
- maxAudioMinutesPerDay: 60
10140
- };
10141
- var TenantManager = _TenantManager;
10142
-
10143
- // src/ai/utils/AudioSyncManager.ts
10144
- var AudioSyncManager = class extends EventEmitter {
10145
- constructor(config = {}) {
10146
- super();
10147
- this.bufferPosition = 0;
10148
- this.playbackQueue = [];
10149
- this.isPlaying = false;
10150
- this.audioContext = null;
10151
- this.playbackStartTime = 0;
10152
- this.samplesPlayed = 0;
10153
- this.config = {
10154
- sampleRate: 16e3,
10155
- bufferSize: 16640,
10156
- overlapSize: 4160,
10157
- maxDriftMs: 100,
10158
- ...config
10159
- };
10160
- this.audioBuffer = new Float32Array(this.config.bufferSize);
10161
- }
10162
- /**
10163
- * Initialize audio context
10164
- */
10165
- async initialize() {
10166
- if (!this.audioContext) {
10167
- this.audioContext = new AudioContext({ sampleRate: this.config.sampleRate });
10168
- }
10169
- if (this.audioContext.state === "suspended") {
10170
- await this.audioContext.resume();
10171
- }
10172
- }
10173
- /**
10174
- * Push audio chunk for processing and playback
10175
- */
10176
- pushAudio(audio) {
10177
- this.playbackQueue.push(audio);
10178
- this.bufferForInference(audio);
10179
- if (!this.isPlaying && this.playbackQueue.length > 0) {
10180
- this.startPlayback();
10181
- }
10182
- }
10183
- /**
10184
- * Buffer audio for inference
10185
- */
10186
- bufferForInference(audio) {
10187
- let offset = 0;
10188
- while (offset < audio.length) {
10189
- const remaining = this.config.bufferSize - this.bufferPosition;
10190
- const toCopy = Math.min(remaining, audio.length - offset);
10191
- this.audioBuffer.set(audio.subarray(offset, offset + toCopy), this.bufferPosition);
10192
- this.bufferPosition += toCopy;
10193
- offset += toCopy;
10194
- if (this.bufferPosition >= this.config.bufferSize) {
10195
- this.emit("buffer.ready", { audio: new Float32Array(this.audioBuffer) });
10196
- const overlapStart = this.config.bufferSize - this.config.overlapSize;
10197
- this.audioBuffer.copyWithin(0, overlapStart);
10198
- this.bufferPosition = this.config.overlapSize;
10199
- }
10200
- }
10201
- }
10202
- /**
10203
- * Start audio playback
10204
- */
10205
- async startPlayback() {
10206
- if (!this.audioContext || this.isPlaying) return;
10207
- this.isPlaying = true;
10208
- this.playbackStartTime = this.audioContext.currentTime;
10209
- this.samplesPlayed = 0;
10210
- this.emit("playback.start", {});
10211
- await this.processPlaybackQueue();
10212
- }
10213
- /**
10214
- * Process playback queue
10215
- */
10216
- async processPlaybackQueue() {
10217
- if (!this.audioContext) return;
10218
- while (this.playbackQueue.length > 0) {
10219
- const audio = this.playbackQueue.shift();
10220
- const buffer = this.audioContext.createBuffer(1, audio.length, this.config.sampleRate);
10221
- buffer.copyToChannel(audio, 0);
10222
- const source = this.audioContext.createBufferSource();
10223
- source.buffer = buffer;
10224
- source.connect(this.audioContext.destination);
10225
- const playTime = this.playbackStartTime + this.samplesPlayed / this.config.sampleRate;
10226
- source.start(playTime);
10227
- this.samplesPlayed += audio.length;
10228
- this.checkDrift();
10229
- await new Promise((resolve) => {
10230
- source.onended = resolve;
10231
- });
10232
- }
10233
- this.isPlaying = false;
10234
- this.emit("playback.end", {});
10235
- }
10236
- /**
10237
- * Check for audio/animation drift
10238
- */
10239
- checkDrift() {
10240
- if (!this.audioContext) return;
10241
- const expectedTime = this.playbackStartTime + this.samplesPlayed / this.config.sampleRate;
10242
- const actualTime = this.audioContext.currentTime;
10243
- const driftMs = (actualTime - expectedTime) * 1e3;
10244
- if (Math.abs(driftMs) > this.config.maxDriftMs) {
10245
- this.emit("sync.drift", { driftMs });
10246
- }
10247
- }
10248
- /**
10249
- * Clear playback queue
10250
- */
10251
- clearQueue() {
10252
- this.playbackQueue = [];
10253
- this.bufferPosition = 0;
10254
- this.audioBuffer.fill(0);
10255
- }
10256
- /**
10257
- * Stop playback
10258
- */
10259
- stop() {
10260
- this.clearQueue();
10261
- this.isPlaying = false;
10262
- }
10263
- /**
10264
- * Get current playback position in seconds
10265
- */
10266
- getPlaybackPosition() {
10267
- if (!this.audioContext) return 0;
10268
- return this.audioContext.currentTime - this.playbackStartTime;
10269
- }
10270
- /**
10271
- * Check if currently playing
10272
- */
10273
- getIsPlaying() {
10274
- return this.isPlaying;
10275
- }
10276
- /**
10277
- * Dispose resources
10278
- */
10279
- dispose() {
10280
- this.stop();
10281
- this.audioContext?.close();
10282
- this.audioContext = null;
10283
- }
10284
- };
10285
-
10286
- // src/ai/utils/InterruptionHandler.ts
10287
- var InterruptionHandler = class extends EventEmitter {
10288
- constructor(config = {}) {
10289
- super();
10290
- this.isSpeaking = false;
10291
- this.speechStartTime = 0;
10292
- this.lastSpeechTime = 0;
10293
- this.silenceTimer = null;
10294
- this.aiIsSpeaking = false;
10295
- // Debouncing: only emit one interruption per speech session
10296
- this.interruptionTriggeredThisSession = false;
10297
- this.config = {
10298
- vadThreshold: 0.5,
10299
- // Silero VAD default
10300
- minSpeechDurationMs: 200,
10301
- // Google/Amazon barge-in standard
10302
- silenceTimeoutMs: 500,
10303
- // OpenAI Realtime API standard
10304
- enabled: true,
10305
- ...config
10306
- };
10307
- }
10308
- /**
10309
- * Process VAD result for interruption detection
10310
- * @param vadProbability - Speech probability from VAD (0-1)
10311
- * @param audioEnergy - Optional RMS energy for logging (default: 0)
10312
- */
10313
- processVADResult(vadProbability, audioEnergy = 0) {
10314
- if (!this.config.enabled) return;
10315
- if (vadProbability > this.config.vadThreshold) {
10316
- this.onSpeechDetected(audioEnergy || vadProbability);
10317
- } else {
10318
- this.onSilenceDetected();
10319
- }
10320
- }
10321
- /**
10322
- * Notify that AI started speaking
10323
- */
10324
- setAISpeaking(speaking) {
10325
- this.aiIsSpeaking = speaking;
10326
- }
10327
- /**
10328
- * Enable/disable interruption detection
10329
- */
10330
- setEnabled(enabled) {
10331
- this.config.enabled = enabled;
10332
- if (!enabled) {
10333
- this.reset();
10334
- }
10335
- }
10336
- /**
10337
- * Update configuration
10338
- */
10339
- updateConfig(config) {
10340
- this.config = { ...this.config, ...config };
10341
- }
10342
- /**
10343
- * Reset state
10344
- */
10345
- reset() {
10346
- this.isSpeaking = false;
10347
- this.speechStartTime = 0;
10348
- this.lastSpeechTime = 0;
10349
- this.interruptionTriggeredThisSession = false;
10350
- if (this.silenceTimer) {
10351
- clearTimeout(this.silenceTimer);
10352
- this.silenceTimer = null;
10353
- }
10354
- }
10355
- /**
10356
- * Get current state
10357
- */
10358
- getState() {
10359
- return {
10360
- isSpeaking: this.isSpeaking,
10361
- speechDurationMs: this.isSpeaking ? Date.now() - this.speechStartTime : 0
10362
- };
10363
- }
10364
- // ==================== Private Methods ====================
10365
- onSpeechDetected(rms) {
10366
- const now = Date.now();
10367
- this.lastSpeechTime = now;
10368
- if (this.silenceTimer) {
10369
- clearTimeout(this.silenceTimer);
10370
- this.silenceTimer = null;
10371
- }
10372
- if (!this.isSpeaking) {
10373
- this.isSpeaking = true;
10374
- this.speechStartTime = now;
10375
- this.emit("speech.detected", { rms });
10376
- }
10377
- if (this.aiIsSpeaking && !this.interruptionTriggeredThisSession) {
10378
- const speechDuration = now - this.speechStartTime;
10379
- if (speechDuration >= this.config.minSpeechDurationMs) {
10380
- this.interruptionTriggeredThisSession = true;
10381
- this.emit("interruption.triggered", { rms, durationMs: speechDuration });
10382
- }
10383
- }
10384
- }
10385
- onSilenceDetected() {
10386
- if (!this.isSpeaking) return;
10387
- if (!this.silenceTimer) {
10388
- this.silenceTimer = setTimeout(() => {
10389
- const durationMs = this.lastSpeechTime - this.speechStartTime;
10390
- this.isSpeaking = false;
10391
- this.silenceTimer = null;
10392
- this.interruptionTriggeredThisSession = false;
10393
- this.emit("speech.ended", { durationMs });
10394
- }, this.config.silenceTimeoutMs);
10395
- }
10396
- }
10397
- };
10398
-
10399
9282
  // src/animation/types.ts
10400
9283
  var DEFAULT_ANIMATION_CONFIG = {
10401
9284
  initialState: "idle",