ttp-agent-sdk 2.48.14 → 2.48.16

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.
@@ -9170,7 +9170,7 @@ if (false) // removed by dead control flow
9170
9170
  /***/ ((module) => {
9171
9171
 
9172
9172
  "use strict";
9173
- module.exports = "/**\n * AudioProcessor - AudioWorklet for real-time audio processing\n * \n * This AudioWorklet processes audio data in real-time and sends it to the main thread\n * for transmission to the WebSocket server.\n */\n\nclass AudioProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n \n // Configuration\n this.config = options.processorOptions || {};\n // Use AudioContext sampleRate (available as global 'sampleRate' in AudioWorkletProcessor)\n // Fall back to config if sampleRate not available, default to 24 kHz to match typical server/TTS output\n this.sampleRate = typeof sampleRate !== 'undefined' ? sampleRate : (this.config.sampleRate || this.config.outputSampleRate || 24000);\n this.bufferSize = 128; // Process 128 samples at a time (256 bytes = 8ms at 16kHz)\n this.buffer = new Float32Array(this.bufferSize);\n this.bufferIndex = 0;\n \n // VAD (Voice Activity Detection) parameters\n this.silenceThreshold = 0.02; // Base RMS threshold for voice detection (scaled by micSensitivity below)\n this.VOICE_FRAMES_REQUIRED = 2; // Require 2 consecutive frames above threshold before activating\n this.minVoiceDuration = 100; // ms - minimum speech duration\n this.pauseThreshold = 3000; // ms - longer pause before processing\n\n // Mic sensitivity: user/agent-adjustable multiplier on the VAD energy gate.\n // 1.0 = default; 2.0 = twice as sensitive (half the RMS floor); 0.5 = half as\n // sensitive (double the floor — only close/loud speech triggers, e.g. TV in the room).\n this.micSensitivity = (typeof this.config.micSensitivity === 'number' && this.config.micSensitivity > 0)\n ? this.config.micSensitivity : 1.0;\n // While agent audio plays, the gate rises by this factor (stricter barge-in: background\n // TV/chatter must not interrupt playback). The SDK main thread tracks playback state.\n this.playbackActive = false;\n this.PLAYBACK_GATE_FACTOR = 1.6;\n // Hysteresis: once voice is active the floor drops, so a borderline signal isn't\n // chopped on every frame (quiet speech tails stay under the trigger level).\n this.HYSTERESIS_FACTOR = 0.75;\n \n // VAD state\n this.isVoiceActive = false;\n this.voiceStartTime = 0;\n this.lastVoiceTime = 0;\n this.consecutiveSilenceFrames = 0;\n this.silenceFramesThreshold = 5; // More frames needed for silence detection\n this.voiceFrameCount = 0; // Count consecutive frames above threshold\n \n // Audio quality tracking\n this.frameCount = 0;\n this.lastLogTime = 0;\n \n // Continuous recording mode\n this.continuousMode = true; // Always send audio when voice is detected\n this.forceContinuous = true; // Force continuous for toggle button behavior\n this.isCurrentlyStreaming = false; // Track if we're currently sending audio\n \n // Batching buffer\n this.sendBuffer = null;\n this.sendBufferBytes = 0;\n\n // Start true so the first process() call cannot terminate the worklet before\n // the main thread posts setForceContinuous (WebKit/iOS often renders audio before port messages are handled).\n this.isProcessing = true;\n \n // Handle messages from main thread\n this.port.onmessage = (event) => {\n const { type, data } = event.data;\n \n switch (type) {\n case 'start':\n this.isProcessing = true;\n this.isCurrentlyStreaming = true;\n break;\n \n case 'stop':\n this.isProcessing = false;\n this.isCurrentlyStreaming = false;\n this.isVoiceActive = false;\n this.forceContinuous = false;\n this.voiceFrameCount = 0; // Reset voice frame count\n // Flush any remaining data\n this.flushBuffer();\n break;\n \n case 'setForceContinuous':\n this.forceContinuous = data.enabled;\n this.isProcessing = true;\n // iOS: bypass client VAD - always send. Desktop: keep VAD (saves bandwidth, reduces noise).\n this.isCurrentlyStreaming = data.enabled && (data.bypassVad === true);\n break;\n\n case 'setMicSensitivity': {\n const v = Number(data && data.value);\n if (isFinite(v) && v > 0) {\n this.micSensitivity = Math.max(0.25, Math.min(4.0, v));\n }\n break;\n }\n\n case 'setPlaybackActive':\n this.playbackActive = !!(data && data.active);\n break;\n \n case 'flush':\n this.flushBuffer();\n break;\n \n case 'config':\n Object.assign(this.config, data);\n break;\n }\n };\n }\n \n /**\n * Process audio data\n */\n process(inputs, outputs, parameters) {\n // CRITICAL: If processing is stopped, terminate the processor\n if (!this.isProcessing) {\n // Return false to terminate the AudioWorklet processor\n // This stops all VAD processing immediately\n return false;\n }\n \n const input = inputs[0];\n const output = outputs[0];\n \n // Copy input to output (pass-through)\n if (input.length > 0 && output.length > 0) {\n output[0].set(input[0]);\n }\n \n // Process audio for PCM recording and VAD\n if (input.length > 0 && input[0].length > 0) {\n this.processAudioData(input[0]);\n }\n \n // Keep the processor alive\n return true;\n }\n \n processAudioData(audioData) {\n // CRITICAL: Early return if processing is stopped\n // This prevents VAD calculations and logging when stopped\n if (!this.isProcessing) {\n return;\n }\n \n this.frameCount++;\n \n // Process audio in consistent 128-sample chunks (256 bytes)\n for (let i = 0; i < audioData.length; i += this.bufferSize) {\n const chunkSize = Math.min(this.bufferSize, audioData.length - i);\n \n // Copy chunk to buffer\n for (let j = 0; j < chunkSize; j++) {\n this.buffer[j] = audioData[i + j];\n }\n \n // Pad with zeros if needed\n for (let j = chunkSize; j < this.bufferSize; j++) {\n this.buffer[j] = 0;\n }\n \n // Calculate RMS for VAD on this chunk\n let sum = 0;\n for (let j = 0; j < this.bufferSize; j++) {\n sum += this.buffer[j] * this.buffer[j];\n }\n const rms = Math.sqrt(sum / this.bufferSize);\n \n // Calculate additional features for better VAD\n let variation = 0;\n let highFreqCount = 0;\n for (let j = 1; j < this.bufferSize; j++) {\n const diff = Math.abs(this.buffer[j] - this.buffer[j-1]);\n variation += diff;\n if (diff > 0.1) highFreqCount++;\n }\n variation = variation / this.bufferSize;\n const highFreqRatio = highFreqCount / this.bufferSize;\n \n const currentTime = Date.now();\n\n // Effective VAD floor: base threshold scaled by the mic-sensitivity multiplier,\n // raised during agent playback (stricter barge-in), lowered while voice is\n // already active (hysteresis, so speech tails aren't clipped).\n let effectiveThreshold = this.silenceThreshold / this.micSensitivity;\n if (this.playbackActive) effectiveThreshold *= this.PLAYBACK_GATE_FACTOR;\n if (this.isVoiceActive) effectiveThreshold *= this.HYSTERESIS_FACTOR;\n\n // VAD with reduced sensitivity - require consecutive frames above threshold\n let hasVoice = rms > effectiveThreshold;\n // Calculate time since last voice detection\n const timeSinceLastVoice = currentTime - this.lastVoiceTime;\n\n // Voice detection logic - require consecutive frames above threshold\n if (hasVoice) {\n this.consecutiveSilenceFrames = 0;\n this.voiceFrameCount++; // Increment consecutive voice frame count\n\n // Only activate streaming after required consecutive frames\n if (this.voiceFrameCount >= this.VOICE_FRAMES_REQUIRED) {\n // Start voice if needed\n if (!this.isVoiceActive) {\n this.isVoiceActive = true;\n this.voiceStartTime = currentTime;\n this.isCurrentlyStreaming = true;\n // Log voice detection (every 50 frames = ~400ms to avoid spam)\n if (this.frameCount % 50 === 0) {\n console.log(`🎤 VAD: VOICE DETECTED (RMS: ${rms.toFixed(4)}, frames: ${this.voiceFrameCount})`);\n }\n }\n }\n\n this.lastVoiceTime = currentTime;\n } else {\n // Silence detected - reset voice frame count\n this.voiceFrameCount = 0;\n this.consecutiveSilenceFrames++;\n \n // In continuous mode, we still use VAD but require longer silence before stopping\n // In non-continuous mode, stop quickly\n const silenceThreshold = this.forceContinuous ? 3000 : 200; // 3s for continuous, 200ms otherwise\n \n // FIXED: Stop condition - also check isCurrentlyStreaming (not just isVoiceActive)\n // This handles case where setForceContinuous set streaming=true but no voice was detected yet\n if (!hasVoice && (this.isVoiceActive || this.isCurrentlyStreaming) && timeSinceLastVoice >= silenceThreshold) {\n this.isVoiceActive = false;\n this.isCurrentlyStreaming = false;\n this.voiceStartTime = 0;\n this.lastVoiceTime = 0;\n this.consecutiveSilenceFrames = 0;\n this.voiceFrameCount = 0; // Reset voice frame count\n // Log silence detection\n console.log(`🔇 VAD: SILENCE DETECTED (${timeSinceLastVoice}ms silence, RMS: ${rms.toFixed(4)})`);\n }\n }\n\n // Send PCM **only if streaming and processing** - hard gate\n // This ensures we only send audio when voice is detected, even in continuous mode\n if (this.isCurrentlyStreaming && this.isProcessing) {\n // Log occasionally when sending (every 200 frames = ~1.6 seconds to avoid spam)\n if (this.frameCount % 200 === 0) {\n console.log(`📤 VAD: Sending audio (isVoiceActive: ${this.isVoiceActive}, RMS: ${rms.toFixed(4)})`);\n }\n this.sendPCMAudioData(this.buffer);\n } else {\n // Log occasionally when blocking (every 200 frames)\n if (this.frameCount % 200 === 0 && this.isProcessing) {\n console.log(`🚫 VAD: Blocking audio (isCurrentlyStreaming: ${this.isCurrentlyStreaming}, RMS: ${rms.toFixed(4)})`);\n }\n }\n }\n }\n \n sendPCMAudioData(float32Data) {\n // Convert Float32Array (-1.0 to 1.0) to Int16Array (-32768 to 32767)\n const pcmData = new Int16Array(float32Data.length);\n \n for (let i = 0; i < float32Data.length; i++) {\n // Clamp and convert to 16-bit PCM\n const sample = Math.max(-1.0, Math.min(1.0, float32Data[i]));\n pcmData[i] = Math.round(sample * 32767);\n }\n \n // Initialize send buffer if not exists\n if (!this.sendBuffer) {\n this.sendBuffer = [];\n this.sendBufferBytes = 0;\n }\n \n // Accumulate chunks in buffer\n this.sendBuffer.push(pcmData);\n this.sendBufferBytes += pcmData.byteLength;\n \n // Send in ~4 KB batches (≈128 ms of audio at 16kHz)\n // Use sliding window approach to maintain continuous flow\n while (this.sendBufferBytes >= 4096) {\n // Calculate how many chunks we need for ~4KB\n let chunksToSend = 0;\n let bytesToSend = 0;\n \n for (let i = 0; i < this.sendBuffer.length; i++) {\n const chunkBytes = this.sendBuffer[i].byteLength;\n if (bytesToSend + chunkBytes <= 4096) {\n chunksToSend++;\n bytesToSend += chunkBytes;\n } else {\n break;\n }\n }\n\n // Create merged buffer from selected chunks\n const chunksForBatch = this.sendBuffer.slice(0, chunksToSend);\n const totalSamples = chunksForBatch.reduce((a, b) => a + b.length, 0);\n const merged = new Int16Array(totalSamples);\n let offset = 0;\n \n for (const chunk of chunksForBatch) {\n merged.set(chunk, offset);\n offset += chunk.length;\n }\n \n // Send batched PCM data to main thread\n this.port.postMessage({\n type: 'pcm_audio_data',\n data: merged, // Send the Int16Array directly, not the buffer\n sampleRate: this.sampleRate,\n channelCount: 1,\n frameCount: this.frameCount,\n batchSize: chunksToSend,\n totalBytes: merged.byteLength\n });\n \n // Remove sent chunks from buffer (sliding window)\n this.sendBuffer = this.sendBuffer.slice(chunksToSend);\n this.sendBufferBytes -= bytesToSend;\n }\n }\n \n // Flush any remaining buffered data\n flushBuffer() {\n if (this.sendBuffer && this.sendBuffer.length > 0) {\n // Merge remaining chunks\n const totalSamples = this.sendBuffer.reduce((a, b) => a + b.length, 0);\n const merged = new Int16Array(totalSamples);\n let offset = 0;\n \n for (const chunk of this.sendBuffer) {\n merged.set(chunk, offset);\n offset += chunk.length;\n }\n \n // Send remaining data\n this.port.postMessage({\n type: 'pcm_audio_data',\n data: merged, // Send the Int16Array directly, not the buffer\n sampleRate: this.sampleRate,\n channelCount: 1,\n frameCount: this.frameCount,\n batchSize: this.sendBuffer.length,\n totalBytes: merged.byteLength,\n isFlush: true\n });\n \n // Reset buffer\n this.sendBuffer = [];\n this.sendBufferBytes = 0;\n }\n }\n}\n\n// Register the processor\nregisterProcessor('audio-processor', AudioProcessor);\n";
9173
+ module.exports = "/**\n * AudioProcessor - AudioWorklet for real-time audio processing\n * \n * This AudioWorklet processes audio data in real-time and sends it to the main thread\n * for transmission to the WebSocket server.\n */\n\nclass AudioProcessor extends AudioWorkletProcessor {\n constructor(options) {\n super();\n \n // Configuration\n this.config = options.processorOptions || {};\n // Use AudioContext sampleRate (available as global 'sampleRate' in AudioWorkletProcessor)\n // Fall back to config if sampleRate not available, default to 24 kHz to match typical server/TTS output\n this.sampleRate = typeof sampleRate !== 'undefined' ? sampleRate : (this.config.sampleRate || this.config.outputSampleRate || 24000);\n this.bufferSize = 128; // Process 128 samples at a time (256 bytes = 8ms at 16kHz)\n this.buffer = new Float32Array(this.bufferSize);\n this.bufferIndex = 0;\n \n // VAD (Voice Activity Detection) parameters\n this.silenceThreshold = 0.02; // Base RMS threshold for voice detection (scaled by micSensitivity below)\n this.VOICE_FRAMES_REQUIRED = 2; // Require 2 consecutive frames above threshold before activating\n this.minVoiceDuration = 100; // ms - minimum speech duration\n this.pauseThreshold = 3000; // ms - longer pause before processing\n\n // Mic sensitivity: user/agent-adjustable multiplier on the VAD energy gate.\n // 1.0 = default; 2.0 = twice as sensitive (half the RMS floor); 0.5 = half as\n // sensitive (double the floor — only close/loud speech triggers, e.g. TV in the room).\n this.micSensitivity = (typeof this.config.micSensitivity === 'number' && this.config.micSensitivity > 0)\n ? this.config.micSensitivity : 1.0;\n // While agent audio plays, the gate rises by this factor (stricter barge-in: background\n // TV/chatter must not interrupt playback). The SDK main thread tracks playback state.\n this.playbackActive = false;\n this.PLAYBACK_GATE_FACTOR = 1.6;\n // Hysteresis: once voice is active the floor drops, so a borderline signal isn't\n // chopped on every frame (quiet speech tails stay under the trigger level).\n this.HYSTERESIS_FACTOR = 0.75;\n \n // VAD state\n this.isVoiceActive = false;\n this.voiceStartTime = 0;\n this.lastVoiceTime = 0;\n this.consecutiveSilenceFrames = 0;\n this.silenceFramesThreshold = 5; // More frames needed for silence detection\n this.voiceFrameCount = 0; // Count consecutive frames above threshold\n \n // Audio quality tracking\n this.frameCount = 0;\n this.lastLogTime = 0;\n \n // Continuous recording mode\n this.continuousMode = true; // Always send audio when voice is detected\n this.forceContinuous = true; // Force continuous for toggle button behavior\n this.isCurrentlyStreaming = false; // Track if we're currently sending audio\n \n // Batching buffer\n this.sendBuffer = null;\n this.sendBufferBytes = 0;\n\n // Start true so the first process() call cannot terminate the worklet before\n // the main thread posts setForceContinuous (WebKit/iOS often renders audio before port messages are handled).\n this.isProcessing = true;\n \n // Handle messages from main thread\n this.port.onmessage = (event) => {\n const { type, data } = event.data;\n \n switch (type) {\n case 'start':\n this.isProcessing = true;\n this.isCurrentlyStreaming = true;\n break;\n \n case 'stop':\n this.isProcessing = false;\n this.isCurrentlyStreaming = false;\n this.isVoiceActive = false;\n this.forceContinuous = false;\n this.voiceFrameCount = 0; // Reset voice frame count\n // Flush any remaining data\n this.flushBuffer();\n break;\n \n case 'setForceContinuous':\n this.forceContinuous = data.enabled;\n this.isProcessing = true;\n // iOS: bypass client VAD - always send. Desktop: keep VAD (saves bandwidth, reduces noise).\n this.isCurrentlyStreaming = data.enabled && (data.bypassVad === true);\n break;\n\n case 'setMicSensitivity': {\n const v = Number(data && data.value);\n if (isFinite(v) && v > 0) {\n this.micSensitivity = Math.max(0.25, Math.min(4.0, v));\n }\n break;\n }\n\n case 'setPlaybackActive':\n this.playbackActive = !!(data && data.active);\n break;\n \n case 'flush':\n this.flushBuffer();\n break;\n \n case 'config':\n Object.assign(this.config, data);\n break;\n }\n };\n }\n \n /**\n * Process audio data\n */\n process(inputs, outputs, parameters) {\n // CRITICAL: If processing is stopped, terminate the processor\n if (!this.isProcessing) {\n // Return false to terminate the AudioWorklet processor\n // This stops all VAD processing immediately\n return false;\n }\n \n const input = inputs[0];\n const output = outputs[0];\n \n // Copy input to output (pass-through)\n if (input.length > 0 && output.length > 0) {\n output[0].set(input[0]);\n }\n \n // Process audio for PCM recording and VAD\n if (input.length > 0 && input[0].length > 0) {\n this.processAudioData(input[0]);\n }\n \n // Keep the processor alive\n return true;\n }\n \n processAudioData(audioData) {\n // CRITICAL: Early return if processing is stopped\n // This prevents VAD calculations and logging when stopped\n if (!this.isProcessing) {\n return;\n }\n \n this.frameCount++;\n \n // Process audio in consistent 128-sample chunks (256 bytes)\n for (let i = 0; i < audioData.length; i += this.bufferSize) {\n const chunkSize = Math.min(this.bufferSize, audioData.length - i);\n \n // Copy chunk to buffer\n for (let j = 0; j < chunkSize; j++) {\n this.buffer[j] = audioData[i + j];\n }\n \n // Pad with zeros if needed\n for (let j = chunkSize; j < this.bufferSize; j++) {\n this.buffer[j] = 0;\n }\n \n // Calculate RMS for VAD on this chunk\n let sum = 0;\n for (let j = 0; j < this.bufferSize; j++) {\n sum += this.buffer[j] * this.buffer[j];\n }\n const rms = Math.sqrt(sum / this.bufferSize);\n \n // Calculate additional features for better VAD\n let variation = 0;\n let highFreqCount = 0;\n for (let j = 1; j < this.bufferSize; j++) {\n const diff = Math.abs(this.buffer[j] - this.buffer[j-1]);\n variation += diff;\n if (diff > 0.1) highFreqCount++;\n }\n variation = variation / this.bufferSize;\n const highFreqRatio = highFreqCount / this.bufferSize;\n \n const currentTime = Date.now();\n\n // Effective VAD floor: base threshold scaled by the mic-sensitivity multiplier —\n // QUADRATIC below 1.0 so the strict end actually bites (0.5 → 0.08 floor, not\n // 0.04: a linear ±6dB band is imperceptible, and browser AGC flattens level\n // differences before we see them). Raised during agent playback (stricter\n // barge-in), lowered while voice is already active (hysteresis, so speech\n // tails aren't clipped).\n const sens = this.micSensitivity;\n let effectiveThreshold = sens >= 1\n ? this.silenceThreshold / sens\n : this.silenceThreshold / (sens * sens);\n if (this.playbackActive) effectiveThreshold *= this.PLAYBACK_GATE_FACTOR;\n if (this.isVoiceActive) effectiveThreshold *= this.HYSTERESIS_FACTOR;\n\n // VAD with reduced sensitivity - require consecutive frames above threshold\n let hasVoice = rms > effectiveThreshold;\n // Calculate time since last voice detection\n const timeSinceLastVoice = currentTime - this.lastVoiceTime;\n\n // Voice detection logic - require consecutive frames above threshold\n if (hasVoice) {\n this.consecutiveSilenceFrames = 0;\n this.voiceFrameCount++; // Increment consecutive voice frame count\n\n // Only activate streaming after required consecutive frames\n if (this.voiceFrameCount >= this.VOICE_FRAMES_REQUIRED) {\n // Start voice if needed\n if (!this.isVoiceActive) {\n this.isVoiceActive = true;\n this.voiceStartTime = currentTime;\n this.isCurrentlyStreaming = true;\n // Log voice detection (every 50 frames = ~400ms to avoid spam)\n if (this.frameCount % 50 === 0) {\n console.log(`🎤 VAD: VOICE DETECTED (RMS: ${rms.toFixed(4)}, frames: ${this.voiceFrameCount})`);\n }\n }\n }\n\n this.lastVoiceTime = currentTime;\n } else {\n // Silence detected - reset voice frame count\n this.voiceFrameCount = 0;\n this.consecutiveSilenceFrames++;\n \n // In continuous mode, we still use VAD but require longer silence before stopping\n // In non-continuous mode, stop quickly\n const silenceThreshold = this.forceContinuous ? 3000 : 200; // 3s for continuous, 200ms otherwise\n \n // FIXED: Stop condition - also check isCurrentlyStreaming (not just isVoiceActive)\n // This handles case where setForceContinuous set streaming=true but no voice was detected yet\n if (!hasVoice && (this.isVoiceActive || this.isCurrentlyStreaming) && timeSinceLastVoice >= silenceThreshold) {\n this.isVoiceActive = false;\n this.isCurrentlyStreaming = false;\n this.voiceStartTime = 0;\n this.lastVoiceTime = 0;\n this.consecutiveSilenceFrames = 0;\n this.voiceFrameCount = 0; // Reset voice frame count\n // Log silence detection\n console.log(`🔇 VAD: SILENCE DETECTED (${timeSinceLastVoice}ms silence, RMS: ${rms.toFixed(4)})`);\n }\n }\n\n // Send PCM **only if streaming and processing** - hard gate\n // This ensures we only send audio when voice is detected, even in continuous mode\n if (this.isCurrentlyStreaming && this.isProcessing) {\n // Log occasionally when sending (every 200 frames = ~1.6 seconds to avoid spam)\n if (this.frameCount % 200 === 0) {\n console.log(`📤 VAD: Sending audio (isVoiceActive: ${this.isVoiceActive}, RMS: ${rms.toFixed(4)})`);\n }\n this.sendPCMAudioData(this.buffer);\n } else {\n // Log occasionally when blocking (every 200 frames)\n if (this.frameCount % 200 === 0 && this.isProcessing) {\n console.log(`🚫 VAD: Blocking audio (isCurrentlyStreaming: ${this.isCurrentlyStreaming}, RMS: ${rms.toFixed(4)})`);\n }\n }\n }\n }\n \n sendPCMAudioData(float32Data) {\n // Convert Float32Array (-1.0 to 1.0) to Int16Array (-32768 to 32767)\n const pcmData = new Int16Array(float32Data.length);\n \n for (let i = 0; i < float32Data.length; i++) {\n // Clamp and convert to 16-bit PCM\n const sample = Math.max(-1.0, Math.min(1.0, float32Data[i]));\n pcmData[i] = Math.round(sample * 32767);\n }\n \n // Initialize send buffer if not exists\n if (!this.sendBuffer) {\n this.sendBuffer = [];\n this.sendBufferBytes = 0;\n }\n \n // Accumulate chunks in buffer\n this.sendBuffer.push(pcmData);\n this.sendBufferBytes += pcmData.byteLength;\n \n // Send in ~4 KB batches (≈128 ms of audio at 16kHz)\n // Use sliding window approach to maintain continuous flow\n while (this.sendBufferBytes >= 4096) {\n // Calculate how many chunks we need for ~4KB\n let chunksToSend = 0;\n let bytesToSend = 0;\n \n for (let i = 0; i < this.sendBuffer.length; i++) {\n const chunkBytes = this.sendBuffer[i].byteLength;\n if (bytesToSend + chunkBytes <= 4096) {\n chunksToSend++;\n bytesToSend += chunkBytes;\n } else {\n break;\n }\n }\n\n // Create merged buffer from selected chunks\n const chunksForBatch = this.sendBuffer.slice(0, chunksToSend);\n const totalSamples = chunksForBatch.reduce((a, b) => a + b.length, 0);\n const merged = new Int16Array(totalSamples);\n let offset = 0;\n \n for (const chunk of chunksForBatch) {\n merged.set(chunk, offset);\n offset += chunk.length;\n }\n \n // Send batched PCM data to main thread\n this.port.postMessage({\n type: 'pcm_audio_data',\n data: merged, // Send the Int16Array directly, not the buffer\n sampleRate: this.sampleRate,\n channelCount: 1,\n frameCount: this.frameCount,\n batchSize: chunksToSend,\n totalBytes: merged.byteLength\n });\n \n // Remove sent chunks from buffer (sliding window)\n this.sendBuffer = this.sendBuffer.slice(chunksToSend);\n this.sendBufferBytes -= bytesToSend;\n }\n }\n \n // Flush any remaining buffered data\n flushBuffer() {\n if (this.sendBuffer && this.sendBuffer.length > 0) {\n // Merge remaining chunks\n const totalSamples = this.sendBuffer.reduce((a, b) => a + b.length, 0);\n const merged = new Int16Array(totalSamples);\n let offset = 0;\n \n for (const chunk of this.sendBuffer) {\n merged.set(chunk, offset);\n offset += chunk.length;\n }\n \n // Send remaining data\n this.port.postMessage({\n type: 'pcm_audio_data',\n data: merged, // Send the Int16Array directly, not the buffer\n sampleRate: this.sampleRate,\n channelCount: 1,\n frameCount: this.frameCount,\n batchSize: this.sendBuffer.length,\n totalBytes: merged.byteLength,\n isFlush: true\n });\n \n // Reset buffer\n this.sendBuffer = [];\n this.sendBufferBytes = 0;\n }\n }\n}\n\n// Register the processor\nregisterProcessor('audio-processor', AudioProcessor);\n";
9174
9174
 
9175
9175
  /***/ }),
