ttp-agent-sdk 2.48.14 → 2.48.18

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-27T12:25:20.756Z";
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.18";
21443
+ var BUILD_TIME = "2026-08-27T12:25:20.756Z";
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
@@ -25573,6 +25615,7 @@ __webpack_require__.r(__webpack_exports__);
25573
25615
  /* harmony import */ var _codecs_PCMCodec_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./codecs/PCMCodec.js */ "./src/v2/codecs/PCMCodec.js");
25574
25616
  /* harmony import */ var _codecs_PCMUCodec_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./codecs/PCMUCodec.js */ "./src/v2/codecs/PCMUCodec.js");
25575
25617
  /* harmony import */ var _codecs_PCMACodec_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./codecs/PCMACodec.js */ "./src/v2/codecs/PCMACodec.js");
25618
+ /* harmony import */ var _hopPlatform_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./hopPlatform.js */ "./src/v2/hopPlatform.js");
25576
25619
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
25577
25620
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
25578
25621
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -25604,6 +25647,7 @@ function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf
25604
25647
 
25605
25648
 
25606
25649
 
25650
+
25607
25651
  // iOS FIX: Shared AudioContext for playback that persists across AudioPlayer instances.
25608
25652
  // iOS WebKit doesn't release audio hardware synchronously when AudioContext.close() is called,
25609
25653
  // so creating a new AudioContext immediately after closing the old one can fail silently.
@@ -25703,6 +25747,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
25703
25747
  _this._mediaStreamDest = null;
25704
25748
  _this._htmlAudioRouteActive = false;
25705
25749
  _this._htmlAudioPlayOk = false;
25750
+ _this._htmlAudioPlayWatchdog = null;
25706
25751
  _this._loggedHopResample = false;
25707
25752
  // Greeting (and other bursts) fire many playChunk()s without await.
25708
25753
  // Serialize context creation so they cannot open two AudioContexts
