ttp-agent-sdk 2.33.5 → 2.34.3

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.
@@ -1 +1,321 @@
1
- class AudioProcessor extends AudioWorkletProcessor{constructor(e){super(),this.config=e.processorOptions||{},this.sampleRate="undefined"!=typeof sampleRate?sampleRate:this.config.sampleRate||this.config.outputSampleRate||44100,this.bufferSize=128,this.buffer=new Float32Array(this.bufferSize),this.bufferIndex=0,this.silenceThreshold=.02,this.VOICE_FRAMES_REQUIRED=2,this.minVoiceDuration=100,this.pauseThreshold=3e3,this.isVoiceActive=!1,this.voiceStartTime=0,this.lastVoiceTime=0,this.consecutiveSilenceFrames=0,this.silenceFramesThreshold=5,this.voiceFrameCount=0,this.frameCount=0,this.lastLogTime=0,this.continuousMode=!0,this.forceContinuous=!0,this.isCurrentlyStreaming=!1,this.sendBuffer=null,this.sendBufferBytes=0,this.port.onmessage=e=>{const{type:t,data:s}=e.data;switch(t){case"start":this.isProcessing=!0,this.isCurrentlyStreaming=!0;break;case"stop":this.isProcessing=!1,this.isCurrentlyStreaming=!1,this.isVoiceActive=!1,this.forceContinuous=!1,this.voiceFrameCount=0,this.flushBuffer();break;case"setForceContinuous":this.forceContinuous=s.enabled,this.isProcessing=!0,this.isCurrentlyStreaming=!1;break;case"flush":this.flushBuffer();break;case"config":Object.assign(this.config,s)}}}process(e,t,s){const i=e[0],o=t[0];return i.length>0&&o.length>0&&o[0].set(i[0]),i.length>0&&i[0].length>0&&this.processAudioData(i[0]),!0}processAudioData(e){this.frameCount++;for(let t=0;t<e.length;t+=this.bufferSize){const s=Math.min(this.bufferSize,e.length-t);for(let i=0;i<s;i++)this.buffer[i]=e[t+i];for(let e=s;e<this.bufferSize;e++)this.buffer[e]=0;let i=0;for(let e=0;e<this.bufferSize;e++)i+=this.buffer[e]*this.buffer[e];const o=Math.sqrt(i/this.bufferSize);let r=0,n=0;for(let e=1;e<this.bufferSize;e++){const t=Math.abs(this.buffer[e]-this.buffer[e-1]);r+=t,t>.1&&n++}r/=this.bufferSize,this.bufferSize;const h=Date.now();let f=o>this.silenceThreshold;const a=h-this.lastVoiceTime;if(f)this.consecutiveSilenceFrames=0,this.voiceFrameCount++,this.voiceFrameCount>=this.VOICE_FRAMES_REQUIRED&&(this.isVoiceActive||(this.isVoiceActive=!0,this.voiceStartTime=h,this.isCurrentlyStreaming=!0,this.frameCount%50==0&&console.log(`🎤 VAD: VOICE DETECTED (RMS: ${o.toFixed(4)}, frames: ${this.voiceFrameCount})`))),this.lastVoiceTime=h;else{this.voiceFrameCount=0,this.consecutiveSilenceFrames++;const e=this.forceContinuous?3e3:200;!f&&(this.isVoiceActive||this.isCurrentlyStreaming)&&a>=e&&(this.isVoiceActive=!1,this.isCurrentlyStreaming=!1,this.voiceStartTime=0,this.lastVoiceTime=0,this.consecutiveSilenceFrames=0,this.voiceFrameCount=0,console.log(`🔇 VAD: SILENCE DETECTED (${a}ms silence, RMS: ${o.toFixed(4)})`))}this.isCurrentlyStreaming&&this.isProcessing?(this.frameCount%200==0&&console.log(`📤 VAD: Sending audio (isVoiceActive: ${this.isVoiceActive}, RMS: ${o.toFixed(4)})`),this.sendPCMAudioData(this.buffer)):this.frameCount%200==0&&this.isProcessing&&console.log(`🚫 VAD: Blocking audio (isCurrentlyStreaming: ${this.isCurrentlyStreaming}, RMS: ${o.toFixed(4)})`)}}sendPCMAudioData(e){const t=new Int16Array(e.length);for(let s=0;s<e.length;s++){const i=Math.max(-1,Math.min(1,e[s]));t[s]=Math.round(32767*i)}for(this.sendBuffer||(this.sendBuffer=[],this.sendBufferBytes=0),this.sendBuffer.push(t),this.sendBufferBytes+=t.byteLength;this.sendBufferBytes>=4096;){let e=0,t=0;for(let s=0;s<this.sendBuffer.length;s++){const i=this.sendBuffer[s].byteLength;if(!(t+i<=4096))break;e++,t+=i}const s=this.sendBuffer.slice(0,e),i=s.reduce((e,t)=>e+t.length,0),o=new Int16Array(i);let r=0;for(const e of s)o.set(e,r),r+=e.length;this.port.postMessage({type:"pcm_audio_data",data:o,sampleRate:this.sampleRate,channelCount:1,frameCount:this.frameCount,batchSize:e,totalBytes:o.byteLength}),this.sendBuffer=this.sendBuffer.slice(e),this.sendBufferBytes-=t}}flushBuffer(){if(this.sendBuffer&&this.sendBuffer.length>0){const e=this.sendBuffer.reduce((e,t)=>e+t.length,0),t=new Int16Array(e);let s=0;for(const e of this.sendBuffer)t.set(e,s),s+=e.length;this.port.postMessage({type:"pcm_audio_data",data:t,sampleRate:this.sampleRate,channelCount:1,frameCount:this.frameCount,batchSize:this.sendBuffer.length,totalBytes:t.byteLength,isFlush:!0}),this.sendBuffer=[],this.sendBufferBytes=0}}}registerProcessor("audio-processor",AudioProcessor);
1
+ /**
2
+ * AudioProcessor - AudioWorklet for real-time audio processing
3
+ *
4
+ * This AudioWorklet processes audio data in real-time and sends it to the main thread
5
+ * for transmission to the WebSocket server.
6
+ */
7
+
8
+ class AudioProcessor extends AudioWorkletProcessor {
9
+ constructor(options) {
10
+ super();
11
+
12
+ // Configuration
13
+ this.config = options.processorOptions || {};
14
+ // Use AudioContext sampleRate (available as global 'sampleRate' in AudioWorkletProcessor)
15
+ // Fall back to config if sampleRate not available, default to 44100 Hz to match server output
16
+ this.sampleRate = typeof sampleRate !== 'undefined' ? sampleRate : (this.config.sampleRate || this.config.outputSampleRate || 44100);
17
+ this.bufferSize = 128; // Process 128 samples at a time (256 bytes = 8ms at 16kHz)
18
+ this.buffer = new Float32Array(this.bufferSize);
19
+ this.bufferIndex = 0;
20
+
21
+ // VAD (Voice Activity Detection) parameters
22
+ this.silenceThreshold = 0.02; // RMS threshold for voice detection (increased to reduce ambient noise sensitivity)
23
+ this.VOICE_FRAMES_REQUIRED = 2; // Require 2 consecutive frames above threshold before activating
24
+ this.minVoiceDuration = 100; // ms - minimum speech duration
25
+ this.pauseThreshold = 3000; // ms - longer pause before processing
26
+
27
+ // VAD state
28
+ this.isVoiceActive = false;
29
+ this.voiceStartTime = 0;
30
+ this.lastVoiceTime = 0;
31
+ this.consecutiveSilenceFrames = 0;
32
+ this.silenceFramesThreshold = 5; // More frames needed for silence detection
33
+ this.voiceFrameCount = 0; // Count consecutive frames above threshold
34
+
35
+ // Audio quality tracking
36
+ this.frameCount = 0;
37
+ this.lastLogTime = 0;
38
+
39
+ // Continuous recording mode
40
+ this.continuousMode = true; // Always send audio when voice is detected
41
+ this.forceContinuous = true; // Force continuous for toggle button behavior
42
+ this.isCurrentlyStreaming = false; // Track if we're currently sending audio
43
+
44
+ // Batching buffer
45
+ this.sendBuffer = null;
46
+ this.sendBufferBytes = 0;
47
+
48
+ // Handle messages from main thread
49
+ this.port.onmessage = (event) => {
50
+ const { type, data } = event.data;
51
+
52
+ switch (type) {
53
+ case 'start':
54
+ this.isProcessing = true;
55
+ this.isCurrentlyStreaming = true;
56
+ break;
57
+
58
+ case 'stop':
59
+ this.isProcessing = false;
60
+ this.isCurrentlyStreaming = false;
61
+ this.isVoiceActive = false;
62
+ this.forceContinuous = false;
63
+ this.voiceFrameCount = 0; // Reset voice frame count
64
+ // Flush any remaining data
65
+ this.flushBuffer();
66
+ break;
67
+
68
+ case 'setForceContinuous':
69
+ this.forceContinuous = data.enabled;
70
+ this.isProcessing = true;
71
+ // FIXED: Don't set isCurrentlyStreaming = true here - let VAD control it
72
+ // VAD will set isCurrentlyStreaming = true when voice is detected
73
+ this.isCurrentlyStreaming = false;
74
+ break;
75
+
76
+ case 'flush':
77
+ this.flushBuffer();
78
+ break;
79
+
80
+ case 'config':
81
+ Object.assign(this.config, data);
82
+ break;
83
+ }
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Process audio data
89
+ */
90
+ process(inputs, outputs, parameters) {
91
+ // CRITICAL: If processing is stopped, terminate the processor
92
+ if (!this.isProcessing) {
93
+ // Return false to terminate the AudioWorklet processor
94
+ // This stops all VAD processing immediately
95
+ return false;
96
+ }
97
+
98
+ const input = inputs[0];
99
+ const output = outputs[0];
100
+
101
+ // Copy input to output (pass-through)
102
+ if (input.length > 0 && output.length > 0) {
103
+ output[0].set(input[0]);
104
+ }
105
+
106
+ // Process audio for PCM recording and VAD
107
+ if (input.length > 0 && input[0].length > 0) {
108
+ this.processAudioData(input[0]);
109
+ }
110
+
111
+ // Keep the processor alive
112
+ return true;
113
+ }
114
+
115
+ processAudioData(audioData) {
116
+ // CRITICAL: Early return if processing is stopped
117
+ // This prevents VAD calculations and logging when stopped
118
+ if (!this.isProcessing) {
119
+ return;
120
+ }
121
+
122
+ this.frameCount++;
123
+
124
+ // Process audio in consistent 128-sample chunks (256 bytes)
125
+ for (let i = 0; i < audioData.length; i += this.bufferSize) {
126
+ const chunkSize = Math.min(this.bufferSize, audioData.length - i);
127
+
128
+ // Copy chunk to buffer
129
+ for (let j = 0; j < chunkSize; j++) {
130
+ this.buffer[j] = audioData[i + j];
131
+ }
132
+
133
+ // Pad with zeros if needed
134
+ for (let j = chunkSize; j < this.bufferSize; j++) {
135
+ this.buffer[j] = 0;
136
+ }
137
+
138
+ // Calculate RMS for VAD on this chunk
139
+ let sum = 0;
140
+ for (let j = 0; j < this.bufferSize; j++) {
141
+ sum += this.buffer[j] * this.buffer[j];
142
+ }
143
+ const rms = Math.sqrt(sum / this.bufferSize);
144
+
145
+ // Calculate additional features for better VAD
146
+ let variation = 0;
147
+ let highFreqCount = 0;
148
+ for (let j = 1; j < this.bufferSize; j++) {
149
+ const diff = Math.abs(this.buffer[j] - this.buffer[j-1]);
150
+ variation += diff;
151
+ if (diff > 0.1) highFreqCount++;
152
+ }
153
+ variation = variation / this.bufferSize;
154
+ const highFreqRatio = highFreqCount / this.bufferSize;
155
+
156
+ const currentTime = Date.now();
157
+
158
+ // VAD with reduced sensitivity - require consecutive frames above threshold
159
+ let hasVoice = rms > this.silenceThreshold;
160
+ // Calculate time since last voice detection
161
+ const timeSinceLastVoice = currentTime - this.lastVoiceTime;
162
+
163
+ // Voice detection logic - require consecutive frames above threshold
164
+ if (hasVoice) {
165
+ this.consecutiveSilenceFrames = 0;
166
+ this.voiceFrameCount++; // Increment consecutive voice frame count
167
+
168
+ // Only activate streaming after required consecutive frames
169
+ if (this.voiceFrameCount >= this.VOICE_FRAMES_REQUIRED) {
170
+ // Start voice if needed
171
+ if (!this.isVoiceActive) {
172
+ this.isVoiceActive = true;
173
+ this.voiceStartTime = currentTime;
174
+ this.isCurrentlyStreaming = true;
175
+ // Log voice detection (every 50 frames = ~400ms to avoid spam)
176
+ if (this.frameCount % 50 === 0) {
177
+ console.log(`🎤 VAD: VOICE DETECTED (RMS: ${rms.toFixed(4)}, frames: ${this.voiceFrameCount})`);
178
+ }
179
+ }
180
+ }
181
+
182
+ this.lastVoiceTime = currentTime;
183
+ } else {
184
+ // Silence detected - reset voice frame count
185
+ this.voiceFrameCount = 0;
186
+ this.consecutiveSilenceFrames++;
187
+
188
+ // In continuous mode, we still use VAD but require longer silence before stopping
189
+ // In non-continuous mode, stop quickly
190
+ const silenceThreshold = this.forceContinuous ? 3000 : 200; // 3s for continuous, 200ms otherwise
191
+
192
+ // FIXED: Stop condition - also check isCurrentlyStreaming (not just isVoiceActive)
193
+ // This handles case where setForceContinuous set streaming=true but no voice was detected yet
194
+ if (!hasVoice && (this.isVoiceActive || this.isCurrentlyStreaming) && timeSinceLastVoice >= silenceThreshold) {
195
+ this.isVoiceActive = false;
196
+ this.isCurrentlyStreaming = false;
197
+ this.voiceStartTime = 0;
198
+ this.lastVoiceTime = 0;
199
+ this.consecutiveSilenceFrames = 0;
200
+ this.voiceFrameCount = 0; // Reset voice frame count
201
+ // Log silence detection
202
+ console.log(`🔇 VAD: SILENCE DETECTED (${timeSinceLastVoice}ms silence, RMS: ${rms.toFixed(4)})`);
203
+ }
204
+ }
205
+
206
+ // Send PCM **only if streaming and processing** - hard gate
207
+ // This ensures we only send audio when voice is detected, even in continuous mode
208
+ if (this.isCurrentlyStreaming && this.isProcessing) {
209
+ // Log occasionally when sending (every 200 frames = ~1.6 seconds to avoid spam)
210
+ if (this.frameCount % 200 === 0) {
211
+ console.log(`📤 VAD: Sending audio (isVoiceActive: ${this.isVoiceActive}, RMS: ${rms.toFixed(4)})`);
212
+ }
213
+ this.sendPCMAudioData(this.buffer);
214
+ } else {
215
+ // Log occasionally when blocking (every 200 frames)
216
+ if (this.frameCount % 200 === 0 && this.isProcessing) {
217
+ console.log(`🚫 VAD: Blocking audio (isCurrentlyStreaming: ${this.isCurrentlyStreaming}, RMS: ${rms.toFixed(4)})`);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ sendPCMAudioData(float32Data) {
224
+ // Convert Float32Array (-1.0 to 1.0) to Int16Array (-32768 to 32767)
225
+ const pcmData = new Int16Array(float32Data.length);
226
+
227
+ for (let i = 0; i < float32Data.length; i++) {
228
+ // Clamp and convert to 16-bit PCM
229
+ const sample = Math.max(-1.0, Math.min(1.0, float32Data[i]));
230
+ pcmData[i] = Math.round(sample * 32767);
231
+ }
232
+
233
+ // Initialize send buffer if not exists
234
+ if (!this.sendBuffer) {
235
+ this.sendBuffer = [];
236
+ this.sendBufferBytes = 0;
237
+ }
238
+
239
+ // Accumulate chunks in buffer
240
+ this.sendBuffer.push(pcmData);
241
+ this.sendBufferBytes += pcmData.byteLength;
242
+
243
+ // Send in ~4 KB batches (≈128 ms of audio at 16kHz)
244
+ // Use sliding window approach to maintain continuous flow
245
+ while (this.sendBufferBytes >= 4096) {
246
+ // Calculate how many chunks we need for ~4KB
247
+ let chunksToSend = 0;
248
+ let bytesToSend = 0;
249
+
250
+ for (let i = 0; i < this.sendBuffer.length; i++) {
251
+ const chunkBytes = this.sendBuffer[i].byteLength;
252
+ if (bytesToSend + chunkBytes <= 4096) {
253
+ chunksToSend++;
254
+ bytesToSend += chunkBytes;
255
+ } else {
256
+ break;
257
+ }
258
+ }
259
+
260
+ // Create merged buffer from selected chunks
261
+ const chunksForBatch = this.sendBuffer.slice(0, chunksToSend);
262
+ const totalSamples = chunksForBatch.reduce((a, b) => a + b.length, 0);
263
+ const merged = new Int16Array(totalSamples);
264
+ let offset = 0;
265
+
266
+ for (const chunk of chunksForBatch) {
267
+ merged.set(chunk, offset);
268
+ offset += chunk.length;
269
+ }
270
+
271
+ // Send batched PCM data to main thread
272
+ this.port.postMessage({
273
+ type: 'pcm_audio_data',
274
+ data: merged, // Send the Int16Array directly, not the buffer
275
+ sampleRate: this.sampleRate,
276
+ channelCount: 1,
277
+ frameCount: this.frameCount,
278
+ batchSize: chunksToSend,
279
+ totalBytes: merged.byteLength
280
+ });
281
+
282
+ // Remove sent chunks from buffer (sliding window)
283
+ this.sendBuffer = this.sendBuffer.slice(chunksToSend);
284
+ this.sendBufferBytes -= bytesToSend;
285
+ }
286
+ }
287
+
288
+ // Flush any remaining buffered data
289
+ flushBuffer() {
290
+ if (this.sendBuffer && this.sendBuffer.length > 0) {
291
+ // Merge remaining chunks
292
+ const totalSamples = this.sendBuffer.reduce((a, b) => a + b.length, 0);
293
+ const merged = new Int16Array(totalSamples);
294
+ let offset = 0;
295
+
296
+ for (const chunk of this.sendBuffer) {
297
+ merged.set(chunk, offset);
298
+ offset += chunk.length;
299
+ }
300
+
301
+ // Send remaining data
302
+ this.port.postMessage({
303
+ type: 'pcm_audio_data',
304
+ data: merged, // Send the Int16Array directly, not the buffer
305
+ sampleRate: this.sampleRate,
306
+ channelCount: 1,
307
+ frameCount: this.frameCount,
308
+ batchSize: this.sendBuffer.length,
309
+ totalBytes: merged.byteLength,
310
+ isFlush: true
311
+ });
312
+
313
+ // Reset buffer
314
+ this.sendBuffer = [];
315
+ this.sendBufferBytes = 0;
316
+ }
317
+ }
318
+ }
319
+
320
+ // Register the processor
321
+ registerProcessor('audio-processor', AudioProcessor);
@@ -271,6 +271,21 @@
271
271
  <span class="badge badge--new">Demo</span>
272
272
  </div>
273
273
  </a>
274
+
275
+ <a href="widget-customization.html" class="test-card">
276
+ <span class="test-card__icon">🎨</span>
277
+ <h3 class="test-card__title">Widget Live Customization</h3>
278
+ <p class="test-card__description">
279
+ Interactive customization tool for the TTP Chat Widget. Click on any element
280
+ to customize colors, sizes, text, and icons. See all available designs and
281
+ get instant code output for your configuration.
282
+ </p>
283
+ <div class="test-card__badges">
284
+ <span class="badge badge--widget">Widget</span>
285
+ <span class="badge badge--sdk">Customization</span>
286
+ <span class="badge badge--new">New</span>
287
+ </div>
288
+ </a>
274
289
  </div>
275
290
  </div>
276
291