@lokutor/sdk 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -79,6 +79,8 @@ function isRetryableCode(code) {
79
79
  "auth.missing_key",
80
80
  "auth.invalid_key",
81
81
  "auth.time_limited",
82
+ "session.ended",
83
+ "audio.microphone_unavailable",
82
84
  "validation.invalid_voice",
83
85
  "validation.invalid_language",
84
86
  "validation.text_too_long",
@@ -233,6 +235,36 @@ var StreamResampler = class {
233
235
  };
234
236
 
235
237
  // src/browser-audio.ts
238
+ function requestMicrophone(constraints = {}) {
239
+ if (typeof navigator === "undefined") {
240
+ return Promise.reject(new Error("Microphone capture needs a browser"));
241
+ }
242
+ const session = navigator.audioSession;
243
+ if (session && session.type !== "auto" && session.type !== "play-and-record") {
244
+ try {
245
+ session.type = "auto";
246
+ } catch (_) {
247
+ }
248
+ }
249
+ if (!navigator.mediaDevices?.getUserMedia) {
250
+ return Promise.reject(new Error("Microphone capture is not available here (it needs HTTPS)"));
251
+ }
252
+ return navigator.mediaDevices.getUserMedia({
253
+ audio: {
254
+ autoGainControl: constraints.autoGainControl ?? true,
255
+ echoCancellation: constraints.echoCancellation ?? true,
256
+ noiseSuppression: constraints.noiseSuppression ?? true
257
+ }
258
+ });
259
+ }
260
+ async function resumeWithin(ctx, ms) {
261
+ if (ctx.state === "running") return;
262
+ await Promise.race([
263
+ ctx.resume().catch(() => {
264
+ }),
265
+ new Promise((r) => setTimeout(r, ms))
266
+ ]);
267
+ }
236
268
  var BrowserAudioManager = class {
237
269
  audioContext = null;
238
270
  mediaStreamAudioSourceNode = null;
@@ -254,6 +286,12 @@ var BrowserAudioManager = class {
254
286
  autoGainControl;
255
287
  echoCancellation;
256
288
  noiseSuppression;
289
+ keepAwake;
290
+ providedMicrophone;
291
+ // Screen wake lock, held while the microphone is live. Browsers release it whenever the page is
292
+ // hidden, so it is taken again when the page becomes visible.
293
+ wakeLock = null;
294
+ visibilityHandler = null;
257
295
  // Callbacks
258
296
  onAudioInput;
259
297
  onInputError;
@@ -266,6 +304,10 @@ var BrowserAudioManager = class {
266
304
  this.autoGainControl = config.autoGainControl ?? true;
267
305
  this.echoCancellation = config.echoCancellation ?? true;
268
306
  this.noiseSuppression = config.noiseSuppression ?? true;
307
+ this.keepAwake = config.keepAwake ?? true;
308
+ this.providedMicrophone = config.microphone ?? null;
309
+ if (this.providedMicrophone instanceof Promise) this.providedMicrophone.catch(() => {
310
+ });
269
311
  this.onInputError = config.onInputError;
270
312
  }
271
313
  /**
@@ -281,9 +323,8 @@ var BrowserAudioManager = class {
281
323
  if (!this.audioContext) {
282
324
  throw new Error("Failed to initialize AudioContext");
283
325
  }
284
- if (this.audioContext.state === "suspended") {
285
- await this.audioContext.resume();
286
- console.log("\u{1F442} AudioContext resumed");
326
+ if (this.audioContext.state !== "running") {
327
+ await resumeWithin(this.audioContext, 500);
287
328
  }
288
329
  if (analyserConfig?.enabled !== false) {
289
330
  this.analyserNode = this.audioContext.createAnalyser();
@@ -297,10 +338,17 @@ var BrowserAudioManager = class {
297
338
  if (!this.audioContext) {
298
339
  await this.init();
299
340
  }
341
+ if (this.isListening && this.mediaStream) {
342
+ this.onAudioInput = onAudioInput;
343
+ return;
344
+ }
300
345
  try {
301
346
  this.onAudioInput = onAudioInput;
302
347
  this.isListening = true;
303
- this.mediaStream = await navigator.mediaDevices.getUserMedia({
348
+ const provided = this.providedMicrophone;
349
+ this.providedMicrophone = null;
350
+ const stream = provided ? await provided : null;
351
+ this.mediaStream = stream && stream.getAudioTracks().some((t) => t.readyState === "live") ? stream : await navigator.mediaDevices.getUserMedia({
304
352
  audio: {
305
353
  autoGainControl: this.autoGainControl,
306
354
  echoCancellation: this.echoCancellation,
@@ -331,7 +379,9 @@ var BrowserAudioManager = class {
331
379
  this._processAudioInput(event);
332
380
  };
333
381
  console.log("\u{1F3A4} Microphone started");
382
+ this.holdScreenAwake();
334
383
  } catch (error) {
384
+ this.isListening = false;
335
385
  const err = error instanceof Error ? error : new Error(String(error));
336
386
  if (this.onInputError) this.onInputError(err);
337
387
  throw err;
@@ -369,6 +419,7 @@ var BrowserAudioManager = class {
369
419
  */
370
420
  stopMicrophone() {
371
421
  this.isListening = false;
422
+ this.releaseScreenAwake();
372
423
  if (this.mediaStream) {
373
424
  this.mediaStream.getTracks().forEach((track) => track.stop());
374
425
  this.mediaStream = null;
@@ -383,6 +434,53 @@ var BrowserAudioManager = class {
383
434
  }
384
435
  console.log("\u{1F3A4} Microphone stopped");
385
436
  }
437
+ /**
438
+ * Keep the screen on while the microphone is live, and bring audio back when the page returns
439
+ * to the foreground (iOS leaves the context "interrupted" after the screen locks).
440
+ */
441
+ holdScreenAwake() {
442
+ if (typeof document === "undefined") return;
443
+ if (!this.visibilityHandler) {
444
+ this.visibilityHandler = () => {
445
+ if (document.visibilityState !== "visible" || !this.isListening) return;
446
+ if (this.audioContext && this.audioContext.state !== "running") {
447
+ this.audioContext.resume().catch(() => {
448
+ });
449
+ }
450
+ void this.requestWakeLock();
451
+ };
452
+ document.addEventListener("visibilitychange", this.visibilityHandler);
453
+ }
454
+ void this.requestWakeLock();
455
+ }
456
+ async requestWakeLock() {
457
+ if (!this.keepAwake || this.wakeLock || typeof navigator === "undefined") return;
458
+ const api = navigator.wakeLock;
459
+ if (!api?.request || document.visibilityState !== "visible") return;
460
+ try {
461
+ const lock = await api.request("screen");
462
+ if (!this.isListening) {
463
+ await lock.release().catch(() => {
464
+ });
465
+ return;
466
+ }
467
+ this.wakeLock = lock;
468
+ lock.addEventListener?.("release", () => {
469
+ if (this.wakeLock === lock) this.wakeLock = null;
470
+ });
471
+ } catch (_) {
472
+ }
473
+ }
474
+ releaseScreenAwake() {
475
+ if (this.visibilityHandler && typeof document !== "undefined") {
476
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
477
+ this.visibilityHandler = null;
478
+ }
479
+ const lock = this.wakeLock;
480
+ this.wakeLock = null;
481
+ lock?.release().catch(() => {
482
+ });
483
+ }
386
484
  /**
387
485
  * Play back audio received from the server
388
486
  * @param pcm16Data Int16 PCM audio data at SPEAKER_SAMPLE_RATE
@@ -518,6 +616,13 @@ var BrowserAudioManager = class {
518
616
  this.analyserNode.disconnect();
519
617
  this.analyserNode = null;
520
618
  }
619
+ if (this.audioContext) {
620
+ const ctx = this.audioContext;
621
+ this.audioContext = null;
622
+ this.nextPlaybackTime = 0;
623
+ ctx.close().catch(() => {
624
+ });
625
+ }
521
626
  }
522
627
  /**
523
628
  * Get current audio context state
@@ -582,6 +687,7 @@ async function fetchJson(url, timeoutMs = 1e4) {
582
687
  });
583
688
  }
584
689
  }
690
+ var RECONNECTABLE_CLOSE_CODES = /* @__PURE__ */ new Set([1001, 1006, 1011, 1012, 1013, 1014]);
585
691
  function base64ToUint8Array(base64) {
586
692
  const binaryString = atob(base64);
587
693
  const bytes = new Uint8Array(binaryString.length);
@@ -661,6 +767,9 @@ var VoiceAgentClient = class {
661
767
  reconnectAttempts = 0;
662
768
  maxReconnectAttempts = 5;
663
769
  reconnectTimer = null;
770
+ // The microphone is started once per session of this client, not once per socket: a reconnect
771
+ // reuses it. Starting it again on every open ran two capture pipelines into one resampler.
772
+ micStarted = false;
664
773
  // Runtime state
665
774
  serverPlaybackRate = 44100;
666
775
  serverInputRate = 16e3;
@@ -731,15 +840,35 @@ var VoiceAgentClient = class {
731
840
  this.isConnected = true;
732
841
  this.reconnectAttempts = 0;
733
842
  this.reconnecting = false;
843
+ this.currentGeneration = 0;
734
844
  console.log("\u2705 Connected to voice agent!");
735
845
  sdkTrace("ws.open");
736
846
  this.sendConfig();
737
- if (this.audioManager) {
738
- await this.audioManager.startMicrophone((data) => {
739
- if (this.isConnected) {
740
- this.sendAudio(data);
847
+ if (this.audioManager && !this.micStarted) {
848
+ try {
849
+ await this.audioManager.startMicrophone((data) => {
850
+ if (this.isConnected) {
851
+ this.sendAudio(data);
852
+ }
853
+ });
854
+ } catch (e) {
855
+ const name = e?.name || "Error";
856
+ const error = new LokutorError(
857
+ "audio.microphone_unavailable",
858
+ name === "NotAllowedError" || name === "SecurityError" ? "Microphone access was not allowed." : "The microphone could not be started.",
859
+ { detail: `${name}: ${e?.message ?? e}`, original: e, retryable: false }
860
+ );
861
+ sdkTrace("mic.failed", { name });
862
+ settle(() => reject(error));
863
+ this.isUserDisconnect = true;
864
+ try {
865
+ this.ws?.close();
866
+ } catch (_) {
741
867
  }
742
- });
868
+ this.emit("error", error);
869
+ return;
870
+ }
871
+ this.micStarted = true;
743
872
  }
744
873
  settle(() => resolve(true));
745
874
  };
@@ -790,6 +919,17 @@ var VoiceAgentClient = class {
790
919
  console.error(" URL:", this.serverUrl.replace(/api_key=[^&]+/, "api_key=***"));
791
920
  }
792
921
  console.log(`\u{1F50C} WebSocket closed \u2014 code: ${event.code}, reason: "${event.reason || "none"}", clean: ${event.wasClean}`);
922
+ if (!this.isUserDisconnect && !RECONNECTABLE_CLOSE_CODES.has(event.code)) {
923
+ const ended = new LokutorError("session.ended", event.reason || "The session was ended by the server.", {
924
+ detail: `close code ${event.code}`,
925
+ retryable: false
926
+ });
927
+ console.log(`Session ended by the server (code ${event.code})`);
928
+ this.emit("ended", { code: event.code, reason: event.reason || "" });
929
+ if (this.onError) this.onError(ended);
930
+ if (this.onStatus) this.onStatus("disconnected");
931
+ return;
932
+ }
793
933
  if (!this.isUserDisconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
794
934
  this.reconnecting = true;
795
935
  this.reconnectAttempts++;
@@ -979,11 +1119,18 @@ var VoiceAgentClient = class {
979
1119
  detail: backendDetail,
980
1120
  retryable: backendRetryable
981
1121
  });
982
- if (this.onError) this.onError(error);
983
1122
  this.emit("error", error);
984
1123
  console.error(`\u274C Server error: [${error.code}] ${error.message}`);
985
1124
  break;
986
1125
  }
1126
+ case "session_limit": {
1127
+ const seconds = Number(msg.data?.seconds ?? msg.seconds);
1128
+ if (Number.isFinite(seconds) && seconds >= 0) {
1129
+ const limit = { seconds, reason: msg.data?.reason ?? msg.reason };
1130
+ this.emit("session_limit", limit);
1131
+ }
1132
+ break;
1133
+ }
987
1134
  case "tool_call":
988
1135
  this.emit("tool_call", { name: msg.name, arguments: msg.arguments });
989
1136
  console.log(`\u{1F6E0}\uFE0F Tool Call: ${msg.name}(${msg.arguments})`);
@@ -1057,6 +1204,7 @@ var VoiceAgentClient = class {
1057
1204
  if (this.audioManager) {
1058
1205
  this.audioManager.cleanup();
1059
1206
  }
1207
+ this.micStarted = false;
1060
1208
  this.isConnected = false;
1061
1209
  this.configConfirmed = false;
1062
1210
  this.reconnecting = false;
@@ -1706,6 +1854,22 @@ var PANEL_CSS = (
1706
1854
  margin-bottom: auto;
1707
1855
  padding-top: 1.5rem;
1708
1856
  }
1857
+ .cv-countdown, .cv-note {
1858
+ display: none;
1859
+ margin-top: 0.4rem;
1860
+ padding: 0.15rem 0.6rem;
1861
+ border-radius: 999px;
1862
+ border: 1px solid var(--cv-ui-border, rgba(255,255,255,0.1));
1863
+ background: var(--cv-ui-bg, rgba(255,255,255,0.05));
1864
+ color: var(--cv-text-dim, rgba(255,255,255,0.6));
1865
+ font-size: 0.75rem;
1866
+ font-variant-numeric: tabular-nums;
1867
+ width: fit-content;
1868
+ margin-left: auto;
1869
+ margin-right: auto;
1870
+ }
1871
+ .cv-countdown.is-visible, .cv-note.is-visible { display: block; }
1872
+ .cv-countdown.is-low { color: var(--cv-accent); border-color: var(--cv-accent); }
1709
1873
  .cv-title {
1710
1874
  font-size: clamp(1rem, 2.5vw, 1.75rem);
1711
1875
  font-weight: 700;
@@ -1916,6 +2080,17 @@ function injectStyles() {
1916
2080
  document.head.appendChild(style);
1917
2081
  styleInjected = true;
1918
2082
  }
2083
+ function microphoneMessage(err) {
2084
+ const name = String(err?.original?.name ?? err?.name ?? "");
2085
+ const detail = String(err?.detail ?? err?.message ?? "");
2086
+ if (/NotAllowedError|SecurityError/.test(name + " " + detail)) {
2087
+ return "Microphone access is blocked. Allow it for this site in your browser settings, then try again.";
2088
+ }
2089
+ if (/NotFoundError|OverconstrainedError/.test(name + " " + detail)) {
2090
+ return "No microphone was found on this device.";
2091
+ }
2092
+ return "The microphone could not start. Please try again.";
2093
+ }
1919
2094
  var ConversationalPanel = class {
1920
2095
  cfg;
1921
2096
  container;
@@ -1942,6 +2117,11 @@ var ConversationalPanel = class {
1942
2117
  muteSvg;
1943
2118
  stopBtn;
1944
2119
  visualizerWrap;
2120
+ countdownEl;
2121
+ noteEl;
2122
+ countdownTicker = null;
2123
+ microphone = null;
2124
+ micError = null;
1945
2125
  // Callbacks
1946
2126
  onTranscription;
1947
2127
  onResponse;
@@ -1949,6 +2129,12 @@ var ConversationalPanel = class {
1949
2129
  onStop;
1950
2130
  onError;
1951
2131
  onToolCall;
2132
+ /**
2133
+ * The call ended without the visitor pressing End: a time limit, the silence timeout, the server
2134
+ * closing the session, or the connection being lost. Receives the message the panel shows, so a
2135
+ * page that renders its own chrome (and hides this panel's curtain) can show it too.
2136
+ */
2137
+ onEnded;
1952
2138
  constructor(cfg) {
1953
2139
  this.cfg = cfg;
1954
2140
  this.container = cfg.container;
@@ -1989,6 +2175,8 @@ var ConversationalPanel = class {
1989
2175
  </div>
1990
2176
  <div class="cv-header">
1991
2177
  <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")}</h2>
2178
+ <div class="cv-countdown" aria-live="off"></div>
2179
+ <div class="cv-note" role="status"></div>
1992
2180
  </div>
1993
2181
  <div class="cv-visualizer-wrap">
1994
2182
  <canvas class="cv-canvas"></canvas>
@@ -2024,6 +2212,8 @@ var ConversationalPanel = class {
2024
2212
  this.muteSvg = this.muteBtn.querySelector("svg");
2025
2213
  this.stopBtn = this.el.querySelector(".cv-btn--end");
2026
2214
  this.visualizerWrap = this.el.querySelector(".cv-visualizer-wrap");
2215
+ this.countdownEl = this.el.querySelector(".cv-countdown");
2216
+ this.noteEl = this.el.querySelector(".cv-note");
2027
2217
  this.startBtn.addEventListener("click", () => this.start());
2028
2218
  this.stopBtn.addEventListener("click", () => this.stop());
2029
2219
  this.muteBtn.addEventListener("click", () => this.toggleMute());
@@ -2033,14 +2223,23 @@ var ConversationalPanel = class {
2033
2223
  d.textContent = s;
2034
2224
  return d.innerHTML;
2035
2225
  }
2036
- /** Start the conversation (called when user clicks "Start Conversation" or externally) */
2226
+ /**
2227
+ * Start the conversation (called when user clicks "Start Conversation" or externally). Call it from
2228
+ * inside the click or tap, before any await: it asks for the microphone there. Resolves true once the
2229
+ * call is live, false when it could not start (the reason is shown, and passed to onError).
2230
+ */
2037
2231
  async start() {
2038
- if (this.isRunning) return;
2232
+ if (this.isRunning) return false;
2039
2233
  if (this._locked) {
2040
2234
  this.showError("This session has ended. Refresh the page to start a new one.");
2041
- return;
2235
+ return false;
2042
2236
  }
2043
2237
  this.isRunning = true;
2238
+ this.microphone = requestMicrophone();
2239
+ this.micError = null;
2240
+ this.microphone.catch((e) => {
2241
+ this.micError = e;
2242
+ });
2044
2243
  this.startBtn.disabled = true;
2045
2244
  this.errorEl.classList.remove("is-visible");
2046
2245
  this.playConnectingSound();
@@ -2071,7 +2270,12 @@ var ConversationalPanel = class {
2071
2270
  this.el.classList.remove("cv-is-speaking", "cv-is-thinking");
2072
2271
  if (status === "speaking") this.el.classList.add("cv-is-speaking");
2073
2272
  else if (status === "thinking") this.el.classList.add("cv-is-thinking");
2273
+ this.setNote(status === "reconnecting" ? "Reconnecting\u2026" : "");
2274
+ if (status === "disconnected" && this.isRunning) {
2275
+ this.end("The call has ended.");
2276
+ }
2074
2277
  },
2278
+ onSessionLimit: (seconds) => this.startCountdown(seconds),
2075
2279
  onTranscription: (text) => {
2076
2280
  this._lastSpeechTime = Date.now();
2077
2281
  this.onTranscription?.(text);
@@ -2086,9 +2290,9 @@ var ConversationalPanel = class {
2086
2290
  onError: (err) => {
2087
2291
  if (!this.isRunning) return;
2088
2292
  this.onError?.(err);
2089
- this.stop();
2090
- this.showError("Connection issue. Try again in a moment.");
2091
- }
2293
+ this.end(err?.code === "audio.microphone_unavailable" ? microphoneMessage(err) : err?.code === "session.ended" && err?.message ? err.message : "Connection issue. Try again in a moment.");
2294
+ },
2295
+ microphone: this.microphone
2092
2296
  });
2093
2297
  this.agent = agentHandle;
2094
2298
  visualizer.setAudioClient(agentHandle);
@@ -2098,6 +2302,7 @@ var ConversationalPanel = class {
2098
2302
  } catch (_) {
2099
2303
  }
2100
2304
  const ok = await agentHandle.connect();
2305
+ if (!this.isRunning) return false;
2101
2306
  if (ok) {
2102
2307
  this.fadeOutConnectingSound();
2103
2308
  const wrap = this.visualizerWrap;
@@ -2111,19 +2316,21 @@ var ConversationalPanel = class {
2111
2316
  this.startTimer();
2112
2317
  this.onStart?.();
2113
2318
  if (window.lucide) window.lucide.createIcons();
2114
- } else {
2115
- this.fadeOutConnectingSound();
2116
- this.playErrorTone();
2117
- this.curtain.classList.remove("is-up");
2118
- this.showError("Could not connect. Please try again.");
2119
- this.isRunning = false;
2319
+ return true;
2120
2320
  }
2321
+ this.fadeOutConnectingSound();
2322
+ this.playErrorTone();
2323
+ const micError = this.micError;
2324
+ this.stop();
2325
+ this.showError(micError ? microphoneMessage(micError) : "Could not connect. Please try again.");
2326
+ return false;
2121
2327
  } catch (err) {
2122
2328
  this.fadeOutConnectingSound();
2123
2329
  this.playErrorTone();
2124
2330
  console.error("ConversationalPanel start error:", err);
2125
2331
  this.stop();
2126
2332
  this.showError("Something went wrong. Please try again.");
2333
+ return false;
2127
2334
  } finally {
2128
2335
  this.startBtn.disabled = false;
2129
2336
  }
@@ -2141,10 +2348,16 @@ var ConversationalPanel = class {
2141
2348
  }
2142
2349
  this.agent = null;
2143
2350
  }
2351
+ const mic = this.microphone;
2352
+ this.microphone = null;
2353
+ mic?.then((s) => s.getTracks().forEach((t) => t.stop()), () => {
2354
+ });
2144
2355
  if (this.timerTicker) {
2145
2356
  clearInterval(this.timerTicker);
2146
2357
  this.timerTicker = null;
2147
2358
  }
2359
+ this.stopCountdown();
2360
+ this.setNote("");
2148
2361
  if (this.visualizer) {
2149
2362
  this.visualizer.stop();
2150
2363
  this.visualizer = null;
@@ -2220,6 +2433,38 @@ var ConversationalPanel = class {
2220
2433
  this.errorEl.classList.remove("is-visible");
2221
2434
  }, 5e3);
2222
2435
  }
2436
+ /** End the call for a reason other than the End button, and say why. */
2437
+ end(message) {
2438
+ this.stop();
2439
+ this.showError(message);
2440
+ this.onEnded?.(message);
2441
+ }
2442
+ /** Show how long the session may still run, as announced by the server. */
2443
+ startCountdown(seconds) {
2444
+ this.stopCountdown();
2445
+ const endsAt = Date.now() + seconds * 1e3;
2446
+ const render = () => {
2447
+ const left = Math.max(0, Math.round((endsAt - Date.now()) / 1e3));
2448
+ this.countdownEl.textContent = `${Math.floor(left / 60)}:${String(left % 60).padStart(2, "0")} left`;
2449
+ this.countdownEl.classList.toggle("is-low", left <= 30);
2450
+ if (left === 0) this.stopCountdown(false);
2451
+ };
2452
+ this.countdownEl.classList.add("is-visible");
2453
+ render();
2454
+ this.countdownTicker = window.setInterval(render, 1e3);
2455
+ }
2456
+ stopCountdown(hide = true) {
2457
+ if (this.countdownTicker) {
2458
+ clearInterval(this.countdownTicker);
2459
+ this.countdownTicker = null;
2460
+ }
2461
+ if (hide) this.countdownEl?.classList.remove("is-visible", "is-low");
2462
+ }
2463
+ setNote(text) {
2464
+ if (!this.noteEl) return;
2465
+ this.noteEl.textContent = text;
2466
+ this.noteEl.classList.toggle("is-visible", Boolean(text));
2467
+ }
2223
2468
  /** Destroy the component, removing all DOM and stopping any active session */
2224
2469
  destroy() {
2225
2470
  this.stop();
@@ -2235,14 +2480,12 @@ var ConversationalPanel = class {
2235
2480
  elapsed++;
2236
2481
  if (elapsed >= maxDur) {
2237
2482
  this._locked = true;
2238
- this.stop();
2239
- this.showError("Tiempo l\xEDmite alcanzado.");
2483
+ this.end("Time limit reached.");
2240
2484
  return;
2241
2485
  }
2242
2486
  if (Date.now() - this._lastSpeechTime > silentMax * 1e3) {
2243
2487
  this._locked = true;
2244
- this.stop();
2245
- this.showError("Sesi\xF3n finalizada por inactividad.");
2488
+ this.end("The call ended after a long silence.");
2246
2489
  }
2247
2490
  }, 1e3);
2248
2491
  }
@@ -2332,9 +2575,11 @@ export {
2332
2575
  calculateRMS,
2333
2576
  float32ToPcm16,
2334
2577
  isRetryable,
2578
+ microphoneMessage,
2335
2579
  normalizeAudio,
2336
2580
  pcm16ToBytes,
2337
2581
  pcm16ToFloat32,
2582
+ requestMicrophone,
2338
2583
  resample,
2339
2584
  resampleWithAntiAliasing,
2340
2585
  simpleConversation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokutor/sdk",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "JavaScript/TypeScript SDK for Lokutor Real-time Voice AI",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",