@drawdream/livespeech 0.1.0

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/index.mjs ADDED
@@ -0,0 +1,938 @@
1
+ // src/types/regions.ts
2
+ var Region = {
3
+ /** Asia Pacific (Seoul) */
4
+ AP_NORTHEAST_2: "ap-northeast-2",
5
+ /** US West (Oregon) - Coming soon */
6
+ US_WEST_2: "us-west-2"
7
+ };
8
+ var REGION_ENDPOINTS = {
9
+ "ap-northeast-2": "wss://talk.drawdream.co.kr",
10
+ "us-west-2": "wss://talk..drawdream.ca"
11
+ // Coming soon
12
+ };
13
+ function getEndpointForRegion(region) {
14
+ const endpoint = REGION_ENDPOINTS[region];
15
+ if (!endpoint) {
16
+ throw new Error(`Unknown region: ${region}. Available regions: ${Object.keys(REGION_ENDPOINTS).join(", ")}`);
17
+ }
18
+ return endpoint;
19
+ }
20
+ function isValidRegion(value) {
21
+ return value in REGION_ENDPOINTS;
22
+ }
23
+
24
+ // src/types/messages.ts
25
+ function isServerMessage(data) {
26
+ return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string";
27
+ }
28
+ function parseServerMessage(json) {
29
+ try {
30
+ const data = JSON.parse(json);
31
+ if (isServerMessage(data)) {
32
+ return data;
33
+ }
34
+ return null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function serializeClientMessage(message) {
40
+ return JSON.stringify(message);
41
+ }
42
+
43
+ // src/utils/logger.ts
44
+ function createLogger(prefix, enabled) {
45
+ const formatMessage = (level, message) => {
46
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
47
+ return `[${timestamp}] [${prefix}] [${level.toUpperCase()}] ${message}`;
48
+ };
49
+ const noop = () => {
50
+ };
51
+ if (!enabled) {
52
+ return {
53
+ debug: noop,
54
+ info: noop,
55
+ warn: noop,
56
+ error: noop
57
+ };
58
+ }
59
+ return {
60
+ debug(message, ...args) {
61
+ console.debug(formatMessage("debug", message), ...args);
62
+ },
63
+ info(message, ...args) {
64
+ console.info(formatMessage("info", message), ...args);
65
+ },
66
+ warn(message, ...args) {
67
+ console.warn(formatMessage("warn", message), ...args);
68
+ },
69
+ error(message, ...args) {
70
+ console.error(formatMessage("error", message), ...args);
71
+ }
72
+ };
73
+ }
74
+
75
+ // src/utils/retry.ts
76
+ var DEFAULT_OPTIONS = {
77
+ maxAttempts: 5,
78
+ baseDelay: 1e3,
79
+ maxDelay: 3e4,
80
+ backoffMultiplier: 2,
81
+ jitter: true,
82
+ isRetryable: () => true
83
+ };
84
+ function calculateDelay(attempt, baseDelay, maxDelay, backoffMultiplier, jitter) {
85
+ const exponentialDelay = baseDelay * Math.pow(backoffMultiplier, attempt - 1);
86
+ const cappedDelay = Math.min(exponentialDelay, maxDelay);
87
+ if (jitter) {
88
+ const jitterRange = cappedDelay * 0.25;
89
+ const jitterValue = Math.random() * jitterRange * 2 - jitterRange;
90
+ return Math.max(0, Math.round(cappedDelay + jitterValue));
91
+ }
92
+ return Math.round(cappedDelay);
93
+ }
94
+ function sleep(ms) {
95
+ return new Promise((resolve) => {
96
+ setTimeout(resolve, ms);
97
+ });
98
+ }
99
+ var RetryController = class {
100
+ attempt = 0;
101
+ options;
102
+ aborted = false;
103
+ constructor(options = {}) {
104
+ this.options = { ...DEFAULT_OPTIONS, ...options };
105
+ }
106
+ /**
107
+ * Get current attempt number
108
+ */
109
+ get currentAttempt() {
110
+ return this.attempt;
111
+ }
112
+ /**
113
+ * Check if more retries are available
114
+ */
115
+ get canRetry() {
116
+ return !this.aborted && this.attempt < this.options.maxAttempts;
117
+ }
118
+ /**
119
+ * Get delay for next retry attempt
120
+ */
121
+ getNextDelay() {
122
+ return calculateDelay(
123
+ this.attempt + 1,
124
+ this.options.baseDelay,
125
+ this.options.maxDelay,
126
+ this.options.backoffMultiplier,
127
+ this.options.jitter
128
+ );
129
+ }
130
+ /**
131
+ * Record a retry attempt
132
+ * @returns The delay to wait before retrying, or null if no more retries
133
+ */
134
+ recordAttempt(error) {
135
+ if (!this.canRetry) {
136
+ return null;
137
+ }
138
+ if (error && !this.options.isRetryable(error)) {
139
+ return null;
140
+ }
141
+ this.attempt++;
142
+ const delay = this.getNextDelay();
143
+ if (this.options.onRetry && error) {
144
+ this.options.onRetry(this.attempt, delay, error);
145
+ }
146
+ return delay;
147
+ }
148
+ /**
149
+ * Reset the controller
150
+ */
151
+ reset() {
152
+ this.attempt = 0;
153
+ this.aborted = false;
154
+ }
155
+ /**
156
+ * Abort any further retries
157
+ */
158
+ abort() {
159
+ this.aborted = true;
160
+ }
161
+ };
162
+
163
+ // src/websocket/connection.ts
164
+ var WebSocketConnection = class {
165
+ ws = null;
166
+ state = "disconnected";
167
+ connectionId = null;
168
+ config;
169
+ logger;
170
+ events;
171
+ retryController;
172
+ pingInterval = null;
173
+ constructor(config, events = {}) {
174
+ this.config = config;
175
+ this.events = events;
176
+ this.logger = createLogger("WebSocket", config.debug);
177
+ this.retryController = new RetryController({
178
+ maxAttempts: config.maxReconnectAttempts,
179
+ baseDelay: config.reconnectDelay
180
+ });
181
+ }
182
+ /**
183
+ * Get current connection state
184
+ */
185
+ get currentState() {
186
+ return this.state;
187
+ }
188
+ /**
189
+ * Get connection ID
190
+ */
191
+ get id() {
192
+ return this.connectionId;
193
+ }
194
+ /**
195
+ * Check if connected
196
+ */
197
+ get isConnected() {
198
+ return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
199
+ }
200
+ /**
201
+ * Connect to WebSocket server
202
+ */
203
+ async connect() {
204
+ if (this.state === "connected" || this.state === "connecting") {
205
+ this.logger.warn("Already connected or connecting");
206
+ return;
207
+ }
208
+ this.state = "connecting";
209
+ this.retryController.reset();
210
+ await this.attemptConnection();
211
+ }
212
+ /**
213
+ * Disconnect from WebSocket server
214
+ */
215
+ disconnect() {
216
+ this.retryController.abort();
217
+ this.stopPingInterval();
218
+ if (this.ws) {
219
+ this.ws.close(1e3, "Client disconnect");
220
+ this.ws = null;
221
+ }
222
+ this.state = "disconnected";
223
+ this.connectionId = null;
224
+ this.logger.info("Disconnected");
225
+ }
226
+ /**
227
+ * Send a message
228
+ */
229
+ send(message) {
230
+ if (!this.isConnected) {
231
+ throw new Error("Not connected");
232
+ }
233
+ const data = serializeClientMessage(message);
234
+ this.logger.debug("Sending message:", message.action);
235
+ this.ws.send(data);
236
+ }
237
+ /**
238
+ * Attempt to establish connection
239
+ */
240
+ async attemptConnection() {
241
+ return new Promise((resolve, reject) => {
242
+ try {
243
+ const url = new URL(this.config.endpoint);
244
+ url.searchParams.set("apiKey", this.config.apiKey);
245
+ this.logger.info("Connecting to", url.origin);
246
+ this.ws = new WebSocket(url.toString());
247
+ const timeoutId = setTimeout(() => {
248
+ if (this.state === "connecting") {
249
+ this.ws?.close();
250
+ reject(new Error("Connection timeout"));
251
+ }
252
+ }, this.config.connectionTimeout);
253
+ this.ws.onopen = () => {
254
+ clearTimeout(timeoutId);
255
+ this.logger.info("Connected");
256
+ this.state = "connected";
257
+ this.connectionId = this.generateConnectionId();
258
+ this.retryController.reset();
259
+ this.startPingInterval();
260
+ this.events.onOpen?.(this.connectionId);
261
+ resolve();
262
+ };
263
+ this.ws.onclose = (event) => {
264
+ clearTimeout(timeoutId);
265
+ this.handleClose(event.code, event.reason);
266
+ if (this.state === "connecting") {
267
+ reject(new Error(`Connection closed: ${event.reason || "Unknown reason"}`));
268
+ }
269
+ };
270
+ this.ws.onerror = (_event) => {
271
+ clearTimeout(timeoutId);
272
+ const error = new Error("WebSocket error");
273
+ this.logger.error("Connection error");
274
+ this.events.onError?.(error);
275
+ if (this.state === "connecting") {
276
+ reject(error);
277
+ }
278
+ };
279
+ this.ws.onmessage = (event) => {
280
+ this.handleMessage(event.data);
281
+ };
282
+ } catch (error) {
283
+ reject(error);
284
+ }
285
+ });
286
+ }
287
+ /**
288
+ * Generate a client-side connection ID
289
+ */
290
+ generateConnectionId() {
291
+ return `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
292
+ }
293
+ /**
294
+ * Handle incoming message
295
+ */
296
+ handleMessage(data, onFirstConnect) {
297
+ const message = parseServerMessage(data);
298
+ if (!message) {
299
+ this.logger.warn("Invalid message received:", data);
300
+ return;
301
+ }
302
+ this.logger.debug("Received message:", message.type);
303
+ if (message.type === "connected") {
304
+ this.connectionId = message.connectionId;
305
+ this.state = "connected";
306
+ this.retryController.reset();
307
+ this.startPingInterval();
308
+ this.events.onOpen?.(message.connectionId);
309
+ onFirstConnect?.();
310
+ return;
311
+ }
312
+ if (message.type === "pong") {
313
+ this.logger.debug("Pong received");
314
+ return;
315
+ }
316
+ this.events.onMessage?.(message);
317
+ }
318
+ /**
319
+ * Handle connection close
320
+ */
321
+ handleClose(code, reason) {
322
+ this.logger.info("Connection closed:", code, reason);
323
+ this.stopPingInterval();
324
+ const wasConnected = this.state === "connected";
325
+ this.ws = null;
326
+ if (wasConnected && this.config.autoReconnect && code !== 1e3) {
327
+ this.handleReconnection();
328
+ } else {
329
+ this.state = "disconnected";
330
+ this.connectionId = null;
331
+ this.events.onClose?.(code, reason);
332
+ }
333
+ }
334
+ /**
335
+ * Handle reconnection logic
336
+ */
337
+ async handleReconnection() {
338
+ const delay = this.retryController.recordAttempt();
339
+ if (delay === null) {
340
+ this.logger.error("Max reconnection attempts reached");
341
+ this.state = "disconnected";
342
+ this.connectionId = null;
343
+ this.events.onClose?.(1006, "Max reconnection attempts reached");
344
+ return;
345
+ }
346
+ this.state = "reconnecting";
347
+ const attempt = this.retryController.currentAttempt;
348
+ const maxAttempts = this.config.maxReconnectAttempts;
349
+ this.logger.info(`Reconnecting in ${delay}ms (attempt ${attempt}/${maxAttempts})`);
350
+ this.events.onReconnecting?.(attempt, maxAttempts, delay);
351
+ await sleep(delay);
352
+ if (this.state !== "reconnecting") {
353
+ return;
354
+ }
355
+ try {
356
+ await this.attemptConnection();
357
+ } catch (error) {
358
+ this.logger.error("Reconnection failed:", error);
359
+ this.handleReconnection();
360
+ }
361
+ }
362
+ /**
363
+ * Start ping interval for keep-alive
364
+ */
365
+ startPingInterval() {
366
+ this.stopPingInterval();
367
+ this.pingInterval = setInterval(() => {
368
+ if (this.isConnected) {
369
+ this.send({ action: "ping" });
370
+ }
371
+ }, 3e4);
372
+ }
373
+ /**
374
+ * Stop ping interval
375
+ */
376
+ stopPingInterval() {
377
+ if (this.pingInterval) {
378
+ clearInterval(this.pingInterval);
379
+ this.pingInterval = null;
380
+ }
381
+ }
382
+ };
383
+
384
+ // src/audio/encoder.ts
385
+ var DEFAULT_OPTIONS2 = {
386
+ format: "pcm16",
387
+ sampleRate: 16e3,
388
+ channels: 1,
389
+ bitDepth: 16
390
+ };
391
+ function encodeAudioToBase64(data) {
392
+ if (typeof Buffer !== "undefined") {
393
+ return Buffer.from(data).toString("base64");
394
+ }
395
+ let binary = "";
396
+ const len = data.byteLength;
397
+ for (let i = 0; i < len; i++) {
398
+ binary += String.fromCharCode(data[i]);
399
+ }
400
+ return typeof btoa !== "undefined" ? btoa(binary) : binary;
401
+ }
402
+ function decodeBase64ToAudio(base64) {
403
+ if (typeof Buffer !== "undefined") {
404
+ return new Uint8Array(Buffer.from(base64, "base64"));
405
+ }
406
+ const binary = typeof atob !== "undefined" ? atob(base64) : base64;
407
+ const len = binary.length;
408
+ const bytes = new Uint8Array(len);
409
+ for (let i = 0; i < len; i++) {
410
+ bytes[i] = binary.charCodeAt(i);
411
+ }
412
+ return bytes;
413
+ }
414
+ function float32ToInt16(float32Array) {
415
+ const int16Array = new Int16Array(float32Array.length);
416
+ for (let i = 0; i < float32Array.length; i++) {
417
+ const sample = float32Array[i];
418
+ const clamped = Math.max(-1, Math.min(1, sample));
419
+ int16Array[i] = clamped < 0 ? clamped * 32768 : clamped * 32767;
420
+ }
421
+ return int16Array;
422
+ }
423
+ function int16ToFloat32(int16Array) {
424
+ const float32Array = new Float32Array(int16Array.length);
425
+ for (let i = 0; i < int16Array.length; i++) {
426
+ const sample = int16Array[i];
427
+ float32Array[i] = sample / (sample < 0 ? 32768 : 32767);
428
+ }
429
+ return float32Array;
430
+ }
431
+ function int16ToUint8(int16Array) {
432
+ const uint8Array = new Uint8Array(int16Array.length * 2);
433
+ const view = new DataView(uint8Array.buffer);
434
+ for (let i = 0; i < int16Array.length; i++) {
435
+ view.setInt16(i * 2, int16Array[i], true);
436
+ }
437
+ return uint8Array;
438
+ }
439
+ function uint8ToInt16(uint8Array) {
440
+ const int16Array = new Int16Array(uint8Array.length / 2);
441
+ const view = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
442
+ for (let i = 0; i < int16Array.length; i++) {
443
+ int16Array[i] = view.getInt16(i * 2, true);
444
+ }
445
+ return int16Array;
446
+ }
447
+ function createWavHeader(dataLength, sampleRate, channels, bitDepth) {
448
+ const header = new ArrayBuffer(44);
449
+ const view = new DataView(header);
450
+ const byteRate = sampleRate * channels * bitDepth / 8;
451
+ const blockAlign = channels * bitDepth / 8;
452
+ writeString(view, 0, "RIFF");
453
+ view.setUint32(4, 36 + dataLength, true);
454
+ writeString(view, 8, "WAVE");
455
+ writeString(view, 12, "fmt ");
456
+ view.setUint32(16, 16, true);
457
+ view.setUint16(20, 1, true);
458
+ view.setUint16(22, channels, true);
459
+ view.setUint32(24, sampleRate, true);
460
+ view.setUint32(28, byteRate, true);
461
+ view.setUint16(32, blockAlign, true);
462
+ view.setUint16(34, bitDepth, true);
463
+ writeString(view, 36, "data");
464
+ view.setUint32(40, dataLength, true);
465
+ return new Uint8Array(header);
466
+ }
467
+ function writeString(view, offset, str) {
468
+ for (let i = 0; i < str.length; i++) {
469
+ view.setUint8(offset + i, str.charCodeAt(i));
470
+ }
471
+ }
472
+ function wrapPcmInWav(pcmData, options = {}) {
473
+ const opts = { ...DEFAULT_OPTIONS2, ...options };
474
+ const header = createWavHeader(pcmData.length, opts.sampleRate, opts.channels, opts.bitDepth);
475
+ const wav = new Uint8Array(header.length + pcmData.length);
476
+ wav.set(header);
477
+ wav.set(pcmData, header.length);
478
+ return wav;
479
+ }
480
+ function extractPcmFromWav(wavData) {
481
+ const view = new DataView(wavData.buffer, wavData.byteOffset, wavData.byteLength);
482
+ const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
483
+ if (riff !== "RIFF") {
484
+ throw new Error("Invalid WAV file: Missing RIFF header");
485
+ }
486
+ const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));
487
+ if (wave !== "WAVE") {
488
+ throw new Error("Invalid WAV file: Missing WAVE format");
489
+ }
490
+ const channels = view.getUint16(22, true);
491
+ const sampleRate = view.getUint32(24, true);
492
+ const bitDepth = view.getUint16(34, true);
493
+ let dataOffset = 44;
494
+ let dataLength = view.getUint32(40, true);
495
+ if (dataLength === 0 || dataOffset + dataLength > wavData.length) {
496
+ for (let i = 36; i < wavData.length - 8; i++) {
497
+ const chunk = String.fromCharCode(
498
+ view.getUint8(i),
499
+ view.getUint8(i + 1),
500
+ view.getUint8(i + 2),
501
+ view.getUint8(i + 3)
502
+ );
503
+ if (chunk === "data") {
504
+ dataLength = view.getUint32(i + 4, true);
505
+ dataOffset = i + 8;
506
+ break;
507
+ }
508
+ }
509
+ }
510
+ const pcmData = wavData.slice(dataOffset, dataOffset + dataLength);
511
+ return {
512
+ pcmData,
513
+ sampleRate,
514
+ channels,
515
+ bitDepth
516
+ };
517
+ }
518
+ var AudioEncoder = class {
519
+ options;
520
+ constructor(options = {}) {
521
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
522
+ }
523
+ /**
524
+ * Get current options
525
+ */
526
+ get format() {
527
+ return this.options.format;
528
+ }
529
+ get sampleRate() {
530
+ return this.options.sampleRate;
531
+ }
532
+ /**
533
+ * Encode audio data for transmission
534
+ */
535
+ encode(data) {
536
+ return encodeAudioToBase64(data);
537
+ }
538
+ /**
539
+ * Decode received audio data
540
+ */
541
+ decode(base64) {
542
+ return decodeBase64ToAudio(base64);
543
+ }
544
+ /**
545
+ * Convert Float32 samples to transmission-ready format
546
+ */
547
+ fromFloat32(samples) {
548
+ const int16 = float32ToInt16(samples);
549
+ return int16ToUint8(int16);
550
+ }
551
+ /**
552
+ * Convert received data to Float32 samples
553
+ */
554
+ toFloat32(data) {
555
+ const int16 = uint8ToInt16(data);
556
+ return int16ToFloat32(int16);
557
+ }
558
+ /**
559
+ * Wrap data in WAV format if needed
560
+ */
561
+ wrapWav(data) {
562
+ if (this.options.format === "wav") {
563
+ return wrapPcmInWav(data, this.options);
564
+ }
565
+ return data;
566
+ }
567
+ };
568
+
569
+ // src/client.ts
570
+ var CONFIG_DEFAULTS = {
571
+ connectionTimeout: 3e4,
572
+ autoReconnect: true,
573
+ maxReconnectAttempts: 5,
574
+ reconnectDelay: 1e3,
575
+ debug: false
576
+ };
577
+ var SESSION_DEFAULTS = {
578
+ voiceId: "en-US-Standard-A",
579
+ languageCode: "en-US",
580
+ inputFormat: "pcm16",
581
+ outputFormat: "pcm16",
582
+ sampleRate: 16e3
583
+ };
584
+ var LiveSpeechClient = class {
585
+ config;
586
+ connection;
587
+ audioEncoder;
588
+ logger;
589
+ sessionId = null;
590
+ sessionConfig = null;
591
+ // Event listeners using a simple map
592
+ eventListeners = /* @__PURE__ */ new Map();
593
+ // Simplified handlers
594
+ transcriptHandler = null;
595
+ responseHandler = null;
596
+ audioHandler = null;
597
+ errorHandler = null;
598
+ constructor(config) {
599
+ if (!config.region) {
600
+ throw new Error("region is required");
601
+ }
602
+ if (!config.apiKey) {
603
+ throw new Error("apiKey is required");
604
+ }
605
+ const endpoint = getEndpointForRegion(config.region);
606
+ this.config = {
607
+ endpoint,
608
+ apiKey: config.apiKey,
609
+ connectionTimeout: config.connectionTimeout ?? CONFIG_DEFAULTS.connectionTimeout,
610
+ autoReconnect: config.autoReconnect ?? CONFIG_DEFAULTS.autoReconnect,
611
+ maxReconnectAttempts: config.maxReconnectAttempts ?? CONFIG_DEFAULTS.maxReconnectAttempts,
612
+ reconnectDelay: config.reconnectDelay ?? CONFIG_DEFAULTS.reconnectDelay,
613
+ debug: config.debug ?? CONFIG_DEFAULTS.debug
614
+ };
615
+ this.logger = createLogger("LiveSpeech", this.config.debug);
616
+ this.audioEncoder = new AudioEncoder();
617
+ this.connection = new WebSocketConnection(this.config, {
618
+ onOpen: (connectionId) => this.handleConnected(connectionId),
619
+ onClose: (code, reason) => this.handleDisconnected(code, reason),
620
+ onError: (error) => this.handleError("connection_failed", error.message),
621
+ onMessage: (message) => this.handleMessage(message),
622
+ onReconnecting: (attempt, maxAttempts, delay) => this.handleReconnecting(attempt, maxAttempts, delay)
623
+ });
624
+ }
625
+ // ==================== Public API ====================
626
+ /**
627
+ * Get current connection state
628
+ */
629
+ get connectionState() {
630
+ return this.connection.currentState;
631
+ }
632
+ /**
633
+ * Get connection ID
634
+ */
635
+ get connectionId() {
636
+ return this.connection.id;
637
+ }
638
+ /**
639
+ * Get current session ID
640
+ */
641
+ get currentSessionId() {
642
+ return this.sessionId;
643
+ }
644
+ /**
645
+ * Check if connected
646
+ */
647
+ get isConnected() {
648
+ return this.connection.isConnected;
649
+ }
650
+ /**
651
+ * Check if session is active
652
+ */
653
+ get hasActiveSession() {
654
+ return this.sessionId !== null;
655
+ }
656
+ /**
657
+ * Connect to the server
658
+ */
659
+ async connect() {
660
+ this.logger.info("Connecting...");
661
+ await this.connection.connect();
662
+ }
663
+ /**
664
+ * Disconnect from the server
665
+ */
666
+ disconnect() {
667
+ this.logger.info("Disconnecting...");
668
+ this.sessionId = null;
669
+ this.sessionConfig = null;
670
+ this.connection.disconnect();
671
+ }
672
+ /**
673
+ * Start a new session
674
+ */
675
+ async startSession(config) {
676
+ if (!this.isConnected) {
677
+ throw new Error("Not connected. Call connect() first.");
678
+ }
679
+ if (this.sessionId) {
680
+ throw new Error("Session already active. Call endSession() first.");
681
+ }
682
+ const resolvedConfig = {
683
+ prePrompt: config.prePrompt,
684
+ voiceId: config.voiceId ?? SESSION_DEFAULTS.voiceId,
685
+ languageCode: config.languageCode ?? SESSION_DEFAULTS.languageCode,
686
+ inputFormat: config.inputFormat ?? SESSION_DEFAULTS.inputFormat,
687
+ outputFormat: config.outputFormat ?? SESSION_DEFAULTS.outputFormat,
688
+ sampleRate: config.sampleRate ?? SESSION_DEFAULTS.sampleRate,
689
+ metadata: config.metadata ?? {}
690
+ };
691
+ this.sessionConfig = resolvedConfig;
692
+ this.logger.info("Starting session...");
693
+ return new Promise((resolve, reject) => {
694
+ const onSessionStarted = (event) => {
695
+ this.off("sessionStarted", onSessionStarted);
696
+ this.off("error", onError);
697
+ resolve(event.sessionId);
698
+ };
699
+ const onError = (event) => {
700
+ if (event.code === "session_error") {
701
+ this.off("sessionStarted", onSessionStarted);
702
+ this.off("error", onError);
703
+ reject(new Error(event.message));
704
+ }
705
+ };
706
+ this.on("sessionStarted", onSessionStarted);
707
+ this.on("error", onError);
708
+ this.connection.send({
709
+ action: "startSession",
710
+ prePrompt: resolvedConfig.prePrompt,
711
+ voiceId: resolvedConfig.voiceId,
712
+ languageCode: resolvedConfig.languageCode,
713
+ inputFormat: resolvedConfig.inputFormat,
714
+ outputFormat: resolvedConfig.outputFormat,
715
+ sampleRate: resolvedConfig.sampleRate,
716
+ metadata: resolvedConfig.metadata
717
+ });
718
+ });
719
+ }
720
+ /**
721
+ * End the current session
722
+ */
723
+ async endSession() {
724
+ if (!this.sessionId) {
725
+ this.logger.warn("No active session to end");
726
+ return;
727
+ }
728
+ this.logger.info("Ending session...");
729
+ return new Promise((resolve) => {
730
+ const onSessionEnded = () => {
731
+ this.off("sessionEnded", onSessionEnded);
732
+ resolve();
733
+ };
734
+ this.on("sessionEnded", onSessionEnded);
735
+ this.connection.send({ action: "endSession" });
736
+ });
737
+ }
738
+ /**
739
+ * Send audio data
740
+ */
741
+ sendAudio(data, options) {
742
+ if (!this.isConnected) {
743
+ throw new Error("Not connected");
744
+ }
745
+ if (!this.sessionId) {
746
+ throw new Error("No active session. Call startSession() first.");
747
+ }
748
+ const base64Data = this.audioEncoder.encode(data);
749
+ const format = options?.format ?? this.sessionConfig?.inputFormat ?? SESSION_DEFAULTS.inputFormat;
750
+ const sampleRate = this.sessionConfig?.sampleRate ?? SESSION_DEFAULTS.sampleRate;
751
+ const audioMessage = {
752
+ action: "audio",
753
+ data: base64Data,
754
+ format,
755
+ sampleRate
756
+ };
757
+ if (options?.isFinal !== void 0) {
758
+ audioMessage.isFinal = options.isFinal;
759
+ }
760
+ this.connection.send(audioMessage);
761
+ }
762
+ // ==================== Event System ====================
763
+ /**
764
+ * Add event listener
765
+ */
766
+ on(event, listener) {
767
+ if (!this.eventListeners.has(event)) {
768
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
769
+ }
770
+ this.eventListeners.get(event).add(listener);
771
+ }
772
+ /**
773
+ * Remove event listener
774
+ */
775
+ off(event, listener) {
776
+ const listeners = this.eventListeners.get(event);
777
+ if (listeners) {
778
+ listeners.delete(listener);
779
+ }
780
+ }
781
+ /**
782
+ * Set transcript handler (simplified)
783
+ */
784
+ setTranscriptHandler(handler) {
785
+ this.transcriptHandler = handler;
786
+ }
787
+ /**
788
+ * Set response handler (simplified)
789
+ */
790
+ setResponseHandler(handler) {
791
+ this.responseHandler = handler;
792
+ }
793
+ /**
794
+ * Set audio handler (simplified)
795
+ */
796
+ setAudioHandler(handler) {
797
+ this.audioHandler = handler;
798
+ }
799
+ /**
800
+ * Set error handler (simplified)
801
+ */
802
+ setErrorHandler(handler) {
803
+ this.errorHandler = handler;
804
+ }
805
+ // ==================== Private Methods ====================
806
+ emit(event, data) {
807
+ const listeners = this.eventListeners.get(event);
808
+ if (listeners) {
809
+ listeners.forEach((listener) => {
810
+ try {
811
+ listener(data);
812
+ } catch (error) {
813
+ this.logger.error(`Error in ${event} listener:`, error);
814
+ }
815
+ });
816
+ }
817
+ }
818
+ handleConnected(connectionId) {
819
+ const event = {
820
+ type: "connected",
821
+ connectionId,
822
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
823
+ };
824
+ this.emit("connected", event);
825
+ }
826
+ handleDisconnected(code, _reason) {
827
+ this.sessionId = null;
828
+ this.sessionConfig = null;
829
+ const event = {
830
+ type: "disconnected",
831
+ reason: code === 1e3 ? "normal" : "error",
832
+ code,
833
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
834
+ };
835
+ this.emit("disconnected", event);
836
+ }
837
+ handleReconnecting(attempt, maxAttempts, delay) {
838
+ const event = {
839
+ type: "reconnecting",
840
+ attempt,
841
+ maxAttempts,
842
+ delay,
843
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
844
+ };
845
+ this.emit("reconnecting", event);
846
+ }
847
+ handleError(code, message, details) {
848
+ const event = {
849
+ type: "error",
850
+ code,
851
+ message,
852
+ details,
853
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
854
+ };
855
+ this.emit("error", event);
856
+ this.errorHandler?.(event);
857
+ }
858
+ handleMessage(message) {
859
+ switch (message.type) {
860
+ case "sessionStarted":
861
+ this.sessionId = message.sessionId;
862
+ this.emit("sessionStarted", {
863
+ type: "sessionStarted",
864
+ sessionId: message.sessionId,
865
+ timestamp: message.timestamp
866
+ });
867
+ break;
868
+ case "sessionEnded":
869
+ this.sessionId = null;
870
+ this.sessionConfig = null;
871
+ this.emit("sessionEnded", {
872
+ type: "sessionEnded",
873
+ sessionId: message.sessionId,
874
+ timestamp: message.timestamp
875
+ });
876
+ break;
877
+ case "transcript": {
878
+ const transcriptEvent = {
879
+ type: "transcript",
880
+ text: message.text,
881
+ isFinal: message.isFinal,
882
+ timestamp: message.timestamp
883
+ };
884
+ if (message.confidence !== void 0) {
885
+ transcriptEvent.confidence = message.confidence;
886
+ }
887
+ this.emit("transcript", transcriptEvent);
888
+ this.transcriptHandler?.(message.text, message.isFinal);
889
+ break;
890
+ }
891
+ case "response": {
892
+ const responseEvent = {
893
+ type: "response",
894
+ text: message.text,
895
+ isFinal: message.isFinal,
896
+ timestamp: message.timestamp
897
+ };
898
+ this.emit("response", responseEvent);
899
+ this.responseHandler?.(message.text, message.isFinal);
900
+ break;
901
+ }
902
+ case "audio": {
903
+ const audioData = this.audioEncoder.decode(message.data);
904
+ const audioEvent = {
905
+ type: "audio",
906
+ data: audioData,
907
+ format: message.format,
908
+ sampleRate: message.sampleRate,
909
+ timestamp: message.timestamp
910
+ };
911
+ this.emit("audio", audioEvent);
912
+ this.audioHandler?.(audioData);
913
+ break;
914
+ }
915
+ case "error":
916
+ this.handleError(message.code, message.message, message.details);
917
+ break;
918
+ default:
919
+ this.logger.warn("Unknown message type:", message.type);
920
+ }
921
+ }
922
+ };
923
+ export {
924
+ AudioEncoder,
925
+ LiveSpeechClient,
926
+ Region,
927
+ createWavHeader,
928
+ decodeBase64ToAudio,
929
+ encodeAudioToBase64,
930
+ extractPcmFromWav,
931
+ float32ToInt16,
932
+ getEndpointForRegion,
933
+ int16ToFloat32,
934
+ int16ToUint8,
935
+ isValidRegion,
936
+ uint8ToInt16,
937
+ wrapPcmInWav
938
+ };