@alexkroman1/aai-ui 2.0.0 → 3.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 (46) hide show
  1. package/dist/_module-url-C_4gRVL0.js +15 -0
  2. package/dist/audio.d.ts +42 -6
  3. package/dist/audio.js +79 -69
  4. package/dist/{chat-view-DKFhxMAT.js → chat-view-C1oJxsWz.js} +4 -4
  5. package/dist/client-config.d.ts +5 -6
  6. package/dist/components/chat-view.js +1 -1
  7. package/dist/components/console-shell.d.ts +2 -2
  8. package/dist/components/controls.js +1 -1
  9. package/dist/components/url-chips.d.ts +0 -3
  10. package/dist/context.d.ts +1 -1
  11. package/dist/context.js +1 -4
  12. package/dist/{controls-4OJoekj6.js → controls-DV368uhb.js} +1 -4
  13. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  14. package/dist/default-client/assets/audio-BGHiiDY_.js +1 -0
  15. package/dist/default-client/assets/{capture-processor-DLHxAIfT.js → capture-processor-BLPsqnGl.js} +4 -4
  16. package/dist/default-client/assets/index-DXf7-M0Q.js +244 -0
  17. package/dist/default-client/assets/index-Dv-Q5VRL.css +2 -0
  18. package/dist/default-client/assets/{playback-processor-bMTdFp-8.js → playback-processor-B_T_1KQP.js} +28 -21
  19. package/dist/default-client/favicon.ico +0 -0
  20. package/dist/default-client/index.html +3 -6
  21. package/dist/define-client-Cyf1jqAl.js +150 -0
  22. package/dist/define-client.js +1 -1
  23. package/dist/index.d.ts +1 -3
  24. package/dist/index.js +7 -6
  25. package/dist/{session-core-BLhiQ18c.js → session-core-BM3WHeoY.js} +30 -128
  26. package/dist/session-core-messages.d.ts +0 -4
  27. package/dist/session-core-types.d.ts +1 -27
  28. package/dist/session-core.js +1 -1
  29. package/dist/types.d.ts +1 -1
  30. package/dist/types.js +2 -2
  31. package/dist/worklets/_module-url.d.ts +10 -0
  32. package/dist/worklets/capture-processor.d.ts +2 -2
  33. package/dist/worklets/capture-processor.js +5 -5
  34. package/dist/worklets/playback-processor.d.ts +3 -3
  35. package/dist/worklets/playback-processor.js +31 -24
  36. package/package.json +2 -2
  37. package/dist/components/workflow-view.d.ts +0 -13
  38. package/dist/default-client/assets/audio-Cgviqo9t.js +0 -1
  39. package/dist/default-client/assets/index-BgbIWnfG.css +0 -2
  40. package/dist/default-client/assets/index-DbKKR3UE.js +0 -62
  41. package/dist/default-client/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
  42. package/dist/default-client/assets/types-Bpg3ZIZK.js +0 -64
  43. package/dist/define-client-yoGybEYR.js +0 -710
  44. package/dist/session-core-upload.d.ts +0 -16
  45. package/dist/sync-mic.d.ts +0 -61
  46. package/dist/sync-session.d.ts +0 -49
@@ -0,0 +1,15 @@
1
+ //#region worklets/_module-url.ts
2
+ /**
3
+ * Blob-URL module for an inlined AudioWorklet processor source.
4
+ *
5
+ * A blob URL rather than a data URI because the agent page's CSP allows
6
+ * `script-src blob:` but not `data:` — a data-URI module fails `addModule`
7
+ * with the opaque "Unable to load a worklet's module". Every inline worklet
8
+ * in this package must load through this helper so that constraint lives in
9
+ * one place.
10
+ */
11
+ function workletModuleUrl(source) {
12
+ return URL.createObjectURL(new Blob([source], { type: "application/javascript" }));
13
+ }
14
+ //#endregion
15
+ export { workletModuleUrl as t };
package/dist/audio.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Decode an audio file (any container/codec the browser can decode) and
3
- * resample it to mono PCM16 at `targetRate` the format the server's STT
4
- * side expects on the wire. Returns the raw clip; any endpointing padding
5
- * is the caller's concern (the one-shot upload path needs none).
2
+ * Clamp-and-convert Float32 samples to PCM16. The one main-thread home of the
3
+ * asymmetric-rounding convention (negative × 0x8000, positive × 0x7fff)
4
+ * the capture worklet's `accumulate` embeds the same math, which cannot
5
+ * import it (worklet source is a string).
6
6
  *
