@simfinity/constellation-react 0.0.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/README.md +241 -0
- package/dist/index.cjs +919 -0
- package/dist/index.d.cts +261 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.js +880 -0
- package/package.json +38 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AudioCaptureConfig: () => AudioCaptureConfig,
|
|
34
|
+
ConstellationProvider: () => ConstellationProvider,
|
|
35
|
+
useAudioCaptureState: () => useAudioCaptureState,
|
|
36
|
+
useAudioPlaybackState: () => useAudioPlaybackState,
|
|
37
|
+
useConstellationSession: () => useConstellationSession
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/constellationProvider.tsx
|
|
42
|
+
var import_react3 = __toESM(require("react"), 1);
|
|
43
|
+
var import_constellation_client = require("@simfinity/constellation-client");
|
|
44
|
+
|
|
45
|
+
// src/useAudioPlayback.tsx
|
|
46
|
+
var import_react = require("react");
|
|
47
|
+
|
|
48
|
+
// src/lib/pcm16.ts
|
|
49
|
+
function float32ToPcm16Base64(float32) {
|
|
50
|
+
const pcm16 = new Int16Array(float32.length);
|
|
51
|
+
for (let i = 0; i < float32.length; i++) {
|
|
52
|
+
const s = Math.max(-1, Math.min(1, float32[i]));
|
|
53
|
+
pcm16[i] = s < 0 ? s * 32768 : s * 32767;
|
|
54
|
+
}
|
|
55
|
+
const bytes = new Uint8Array(pcm16.buffer);
|
|
56
|
+
let binary = "";
|
|
57
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
58
|
+
binary += String.fromCharCode(bytes[i]);
|
|
59
|
+
}
|
|
60
|
+
return btoa(binary);
|
|
61
|
+
}
|
|
62
|
+
function pcm16Base64ToFloat32(base64) {
|
|
63
|
+
const binary = atob(base64);
|
|
64
|
+
const bytes = new Uint8Array(binary.length);
|
|
65
|
+
for (let i = 0; i < binary.length; i++) {
|
|
66
|
+
bytes[i] = binary.charCodeAt(i);
|
|
67
|
+
}
|
|
68
|
+
const pcm16 = new Int16Array(bytes.buffer);
|
|
69
|
+
const float32 = new Float32Array(pcm16.length);
|
|
70
|
+
for (let i = 0; i < pcm16.length; i++) {
|
|
71
|
+
float32[i] = pcm16[i] / (pcm16[i] < 0 ? 32768 : 32767);
|
|
72
|
+
}
|
|
73
|
+
return float32;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/lib/stateStore.ts
|
|
77
|
+
var StateStore = class {
|
|
78
|
+
constructor(state) {
|
|
79
|
+
this.state = state;
|
|
80
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
81
|
+
this.subscribe = (listener) => {
|
|
82
|
+
this.listeners.add(listener);
|
|
83
|
+
return () => this.listeners.delete(listener);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
update(patch) {
|
|
87
|
+
this.state = { ...this.state, ...patch };
|
|
88
|
+
this.listeners.forEach((l) => l());
|
|
89
|
+
}
|
|
90
|
+
get snapshot() {
|
|
91
|
+
return this.state;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/useAudioPlayback.tsx
|
|
96
|
+
var PLAYBACK_SAMPLE_RATE = 24e3;
|
|
97
|
+
var PREBUFFER_SAMPLES = Math.ceil(PLAYBACK_SAMPLE_RATE * 0.2);
|
|
98
|
+
var PREBUFFER_FLUSH_TIMEOUT_MS = 400;
|
|
99
|
+
function useAudioPlayback() {
|
|
100
|
+
const stateRef = (0, import_react.useRef)(new StateStore({ playing: false }));
|
|
101
|
+
const contextRef = (0, import_react.useRef)(null);
|
|
102
|
+
const queueRef = (0, import_react.useRef)([]);
|
|
103
|
+
const sourcesRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
104
|
+
const nextStartTimeRef = (0, import_react.useRef)(0);
|
|
105
|
+
const playbackStartTimeRef = (0, import_react.useRef)(null);
|
|
106
|
+
const prebufferRef = (0, import_react.useRef)([]);
|
|
107
|
+
const prebufferedSamplesRef = (0, import_react.useRef)(0);
|
|
108
|
+
const isPreBufferingRef = (0, import_react.useRef)(false);
|
|
109
|
+
const prebufferFlushTimerRef = (0, import_react.useRef)(null);
|
|
110
|
+
const getContext = (0, import_react.useCallback)(() => {
|
|
111
|
+
if (!contextRef.current || contextRef.current.state === "closed") {
|
|
112
|
+
contextRef.current = new AudioContext({ sampleRate: PLAYBACK_SAMPLE_RATE });
|
|
113
|
+
}
|
|
114
|
+
return contextRef.current;
|
|
115
|
+
}, []);
|
|
116
|
+
const scheduleChunk = (0, import_react.useCallback)((ctx, samples) => {
|
|
117
|
+
const buffer = ctx.createBuffer(1, samples.length, PLAYBACK_SAMPLE_RATE);
|
|
118
|
+
buffer.getChannelData(0).set(samples);
|
|
119
|
+
const source = ctx.createBufferSource();
|
|
120
|
+
source.buffer = buffer;
|
|
121
|
+
source.connect(ctx.destination);
|
|
122
|
+
const startTime = Math.max(ctx.currentTime, nextStartTimeRef.current);
|
|
123
|
+
source.start(startTime);
|
|
124
|
+
sourcesRef.current.add(source);
|
|
125
|
+
nextStartTimeRef.current = startTime + buffer.duration;
|
|
126
|
+
source.onended = () => {
|
|
127
|
+
sourcesRef.current.delete(source);
|
|
128
|
+
if (ctx.currentTime >= nextStartTimeRef.current - 0.01) {
|
|
129
|
+
playbackStartTimeRef.current = null;
|
|
130
|
+
stateRef.current.update({ playing: false });
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
const flushPrebuffer = (0, import_react.useCallback)((ctx) => {
|
|
135
|
+
if (prebufferFlushTimerRef.current !== null) {
|
|
136
|
+
clearTimeout(prebufferFlushTimerRef.current);
|
|
137
|
+
prebufferFlushTimerRef.current = null;
|
|
138
|
+
}
|
|
139
|
+
isPreBufferingRef.current = false;
|
|
140
|
+
nextStartTimeRef.current = ctx.currentTime;
|
|
141
|
+
for (const bufferedSamples of prebufferRef.current) {
|
|
142
|
+
scheduleChunk(ctx, bufferedSamples);
|
|
143
|
+
}
|
|
144
|
+
prebufferRef.current = [];
|
|
145
|
+
prebufferedSamplesRef.current = 0;
|
|
146
|
+
}, [scheduleChunk]);
|
|
147
|
+
const enqueue = (0, import_react.useCallback)((base64Chunk) => {
|
|
148
|
+
const samples = pcm16Base64ToFloat32(base64Chunk);
|
|
149
|
+
const ctx = getContext();
|
|
150
|
+
const isSilent = !isPreBufferingRef.current && sourcesRef.current.size === 0 && ctx.currentTime >= nextStartTimeRef.current - 0.01;
|
|
151
|
+
const flushPreBufferOrScheduleTimeout = () => {
|
|
152
|
+
if (prebufferedSamplesRef.current >= PREBUFFER_SAMPLES) {
|
|
153
|
+
flushPrebuffer(ctx);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (prebufferFlushTimerRef.current !== null) {
|
|
157
|
+
clearTimeout(prebufferFlushTimerRef.current);
|
|
158
|
+
}
|
|
159
|
+
prebufferFlushTimerRef.current = setTimeout(() => {
|
|
160
|
+
prebufferFlushTimerRef.current = null;
|
|
161
|
+
if (isPreBufferingRef.current && prebufferRef.current.length > 0) {
|
|
162
|
+
flushPrebuffer(getContext());
|
|
163
|
+
}
|
|
164
|
+
}, PREBUFFER_FLUSH_TIMEOUT_MS);
|
|
165
|
+
};
|
|
166
|
+
if (isSilent) {
|
|
167
|
+
isPreBufferingRef.current = true;
|
|
168
|
+
prebufferRef.current = [samples];
|
|
169
|
+
prebufferedSamplesRef.current = samples.length;
|
|
170
|
+
playbackStartTimeRef.current = performance.now();
|
|
171
|
+
stateRef.current.update({ playing: true });
|
|
172
|
+
flushPreBufferOrScheduleTimeout();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (isPreBufferingRef.current) {
|
|
176
|
+
prebufferRef.current.push(samples);
|
|
177
|
+
prebufferedSamplesRef.current += samples.length;
|
|
178
|
+
flushPreBufferOrScheduleTimeout();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
scheduleChunk(ctx, samples);
|
|
182
|
+
if (!playbackStartTimeRef.current) {
|
|
183
|
+
playbackStartTimeRef.current = performance.now();
|
|
184
|
+
stateRef.current.update({ playing: true });
|
|
185
|
+
}
|
|
186
|
+
}, [getContext, scheduleChunk, flushPrebuffer]);
|
|
187
|
+
const clearQueue = (0, import_react.useCallback)((minPlaybackDurationMs) => {
|
|
188
|
+
const ctx = contextRef.current;
|
|
189
|
+
const playbackDurationMs = getPlayingDurationMs();
|
|
190
|
+
if (minPlaybackDurationMs && playbackDurationMs <= minPlaybackDurationMs) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
sourcesRef.current.forEach((source) => {
|
|
194
|
+
try {
|
|
195
|
+
source.stop(0);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
sourcesRef.current.clear();
|
|
200
|
+
if (ctx) {
|
|
201
|
+
nextStartTimeRef.current = ctx.currentTime;
|
|
202
|
+
} else {
|
|
203
|
+
nextStartTimeRef.current = 0;
|
|
204
|
+
}
|
|
205
|
+
if (prebufferFlushTimerRef.current !== null) {
|
|
206
|
+
clearTimeout(prebufferFlushTimerRef.current);
|
|
207
|
+
prebufferFlushTimerRef.current = null;
|
|
208
|
+
}
|
|
209
|
+
isPreBufferingRef.current = false;
|
|
210
|
+
prebufferRef.current = [];
|
|
211
|
+
prebufferedSamplesRef.current = 0;
|
|
212
|
+
playbackStartTimeRef.current = null;
|
|
213
|
+
stateRef.current.update({ playing: false });
|
|
214
|
+
}, []);
|
|
215
|
+
const release = (0, import_react.useCallback)(async () => {
|
|
216
|
+
if (prebufferFlushTimerRef.current !== null) {
|
|
217
|
+
clearTimeout(prebufferFlushTimerRef.current);
|
|
218
|
+
prebufferFlushTimerRef.current = null;
|
|
219
|
+
}
|
|
220
|
+
if (contextRef.current && contextRef.current.state !== "closed") {
|
|
221
|
+
await contextRef.current.close();
|
|
222
|
+
contextRef.current = null;
|
|
223
|
+
}
|
|
224
|
+
queueRef.current = [];
|
|
225
|
+
isPreBufferingRef.current = false;
|
|
226
|
+
prebufferRef.current = [];
|
|
227
|
+
prebufferedSamplesRef.current = 0;
|
|
228
|
+
nextStartTimeRef.current = 0;
|
|
229
|
+
playbackStartTimeRef.current = null;
|
|
230
|
+
stateRef.current.update({ playing: false });
|
|
231
|
+
}, []);
|
|
232
|
+
const getPlayingDurationMs = (0, import_react.useCallback)(() => {
|
|
233
|
+
return isPreBufferingRef.current || !playbackStartTimeRef.current ? Infinity : performance.now() - playbackStartTimeRef.current;
|
|
234
|
+
}, []);
|
|
235
|
+
return {
|
|
236
|
+
sampleRate: PLAYBACK_SAMPLE_RATE,
|
|
237
|
+
preBufferDurationsMs: PREBUFFER_SAMPLES,
|
|
238
|
+
enqueue,
|
|
239
|
+
clearQueue,
|
|
240
|
+
release,
|
|
241
|
+
getPlayingDurationMs,
|
|
242
|
+
subscribeState: stateRef.current.subscribe,
|
|
243
|
+
getState: () => stateRef.current.snapshot
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/useAudioCapture.tsx
|
|
248
|
+
var import_react2 = require("react");
|
|
249
|
+
|
|
250
|
+
// src/lib/audioRingBuffer.ts
|
|
251
|
+
var VadState = {
|
|
252
|
+
Idle: 0,
|
|
253
|
+
PreSpeech: 1,
|
|
254
|
+
// noise candidate
|
|
255
|
+
Speaking: 2,
|
|
256
|
+
// confirmed speech
|
|
257
|
+
PostSpeech: 3
|
|
258
|
+
// silence after speech
|
|
259
|
+
};
|
|
260
|
+
var AudioRingBufferConstants = {
|
|
261
|
+
/**
|
|
262
|
+
* Number of speech-chunks before the early speech signal is fired.
|
|
263
|
+
*/
|
|
264
|
+
preSpeechEarlySignalChunksCount: 2,
|
|
265
|
+
/**
|
|
266
|
+
* Number of silence-chunks before the pre-speech state fails & return to idle.
|
|
267
|
+
*/
|
|
268
|
+
preSpeechSilenceChunksDebounceCount: 10,
|
|
269
|
+
/**
|
|
270
|
+
* Silence duration allowed before transitioning from Speaking to Post-Speech state.
|
|
271
|
+
* Expressed in % of total silence allowed before end-of-speech i.e. RingBufferConfig.silenceDurationMs.
|
|
272
|
+
*
|
|
273
|
+
* @remarks
|
|
274
|
+
* This does not delay end-of-speech, the amount of time waited before transitioning to post-speech is
|
|
275
|
+
* transferred to the post-speech state. Thus, a value of 0.5 here for a silence duration of 1s means that the
|
|
276
|
+
* post-speech silence only has to last 500ms, having already waited 500ms before leaving Speaking state.
|
|
277
|
+
*
|
|
278
|
+
* TODO: consider exposing this as setting
|
|
279
|
+
*/
|
|
280
|
+
speechSilenceThresholdPercent: 0.5,
|
|
281
|
+
/**
|
|
282
|
+
* RMS detection stabilizer:
|
|
283
|
+
* Very important with fine-grained chunks (AudioWorklet typically have ~8ms chunks):
|
|
284
|
+
* The subtleties of the human voice are reflected in the chunks, having many ups and down
|
|
285
|
+
* with very short silence or near silence gaps.
|
|
286
|
+
*
|
|
287
|
+
* @remarks
|
|
288
|
+
* Smaller is smoother with more lag, higher is more responsive but jumpy.
|
|
289
|
+
*/
|
|
290
|
+
rmsSmoothing: 0.3
|
|
291
|
+
};
|
|
292
|
+
var AudioRingBuffer = class {
|
|
293
|
+
constructor(config) {
|
|
294
|
+
this.consts = AudioRingBufferConstants;
|
|
295
|
+
this.chunkDuration = 8;
|
|
296
|
+
this.buffer = [];
|
|
297
|
+
this.smoothedRms = 0;
|
|
298
|
+
// -- State machine
|
|
299
|
+
this.state = VadState.Idle;
|
|
300
|
+
// Current stage
|
|
301
|
+
this.readIndex = 0;
|
|
302
|
+
// Streaming cursor: consumer read new data start here
|
|
303
|
+
// Stage index trackers
|
|
304
|
+
this.globalIndex = 0;
|
|
305
|
+
// Global/absolute chunk index counter, goes beyond buffer boundaries
|
|
306
|
+
this.globalBufferStartIndex = 0;
|
|
307
|
+
// The global index of this.buffer[0]
|
|
308
|
+
this.speechStartIndex = 0;
|
|
309
|
+
// Global index of when confirmed speech started
|
|
310
|
+
// Not used for now, but might become useful in future features
|
|
311
|
+
//private silenceStartIndex = 0; // Global index of when post-speech silence started
|
|
312
|
+
// -- State transition counters
|
|
313
|
+
// [Post-Speech Silence] count consequence silence chunks (in post-speech)
|
|
314
|
+
this.silenceChunkCount = 0;
|
|
315
|
+
// [Post-Speech Silence] For confirmed silence, add a short debounce period to go back to speaking
|
|
316
|
+
this.silenceDebounceStartIndex = 0;
|
|
317
|
+
// [Post-Speech Silence] In silence debounce, queue a pending commit
|
|
318
|
+
this.pendingCommit = false;
|
|
319
|
+
// [Pre-Speech] In Pre-Speech add short debounce to not reset on first silence
|
|
320
|
+
this.preSpeechSilenceChunks = 0;
|
|
321
|
+
// [Speaking] Count consecutive speech chunks
|
|
322
|
+
this.speechChunkCount = 0;
|
|
323
|
+
// [Speaking] Early signal speech-start, before confirmed speech
|
|
324
|
+
this.speechStartSignaled = false;
|
|
325
|
+
this.config = config;
|
|
326
|
+
this.maxChunks = Math.ceil(config.maxDurationMs / this.chunkDuration);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Wipes internal state and speech tracking, the ring buffer remains
|
|
330
|
+
* ready to be filled again, only starting from a blank page.s
|
|
331
|
+
*/
|
|
332
|
+
forceResetState() {
|
|
333
|
+
this.reset();
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Returns chunks that should be streamed NOW
|
|
337
|
+
*/
|
|
338
|
+
consume() {
|
|
339
|
+
if (this.state === VadState.Idle || this.state === VadState.PreSpeech) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
const out = [];
|
|
343
|
+
while (this.readIndex < this.globalIndex) {
|
|
344
|
+
const index = this.readIndex - this.globalBufferStartIndex;
|
|
345
|
+
if (index >= 0 && index < this.buffer.length) {
|
|
346
|
+
out.push(this.buffer[index].data);
|
|
347
|
+
}
|
|
348
|
+
this.readIndex++;
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Push a new chunk of data into the ring buffer
|
|
354
|
+
* @param chunk
|
|
355
|
+
*/
|
|
356
|
+
push(chunk) {
|
|
357
|
+
this.buffer.push(chunk);
|
|
358
|
+
this.globalIndex++;
|
|
359
|
+
if (this.buffer.length > this.maxChunks) {
|
|
360
|
+
this.buffer.shift();
|
|
361
|
+
this.globalBufferStartIndex++;
|
|
362
|
+
if (this.readIndex < this.globalBufferStartIndex) {
|
|
363
|
+
this.readIndex = this.globalBufferStartIndex;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.updateSmoothRms(chunk.rms);
|
|
367
|
+
this.updateState();
|
|
368
|
+
}
|
|
369
|
+
// --- Getters
|
|
370
|
+
get speaking() {
|
|
371
|
+
return this.state === VadState.Speaking;
|
|
372
|
+
}
|
|
373
|
+
get stateName() {
|
|
374
|
+
return this.state;
|
|
375
|
+
}
|
|
376
|
+
// --- Setters
|
|
377
|
+
set chunkDurationMs(duration) {
|
|
378
|
+
this.chunkDuration = duration;
|
|
379
|
+
this.maxChunks = Math.ceil(this.config.maxDurationMs / this.chunkDuration);
|
|
380
|
+
}
|
|
381
|
+
// --- Private API ---
|
|
382
|
+
updateState() {
|
|
383
|
+
var _a, _b, _c;
|
|
384
|
+
const log = (message) => {
|
|
385
|
+
console.log(`${Date.now() % 1e5} - ${message}`);
|
|
386
|
+
};
|
|
387
|
+
const isSpeech = this.smoothedRms > this.config.silenceThreshold;
|
|
388
|
+
switch (this.state) {
|
|
389
|
+
case VadState.Idle:
|
|
390
|
+
if (isSpeech) {
|
|
391
|
+
log("[AudioBuffer] [Idle] Entering Pre-Speech state");
|
|
392
|
+
this.state = VadState.PreSpeech;
|
|
393
|
+
this.speechStartIndex = this.globalIndex;
|
|
394
|
+
this.readIndex = this.speechStartIndex;
|
|
395
|
+
this.speechChunkCount = 1;
|
|
396
|
+
this.speechStartSignaled = false;
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
case VadState.PreSpeech:
|
|
400
|
+
if (isSpeech) {
|
|
401
|
+
this.preSpeechSilenceChunks = 0;
|
|
402
|
+
this.speechChunkCount++;
|
|
403
|
+
if (!this.speechStartSignaled && this.speechChunkCount >= this.consts.preSpeechEarlySignalChunksCount) {
|
|
404
|
+
(_a = this.onSpeechStart) == null ? void 0 : _a.call(this, "early");
|
|
405
|
+
this.speechStartSignaled = true;
|
|
406
|
+
}
|
|
407
|
+
if (this.speechChunkCount * this.chunkDuration >= this.config.minSpeechDurationMs) {
|
|
408
|
+
log("[AudioBuffer] [Pre-Speech] Entering Speaking state");
|
|
409
|
+
this.state = VadState.Speaking;
|
|
410
|
+
(_b = this.onSpeechStart) == null ? void 0 : _b.call(this, "confirmed");
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
this.preSpeechSilenceChunks++;
|
|
414
|
+
if (this.preSpeechSilenceChunks > this.consts.preSpeechSilenceChunksDebounceCount) {
|
|
415
|
+
log("[AudioBuffer] [Pre-Speech] Debounce failed");
|
|
416
|
+
this.reset();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case VadState.Speaking:
|
|
421
|
+
if (isSpeech) {
|
|
422
|
+
this.silenceChunkCount = 0;
|
|
423
|
+
} else {
|
|
424
|
+
this.silenceChunkCount++;
|
|
425
|
+
const silenceDuration = this.silenceChunkCount * this.chunkDuration;
|
|
426
|
+
if (silenceDuration >= this.config.silenceDurationMs * this.consts.speechSilenceThresholdPercent) {
|
|
427
|
+
log("[AudioBuffer] [Speaking] Speaking silence threshold reached, entering Post-Speech state");
|
|
428
|
+
this.state = VadState.PostSpeech;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
case VadState.PostSpeech:
|
|
433
|
+
if (isSpeech) {
|
|
434
|
+
log("[AudioBuffer] [Post-Speech] Speech detected, reverting to Speaking state");
|
|
435
|
+
this.state = VadState.Speaking;
|
|
436
|
+
this.silenceChunkCount = 0;
|
|
437
|
+
this.pendingCommit = false;
|
|
438
|
+
} else {
|
|
439
|
+
this.silenceChunkCount++;
|
|
440
|
+
const silenceDuration = this.silenceChunkCount * this.chunkDuration;
|
|
441
|
+
if (!this.pendingCommit) {
|
|
442
|
+
if (silenceDuration >= this.config.silenceDurationMs) {
|
|
443
|
+
log("[AudioBuffer] [Post-Speech] Silence debounce state");
|
|
444
|
+
this.pendingCommit = true;
|
|
445
|
+
this.silenceDebounceStartIndex = this.globalIndex;
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
const debounceDuration = this.durationSince(this.silenceDebounceStartIndex);
|
|
449
|
+
if (debounceDuration >= this.config.silenceDebounceMs) {
|
|
450
|
+
log("[AudioBuffer] [Post-Speech] Silence debounce passed");
|
|
451
|
+
(_c = this.onCommit) == null ? void 0 : _c.call(this);
|
|
452
|
+
this.reset();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
durationSince(startIndex) {
|
|
460
|
+
return (this.globalIndex - startIndex) * this.chunkDuration;
|
|
461
|
+
}
|
|
462
|
+
updateSmoothRms(rms) {
|
|
463
|
+
this.smoothedRms = this.consts.rmsSmoothing * rms + (1 - this.consts.rmsSmoothing) * this.smoothedRms;
|
|
464
|
+
}
|
|
465
|
+
reset() {
|
|
466
|
+
this.state = VadState.Idle;
|
|
467
|
+
this.speechStartIndex = 0;
|
|
468
|
+
this.silenceChunkCount = 0;
|
|
469
|
+
this.pendingCommit = false;
|
|
470
|
+
this.silenceDebounceStartIndex = 0;
|
|
471
|
+
this.preSpeechSilenceChunks = 0;
|
|
472
|
+
this.readIndex = this.globalIndex;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/lib/rms.ts
|
|
477
|
+
function computeRms(samples) {
|
|
478
|
+
let sum = 0;
|
|
479
|
+
for (let i = 0; i < samples.length; i++) {
|
|
480
|
+
sum += samples[i] * samples[i];
|
|
481
|
+
}
|
|
482
|
+
return Math.sqrt(sum / samples.length);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/useAudioCapture.tsx
|
|
486
|
+
var workletCode = `
|
|
487
|
+
class CaptureProcessor extends AudioWorkletProcessor {
|
|
488
|
+
process(inputs) {
|
|
489
|
+
const input = inputs[0];
|
|
490
|
+
if (input && input[0]) {
|
|
491
|
+
// Send Float32Array to main thread
|
|
492
|
+
this.port.postMessage(input[0]);
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
registerProcessor('capture-processor', CaptureProcessor);
|
|
499
|
+
`;
|
|
500
|
+
var SAMPLE_RATE = 24e3;
|
|
501
|
+
var SILENCE_THRESHOLD = 0.04;
|
|
502
|
+
var SILENCE_DURATION_MS = 1e3;
|
|
503
|
+
var MIN_SPEECH_DURATION_MS = 200;
|
|
504
|
+
var AudioCaptureConfig = class {
|
|
505
|
+
constructor() {
|
|
506
|
+
this.sampleRate = SAMPLE_RATE;
|
|
507
|
+
/**
|
|
508
|
+
* Minimum amount of energy for a noise to be considered as actual input and be recorded.
|
|
509
|
+
* - Below it, audio data is silently discarded
|
|
510
|
+
* - Above it, onAudioChunk events containing the audio stream will be fired
|
|
511
|
+
*
|
|
512
|
+
* @remarks
|
|
513
|
+
* This is the main parameter for noise-cancelling
|
|
514
|
+
*/
|
|
515
|
+
this.silenceThreshold = SILENCE_THRESHOLD;
|
|
516
|
+
/**
|
|
517
|
+
* Once an input was detected, this is the duration of silence required before
|
|
518
|
+
* detecting the end-of-speech and triggering the onSilenceCommit event.
|
|
519
|
+
*
|
|
520
|
+
* IMPORTANT:
|
|
521
|
+
* Internally, duration is computed on audio chunks count, not system clock.
|
|
522
|
+
* A chunk is usually 8ms, therefore to see the effect of a silence duration change, it must be
|
|
523
|
+
* such that the number of chunks count changes e.g. going from 800ms to 810ms increases
|
|
524
|
+
* the chunk count by one.
|
|
525
|
+
*
|
|
526
|
+
* @remarks
|
|
527
|
+
* This is the main voice-activation-detection parameter.
|
|
528
|
+
*/
|
|
529
|
+
this.silenceDurationMs = SILENCE_DURATION_MS;
|
|
530
|
+
/**
|
|
531
|
+
* Minimum input duration: below it, an input will not trigger an onSilenceCommit event.
|
|
532
|
+
*
|
|
533
|
+
* @remarks
|
|
534
|
+
* This is an important interruption parameter: it prevents very short inputs from
|
|
535
|
+
* interrupting an ongoing audio response.
|
|
536
|
+
*/
|
|
537
|
+
this.minSpeechDurationMs = MIN_SPEECH_DURATION_MS;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function useAudioCapture() {
|
|
541
|
+
const stateRef = (0, import_react2.useRef)(new StateStore({
|
|
542
|
+
recording: false,
|
|
543
|
+
speaking: false,
|
|
544
|
+
muted: false
|
|
545
|
+
}));
|
|
546
|
+
const configRef = (0, import_react2.useRef)(new AudioCaptureConfig());
|
|
547
|
+
const mediaStreamRef = (0, import_react2.useRef)(null);
|
|
548
|
+
const contextRef = (0, import_react2.useRef)(null);
|
|
549
|
+
const sourceRef = (0, import_react2.useRef)(null);
|
|
550
|
+
const workletUrlRef = (0, import_react2.useRef)(null);
|
|
551
|
+
const workletNodeRef = (0, import_react2.useRef)(null);
|
|
552
|
+
const audioCallbackRef = (0, import_react2.useRef)(null);
|
|
553
|
+
const commitCallbackRef = (0, import_react2.useRef)(null);
|
|
554
|
+
const speechEarlyRef = (0, import_react2.useRef)(null);
|
|
555
|
+
const speechConfirmedRef = (0, import_react2.useRef)(null);
|
|
556
|
+
const ringRef = (0, import_react2.useRef)(null);
|
|
557
|
+
const chunkDurationMsRef = (0, import_react2.useRef)(0);
|
|
558
|
+
const configure = (0, import_react2.useCallback)((settings) => {
|
|
559
|
+
configRef.current = settings;
|
|
560
|
+
}, []);
|
|
561
|
+
const start = (0, import_react2.useCallback)(async (params) => {
|
|
562
|
+
var _a, _b, _c;
|
|
563
|
+
await stop();
|
|
564
|
+
try {
|
|
565
|
+
audioCallbackRef.current = params.onAudioChunk;
|
|
566
|
+
commitCallbackRef.current = (_a = params.onSilenceCommit) != null ? _a : null;
|
|
567
|
+
speechEarlyRef.current = (_b = params.onSpeechConsidered) != null ? _b : null;
|
|
568
|
+
speechConfirmedRef.current = (_c = params.onSpeechConfirmed) != null ? _c : null;
|
|
569
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
570
|
+
audio: {
|
|
571
|
+
sampleRate: configRef.current.sampleRate,
|
|
572
|
+
channelCount: 1,
|
|
573
|
+
echoCancellation: true,
|
|
574
|
+
noiseSuppression: true
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
mediaStreamRef.current = stream;
|
|
578
|
+
const audioContext = new AudioContext({ sampleRate: configRef.current.sampleRate });
|
|
579
|
+
contextRef.current = audioContext;
|
|
580
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
581
|
+
sourceRef.current = source;
|
|
582
|
+
const ringDuration = 1.25 * configRef.current.minSpeechDurationMs + configRef.current.silenceDurationMs;
|
|
583
|
+
ringRef.current = new AudioRingBuffer({
|
|
584
|
+
maxDurationMs: ringDuration,
|
|
585
|
+
silenceThreshold: configRef.current.silenceThreshold,
|
|
586
|
+
minSpeechDurationMs: configRef.current.minSpeechDurationMs,
|
|
587
|
+
silenceDurationMs: configRef.current.silenceDurationMs,
|
|
588
|
+
silenceDebounceMs: 350
|
|
589
|
+
});
|
|
590
|
+
ringRef.current.onCommit = () => {
|
|
591
|
+
var _a2;
|
|
592
|
+
console.log("[Audio] Commit triggered");
|
|
593
|
+
stateRef.current.update({ speaking: false });
|
|
594
|
+
(_a2 = commitCallbackRef.current) == null ? void 0 : _a2.call(commitCallbackRef);
|
|
595
|
+
};
|
|
596
|
+
ringRef.current.onSpeechStart = (stage) => {
|
|
597
|
+
var _a2, _b2;
|
|
598
|
+
if (stage === "early") {
|
|
599
|
+
(_a2 = speechEarlyRef.current) == null ? void 0 : _a2.call(speechEarlyRef);
|
|
600
|
+
}
|
|
601
|
+
if (stage === "confirmed") {
|
|
602
|
+
(_b2 = speechConfirmedRef.current) == null ? void 0 : _b2.call(speechConfirmedRef);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
const onAudioChunkReceived = (inputData) => {
|
|
606
|
+
var _a2;
|
|
607
|
+
if (stateRef.current.snapshot.muted) {
|
|
608
|
+
if (stateRef.current.snapshot.speaking) {
|
|
609
|
+
stateRef.current.update({ speaking: false });
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const rms = computeRms(inputData);
|
|
614
|
+
const chunkBase64 = float32ToPcm16Base64(inputData);
|
|
615
|
+
const ring = ringRef.current;
|
|
616
|
+
if (!ring) return;
|
|
617
|
+
const originalState = ring.stateName;
|
|
618
|
+
ring.push({ data: chunkBase64, rms });
|
|
619
|
+
if (ring.speaking && !stateRef.current.snapshot.speaking) {
|
|
620
|
+
stateRef.current.update({ speaking: true });
|
|
621
|
+
}
|
|
622
|
+
const chunksToSend = ring.consume();
|
|
623
|
+
for (const chunk of chunksToSend) {
|
|
624
|
+
(_a2 = audioCallbackRef.current) == null ? void 0 : _a2.call(audioCallbackRef, chunk);
|
|
625
|
+
}
|
|
626
|
+
if (originalState !== ring.stateName) {
|
|
627
|
+
console.log("[Audio]", { state: ring.stateName, speaking: ring.speaking });
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const blob = new Blob([workletCode], { type: "application/javascript" });
|
|
631
|
+
const url = URL.createObjectURL(blob);
|
|
632
|
+
workletUrlRef.current = url;
|
|
633
|
+
await audioContext.audioWorklet.addModule(url);
|
|
634
|
+
const workletNode = new AudioWorkletNode(audioContext, "capture-processor");
|
|
635
|
+
workletNodeRef.current = workletNode;
|
|
636
|
+
workletNode.port.onmessage = (event) => {
|
|
637
|
+
const inputData = event.data;
|
|
638
|
+
if (!chunkDurationMsRef.current) {
|
|
639
|
+
chunkDurationMsRef.current = inputData.length / configRef.current.sampleRate * 1e3;
|
|
640
|
+
if (ringRef.current) {
|
|
641
|
+
ringRef.current.chunkDurationMs = chunkDurationMsRef.current;
|
|
642
|
+
}
|
|
643
|
+
console.log("[AudioCapture] chunkDurationMs =", chunkDurationMsRef.current);
|
|
644
|
+
}
|
|
645
|
+
onAudioChunkReceived(inputData);
|
|
646
|
+
};
|
|
647
|
+
source.connect(workletNode);
|
|
648
|
+
workletNode.connect(audioContext.destination);
|
|
649
|
+
stateRef.current.update({ recording: true });
|
|
650
|
+
} catch (ex) {
|
|
651
|
+
await stop();
|
|
652
|
+
throw ex;
|
|
653
|
+
}
|
|
654
|
+
}, []);
|
|
655
|
+
const stop = (0, import_react2.useCallback)(async () => {
|
|
656
|
+
try {
|
|
657
|
+
if (mediaStreamRef.current) {
|
|
658
|
+
mediaStreamRef.current.getTracks().forEach((t) => t.stop());
|
|
659
|
+
}
|
|
660
|
+
if (sourceRef.current) {
|
|
661
|
+
sourceRef.current.disconnect();
|
|
662
|
+
}
|
|
663
|
+
if (workletNodeRef.current) {
|
|
664
|
+
workletNodeRef.current.port.onmessage = null;
|
|
665
|
+
workletNodeRef.current = null;
|
|
666
|
+
}
|
|
667
|
+
if (contextRef.current) {
|
|
668
|
+
await contextRef.current.close();
|
|
669
|
+
}
|
|
670
|
+
sourceRef.current = null;
|
|
671
|
+
contextRef.current = null;
|
|
672
|
+
mediaStreamRef.current = null;
|
|
673
|
+
audioCallbackRef.current = null;
|
|
674
|
+
commitCallbackRef.current = null;
|
|
675
|
+
speechEarlyRef.current = null;
|
|
676
|
+
speechConfirmedRef.current = null;
|
|
677
|
+
chunkDurationMsRef.current = 0;
|
|
678
|
+
ringRef.current = null;
|
|
679
|
+
stateRef.current.update({ recording: false, speaking: false });
|
|
680
|
+
} finally {
|
|
681
|
+
if (workletUrlRef.current) {
|
|
682
|
+
URL.revokeObjectURL(workletUrlRef.current);
|
|
683
|
+
workletUrlRef.current = null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}, []);
|
|
687
|
+
const setMuted = (0, import_react2.useCallback)((muted) => {
|
|
688
|
+
if (!stateRef.current.snapshot.muted && muted) {
|
|
689
|
+
if (ringRef.current) {
|
|
690
|
+
ringRef.current.forceResetState();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (stateRef.current.snapshot.muted != muted) {
|
|
694
|
+
const speaking = muted ? false : stateRef.current.snapshot.speaking;
|
|
695
|
+
stateRef.current.update({ muted, speaking });
|
|
696
|
+
}
|
|
697
|
+
}, []);
|
|
698
|
+
return {
|
|
699
|
+
start,
|
|
700
|
+
stop,
|
|
701
|
+
configure,
|
|
702
|
+
setMuted,
|
|
703
|
+
subscribeState: stateRef.current.subscribe,
|
|
704
|
+
getState: () => stateRef.current.snapshot
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/constellationProvider.tsx
|
|
709
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
710
|
+
var ConstellationContext = import_react3.default.createContext(null);
|
|
711
|
+
function ConstellationProvider({ config, children }) {
|
|
712
|
+
const clientRef = (0, import_react3.useRef)(null);
|
|
713
|
+
if (!clientRef.current) {
|
|
714
|
+
clientRef.current = new import_constellation_client.WebClient(config);
|
|
715
|
+
}
|
|
716
|
+
const audioPlayback = useAudioPlayback();
|
|
717
|
+
const audioCapture = useAudioCapture();
|
|
718
|
+
const value = (0, import_react3.useMemo)(() => ({
|
|
719
|
+
client: clientRef.current,
|
|
720
|
+
audioPlayback,
|
|
721
|
+
audioCapture
|
|
722
|
+
}), [audioPlayback, audioCapture]);
|
|
723
|
+
(0, import_react3.useEffect)(() => {
|
|
724
|
+
return () => {
|
|
725
|
+
var _a, _b;
|
|
726
|
+
(_b = (_a = clientRef.current) == null ? void 0 : _a.closeStream) == null ? void 0 : _b.call(_a);
|
|
727
|
+
void audioCapture.stop();
|
|
728
|
+
void audioPlayback.release();
|
|
729
|
+
};
|
|
730
|
+
}, [audioCapture, audioPlayback]);
|
|
731
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ConstellationContext.Provider, { value, children });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/useConstellationSession.tsx
|
|
735
|
+
var import_react4 = require("react");
|
|
736
|
+
var import_constellation_client2 = require("@simfinity/constellation-client");
|
|
737
|
+
function handleResponsePacket(event, handlers, enqueueAudio) {
|
|
738
|
+
var _a, _b, _c, _d, _e, _f;
|
|
739
|
+
const data = event.data;
|
|
740
|
+
if (data.audio) {
|
|
741
|
+
if (data.audio.data !== "") {
|
|
742
|
+
enqueueAudio(data.audio.data);
|
|
743
|
+
}
|
|
744
|
+
if (data.audio.done) {
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (data.transcript) {
|
|
748
|
+
if (data.transcript.input) {
|
|
749
|
+
if (data.transcript.input.done) {
|
|
750
|
+
(_a = handlers.onTranscriptInput) == null ? void 0 : _a.call(handlers, data.transcript.input.text);
|
|
751
|
+
} else {
|
|
752
|
+
(_b = handlers.onTranscriptInputPart) == null ? void 0 : _b.call(handlers, data.transcript.input.text, data.transcript.input.done);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (data.transcript.response) {
|
|
756
|
+
if (data.transcript.response.done) {
|
|
757
|
+
(_c = handlers.onTranscriptResponse) == null ? void 0 : _c.call(handlers, data.transcript.response.text);
|
|
758
|
+
} else {
|
|
759
|
+
(_d = handlers.onTranscriptResponsePart) == null ? void 0 : _d.call(handlers, data.transcript.response.text, data.transcript.response.done);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (data.done) {
|
|
764
|
+
(_f = handlers.onResponseEnd) == null ? void 0 : _f.call(handlers, (_e = data.canceled) != null ? _e : false);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function useConstellationSession() {
|
|
768
|
+
const ctx = (0, import_react4.useContext)(ConstellationContext);
|
|
769
|
+
if (!ctx) throw new Error("Must be used inside ConstellationProvider");
|
|
770
|
+
const [sessionId, setSessionId] = (0, import_react4.useState)(null);
|
|
771
|
+
const unsubscribersRef = (0, import_react4.useRef)([]);
|
|
772
|
+
const clearEventSubscriptions = (0, import_react4.useCallback)(() => {
|
|
773
|
+
for (const unsubscribe of unsubscribersRef.current) {
|
|
774
|
+
unsubscribe();
|
|
775
|
+
}
|
|
776
|
+
unsubscribersRef.current = [];
|
|
777
|
+
}, []);
|
|
778
|
+
(0, import_react4.useEffect)(() => {
|
|
779
|
+
return () => clearEventSubscriptions();
|
|
780
|
+
}, [clearEventSubscriptions]);
|
|
781
|
+
return (0, import_react4.useMemo)(() => ({
|
|
782
|
+
startSession: async (params) => {
|
|
783
|
+
await ctx.client.startSession(params);
|
|
784
|
+
setSessionId(ctx.client.session);
|
|
785
|
+
},
|
|
786
|
+
endSession: async () => {
|
|
787
|
+
clearEventSubscriptions();
|
|
788
|
+
const stats = await ctx.client.endSession();
|
|
789
|
+
setSessionId(null);
|
|
790
|
+
ctx.audioCapture.stop();
|
|
791
|
+
ctx.audioPlayback.release();
|
|
792
|
+
return stats;
|
|
793
|
+
},
|
|
794
|
+
joinSession: async (audio, handlers) => {
|
|
795
|
+
clearEventSubscriptions();
|
|
796
|
+
unsubscribersRef.current.push(
|
|
797
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.SystemConnectionClosed, (e) => {
|
|
798
|
+
var _a;
|
|
799
|
+
handlers.onStreamClosed((_a = e.data.reason) != null ? _a : "");
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
unsubscribersRef.current.push(
|
|
803
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.ResponsePacket, (e) => {
|
|
804
|
+
handleResponsePacket(e, handlers, ctx.audioPlayback.enqueue);
|
|
805
|
+
})
|
|
806
|
+
);
|
|
807
|
+
if (handlers.onLatencyUpdate) {
|
|
808
|
+
unsubscribersRef.current.push(
|
|
809
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.SystemLatency, (e) => {
|
|
810
|
+
var _a;
|
|
811
|
+
(_a = handlers.onLatencyUpdate) == null ? void 0 : _a.call(handlers, e.data.latencyMs);
|
|
812
|
+
})
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (handlers.onSessionConfigured) {
|
|
816
|
+
unsubscribersRef.current.push(
|
|
817
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.SessionConfigured, (e) => {
|
|
818
|
+
var _a;
|
|
819
|
+
(_a = handlers.onSessionConfigured) == null ? void 0 : _a.call(handlers, e.data);
|
|
820
|
+
})
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (handlers.onAgentResponse) {
|
|
824
|
+
unsubscribersRef.current.push(
|
|
825
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.AgentResponse, (e) => {
|
|
826
|
+
var _a;
|
|
827
|
+
(_a = handlers.onAgentResponse) == null ? void 0 : _a.call(handlers, e.data);
|
|
828
|
+
})
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
if (handlers.onClientAction) {
|
|
832
|
+
unsubscribersRef.current.push(
|
|
833
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.ClientAction, (e) => {
|
|
834
|
+
var _a;
|
|
835
|
+
(_a = handlers.onClientAction) == null ? void 0 : _a.call(handlers, e.data.action, e.data.payload);
|
|
836
|
+
})
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
if (handlers.onTechnicalError) {
|
|
840
|
+
unsubscribersRef.current.push(
|
|
841
|
+
ctx.client.on(import_constellation_client2.WebClientEvents.TechnicalError, (e) => {
|
|
842
|
+
var _a;
|
|
843
|
+
(_a = handlers.onTechnicalError) == null ? void 0 : _a.call(handlers, e.data.error);
|
|
844
|
+
})
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
await ctx.client.joinSession(
|
|
849
|
+
audio
|
|
850
|
+
/*, { ...handlers, onAudioResponseChunk: (chunk: string) => { ctx.audioPlayback.enqueue(chunk); }}*/
|
|
851
|
+
);
|
|
852
|
+
} catch (ex) {
|
|
853
|
+
clearEventSubscriptions();
|
|
854
|
+
throw ex;
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
configureSession: (settings) => {
|
|
858
|
+
ctx.client.configureSession(settings);
|
|
859
|
+
},
|
|
860
|
+
sendText: (text) => {
|
|
861
|
+
ctx.client.sendText(text);
|
|
862
|
+
},
|
|
863
|
+
sendWhisper: (text) => {
|
|
864
|
+
ctx.client.sendWhisper(text);
|
|
865
|
+
},
|
|
866
|
+
audioDevice: () => ({
|
|
867
|
+
in: {
|
|
868
|
+
start: async () => {
|
|
869
|
+
const captureParams = {
|
|
870
|
+
onSpeechConsidered: () => {
|
|
871
|
+
},
|
|
872
|
+
onSpeechConfirmed: () => ctx.audioPlayback.clearQueue(),
|
|
873
|
+
onAudioChunk: (chunk) => ctx.client.sendAudioChunk(chunk),
|
|
874
|
+
onSilenceCommit: () => ctx.client.commitAudioChunks()
|
|
875
|
+
};
|
|
876
|
+
await ctx.audioCapture.start(captureParams);
|
|
877
|
+
},
|
|
878
|
+
stop: async () => {
|
|
879
|
+
await ctx.audioCapture.stop();
|
|
880
|
+
},
|
|
881
|
+
setMuted: (muted) => {
|
|
882
|
+
ctx.audioCapture.setMuted(muted);
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
out: {}
|
|
886
|
+
}),
|
|
887
|
+
sessionId
|
|
888
|
+
}), [ctx, sessionId]);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/useAudioPlaybackState.tsx
|
|
892
|
+
var import_react5 = require("react");
|
|
893
|
+
function useAudioPlaybackState(selector) {
|
|
894
|
+
const ctx = (0, import_react5.useContext)(ConstellationContext);
|
|
895
|
+
if (!ctx) throw new Error("Must be used inside ConstellationProvider");
|
|
896
|
+
return (0, import_react5.useSyncExternalStore)(
|
|
897
|
+
ctx.audioPlayback.subscribeState,
|
|
898
|
+
() => selector(ctx.audioPlayback.getState())
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/useAudioCaptureState.tsx
|
|
903
|
+
var import_react6 = require("react");
|
|
904
|
+
function useAudioCaptureState(selector) {
|
|
905
|
+
const ctx = (0, import_react6.useContext)(ConstellationContext);
|
|
906
|
+
if (!ctx) throw new Error("Must be used inside ConstellationProvider");
|
|
907
|
+
return (0, import_react6.useSyncExternalStore)(
|
|
908
|
+
ctx.audioCapture.subscribeState,
|
|
909
|
+
() => selector(ctx.audioCapture.getState())
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
913
|
+
0 && (module.exports = {
|
|
914
|
+
AudioCaptureConfig,
|
|
915
|
+
ConstellationProvider,
|
|
916
|
+
useAudioCaptureState,
|
|
917
|
+
useAudioPlaybackState,
|
|
918
|
+
useConstellationSession
|
|
919
|
+
});
|