ttp-agent-sdk 2.0.1 → 2.0.2
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/package.json +2 -3
- package/src/core/AudioPlayer.js +0 -185
- package/src/core/AudioRecorder.js +0 -128
- package/src/core/ConnectionManager.js +0 -86
- package/src/core/EventEmitter.js +0 -53
- package/src/core/VoiceSDK.js +0 -390
- package/src/core/WebSocketManager.js +0 -218
- package/src/core/WebSocketManagerV2.js +0 -211
- package/src/core/WebSocketSingleton.js +0 -171
- package/src/index.js +0 -64
- package/src/legacy/AgentSDK.js +0 -462
- package/src/react/VoiceButton.jsx +0 -163
- package/src/vanilla/VoiceButton.js +0 -190
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ttp-agent-sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Comprehensive Voice Agent SDK for web integration with real-time audio, WebSocket communication, and React components",
|
|
5
5
|
"main": "dist/agent-widget.js",
|
|
6
|
-
"module": "
|
|
6
|
+
"module": "dist/agent-widget.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/",
|
|
10
|
-
"src/",
|
|
11
10
|
"examples/",
|
|
12
11
|
"README.md",
|
|
13
12
|
"GETTING_STARTED.md"
|
package/src/core/AudioPlayer.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AudioPlayer - Handles audio playback with queue system
|
|
3
|
-
*/
|
|
4
|
-
import EventEmitter from './EventEmitter.js';
|
|
5
|
-
|
|
6
|
-
export default class AudioPlayer extends EventEmitter {
|
|
7
|
-
constructor(config) {
|
|
8
|
-
super();
|
|
9
|
-
this.config = config;
|
|
10
|
-
this.audioContext = null;
|
|
11
|
-
this.audioQueue = [];
|
|
12
|
-
this.isPlaying = false;
|
|
13
|
-
this.isProcessingQueue = false;
|
|
14
|
-
this.currentSource = null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Add audio data to playback queue
|
|
19
|
-
*/
|
|
20
|
-
playAudio(audioData) {
|
|
21
|
-
try {
|
|
22
|
-
const audioBlob = this.createAudioBlob(audioData);
|
|
23
|
-
this.audioQueue.push(audioBlob);
|
|
24
|
-
|
|
25
|
-
// Process queue if not already playing or processing
|
|
26
|
-
if (!this.isPlaying && !this.isProcessingQueue) {
|
|
27
|
-
setTimeout(() => this.processQueue(), 50);
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
this.emit('playbackError', error);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Create audio blob from ArrayBuffer
|
|
36
|
-
*/
|
|
37
|
-
createAudioBlob(arrayBuffer) {
|
|
38
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
39
|
-
|
|
40
|
-
// Detect audio format
|
|
41
|
-
if (uint8Array.length >= 4) {
|
|
42
|
-
// WAV header (RIFF)
|
|
43
|
-
if (uint8Array[0] === 0x52 && uint8Array[1] === 0x49 &&
|
|
44
|
-
uint8Array[2] === 0x46 && uint8Array[3] === 0x46) {
|
|
45
|
-
return new Blob([arrayBuffer], { type: 'audio/wav' });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// MP3 header
|
|
49
|
-
if (uint8Array[0] === 0xFF && (uint8Array[1] & 0xE0) === 0xE0) {
|
|
50
|
-
return new Blob([arrayBuffer], { type: 'audio/mpeg' });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// OGG header
|
|
54
|
-
if (uint8Array[0] === 0x4F && uint8Array[1] === 0x67 &&
|
|
55
|
-
uint8Array[2] === 0x67 && uint8Array[3] === 0x53) {
|
|
56
|
-
return new Blob([arrayBuffer], { type: 'audio/ogg' });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Default to WAV format
|
|
61
|
-
return new Blob([arrayBuffer], { type: 'audio/wav' });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Process audio queue
|
|
66
|
-
*/
|
|
67
|
-
async processQueue() {
|
|
68
|
-
// Prevent multiple simultaneous queue processing
|
|
69
|
-
if (this.isProcessingQueue || this.isPlaying || this.audioQueue.length === 0) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.isProcessingQueue = true;
|
|
74
|
-
|
|
75
|
-
const audioBlob = this.audioQueue.shift();
|
|
76
|
-
if (!audioBlob) {
|
|
77
|
-
this.isProcessingQueue = false;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
this.isPlaying = true;
|
|
83
|
-
this.emit('playbackStarted');
|
|
84
|
-
|
|
85
|
-
// Create AudioContext if not exists
|
|
86
|
-
if (!this.audioContext) {
|
|
87
|
-
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const audioContext = this.audioContext;
|
|
91
|
-
|
|
92
|
-
// Resume AudioContext if suspended
|
|
93
|
-
if (audioContext.state === 'suspended') {
|
|
94
|
-
await audioContext.resume();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Create audio source from blob
|
|
98
|
-
const arrayBuffer = await audioBlob.arrayBuffer();
|
|
99
|
-
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
100
|
-
|
|
101
|
-
const source = audioContext.createBufferSource();
|
|
102
|
-
source.buffer = audioBuffer;
|
|
103
|
-
source.connect(audioContext.destination);
|
|
104
|
-
|
|
105
|
-
this.currentSource = source;
|
|
106
|
-
|
|
107
|
-
// Handle audio end
|
|
108
|
-
source.onended = () => {
|
|
109
|
-
this.isPlaying = false;
|
|
110
|
-
this.isProcessingQueue = false;
|
|
111
|
-
this.currentSource = null;
|
|
112
|
-
this.emit('playbackStopped');
|
|
113
|
-
|
|
114
|
-
// Process next audio in queue if there are more items
|
|
115
|
-
if (this.audioQueue.length > 0) {
|
|
116
|
-
setTimeout(() => this.processQueue(), 100);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Start playback
|
|
121
|
-
source.start();
|
|
122
|
-
|
|
123
|
-
} catch (error) {
|
|
124
|
-
this.isPlaying = false;
|
|
125
|
-
this.isProcessingQueue = false;
|
|
126
|
-
this.currentSource = null;
|
|
127
|
-
this.emit('playbackError', error);
|
|
128
|
-
|
|
129
|
-
// Try to process next audio in queue if there are more items
|
|
130
|
-
if (this.audioQueue.length > 0) {
|
|
131
|
-
setTimeout(() => this.processQueue(), 100);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Stop current playback and clear queue
|
|
138
|
-
*/
|
|
139
|
-
stop() {
|
|
140
|
-
this.stopImmediate();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Stop current playback immediately and clear queue
|
|
145
|
-
*/
|
|
146
|
-
stopImmediate() {
|
|
147
|
-
if (this.currentSource) {
|
|
148
|
-
try {
|
|
149
|
-
this.currentSource.stop();
|
|
150
|
-
} catch (error) {
|
|
151
|
-
// Ignore errors when stopping
|
|
152
|
-
}
|
|
153
|
-
this.currentSource = null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this.isPlaying = false;
|
|
157
|
-
this.isProcessingQueue = false;
|
|
158
|
-
this.audioQueue = [];
|
|
159
|
-
this.emit('playbackStopped');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get playback status
|
|
164
|
-
*/
|
|
165
|
-
getStatus() {
|
|
166
|
-
return {
|
|
167
|
-
isPlaying: this.isPlaying,
|
|
168
|
-
isProcessingQueue: this.isProcessingQueue,
|
|
169
|
-
queueLength: this.audioQueue.length,
|
|
170
|
-
audioContextState: this.audioContext ? this.audioContext.state : 'closed'
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Cleanup resources
|
|
176
|
-
*/
|
|
177
|
-
destroy() {
|
|
178
|
-
this.stop();
|
|
179
|
-
|
|
180
|
-
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
181
|
-
this.audioContext.close();
|
|
182
|
-
this.audioContext = null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AudioRecorder - Handles audio recording using AudioWorklet
|
|
3
|
-
*/
|
|
4
|
-
import EventEmitter from './EventEmitter.js';
|
|
5
|
-
|
|
6
|
-
export default class AudioRecorder extends EventEmitter {
|
|
7
|
-
constructor(config) {
|
|
8
|
-
super();
|
|
9
|
-
this.config = config;
|
|
10
|
-
this.audioContext = null;
|
|
11
|
-
this.audioWorkletNode = null;
|
|
12
|
-
this.mediaStream = null;
|
|
13
|
-
this.isRecording = false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Start audio recording
|
|
18
|
-
*/
|
|
19
|
-
async start() {
|
|
20
|
-
try {
|
|
21
|
-
// Get user media
|
|
22
|
-
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
23
|
-
audio: {
|
|
24
|
-
sampleRate: this.config.sampleRate,
|
|
25
|
-
channelCount: 1,
|
|
26
|
-
echoCancellation: true,
|
|
27
|
-
noiseSuppression: true,
|
|
28
|
-
autoGainControl: true
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Create AudioContext
|
|
33
|
-
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
34
|
-
sampleRate: this.config.sampleRate
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Resume AudioContext if suspended
|
|
38
|
-
if (this.audioContext.state === 'suspended') {
|
|
39
|
-
await this.audioContext.resume();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Load AudioWorklet module
|
|
43
|
-
await this.audioContext.audioWorklet.addModule('/audio-processor.js');
|
|
44
|
-
|
|
45
|
-
// Create AudioWorklet node
|
|
46
|
-
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'audio-processor');
|
|
47
|
-
|
|
48
|
-
// Create media stream source
|
|
49
|
-
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
50
|
-
source.connect(this.audioWorkletNode);
|
|
51
|
-
|
|
52
|
-
// Handle messages from AudioWorklet
|
|
53
|
-
this.audioWorkletNode.port.onmessage = (event) => {
|
|
54
|
-
const { type, data } = event.data;
|
|
55
|
-
|
|
56
|
-
if (type === 'pcm_audio_data') {
|
|
57
|
-
this.emit('audioData', data);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Enable continuous mode
|
|
62
|
-
this.audioWorkletNode.port.postMessage({
|
|
63
|
-
type: 'setForceContinuous',
|
|
64
|
-
data: { enabled: true }
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.isRecording = true;
|
|
68
|
-
this.emit('recordingStarted');
|
|
69
|
-
|
|
70
|
-
} catch (error) {
|
|
71
|
-
this.emit('error', error);
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Stop audio recording
|
|
78
|
-
*/
|
|
79
|
-
async stop() {
|
|
80
|
-
if (!this.isRecording) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
// Flush any remaining audio data
|
|
86
|
-
if (this.audioWorkletNode) {
|
|
87
|
-
this.audioWorkletNode.port.postMessage({ type: 'flush' });
|
|
88
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Disconnect and cleanup
|
|
92
|
-
if (this.mediaStream) {
|
|
93
|
-
this.mediaStream.getTracks().forEach(track => track.stop());
|
|
94
|
-
this.mediaStream = null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
98
|
-
await this.audioContext.close();
|
|
99
|
-
this.audioContext = null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.audioWorkletNode = null;
|
|
103
|
-
this.isRecording = false;
|
|
104
|
-
this.emit('recordingStopped');
|
|
105
|
-
|
|
106
|
-
} catch (error) {
|
|
107
|
-
this.emit('error', error);
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get recording status
|
|
114
|
-
*/
|
|
115
|
-
getStatus() {
|
|
116
|
-
return {
|
|
117
|
-
isRecording: this.isRecording,
|
|
118
|
-
audioContextState: this.audioContext ? this.audioContext.state : 'closed'
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Cleanup resources
|
|
124
|
-
*/
|
|
125
|
-
destroy() {
|
|
126
|
-
this.stop();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConnectionManager - Global connection manager to prevent multiple connections to the same URL
|
|
3
|
-
*/
|
|
4
|
-
class ConnectionManager {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.connections = new Map(); // Map of URL -> connection info
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Register a connection attempt
|
|
11
|
-
*/
|
|
12
|
-
registerConnection(url, connectionId) {
|
|
13
|
-
if (!this.connections.has(url)) {
|
|
14
|
-
this.connections.set(url, {
|
|
15
|
-
connectionId,
|
|
16
|
-
timestamp: Date.now(),
|
|
17
|
-
count: 1
|
|
18
|
-
});
|
|
19
|
-
console.log(`🔌 ConnectionManager: Registered connection ${connectionId} for ${url}`);
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const existing = this.connections.get(url);
|
|
24
|
-
const timeSinceLastConnection = Date.now() - existing.timestamp;
|
|
25
|
-
|
|
26
|
-
// If it's been more than 30 seconds since the last connection, allow it
|
|
27
|
-
if (timeSinceLastConnection > 30000) {
|
|
28
|
-
this.connections.set(url, {
|
|
29
|
-
connectionId,
|
|
30
|
-
timestamp: Date.now(),
|
|
31
|
-
count: 1
|
|
32
|
-
});
|
|
33
|
-
console.log(`🔌 ConnectionManager: Allowed new connection ${connectionId} for ${url} (old connection was ${timeSinceLastConnection}ms ago)`);
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Otherwise, prevent the connection
|
|
38
|
-
existing.count++;
|
|
39
|
-
console.log(`🔌 ConnectionManager: Blocked connection ${connectionId} for ${url} (${existing.count} attempts in ${timeSinceLastConnection}ms)`);
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Unregister a connection
|
|
45
|
-
*/
|
|
46
|
-
unregisterConnection(url, connectionId) {
|
|
47
|
-
const existing = this.connections.get(url);
|
|
48
|
-
if (existing && existing.connectionId === connectionId) {
|
|
49
|
-
this.connections.delete(url);
|
|
50
|
-
console.log(`🔌 ConnectionManager: Unregistered connection ${connectionId} for ${url}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Check if a connection is allowed
|
|
56
|
-
*/
|
|
57
|
-
isConnectionAllowed(url) {
|
|
58
|
-
const existing = this.connections.get(url);
|
|
59
|
-
if (!existing) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const timeSinceLastConnection = Date.now() - existing.timestamp;
|
|
64
|
-
return timeSinceLastConnection > 30000; // Allow if more than 30 seconds ago
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get connection info
|
|
69
|
-
*/
|
|
70
|
-
getConnectionInfo(url) {
|
|
71
|
-
return this.connections.get(url);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Clear all connections (useful for testing)
|
|
76
|
-
*/
|
|
77
|
-
clearAll() {
|
|
78
|
-
this.connections.clear();
|
|
79
|
-
console.log('🔌 ConnectionManager: Cleared all connections');
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Global instance
|
|
84
|
-
const connectionManager = new ConnectionManager();
|
|
85
|
-
|
|
86
|
-
export default connectionManager;
|
package/src/core/EventEmitter.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EventEmitter - Simple event system for the VoiceSDK
|
|
3
|
-
*/
|
|
4
|
-
export default class EventEmitter {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.events = {};
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Add event listener
|
|
11
|
-
*/
|
|
12
|
-
on(event, callback) {
|
|
13
|
-
if (!this.events[event]) {
|
|
14
|
-
this.events[event] = [];
|
|
15
|
-
}
|
|
16
|
-
this.events[event].push(callback);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Remove event listener
|
|
21
|
-
*/
|
|
22
|
-
off(event, callback) {
|
|
23
|
-
if (!this.events[event]) return;
|
|
24
|
-
|
|
25
|
-
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Emit event
|
|
30
|
-
*/
|
|
31
|
-
emit(event, ...args) {
|
|
32
|
-
if (!this.events[event]) return;
|
|
33
|
-
|
|
34
|
-
this.events[event].forEach(callback => {
|
|
35
|
-
try {
|
|
36
|
-
callback(...args);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error(`Error in event listener for ${event}:`, error);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Remove all listeners for an event
|
|
45
|
-
*/
|
|
46
|
-
removeAllListeners(event) {
|
|
47
|
-
if (event) {
|
|
48
|
-
delete this.events[event];
|
|
49
|
-
} else {
|
|
50
|
-
this.events = {};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|