7
- * @throws If the browser cannot decode the payload.
7
+ * @public
8
8
  */
9
- export declare function decodeAudioToPcm16(data: ArrayBuffer, targetRate: number): Promise<Int16Array>;
9
+ export declare function floatToPcm16(samples: Float32Array): Int16Array;
10
10
  /**
11
11
  * How much of one turn's playback was covered by concealment rather than
12
12
  * received audio — the playback worklet's underrun report, in the shape
@@ -78,6 +78,42 @@ export type VoiceIO = AsyncDisposable & {
78
78
  /** Release all audio resources (microphone, AudioContext, worklets). */
79
79
  close(): Promise<void>;
80
80
  };
81
+ /**
82
+ * Throw unless the browser honored a requested context sample rate. The
83
+ * requested rates are never advisory: captured audio is tagged with the
84
+ * requested rate on the wire, so a context running at some other rate ships
85
+ * audio that only sounds like speech to the wrong decoder. Every capture
86
+ * path must call this after context creation.
87
+ */
88
+ export declare function assertGranted(granted: number, requested: number, side: string): void;
89
+ /**
90
+ * Release a microphone that was (or later gets) granted while another init
91
+ * step failed; if `getUserMedia` itself rejected, this is a no-op. Without
92
+ * it, a mic granted after a failed init keeps the browser's recording
93
+ * indicator lit with no way to turn it off.
94
+ */
95
+ export declare function releaseStreamOnFailure(streamPromise: Promise<MediaStream>): void;
96
+ /** Handle to one capture worklet node (`worklets/capture-processor.ts`). */
97
+ export type CaptureNode = {
98
+ node: AudioWorkletNode;
99
+ /** Begin accumulating — the worklet gates capture on its start/stop protocol. */
100
+ start(): void;
101
+ /**
102
+ * Stop accumulating and wait (bounded by {@link CAPTURE_STOP_ACK_TIMEOUT_MS})
103
+ * for the 'stopped' ack that follows the worklet's final flush, so the tail
104
+ * of speech reaches `onChunk` before the node is torn down.
105
+ */
106
+ stop(): Promise<void>;
107
+ };
108
+ /**
109
+ * Wire one capture worklet node: node construction, the chunk/silent/stopped
110
+ * port protocol, and the stop→ack handshake — one spelling for both the
111
+ * WebSocket mic and the push-to-talk recorder. No `processorOptions`: the
112
+ * worklet reads the context rate from its global scope (callers assert the
113
+ * granted rate first) and owns its own batching default — re-spelling
114
+ * defaults caller-side is drift.
115
+ */
116
+ export declare function createCaptureNode(ctx: AudioContext, onChunk: (pcm16: ArrayBuffer) => void, onSilent?: () => void): CaptureNode;
81
117
  /**
82
118
  * Create a {@link VoiceIO} instance that captures microphone audio and
83
119
  * plays back TTS audio using the Web Audio API.
package/dist/audio.js CHANGED
@@ -1,50 +1,85 @@
1
- import { MIC_BUFFER_SECONDS, VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
1
+ import { CAPTURE_STOP_ACK_TIMEOUT_MS, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
2
2
  //#region audio.ts
3
- /** How often {@link VoiceIO.done} checks that the AudioContext is still rendering. */
4
- const DONE_POLL_INTERVAL_MS = 1e3;
5
3
  /**
6
- * Hard cap on waiting for playback to drain. The playback worklet buffers up
7
- * to 60s of audio, so the longest legitimate drain is just under that a
8
- * wait past this means the processor died without reporting 'stop'.
9
- */
10
- const DONE_MAX_WAIT_MS = 65e3;
11
- /**
12
- * Bounded wait for the capture worklet's 'stopped' ack during close(). The
13
- * ack follows the final flush, so waiting for it keeps the tail of speech
14
- * from being dropped; the timeout covers a dead worklet.
15
- */
16
- const CAPTURE_STOP_ACK_TIMEOUT_MS = 250;
17
- /**
18
- * Decode an audio file (any container/codec the browser can decode) and
19
- * resample it to mono PCM16 at `targetRate` — the format the server's STT
20
- * side expects on the wire. Returns the raw clip; any endpointing padding
21
- * is the caller's concern (the one-shot upload path needs none).
4
+ * Clamp-and-convert Float32 samples to PCM16. The one main-thread home of the
5
+ * asymmetric-rounding convention (negative × 0x8000, positive × 0x7fff)
6
+ * the capture worklet's `accumulate` embeds the same math, which cannot
7
+ * import it (worklet source is a string).
22
8
  *
23
- * @throws If the browser cannot decode the payload.
9
+ * @public
24
10
  */
