@alexkroman1/aai-ui 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_module-url-C_4gRVL0.js +15 -0
- package/dist/audio.d.ts +42 -6
- package/dist/audio.js +79 -69
- package/dist/{chat-view-DKFhxMAT.js → chat-view-C1oJxsWz.js} +4 -4
- package/dist/client-config.d.ts +5 -6
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +2 -2
- package/dist/components/controls.js +1 -1
- package/dist/components/url-chips.d.ts +0 -3
- package/dist/context.d.ts +1 -1
- package/dist/context.js +1 -4
- package/dist/{controls-4OJoekj6.js → controls-DV368uhb.js} +1 -4
- package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
- package/dist/default-client/assets/audio-BGHiiDY_.js +1 -0
- package/dist/default-client/assets/{capture-processor-DLHxAIfT.js → capture-processor-BLPsqnGl.js} +4 -4
- package/dist/default-client/assets/index-DXf7-M0Q.js +244 -0
- package/dist/default-client/assets/index-Dv-Q5VRL.css +2 -0
- package/dist/default-client/assets/{playback-processor-bMTdFp-8.js → playback-processor-B_T_1KQP.js} +28 -21
- package/dist/default-client/index.html +2 -5
- package/dist/define-client-Cyf1jqAl.js +150 -0
- package/dist/define-client.js +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +7 -6
- package/dist/{session-core-BLhiQ18c.js → session-core-BM3WHeoY.js} +30 -128
- package/dist/session-core-messages.d.ts +0 -4
- package/dist/session-core-types.d.ts +1 -27
- package/dist/session-core.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/dist/worklets/_module-url.d.ts +10 -0
- package/dist/worklets/capture-processor.d.ts +2 -2
- package/dist/worklets/capture-processor.js +5 -5
- package/dist/worklets/playback-processor.d.ts +3 -3
- package/dist/worklets/playback-processor.js +31 -24
- package/package.json +2 -2
- package/dist/components/workflow-view.d.ts +0 -13
- package/dist/default-client/assets/audio-Cgviqo9t.js +0 -1
- package/dist/default-client/assets/index-BgbIWnfG.css +0 -2
- package/dist/default-client/assets/index-DbKKR3UE.js +0 -62
- package/dist/default-client/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
- package/dist/default-client/assets/types-Bpg3ZIZK.js +0 -64
- package/dist/define-client-yoGybEYR.js +0 -710
- package/dist/session-core-upload.d.ts +0 -16
- package/dist/sync-mic.d.ts +0 -61
- 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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
* @
|
|
7
|
+
* @public
|
|
8
8
|
*/
|
|
9
|
-
export declare function
|
|
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 {
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
* @
|
|
9
|
+
* @public
|
|
24
10
|
*/
|
|
25
|
-
|
|
26
|
-
const
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
},
|
|
175
|
-
const cap = setTimeout(settle,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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-
|
|
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
|
|
28
|
-
*
|
|
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 {
|
|
129
|
+
export { ChatView as t };
|
package/dist/client-config.d.ts
CHANGED
|
@@ -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`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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";
|
|
@@ -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
|
|
11
|
-
*
|
|
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
|
*
|
|
@@ -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"
|
|
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 {
|
|
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};
|
package/dist/default-client/assets/{capture-processor-DLHxAIfT.js → capture-processor-BLPsqnGl.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
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 ||
|
|
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 ?? ${
|
|
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
|
-
|
|
90
|
+
`);export{r as default};
|