9176
9176
 
@@ -10073,6 +10073,15 @@ var AudioRecorder = /*#__PURE__*/function (_EventEmitter) {
10073
10073
  case 8:
10074
10074
  this.mediaStream = _context4.v;
10075
10075
  case 9:
10076
+ // Fresh track: it always starts with the getUserMedia defaults (AGC on), so a
10077
+ // persisted low sensitivity must re-apply its AGC-off constraint on it.
10078
+ if (typeof this.config.micSensitivity === 'number' && this.config.micSensitivity < 0.999) {
10079
+ this._agcApplied = undefined;
10080
+ this._applyAgcForSensitivity(this.config.micSensitivity);
10081
+ }
10082
+
10083
+ // CRITICAL: Check connection status AFTER getting permission but BEFORE creating AudioWorkletNode
10084
+ // Server might have rejected during the async permission request
10076
10085
  if (!(this.config.checkConnection && typeof this.config.checkConnection === 'function')) {
10077
10086
  _context4.n = 10;
10078
10087
  break;
@@ -10354,6 +10363,39 @@ var AudioRecorder = /*#__PURE__*/function (_EventEmitter) {
10354
10363
  }
10355
10364
  });
10356
10365
  }
10366
+ this._applyAgcForSensitivity(clamped);
10367
+ }
10368
+
10369
+ /**
10370
+ * AGC fights the sensitivity gate: it re-amplifies quiet background noise (a TV
10371
+ * across the room) toward speech level before the worklet's VAD ever measures it,
10372
+ * so a lower floor alone changes nothing. Below sensitivity 1.0 we disable auto
10373
+ * gain control on the live track (Chrome supports applyConstraints without a
10374
+ * stream restart); at/above 1.0 the platform default (AGC on) is restored.
10375
+ */
10376
+ }, {
10377
+ key: "_applyAgcForSensitivity",
10378
+ value: function _applyAgcForSensitivity(sensitivity) {
10379
+ var _this$mediaStream,
10380
+ _this$mediaStream$get,
10381
+ _track$getCapabilitie,
10382
+ _this3 = this;
10383
+ var wantAgc = !(typeof sensitivity === 'number' && sensitivity < 0.999);
10384
+ if (this._agcApplied === wantAgc) return;
10385
+ var track = (_this$mediaStream = this.mediaStream) === null || _this$mediaStream === void 0 || (_this$mediaStream$get = _this$mediaStream.getAudioTracks) === null || _this$mediaStream$get === void 0 ? void 0 : _this$mediaStream$get.call(_this$mediaStream)[0];
10386
+ if (!track || track.readyState !== 'live' || typeof track.applyConstraints !== 'function') return;
10387
+ var caps = (_track$getCapabilitie = track.getCapabilities) === null || _track$getCapabilitie === void 0 ? void 0 : _track$getCapabilitie.call(track);
10388
+ if (caps !== null && caps !== void 0 && caps.autoGainControl && !caps.autoGainControl.includes(wantAgc)) return;
10389
+ // Mark before the async call so rapid slider moves don't stack constraint calls.
10390
+ this._agcApplied = wantAgc;
10391
+ track.applyConstraints({
10392
+ autoGainControl: wantAgc
10393
+ }).then(function () {
10394
+ return console.log("\uD83C\uDF99\uFE0F AudioRecorder: autoGainControl ".concat(wantAgc ? 'ON (default)' : 'OFF (low mic sensitivity)'));
10395
+ }).catch(function (e) {
10396
+ _this3._agcApplied = undefined;
10397
+ console.warn('⚠️ AudioRecorder: applyConstraints(autoGainControl) failed:', e);
10398
+ });
10357
10399
  }
10358
10400
 
10359
10401
  /**
@@ -12087,7 +12129,7 @@ var TextChatSDK = /*#__PURE__*/function (_EventEmitter) {
12087
12129
 
12088
12130
  // SDK build time for debugging
12089
12131
  if (true) {
12090
- helloMessage.lastBuildTime = "2026-08-21T10:23:43.383Z";
12132
+ helloMessage.lastBuildTime = "2026-08-21T10:43:17.433Z";
12091
12133
  }
12092
12134
  try {
12093
12135
  this.ws.send(JSON.stringify(helloMessage));
@@ -21397,8 +21439,8 @@ var VoiceSDK = _v2_VoiceSDK_js__WEBPACK_IMPORTED_MODULE_0__["default"];
21397
21439
 
21398
21440
 
21399
21441
  // Version - injected at build time from package.json via webpack DefinePlugin
21400
- var VERSION = "2.48.14";
21401
- var BUILD_TIME = "2026-08-21T10:23:43.383Z";
21442
+ var VERSION = "2.48.16";
21443
+ var BUILD_TIME = "2026-08-21T10:43:17.433Z";
21402
21444
  console.log("%c TTP Agent SDK v".concat(VERSION, " (").concat(BUILD_TIME, ") "), 'background: #4f46e5; color: white; font-size: 12px; font-weight: bold; padding: 2px 6px; border-radius: 4px;');
21403
21445
 
21404
21446
  // Named exports
@@ -29375,7 +29417,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29375
29417
  // iOS in-app webviews lack "Safari" in the UA (real Safari always has it).
29376
29418
  var webview = isAndroid && (/\bwv\b/.test(ua) || /Version\/[\d.]+.*Chrome/.test(ua)) || isIos && !/Safari/i.test(ua) || false;
29377
29419
  var env = {
29378
- sdkVersion: true ? "2.48.14" : 0,
29420
+ sdkVersion: true ? "2.48.16" : 0,
29379
29421
  ua: ua,
29380
29422
  platform: (uaData === null || uaData === void 0 ? void 0 : uaData.platform) || navigator.platform || '',
29381
29423
  mobile: (_uaData$mobile = uaData === null || uaData === void 0 ? void 0 : uaData.mobile) !== null && _uaData$mobile !== void 0 ? _uaData$mobile : isAndroid || isIos,
@@ -29390,7 +29432,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29390
29432
  } catch (e) {
29391
29433
  console.warn('⚠️ VoiceSDK v2: Failed to build client env:', e);
29392
29434
  return {
29393
- sdkVersion: true ? "2.48.14" : 0
29435
+ sdkVersion: true ? "2.48.16" : 0
29394
29436
  };
29395
29437
  }
29396
29438
  }
@@ -29532,7 +29574,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29532
29574
 
29533
29575
  // Include SDK build time for debugging
29534
29576
  if (true) {
29535
- helloMessage.lastBuildTime = "2026-08-21T10:23:43.383Z";
29577
+ helloMessage.lastBuildTime = "2026-08-21T10:43:17.433Z";
29536
29578
  }
29537
29579
 
29538
29580
  // Client environment (device/browser/webview) for backend logs + Langfuse metadata
@@ -35761,7 +35803,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
35761
35803
  return;
35762
35804
  }
35763
35805
  this._ensureAboutStyles();
35764
- var version = true ? "2.48.14" : 0;
35806
+ var version = true ? "2.48.16" : 0;
35765
35807
  var convId = this._getLastConversationId();
35766
35808
  var t = function t(k, fb) {
35767
35809
  try {