25
- async function decodeAudioToPcm16(data, targetRate) {
26
- const decoded = await new OfflineAudioContext(1, 1, targetRate).decodeAudioData(data);
27
- const frames = Math.ceil(decoded.duration * targetRate);
28
- const offline = new OfflineAudioContext(1, frames, targetRate);
29
- const source = offline.createBufferSource();
30
- source.buffer = decoded;
31
- source.connect(offline.destination);
32
- source.start();
33
- const f32 = (await offline.startRendering()).getChannelData(0);
34
- const pcm = new Int16Array(f32.length);
11
+ function floatToPcm16(samples) {
12
+ const pcm = new Int16Array(samples.length);
35
13
  let i = 0;
36
- for (const sample of f32) {
14
+ for (const sample of samples) {
37
15
  const s = Math.max(-1, Math.min(1, sample));
38
16
  pcm[i++] = s < 0 ? s * 32768 : s * 32767;
39
17
  }
40
18
  return pcm;
41
19
  }
42
- /** Throw unless the browser honored a requested context sample rate. */
20
+ /**
21
+ * Throw unless the browser honored a requested context sample rate. The
22
+ * requested rates are never advisory: captured audio is tagged with the
23
+ * requested rate on the wire, so a context running at some other rate ships
24
+ * audio that only sounds like speech to the wrong decoder. Every capture
25
+ * path must call this after context creation.
26
+ */
43
27
  function assertGranted(granted, requested, side) {
44
28
  if (granted === requested) return;
45
29
  throw new Error(`Browser refused the ${side} sample rate: asked for ${requested} Hz, got ${granted} Hz`);
46
30
  }
47
31
  /**
32
+ * Release a microphone that was (or later gets) granted while another init
33
+ * step failed; if `getUserMedia` itself rejected, this is a no-op. Without
34
+ * it, a mic granted after a failed init keeps the browser's recording
35
+ * indicator lit with no way to turn it off.
36
+ */
37
+ function releaseStreamOnFailure(streamPromise) {
38
+ streamPromise.then((s) => {
39
+ for (const t of s.getTracks()) t.stop();
40
+ }).catch(() => {});
41
+ }
42
+ /**
43
+ * Wire one capture worklet node: node construction, the chunk/silent/stopped
44
+ * port protocol, and the stop→ack handshake — one spelling for both the
45
+ * WebSocket mic and the push-to-talk recorder. No `processorOptions`: the
46
+ * worklet reads the context rate from its global scope (callers assert the
47
+ * granted rate first) and owns its own batching default — re-spelling
48
+ * defaults caller-side is drift.
49
+ */
50
+ function createCaptureNode(ctx, onChunk, onSilent) {
51
+ const node = new AudioWorkletNode(ctx, "capture-processor", {
52
+ channelCount: 1,
53
+ channelCountMode: "explicit"
54
+ });
55
+ let onStopped = null;
56
+ node.port.onmessage = (e) => {
57
+ const d = e.data;
58
+ if (d.event === "chunk" && d.buffer) onChunk(d.buffer);
59
+ else if (d.event === "silent") onSilent?.();
60
+ else if (d.event === "stopped") {
61
+ onStopped?.();
62
+ onStopped = null;
63
+ }
64
+ };
65
+ return {
66
+ node,
67
+ start() {
68
+ node.port.postMessage({ event: "start" });
69
+ },
70
+ stop() {
71
+ return new Promise((resolve) => {
72
+ const cap = setTimeout(resolve, CAPTURE_STOP_ACK_TIMEOUT_MS);
73
+ onStopped = () => {
74
+ clearTimeout(cap);
75
+ resolve();
76
+ };
77
+ node.port.postMessage({ event: "stop" });
78
+ });
79
+ }
80
+ };
81
+ }
82
+ /**
48
83
  * Create a {@link VoiceIO} instance that captures microphone audio and
49
84
  * plays back TTS audio using the Web Audio API.
50
85
  *
@@ -92,43 +127,25 @@ async function createVoiceIO(opts) {
92
127
  assertGranted(capCtx.sampleRate, sttSampleRate, "capture");
93
128
  assertGranted(ctx.sampleRate, ttsSampleRate, "playback");
94
129
  } catch (err) {
95
- streamPromise.then((s) => {
96
- for (const t of s.getTracks()) t.stop();
97
- }).catch(() => {});
130
+ releaseStreamOnFailure(streamPromise);
98
131
  await closeContexts();
99
132
  throw err;
100
133
  }
101
134
  const mic = capCtx.createMediaStreamSource(stream);
102
- const capNode = new AudioWorkletNode(capCtx, "capture-processor", {
103
- channelCount: 1,
104
- channelCountMode: "explicit",
105
- processorOptions: {
106
- sampleRate: sttSampleRate,
107
- bufferSeconds: MIC_BUFFER_SECONDS
108
- }
109
- });
110
- mic.connect(capNode);
111
- capNode.onprocessorerror = () => {
135
+ const capture = createCaptureNode(capCtx, onMicData, onMicSilent);
136
+ mic.connect(capture.node);
137
+ capture.node.onprocessorerror = () => {
112
138
  const err = /* @__PURE__ */ new Error("Audio capture worklet crashed");
113
139
  console.error("[aai-ui]", err.message);
114
140
  onError?.(err);
115
141
  };
116
- capNode.port.postMessage({ event: "start" });
117
- let onCaptureStopped = null;
118
- capNode.port.onmessage = (e) => {
119
- if (e.data.event === "chunk") onMicData(e.data.buffer);
120
- else if (e.data.event === "silent") onMicSilent?.();
121
- else if (e.data.event === "stopped") {
122
- onCaptureStopped?.();
123
- onCaptureStopped = null;
124
- }
125
- };
142
+ capture.start();
126
143
  let playNode = null;
127
144
  let onPlaybackStop = null;
128
145
  const lifecycle = new AbortController();
129
146
  function ensurePlayNode() {
130
147
  if (playNode) return playNode;
131
- const node = new AudioWorkletNode(ctx, "playback-processor", { processorOptions: { sampleRate: ctx.sampleRate } });
148
+ const node = new AudioWorkletNode(ctx, "playback-processor");
132
149
  node.connect(ctx.destination);
133
150
  node.port.onmessage = (e) => {
134
151
  if (e.data.event === "stop") {
@@ -160,6 +177,7 @@ async function createVoiceIO(opts) {
160
177
  },
161
178
  done() {
162
179
  if (!playNode) return Promise.resolve();
180
+ playNode.port.postMessage({ event: "done" });
163
181
  if (ctx.state !== "running") return Promise.resolve();
164
182
  return new Promise((resolve) => {
165
183
  onPlaybackStop?.();
@@ -171,10 +189,9 @@ async function createVoiceIO(opts) {
171
189
  };
172
190
  const poll = setInterval(() => {
173
191
  if (ctx.state !== "running") settle();
174
- }, DONE_POLL_INTERVAL_MS);
175
- const cap = setTimeout(settle, DONE_MAX_WAIT_MS);
192
+ }, PLAYBACK_DONE_POLL_MS);
193
+ const cap = setTimeout(settle, PLAYBACK_DONE_MAX_WAIT_MS);
176
194
  onPlaybackStop = settle;
177
- playNode?.port.postMessage({ event: "done" });
178
195
  });
179
196
  },
180
197
  flush() {
@@ -186,16 +203,9 @@ async function createVoiceIO(opts) {
186
203
  async close() {
187
204
  if (lifecycle.signal.aborted) return;
188
205
  lifecycle.abort();
189
- await new Promise((resolve) => {
190
- const cap = setTimeout(resolve, CAPTURE_STOP_ACK_TIMEOUT_MS);
191
- onCaptureStopped = () => {
192
- clearTimeout(cap);
193
- resolve();
194
- };
195
- capNode.port.postMessage({ event: "stop" });
196
- });
206
+ await capture.stop();
197
207
  mic.disconnect();
198
- capNode.disconnect();
208
+ capture.node.disconnect();
199
209
  if (playNode) playNode.disconnect();
200
210
  for (const t of stream.getTracks()) t.stop();
201
211
  await closeContexts();
@@ -207,4 +217,4 @@ async function createVoiceIO(opts) {
207
217
  return io;
208
218
  }
209
219
  //#endregion
210
- export { createVoiceIO, decodeAudioToPcm16 };
220
+ export { assertGranted, createCaptureNode, createVoiceIO, floatToPcm16, releaseStreamOnFailure };
@@ -2,7 +2,7 @@ import { useSessionSelector, useTheme } from "./context.js";
2
2
  import { r as TEXT_FAINT, t as ERROR_COLOR } from "./_colors-DYX7XRTr.js";
3
3
  import { t as AaiLogo } from "./aai-logo-B8lDmsut.js";
4
4
  import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
5
- import { t as Controls } from "./controls-4OJoekj6.js";
5
+ import { t as Controls } from "./controls-DV368uhb.js";
6
6
  import { MessageList } from "./components/message-list.js";
7
7
  import clsx from "clsx";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -24,8 +24,8 @@ function stateColor(state, primary) {
24
24
  }
25
25
  }
26
26
  /**
27
- * The design-system "console" chrome shared by the chat shell and the
28
- * workflow run surface: a 760px column on the cream page with a header
27
+ * The design-system "console" chrome for the chat shell:
28
+ * a 760px column on the cream page with a header
29
29
  * (logo + live-status eyebrow), an optional error banner, the main content
30
30
  * on a raised white card, and a footer row beneath it.
31
31
  *
@@ -126,4 +126,4 @@ function ChatView({ icon, title, className }) {
126
126
  });
127
127
  }
128
128
  //#endregion
129
- export { ConsoleShell as n, ChatView as t };
129
+ export { ChatView as t };
@@ -2,12 +2,11 @@
2
2
  * Pre-connection client-config lookup.
3
3
  *
4
4
  * `GET client-config` (relative to the agent's base URL — see
5
- * `sdk/client-config.ts` in `@alexkroman1/aai`) tells the default client
6
- * what kind of app the agent is before any connection exists — a
7
- * conversational agent (WebSocket chat shell) or a workflow (one-shot run
8
- * surface). Every failure path network error, 404 from an older server,
9
- * malformed body — degrades to the agent default, so this lookup can never
10
- * break an existing agent.
5
+ * `sdk/client-config.ts` in `@alexkroman1/aai`) gives the default client the
6
+ * agent's display name and greeting before any connection exists. Every
7
+ * failure path network error, 404 from an older server, malformed body —
8
+ * degrades to the empty default, so this lookup can never break an existing
9
+ * agent.
11
10
  */
12
11
  import { type ClientConfigResponse } from "@alexkroman1/aai/protocol";
13
12
  export type { ClientConfigResponse } from "@alexkroman1/aai/protocol";
@@ -1,3 +1,3 @@
1
1
  import "../context.js";
2
- import { t as ChatView } from "../chat-view-DKFhxMAT.js";
2
+ import { t as ChatView } from "../chat-view-C1oJxsWz.js";
3
3
  export { ChatView };
@@ -7,8 +7,8 @@ import type { AgentState } from "../types.ts";
7
7
  */
8
8
  export declare function stateColor(state: AgentState, primary: string): string;
9
9
  /**
10
- * The design-system "console" chrome shared by the chat shell and the
11
- * workflow run surface: a 760px column on the cream page with a header
10
+ * The design-system "console" chrome for the chat shell:
11
+ * a 760px column on the cream page with a header
12
12
  * (logo + live-status eyebrow), an optional error banner, the main content
13
13
  * on a raised white card, and a footer row beneath it.
14
14
  *
@@ -1,3 +1,3 @@
1
1
  import "../context.js";
2
- import { t as Controls } from "../controls-4OJoekj6.js";
2
+ import { t as Controls } from "../controls-DV368uhb.js";
3
3
  export { Controls };
@@ -4,9 +4,6 @@
4
4
  * The label is what makes a pair of these readable — on its own a bare URL
5
5
  * leaves you guessing whether it's the page or the socket.
6
6
  *
7
- * Exported for the sync-transport shell, which labels its HTTP endpoint
8
- * without a session snapshot to read from.
9
- *
10
7
  * @internal
11
8
  */
12
9
  export declare function UrlChip({ label, url, hint, testId, className, }: {
package/dist/context.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare function SessionProvider({ value, children }: {
7
7
  }): import("react").FunctionComponentElement<import("react").ProviderProps<SessionCore | null>>;
8
8
  /** The session snapshot merged with the core's control methods. Method
9
9
  * signatures come from {@link SessionCore} — one source of truth. */
10
- export type Session = SessionSnapshot & Pick<SessionCore, "start" | "cancel" | "resetState" | "reset" | "disconnect" | "toggle" | "startRecording" | "stopRecording" | "sendAudioFile">;
10
+ export type Session = SessionSnapshot & Pick<SessionCore, "start" | "cancel" | "resetState" | "reset" | "disconnect" | "toggle">;
11
11
  /**
12
12
  * Return the raw {@link SessionCore} from context without subscribing to
13
13
  * snapshot changes. Useful for accessing stable methods (`start`, `toggle`,
package/dist/context.js CHANGED
@@ -36,10 +36,7 @@ function useSession() {
36
36
  resetState: core.resetState,
37
37
  reset: core.reset,
38
38
  disconnect: core.disconnect,
39
- toggle: core.toggle,
40
- startRecording: core.startRecording,
41
- stopRecording: core.stopRecording,
42
- sendAudioFile: core.sendAudioFile
39
+ toggle: core.toggle
43
40
  }), [snapshot, core]);
44
41
  }
45
42
  /**
@@ -14,9 +14,6 @@ const COPIED_FEEDBACK_MS = 1500;
14
14
  * The label is what makes a pair of these readable — on its own a bare URL
15
15
  * leaves you guessing whether it's the page or the socket.
16
16
  *
17
- * Exported for the sync-transport shell, which labels its HTTP endpoint
18
- * without a session snapshot to read from.
19
- *
20
17
  * @internal
21
18
  */
22
19
  function UrlChip({ label, url, hint, testId, className }) {
@@ -140,4 +137,4 @@ const Controls = memo(function Controls({ className }) {
140
137
  });
141
138
  });
142
139
  //#endregion
143
- export { UrlChip as a, UiUrlChip as i, ApiUrlChip as n, SessionUrlChips as r, Controls as t };
140
+ export { UiUrlChip as i, ApiUrlChip as n, SessionUrlChips as r, Controls as t };
@@ -0,0 +1 @@
1
+ function e(e){return URL.createObjectURL(new Blob([e],{type:`application/javascript`}))}export{e as t};
@@ -0,0 +1 @@
1
+ import{a as e,o as t,t as n}from"./index-DXf7-M0Q.js";function r(e){let t=new Int16Array(e.length),n=0;for(let r of e){let e=Math.max(-1,Math.min(1,r));t[n++]=e<0?e*32768:e*32767}return t}function i(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function a(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function o(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){return new Promise(e=>{let t=setTimeout(e,250);i=()=>{clearTimeout(t),e()},r.port.postMessage({event:`stop`})})}}}async function s(r){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:d,onError:f,onPlaybackStats:p,onMicSilent:m}=r,h=new AudioContext({sampleRate:c,latencyHint:`playback`}),g=s===c,_=g?h:new AudioContext({sampleRate:s,latencyHint:`interactive`});async function v(){let e=g?[h]:[h,_];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let y=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),b;try{[b]=await Promise.all([y,h.resume(),_.resume(),_.audioWorklet.addModule(l),h.audioWorklet.addModule(u)]),i(_.sampleRate,s,`capture`),i(h.sampleRate,c,`playback`)}catch(e){throw a(y),await v(),e}let x=_.createMediaStreamSource(b),S=o(_,d,m);x.connect(S.node),S.node.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),f?.(e)},S.start();let C=null,w=null,T=new AbortController;function E(){if(C)return C;let e=new AudioWorkletNode(h,`playback-processor`);return e.connect(h.destination),e.port.onmessage=e=>{if(e.data.event===`stop`){let t=e.data.stats;if(t&&t.concealedSamples>0&&p?.(t),e.data.reason===`interrupt`)return;w?.(),w=null}},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),w?.(),w=null,f?.(e)},C=e,e}let D={enqueue(e){T.signal.aborted||e.byteLength!==0&&E().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!C||(C.port.postMessage({event:`done`}),h.state!==`running`)?Promise.resolve():new Promise(n=>{w?.();let r=()=>{clearInterval(i),clearTimeout(a),w===r&&(w=null),n()},i=setInterval(()=>{h.state!==`running`&&r()},t),a=setTimeout(r,e);w=r})},flush(){C&&(w?.(),w=null,C.port.postMessage({event:`interrupt`}))},async close(){if(!T.signal.aborted){T.abort(),await S.stop(),x.disconnect(),S.node.disconnect(),C&&C.disconnect();for(let e of b.getTracks())e.stop();await v()}},async[Symbol.asyncDispose](){await D.close()}};return D}export{i as assertGranted,o as createCaptureNode,s as createVoiceIO,r as floatToPcm16,a as releaseStreamOnFailure};
@@ -1,4 +1,4 @@
1
- import{p as e}from"./types-Bpg3ZIZK.js";var t=`
1
+ import{n as e,r as t}from"./index-DXf7-M0Q.js";import{t as n}from"./_module-url-BX0RuRU2.js";var r=n(`
2
2
  class CaptureProcessor extends AudioWorkletProcessor {
3
3
  constructor(options) {
4
4
  super();
@@ -10,14 +10,14 @@ class CaptureProcessor extends AudioWorkletProcessor {
10
10
  // Int16 accumulation buffer: flushed to the main thread as one transferred
11
11
  // ArrayBuffer once ~bufferSeconds of samples are batched. Sized 2x the
12
12
  // flush target so a whole render quantum always fits before flushing.
13
- this.targetSamples = Math.max(1, Math.round(this.rate * (opts.bufferSeconds || 0.1)));
13
+ this.targetSamples = Math.max(1, Math.round(this.rate * (opts.bufferSeconds || ${e})));
14
14
  this.pending = new Int16Array(this.targetSamples * 2);
15
15
  this.pendingLen = 0;
16
16
  // Dead-mic probe: samples left to inspect before concluding the device
17
17
  // delivers nothing but digital silence. Only consumed while recording, so
18
18
  // the cost disappears after the window (or after the first real sample).
19
19
  this.probeSamplesLeft = Math.round(
20
- (this.rate * (opts.silenceProbeMs ?? ${e})) / 1000,
20
+ (this.rate * (opts.silenceProbeMs ?? ${t})) / 1000,
21
21
  );
22
22
  this.port.onmessage = (e) => {
23
23
  if (e.data.event === 'start') this.recording = true;
@@ -87,4 +87,4 @@ class CaptureProcessor extends AudioWorkletProcessor {
87
87
  }
88
88
 
89
89
  registerProcessor('capture-processor', CaptureProcessor);
90
- `,n=new Blob([t],{type:`application/javascript`}),r=URL.createObjectURL(n);export{r as default};
90
+ `);export{r as default};