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.
- package/dist/agent-widget.dev.js +29049 -0
- package/dist/agent-widget.dev.js.map +1 -0
- package/dist/agent-widget.esm.js +1 -1
- package/dist/agent-widget.esm.js.map +1 -1
- package/dist/agent-widget.js +1 -1
- package/dist/agent-widget.js.map +1 -1
- package/dist/audio-processor.js +321 -1
- package/dist/demos/index.html +15 -0
- package/dist/demos/widget-customization.html +4373 -0
- package/dist/examples/demo-v2.html +25 -45
- package/dist/examples/test-index.html +15 -0
- package/dist/examples/widget-customization.html +4373 -0
- package/dist/index.html +3 -3
- package/examples/demo-v2.html +25 -45
- package/examples/test-index.html +15 -0
- package/examples/widget-customization.html +4373 -0
- package/package.json +8 -4
package/dist/audio-processor.js
CHANGED
|
@@ -1 +1,321 @@
|
|
|
1
|
-
|
|
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);
|
package/dist/demos/index.html
CHANGED
|
@@ -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
|
|