@estuary-ai/sdk 0.1.22 → 0.1.24

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,4 +1,4 @@
1
- import { EstuaryError } from './chunk-6M5LSBMK.mjs';
1
+ import { EstuaryError } from './chunk-W5QYPYX3.mjs';
2
2
 
3
3
  // src/voice/livekit-voice.ts
4
4
  var LiveKitVoiceManager = class {
@@ -8,6 +8,13 @@ var LiveKitVoiceManager = class {
8
8
  // livekit-client Room (dynamically imported)
9
9
  _isMuted = false;
10
10
  _isActive = false;
11
+ speakingStateCallback = null;
12
+ audioLevelCallback = null;
13
+ // Audio analyser (via livekit-client's createAudioAnalyser)
14
+ calculateVolume = null;
15
+ analyserCleanup = null;
16
+ audioLevelPollTimer = null;
17
+ _isBotSpeaking = false;
11
18
  constructor(socketManager, logger) {
12
19
  this.socketManager = socketManager;
13
20
  this.logger = logger;
@@ -18,6 +25,12 @@ var LiveKitVoiceManager = class {
18
25
  get isActive() {
19
26
  return this._isActive;
20
27
  }
28
+ setSpeakingStateCallback(cb) {
29
+ this.speakingStateCallback = cb;
30
+ }
31
+ setAudioLevelCallback(cb) {
32
+ this.audioLevelCallback = cb;
33
+ }
21
34
  async start() {
22
35
  if (this._isActive) {
23
36
  throw new EstuaryError("VOICE_ALREADY_ACTIVE" /* VOICE_ALREADY_ACTIVE */, "Voice is already active");
@@ -57,16 +70,24 @@ var LiveKitVoiceManager = class {
57
70
  }
58
71
  audioElement.play().catch(() => {
59
72
  });
73
+ this.setupAnalyser(track);
74
+ if (this._isBotSpeaking) {
75
+ setTimeout(() => this.startAudioLevelPolling(), 50);
76
+ }
60
77
  }
61
78
  });
62
79
  this.room.on(RoomEvent.TrackUnsubscribed, (track) => {
63
80
  if (track.kind === Track.Kind.Audio) {
81
+ this.teardownAnalyser();
64
82
  track.detach().forEach((el) => el.remove());
65
83
  }
66
84
  });
67
85
  this.room.on(RoomEvent.Disconnected, () => {
68
86
  this.logger.debug("LiveKit room disconnected");
69
87
  this._isActive = false;
88
+ this._isBotSpeaking = false;
89
+ this.teardownAnalyser();
90
+ this.speakingStateCallback?.(false);
70
91
  });
71
92
  try {
72
93
  await this.room.connect(tokenData.url, tokenData.token);
@@ -80,6 +101,23 @@ var LiveKitVoiceManager = class {
80
101
  err
81
102
  );
82
103
  }
104
+ this.room.on(
105
+ RoomEvent.ParticipantAttributesChanged,
106
+ (changedAttributes, participant) => {
107
+ if (participant === this.room?.localParticipant) return;
108
+ const state = changedAttributes["estuary.state"];
109
+ if (state === "speaking") {
110
+ this._isBotSpeaking = true;
111
+ this.speakingStateCallback?.(true);
112
+ this.startAudioLevelPolling();
113
+ } else if (state === "idle") {
114
+ this._isBotSpeaking = false;
115
+ this.stopAudioLevelPolling();
116
+ this.speakingStateCallback?.(false);
117
+ this.audioLevelCallback?.(0);
118
+ }
119
+ }
120
+ );
83
121
  try {
84
122
  await this.room.localParticipant.setMicrophoneEnabled(true);
85
123
  this.logger.debug("Microphone enabled");
@@ -103,6 +141,9 @@ var LiveKitVoiceManager = class {
103
141
  this.socketManager.emitEvent("livekit_leave");
104
142
  } catch {
105
143
  }
144
+ this._isBotSpeaking = false;
145
+ this.teardownAnalyser();
146
+ this.speakingStateCallback?.(false);
106
147
  if (this.room) {
107
148
  for (const [, publication] of this.room.localParticipant.trackPublications) {
108
149
  if (publication.track) {
@@ -123,6 +164,10 @@ var LiveKitVoiceManager = class {
123
164
  this.logger.debug("Mute toggled:", this._isMuted);
124
165
  }
125
166
  dispose() {
167
+ this.speakingStateCallback = null;
168
+ this.audioLevelCallback = null;
169
+ this._isBotSpeaking = false;
170
+ this.teardownAnalyser();
126
171
  if (this.room) {
127
172
  this.room.disconnect();
128
173
  this.room = null;
@@ -130,6 +175,53 @@ var LiveKitVoiceManager = class {
130
175
  this._isActive = false;
131
176
  this._isMuted = false;
132
177
  }
178
+ // ─── Audio Analyser (livekit-client built-in) ───────────────────
179
+ async setupAnalyser(track) {
180
+ this.teardownAnalyser();
181
+ try {
182
+ const { createAudioAnalyser } = await import('livekit-client');
183
+ const { analyser, calculateVolume, cleanup } = createAudioAnalyser(track, {
184
+ fftSize: 256,
185
+ smoothingTimeConstant: 0.3
186
+ });
187
+ if (analyser.context.state === "suspended") {
188
+ await analyser.context.resume();
189
+ }
190
+ this.calculateVolume = calculateVolume;
191
+ this.analyserCleanup = cleanup;
192
+ this.logger.debug("Audio analyser created for bot track");
193
+ } catch (err) {
194
+ this.logger.debug("Failed to create audio analyser:", err);
195
+ }
196
+ }
197
+ teardownAnalyser() {
198
+ this.stopAudioLevelPolling();
199
+ if (this.analyserCleanup) {
200
+ this.analyserCleanup().catch(() => {
201
+ });
202
+ this.analyserCleanup = null;
203
+ }
204
+ this.calculateVolume = null;
205
+ }
206
+ startAudioLevelPolling() {
207
+ if (this.audioLevelPollTimer !== null) return;
208
+ if (!this.calculateVolume) return;
209
+ this.audioLevelPollTimer = setInterval(() => {
210
+ if (!this.calculateVolume) {
211
+ this.stopAudioLevelPolling();
212
+ return;
213
+ }
214
+ const vol = this.calculateVolume();
215
+ this.audioLevelCallback?.(vol);
216
+ }, 33);
217
+ }
218
+ stopAudioLevelPolling() {
219
+ if (this.audioLevelPollTimer !== null) {
220
+ clearInterval(this.audioLevelPollTimer);
221
+ this.audioLevelPollTimer = null;
222
+ }
223
+ }
224
+ // ─── Private ────────────────────────────────────────────────────
133
225
  requestToken() {
134
226
  return new Promise((resolve, reject) => {
135
227
  const timeout = setTimeout(() => {
@@ -150,5 +242,5 @@ var LiveKitVoiceManager = class {
150
242
  };
151
243
 
152
244
  export { LiveKitVoiceManager };
153
- //# sourceMappingURL=livekit-voice-RWXL7IXC.mjs.map
154
- //# sourceMappingURL=livekit-voice-RWXL7IXC.mjs.map
245
+ //# sourceMappingURL=livekit-voice-PV3TGH2Q.mjs.map
246
+ //# sourceMappingURL=livekit-voice-PV3TGH2Q.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/voice/livekit-voice.ts"],"names":[],"mappings":"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","file":"livekit-voice-PV3TGH2Q.mjs","sourcesContent":["import type { VoiceManager, LiveKitTokenResponse } from '../types';\nimport type { SocketManager } from '../connection/socket-manager';\nimport type { Logger } from '../utils/logger';\nimport { EstuaryError, ErrorCode } from '../errors';\n\nexport class LiveKitVoiceManager implements VoiceManager {\n private socketManager: SocketManager;\n private logger: Logger;\n private room: any = null; // livekit-client Room (dynamically imported)\n private _isMuted = false;\n private _isActive = false;\n private speakingStateCallback: ((speaking: boolean) => void) | null = null;\n private audioLevelCallback: ((level: number) => void) | null = null;\n\n // Audio analyser (via livekit-client's createAudioAnalyser)\n private calculateVolume: (() => number) | null = null;\n private analyserCleanup: (() => Promise<void>) | null = null;\n private audioLevelPollTimer: ReturnType<typeof setInterval> | null = null;\n private _isBotSpeaking = false;\n\n constructor(socketManager: SocketManager, logger: Logger) {\n this.socketManager = socketManager;\n this.logger = logger;\n }\n\n get isMuted(): boolean {\n return this._isMuted;\n }\n\n get isActive(): boolean {\n return this._isActive;\n }\n\n setSpeakingStateCallback(cb: (speaking: boolean) => void): void {\n this.speakingStateCallback = cb;\n }\n\n setAudioLevelCallback(cb: (level: number) => void): void {\n this.audioLevelCallback = cb;\n }\n\n async start(): Promise<void> {\n if (this._isActive) {\n throw new EstuaryError(ErrorCode.VOICE_ALREADY_ACTIVE, 'Voice is already active');\n }\n\n let Room: any;\n let RoomEvent: any;\n let Track: any;\n try {\n const lk = await import('livekit-client');\n Room = lk.Room;\n RoomEvent = lk.RoomEvent;\n Track = lk.Track;\n } catch {\n throw new EstuaryError(\n ErrorCode.LIVEKIT_UNAVAILABLE,\n 'livekit-client package is not installed',\n );\n }\n\n // Request token from server\n const tokenData = await this.requestToken();\n\n // Create and configure room\n this.room = new Room({\n adaptiveStream: true,\n dynacast: true,\n audioCaptureDefaults: {\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n\n // Handle remote audio tracks (bot audio)\n this.room.on(RoomEvent.TrackSubscribed, (\n track: any,\n _publication: any,\n participant: any,\n ) => {\n if (track.kind === Track.Kind.Audio) {\n this.logger.debug('Bot audio track subscribed from', participant.identity);\n const audioElement = track.attach();\n audioElement.autoplay = true;\n audioElement.style.display = 'none';\n if (typeof document !== 'undefined') {\n document.body.appendChild(audioElement);\n }\n audioElement.play().catch(() => {});\n\n // Set up audio analyser for real-time level metering\n this.setupAnalyser(track);\n if (this._isBotSpeaking) {\n // Async setupAnalyser may not have completed yet — retry shortly\n setTimeout(() => this.startAudioLevelPolling(), 50);\n }\n }\n });\n\n this.room.on(RoomEvent.TrackUnsubscribed, (track: any) => {\n if (track.kind === Track.Kind.Audio) {\n this.teardownAnalyser();\n track.detach().forEach((el: HTMLMediaElement) => el.remove());\n }\n });\n\n this.room.on(RoomEvent.Disconnected, () => {\n this.logger.debug('LiveKit room disconnected');\n this._isActive = false;\n this._isBotSpeaking = false;\n this.teardownAnalyser();\n this.speakingStateCallback?.(false);\n });\n\n // Connect to room\n try {\n await this.room.connect(tokenData.url, tokenData.token);\n this.logger.debug('Connected to LiveKit room:', tokenData.room);\n } catch (err) {\n this.room = null;\n const reason = err instanceof Error ? `: ${err.message}` : '';\n throw new EstuaryError(\n ErrorCode.CONNECTION_FAILED,\n `Failed to connect to LiveKit room${reason}`,\n err,\n );\n }\n\n // Listen for participant attribute changes (speaking state from backend)\n this.room.on(RoomEvent.ParticipantAttributesChanged,\n (changedAttributes: Record<string, string>, participant: any) => {\n if (participant === this.room?.localParticipant) return;\n const state = changedAttributes['estuary.state'];\n if (state === 'speaking') {\n this._isBotSpeaking = true;\n this.speakingStateCallback?.(true);\n this.startAudioLevelPolling();\n } else if (state === 'idle') {\n this._isBotSpeaking = false;\n this.stopAudioLevelPolling();\n this.speakingStateCallback?.(false);\n this.audioLevelCallback?.(0);\n }\n }\n );\n\n // Enable microphone\n try {\n await this.room.localParticipant.setMicrophoneEnabled(true);\n this.logger.debug('Microphone enabled');\n } catch (err) {\n this.room.disconnect();\n this.room = null;\n const reason = err instanceof Error ? `: ${err.message}` : '';\n throw new EstuaryError(\n ErrorCode.MICROPHONE_DENIED,\n `Failed to enable microphone${reason}`,\n err,\n );\n }\n\n // Notify backend\n this.socketManager.emitEvent('livekit_join', { room: tokenData.room });\n this._isActive = true;\n this.logger.debug('LiveKit voice started');\n }\n\n async stop(): Promise<void> {\n if (!this._isActive) return;\n\n try {\n this.socketManager.emitEvent('livekit_leave');\n } catch {\n // May not be connected\n }\n\n this._isBotSpeaking = false;\n this.teardownAnalyser();\n\n // Fire final \"stopped\" if bot was considered speaking\n this.speakingStateCallback?.(false);\n\n if (this.room) {\n // Stop local tracks\n for (const [, publication] of this.room.localParticipant.trackPublications) {\n if (publication.track) {\n publication.track.stop();\n }\n }\n this.room.disconnect();\n this.room = null;\n }\n\n this._isActive = false;\n this._isMuted = false;\n this.logger.debug('LiveKit voice stopped');\n }\n\n toggleMute(): void {\n if (!this._isActive || !this.room) return;\n this._isMuted = !this._isMuted;\n this.room.localParticipant.setMicrophoneEnabled(!this._isMuted);\n this.logger.debug('Mute toggled:', this._isMuted);\n }\n\n dispose(): void {\n this.speakingStateCallback = null;\n this.audioLevelCallback = null;\n this._isBotSpeaking = false;\n this.teardownAnalyser();\n\n if (this.room) {\n this.room.disconnect();\n this.room = null;\n }\n this._isActive = false;\n this._isMuted = false;\n }\n\n // ─── Audio Analyser (livekit-client built-in) ───────────────────\n\n private async setupAnalyser(track: any): Promise<void> {\n this.teardownAnalyser();\n try {\n const { createAudioAnalyser } = await import('livekit-client');\n const { analyser, calculateVolume, cleanup } = createAudioAnalyser(track, {\n fftSize: 256,\n smoothingTimeConstant: 0.3,\n });\n // Resume the AudioContext — it may start suspended due to browser autoplay policy.\n // The user has already clicked to start voice, so resume() will succeed.\n if (analyser.context.state === 'suspended') {\n await (analyser.context as AudioContext).resume();\n }\n this.calculateVolume = calculateVolume;\n this.analyserCleanup = cleanup;\n this.logger.debug('Audio analyser created for bot track');\n } catch (err) {\n this.logger.debug('Failed to create audio analyser:', err);\n }\n }\n\n private teardownAnalyser(): void {\n this.stopAudioLevelPolling();\n if (this.analyserCleanup) {\n this.analyserCleanup().catch(() => {});\n this.analyserCleanup = null;\n }\n this.calculateVolume = null;\n }\n\n private startAudioLevelPolling(): void {\n if (this.audioLevelPollTimer !== null) return;\n if (!this.calculateVolume) return;\n\n // ~30fps polling\n this.audioLevelPollTimer = setInterval(() => {\n if (!this.calculateVolume) {\n this.stopAudioLevelPolling();\n return;\n }\n const vol = this.calculateVolume();\n this.audioLevelCallback?.(vol);\n }, 33);\n }\n\n private stopAudioLevelPolling(): void {\n if (this.audioLevelPollTimer !== null) {\n clearInterval(this.audioLevelPollTimer);\n this.audioLevelPollTimer = null;\n }\n }\n\n // ─── Private ────────────────────────────────────────────────────\n\n private requestToken(): Promise<LiveKitTokenResponse> {\n return new Promise<LiveKitTokenResponse>((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.socketManager.onLiveKitToken(() => {}); // clear callback\n reject(new EstuaryError(\n ErrorCode.CONNECTION_TIMEOUT,\n 'Timed out waiting for LiveKit token',\n ));\n }, 10000);\n\n this.socketManager.onLiveKitToken((data: LiveKitTokenResponse) => {\n clearTimeout(timeout);\n resolve(data);\n });\n\n this.socketManager.emitEvent('livekit_token');\n });\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { EstuaryError } from './chunk-6M5LSBMK.mjs';
1
+ import { EstuaryError } from './chunk-W5QYPYX3.mjs';
2
2
 
3
3
  // src/audio/audio-utils.ts
4
4
  function resample(input, fromRate, toRate) {
@@ -173,5 +173,5 @@ var WebSocketVoiceManager = class {
173
173
  };
174
174
 
175
175
  export { WebSocketVoiceManager };
176
- //# sourceMappingURL=websocket-voice-IFM6J5ES.mjs.map
177
- //# sourceMappingURL=websocket-voice-IFM6J5ES.mjs.map
176
+ //# sourceMappingURL=websocket-voice-6DMYBGHP.mjs.map
177
+ //# sourceMappingURL=websocket-voice-6DMYBGHP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audio/audio-utils.ts","../src/voice/websocket-voice.ts"],"names":[],"mappings":"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","file":"websocket-voice-6DMYBGHP.mjs","sourcesContent":["export function resample(input: Float32Array, fromRate: number, toRate: number): Float32Array {\n const ratio = fromRate / toRate;\n const outputLength = Math.round(input.length / ratio);\n const output = new Float32Array(outputLength);\n for (let i = 0; i < outputLength; i++) {\n const srcIndex = i * ratio;\n const low = Math.floor(srcIndex);\n const high = Math.min(low + 1, input.length - 1);\n const frac = srcIndex - low;\n output[i] = input[low] * (1 - frac) + input[high] * frac;\n }\n return output;\n}\n\nexport function float32ToInt16(float32: Float32Array): Int16Array {\n const int16 = new Int16Array(float32.length);\n for (let i = 0; i < float32.length; i++) {\n const clamped = Math.max(-1, Math.min(1, float32[i]));\n int16[i] = clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff;\n }\n return int16;\n}\n\nexport function uint8ArrayToBase64(bytes: Uint8Array): string {\n if (typeof btoa === 'function') {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n }\n // Node.js fallback\n return Buffer.from(bytes).toString('base64');\n}\n","import type { VoiceManager } from '../types';\nimport type { SocketManager } from '../connection/socket-manager';\nimport type { Logger } from '../utils/logger';\nimport { EstuaryError, ErrorCode } from '../errors';\nimport { resample, float32ToInt16, uint8ArrayToBase64 } from '../audio/audio-utils';\n\nexport class WebSocketVoiceManager implements VoiceManager {\n private socketManager: SocketManager;\n private sampleRate: number;\n private logger: Logger;\n private audioContext: AudioContext | null = null;\n private mediaStream: MediaStream | null = null;\n private scriptProcessor: ScriptProcessorNode | null = null;\n private sourceNode: MediaStreamAudioSourceNode | null = null;\n private zeroGainNode: GainNode | null = null;\n private _isMuted = false;\n private _isSuppressed = false;\n private _isActive = false;\n\n constructor(socketManager: SocketManager, sampleRate: number, logger: Logger) {\n this.socketManager = socketManager;\n this.sampleRate = sampleRate;\n this.logger = logger;\n }\n\n get isMuted(): boolean {\n return this._isMuted;\n }\n\n get isActive(): boolean {\n return this._isActive;\n }\n\n async start(): Promise<void> {\n if (this._isActive) {\n throw new EstuaryError(ErrorCode.VOICE_ALREADY_ACTIVE, 'Voice is already active');\n }\n\n if (typeof AudioContext === 'undefined' && typeof (globalThis as any).webkitAudioContext === 'undefined') {\n throw new EstuaryError(ErrorCode.VOICE_NOT_SUPPORTED, 'AudioContext is not available in this environment');\n }\n\n let stream: MediaStream;\n try {\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n sampleRate: this.sampleRate,\n channelCount: 1,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n } catch (err) {\n throw new EstuaryError(\n ErrorCode.MICROPHONE_DENIED,\n 'Microphone access denied',\n err,\n );\n }\n\n this.mediaStream = stream;\n const AudioCtx = globalThis.AudioContext || (globalThis as any).webkitAudioContext;\n this.audioContext = new AudioCtx({ sampleRate: this.sampleRate });\n\n this.sourceNode = this.audioContext.createMediaStreamSource(stream);\n this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);\n\n const nativeRate = this.audioContext.sampleRate;\n const targetRate = this.sampleRate;\n\n this.scriptProcessor.onaudioprocess = (event: AudioProcessingEvent) => {\n if (this._isMuted || this._isSuppressed) return;\n\n const inputData = event.inputBuffer.getChannelData(0);\n let pcmFloat: Float32Array;\n\n if (nativeRate !== targetRate) {\n pcmFloat = resample(inputData, nativeRate, targetRate);\n } else {\n pcmFloat = inputData;\n }\n\n const pcm16 = float32ToInt16(pcmFloat);\n const base64 = uint8ArrayToBase64(new Uint8Array(pcm16.buffer));\n\n try {\n this.socketManager.emitEvent('stream_audio', { audio: base64 });\n } catch {\n // Not connected — ignore, will be handled by disconnect logic\n }\n };\n\n this.sourceNode.connect(this.scriptProcessor);\n this.zeroGainNode = this.audioContext.createGain();\n this.zeroGainNode.gain.value = 0;\n this.scriptProcessor.connect(this.zeroGainNode);\n this.zeroGainNode.connect(this.audioContext.destination);\n\n this._isActive = true;\n this.socketManager.emitEvent('start_voice');\n this.logger.debug('WebSocket voice started');\n }\n\n async stop(): Promise<void> {\n if (!this._isActive) return;\n\n try {\n this.socketManager.emitEvent('stop_voice');\n } catch {\n // May not be connected\n }\n\n this.cleanup();\n this._isActive = false;\n this._isMuted = false;\n this._isSuppressed = false;\n this.logger.debug('WebSocket voice stopped');\n }\n\n toggleMute(): void {\n if (!this._isActive || !this.mediaStream) return;\n this._isMuted = !this._isMuted;\n for (const track of this.mediaStream.getAudioTracks()) {\n track.enabled = !this._isMuted;\n }\n this.logger.debug('Mute toggled:', this._isMuted);\n }\n\n setSuppressed(suppressed: boolean): void {\n this._isSuppressed = suppressed;\n this.logger.debug('Audio suppression:', suppressed ? 'on' : 'off');\n }\n\n dispose(): void {\n this.cleanup();\n this._isActive = false;\n this._isMuted = false;\n this._isSuppressed = false;\n }\n\n private cleanup(): void {\n if (this.scriptProcessor) {\n this.scriptProcessor.onaudioprocess = null;\n this.scriptProcessor.disconnect();\n this.scriptProcessor = null;\n }\n if (this.zeroGainNode) {\n this.zeroGainNode.disconnect();\n this.zeroGainNode = null;\n }\n if (this.sourceNode) {\n this.sourceNode.disconnect();\n this.sourceNode = null;\n }\n if (this.mediaStream) {\n for (const track of this.mediaStream.getTracks()) {\n track.stop();\n }\n this.mediaStream = null;\n }\n if (this.audioContext) {\n this.audioContext.close().catch(() => {});\n this.audioContext = null;\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "@estuary-ai/sdk",
3
- "version": "0.1.22",
4
- "description": "TypeScript SDK for the Estuary real-time AI conversation platform",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": {
11
- "types": "./dist/index.d.mts",
12
- "default": "./dist/index.mjs"
13
- },
14
- "require": {
15
- "types": "./dist/index.d.ts",
16
- "default": "./dist/index.js"
17
- }
18
- }
19
- },
20
- "files": [
21
- "dist",
22
- "README.md"
23
- ],
24
- "scripts": {
25
- "build": "tsup",
26
- "dev": "tsup --watch",
27
- "typecheck": "tsc --noEmit",
28
- "test": "vitest run",
29
- "test:watch": "vitest",
30
- "lint": "eslint src/",
31
- "format": "prettier --write src/ tests/"
32
- },
33
- "keywords": [
34
- "estuary",
35
- "ai",
36
- "conversational-ai",
37
- "voice",
38
- "livekit",
39
- "socket.io",
40
- "realtime"
41
- ],
42
- "author": "Estuary Systems, Inc.",
43
- "license": "MIT",
44
- "repository": {
45
- "type": "git",
46
- "url": "https://github.com/estuary-ai/estuary-ts-sdk"
47
- },
48
- "dependencies": {
49
- "socket.io-client": "^4.8.0"
50
- },
51
- "peerDependencies": {
52
- "livekit-client": "^2.0.0"
53
- },
54
- "peerDependenciesMeta": {
55
- "livekit-client": {
56
- "optional": true
57
- }
58
- },
59
- "devDependencies": {
60
- "typescript": "^5.7.0",
61
- "tsup": "^8.4.0",
62
- "vitest": "^3.0.0",
63
- "eslint": "^9.0.0",
64
- "prettier": "^3.4.0",
65
- "@types/node": "^22.0.0",
66
- "livekit-client": "^2.0.0"
67
- },
68
- "engines": {
69
- "node": ">=18.0.0"
70
- }
71
- }
1
+ {
2
+ "name": "@estuary-ai/sdk",
3
+ "version": "0.1.24",
4
+ "description": "Web SDK for the Estuary real-time AI conversation platform",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "lint": "eslint src/",
31
+ "format": "prettier --write src/ tests/"
32
+ },
33
+ "keywords": [
34
+ "estuary",
35
+ "ai",
36
+ "conversational-ai",
37
+ "voice",
38
+ "livekit",
39
+ "socket.io",
40
+ "realtime"
41
+ ],
42
+ "author": "Estuary Systems, Inc.",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/estuary-ai/estuary-ts-sdk"
47
+ },
48
+ "dependencies": {
49
+ "socket.io-client": "^4.8.0"
50
+ },
51
+ "peerDependencies": {
52
+ "livekit-client": "^2.0.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "livekit-client": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "typescript": "^5.7.0",
61
+ "tsup": "^8.4.0",
62
+ "vitest": "^3.0.0",
63
+ "eslint": "^9.0.0",
64
+ "prettier": "^3.4.0",
65
+ "@types/node": "^22.0.0",
66
+ "livekit-client": "^2.0.0"
67
+ },
68
+ "engines": {
69
+ "node": ">=18.0.0"
70
+ }
71
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/errors.ts"],"names":["ErrorCode"],"mappings":";AAAO,IAAK,SAAA,qBAAAA,UAAAA,KAAL;AACL,EAAAA,WAAA,mBAAA,CAAA,GAAoB,mBAAA;AACpB,EAAAA,WAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,WAAA,oBAAA,CAAA,GAAqB,oBAAA;AACrB,EAAAA,WAAA,gBAAA,CAAA,GAAiB,gBAAA;AACjB,EAAAA,WAAA,qBAAA,CAAA,GAAsB,qBAAA;AACtB,EAAAA,WAAA,sBAAA,CAAA,GAAuB,sBAAA;AACvB,EAAAA,WAAA,kBAAA,CAAA,GAAmB,kBAAA;AACnB,EAAAA,WAAA,qBAAA,CAAA,GAAsB,qBAAA;AACtB,EAAAA,WAAA,mBAAA,CAAA,GAAoB,mBAAA;AACpB,EAAAA,WAAA,eAAA,CAAA,GAAgB,eAAA;AAChB,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AACb,EAAAA,WAAA,SAAA,CAAA,GAAU,SAAA;AAZA,EAAA,OAAAA,UAAAA;AAAA,CAAA,EAAA,SAAA,IAAA,EAAA;AAeL,IAAM,YAAA,GAAN,cAA2B,KAAA,CAAM;AAAA,EAC7B,IAAA;AAAA,EACA,OAAA;AAAA,EAET,WAAA,CAAY,IAAA,EAAiB,OAAA,EAAiB,OAAA,EAAmB;AAC/D,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AACF","file":"chunk-6M5LSBMK.mjs","sourcesContent":["export enum ErrorCode {\r\n CONNECTION_FAILED = 'CONNECTION_FAILED',\r\n AUTH_FAILED = 'AUTH_FAILED',\r\n CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT',\r\n QUOTA_EXCEEDED = 'QUOTA_EXCEEDED',\r\n VOICE_NOT_SUPPORTED = 'VOICE_NOT_SUPPORTED',\r\n VOICE_ALREADY_ACTIVE = 'VOICE_ALREADY_ACTIVE',\r\n VOICE_NOT_ACTIVE = 'VOICE_NOT_ACTIVE',\r\n LIVEKIT_UNAVAILABLE = 'LIVEKIT_UNAVAILABLE',\r\n MICROPHONE_DENIED = 'MICROPHONE_DENIED',\r\n NOT_CONNECTED = 'NOT_CONNECTED',\r\n REST_ERROR = 'REST_ERROR',\r\n UNKNOWN = 'UNKNOWN',\r\n}\r\n\r\nexport class EstuaryError extends Error {\r\n readonly code: ErrorCode;\r\n readonly details?: unknown;\r\n\r\n constructor(code: ErrorCode, message: string, details?: unknown) {\r\n super(message);\r\n this.name = 'EstuaryError';\r\n this.code = code;\r\n this.details = details;\r\n }\r\n}\r\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/voice/livekit-voice.ts"],"names":[],"mappings":"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","file":"livekit-voice-RWXL7IXC.mjs","sourcesContent":["import type { VoiceManager, LiveKitTokenResponse } from '../types';\r\nimport type { SocketManager } from '../connection/socket-manager';\r\nimport type { Logger } from '../utils/logger';\r\nimport { EstuaryError, ErrorCode } from '../errors';\r\n\r\nexport class LiveKitVoiceManager implements VoiceManager {\r\n private socketManager: SocketManager;\r\n private logger: Logger;\r\n private room: any = null; // livekit-client Room (dynamically imported)\r\n private _isMuted = false;\r\n private _isActive = false;\r\n\r\n constructor(socketManager: SocketManager, logger: Logger) {\r\n this.socketManager = socketManager;\r\n this.logger = logger;\r\n }\r\n\r\n get isMuted(): boolean {\r\n return this._isMuted;\r\n }\r\n\r\n get isActive(): boolean {\r\n return this._isActive;\r\n }\r\n\r\n async start(): Promise<void> {\r\n if (this._isActive) {\r\n throw new EstuaryError(ErrorCode.VOICE_ALREADY_ACTIVE, 'Voice is already active');\r\n }\r\n\r\n let Room: any;\r\n let RoomEvent: any;\r\n let Track: any;\r\n try {\r\n const lk = await import('livekit-client');\r\n Room = lk.Room;\r\n RoomEvent = lk.RoomEvent;\r\n Track = lk.Track;\r\n } catch {\r\n throw new EstuaryError(\r\n ErrorCode.LIVEKIT_UNAVAILABLE,\r\n 'livekit-client package is not installed',\r\n );\r\n }\r\n\r\n // Request token from server\r\n const tokenData = await this.requestToken();\r\n\r\n // Create and configure room\r\n this.room = new Room({\r\n adaptiveStream: true,\r\n dynacast: true,\r\n audioCaptureDefaults: {\r\n echoCancellation: true,\r\n noiseSuppression: true,\r\n autoGainControl: true,\r\n },\r\n });\r\n\r\n // Handle remote audio tracks (bot audio)\r\n this.room.on(RoomEvent.TrackSubscribed, (\r\n track: any,\r\n _publication: any,\r\n participant: any,\r\n ) => {\r\n if (track.kind === Track.Kind.Audio) {\r\n this.logger.debug('Bot audio track subscribed from', participant.identity);\r\n const audioElement = track.attach();\r\n audioElement.autoplay = true;\r\n audioElement.style.display = 'none';\r\n if (typeof document !== 'undefined') {\r\n document.body.appendChild(audioElement);\r\n }\r\n audioElement.play().catch(() => {});\r\n }\r\n });\r\n\r\n this.room.on(RoomEvent.TrackUnsubscribed, (track: any) => {\r\n if (track.kind === Track.Kind.Audio) {\r\n track.detach().forEach((el: HTMLMediaElement) => el.remove());\r\n }\r\n });\r\n\r\n this.room.on(RoomEvent.Disconnected, () => {\r\n this.logger.debug('LiveKit room disconnected');\r\n this._isActive = false;\r\n });\r\n\r\n // Connect to room\r\n try {\r\n await this.room.connect(tokenData.url, tokenData.token);\r\n this.logger.debug('Connected to LiveKit room:', tokenData.room);\r\n } catch (err) {\r\n this.room = null;\r\n const reason = err instanceof Error ? `: ${err.message}` : '';\r\n throw new EstuaryError(\r\n ErrorCode.CONNECTION_FAILED,\r\n `Failed to connect to LiveKit room${reason}`,\r\n err,\r\n );\r\n }\r\n\r\n // Enable microphone\r\n try {\r\n await this.room.localParticipant.setMicrophoneEnabled(true);\r\n this.logger.debug('Microphone enabled');\r\n } catch (err) {\r\n this.room.disconnect();\r\n this.room = null;\r\n const reason = err instanceof Error ? `: ${err.message}` : '';\r\n throw new EstuaryError(\r\n ErrorCode.MICROPHONE_DENIED,\r\n `Failed to enable microphone${reason}`,\r\n err,\r\n );\r\n }\r\n\r\n // Notify backend\r\n this.socketManager.emitEvent('livekit_join', { room: tokenData.room });\r\n this._isActive = true;\r\n this.logger.debug('LiveKit voice started');\r\n }\r\n\r\n async stop(): Promise<void> {\r\n if (!this._isActive) return;\r\n\r\n try {\r\n this.socketManager.emitEvent('livekit_leave');\r\n } catch {\r\n // May not be connected\r\n }\r\n\r\n if (this.room) {\r\n // Stop local tracks\r\n for (const [, publication] of this.room.localParticipant.trackPublications) {\r\n if (publication.track) {\r\n publication.track.stop();\r\n }\r\n }\r\n this.room.disconnect();\r\n this.room = null;\r\n }\r\n\r\n this._isActive = false;\r\n this._isMuted = false;\r\n this.logger.debug('LiveKit voice stopped');\r\n }\r\n\r\n toggleMute(): void {\r\n if (!this._isActive || !this.room) return;\r\n this._isMuted = !this._isMuted;\r\n this.room.localParticipant.setMicrophoneEnabled(!this._isMuted);\r\n this.logger.debug('Mute toggled:', this._isMuted);\r\n }\r\n\r\n dispose(): void {\r\n if (this.room) {\r\n this.room.disconnect();\r\n this.room = null;\r\n }\r\n this._isActive = false;\r\n this._isMuted = false;\r\n }\r\n\r\n private requestToken(): Promise<LiveKitTokenResponse> {\r\n return new Promise<LiveKitTokenResponse>((resolve, reject) => {\r\n const timeout = setTimeout(() => {\r\n this.socketManager.onLiveKitToken(() => {}); // clear callback\r\n reject(new EstuaryError(\r\n ErrorCode.CONNECTION_TIMEOUT,\r\n 'Timed out waiting for LiveKit token',\r\n ));\r\n }, 10000);\r\n\r\n this.socketManager.onLiveKitToken((data: LiveKitTokenResponse) => {\r\n clearTimeout(timeout);\r\n resolve(data);\r\n });\r\n\r\n this.socketManager.emitEvent('livekit_token');\r\n });\r\n }\r\n}\r\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/audio/audio-utils.ts","../src/voice/websocket-voice.ts"],"names":[],"mappings":"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","file":"websocket-voice-IFM6J5ES.mjs","sourcesContent":["export function resample(input: Float32Array, fromRate: number, toRate: number): Float32Array {\r\n const ratio = fromRate / toRate;\r\n const outputLength = Math.round(input.length / ratio);\r\n const output = new Float32Array(outputLength);\r\n for (let i = 0; i < outputLength; i++) {\r\n const srcIndex = i * ratio;\r\n const low = Math.floor(srcIndex);\r\n const high = Math.min(low + 1, input.length - 1);\r\n const frac = srcIndex - low;\r\n output[i] = input[low] * (1 - frac) + input[high] * frac;\r\n }\r\n return output;\r\n}\r\n\r\nexport function float32ToInt16(float32: Float32Array): Int16Array {\r\n const int16 = new Int16Array(float32.length);\r\n for (let i = 0; i < float32.length; i++) {\r\n const clamped = Math.max(-1, Math.min(1, float32[i]));\r\n int16[i] = clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff;\r\n }\r\n return int16;\r\n}\r\n\r\nexport function uint8ArrayToBase64(bytes: Uint8Array): string {\r\n if (typeof btoa === 'function') {\r\n let binary = '';\r\n for (let i = 0; i < bytes.length; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n }\r\n // Node.js fallback\r\n return Buffer.from(bytes).toString('base64');\r\n}\r\n","import type { VoiceManager } from '../types';\r\nimport type { SocketManager } from '../connection/socket-manager';\r\nimport type { Logger } from '../utils/logger';\r\nimport { EstuaryError, ErrorCode } from '../errors';\r\nimport { resample, float32ToInt16, uint8ArrayToBase64 } from '../audio/audio-utils';\r\n\r\nexport class WebSocketVoiceManager implements VoiceManager {\r\n private socketManager: SocketManager;\r\n private sampleRate: number;\r\n private logger: Logger;\r\n private audioContext: AudioContext | null = null;\r\n private mediaStream: MediaStream | null = null;\r\n private scriptProcessor: ScriptProcessorNode | null = null;\r\n private sourceNode: MediaStreamAudioSourceNode | null = null;\r\n private zeroGainNode: GainNode | null = null;\r\n private _isMuted = false;\r\n private _isSuppressed = false;\r\n private _isActive = false;\r\n\r\n constructor(socketManager: SocketManager, sampleRate: number, logger: Logger) {\r\n this.socketManager = socketManager;\r\n this.sampleRate = sampleRate;\r\n this.logger = logger;\r\n }\r\n\r\n get isMuted(): boolean {\r\n return this._isMuted;\r\n }\r\n\r\n get isActive(): boolean {\r\n return this._isActive;\r\n }\r\n\r\n async start(): Promise<void> {\r\n if (this._isActive) {\r\n throw new EstuaryError(ErrorCode.VOICE_ALREADY_ACTIVE, 'Voice is already active');\r\n }\r\n\r\n if (typeof AudioContext === 'undefined' && typeof (globalThis as any).webkitAudioContext === 'undefined') {\r\n throw new EstuaryError(ErrorCode.VOICE_NOT_SUPPORTED, 'AudioContext is not available in this environment');\r\n }\r\n\r\n let stream: MediaStream;\r\n try {\r\n stream = await navigator.mediaDevices.getUserMedia({\r\n audio: {\r\n sampleRate: this.sampleRate,\r\n channelCount: 1,\r\n echoCancellation: true,\r\n noiseSuppression: true,\r\n autoGainControl: true,\r\n },\r\n });\r\n } catch (err) {\r\n throw new EstuaryError(\r\n ErrorCode.MICROPHONE_DENIED,\r\n 'Microphone access denied',\r\n err,\r\n );\r\n }\r\n\r\n this.mediaStream = stream;\r\n const AudioCtx = globalThis.AudioContext || (globalThis as any).webkitAudioContext;\r\n this.audioContext = new AudioCtx({ sampleRate: this.sampleRate });\r\n\r\n this.sourceNode = this.audioContext.createMediaStreamSource(stream);\r\n this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);\r\n\r\n const nativeRate = this.audioContext.sampleRate;\r\n const targetRate = this.sampleRate;\r\n\r\n this.scriptProcessor.onaudioprocess = (event: AudioProcessingEvent) => {\r\n if (this._isMuted || this._isSuppressed) return;\r\n\r\n const inputData = event.inputBuffer.getChannelData(0);\r\n let pcmFloat: Float32Array;\r\n\r\n if (nativeRate !== targetRate) {\r\n pcmFloat = resample(inputData, nativeRate, targetRate);\r\n } else {\r\n pcmFloat = inputData;\r\n }\r\n\r\n const pcm16 = float32ToInt16(pcmFloat);\r\n const base64 = uint8ArrayToBase64(new Uint8Array(pcm16.buffer));\r\n\r\n try {\r\n this.socketManager.emitEvent('stream_audio', { audio: base64 });\r\n } catch {\r\n // Not connected — ignore, will be handled by disconnect logic\r\n }\r\n };\r\n\r\n this.sourceNode.connect(this.scriptProcessor);\r\n this.zeroGainNode = this.audioContext.createGain();\r\n this.zeroGainNode.gain.value = 0;\r\n this.scriptProcessor.connect(this.zeroGainNode);\r\n this.zeroGainNode.connect(this.audioContext.destination);\r\n\r\n this._isActive = true;\r\n this.socketManager.emitEvent('start_voice');\r\n this.logger.debug('WebSocket voice started');\r\n }\r\n\r\n async stop(): Promise<void> {\r\n if (!this._isActive) return;\r\n\r\n try {\r\n this.socketManager.emitEvent('stop_voice');\r\n } catch {\r\n // May not be connected\r\n }\r\n\r\n this.cleanup();\r\n this._isActive = false;\r\n this._isMuted = false;\r\n this._isSuppressed = false;\r\n this.logger.debug('WebSocket voice stopped');\r\n }\r\n\r\n toggleMute(): void {\r\n if (!this._isActive || !this.mediaStream) return;\r\n this._isMuted = !this._isMuted;\r\n for (const track of this.mediaStream.getAudioTracks()) {\r\n track.enabled = !this._isMuted;\r\n }\r\n this.logger.debug('Mute toggled:', this._isMuted);\r\n }\r\n\r\n setSuppressed(suppressed: boolean): void {\r\n this._isSuppressed = suppressed;\r\n this.logger.debug('Audio suppression:', suppressed ? 'on' : 'off');\r\n }\r\n\r\n dispose(): void {\r\n this.cleanup();\r\n this._isActive = false;\r\n this._isMuted = false;\r\n this._isSuppressed = false;\r\n }\r\n\r\n private cleanup(): void {\r\n if (this.scriptProcessor) {\r\n this.scriptProcessor.onaudioprocess = null;\r\n this.scriptProcessor.disconnect();\r\n this.scriptProcessor = null;\r\n }\r\n if (this.zeroGainNode) {\r\n this.zeroGainNode.disconnect();\r\n this.zeroGainNode = null;\r\n }\r\n if (this.sourceNode) {\r\n this.sourceNode.disconnect();\r\n this.sourceNode = null;\r\n }\r\n if (this.mediaStream) {\r\n for (const track of this.mediaStream.getTracks()) {\r\n track.stop();\r\n }\r\n this.mediaStream = null;\r\n }\r\n if (this.audioContext) {\r\n this.audioContext.close().catch(() => {});\r\n this.audioContext = null;\r\n }\r\n }\r\n}\r\n"]}