@lumavox/client 0.16.0 → 0.17.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/README.md +7 -0
- package/dist/audio.d.ts +6 -0
- package/dist/hands-free.d.ts +46 -6
- package/dist/hands-free.js +147 -19
- package/dist/web-audio/web-audio.d.ts +5 -4
- package/dist/web-audio/web-audio.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -191,6 +191,13 @@ allows it where the browser cancels echo (not on an iPhone or iPad);
|
|
|
191
191
|
`assumeNoEcho` with headphones on. `loudness()` is the built-in detector,
|
|
192
192
|
`detector` your own.
|
|
193
193
|
|
|
194
|
+
**Half-duplex**, where echo cannot be cancelled: `listen(conversation,
|
|
195
|
+
audio, { halfDuplex: true })` keeps the microphone open but sends nothing
|
|
196
|
+
while the agent's voice is heard — on the player's clock, then 300 ms more
|
|
197
|
+
(`afterVoiceMs`) for the room's echo — and talking never stops it, so the
|
|
198
|
+
agent never hears itself and it is never refused for echo. Something said
|
|
199
|
+
as the voice starts ends there as a turn, answered after the reply.
|
|
200
|
+
|
|
194
201
|
## Runtimes
|
|
195
202
|
|
|
196
203
|
Browsers, React Native (Hermes, ES2020 — and each binary frame reaches JS
|
package/dist/audio.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface AgentAudio {
|
|
|
34
34
|
*/
|
|
35
35
|
startCapture(onPcm: (pcm: ArrayBuffer) => void, options?: {
|
|
36
36
|
rate?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Hands-free asks it, once `handsFree` said yes: take the agent's
|
|
39
|
+
* voice out of the microphone — a phone's voice processing and call
|
|
40
|
+
* audio. An adapter that always does, as a browser's, ignores it.
|
|
41
|
+
*/
|
|
42
|
+
echoCancellation?: boolean;
|
|
37
43
|
}): Promise<{
|
|
38
44
|
rate: number;
|
|
39
45
|
}>;
|
package/dist/hands-free.d.ts
CHANGED
|
@@ -25,10 +25,36 @@
|
|
|
25
25
|
* cancellation leaves of its voice is louder than a quiet room.
|
|
26
26
|
* - **Refused where the agent would hear itself** (D16): an adapter that
|
|
27
27
|
* says so, or cannot say (`AgentAudio.handsFree`) unless the app knows.
|
|
28
|
+
* On its yes the recorder is asked to cancel the echo
|
|
29
|
+
* (`startCapture`'s `echoCancellation`): a phone's is a mode of its own —
|
|
30
|
+
* the call's audio, quieter — never wanted with headphones on or in
|
|
31
|
+
* half-duplex.
|
|
32
|
+
* - **Half-duplex** (D19, `halfDuplex`), where there is no echo to cancel —
|
|
33
|
+
* a phone on `react-native-audio-api` 0.13, a browser that cannot cancel
|
|
34
|
+
* it: the microphone stays open, but **while the agent's voice is heard
|
|
35
|
+
* nothing is sent, the detector rests and there is no barge-in**, so the
|
|
36
|
+
* agent never hears itself and `handsFree()` is not asked. Heard is the
|
|
37
|
+
* player's clock (`PlaybackClock`: each frame's `startsAt` and
|
|
38
|
+
* `duration`, against `heardTime()`), never when frames are queued: a
|
|
39
|
+
* phone queues a reply seconds ahead of its sound. An adapter with no
|
|
40
|
+
* clock is taken at its word, from an `audio.start` to `onDrained`.
|
|
41
|
+
* **The voice has the floor**: an utterance open when it starts ends
|
|
42
|
+
* there as a turn — what was said before it is kept, and answered after
|
|
43
|
+
* the reply. **Then `afterVoiceMs`, 300 ms, before the detector listens
|
|
44
|
+
* again**: what the clock cannot know of a phone's sound path (0.13 says
|
|
45
|
+
* none; most phones' loudspeakers take 20 to 100 ms), the microphone's own
|
|
46
|
+
* latency (10 to 50 ms), and the room's echo falling 30 dB (half of a
|
|
47
|
+
* furnished room's 0.3 to 0.6 s reverberation) — else the agent's last
|
|
48
|
+
* syllable, ringing on, opens a recording of itself. It costs no reply
|
|
49
|
+
* any time: those frames are kept as pre-roll, so words begun in it keep
|
|
50
|
+
* their start; only a word over before it ends — a quick "yes" under
|
|
51
|
+
* 0.3 s — goes unheard.
|
|
28
52
|
*
|
|
29
53
|
* Time here is audio time, counted in 20 ms frames, never the clock's: a
|
|
30
54
|
* busy thread delivers late, and the frames still say how long the
|
|
31
|
-
* silence was.
|
|
55
|
+
* silence was. Half-duplex alone reads the player's clock, to set each
|
|
56
|
+
* frame against the voice: the last sample delivered is heard now, and
|
|
57
|
+
* each before it a sample earlier.
|
|
32
58
|
*/
|
|
33
59
|
import type { AgentAudio } from "./audio.js";
|
|
34
60
|
import type { Conversation, TurnOptions } from "./conversation.js";
|
|
@@ -88,6 +114,19 @@ export interface ListenOptions extends TurnOptions {
|
|
|
88
114
|
wordsWithinMs?: number;
|
|
89
115
|
/** The app knows the agent's voice stays out of the microphone — headphones, say — though the adapter cannot tell. */
|
|
90
116
|
assumeNoEcho?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Half-duplex: the microphone stays open, but nothing is sent while the
|
|
119
|
+
* agent's voice is heard, and talking never stops it — for where echo
|
|
120
|
+
* cannot be cancelled, such as a phone, so `handsFree()` is not asked.
|
|
121
|
+
*/
|
|
122
|
+
halfDuplex?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Half-duplex: after the agent's voice is heard ending, how long the
|
|
125
|
+
* detector still waits, ms: 300 — so the room's echo of its last word
|
|
126
|
+
* does not open a recording. Words begun in it keep their start; one
|
|
127
|
+
* over before it ends is not heard. Lower with headphones on.
|
|
128
|
+
*/
|
|
129
|
+
afterVoiceMs?: number;
|
|
91
130
|
/** `listening` for the person, `hearing` them, `paused` while the device has the audio, then `stopped`. */
|
|
92
131
|
onState?(state: ListeningState): void;
|
|
93
132
|
/** The person talked over the agent: its voice was stopped. */
|
|
@@ -107,12 +146,13 @@ export type ListenOutcome = {
|
|
|
107
146
|
reason: string;
|
|
108
147
|
};
|
|
109
148
|
/** Why `listen` refuses an adapter that cannot say whether the agent hears itself. */
|
|
110
|
-
export declare const NO_ECHO_ANSWER = "This audio adapter cannot say whether the agent's voice stays out of the microphone (`AgentAudio.handsFree`). Use push-to-talk, or pass `assumeNoEcho` if you know it does.";
|
|
149
|
+
export declare const NO_ECHO_ANSWER = "This audio adapter cannot say whether the agent's voice stays out of the microphone (`AgentAudio.handsFree`). Use half-duplex (`halfDuplex`) or push-to-talk, or pass `assumeNoEcho` if you know it does.";
|
|
111
150
|
/**
|
|
112
151
|
* Leave the microphone open on `conversation`: every utterance the
|
|
113
|
-
* detector hears becomes a turn, and talking over the agent stops it
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
152
|
+
* detector hears becomes a turn, and talking over the agent stops it — or,
|
|
153
|
+
* with `halfDuplex`, nothing is heard while the agent speaks. Answers
|
|
154
|
+
* `ok: false` with the reason, in words an app can show, where the agent
|
|
155
|
+
* would hear itself or the microphone would not open — half-duplex and
|
|
156
|
+
* push-to-talk still work there.
|
|
117
157
|
*/
|
|
118
158
|
export declare function listen(conversation: Conversation, audio: AgentAudio, options?: ListenOptions): Promise<ListenOutcome>;
|
package/dist/hands-free.js
CHANGED
|
@@ -40,16 +40,17 @@ export function loudness(options = {}) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
/** Why `listen` refuses an adapter that cannot say whether the agent hears itself. */
|
|
43
|
-
export const NO_ECHO_ANSWER = "This audio adapter cannot say whether the agent's voice stays out of the microphone (`AgentAudio.handsFree`). Use push-to-talk, or pass `assumeNoEcho` if you know it does.";
|
|
43
|
+
export const NO_ECHO_ANSWER = "This audio adapter cannot say whether the agent's voice stays out of the microphone (`AgentAudio.handsFree`). Use half-duplex (`halfDuplex`) or push-to-talk, or pass `assumeNoEcho` if you know it does.";
|
|
44
44
|
/**
|
|
45
45
|
* Leave the microphone open on `conversation`: every utterance the
|
|
46
|
-
* detector hears becomes a turn, and talking over the agent stops it
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* detector hears becomes a turn, and talking over the agent stops it — or,
|
|
47
|
+
* with `halfDuplex`, nothing is heard while the agent speaks. Answers
|
|
48
|
+
* `ok: false` with the reason, in words an app can show, where the agent
|
|
49
|
+
* would hear itself or the microphone would not open — half-duplex and
|
|
50
|
+
* push-to-talk still work there.
|
|
50
51
|
*/
|
|
51
52
|
export async function listen(conversation, audio, options = {}) {
|
|
52
|
-
if (!options.assumeNoEcho) {
|
|
53
|
+
if (!options.assumeNoEcho && !options.halfDuplex) {
|
|
53
54
|
const echo = audio.handsFree
|
|
54
55
|
? await audio.handsFree()
|
|
55
56
|
: { ok: false, reason: NO_ECHO_ANSWER };
|
|
@@ -80,6 +81,12 @@ class HandsFree {
|
|
|
80
81
|
this.playingTurn = null;
|
|
81
82
|
/** The reply a barge-in stopped: its late windows are not the agent speaking. */
|
|
82
83
|
this.stoppedTurn = null;
|
|
84
|
+
/** Half-duplex, on that clock: the agent's voice as scheduled, in runs of frames back to back. */
|
|
85
|
+
this.voice = [];
|
|
86
|
+
/** The clock as last read, to see it go back. */
|
|
87
|
+
this.read = 0;
|
|
88
|
+
/** Half-duplex with no clock: audio time since the voice was last playing. */
|
|
89
|
+
this.sinceVoiceMs = Number.POSITIVE_INFINITY;
|
|
83
90
|
this.off = [];
|
|
84
91
|
this.conversation = conversation;
|
|
85
92
|
this.audio = audio;
|
|
@@ -89,12 +96,18 @@ class HandsFree {
|
|
|
89
96
|
this.startAfterMs = options.startAfterMs ?? 100;
|
|
90
97
|
this.wordsWithinMs = options.wordsWithinMs ?? 3000;
|
|
91
98
|
this.ringFrames = Math.ceil(((options.preRollMs ?? 300) + this.startAfterMs) / FRAME_MS);
|
|
99
|
+
this.halfDuplex = options.halfDuplex === true;
|
|
100
|
+
this.afterVoiceMs = options.afterVoiceMs ?? 300;
|
|
101
|
+
this.clock = this.halfDuplex ? clockOf(audio) : null;
|
|
92
102
|
}
|
|
93
103
|
/** The microphone, and everything watched: `null`, or why it did not open. */
|
|
94
104
|
async start() {
|
|
95
105
|
this.off.push(this.conversation.onFrame((frame) => this.frame(frame)), this.audio.onDrained(() => {
|
|
96
106
|
this.playing = false;
|
|
97
107
|
}));
|
|
108
|
+
if (this.clock) {
|
|
109
|
+
this.off.push(this.clock.onScheduled((audio) => this.scheduled(audio)), this.clock.onStopped(() => this.cut()));
|
|
110
|
+
}
|
|
98
111
|
const interrupted = this.audio.onInterruption?.((now) => this.interrupted(now));
|
|
99
112
|
if (interrupted)
|
|
100
113
|
this.off.push(interrupted);
|
|
@@ -119,7 +132,14 @@ class HandsFree {
|
|
|
119
132
|
async capture() {
|
|
120
133
|
let granted;
|
|
121
134
|
try {
|
|
122
|
-
({ rate: granted } = await this.audio.startCapture((pcm) => this.pcm(pcm), {
|
|
135
|
+
({ rate: granted } = await this.audio.startCapture((pcm) => this.pcm(pcm), {
|
|
136
|
+
rate: this.options.rate ?? 16000,
|
|
137
|
+
// On the adapter's word that it can: half-duplex and the app's
|
|
138
|
+
// own word need none, and a phone's sounds better without.
|
|
139
|
+
...(this.options.halfDuplex || this.options.assumeNoEcho
|
|
140
|
+
? {}
|
|
141
|
+
: { echoCancellation: true }),
|
|
142
|
+
}));
|
|
123
143
|
}
|
|
124
144
|
catch (failure) {
|
|
125
145
|
// swallowed: it is the answer's reason, the app's to show.
|
|
@@ -170,11 +190,16 @@ class HandsFree {
|
|
|
170
190
|
void this.capture().then((failed) => {
|
|
171
191
|
if (this.state !== "paused")
|
|
172
192
|
return;
|
|
173
|
-
|
|
174
|
-
if (failed === null)
|
|
193
|
+
if (failed === null) {
|
|
175
194
|
this.set("listening");
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Still taken, or gone: stopped, and the app's next `listen` tries
|
|
198
|
+
// again. Given back, so what the capture set up is undone.
|
|
199
|
+
this.close();
|
|
200
|
+
this.audio.stopCapture().catch(() => {
|
|
201
|
+
// swallowed: nothing was open; the adapter has let go either way.
|
|
202
|
+
});
|
|
178
203
|
});
|
|
179
204
|
}
|
|
180
205
|
pcm(buffer) {
|
|
@@ -187,14 +212,25 @@ class HandsFree {
|
|
|
187
212
|
samples.set(this.carry, 0);
|
|
188
213
|
samples.set(fresh, this.carry.length);
|
|
189
214
|
}
|
|
215
|
+
// Half-duplex: the last sample is heard now, on the player's clock.
|
|
216
|
+
const heard = this.clock ? this.heardNow() : 0;
|
|
190
217
|
let at = 0;
|
|
191
218
|
for (; at + this.frameSamples <= samples.length; at += this.frameSamples) {
|
|
192
|
-
|
|
219
|
+
const end = at + this.frameSamples;
|
|
220
|
+
this.step(samples.slice(at, end), heard - (samples.length - end) / this.rate);
|
|
193
221
|
}
|
|
194
222
|
this.carry = samples.slice(at);
|
|
195
223
|
}
|
|
196
|
-
|
|
197
|
-
|
|
224
|
+
/** One 20 ms frame; `endsAt`, half-duplex on a clock, when its last sample was heard. */
|
|
225
|
+
step(frame, endsAt) {
|
|
226
|
+
if (this.halfDuplex) {
|
|
227
|
+
const ago = this.voiceAgo(endsAt);
|
|
228
|
+
if (ago === 0 || ago < this.afterVoiceMs) {
|
|
229
|
+
this.voiceHeard(ago === 0 ? null : frame);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const voice = this.detector.push(frame, this.rate, this.playing && !this.halfDuplex);
|
|
198
234
|
const talking = voice === "talking";
|
|
199
235
|
if (talking) {
|
|
200
236
|
this.talkMs += FRAME_MS;
|
|
@@ -206,9 +242,7 @@ class HandsFree {
|
|
|
206
242
|
}
|
|
207
243
|
const open = this.open;
|
|
208
244
|
if (!open) {
|
|
209
|
-
this.
|
|
210
|
-
if (this.ring.length > this.ringFrames)
|
|
211
|
-
this.ring.shift();
|
|
245
|
+
this.keep(frame);
|
|
212
246
|
if (this.waitForQuiet && this.quietMs >= this.endAfterMs) {
|
|
213
247
|
this.waitForQuiet = false;
|
|
214
248
|
}
|
|
@@ -228,12 +262,96 @@ class HandsFree {
|
|
|
228
262
|
this.end();
|
|
229
263
|
}
|
|
230
264
|
}
|
|
265
|
+
/** A frame of the room kept for the next utterance's pre-roll. */
|
|
266
|
+
keep(frame) {
|
|
267
|
+
this.ring.push(frame);
|
|
268
|
+
if (this.ring.length > this.ringFrames)
|
|
269
|
+
this.ring.shift();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Half-duplex: ms between the agent's voice last heard and the frame
|
|
273
|
+
* ending at `endsAt` — 0 while it is heard, infinite before it ever was.
|
|
274
|
+
*/
|
|
275
|
+
voiceAgo(endsAt) {
|
|
276
|
+
if (!this.clock) {
|
|
277
|
+
// Taken at the adapter's word: playing from `audio.start` to `onDrained`.
|
|
278
|
+
this.sinceVoiceMs = this.playing ? 0 : this.sinceVoiceMs + FRAME_MS;
|
|
279
|
+
return this.playing ? 0 : this.sinceVoiceMs;
|
|
280
|
+
}
|
|
281
|
+
const startsAt = endsAt - FRAME_MS / 1000;
|
|
282
|
+
let ago = Number.POSITIVE_INFINITY;
|
|
283
|
+
for (const run of this.voice) {
|
|
284
|
+
if (run.from < endsAt && run.until > startsAt)
|
|
285
|
+
return 0;
|
|
286
|
+
if (run.until <= startsAt)
|
|
287
|
+
ago = (startsAt - run.until) * 1000;
|
|
288
|
+
}
|
|
289
|
+
return ago;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Half-duplex: the agent's voice is heard, or its tail (`tail`, the
|
|
293
|
+
* frame). The voice has the floor — an utterance open ends as a turn
|
|
294
|
+
* before any of it is sent — and the detector rests; nothing of the
|
|
295
|
+
* voice is pre-roll, its tail is.
|
|
296
|
+
*/
|
|
297
|
+
voiceHeard(tail) {
|
|
298
|
+
if (this.open)
|
|
299
|
+
this.end();
|
|
300
|
+
this.talkMs = 0;
|
|
301
|
+
this.quietMs = 0;
|
|
302
|
+
if (tail)
|
|
303
|
+
this.keep(tail);
|
|
304
|
+
else
|
|
305
|
+
this.ring = [];
|
|
306
|
+
}
|
|
307
|
+
/** The clock now; one that went back — a new context, the phone's for a reply at another rate — leaves the runs kept on the old one. */
|
|
308
|
+
now() {
|
|
309
|
+
const now = this.clock.now();
|
|
310
|
+
// The stop that came with it was told; the new run starts at once.
|
|
311
|
+
if (now < this.read)
|
|
312
|
+
this.voice = [];
|
|
313
|
+
this.read = now;
|
|
314
|
+
return now;
|
|
315
|
+
}
|
|
316
|
+
/** What is heard now on the clock, the runs long past forgotten. */
|
|
317
|
+
heardNow() {
|
|
318
|
+
this.now();
|
|
319
|
+
const heard = this.clock.heardTime();
|
|
320
|
+
const past = heard - (this.afterVoiceMs + 1000) / 1000;
|
|
321
|
+
while (this.voice.length > 1 && (this.voice[0]?.until ?? 0) < past) {
|
|
322
|
+
this.voice.shift();
|
|
323
|
+
}
|
|
324
|
+
return heard;
|
|
325
|
+
}
|
|
326
|
+
/** A frame of the reply, queued to start at `startsAt`: its run of the voice, or a new one after a gap. */
|
|
327
|
+
scheduled(audio) {
|
|
328
|
+
this.now();
|
|
329
|
+
const until = audio.startsAt + audio.duration;
|
|
330
|
+
const last = this.voice[this.voice.length - 1];
|
|
331
|
+
// Back to back with the one before, to a millisecond's rounding.
|
|
332
|
+
if (last &&
|
|
333
|
+
audio.startsAt >= last.from &&
|
|
334
|
+
audio.startsAt <= last.until + 0.001) {
|
|
335
|
+
last.until = Math.max(last.until, until);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.voice.push({ from: audio.startsAt, until });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** The player dropped what it had queued: the voice ends where the clock is. */
|
|
342
|
+
cut() {
|
|
343
|
+
const now = this.now();
|
|
344
|
+
this.voice = this.voice.filter((run) => run.from < now);
|
|
345
|
+
for (const run of this.voice)
|
|
346
|
+
run.until = Math.min(run.until, now);
|
|
347
|
+
}
|
|
231
348
|
begin() {
|
|
232
|
-
|
|
349
|
+
// Half-duplex never opens over the voice: no barge-in to decide.
|
|
350
|
+
const barging = this.playing && !this.halfDuplex;
|
|
233
351
|
const options = this.options;
|
|
234
352
|
const id = this.conversation.startAudio({
|
|
235
353
|
rate: this.rate,
|
|
236
|
-
bargeIn:
|
|
354
|
+
bargeIn: !this.halfDuplex,
|
|
237
355
|
...(options.language ? { language: options.language } : {}),
|
|
238
356
|
...(options.model ? { model: options.model } : {}),
|
|
239
357
|
...(options.speech === undefined ? {} : { speech: options.speech }),
|
|
@@ -273,6 +391,16 @@ class HandsFree {
|
|
|
273
391
|
this.options.onState?.(state);
|
|
274
392
|
}
|
|
275
393
|
}
|
|
394
|
+
/** The adapter's `PlaybackClock`, when it has one: both of this SDK's do; one of an app's own need not. */
|
|
395
|
+
function clockOf(audio) {
|
|
396
|
+
const clock = audio;
|
|
397
|
+
return typeof clock.now === "function" &&
|
|
398
|
+
typeof clock.heardTime === "function" &&
|
|
399
|
+
typeof clock.onScheduled === "function" &&
|
|
400
|
+
typeof clock.onStopped === "function"
|
|
401
|
+
? audio
|
|
402
|
+
: null;
|
|
403
|
+
}
|
|
276
404
|
/** 16-bit little-endian PCM as samples, on a big-endian device too. */
|
|
277
405
|
function samplesOf(buffer) {
|
|
278
406
|
const count = buffer.byteLength >> 1;
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* only with the context at the device's rate, which the player keeps.
|
|
20
20
|
* Every browser on an iPhone or iPad is WebKit, whose canceller fails
|
|
21
21
|
* three ways (WebKit bugs 218012, 311451, 326286): refused there — and
|
|
22
|
-
* wherever the app turned echo cancellation off.
|
|
22
|
+
* wherever the app turned echo cancellation off. Half-duplex runs
|
|
23
|
+
* anywhere (`listen`'s `halfDuplex`): it never asks.
|
|
23
24
|
* - **The clock and the frames are the app's** (`PlaybackClock`): each
|
|
24
25
|
* frame told as it is queued, with when it starts; the stop; what the
|
|
25
26
|
* microphone captures. A handler that throws is rethrown on its own turn,
|
|
@@ -47,11 +48,11 @@ export interface WebAudioOptions {
|
|
|
47
48
|
/** What `play` is given: the frame's rate, and what `onScheduled` passes on when it has it. */
|
|
48
49
|
type Frame = Parameters<AgentAudio["play"]>[1];
|
|
49
50
|
/** Why hands-free is refused on an iPhone or iPad. */
|
|
50
|
-
export declare const IOS_ECHO = "Hands-free is not available on an iPhone or iPad browser: its echo cancellation is broken (WebKit bugs 218012, 311451 and 326286). Use push-to-talk.";
|
|
51
|
+
export declare const IOS_ECHO = "Hands-free is not available on an iPhone or iPad browser: its echo cancellation is broken (WebKit bugs 218012, 311451 and 326286). Use half-duplex or push-to-talk.";
|
|
51
52
|
/** Why it is refused in a browser that cannot cancel echo at all. */
|
|
52
|
-
export declare const NO_ECHO = "This browser cannot cancel echo, so the agent would hear itself. Use push-to-talk.";
|
|
53
|
+
export declare const NO_ECHO = "This browser cannot cancel echo, so the agent would hear itself. Use half-duplex or push-to-talk.";
|
|
53
54
|
/** Why it is refused with echo cancellation turned off (`setProcessing`). */
|
|
54
|
-
export declare const ECHO_OFF = "Echo cancellation is turned off, so the agent would hear itself. Turn it on, or use push-to-talk.";
|
|
55
|
+
export declare const ECHO_OFF = "Echo cancellation is turned off, so the agent would hear itself. Turn it on, or use half-duplex or push-to-talk.";
|
|
55
56
|
export declare class WebAudio implements AgentAudio, PlaybackClock, DeviceChoices {
|
|
56
57
|
private player;
|
|
57
58
|
private recorder;
|
|
@@ -2,11 +2,11 @@ import { PcmPlayer } from "./player.js";
|
|
|
2
2
|
/** The reply's loudness while the person may be talking over it: about -10 dB. */
|
|
3
3
|
const DUCKED = 0.3;
|
|
4
4
|
/** Why hands-free is refused on an iPhone or iPad. */
|
|
5
|
-
export const IOS_ECHO = "Hands-free is not available on an iPhone or iPad browser: its echo cancellation is broken (WebKit bugs 218012, 311451 and 326286). Use push-to-talk.";
|
|
5
|
+
export const IOS_ECHO = "Hands-free is not available on an iPhone or iPad browser: its echo cancellation is broken (WebKit bugs 218012, 311451 and 326286). Use half-duplex or push-to-talk.";
|
|
6
6
|
/** Why it is refused in a browser that cannot cancel echo at all. */
|
|
7
|
-
export const NO_ECHO = "This browser cannot cancel echo, so the agent would hear itself. Use push-to-talk.";
|
|
7
|
+
export const NO_ECHO = "This browser cannot cancel echo, so the agent would hear itself. Use half-duplex or push-to-talk.";
|
|
8
8
|
/** Why it is refused with echo cancellation turned off (`setProcessing`). */
|
|
9
|
-
export const ECHO_OFF = "Echo cancellation is turned off, so the agent would hear itself. Turn it on, or use push-to-talk.";
|
|
9
|
+
export const ECHO_OFF = "Echo cancellation is turned off, so the agent would hear itself. Turn it on, or use half-duplex or push-to-talk.";
|
|
10
10
|
export class WebAudio {
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.player = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumavox/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Talk to Lumavox agents from a browser, a React Native app or Node, with an end user's ephemeral key: conversations on the agent's socket, sessions, memory and relations. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|