@autoscriber/core 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.
Files changed (42) hide show
  1. package/README.md +147 -0
  2. package/dist/audio/audio-manager.d.ts +27 -0
  3. package/dist/audio/audio-manager.d.ts.map +1 -0
  4. package/dist/audio/permissions.d.ts +8 -0
  5. package/dist/audio/permissions.d.ts.map +1 -0
  6. package/dist/audio/resampler.d.ts +5 -0
  7. package/dist/audio/resampler.d.ts.map +1 -0
  8. package/dist/audio/silence-detector.d.ts +16 -0
  9. package/dist/audio/silence-detector.d.ts.map +1 -0
  10. package/dist/audio/wake-lock.d.ts +29 -0
  11. package/dist/audio/wake-lock.d.ts.map +1 -0
  12. package/dist/audio/worklets.d.ts +3 -0
  13. package/dist/audio/worklets.d.ts.map +1 -0
  14. package/dist/core/assistant-client.d.ts +78 -0
  15. package/dist/core/assistant-client.d.ts.map +1 -0
  16. package/dist/core/message-store.d.ts +28 -0
  17. package/dist/core/message-store.d.ts.map +1 -0
  18. package/dist/core/offline-queue.d.ts +9 -0
  19. package/dist/core/offline-queue.d.ts.map +1 -0
  20. package/dist/core/websocket-client.d.ts +31 -0
  21. package/dist/core/websocket-client.d.ts.map +1 -0
  22. package/dist/headless/create-assistant.d.ts +9 -0
  23. package/dist/headless/create-assistant.d.ts.map +1 -0
  24. package/dist/index.cjs +2005 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1977 -0
  28. package/dist/types/index.d.ts +103 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/utils/base64.d.ts +3 -0
  31. package/dist/utils/base64.d.ts.map +1 -0
  32. package/dist/utils/event-bus.d.ts +11 -0
  33. package/dist/utils/event-bus.d.ts.map +1 -0
  34. package/dist/utils/i18n.d.ts +5 -0
  35. package/dist/utils/i18n.d.ts.map +1 -0
  36. package/dist/utils/id.d.ts +2 -0
  37. package/dist/utils/id.d.ts.map +1 -0
  38. package/dist/utils/logger.d.ts +8 -0
  39. package/dist/utils/logger.d.ts.map +1 -0
  40. package/dist/utils/token.d.ts +4 -0
  41. package/dist/utils/token.d.ts.map +1 -0
  42. package/package.json +60 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,2005 @@
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 src_exports = {};
22
+ __export(src_exports, {
23
+ AssistantClient: () => AssistantClient,
24
+ createAssistant: () => createAssistant
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/utils/event-bus.ts
29
+ var EventBus = class {
30
+ constructor() {
31
+ this.listeners = /* @__PURE__ */ new Map();
32
+ }
33
+ on(event, listener) {
34
+ const listeners = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
35
+ listeners.add(listener);
36
+ this.listeners.set(event, listeners);
37
+ return () => this.off(event, listener);
38
+ }
39
+ once(event, listener) {
40
+ const off = this.on(event, (payload) => {
41
+ off();
42
+ listener(payload);
43
+ });
44
+ return off;
45
+ }
46
+ off(event, listener) {
47
+ const listeners = this.listeners.get(event);
48
+ if (!listeners)
49
+ return;
50
+ listeners.delete(listener);
51
+ if (listeners.size === 0) {
52
+ this.listeners.delete(event);
53
+ }
54
+ }
55
+ emit(event, payload) {
56
+ const listeners = this.listeners.get(event);
57
+ if (!listeners)
58
+ return;
59
+ listeners.forEach((listener) => {
60
+ try {
61
+ listener(payload);
62
+ } catch (error) {
63
+ console.error("[EventBus] listener error", error);
64
+ }
65
+ });
66
+ }
67
+ clear() {
68
+ this.listeners.clear();
69
+ }
70
+ };
71
+
72
+ // src/utils/logger.ts
73
+ var noop = () => {
74
+ };
75
+ function createLogger(overrides) {
76
+ return {
77
+ debug: overrides?.debug?.bind(overrides) ?? noop,
78
+ info: overrides?.info?.bind(overrides) ?? noop,
79
+ warn: overrides?.warn?.bind(overrides) ?? console.warn,
80
+ error: overrides?.error?.bind(overrides) ?? console.error
81
+ };
82
+ }
83
+
84
+ // src/utils/i18n.ts
85
+ var TRANSLATIONS = {
86
+ en: {
87
+ askAnything: "Ask anything...",
88
+ typeOrHoldSpace: 'Type or hold "space" to talk',
89
+ paused: "Paused...",
90
+ listening: "Listening...",
91
+ title: "Assistant",
92
+ aiDisclaimer: "AI generated answers can be inaccurate.",
93
+ newChat: "New chat",
94
+ emergencyStop: "Recording emergency stopped",
95
+ transcriptionTimeout: "Transcription timeout - please try again",
96
+ voiceCommandFailed: "\u26A0\uFE0F Voice command failed to start - microphone unavailable",
97
+ transcriptionFailed: "\u26A0\uFE0F Transcription failed to start - microphone unavailable",
98
+ microphoneRequired: "\u26A0\uFE0F Microphone access required - please allow microphone access when prompted",
99
+ microphoneNotFound: "\u26A0\uFE0F No microphone found - please connect a microphone",
100
+ microphonePermissionError: "\u26A0\uFE0F Unable to check microphone permissions",
101
+ microphoneAccessError: "\u26A0\uFE0F Unable to access microphone - please check your browser settings",
102
+ microphoneAccessDenied: "\u26A0\uFE0F Microphone access denied - please enable in browser settings",
103
+ settings: "Settings",
104
+ microphone: "Microphone",
105
+ noMicrophonesFound: "No microphones found",
106
+ transcriptionStarted: "Transcription started",
107
+ transcriptionStopped: "Transcription stopped",
108
+ voiceCommandSilence: "Voice command cancelled - silence detected",
109
+ networkOffline: "\u26A0\uFE0F Network offline - messages will be sent when connection returns",
110
+ transcriptionStoppedOffline: "Transcription stopped - Network offline",
111
+ transcriptionBufferingOffline: "Transcription buffering - will resume when connection returns",
112
+ voiceCommandCancelledOffline: "Voice command cancelled - Network offline",
113
+ connectionRestored: "Connection restored",
114
+ offlineBuffering: "Message buffered - will resend once connected"
115
+ },
116
+ nl: {
117
+ askAnything: "Stel je vraag...",
118
+ typeOrHoldSpace: 'Typ of houd "spatie" ingedrukt om te praten',
119
+ paused: "Gepauzeerd...",
120
+ listening: "Luisteren...",
121
+ title: "Assistent",
122
+ aiDisclaimer: "AI-gegenereerde antwoorden kunnen onnauwkeurig zijn.",
123
+ newChat: "Nieuwe chat",
124
+ emergencyStop: "Opname noodstop uitgevoerd",
125
+ transcriptionTimeout: "Transcriptie timeout - probeer opnieuw",
126
+ voiceCommandFailed: "\u26A0\uFE0F Spraakopdracht starten mislukt - microfoon niet beschikbaar",
127
+ transcriptionFailed: "\u26A0\uFE0F Transcriptie starten mislukt - microfoon niet beschikbaar",
128
+ microphoneRequired: "\u26A0\uFE0F Microfoon toegang vereist - sta microfoon toegang toe wanneer gevraagd",
129
+ microphoneNotFound: "\u26A0\uFE0F Geen microfoon gevonden - sluit een microfoon aan",
130
+ microphonePermissionError: "\u26A0\uFE0F Kan microfoon machtigingen niet controleren",
131
+ microphoneAccessError: "\u26A0\uFE0F Kan microfoon niet benaderen - controleer uw browserinstellingen",
132
+ microphoneAccessDenied: "\u26A0\uFE0F Microfoon toegang geweigerd - schakel in browserinstellingen in",
133
+ settings: "Instellingen",
134
+ microphone: "Microfoon",
135
+ noMicrophonesFound: "Geen microfoons gevonden",
136
+ transcriptionStarted: "Transcriptie gestart",
137
+ transcriptionStopped: "Transcriptie gestopt",
138
+ voiceCommandSilence: "Spraakopdracht geannuleerd - stilte gedetecteerd",
139
+ networkOffline: "\u26A0\uFE0F Netwerk offline - berichten worden verzonden zodra de verbinding terug is",
140
+ transcriptionStoppedOffline: "Transcriptie gestopt - Netwerk offline",
141
+ transcriptionBufferingOffline: "Transcriptie wordt gebufferd - gaat verder zodra we weer online zijn",
142
+ voiceCommandCancelledOffline: "Spraakopdracht geannuleerd - Netwerk offline",
143
+ connectionRestored: "Verbinding hersteld",
144
+ offlineBuffering: "Bericht gebufferd - wordt verzonden zodra we weer online zijn"
145
+ },
146
+ de: {
147
+ askAnything: "Stellen Sie eine Frage...",
148
+ typeOrHoldSpace: "Tippen zum Schreiben oder Leertaste halten zum Sprechen",
149
+ paused: "Pausiert...",
150
+ listening: "H\xF6rt zu...",
151
+ title: "Assistent",
152
+ aiDisclaimer: "KI-generierte Antworten k\xF6nnen ungenau sein.",
153
+ newChat: "Neuer Chat",
154
+ emergencyStop: "Notfallstopp der Aufnahme ausgef\xFChrt",
155
+ transcriptionTimeout: "Transkriptions-Timeout \u2013 bitte erneut versuchen",
156
+ voiceCommandFailed: "\u26A0\uFE0F Sprachbefehl konnte nicht gestartet werden \u2013 Mikrofon nicht verf\xFCgbar",
157
+ transcriptionFailed: "\u26A0\uFE0F Transkription konnte nicht gestartet werden \u2013 Mikrofon nicht verf\xFCgbar",
158
+ microphoneRequired: "\u26A0\uFE0F Mikrofonzugriff erforderlich \u2013 bitte Zugriff bei Aufforderung erlauben",
159
+ microphoneNotFound: "\u26A0\uFE0F Kein Mikrofon gefunden \u2013 bitte Mikrofon anschlie\xDFen",
160
+ microphonePermissionError: "\u26A0\uFE0F Mikrofonberechtigungen k\xF6nnen nicht \xFCberpr\xFCft werden",
161
+ microphoneAccessError: "\u26A0\uFE0F Auf Mikrofon kann nicht zugegriffen werden \u2013 bitte Browsereinstellungen \xFCberpr\xFCfen",
162
+ microphoneAccessDenied: "\u26A0\uFE0F Mikrofonzugriff verweigert \u2013 bitte in den Browsereinstellungen aktivieren",
163
+ settings: "Einstellungen",
164
+ microphone: "Mikrofon",
165
+ noMicrophonesFound: "Keine Mikrofone gefunden",
166
+ transcriptionStarted: "Transkription gestartet",
167
+ transcriptionStopped: "Transkription gestoppt",
168
+ voiceCommandSilence: "Sprachbefehl abgebrochen \u2013 Stille erkannt",
169
+ networkOffline: "\u26A0\uFE0F Netzwerk offline \u2013 Nachrichten werden gesendet, sobald die Verbindung zur\xFCck ist",
170
+ transcriptionStoppedOffline: "Transkription gestoppt \u2013 Netzwerk offline",
171
+ transcriptionBufferingOffline: "Transkription wird gepuffert \u2013 l\xE4uft weiter, sobald die Verbindung zur\xFCck ist",
172
+ voiceCommandCancelledOffline: "Sprachbefehl abgebrochen \u2013 Netzwerk offline",
173
+ connectionRestored: "Verbindung wiederhergestellt",
174
+ offlineBuffering: "Nachricht gepuffert \u2013 wird gesendet, sobald wir wieder online sind"
175
+ }
176
+ };
177
+ var DEFAULT_LANGUAGE = "en";
178
+ function translate(language, key, params = {}) {
179
+ const dictionary = TRANSLATIONS[language] ?? TRANSLATIONS[DEFAULT_LANGUAGE];
180
+ let text = dictionary[key] ?? TRANSLATIONS[DEFAULT_LANGUAGE][key] ?? key;
181
+ Object.entries(params).forEach(([placeholder, value]) => {
182
+ text = text.replace(new RegExp(`{{${placeholder}}}`, "g"), String(value));
183
+ });
184
+ return text;
185
+ }
186
+ function validateLanguage(lang) {
187
+ if (!lang)
188
+ return DEFAULT_LANGUAGE;
189
+ if (["en", "nl", "de"].includes(lang)) {
190
+ return lang;
191
+ }
192
+ console.warn(
193
+ `[Autoscriber] Unsupported language "${lang}" provided. Falling back to "${DEFAULT_LANGUAGE}".`
194
+ );
195
+ return DEFAULT_LANGUAGE;
196
+ }
197
+
198
+ // src/utils/id.ts
199
+ function generateMessageId() {
200
+ return Math.random().toString(36).substring(2, 10);
201
+ }
202
+
203
+ // src/core/message-store.ts
204
+ var MessageStore = class {
205
+ constructor() {
206
+ this.messages = [];
207
+ this.interactions = [];
208
+ }
209
+ createMessage(input = {}) {
210
+ const message = {
211
+ id: input.id ?? generateMessageId(),
212
+ request: input.request ?? null,
213
+ partialResults: [],
214
+ result: null,
215
+ type: input.type ?? "chat",
216
+ metadata: input.metadata ?? {}
217
+ };
218
+ this.messages.push(message);
219
+ return message;
220
+ }
221
+ addMessage(message) {
222
+ this.messages.push(message);
223
+ }
224
+ getMessageById(id) {
225
+ return this.messages.find((msg) => msg.id === id);
226
+ }
227
+ setMessageResult(id, result) {
228
+ const target = this.getMessageById(id);
229
+ if (!target)
230
+ return;
231
+ target.result = result;
232
+ target.partialResults = [];
233
+ }
234
+ setMessagePartialResult(id, payload, append = true) {
235
+ const target = this.getMessageById(id);
236
+ if (!target)
237
+ return;
238
+ if (!append) {
239
+ target.partialResults = [payload];
240
+ } else {
241
+ target.partialResults.push(payload);
242
+ }
243
+ }
244
+ applyJsonPatch(id, patchOperations) {
245
+ const target = this.getMessageById(id);
246
+ if (!target)
247
+ return;
248
+ if (!target.metadata)
249
+ target.metadata = {};
250
+ if (!target.metadata.transcriptData) {
251
+ target.metadata.transcriptData = {
252
+ resourceType: "Transcript",
253
+ entries: []
254
+ };
255
+ }
256
+ const transcriptData = target.metadata.transcriptData;
257
+ patchOperations.forEach((operation) => {
258
+ if (operation.op === "add" && operation.path === "/entries") {
259
+ if (Array.isArray(operation.value)) {
260
+ transcriptData.entries.push(...operation.value);
261
+ } else {
262
+ transcriptData.entries.push(operation.value);
263
+ }
264
+ }
265
+ });
266
+ const transcriptResult = {
267
+ mediaType: "application/fhir+json",
268
+ data: { ...transcriptData }
269
+ };
270
+ target.result = [transcriptResult];
271
+ }
272
+ setError(id, errorMessage, result) {
273
+ const target = this.getMessageById(id);
274
+ if (!target)
275
+ return;
276
+ target.errorMessage = errorMessage;
277
+ if (result) {
278
+ target.result = result;
279
+ }
280
+ }
281
+ upsertInteraction(pill) {
282
+ const existing = this.interactions.find((item) => item.id === pill.id);
283
+ if (existing) {
284
+ Object.assign(existing, pill);
285
+ return existing;
286
+ }
287
+ this.interactions.push(pill);
288
+ return pill;
289
+ }
290
+ updateInteractionStatus(id, status, metadata) {
291
+ const target = this.interactions.find((item) => item.id === id);
292
+ if (!target)
293
+ return;
294
+ target.status = status;
295
+ if (metadata) {
296
+ target.metadata = { ...target.metadata ?? {}, ...metadata };
297
+ }
298
+ }
299
+ getInteractions() {
300
+ return [...this.interactions];
301
+ }
302
+ getMessages() {
303
+ return [...this.messages];
304
+ }
305
+ reset() {
306
+ this.messages = [];
307
+ this.interactions = [];
308
+ }
309
+ };
310
+
311
+ // src/core/offline-queue.ts
312
+ var OfflineQueue = class {
313
+ constructor() {
314
+ this.queue = [];
315
+ }
316
+ enqueue(item) {
317
+ this.queue.push(item);
318
+ }
319
+ drain() {
320
+ const items = [...this.queue];
321
+ this.queue = [];
322
+ return items;
323
+ }
324
+ clear() {
325
+ this.queue = [];
326
+ }
327
+ get size() {
328
+ return this.queue.length;
329
+ }
330
+ get items() {
331
+ return [...this.queue];
332
+ }
333
+ };
334
+
335
+ // src/core/websocket-client.ts
336
+ var WebSocketClient = class {
337
+ constructor(options = {}) {
338
+ this.socket = null;
339
+ this.manualClose = false;
340
+ this.reconnectAttempts = 0;
341
+ this.bus = new EventBus();
342
+ this.reconnectDelay = options.reconnectDelay ?? 3e3;
343
+ this.autoReconnect = options.autoReconnect ?? true;
344
+ this.logger = {
345
+ ...console,
346
+ ...options.logger
347
+ };
348
+ }
349
+ on(event, listener) {
350
+ return this.bus.on(event, listener);
351
+ }
352
+ async connect(url) {
353
+ this.manualClose = false;
354
+ if (this.socket) {
355
+ this.logger.warn("[Autoscriber] WebSocket already connected - closing existing connection");
356
+ this.socket.close(4e3, "Reconnecting");
357
+ this.socket = null;
358
+ }
359
+ await this.openSocket(url);
360
+ }
361
+ openSocket(url) {
362
+ return new Promise((resolve, reject) => {
363
+ try {
364
+ const socket = new WebSocket(url);
365
+ this.socket = socket;
366
+ socket.onopen = (event) => {
367
+ this.logger.info("[Autoscriber] websocket open");
368
+ this.reconnectAttempts = 0;
369
+ this.bus.emit("open", event);
370
+ resolve();
371
+ };
372
+ socket.onclose = (event) => {
373
+ this.logger.warn("[Autoscriber] websocket closed", event.code, event.reason);
374
+ this.socket = null;
375
+ this.bus.emit("close", event);
376
+ if (!this.manualClose && this.autoReconnect) {
377
+ this.scheduleReconnect(url);
378
+ }
379
+ };
380
+ socket.onerror = (event) => {
381
+ const errorInfo = {
382
+ type: event.type,
383
+ timeStamp: event.timeStamp,
384
+ readyState: socket.readyState,
385
+ url: socket.url
386
+ };
387
+ if (event instanceof ErrorEvent) {
388
+ errorInfo.message = event.message;
389
+ errorInfo.filename = event.filename;
390
+ errorInfo.lineno = event.lineno;
391
+ errorInfo.colno = event.colno;
392
+ if (event.error) {
393
+ errorInfo.error = event.error instanceof Error ? { name: event.error.name, message: event.error.message, stack: event.error.stack } : String(event.error);
394
+ }
395
+ }
396
+ this.logger.error("[Autoscriber] websocket error", errorInfo);
397
+ this.bus.emit("error", event);
398
+ const errorMessage = event instanceof ErrorEvent && event.message ? event.message : `WebSocket error (readyState: ${socket.readyState}, url: ${socket.url})`;
399
+ const enhancedError = new Error(errorMessage);
400
+ enhancedError.originalEvent = event;
401
+ enhancedError.readyState = socket.readyState;
402
+ enhancedError.url = socket.url;
403
+ reject(enhancedError);
404
+ };
405
+ socket.onmessage = (event) => {
406
+ this.bus.emit("message", event);
407
+ };
408
+ } catch (error) {
409
+ reject(error);
410
+ }
411
+ });
412
+ }
413
+ scheduleReconnect(url) {
414
+ this.reconnectAttempts += 1;
415
+ const delay = this.reconnectDelay;
416
+ if (!this.autoReconnect) {
417
+ return;
418
+ }
419
+ this.logger.info("[Autoscriber] scheduling reconnect", { attempt: this.reconnectAttempts, delay });
420
+ this.bus.emit("reconnecting", this.reconnectAttempts);
421
+ setTimeout(() => {
422
+ if (this.manualClose)
423
+ return;
424
+ this.openSocket(url).catch((error) => {
425
+ this.logger.error("[Autoscriber] reconnect attempt failed", error);
426
+ this.scheduleReconnect(url);
427
+ });
428
+ }, delay);
429
+ }
430
+ send(payload) {
431
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
432
+ throw new Error("WebSocket not connected");
433
+ }
434
+ this.socket.send(payload);
435
+ }
436
+ close(code, reason) {
437
+ this.manualClose = true;
438
+ this.socket?.close(code, reason);
439
+ this.socket = null;
440
+ }
441
+ get readyState() {
442
+ return this.socket?.readyState ?? WebSocket.CLOSED;
443
+ }
444
+ };
445
+
446
+ // src/utils/base64.ts
447
+ function arrayBufferToBase64(buffer) {
448
+ let binary = "";
449
+ const bytes = new Uint8Array(buffer);
450
+ const len = bytes.byteLength;
451
+ for (let i = 0; i < len; i += 1) {
452
+ binary += String.fromCharCode(bytes[i]);
453
+ }
454
+ return btoa(binary);
455
+ }
456
+ function base64ToArray(base64) {
457
+ const binaryString = atob(base64);
458
+ const len = binaryString.length;
459
+ const bytes = new Uint8Array(len);
460
+ for (let i = 0; i < len; i += 1) {
461
+ bytes[i] = binaryString.charCodeAt(i);
462
+ }
463
+ return bytes.buffer;
464
+ }
465
+
466
+ // src/audio/worklets.ts
467
+ var PCM_PLAYER_PROCESSOR = `
468
+ class PCMPlayerProcessor extends AudioWorkletProcessor {
469
+ constructor() {
470
+ super();
471
+ this.bufferSize = 24000 * 180;
472
+ this.buffer = new Float32Array(this.bufferSize);
473
+ this.writeIndex = 0;
474
+ this.readIndex = 0;
475
+ this.port.onmessage = (event) => {
476
+ if (event.data.command === 'endOfAudio') {
477
+ this.readIndex = this.writeIndex;
478
+ return;
479
+ }
480
+ const int16Samples = new Int16Array(event.data);
481
+ this._enqueue(int16Samples);
482
+ };
483
+ }
484
+ _enqueue(int16Samples) {
485
+ for (let i = 0; i < int16Samples.length; i++) {
486
+ const floatVal = int16Samples[i] / 32768;
487
+ this.buffer[this.writeIndex] = floatVal;
488
+ this.writeIndex = (this.writeIndex + 1) % this.bufferSize;
489
+ if (this.writeIndex === this.readIndex) {
490
+ this.readIndex = (this.readIndex + 1) % this.bufferSize;
491
+ }
492
+ }
493
+ }
494
+ process(inputs, outputs) {
495
+ const output = outputs[0];
496
+ const framesPerBlock = output[0].length;
497
+ for (let frame = 0; frame < framesPerBlock; frame++) {
498
+ output[0][frame] = this.buffer[this.readIndex];
499
+ if (output.length > 1) {
500
+ output[1][frame] = this.buffer[this.readIndex];
501
+ }
502
+ if (this.readIndex !== this.writeIndex) {
503
+ this.readIndex = (this.readIndex + 1) % this.bufferSize;
504
+ }
505
+ }
506
+ return true;
507
+ }
508
+ }
509
+ registerProcessor('pcm-player-processor', PCMPlayerProcessor);
510
+ `;
511
+ var PCM_RECORDER_PROCESSOR = `
512
+ class PCMProcessor extends AudioWorkletProcessor {
513
+ process(inputs) {
514
+ if (inputs.length > 0 && inputs[0].length > 0) {
515
+ const inputChannel = inputs[0][0];
516
+ const inputCopy = new Float32Array(inputChannel);
517
+ this.port.postMessage(inputCopy);
518
+ }
519
+ return true;
520
+ }
521
+ }
522
+ registerProcessor("pcm-recorder-processor", PCMProcessor);
523
+ `;
524
+
525
+ // src/audio/resampler.ts
526
+ function resamplePCM16(inputBuffer, inputSampleRate, outputSampleRate) {
527
+ if (inputSampleRate === outputSampleRate) {
528
+ return inputBuffer;
529
+ }
530
+ const inputSamples = new Int16Array(inputBuffer.buffer, inputBuffer.byteOffset, inputBuffer.length / 2);
531
+ const inputLength = inputSamples.length;
532
+ const ratio = inputSampleRate / outputSampleRate;
533
+ const outputLength = Math.round(inputLength / ratio);
534
+ const outputSamples = new Int16Array(outputLength);
535
+ for (let i = 0; i < outputLength; i++) {
536
+ const srcIndex = i * ratio;
537
+ const srcIndexFloor = Math.floor(srcIndex);
538
+ const srcIndexCeil = Math.min(srcIndexFloor + 1, inputLength - 1);
539
+ const fraction = srcIndex - srcIndexFloor;
540
+ const sample1 = inputSamples[srcIndexFloor];
541
+ const sample2 = inputSamples[srcIndexCeil];
542
+ outputSamples[i] = Math.round(sample1 + (sample2 - sample1) * fraction);
543
+ }
544
+ return new Uint8Array(outputSamples.buffer);
545
+ }
546
+
547
+ // src/audio/audio-manager.ts
548
+ var AudioManager = class {
549
+ constructor() {
550
+ this.recorderContext = null;
551
+ this.recorderNode = null;
552
+ this.playerContext = null;
553
+ this.playerNode = null;
554
+ this.micStream = null;
555
+ this.actualSampleRate = null;
556
+ this.targetSampleRate = 16e3;
557
+ }
558
+ async init(options = {}) {
559
+ this.targetSampleRate = options.targetSampleRate ?? 16e3;
560
+ await this.setupRecorder(options);
561
+ await this.setupPlayer(options);
562
+ }
563
+ async setupPlayer(options = {}) {
564
+ if (this.playerContext)
565
+ return;
566
+ if (this.actualSampleRate === null) {
567
+ const tempContext = new AudioContext();
568
+ this.actualSampleRate = tempContext.sampleRate;
569
+ tempContext.close();
570
+ }
571
+ const audioContext = new AudioContext({
572
+ sampleRate: this.actualSampleRate
573
+ });
574
+ const blob = new Blob([PCM_PLAYER_PROCESSOR], { type: "application/javascript" });
575
+ const workletURL = URL.createObjectURL(blob);
576
+ await audioContext.audioWorklet.addModule(workletURL);
577
+ URL.revokeObjectURL(workletURL);
578
+ const playerNode = new AudioWorkletNode(audioContext, "pcm-player-processor");
579
+ playerNode.connect(audioContext.destination);
580
+ this.playerContext = audioContext;
581
+ this.playerNode = playerNode;
582
+ }
583
+ async setupRecorder(options = {}) {
584
+ if (this.recorderContext)
585
+ return;
586
+ const constraints = { channelCount: 1 };
587
+ if (options.deviceId) {
588
+ constraints.deviceId = { exact: options.deviceId };
589
+ }
590
+ const stream = await navigator.mediaDevices.getUserMedia({
591
+ audio: constraints
592
+ });
593
+ if (this.actualSampleRate === null) {
594
+ const tempContext = new AudioContext();
595
+ const tempSource = tempContext.createMediaStreamSource(stream);
596
+ this.actualSampleRate = tempContext.sampleRate;
597
+ tempSource.disconnect();
598
+ tempContext.close();
599
+ }
600
+ const audioContext = new AudioContext({
601
+ sampleRate: this.actualSampleRate
602
+ });
603
+ const blob = new Blob([PCM_RECORDER_PROCESSOR], { type: "application/javascript" });
604
+ const workletURL = URL.createObjectURL(blob);
605
+ await audioContext.audioWorklet.addModule(workletURL);
606
+ URL.revokeObjectURL(workletURL);
607
+ const source = audioContext.createMediaStreamSource(stream);
608
+ const recorderNode = new AudioWorkletNode(audioContext, "pcm-recorder-processor");
609
+ recorderNode.port.onmessage = (event) => {
610
+ const float32 = event.data;
611
+ let pcm16 = this.convertFloat32ToPCM(float32);
612
+ if (this.actualSampleRate && this.actualSampleRate !== this.targetSampleRate) {
613
+ pcm16 = resamplePCM16(pcm16, this.actualSampleRate, this.targetSampleRate);
614
+ }
615
+ options.onRecorderData?.(pcm16);
616
+ };
617
+ source.connect(recorderNode);
618
+ this.recorderContext = audioContext;
619
+ this.recorderNode = recorderNode;
620
+ this.micStream = stream;
621
+ }
622
+ stop() {
623
+ this.stopRecorder();
624
+ this.stopPlayer();
625
+ }
626
+ stopRecorder() {
627
+ this.recorderNode?.disconnect();
628
+ this.recorderNode = null;
629
+ this.recorderContext?.close();
630
+ this.recorderContext = null;
631
+ if (this.micStream) {
632
+ this.micStream.getTracks().forEach((track) => track.stop());
633
+ }
634
+ this.micStream = null;
635
+ if (!this.playerContext) {
636
+ this.actualSampleRate = null;
637
+ }
638
+ }
639
+ stopPlayer() {
640
+ this.playerNode?.disconnect();
641
+ this.playerNode = null;
642
+ this.playerContext?.close();
643
+ this.playerContext = null;
644
+ if (!this.recorderContext) {
645
+ this.actualSampleRate = null;
646
+ }
647
+ }
648
+ teardown() {
649
+ this.stop();
650
+ }
651
+ playbackPcm(buffer) {
652
+ if (!this.playerNode)
653
+ return;
654
+ this.playerNode.port.postMessage(buffer);
655
+ }
656
+ endOfAudio() {
657
+ this.playerNode?.port.postMessage({ command: "endOfAudio" });
658
+ }
659
+ convertFloat32ToPCM(inputData) {
660
+ const pcm16 = new Int16Array(inputData.length);
661
+ for (let i = 0; i < inputData.length; i += 1) {
662
+ pcm16[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767;
663
+ }
664
+ return new Uint8Array(pcm16.buffer);
665
+ }
666
+ };
667
+
668
+ // src/audio/silence-detector.ts
669
+ function convertPCMToFloat32(pcmBuffer) {
670
+ const int16View = new Int16Array(
671
+ pcmBuffer.buffer,
672
+ pcmBuffer.byteOffset,
673
+ pcmBuffer.byteLength / 2
674
+ );
675
+ const float32Array = new Float32Array(int16View.length);
676
+ for (let i = 0; i < int16View.length; i += 1) {
677
+ float32Array[i] = int16View[i] / 32768;
678
+ }
679
+ return float32Array;
680
+ }
681
+ function analyseAudioAmplitude(channelData, silenceThreshold = 0.01, silenceRatio = 0.95) {
682
+ let silentSamples = 0;
683
+ const totalSamples = channelData.length;
684
+ for (let i = 0; i < channelData.length; i += 1) {
685
+ const amplitude = Math.abs(channelData[i]);
686
+ if (amplitude < silenceThreshold) {
687
+ silentSamples += 1;
688
+ }
689
+ }
690
+ const silencePercentage = silentSamples / totalSamples;
691
+ const isSilence = silencePercentage > silenceRatio;
692
+ return {
693
+ isSilence,
694
+ silencePercentage,
695
+ totalSamples,
696
+ silentSamples
697
+ };
698
+ }
699
+ function analyseAudioEnergy(channelData, energyThreshold = 1e-3, silenceRatio = 0.9) {
700
+ const windowSize = 1024;
701
+ const energyLevels = [];
702
+ for (let i = 0; i < channelData.length; i += windowSize) {
703
+ let energy = 0;
704
+ const windowEnd = Math.min(i + windowSize, channelData.length);
705
+ for (let j = i; j < windowEnd; j += 1) {
706
+ energy += channelData[j] * channelData[j];
707
+ }
708
+ energy = energy / (windowEnd - i);
709
+ energyLevels.push(energy);
710
+ }
711
+ const silentWindows = energyLevels.filter((energy) => energy < energyThreshold).length;
712
+ const silencePercentage = energyLevels.length === 0 ? 1 : silentWindows / energyLevels.length;
713
+ const averageEnergy = energyLevels.length === 0 ? 0 : energyLevels.reduce((a, b) => a + b, 0) / energyLevels.length;
714
+ return {
715
+ isSilence: silencePercentage > silenceRatio,
716
+ silencePercentage,
717
+ averageEnergy,
718
+ energyLevels
719
+ };
720
+ }
721
+ function detectSilence(pcmBuffer) {
722
+ try {
723
+ const channelData = convertPCMToFloat32(pcmBuffer);
724
+ const amplitudeResult = analyseAudioAmplitude(channelData);
725
+ if (amplitudeResult.isSilence) {
726
+ const energyResult = analyseAudioEnergy(channelData);
727
+ if (energyResult.isSilence) {
728
+ return {
729
+ isSilence: true,
730
+ amplitudeAnalysis: amplitudeResult,
731
+ energyAnalysis: {
732
+ silencePercentage: energyResult.silencePercentage,
733
+ averageEnergy: energyResult.averageEnergy
734
+ }
735
+ };
736
+ }
737
+ return {
738
+ isSilence: false,
739
+ amplitudeAnalysis: amplitudeResult,
740
+ energyAnalysis: {
741
+ silencePercentage: energyResult.silencePercentage,
742
+ averageEnergy: energyResult.averageEnergy
743
+ }
744
+ };
745
+ }
746
+ return {
747
+ isSilence: false,
748
+ amplitudeAnalysis: amplitudeResult
749
+ };
750
+ } catch (error) {
751
+ console.error("[Autoscriber] silence detection failed", error);
752
+ return {
753
+ isSilence: false,
754
+ error: error instanceof Error ? error.message : String(error)
755
+ };
756
+ }
757
+ }
758
+
759
+ // src/audio/permissions.ts
760
+ async function checkMicrophonePermission(language) {
761
+ const lang = validateLanguage(language);
762
+ if (!("navigator" in globalThis) || !navigator.mediaDevices) {
763
+ return {
764
+ granted: false,
765
+ state: "denied",
766
+ message: translate(lang, "microphoneAccessError")
767
+ };
768
+ }
769
+ try {
770
+ if ("permissions" in navigator && "query" in navigator.permissions) {
771
+ try {
772
+ const permissionResult = await navigator.permissions.query({ name: "microphone" });
773
+ if (permissionResult.state === "granted") {
774
+ return { granted: true, state: "granted" };
775
+ }
776
+ if (permissionResult.state === "denied") {
777
+ return {
778
+ granted: false,
779
+ state: "denied",
780
+ message: translate(lang, "microphoneAccessDenied")
781
+ };
782
+ }
783
+ } catch (error) {
784
+ console.warn("[Autoscriber] permissions API error", error);
785
+ }
786
+ }
787
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1 } });
788
+ stream.getTracks().forEach((track) => track.stop());
789
+ return { granted: true, state: "granted" };
790
+ } catch (error) {
791
+ const err = error;
792
+ if (err?.name === "NotAllowedError" || err?.name === "PermissionDeniedError") {
793
+ return {
794
+ granted: false,
795
+ state: "denied",
796
+ message: translate(lang, "microphoneRequired")
797
+ };
798
+ }
799
+ if (err?.name === "NotFoundError" || err?.name === "DevicesNotFoundError") {
800
+ return {
801
+ granted: false,
802
+ state: "denied",
803
+ message: translate(lang, "microphoneNotFound")
804
+ };
805
+ }
806
+ return {
807
+ granted: false,
808
+ state: "denied",
809
+ message: translate(lang, "microphoneAccessError")
810
+ };
811
+ }
812
+ }
813
+ async function getAvailableMicrophones(language) {
814
+ const lang = validateLanguage(language);
815
+ if (!("navigator" in globalThis) || !navigator.mediaDevices?.enumerateDevices) {
816
+ console.warn("[Autoscriber] media devices API unavailable");
817
+ return [];
818
+ }
819
+ try {
820
+ const devices = await navigator.mediaDevices.enumerateDevices();
821
+ return devices.filter((device) => device.kind === "audioinput").map((device) => ({
822
+ deviceId: device.deviceId,
823
+ label: device.label || `${translate(lang, "microphone")} ${device.deviceId.slice(0, 8)}`,
824
+ groupId: device.groupId
825
+ }));
826
+ } catch (error) {
827
+ console.error("[Autoscriber] failed to enumerate microphones", error);
828
+ return [];
829
+ }
830
+ }
831
+
832
+ // src/utils/token.ts
833
+ function decodeJwtPayload(token) {
834
+ const parts = token.split(".");
835
+ if (parts.length !== 3) {
836
+ return null;
837
+ }
838
+ try {
839
+ const payload = JSON.parse(atob(parts[1]));
840
+ return payload;
841
+ } catch (error) {
842
+ console.error("[Autoscriber] failed to decode JWT payload", error);
843
+ return null;
844
+ }
845
+ }
846
+ function getSessionIdFromToken(token) {
847
+ const payload = decodeJwtPayload(token);
848
+ if (!payload)
849
+ return null;
850
+ const sessionId = payload.sessionId ?? payload.session_id;
851
+ return typeof sessionId === "string" ? sessionId : null;
852
+ }
853
+ function isTokenExpiredOrExpiring(token, secondsBuffer = 10) {
854
+ const payload = decodeJwtPayload(token);
855
+ if (!payload)
856
+ return true;
857
+ const exp = payload.exp;
858
+ if (typeof exp !== "number")
859
+ return true;
860
+ const current = Math.floor(Date.now() / 1e3);
861
+ return current >= exp - secondsBuffer;
862
+ }
863
+
864
+ // src/audio/wake-lock.ts
865
+ var WakeLockManager = class _WakeLockManager {
866
+ constructor() {
867
+ this.wakeLock = null;
868
+ }
869
+ /**
870
+ * Check if Wake Lock API is supported in the current browser
871
+ */
872
+ static isSupported() {
873
+ return typeof navigator !== "undefined" && "wakeLock" in navigator;
874
+ }
875
+ /**
876
+ * Request a wake lock to prevent the screen from sleeping
877
+ * @returns Promise that resolves when wake lock is acquired, or rejects if not supported/failed
878
+ */
879
+ async request() {
880
+ if (!_WakeLockManager.isSupported()) {
881
+ throw new Error("Wake Lock API is not supported in this browser");
882
+ }
883
+ try {
884
+ this.wakeLock = await navigator.wakeLock.request("screen");
885
+ this.wakeLock.addEventListener("release", () => {
886
+ this.wakeLock = null;
887
+ });
888
+ } catch (error) {
889
+ throw new Error(`Failed to acquire wake lock: ${error instanceof Error ? error.message : String(error)}`);
890
+ }
891
+ }
892
+ /**
893
+ * Release the wake lock if it's currently active
894
+ */
895
+ async release() {
896
+ if (this.wakeLock) {
897
+ try {
898
+ await this.wakeLock.release();
899
+ } catch (error) {
900
+ console.warn("[WakeLock] Error releasing wake lock", error);
901
+ } finally {
902
+ this.wakeLock = null;
903
+ }
904
+ }
905
+ }
906
+ /**
907
+ * Check if a wake lock is currently active
908
+ */
909
+ isActive() {
910
+ return this.wakeLock !== null;
911
+ }
912
+ /**
913
+ * Clean up resources (release wake lock and remove listeners)
914
+ */
915
+ async destroy() {
916
+ await this.release();
917
+ }
918
+ };
919
+
920
+ // src/core/assistant-client.ts
921
+ var TOKEN_REFRESH_THROTTLE_MS = 15e3;
922
+ var AssistantClient = class {
923
+ constructor(options) {
924
+ this.bus = new EventBus();
925
+ this.logger = createLogger();
926
+ this.messageStore = new MessageStore();
927
+ this.offlineQueue = new OfflineQueue();
928
+ this.audioManager = new AudioManager();
929
+ this.wakeLockManager = new WakeLockManager();
930
+ this.bufferTimer = null;
931
+ this.audioBuffer = [];
932
+ this.voiceCommandBuffer = [];
933
+ this.manualDisconnect = false;
934
+ this.pendingRequests = /* @__PURE__ */ new Map();
935
+ this.reconnectTimer = null;
936
+ this.lastTokenRefreshAttempt = null;
937
+ this.tokenRefreshInFlight = false;
938
+ this.hasConnected = false;
939
+ this.handleBrowserOnline = () => {
940
+ this.snapshot.connection.online = true;
941
+ if (this.snapshot.connection.reason === "offline") {
942
+ this.snapshot.connection.reason = "reconnecting";
943
+ }
944
+ this.emitConnection();
945
+ this.manualDisconnect = false;
946
+ if (this.websocket.readyState !== WebSocket.OPEN) {
947
+ void this.connectWebsocket();
948
+ } else {
949
+ this.flushOfflineQueue();
950
+ }
951
+ };
952
+ this.handleBrowserOffline = () => {
953
+ if (!this.snapshot.connection.online && this.snapshot.connection.reason === "offline") {
954
+ return;
955
+ }
956
+ this.snapshot.connection.online = false;
957
+ this.snapshot.connection.reason = "offline";
958
+ if (this.snapshot.connection.websocketStatus === "open") {
959
+ this.snapshot.connection.websocketStatus = "closing";
960
+ }
961
+ this.snapshot.connection.buffering = this.offlineQueue.size > 0 || this.snapshot.offlineQueueLength > 0;
962
+ this.emitConnection();
963
+ if (this.snapshot.recording.isRecording) {
964
+ if (this.snapshot.recording.mode === "transcription") {
965
+ this.pushInteraction({
966
+ id: generateMessageId(),
967
+ label: translate(this.snapshot.language, "transcriptionBufferingOffline"),
968
+ status: "pending",
969
+ type: "transcription",
970
+ metadata: { reason: "offline" }
971
+ });
972
+ } else if (this.snapshot.recording.mode === "voice_command") {
973
+ this.snapshot.recording.wasTranscriptionActive = false;
974
+ this.snapshot.recording.transcriptionPausedState = false;
975
+ this.pushInteraction({
976
+ id: generateMessageId(),
977
+ label: translate(this.snapshot.language, "voiceCommandCancelledOffline"),
978
+ status: "error",
979
+ type: "voice"
980
+ });
981
+ this.stopRecording();
982
+ }
983
+ }
984
+ this.pushInteraction({
985
+ id: generateMessageId(),
986
+ label: translate(this.snapshot.language, "networkOffline"),
987
+ status: "error",
988
+ type: "info"
989
+ });
990
+ const readyState = this.websocket.readyState;
991
+ if (readyState === WebSocket.OPEN || readyState === WebSocket.CONNECTING) {
992
+ this.manualDisconnect = true;
993
+ this.websocket.close(4001, "Browser offline");
994
+ }
995
+ };
996
+ if (!options?.token) {
997
+ throw new Error("Autoscriber Assistant requires a token");
998
+ }
999
+ this.options = options;
1000
+ this.logger = createLogger(options.logger);
1001
+ this.token = options.token;
1002
+ const language = validateLanguage(options.language);
1003
+ this.snapshot = {
1004
+ language,
1005
+ debugMode: options.debugMode ?? false,
1006
+ messages: [],
1007
+ interactions: [],
1008
+ connection: {
1009
+ websocketStatus: "idle",
1010
+ online: typeof navigator !== "undefined" ? navigator.onLine : true,
1011
+ buffering: false
1012
+ },
1013
+ microphones: {
1014
+ available: [],
1015
+ permission: "unknown"
1016
+ },
1017
+ recording: {
1018
+ mode: "idle",
1019
+ transcriptionId: null,
1020
+ isRecording: false,
1021
+ isPaused: false,
1022
+ bufferedChunks: 0,
1023
+ wasTranscriptionActive: false,
1024
+ transcriptionPausedState: false
1025
+ },
1026
+ offlineQueueLength: 0,
1027
+ lastUpdatedAt: Date.now()
1028
+ };
1029
+ this.websocket = new WebSocketClient({
1030
+ logger: options.logger,
1031
+ reconnectDelay: options.websocketReconnectDelay,
1032
+ autoReconnect: false
1033
+ });
1034
+ this.registerWebsocketEvents();
1035
+ this.setupNetworkListeners();
1036
+ this.initializeMicrophones();
1037
+ this.connectWebsocket();
1038
+ const sessionId = getSessionIdFromToken(this.token);
1039
+ this.logStructured("info", "Session created", {
1040
+ sessionId: sessionId ?? "unknown",
1041
+ language,
1042
+ websocketUrl: options.websocketUrl ?? "default",
1043
+ debugMode: options.debugMode ?? false
1044
+ });
1045
+ }
1046
+ /**
1047
+ * Internal structured logging helper for Datadog monitoring
1048
+ * Formats logs as structured JSON for better parsing and filtering
1049
+ */
1050
+ logStructured(level, message, metadata) {
1051
+ try {
1052
+ const structuredLog = {
1053
+ message,
1054
+ level,
1055
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1056
+ sdk: "autoscriber-core",
1057
+ ...metadata
1058
+ };
1059
+ const logMessage = `[Autoscriber] ${message}${metadata ? ` ${JSON.stringify(metadata)}` : ""}`;
1060
+ if (this.options.onLog) {
1061
+ try {
1062
+ this.options.onLog(level, message, structuredLog);
1063
+ } catch (error) {
1064
+ this.logger.warn("[Autoscriber] onLog callback failed", error);
1065
+ }
1066
+ }
1067
+ switch (level) {
1068
+ case "info":
1069
+ this.logger.info(logMessage, structuredLog);
1070
+ break;
1071
+ case "warn":
1072
+ this.logger.warn(logMessage, structuredLog);
1073
+ break;
1074
+ case "error":
1075
+ this.logger.error(logMessage, structuredLog);
1076
+ break;
1077
+ }
1078
+ } catch (error) {
1079
+ this.logger.error(`[Autoscriber] Failed to log structured message: ${message}`, error);
1080
+ }
1081
+ }
1082
+ on(event, listener) {
1083
+ return this.bus.on(event, listener);
1084
+ }
1085
+ getSnapshot() {
1086
+ return { ...this.snapshot, messages: this.messageStore.getMessages(), interactions: this.messageStore.getInteractions() };
1087
+ }
1088
+ setDebugMode(enabled) {
1089
+ const next = Boolean(enabled);
1090
+ if (this.snapshot.debugMode === next) {
1091
+ return;
1092
+ }
1093
+ this.snapshot.debugMode = next;
1094
+ this.emitSnapshot();
1095
+ }
1096
+ setLanguage(language) {
1097
+ const validatedLanguage = validateLanguage(language);
1098
+ if (this.snapshot.language === validatedLanguage) {
1099
+ return;
1100
+ }
1101
+ const oldLanguage = this.snapshot.language;
1102
+ this.snapshot.language = validatedLanguage;
1103
+ this.emitSnapshot();
1104
+ this.bus.emit("language:changed", validatedLanguage);
1105
+ const sessionId = getSessionIdFromToken(this.token);
1106
+ this.logStructured("info", "Language changed", {
1107
+ oldLanguage,
1108
+ newLanguage: validatedLanguage,
1109
+ sessionId: sessionId ?? "unknown"
1110
+ });
1111
+ }
1112
+ destroy() {
1113
+ this.manualDisconnect = true;
1114
+ if (this.reconnectTimer) {
1115
+ clearTimeout(this.reconnectTimer);
1116
+ this.reconnectTimer = null;
1117
+ }
1118
+ this.websocket.close(1e3, "Destroyed");
1119
+ this.audioManager.teardown();
1120
+ void this.wakeLockManager.destroy().catch((error) => {
1121
+ this.logger.warn("[Autoscriber] Error releasing wake lock on destroy", error);
1122
+ });
1123
+ if (typeof window !== "undefined") {
1124
+ window.removeEventListener("online", this.handleBrowserOnline);
1125
+ window.removeEventListener("offline", this.handleBrowserOffline);
1126
+ }
1127
+ if (this.bufferTimer) {
1128
+ clearInterval(this.bufferTimer);
1129
+ this.bufferTimer = null;
1130
+ }
1131
+ this.messageStore.reset();
1132
+ this.bus.clear();
1133
+ }
1134
+ /** Chat related methods */
1135
+ sendTextMessage(text) {
1136
+ const trimmed = text.trim();
1137
+ if (!trimmed) {
1138
+ throw new Error("Cannot send empty message");
1139
+ }
1140
+ const id = generateMessageId();
1141
+ const message = this.messageStore.createMessage({
1142
+ id,
1143
+ request: trimmed,
1144
+ type: "chat"
1145
+ });
1146
+ this.emitMessages();
1147
+ this.sendMessage({
1148
+ jsonrpc: "2.0",
1149
+ id,
1150
+ method: "chat",
1151
+ params: {
1152
+ mediaType: "text/plain",
1153
+ data: trimmed
1154
+ }
1155
+ });
1156
+ return message;
1157
+ }
1158
+ /** Recording controls */
1159
+ async startTranscription(customId) {
1160
+ if (this.snapshot.recording.isRecording) {
1161
+ this.logger.warn("[Autoscriber] startTranscription called while recording");
1162
+ return this.snapshot.recording.transcriptionId ?? "";
1163
+ }
1164
+ const permission = await checkMicrophonePermission(this.snapshot.language);
1165
+ this.updateMicrophonePermission(permission.state);
1166
+ if (!permission.granted) {
1167
+ if (permission.message) {
1168
+ this.pushInteraction({
1169
+ id: generateMessageId(),
1170
+ label: permission.message,
1171
+ status: "error",
1172
+ type: "transcription"
1173
+ });
1174
+ }
1175
+ return "";
1176
+ }
1177
+ const transcriptionId = customId ?? generateMessageId();
1178
+ await this.audioManager.init({
1179
+ deviceId: this.snapshot.microphones.selected?.deviceId,
1180
+ targetSampleRate: this.options.targetSampleRate,
1181
+ onRecorderData: (pcm16) => this.handleRecorderData(pcm16, "transcription")
1182
+ });
1183
+ try {
1184
+ await this.wakeLockManager.request();
1185
+ } catch (error) {
1186
+ this.logger.warn("[Autoscriber] Failed to acquire wake lock", error);
1187
+ }
1188
+ this.audioBuffer = [];
1189
+ this.snapshot.recording = {
1190
+ ...this.snapshot.recording,
1191
+ mode: "transcription",
1192
+ transcriptionId,
1193
+ isRecording: true,
1194
+ isPaused: false,
1195
+ bufferedChunks: 0
1196
+ };
1197
+ this.emitRecording();
1198
+ const sessionId = getSessionIdFromToken(this.token);
1199
+ this.logStructured("info", "Transcription started", {
1200
+ transcriptionId,
1201
+ mode: "transcription",
1202
+ sessionId: sessionId ?? "unknown",
1203
+ deviceId: this.snapshot.microphones.selected?.deviceId
1204
+ });
1205
+ this.pushInteraction({
1206
+ id: transcriptionId,
1207
+ label: translate(this.snapshot.language, "transcriptionStarted"),
1208
+ status: "pending",
1209
+ type: "transcription"
1210
+ });
1211
+ if (this.bufferTimer) {
1212
+ clearInterval(this.bufferTimer);
1213
+ }
1214
+ this.bufferTimer = window.setInterval(() => {
1215
+ if (this.snapshot.recording.mode === "transcription" && this.audioBuffer.length > 0 && !this.snapshot.recording.isPaused) {
1216
+ this.sendBufferedAudio();
1217
+ }
1218
+ }, 5e3);
1219
+ return transcriptionId;
1220
+ }
1221
+ pauseTranscription() {
1222
+ if (this.snapshot.recording.mode !== "transcription")
1223
+ return;
1224
+ this.snapshot.recording.isPaused = !this.snapshot.recording.isPaused;
1225
+ this.emitRecording();
1226
+ }
1227
+ stopRecording() {
1228
+ if (!this.snapshot.recording.isRecording)
1229
+ return;
1230
+ const currentMode = this.snapshot.recording.mode;
1231
+ const shouldResumeTranscription = currentMode === "voice_command" && this.snapshot.recording.wasTranscriptionActive;
1232
+ const resumePausedState = this.snapshot.recording.transcriptionPausedState;
1233
+ if (currentMode === "transcription") {
1234
+ const transcriptionId = this.snapshot.recording.transcriptionId ?? generateMessageId();
1235
+ this.pushInteraction({
1236
+ id: transcriptionId,
1237
+ label: translate(this.snapshot.language, "transcriptionStopped"),
1238
+ status: "success",
1239
+ type: "transcription"
1240
+ });
1241
+ if (this.audioBuffer.length > 0) {
1242
+ this.sendBufferedAudio(true);
1243
+ }
1244
+ const sessionId = getSessionIdFromToken(this.token);
1245
+ this.logStructured("info", "Transcription stopped", {
1246
+ transcriptionId,
1247
+ mode: "transcription",
1248
+ sessionId: sessionId ?? "unknown",
1249
+ bufferedChunks: this.snapshot.recording.bufferedChunks
1250
+ });
1251
+ } else if (currentMode === "voice_command") {
1252
+ if (this.voiceCommandBuffer.length > 0) {
1253
+ this.sendVoiceCommandAudio();
1254
+ }
1255
+ }
1256
+ this.audioManager.stop();
1257
+ void this.wakeLockManager.release().catch((error) => {
1258
+ this.logger.warn("[Autoscriber] Failed to release wake lock", error);
1259
+ });
1260
+ if (shouldResumeTranscription) {
1261
+ void this.resumeTranscriptionAfterVoiceCommand(resumePausedState);
1262
+ } else {
1263
+ this.resetRecordingState();
1264
+ }
1265
+ }
1266
+ async startVoiceCommand() {
1267
+ if (this.snapshot.recording.mode === "transcription") {
1268
+ this.snapshot.recording.wasTranscriptionActive = true;
1269
+ this.snapshot.recording.transcriptionPausedState = this.snapshot.recording.isPaused;
1270
+ this.snapshot.recording.isPaused = true;
1271
+ }
1272
+ const permission = await checkMicrophonePermission(this.snapshot.language);
1273
+ this.updateMicrophonePermission(permission.state);
1274
+ if (!permission.granted) {
1275
+ if (permission.message) {
1276
+ this.pushInteraction({
1277
+ id: generateMessageId(),
1278
+ label: permission.message,
1279
+ status: "error",
1280
+ type: "voice"
1281
+ });
1282
+ }
1283
+ return;
1284
+ }
1285
+ await this.audioManager.init({
1286
+ deviceId: this.snapshot.microphones.selected?.deviceId,
1287
+ targetSampleRate: this.options.targetSampleRate,
1288
+ onRecorderData: (pcm16) => this.handleRecorderData(pcm16, "voice_command")
1289
+ });
1290
+ try {
1291
+ await this.wakeLockManager.request();
1292
+ } catch (error) {
1293
+ this.logger.warn("[Autoscriber] Failed to acquire wake lock", error);
1294
+ }
1295
+ this.voiceCommandBuffer = [];
1296
+ this.snapshot.recording = {
1297
+ ...this.snapshot.recording,
1298
+ mode: "voice_command",
1299
+ isRecording: true,
1300
+ isPaused: false,
1301
+ bufferedChunks: 0
1302
+ };
1303
+ this.emitRecording();
1304
+ const sessionId = getSessionIdFromToken(this.token);
1305
+ this.logStructured("info", "Voice command started", {
1306
+ mode: "voice_command",
1307
+ sessionId: sessionId ?? "unknown",
1308
+ deviceId: this.snapshot.microphones.selected?.deviceId
1309
+ });
1310
+ }
1311
+ /** FHIR context */
1312
+ setFhirContext(resource) {
1313
+ const id = generateMessageId();
1314
+ const resourceType = typeof resource === "object" && resource && "resourceType" in resource ? String(resource.resourceType) : "Resource";
1315
+ this.pushInteraction({
1316
+ id,
1317
+ label: `Updating ${resourceType} context`,
1318
+ status: "pending",
1319
+ type: "fhir",
1320
+ metadata: { resourceType }
1321
+ });
1322
+ this.sendMessage({
1323
+ jsonrpc: "2.0",
1324
+ id,
1325
+ method: "fhirContext",
1326
+ params: {
1327
+ mediaType: "application/fhir+json",
1328
+ data: resource
1329
+ }
1330
+ });
1331
+ this.pendingRequests.set(id, { id, method: "fhirContext" });
1332
+ return id;
1333
+ }
1334
+ fillQuestionnaire(context) {
1335
+ const id = generateMessageId();
1336
+ const message = this.messageStore.createMessage({
1337
+ id,
1338
+ request: null,
1339
+ type: "questionnaire"
1340
+ });
1341
+ this.emitMessages();
1342
+ this.pushInteraction({
1343
+ id,
1344
+ label: "Summarising",
1345
+ status: "pending",
1346
+ type: "questionnaire"
1347
+ });
1348
+ this.sendMessage({
1349
+ jsonrpc: "2.0",
1350
+ id,
1351
+ method: "tool",
1352
+ params: {
1353
+ name: "fillQuestionnaire",
1354
+ args: context ? { context } : {}
1355
+ }
1356
+ });
1357
+ this.pendingRequests.set(id, { id, method: "fillQuestionnaire" });
1358
+ return id;
1359
+ }
1360
+ /** Microphone controls */
1361
+ async refreshMicrophones() {
1362
+ const microphones = await getAvailableMicrophones(this.snapshot.language);
1363
+ const selected = this.snapshot.microphones.selected;
1364
+ const stillAvailable = selected && microphones.find((mic) => mic.deviceId === selected.deviceId);
1365
+ this.snapshot.microphones = {
1366
+ ...this.snapshot.microphones,
1367
+ available: microphones,
1368
+ selected: stillAvailable ?? microphones[0] ?? selected
1369
+ };
1370
+ this.emitMicrophones();
1371
+ }
1372
+ setSelectedMicrophone(device) {
1373
+ this.snapshot.microphones = {
1374
+ ...this.snapshot.microphones,
1375
+ selected: device
1376
+ };
1377
+ if (typeof window !== "undefined" && window.localStorage) {
1378
+ window.localStorage.setItem(
1379
+ "autoscriber_selected_microphone",
1380
+ JSON.stringify(device)
1381
+ );
1382
+ }
1383
+ this.emitMicrophones();
1384
+ }
1385
+ /** Internal plumbing */
1386
+ registerWebsocketEvents() {
1387
+ this.websocket.on("open", () => {
1388
+ const wasOffline = this.hasConnected && (this.snapshot.connection.reason === "offline" || this.snapshot.connection.reason === "reconnecting");
1389
+ if (this.reconnectTimer) {
1390
+ clearTimeout(this.reconnectTimer);
1391
+ this.reconnectTimer = null;
1392
+ }
1393
+ this.snapshot.connection.websocketStatus = "open";
1394
+ this.snapshot.connection.reason = void 0;
1395
+ this.snapshot.connection.lastError = void 0;
1396
+ this.snapshot.connection.buffering = this.offlineQueue.size > 0;
1397
+ this.emitConnection();
1398
+ if (wasOffline) {
1399
+ this.pushInteraction({
1400
+ id: generateMessageId(),
1401
+ label: translate(this.snapshot.language, "connectionRestored"),
1402
+ status: "success",
1403
+ type: "info"
1404
+ });
1405
+ }
1406
+ this.flushOfflineQueue();
1407
+ this.hasConnected = true;
1408
+ });
1409
+ this.websocket.on("close", () => {
1410
+ const preserveOffline = this.snapshot.connection.reason === "offline";
1411
+ this.snapshot.connection.websocketStatus = "closed";
1412
+ if (!preserveOffline) {
1413
+ this.snapshot.connection.reason = this.manualDisconnect ? "manual" : "closed";
1414
+ }
1415
+ this.emitConnection();
1416
+ this.scheduleReconnectAttempt();
1417
+ });
1418
+ this.websocket.on("error", (event) => {
1419
+ this.snapshot.connection.websocketStatus = "closed";
1420
+ if (this.snapshot.connection.reason !== "offline") {
1421
+ this.snapshot.connection.reason = "error";
1422
+ }
1423
+ let errorMessage;
1424
+ let errorDetails = {};
1425
+ if (event instanceof ErrorEvent) {
1426
+ errorMessage = event.message || "WebSocket error event";
1427
+ errorDetails = {
1428
+ type: event.type,
1429
+ filename: event.filename,
1430
+ lineno: event.lineno,
1431
+ colno: event.colno,
1432
+ error: event.error ? String(event.error) : void 0
1433
+ };
1434
+ } else if (event instanceof Event) {
1435
+ errorMessage = `WebSocket error: ${event.type}`;
1436
+ errorDetails = {
1437
+ type: event.type,
1438
+ target: event.target ? String(event.target) : void 0,
1439
+ currentTarget: event.currentTarget ? String(event.currentTarget) : void 0,
1440
+ timeStamp: event.timeStamp
1441
+ };
1442
+ } else {
1443
+ errorMessage = "WebSocket error";
1444
+ errorDetails = {
1445
+ errorType: typeof event,
1446
+ errorValue: String(event)
1447
+ };
1448
+ }
1449
+ this.snapshot.connection.lastError = errorMessage;
1450
+ this.emitConnection();
1451
+ const sessionId = getSessionIdFromToken(this.token);
1452
+ const wsUrl = this.buildWebsocketUrl();
1453
+ this.logStructured("error", "WebSocket error event", {
1454
+ error: errorMessage,
1455
+ ...errorDetails,
1456
+ websocketUrl: wsUrl,
1457
+ sessionId: sessionId ?? "unknown",
1458
+ readyState: this.websocket.readyState
1459
+ });
1460
+ this.scheduleReconnectAttempt();
1461
+ });
1462
+ this.websocket.on("reconnecting", () => {
1463
+ if (this.snapshot.connection.reason === "offline")
1464
+ return;
1465
+ this.snapshot.connection.websocketStatus = "connecting";
1466
+ this.snapshot.connection.reason = "reconnecting";
1467
+ this.emitConnection();
1468
+ });
1469
+ this.websocket.on("message", (event) => {
1470
+ this.handleMessage(event.data);
1471
+ });
1472
+ }
1473
+ setupNetworkListeners() {
1474
+ if (typeof window === "undefined")
1475
+ return;
1476
+ window.addEventListener("online", this.handleBrowserOnline);
1477
+ window.addEventListener("offline", this.handleBrowserOffline);
1478
+ if (!navigator.onLine) {
1479
+ this.handleBrowserOffline();
1480
+ }
1481
+ }
1482
+ scheduleReconnectAttempt() {
1483
+ if (this.manualDisconnect) {
1484
+ return;
1485
+ }
1486
+ if (!this.snapshot.connection.online) {
1487
+ return;
1488
+ }
1489
+ if (this.reconnectTimer) {
1490
+ return;
1491
+ }
1492
+ const delay = this.options.websocketReconnectDelay ?? 3e3;
1493
+ if (this.hasConnected && this.snapshot.connection.reason !== "offline") {
1494
+ this.snapshot.connection.reason = "reconnecting";
1495
+ }
1496
+ if (this.snapshot.connection.websocketStatus !== "open") {
1497
+ this.snapshot.connection.websocketStatus = "connecting";
1498
+ }
1499
+ this.emitConnection();
1500
+ this.reconnectTimer = setTimeout(() => {
1501
+ this.reconnectTimer = null;
1502
+ if (this.manualDisconnect || !this.snapshot.connection.online) {
1503
+ return;
1504
+ }
1505
+ void this.connectWebsocket();
1506
+ }, delay);
1507
+ }
1508
+ async initializeMicrophones() {
1509
+ if (typeof window !== "undefined" && window.localStorage) {
1510
+ try {
1511
+ const stored = window.localStorage.getItem("autoscriber_selected_microphone");
1512
+ if (stored) {
1513
+ const parsed = JSON.parse(stored);
1514
+ this.snapshot.microphones.selected = parsed;
1515
+ }
1516
+ } catch (error) {
1517
+ this.logger.warn("[Autoscriber] unable to parse stored microphone", error);
1518
+ }
1519
+ }
1520
+ await this.refreshMicrophones();
1521
+ }
1522
+ async connectWebsocket() {
1523
+ try {
1524
+ if (this.options.refreshToken && !this.tokenRefreshInFlight) {
1525
+ const isExpired = isTokenExpiredOrExpiring(this.token, 0);
1526
+ const isExpiringSoon = isTokenExpiredOrExpiring(this.token);
1527
+ if (isExpired || isExpiringSoon) {
1528
+ const now = Date.now();
1529
+ const throttleElapsed = !this.lastTokenRefreshAttempt || now - this.lastTokenRefreshAttempt >= TOKEN_REFRESH_THROTTLE_MS;
1530
+ if (isExpired || throttleElapsed) {
1531
+ this.lastTokenRefreshAttempt = now;
1532
+ const sessionId = getSessionIdFromToken(this.token);
1533
+ if (sessionId) {
1534
+ this.tokenRefreshInFlight = true;
1535
+ try {
1536
+ const newToken = await this.options.refreshToken(sessionId);
1537
+ if (newToken) {
1538
+ this.token = newToken;
1539
+ this.lastTokenRefreshAttempt = null;
1540
+ this.logStructured("info", "Token refresh succeeded", {
1541
+ sessionId,
1542
+ wasExpired: isExpired
1543
+ });
1544
+ } else if (isExpired) {
1545
+ this.logger.warn("[Autoscriber] refresh returned no token while current token expired");
1546
+ this.lastTokenRefreshAttempt = null;
1547
+ this.logStructured("warn", "Token refresh returned no token", {
1548
+ sessionId,
1549
+ wasExpired: true
1550
+ });
1551
+ }
1552
+ } catch (refreshError) {
1553
+ this.logger.error("[Autoscriber] token refresh failed", refreshError);
1554
+ this.logStructured("error", "Token refresh failed", {
1555
+ sessionId,
1556
+ wasExpired: isExpired,
1557
+ error: refreshError instanceof Error ? refreshError.message : String(refreshError)
1558
+ });
1559
+ if (isExpired) {
1560
+ this.lastTokenRefreshAttempt = null;
1561
+ }
1562
+ } finally {
1563
+ this.tokenRefreshInFlight = false;
1564
+ }
1565
+ }
1566
+ }
1567
+ if (isExpired && isTokenExpiredOrExpiring(this.token, 0)) {
1568
+ throw new Error("Token expired and refresh unavailable");
1569
+ }
1570
+ }
1571
+ }
1572
+ const wsUrl = this.buildWebsocketUrl();
1573
+ this.snapshot.connection.websocketStatus = "connecting";
1574
+ if (this.hasConnected && this.snapshot.connection.reason !== "offline") {
1575
+ this.snapshot.connection.reason = "reconnecting";
1576
+ }
1577
+ this.emitConnection();
1578
+ await this.websocket.connect(wsUrl);
1579
+ } catch (error) {
1580
+ this.logger.error("[Autoscriber] failed to connect websocket", error);
1581
+ let errorMessage;
1582
+ let errorDetails = {};
1583
+ if (error instanceof Error) {
1584
+ errorMessage = error.message;
1585
+ errorDetails = {
1586
+ name: error.name,
1587
+ stack: error.stack
1588
+ };
1589
+ } else if (error instanceof ErrorEvent) {
1590
+ errorMessage = error.message || "WebSocket error event";
1591
+ errorDetails = {
1592
+ type: error.type,
1593
+ filename: error.filename,
1594
+ lineno: error.lineno,
1595
+ colno: error.colno,
1596
+ error: error.error ? String(error.error) : void 0
1597
+ };
1598
+ } else if (error instanceof Event) {
1599
+ errorMessage = `WebSocket error: ${error.type}`;
1600
+ errorDetails = {
1601
+ type: error.type,
1602
+ target: error.target ? String(error.target) : void 0,
1603
+ currentTarget: error.currentTarget ? String(error.currentTarget) : void 0,
1604
+ timeStamp: error.timeStamp
1605
+ };
1606
+ } else {
1607
+ errorMessage = String(error);
1608
+ errorDetails = {
1609
+ errorType: typeof error,
1610
+ errorValue: String(error)
1611
+ };
1612
+ }
1613
+ this.snapshot.connection.lastError = errorMessage;
1614
+ this.snapshot.connection.websocketStatus = "closed";
1615
+ if (this.snapshot.connection.reason !== "offline") {
1616
+ this.snapshot.connection.reason = "error";
1617
+ }
1618
+ this.emitConnection();
1619
+ const sessionId = getSessionIdFromToken(this.token);
1620
+ const wsUrl = this.buildWebsocketUrl();
1621
+ this.logStructured("error", "WebSocket connection failed", {
1622
+ error: errorMessage,
1623
+ ...errorDetails,
1624
+ websocketUrl: wsUrl,
1625
+ sessionId: sessionId ?? "unknown"
1626
+ });
1627
+ this.scheduleReconnectAttempt();
1628
+ }
1629
+ }
1630
+ buildWebsocketUrl() {
1631
+ if (this.options.websocketUrl) {
1632
+ return `${this.options.websocketUrl}?token=${this.token}`;
1633
+ }
1634
+ if (typeof window === "undefined") {
1635
+ throw new Error("Window is not available to resolve WebSocket URL");
1636
+ }
1637
+ const origin = window.location.origin;
1638
+ const wsBase = origin.replace(/^http/, "ws");
1639
+ return `${wsBase}/ws?token=${this.token}`;
1640
+ }
1641
+ sendMessage(payload) {
1642
+ if (this.snapshot.connection.online && this.websocket.readyState === WebSocket.OPEN) {
1643
+ const message = JSON.stringify(payload);
1644
+ this.websocket.send(message);
1645
+ return;
1646
+ }
1647
+ const wasEmpty = this.offlineQueue.size === 0;
1648
+ this.offlineQueue.enqueue(payload);
1649
+ this.snapshot.offlineQueueLength = this.offlineQueue.size;
1650
+ this.snapshot.connection.buffering = true;
1651
+ if (wasEmpty) {
1652
+ this.pushInteraction({
1653
+ id: generateMessageId(),
1654
+ label: translate(this.snapshot.language, "offlineBuffering"),
1655
+ status: "pending",
1656
+ type: "info"
1657
+ });
1658
+ }
1659
+ this.emitConnection();
1660
+ }
1661
+ flushOfflineQueue() {
1662
+ if (this.offlineQueue.size === 0) {
1663
+ if (this.snapshot.connection.buffering) {
1664
+ this.snapshot.connection.buffering = false;
1665
+ this.emitConnection();
1666
+ }
1667
+ return;
1668
+ }
1669
+ const items = this.offlineQueue.drain();
1670
+ this.snapshot.offlineQueueLength = 0;
1671
+ items.forEach((payload) => this.sendMessage(payload));
1672
+ const hasQueued = this.offlineQueue.size > 0;
1673
+ this.snapshot.connection.buffering = hasQueued;
1674
+ this.snapshot.offlineQueueLength = hasQueued ? this.offlineQueue.size : 0;
1675
+ this.emitConnection();
1676
+ }
1677
+ emitSnapshot() {
1678
+ this.snapshot.lastUpdatedAt = Date.now();
1679
+ this.bus.emit("snapshot", this.getSnapshot());
1680
+ }
1681
+ emitMessages() {
1682
+ const messages = this.messageStore.getMessages();
1683
+ this.snapshot.messages = messages;
1684
+ this.bus.emit("messages:updated", messages);
1685
+ this.emitSnapshot();
1686
+ }
1687
+ emitInteractions() {
1688
+ const interactions = this.messageStore.getInteractions();
1689
+ this.snapshot.interactions = interactions;
1690
+ this.bus.emit("interactions:updated", interactions);
1691
+ this.emitSnapshot();
1692
+ }
1693
+ emitConnection() {
1694
+ this.bus.emit("connection:changed", this.snapshot.connection);
1695
+ this.emitSnapshot();
1696
+ }
1697
+ emitRecording() {
1698
+ this.bus.emit("recording:changed", this.snapshot.recording);
1699
+ this.emitSnapshot();
1700
+ }
1701
+ emitMicrophones() {
1702
+ this.bus.emit("microphones:changed", this.snapshot.microphones);
1703
+ this.emitSnapshot();
1704
+ }
1705
+ pushInteraction(pill) {
1706
+ this.messageStore.upsertInteraction(pill);
1707
+ this.emitInteractions();
1708
+ }
1709
+ updateMicrophonePermission(state) {
1710
+ this.snapshot.microphones.permission = state;
1711
+ this.emitMicrophones();
1712
+ }
1713
+ handleRecorderData(pcm16, mode) {
1714
+ if (mode === "transcription") {
1715
+ if (!this.snapshot.recording.isRecording || this.snapshot.recording.mode !== "transcription") {
1716
+ return;
1717
+ }
1718
+ if (!this.snapshot.recording.isPaused) {
1719
+ this.audioBuffer.push(pcm16);
1720
+ this.snapshot.recording.bufferedChunks = this.audioBuffer.length;
1721
+ }
1722
+ } else if (mode === "voice_command") {
1723
+ if (!this.snapshot.recording.isRecording || this.snapshot.recording.mode !== "voice_command") {
1724
+ return;
1725
+ }
1726
+ this.voiceCommandBuffer.push(pcm16);
1727
+ }
1728
+ }
1729
+ sendBufferedAudio(isFinal = false) {
1730
+ if (!this.snapshot.recording.transcriptionId || this.audioBuffer.length === 0) {
1731
+ return;
1732
+ }
1733
+ const combined = this.concatPcmChunks(this.audioBuffer);
1734
+ this.audioBuffer = [];
1735
+ this.snapshot.recording.bufferedChunks = 0;
1736
+ this.emitRecording();
1737
+ const payload = {
1738
+ jsonrpc: "2.0",
1739
+ method: "transcribe",
1740
+ params: {
1741
+ id: this.snapshot.recording.transcriptionId,
1742
+ mediaType: "audio/pcm",
1743
+ data: arrayBufferToBase64(combined.buffer),
1744
+ isFinal
1745
+ }
1746
+ };
1747
+ if (!this.messageStore.getMessageById(this.snapshot.recording.transcriptionId)) {
1748
+ const message = {
1749
+ id: this.snapshot.recording.transcriptionId,
1750
+ request: null,
1751
+ partialResults: [],
1752
+ result: null,
1753
+ type: "transcription"
1754
+ };
1755
+ this.messageStore.addMessage(message);
1756
+ payload.id = this.snapshot.recording.transcriptionId;
1757
+ }
1758
+ this.sendMessage(payload);
1759
+ }
1760
+ concatPcmChunks(chunks) {
1761
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
1762
+ const combined = new Uint8Array(totalLength);
1763
+ let offset = 0;
1764
+ chunks.forEach((chunk) => {
1765
+ combined.set(chunk, offset);
1766
+ offset += chunk.length;
1767
+ });
1768
+ return combined;
1769
+ }
1770
+ async resumeTranscriptionAfterVoiceCommand(shouldBePaused) {
1771
+ this.audioBuffer = [];
1772
+ const transcriptionId = generateMessageId();
1773
+ try {
1774
+ await this.audioManager.init({
1775
+ deviceId: this.snapshot.microphones.selected?.deviceId,
1776
+ targetSampleRate: this.options.targetSampleRate,
1777
+ onRecorderData: (pcm16) => this.handleRecorderData(pcm16, "transcription")
1778
+ });
1779
+ try {
1780
+ await this.wakeLockManager.request();
1781
+ } catch (error) {
1782
+ this.logger.warn("[Autoscriber] Failed to acquire wake lock", error);
1783
+ }
1784
+ this.snapshot.recording = {
1785
+ mode: "transcription",
1786
+ transcriptionId,
1787
+ isRecording: true,
1788
+ isPaused: shouldBePaused,
1789
+ bufferedChunks: 0,
1790
+ wasTranscriptionActive: false,
1791
+ transcriptionPausedState: false
1792
+ };
1793
+ this.emitRecording();
1794
+ if (this.bufferTimer) {
1795
+ clearInterval(this.bufferTimer);
1796
+ }
1797
+ this.bufferTimer = window.setInterval(() => {
1798
+ if (this.snapshot.recording.mode === "transcription" && this.audioBuffer.length > 0 && !this.snapshot.recording.isPaused) {
1799
+ this.sendBufferedAudio();
1800
+ }
1801
+ }, 5e3);
1802
+ } catch (error) {
1803
+ this.logger.error("[Autoscriber] failed to resume transcription", error);
1804
+ this.resetRecordingState();
1805
+ }
1806
+ }
1807
+ sendVoiceCommandAudio() {
1808
+ if (this.voiceCommandBuffer.length === 0)
1809
+ return;
1810
+ const combined = this.concatPcmChunks(this.voiceCommandBuffer);
1811
+ this.voiceCommandBuffer = [];
1812
+ const silence = detectSilence(combined);
1813
+ if (silence.isSilence) {
1814
+ this.pushInteraction({
1815
+ id: generateMessageId(),
1816
+ label: translate(this.snapshot.language, "voiceCommandSilence"),
1817
+ status: "error",
1818
+ type: "voice"
1819
+ });
1820
+ return;
1821
+ }
1822
+ const messageId = generateMessageId();
1823
+ const message = this.messageStore.createMessage({
1824
+ id: messageId,
1825
+ request: "VOICE_COMMAND_ICON",
1826
+ type: "voice"
1827
+ });
1828
+ this.emitMessages();
1829
+ this.sendMessage({
1830
+ jsonrpc: "2.0",
1831
+ id: messageId,
1832
+ method: "voiceCommand",
1833
+ params: {
1834
+ mediaType: "audio/pcm",
1835
+ data: arrayBufferToBase64(combined.buffer)
1836
+ }
1837
+ });
1838
+ this.pendingRequests.set(messageId, { id: messageId, method: "voiceCommand" });
1839
+ }
1840
+ handleMessage(data) {
1841
+ let payload;
1842
+ try {
1843
+ payload = JSON.parse(data);
1844
+ } catch (error) {
1845
+ this.logger.error("[Autoscriber] failed to parse websocket payload", error);
1846
+ return;
1847
+ }
1848
+ if (payload.error) {
1849
+ this.handleErrorResponse(payload);
1850
+ return;
1851
+ }
1852
+ if (payload.result) {
1853
+ this.handleResultResponse(payload);
1854
+ return;
1855
+ }
1856
+ if (payload.method === "partialResult") {
1857
+ this.handlePartialResult(payload.params);
1858
+ return;
1859
+ }
1860
+ if (payload.method === "voiceTranscription") {
1861
+ this.handleVoiceTranscription(payload.params);
1862
+ }
1863
+ }
1864
+ handleErrorResponse(payload) {
1865
+ const id = payload.id;
1866
+ const message = this.messageStore.getMessageById(id);
1867
+ const errorMessage = payload.error?.message ?? "Unknown error";
1868
+ const errorCode = payload.error?.code;
1869
+ if (message) {
1870
+ this.messageStore.setError(id, `Error: ${errorMessage}`, {
1871
+ code: String(errorCode ?? ""),
1872
+ message: errorMessage
1873
+ });
1874
+ this.emitMessages();
1875
+ } else {
1876
+ this.logger.error("[Autoscriber] error for unknown message", payload);
1877
+ }
1878
+ const pending = this.pendingRequests.get(id);
1879
+ if (pending) {
1880
+ this.updateInteractionFromResult(id, { code: String(errorCode ?? ""), message: errorMessage });
1881
+ }
1882
+ this.bus.emit("error", { code: errorCode, message: errorMessage });
1883
+ const sessionId = getSessionIdFromToken(this.token);
1884
+ this.logStructured("error", "Error response received", {
1885
+ messageId: id,
1886
+ errorCode: String(errorCode ?? ""),
1887
+ errorMessage,
1888
+ method: pending?.method,
1889
+ sessionId: sessionId ?? "unknown"
1890
+ });
1891
+ }
1892
+ handleResultResponse(payload) {
1893
+ this.logStructured("info", "Result response received", {
1894
+ messageId: payload.id,
1895
+ result: payload.result
1896
+ });
1897
+ const id = payload.id;
1898
+ const result = payload.result;
1899
+ if (result?.mediaType === "audio/pcm" && result?.data) {
1900
+ const buffer = base64ToArray(result.data);
1901
+ this.audioManager.playbackPcm(buffer);
1902
+ return;
1903
+ }
1904
+ const message = this.messageStore.getMessageById(id);
1905
+ if (message) {
1906
+ const data = result?.data ?? result;
1907
+ if (Array.isArray(data) || typeof data === "string") {
1908
+ this.messageStore.setMessageResult(id, data);
1909
+ } else if (result?.mediaType === "application/fhir+json") {
1910
+ this.messageStore.setMessageResult(id, [
1911
+ {
1912
+ mediaType: "application/fhir+json",
1913
+ data
1914
+ }
1915
+ ]);
1916
+ this.bus.emit("fhir:data", data);
1917
+ const sessionId = getSessionIdFromToken(this.token);
1918
+ const resourceType = typeof data === "object" && data && "resourceType" in data ? String(data.resourceType) : "unknown";
1919
+ this.logStructured("info", "FHIR data received", {
1920
+ messageId: id,
1921
+ resourceType,
1922
+ sessionId: sessionId ?? "unknown"
1923
+ });
1924
+ } else {
1925
+ this.messageStore.setMessageResult(id, data);
1926
+ }
1927
+ this.emitMessages();
1928
+ }
1929
+ this.updateInteractionFromResult(id, result);
1930
+ }
1931
+ updateInteractionFromResult(id, result) {
1932
+ const pending = this.pendingRequests.get(id);
1933
+ if (!pending)
1934
+ return;
1935
+ const isSuccess = String(result?.code ?? "200") === "200";
1936
+ this.messageStore.updateInteractionStatus(id, isSuccess ? "success" : "error", {
1937
+ result
1938
+ });
1939
+ this.emitInteractions();
1940
+ this.pendingRequests.delete(id);
1941
+ }
1942
+ handlePartialResult(params) {
1943
+ const id = params.id;
1944
+ const message = this.messageStore.getMessageById(id);
1945
+ if (!message)
1946
+ return;
1947
+ if (params.mediaType === "application/json-patch+json") {
1948
+ this.messageStore.applyJsonPatch(id, params.data);
1949
+ } else if (params.mediaType === "application/fhir+json" && params.data?.resourceType === "Transcript") {
1950
+ this.messageStore.setMessageResult(id, [
1951
+ {
1952
+ mediaType: "application/fhir+json",
1953
+ data: params.data
1954
+ }
1955
+ ]);
1956
+ } else if (typeof params.data === "string") {
1957
+ this.messageStore.setMessagePartialResult(id, params.data);
1958
+ }
1959
+ this.emitMessages();
1960
+ }
1961
+ handleVoiceTranscription(params) {
1962
+ const id = params.id;
1963
+ const text = params.text;
1964
+ const message = this.messageStore.getMessageById(id);
1965
+ if (!message)
1966
+ return;
1967
+ message.request = text;
1968
+ this.emitMessages();
1969
+ this.bus.emit("voice:transcription", {
1970
+ transcriptionId: id,
1971
+ finalText: text
1972
+ });
1973
+ }
1974
+ resetRecordingState() {
1975
+ if (this.bufferTimer) {
1976
+ clearInterval(this.bufferTimer);
1977
+ this.bufferTimer = null;
1978
+ }
1979
+ this.snapshot.recording = {
1980
+ mode: "idle",
1981
+ transcriptionId: null,
1982
+ isRecording: false,
1983
+ isPaused: false,
1984
+ bufferedChunks: 0,
1985
+ wasTranscriptionActive: false,
1986
+ transcriptionPausedState: false
1987
+ };
1988
+ this.emitRecording();
1989
+ }
1990
+ };
1991
+
1992
+ // src/headless/create-assistant.ts
1993
+ function createAssistant(options) {
1994
+ const client = new AssistantClient(options);
1995
+ return {
1996
+ client,
1997
+ getSnapshot: () => client.getSnapshot(),
1998
+ subscribe: (event, listener) => client.on(event, listener)
1999
+ };
2000
+ }
2001
+ // Annotate the CommonJS export names for ESM import in node:
2002
+ 0 && (module.exports = {
2003
+ AssistantClient,
2004
+ createAssistant
2005
+ });