@@ -27122,37 +27167,40 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27122
27167
  }, {
27123
27168
  key: "wantsHtmlAudioPlayback",
27124
27169
  value: function wantsHtmlAudioPlayback() {
27125
- // Debug/QA override: ?ttp_hop=0|1 or localStorage 'ttp_hop' forces the route
27126
- // at the decision point, regardless of which config object reached us.
27127
- try {
27128
- var _URLSearchParams$get;
27129
- var flag = (_URLSearchParams$get = new URLSearchParams(window.location.search).get('ttp_hop')) !== null && _URLSearchParams$get !== void 0 ? _URLSearchParams$get : window.localStorage.getItem('ttp_hop');
27130
- if (flag === '0' || flag === '1') {
27131
- return flag === '1';
27132
- }
27133
- } catch (e) {/* non-browser env */}
27170
+ // Debug/QA override: ?ttp_hop=0|1, its sessionStorage mirror (survives
27171
+ // same-origin full page loads that drop the query string Borusan's
27172
+ // navigate_to_form), or localStorage 'ttp_hop'. Wins over config.
27173
+ var override = (0,_hopPlatform_js__WEBPACK_IMPORTED_MODULE_4__.readTtpHopOverride)(typeof window !== 'undefined' ? window : null);
27174
+ if (override !== null) {
27175
+ return override;
27176
+ }
27134
27177
  if (typeof this.config.htmlAudioPlayback === 'boolean') {
27135
27178
  return this.config.htmlAudioPlayback;
27136
27179
  }
27137
- // Default: the hop (AudioContext → MediaStreamDestination → HTMLAudioElement)
27138
- // gives Chrome's AEC a far-end reference, but Chrome plays that live stream
27139
- // with adaptive jitter-buffer speed control, which audibly wobbles on
27140
- // desktop Linux (PipeWire/PulseAudio). Off there, on elsewhere.
27141
- var hopOn = !this._isDesktopLinux();
27180
+ // Default: HTML hop (AudioContext → MediaStreamDestination → <audio>) gives
27181
+ // Safari/Chrome a far-end AEC reference on Apple mobile + macOS where Web
27182
+ // Audio destination is not in the echo-cancellation loop. On Windows and
27183
+ // Linux desktop, Chrome-wide AEC already covers speaker echo; the hop only
27184
+ // adds adaptive live-stream speed wobble — keep it off there (same as Linux).
27185
+ var hopOn = this._needsHtmlAudioHopForAec();
27142
27186
  if (!this._hopDefaultLogged) {
27143
27187
  this._hopDefaultLogged = true;
27144
- console.log("\uD83D\uDD0A AudioPlayer: htmlAudioPlayback default \u2192 ".concat(hopOn ? 'on (AEC far-end route)' : 'off (desktop Linux: hop causes playback speed wobble)'));
27188
+ var reason = hopOn ? 'on (mobile / macOS: AEC far-end route)' : 'off (desktop Windows/Linux: stable direct Web Audio playback)';
27189
+ console.log("\uD83D\uDD0A AudioPlayer: htmlAudioPlayback default \u2192 ".concat(reason));
27145
27190
  }
27146
27191
  return hopOn;
27147
27192
  }
27193
+
27194
+ /** Hop default on only where Web Audio destination lacks AEC far-end reference.
27195
+ * Policy lives in hopPlatform.js (pure, unit-tested); this only supplies the
27196
+ * environment strings. */
27148
27197
  }, {
27149
- key: "_isDesktopLinux",
27150
- value: function _isDesktopLinux() {
27198
+ key: "_needsHtmlAudioHopForAec",
27199
+ value: function _needsHtmlAudioHopForAec() {
27151
27200
  try {
27152
27201
  var ua = navigator.userAgent || '';
27153
- if (/Android|iPhone|iPad|iPod/i.test(ua)) return false;
27154
27202
  var platform = navigator.userAgentData && navigator.userAgentData.platform || navigator.platform || '';
27155
- return /linux/i.test(platform) || /Linux|X11/i.test(ua);
27203
+ return (0,_hopPlatform_js__WEBPACK_IMPORTED_MODULE_4__.needsHtmlAudioHopForAec)(ua, platform);
27156
27204
  } catch (e) {
27157
27205
  return false;
27158
27206
  }
@@ -27247,6 +27295,10 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27247
27295
  }
27248
27296
  if (!this.audioContext || typeof this.audioContext.createMediaStreamDestination !== 'function') {
27249
27297
  console.warn('⚠️ AudioPlayer: MediaStreamDestination unavailable — using AudioContext.destination');
27298
+ this.emit('htmlAudioRoute', {
27299
+ state: 'fallback',
27300
+ reason: 'no_media_stream_destination'
27301
+ });
27250
27302
  return false;
27251
27303
  }
27252
27304
  try {
@@ -27264,31 +27316,72 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27264
27316
  this._htmlAudioEl = el;
27265
27317
  this._htmlAudioRouteActive = true;
27266
27318
  this._ensureHtmlAudioPlaying();
27319
+ this._armHtmlAudioPlayWatchdog(el);
27267
27320
  console.log('🔊 AudioPlayer: Playback last-hop is HTMLAudioElement (AEC far-end route)');
27268
27321
  return true;
27269
27322
  } catch (e) {
27270
27323
  console.warn('⚠️ AudioPlayer: HTML audio route failed, falling back to destination:', e);
27271
27324
  this._teardownHtmlAudioRoute();
27325
+ this.emit('htmlAudioRoute', {
27326
+ state: 'fallback',
27327
+ reason: 'route_init_failed'
27328
+ });
27272
27329
  return false;
27273
27330
  }
27274
27331
  }
27332
+
27333
+ /**
27334
+ * play() on a MediaStream-backed element can stay pending forever on some
27335
+ * WebKit builds — neither resolving nor rejecting. If the element is still
27336
+ * paused shortly after route creation, report it: the hop never engaged and
27337
+ * AEC has no far-end reference, but audio still reaches the user via the
27338
+ * (silent-failure) direct path only if a fallback happened — so this state
27339
+ * means "route exists, playback through it unconfirmed".
27340
+ */
27341
+ }, {
27342
+ key: "_armHtmlAudioPlayWatchdog",
27343
+ value: function _armHtmlAudioPlayWatchdog(el) {
27344
+ var _this7 = this;
27345
+ this._clearHtmlAudioPlayWatchdog();
27346
+ this._htmlAudioPlayWatchdog = setTimeout(function () {
27347
+ _this7._htmlAudioPlayWatchdog = null;
27348
+ if (_this7._htmlAudioRouteActive && _this7._htmlAudioEl === el && !_this7._htmlAudioPlayOk && el.paused) {
27349
+ console.warn('⚠️ AudioPlayer: HTMLAudioElement.play() still pending after 2.5s — hop engagement unconfirmed');
27350
+ _this7.emit('htmlAudioRoute', {
27351
+ state: 'play_pending'
27352
+ });
27353
+ }
27354
+ }, 2500);
27355
+ }
27356
+ }, {
27357
+ key: "_clearHtmlAudioPlayWatchdog",
27358
+ value: function _clearHtmlAudioPlayWatchdog() {
27359
+ if (this._htmlAudioPlayWatchdog) {
27360
+ clearTimeout(this._htmlAudioPlayWatchdog);
27361
+ this._htmlAudioPlayWatchdog = null;
27362
+ }
27363
+ }
27275
27364
  }, {
27276
27365
  key: "_ensureHtmlAudioPlaying",
27277
27366
  value: function _ensureHtmlAudioPlaying() {
27278
- var _this7 = this;
27367
+ var _this8 = this;
27279
27368
  var el = this._htmlAudioEl;
27280
27369
  if (!el) return;
27281
27370
  if (!el.paused && this._htmlAudioPlayOk) return;
27282
27371
  var playResult = el.play();
27283
27372
  if (playResult && typeof playResult.then === 'function') {
27284
27373
  playResult.then(function () {
27285
- _this7._htmlAudioPlayOk = true;
27374
+ _this8._htmlAudioPlayOk = true;
27375
+ _this8._clearHtmlAudioPlayWatchdog();
27376
+ _this8.emit('htmlAudioRoute', {
27377
+ state: 'established'
27378
+ });
27286
27379
  }).catch(function (err) {
27287
- if (_this7._htmlAudioPlayOk || !el.paused) {
27380
+ if (_this8._htmlAudioPlayOk || !el.paused) {
27288
27381
  return;
27289
27382
  }
27290
27383
  console.warn('⚠️ AudioPlayer: HTMLAudioElement.play() rejected — falling back to destination:', err);
27291
- _this7._fallbackToDestination('play_rejected');
27384
+ _this8._fallbackToDestination('play_rejected');
27292
27385
  });
27293
27386
  }
27294
27387
  }
@@ -27302,10 +27395,15 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27302
27395
  this._teardownHtmlAudioRoute();
27303
27396
  this.gainNode.connect(this.audioContext.destination);
27304
27397
  console.warn("\u26A0\uFE0F AudioPlayer: Fell back to AudioContext.destination (".concat(reason, ")"));
27398
+ this.emit('htmlAudioRoute', {
27399
+ state: 'fallback',
27400
+ reason: reason
27401
+ });
27305
27402
  }
27306
27403
  }, {
27307
27404
  key: "_teardownHtmlAudioRoute",
27308
27405
  value: function _teardownHtmlAudioRoute() {
27406
+ this._clearHtmlAudioPlayWatchdog();
27309
27407
  if (this._htmlAudioEl) {
27310
27408
  try {
27311
27409
  this._htmlAudioEl.pause();
@@ -27329,7 +27427,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27329
27427
  key: "initializeAudioContext",
27330
27428
  value: (function () {
27331
27429
  var _initializeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
27332
- var _this8 = this;
27430
+ var _this9 = this;
27333
27431
  return _regenerator().w(function (_context8) {
27334
27432
  while (1) switch (_context8.n) {
27335
27433
  case 0:
@@ -27340,7 +27438,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27340
27438
  return _context8.a(2, this._audioContextInitPromise);
27341
27439
  case 1:
27342
27440
  this._audioContextInitPromise = this._initializeAudioContextImpl().finally(function () {
27343
- _this8._audioContextInitPromise = null;
27441
+ _this9._audioContextInitPromise = null;
27344
27442
  });
27345
27443
  return _context8.a(2, this._audioContextInitPromise);
27346
27444
  }
@@ -27355,7 +27453,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27355
27453
  key: "_initializeAudioContextImpl",
27356
27454
  value: function () {
27357
27455
  var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
27358
- var _this9 = this;
27456
+ var _this0 = this;
27359
27457
  var ttsSampleRate, useHardwareRate, desiredSampleRate, contextFits, canReuseShared, setupAfterResume, ctxOpts, _t4;
27360
27458
  return _regenerator().w(function (_context9) {
27361
27459
  while (1) switch (_context9.p = _context9.n) {
@@ -27376,7 +27474,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27376
27474
  _sharedPlayerSampleRate = null;
27377
27475
  }
27378
27476
  contextFits = function contextFits(rate) {
27379
- if (useHardwareRate) return _this9._isHardwarePlaybackRate(rate);
27477
+ if (useHardwareRate) return _this0._isHardwarePlaybackRate(rate);
27380
27478
  return Math.abs(rate - desiredSampleRate) <= 100;
27381
27479
  }; // Check if current instance AudioContext exists and matches
27382
27480
  if (!this.audioContext) {
@@ -27413,19 +27511,19 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27413
27511
  console.log("\u267B\uFE0F AudioPlayer: Reusing shared AudioContext at ".concat(_sharedPlayerContext.sampleRate, "Hz (iOS-safe)"));
27414
27512
  this.audioContext = _sharedPlayerContext;
27415
27513
  setupAfterResume = function setupAfterResume() {
27416
- _this9.setupAudioContextStateMonitoring();
27417
- if (_this9.gainNode) {
27514
+ _this0.setupAudioContextStateMonitoring();
27515
+ if (_this0.gainNode) {
27418
27516
  try {
27419
- _this9.gainNode.disconnect();
27517
+ _this0.gainNode.disconnect();
27420
27518
  } catch (e) {
27421
27519
  console.warn('⚠️ AudioPlayer: Error disconnecting old GainNode:', e);
27422
27520
  }
27423
27521
  }
27424
- _this9.gainNode = _this9.audioContext.createGain();
27425
- _this9.gainNode.gain.value = 1.0;
27426
- _this9._connectGainToOutput();
27427
- if (!_this9._audioContextPrimed) {
27428
- _this9._primeAudioContext();
27522
+ _this0.gainNode = _this0.audioContext.createGain();
27523
+ _this0.gainNode.gain.value = 1.0;
27524
+ _this0._connectGainToOutput();
27525
+ if (!_this0._audioContextPrimed) {
27526
+ _this0._primeAudioContext();
27429
27527
  }
27430
27528
  };
27431
27529
  if (!(this.audioContext.state === 'suspended')) {
@@ -27617,7 +27715,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27617
27715
  * Handles mic permission grants, tab switching, browser suspension, etc.
27618
27716
  */
27619
27717
  function setupAudioContextStateMonitoring() {
27620
- var _this0 = this;
27718
+ var _this1 = this;
27621
27719
  if (!this.audioContext) {
27622
27720
  return;
27623
27721
  }
@@ -27630,38 +27728,38 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27630
27728
  // Create handler that references this.audioContext dynamically
27631
27729
  this._audioContextStateChangeHandler = function () {
27632
27730
  // Null check required because audioContext may be cleaned up while handler is queued
27633
- if (!_this0.audioContext) {
27731
+ if (!_this1.audioContext) {
27634
27732
  console.warn('⚠️ AudioPlayer: State change handler fired but AudioContext is null');
27635
27733
  return;
27636
27734
  }
27637
- console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this0.audioContext.state));
27638
- if (_this0.audioContext.state === 'running') {
27639
- _this0._ensureHtmlAudioPlaying();
27735
+ console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this1.audioContext.state));
27736
+ if (_this1.audioContext.state === 'running') {
27737
+ _this1._ensureHtmlAudioPlaying();
27640
27738
  }
27641
- if (_this0.audioContext.state === 'suspended' && _this0.isPlaying) {
27739
+ if (_this1.audioContext.state === 'suspended' && _this1.isPlaying) {
27642
27740
  // AudioContext was suspended during playback (tab switch, mic permission, etc.)
27643
27741
  console.warn('⚠️ AudioPlayer: AudioContext suspended during playback');
27644
27742
  // Note: Playback will pause automatically, but we should handle queue processing
27645
27743
  // The state change will be handled when we try to process next frame
27646
- } else if (_this0.audioContext.state === 'running' && !_this0.isPlaying && (_this0.audioQueue.length > 0 || _this0.pcmChunkQueue.length > 0 || _this0.preparedBuffer.length > 0)) {
27744
+ } else if (_this1.audioContext.state === 'running' && !_this1.isPlaying && (_this1.audioQueue.length > 0 || _this1.pcmChunkQueue.length > 0 || _this1.preparedBuffer.length > 0)) {
27647
27745
  // AudioContext resumed and we have queued frames
27648
27746
  // This handles: mic permission grant, tab switching back, browser resume, etc.
27649
27747
  console.log('✅ AudioPlayer: AudioContext resumed - resuming queue processing');
27650
27748
 
27651
27749
  // Resume queue processing if we have frames
27652
- if (_this0.audioQueue.length > 0 && !_this0.isProcessingQueue) {
27750
+ if (_this1.audioQueue.length > 0 && !_this1.isProcessingQueue) {
27653
27751
  setTimeout(function () {
27654
- return _this0.processQueue();
27752
+ return _this1.processQueue();
27655
27753
  }, 50);
27656
27754
  }
27657
- if (_this0.pcmChunkQueue.length > 0 && !_this0.isProcessingPcmQueue) {
27755
+ if (_this1.pcmChunkQueue.length > 0 && !_this1.isProcessingPcmQueue) {
27658
27756
  setTimeout(function () {
27659
- return _this0.processPcmQueue();
27757
+ return _this1.processPcmQueue();
27660
27758
  }, 50);
27661
27759
  }
27662
- if (_this0.preparedBuffer.length > 0 && !_this0.isSchedulingFrames) {
27760
+ if (_this1.preparedBuffer.length > 0 && !_this1.isSchedulingFrames) {
27663
27761
  setTimeout(function () {
27664
- return _this0.scheduleFrames();
27762
+ return _this1.scheduleFrames();
27665
27763
  }, 50);
27666
27764
  }
27667
27765
  }
@@ -27680,7 +27778,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27680
27778
  key: "processQueue",
27681
27779
  value: (function () {
27682
27780
  var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27683
- var _this1 = this;
27781
+ var _this10 = this;
27684
27782
  var audioBlob, wasFirstPlay, audioContext, arrayBuffer, audioBuffer, shouldEmitStart, source, _t6;
27685
27783
  return _regenerator().w(function (_context1) {
27686
27784
  while (1) switch (_context1.p = _context1.n) {
@@ -27740,22 +27838,22 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27740
27838
  // Handle end
27741
27839
 
27742
27840
  source.onended = function () {
27743
- _this1.currentSource = null;
27744
- _this1.isProcessingQueue = false;
27841
+ _this10.currentSource = null;
27842
+ _this10.isProcessingQueue = false;
27745
27843
 
27746
27844
  // Process next chunk
27747
27845
 
27748
- if (_this1.audioQueue.length > 0) {
27846
+ if (_this10.audioQueue.length > 0) {
27749
27847
  setTimeout(function () {
27750
- return _this1.processQueue();
27848
+ return _this10.processQueue();
27751
27849
  }, 50);
27752
27850
  } else {
27753
27851
  // No more chunks - stop after delay
27754
27852
 
27755
27853
  setTimeout(function () {
27756
- if (_this1.audioQueue.length === 0 && !_this1.currentSource) {
27757
- _this1.isPlaying = false;
27758
- _this1.emit('playbackStopped');
27854
+ if (_this10.audioQueue.length === 0 && !_this10.currentSource) {
27855
+ _this10.isPlaying = false;
27856
+ _this10.emit('playbackStopped');
27759
27857
  }
27760
27858
  }, 100);
27761
27859
  }
@@ -27778,7 +27876,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27778
27876
  if (this.audioQueue.length > 0) {
27779
27877
  this.isProcessingQueue = false;
27780
27878
  setTimeout(function () {
27781
- return _this1.processQueue();
27879
+ return _this10.processQueue();
27782
27880
  }, 100);
27783
27881
  } else {
27784
27882
  this.isPlaying = false;
@@ -28001,7 +28099,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28001
28099
  }, {
28002
28100
  key: "markNewSentence",
28003
28101
  value: function markNewSentence(text, synced, segmentId) {
28004
- var _this10 = this;
28102
+ var _this11 = this;
28005
28103
  var wasStopped = this._isStopped;
28006
28104
  var isCurrentlyPlaying = this.isPlaying || this.scheduledSources.size > 0;
28007
28105
 
@@ -28060,34 +28158,34 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28060
28158
  var sentenceText = text; // Capture for timeout callback
28061
28159
  this._emptySentenceTimeout = setTimeout(function () {
28062
28160
  // Check if this sentence still has no chunks after timeout
28063
- if (_this10.pendingSentenceText === sentenceText && _this10.scheduledBuffers === 0 && _this10.preparedBuffer.length === 0 && _this10.pcmChunkQueue.length === 0 && !_this10._isStopped) {
28161
+ if (_this11.pendingSentenceText === sentenceText && _this11.scheduledBuffers === 0 && _this11.preparedBuffer.length === 0 && _this11.pcmChunkQueue.length === 0 && !_this11._isStopped) {
28064
28162
  console.warn("\u26A0\uFE0F AudioPlayer: Empty sentence detected after 5s timeout - no chunks received for: \"".concat(sentenceText.substring(0, 40), "...\""));
28065
28163
  // If this empty sentence carried a segment id, report it done (nothing was heard) and
28066
28164
  // adopt it as current so the coarse stop below matches the backend's last-sent id.
28067
- if (_this10.pendingSegmentId != null) {
28068
- var emptySegId = _this10.pendingSegmentId;
28069
- _this10.currentSegmentId = emptySegId;
28070
- _this10.pendingSegmentId = null;
28071
- _this10.emit('segmentDone', {
28165
+ if (_this11.pendingSegmentId != null) {
28166
+ var emptySegId = _this11.pendingSegmentId;
28167
+ _this11.currentSegmentId = emptySegId;
28168
+ _this11.pendingSegmentId = null;
28169
+ _this11.emit('segmentDone', {
28072
28170
  segmentId: emptySegId,
28073
28171
  status: 'finished',
28074
28172
  playedMs: 0
28075
28173
  });
28076
28174
  }
28077
28175
  // Clear pending sentence to unblock next sentence
28078
- if (_this10.pendingSentenceText === sentenceText) {
28079
- _this10.pendingSentenceText = null;
28176
+ if (_this11.pendingSentenceText === sentenceText) {
28177
+ _this11.pendingSentenceText = null;
28080
28178
  }
28081
28179
  // Emit playbackStopped to allow next sentence to start
28082
28180
  // Only if we're not currently playing (to avoid interrupting real playback)
28083
- if (!_this10.isPlaying && _this10.scheduledSources.size === 0) {
28181
+ if (!_this11.isPlaying && _this11.scheduledSources.size === 0) {
28084
28182
  console.log('🛑 AudioPlayer: Emitting playbackStopped for empty sentence timeout');
28085
- _this10.emit('playbackStopped', {
28086
- segmentId: _this10.currentSegmentId
28183
+ _this11.emit('playbackStopped', {
28184
+ segmentId: _this11.currentSegmentId
28087
28185
  });
28088
28186
  }
28089
28187
  }
28090
- _this10._emptySentenceTimeout = null;
28188
+ _this11._emptySentenceTimeout = null;
28091
28189
  }, 5000); // 5 second timeout - adjust based on expected chunk arrival rate
28092
28190
  }
28093
28191
 
@@ -28097,14 +28195,14 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28097
28195
  }, {
28098
28196
  key: "startTranscriptChecker",
28099
28197
  value: function startTranscriptChecker() {
28100
- var _this11 = this;
28198
+ var _this12 = this;
28101
28199
  if (this.isCheckingTranscripts) return;
28102
28200
  this.isCheckingTranscripts = true;
28103
28201
  console.log('📝 AudioPlayer: Transcript checker started');
28104
28202
  var _checkLoop = function checkLoop() {
28105
- if (!_this11.isCheckingTranscripts || !_this11.audioContext) return;
28106
- var currentTime = _this11.audioContext.currentTime;
28107
- var _iterator2 = _createForOfIteratorHelper(_this11.sentenceTimings),
28203
+ if (!_this12.isCheckingTranscripts || !_this12.audioContext) return;
28204
+ var currentTime = _this12.audioContext.currentTime;
28205
+ var _iterator2 = _createForOfIteratorHelper(_this12.sentenceTimings),
28108
28206
  _step2;
28109
28207
  try {
28110
28208
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
@@ -28118,13 +28216,13 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28118
28216
  eventData.synced = _timing.synced;
28119
28217
  }
28120
28218
  console.log("\uD83D\uDCDD AudioPlayer: Display transcript at ".concat(currentTime.toFixed(3), "s: \"").concat(_timing.text.substring(0, 40), "...\" (synced: ").concat(_timing.synced ? _timing.synced.length : 0, ")"));
28121
- _this11.emit('transcriptDisplay', eventData);
28219
+ _this12.emit('transcriptDisplay', eventData);
28122
28220
  }
28123
28221
  // Per-segment natural finish: this segment's audio window has fully elapsed.
28124
28222
  if (!_timing.doneReported && _timing.endTime != null && currentTime >= _timing.endTime) {
28125
28223
  _timing.doneReported = true;
28126
28224
  var _playedMs = Math.round((_timing.endTime - _timing.startTime) * 1000);
28127
- _this11.emit('segmentDone', {
28225
+ _this12.emit('segmentDone', {
28128
28226
  segmentId: _timing.segmentId,
28129
28227
  status: 'finished',
28130
28228
  playedMs: _playedMs
@@ -28136,12 +28234,12 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28136
28234
  } finally {
28137
28235
  _iterator2.f();
28138
28236
  }
28139
- if (_this11.isPlaying || _this11.scheduledBuffers > 0) {
28237
+ if (_this12.isPlaying || _this12.scheduledBuffers > 0) {
28140
28238
  requestAnimationFrame(_checkLoop);
28141
28239
  } else {
28142
28240
  // Playback drained naturally — flush any segment whose finish tick we may have missed
28143
28241
  // (the last buffer's onended can flip isPlaying=false before this loop's next tick).
28144
- var _iterator3 = _createForOfIteratorHelper(_this11.sentenceTimings),
28242
+ var _iterator3 = _createForOfIteratorHelper(_this12.sentenceTimings),
28145
28243
  _step3;
28146
28244
  try {
28147
28245
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -28150,7 +28248,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28150
28248
  timing.doneReported = true;
28151
28249
  var end = timing.endTime != null ? timing.endTime : timing.startTime;
28152
28250
  var playedMs = Math.round((end - timing.startTime) * 1000);
28153
- _this11.emit('segmentDone', {
28251
+ _this12.emit('segmentDone', {
28154
28252
  segmentId: timing.segmentId,
28155
28253
  status: 'finished',
28156
28254
  playedMs: playedMs
@@ -28162,7 +28260,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28162
28260
  } finally {
28163
28261
  _iterator3.f();
28164
28262
  }
28165
- _this11.isCheckingTranscripts = false;
28263
+ _this12.isCheckingTranscripts = false;
28166
28264
  console.log('📝 AudioPlayer: Transcript checker stopped');
28167
28265
  }
28168
28266
  };
@@ -28326,8 +28424,9 @@ __webpack_require__.r(__webpack_exports__);
28326
28424
  /* harmony import */ var _core_ClientToolsRegistry_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../core/ClientToolsRegistry.js */ "./src/core/ClientToolsRegistry.js");
28327
28425
  /* harmony import */ var _core_ClientScriptManager_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../core/ClientScriptManager.js */ "./src/core/ClientScriptManager.js");
28328
28426
  /* harmony import */ var _core_helloFlavor_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../core/helloFlavor.js */ "./src/core/helloFlavor.js");
28329
- /* harmony import */ var _utils_screenshot_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../utils/screenshot.js */ "./src/utils/screenshot.js");
28330
- /* harmony import */ var _utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../utils/visual-tools.js */ "./src/utils/visual-tools.js");
28427
+ /* harmony import */ var _hopPlatform_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hopPlatform.js */ "./src/v2/hopPlatform.js");
28428
+ /* harmony import */ var _utils_screenshot_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../utils/screenshot.js */ "./src/utils/screenshot.js");
28429
+ /* harmony import */ var _utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../utils/visual-tools.js */ "./src/utils/visual-tools.js");
28331
28430
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
28332
28431
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
28333
28432
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -28363,6 +28462,7 @@ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e
28363
28462
 
28364
28463
 
28365
28464
 
28465
+
28366
28466
  /**
28367
28467
 
28368
28468
  * VoiceSDK v2 - Multi-codec speech-to-speech SDK
@@ -28460,23 +28560,20 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28460
28560
  // Audio constraints for getUserMedia (optional)
28461
28561
  // If not provided, defaults will be used: echoCancellation: true, noiseSuppression: true, autoGainControl: true
28462
28562
  audioConstraints: config.audioConstraints || null,
28463
- // Last-hop TTS playback via hidden <audio> (AEC far-end). Default on.
28464
- // Set false to fall back to AudioContext.destination (old path).
28465
- // QA/debug override wins over config: ?ttp_hop=0 / localStorage ttp_hop=0
28466
- // forces AudioContext.destination; =1 forces the HTML hop.
28563
+ // Last-hop TTS playback via hidden <audio> (AEC far-end). Platform default
28564
+ // (hop on mobile/macOS only) applies when unset; set explicitly to force.
28565
+ // QA/debug override wins over config: ?ttp_hop=0|1 mirrored to
28566
+ // sessionStorage 'ttp_hop_pref' so it survives same-origin full page loads
28567
+ // (Borusan navigate_to_form) — then localStorage 'ttp_hop'.
28467
28568
  htmlAudioPlayback: function () {
28468
28569
  try {
28469
- var _URLSearchParams$get;
28470
- var flag = (_URLSearchParams$get = new URLSearchParams(window.location.search).get('ttp_hop')) !== null && _URLSearchParams$get !== void 0 ? _URLSearchParams$get : window.localStorage.getItem('ttp_hop');
28471
- if (flag === '0' || flag === '1') {
28472
- console.log("\uD83D\uDD27 VoiceSDK v2: ttp_hop override=".concat(flag, " \u2192 htmlAudioPlayback=").concat(flag === '1'));
28473
- return flag === '1';
28474
- }
28475
- if (flag !== null && flag !== undefined) {
28476
- console.log("\uD83D\uDD27 VoiceSDK v2: ttp_hop ignored (value '".concat(flag, "', expected '0' or '1')"));
28570
+ var override = (0,_hopPlatform_js__WEBPACK_IMPORTED_MODULE_7__.readTtpHopOverride)(typeof window !== 'undefined' ? window : null);
28571
+ if (override !== null) {
28572
+ console.log("\uD83D\uDD27 VoiceSDK v2: ttp_hop override \u2192 htmlAudioPlayback=".concat(override));
28573
+ return override;
28477
28574
  }
28478
28575
  } catch (e) {
28479
- console.log('🔧 VoiceSDK v2: ttp_hop check threw (storage blocked?):', e && e.message);
28576
+ console.log('🔧 VoiceSDK v2: ttp_hop check threw:', e && e.message);
28480
28577
  }
28481
28578
  // Pass through undefined so AudioPlayer applies the platform default
28482
28579
  return config.htmlAudioPlayback;
@@ -28623,7 +28720,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28623
28720
  key: "_registerBuiltInTools",
28624
28721
  value: function _registerBuiltInTools() {
28625
28722
  try {
28626
- (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_8__.registerVisualTools)(this.clientToolsRegistry);
28723
+ (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_9__.registerVisualTools)(this.clientToolsRegistry);
28627
28724
  } catch (error) {
28628
28725
  console.error('❌ VoiceSDK: Error registering built-in tools:', error);
28629
28726
  console.error(' Error details:', error.message, error.stack);
@@ -28949,6 +29046,12 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28949
29046
  key: "setupAudioPlayerEvents",
28950
29047
  value: function setupAudioPlayerEvents() {
28951
29048
  var _this3 = this;
29049
+ // The initial client_audio_info snapshot (at recordingStarted) predates
29050
+ // HTML-hop establishment, so its htmlAudioRouteActive/htmlAudioPlayOk are
29051
+ // always false. Re-report once the route outcome is known.
29052
+ this.audioPlayer.on('htmlAudioRoute', function (info) {
29053
+ _this3._sendClientAudioInfo(info);
29054
+ });
28952
29055
  this.audioPlayer.on('playbackStarted', function (info) {
28953
29056
  _this3.isPlaying = true;
28954
29057
 
@@ -29375,7 +29478,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29375
29478
  // iOS in-app webviews lack "Safari" in the UA (real Safari always has it).
29376
29479
  var webview = isAndroid && (/\bwv\b/.test(ua) || /Version\/[\d.]+.*Chrome/.test(ua)) || isIos && !/Safari/i.test(ua) || false;
29377
29480
  var env = {
29378
- sdkVersion: true ? "2.48.14" : 0,
29481
+ sdkVersion: true ? "2.48.18" : 0,
29379
29482
  ua: ua,
29380
29483
  platform: (uaData === null || uaData === void 0 ? void 0 : uaData.platform) || navigator.platform || '',
29381
29484
  mobile: (_uaData$mobile = uaData === null || uaData === void 0 ? void 0 : uaData.mobile) !== null && _uaData$mobile !== void 0 ? _uaData$mobile : isAndroid || isIos,
@@ -29390,7 +29493,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29390
29493
  } catch (e) {
29391
29494
  console.warn('⚠️ VoiceSDK v2: Failed to build client env:', e);
29392
29495
  return {
29393
- sdkVersion: true ? "2.48.14" : 0
29496
+ sdkVersion: true ? "2.48.18" : 0
29394
29497
  };
29395
29498
  }
29396
29499
  }
@@ -29404,12 +29507,14 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29404
29507
  */
29405
29508
  }, {
29406
29509
  key: "_sendClientAudioInfo",
29407
- value: function _sendClientAudioInfo() {
29510
+ value: function _sendClientAudioInfo(routeUpdate) {
29408
29511
  try {
29409
29512
  var _this$audioRecorder, _this$audioRecorder$g, _s$echoCancellation, _s$noiseSuppression, _s$autoGainControl, _s$voiceIsolation, _s$sampleRate, _s$channelCount, _this$audioRecorder2, _this$audioPlayer, _this$audioRecorder3, _this$audioPlayer2, _this$audioPlayer2$ge;
29410
29513
  var track = (_this$audioRecorder = this.audioRecorder) === null || _this$audioRecorder === void 0 || (_this$audioRecorder = _this$audioRecorder.mediaStream) === null || _this$audioRecorder === void 0 || (_this$audioRecorder$g = _this$audioRecorder.getAudioTracks) === null || _this$audioRecorder$g === void 0 ? void 0 : _this$audioRecorder$g.call(_this$audioRecorder)[0];
29411
- if (!track) return;
29412
- var s = track.getSettings ? track.getSettings() : {};
29514
+ // Route updates must go out even before the mic track exists — the hop
29515
+ // state is the whole point of the follow-up.
29516
+ if (!track && !routeUpdate) return;
29517
+ var s = track && track.getSettings ? track.getSettings() : {};
29413
29518
  var audio = _objectSpread({
29414
29519
  aec: (_s$echoCancellation = s.echoCancellation) !== null && _s$echoCancellation !== void 0 ? _s$echoCancellation : null,
29415
29520
  ns: (_s$noiseSuppression = s.noiseSuppression) !== null && _s$noiseSuppression !== void 0 ? _s$noiseSuppression : null,
@@ -29417,11 +29522,15 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29417
29522
  voiceIsolation: (_s$voiceIsolation = s.voiceIsolation) !== null && _s$voiceIsolation !== void 0 ? _s$voiceIsolation : null,
29418
29523
  trackRate: (_s$sampleRate = s.sampleRate) !== null && _s$sampleRate !== void 0 ? _s$sampleRate : null,
29419
29524
  channels: (_s$channelCount = s.channelCount) !== null && _s$channelCount !== void 0 ? _s$channelCount : null,
29420
- mic: track.label || null,
29525
+ mic: track ? track.label || null : null,
29421
29526
  captureCtxRate: ((_this$audioRecorder2 = this.audioRecorder) === null || _this$audioRecorder2 === void 0 || (_this$audioRecorder2 = _this$audioRecorder2.audioContext) === null || _this$audioRecorder2 === void 0 ? void 0 : _this$audioRecorder2.sampleRate) || null,
29422
29527
  playbackCtxRate: ((_this$audioPlayer = this.audioPlayer) === null || _this$audioPlayer === void 0 || (_this$audioPlayer = _this$audioPlayer.audioContext) === null || _this$audioPlayer === void 0 ? void 0 : _this$audioPlayer.sampleRate) || null,
29423
29528
  constraintsFallback: ((_this$audioRecorder3 = this.audioRecorder) === null || _this$audioRecorder3 === void 0 || (_this$audioRecorder3 = _this$audioRecorder3.mediaStream) === null || _this$audioRecorder3 === void 0 ? void 0 : _this$audioRecorder3._ttpConstraintsFallback) === true
29424
29529
  }, ((_this$audioPlayer2 = this.audioPlayer) === null || _this$audioPlayer2 === void 0 || (_this$audioPlayer2$ge = _this$audioPlayer2.getHtmlAudioPlaybackInfo) === null || _this$audioPlayer2$ge === void 0 ? void 0 : _this$audioPlayer2$ge.call(_this$audioPlayer2)) || {});
29530
+ if (routeUpdate && routeUpdate.state) {
29531
+ audio.routeUpdate = routeUpdate.state; // 'established' | 'fallback' | 'play_pending'
29532
+ if (routeUpdate.reason) audio.routeFallbackReason = routeUpdate.reason;
29533
+ }
29425
29534
  this.sendMessage({
29426
29535
  t: 'client_audio_info',
29427
29536
  audio: audio
@@ -29532,7 +29641,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29532
29641
 
29533
29642
  // Include SDK build time for debugging
29534
29643
  if (true) {
29535
- helloMessage.lastBuildTime = "2026-08-21T10:23:43.383Z";
29644
+ helloMessage.lastBuildTime = "2026-08-27T12:25:20.756Z";
29536
29645
  }
29537
29646
 
29538
29647
  // Client environment (device/browser/webview) for backend logs + Langfuse metadata
@@ -29776,7 +29885,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29776
29885
  case 'get_page_context':
29777
29886
  // Handle get_page_context request from backend
29778
29887
  console.log('📖 [VISUAL ASSISTANT] Backend requested page context');
29779
- (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_8__.extractPageContext)().then(function (pageContext) {
29888
+ (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_9__.extractPageContext)().then(function (pageContext) {
29780
29889
  // Use existing DOM scanner
29781
29890
  _this6.sendMessage({
29782
29891
  t: 'page_context',
@@ -29797,7 +29906,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29797
29906
  console.log('📸 [VISUAL ASSISTANT] Backend requested screenshot');
29798
29907
  try {
29799
29908
  // Use existing screenshot capture function
29800
- (0,_utils_screenshot_js__WEBPACK_IMPORTED_MODULE_7__.captureScreenshot)().then(function (screenshot) {
29909
+ (0,_utils_screenshot_js__WEBPACK_IMPORTED_MODULE_8__.captureScreenshot)().then(function (screenshot) {
29801
29910
  _this6.sendMessage({
29802
29911
  t: 'screenshot',
29803
29912
  screenshot: {
@@ -32157,6 +32266,99 @@ var PCMUCodec = /*#__PURE__*/function () {
32157
32266
 
32158
32267
  /***/ }),
32159
32268
 
32269
+ /***/ "./src/v2/hopPlatform.js":
32270
+ /*!*******************************!*\
32271
+ !*** ./src/v2/hopPlatform.js ***!
32272
+ \*******************************/
32273
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
32274
+
32275
+ "use strict";
32276
+ __webpack_require__.r(__webpack_exports__);
32277
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
32278
+ /* harmony export */ TTP_HOP_SESSION_KEY: () => (/* binding */ TTP_HOP_SESSION_KEY),
32279
+ /* harmony export */ isMacDesktopUa: () => (/* binding */ isMacDesktopUa),
32280
+ /* harmony export */ isMobileUa: () => (/* binding */ isMobileUa),
32281
+ /* harmony export */ needsHtmlAudioHopForAec: () => (/* binding */ needsHtmlAudioHopForAec),
32282
+ /* harmony export */ readTtpHopOverride: () => (/* binding */ readTtpHopOverride)
32283
+ /* harmony export */ });
32284
+ /**
32285
+ * HTML-audio-hop platform policy + the ttp_hop QA override.
32286
+ *
32287
+ * The "hop" is AudioContext → MediaStreamDestination → hidden <audio>, used so
32288
+ * the browser's echo canceller sees agent TTS as a far-end reference. Chrome
32289
+ * renders that live stream with adaptive jitter-buffer speed control, which
32290
+ * audibly wobbles on Windows/Linux desktop — so the hop defaults ON only where
32291
+ * Web Audio → destination genuinely lacks an AEC reference (mobile + macOS).
32292
+ *
32293
+ * Kept DOM-free (all inputs passed in) so the policy is unit-testable without
32294
+ * a browser: see test/hop-platform.test.mjs.
32295
+ */
32296
+
32297
+ /** sessionStorage key mirroring an explicit ?ttp_hop= URL value (see readTtpHopOverride). */
32298
+ var TTP_HOP_SESSION_KEY = 'ttp_hop_pref';
32299
+ function isMobileUa(ua) {
32300
+ return /Android|iPhone|iPad|iPod/i.test(ua || '');
32301
+ }
32302
+ function isMacDesktopUa(ua, platform) {
32303
+ if (/iPhone|iPad|iPod/i.test(ua || '')) return false;
32304
+ return /mac/i.test(platform || '') || /Mac OS X/i.test(ua || '');
32305
+ }
32306
+
32307
+ /**
32308
+ * Hop default: on only where Web Audio destination lacks an AEC far-end
32309
+ * reference (mobile + macOS). Windows/Linux/ChromeOS desktop: off — Chrome-wide
32310
+ * AEC already covers speaker echo there, and the hop only adds speed wobble.
32311
+ * Unknown/empty environment: off (fail toward the stable direct path).
32312
+ */
32313
+ function needsHtmlAudioHopForAec(ua, platform) {
32314
+ return isMobileUa(ua) || isMacDesktopUa(ua, platform);
32315
+ }
32316
+
32317
+ /**
32318
+ * QA/debug override for the hop route. Returns true (force hop on), false
32319
+ * (force off), or null (no override — apply config/platform default).
32320
+ *
32321
+ * Precedence: URL ?ttp_hop=0|1 → sessionStorage mirror → localStorage 'ttp_hop'.
32322
+ *
32323
+ * An explicit URL value is mirrored into sessionStorage (TTP_HOP_SESSION_KEY)
32324
+ * so it survives same-origin full page loads that drop the query string —
32325
+ * Borusan's navigate_to_form does window.location.href = '/path' (call
32326
+ * 84b5af7d), which used to silently re-apply the platform default mid-call.
32327
+ * The mirror dies with the tab; localStorage remains the sticky manual knob
32328
+ * and ranks below the mirror because the URL value is the more recent intent.
32329
+ *
32330
+ * Every storage touch is individually guarded: browsers can block one storage
32331
+ * area but not another, and a blocked sessionStorage must not cost us the
32332
+ * localStorage fallback.
32333
+ */
32334
+ function readTtpHopOverride(win) {
32335
+ var w = win || (typeof window !== 'undefined' ? window : null);
32336
+ if (!w) return null;
32337
+ var flag = null;
32338
+ try {
32339
+ flag = new URLSearchParams(w.location.search).get('ttp_hop');
32340
+ } catch (e) {/* no location in this env */}
32341
+ if (flag === '0' || flag === '1') {
32342
+ try {
32343
+ w.sessionStorage.setItem(TTP_HOP_SESSION_KEY, flag);
32344
+ } catch (e) {/* storage blocked */}
32345
+ return flag === '1';
32346
+ }
32347
+ var mirrored = null;
32348
+ try {
32349
+ mirrored = w.sessionStorage.getItem(TTP_HOP_SESSION_KEY);
32350
+ } catch (e) {/* storage blocked */}
32351
+ if (mirrored === '0' || mirrored === '1') return mirrored === '1';
32352
+ var stored = null;
32353
+ try {
32354
+ stored = w.localStorage.getItem('ttp_hop');
32355
+ } catch (e) {/* storage blocked */}
32356
+ if (stored === '0' || stored === '1') return stored === '1';
32357
+ return null;
32358
+ }
32359
+
32360
+ /***/ }),
32361
+
32160
32362
  /***/ "./src/v2/utils/AudioFormatConverter.js":
32161
32363
  /*!**********************************************!*\
32162
32364
  !*** ./src/v2/utils/AudioFormatConverter.js ***!
@@ -35761,7 +35963,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
35761
35963
  return;
35762
35964
  }
35763
35965
  this._ensureAboutStyles();
35764
- var version = true ? "2.48.14" : 0;
35966
+ var version = true ? "2.48.18" : 0;
35765
35967
  var convId = this._getLastConversationId();
35766
35968
  var t = function t(k, fb) {
35767
35969
  try {