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