@reins-ai/voice 0.0.0-stage → 0.1.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/barge-in.d.ts +42 -0
  4. package/dist/barge-in.d.ts.map +1 -0
  5. package/dist/barge-in.js +78 -0
  6. package/dist/barge-in.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +106 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +32 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +53 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/download.d.ts +18 -0
  16. package/dist/download.d.ts.map +1 -0
  17. package/dist/download.js +37 -0
  18. package/dist/download.js.map +1 -0
  19. package/dist/ear.d.ts +173 -0
  20. package/dist/ear.d.ts.map +1 -0
  21. package/dist/ear.js +388 -0
  22. package/dist/ear.js.map +1 -0
  23. package/dist/log.d.ts +6 -0
  24. package/dist/log.d.ts.map +1 -0
  25. package/dist/log.js +13 -0
  26. package/dist/log.js.map +1 -0
  27. package/dist/mic.d.ts +19 -0
  28. package/dist/mic.d.ts.map +1 -0
  29. package/dist/mic.js +43 -0
  30. package/dist/mic.js.map +1 -0
  31. package/dist/piper.d.ts +38 -0
  32. package/dist/piper.d.ts.map +1 -0
  33. package/dist/piper.js +90 -0
  34. package/dist/piper.js.map +1 -0
  35. package/dist/server.d.ts +21 -0
  36. package/dist/server.d.ts.map +1 -0
  37. package/dist/server.js +92 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/setup.d.ts +15 -0
  40. package/dist/setup.d.ts.map +1 -0
  41. package/dist/setup.js +73 -0
  42. package/dist/setup.js.map +1 -0
  43. package/dist/speak.d.ts +77 -0
  44. package/dist/speak.d.ts.map +1 -0
  45. package/dist/speak.js +191 -0
  46. package/dist/speak.js.map +1 -0
  47. package/dist/speech-check.d.ts +27 -0
  48. package/dist/speech-check.d.ts.map +1 -0
  49. package/dist/speech-check.js +53 -0
  50. package/dist/speech-check.js.map +1 -0
  51. package/dist/stdout-guard.d.ts +2 -0
  52. package/dist/stdout-guard.d.ts.map +1 -0
  53. package/dist/stdout-guard.js +10 -0
  54. package/dist/stdout-guard.js.map +1 -0
  55. package/dist/stt.d.ts +34 -0
  56. package/dist/stt.d.ts.map +1 -0
  57. package/dist/stt.js +89 -0
  58. package/dist/stt.js.map +1 -0
  59. package/dist/turn.d.ts +31 -0
  60. package/dist/turn.d.ts.map +1 -0
  61. package/dist/turn.js +62 -0
  62. package/dist/turn.js.map +1 -0
  63. package/dist/vad.d.ts +53 -0
  64. package/dist/vad.d.ts.map +1 -0
  65. package/dist/vad.js +119 -0
  66. package/dist/vad.js.map +1 -0
  67. package/package.json +31 -4
package/dist/ear.d.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { type Log } from './log.js';
2
+ import type { OpenMic } from './mic.js';
3
+ import type { SpeechCheck } from './speech-check.js';
4
+ import type { Transcriber } from './stt.js';
5
+ import { type TurnCheck } from './turn.js';
6
+ import { type VadOptions } from './vad.js';
7
+ export declare const MAX_WAIT_MS = 60000;
8
+ /**
9
+ * What the AI said may come back through the microphone (echo) up to this long after it finished saying it.
10
+ */
11
+ export declare const ECHO_WINDOW_MS = 10000;
12
+ /** A sentence is echo if at least this share of its words came from what the AI said… */
13
+ export declare const ECHO_OVERLAP = 0.7;
14
+ /** …and it has at least this many words (a user's "yes" or "ok" is never echo). */
15
+ export declare const ECHO_MIN_WORDS = 3;
16
+ /**
17
+ * If the AI does not call listen again within this time, the conversation is over and the microphone closes.
18
+ */
19
+ export declare const IDLE_CLOSE_MS = 30000;
20
+ /** A "sentence" with less voice energy than this is a knock or a noise and is not transcribed. */
21
+ export declare const MIN_VOICED_MS = 300;
22
+ /**
23
+ * With something already understood, listen waits at most this long for the speaker to stop (it may be a TV).
24
+ */
25
+ export declare const SETTLE_MS = 3000;
26
+ /**
27
+ * At every 0.3 s pause (10 blocks) Smart Turn is asked whether the user has finished. If not, the 1.5 s limit
28
+ * applies.
29
+ */
30
+ export declare const PAUSE_FRAMES = 10;
31
+ /**
32
+ * Smart Turn only closes a sentence early when it has at least this much voice, so it does not split a short
33
+ * question in pieces.
34
+ */
35
+ export declare const MIN_TURN_VOICED_MS = 1000;
36
+ /**
37
+ * A sentence that started while the AI was talking and is not its echo only cuts the AI off with at least
38
+ * this much voice. Echo leftovers are chunks with little voice (330 ms in 3 s) that Whisper sometimes
39
+ * transcribes as other words; a user saying "stop, stop" had 750 ms.
40
+ */
41
+ export declare const MIN_INTERRUPT_VOICED_MS = 500;
42
+ /**
43
+ * …and, under 3 words, it must be almost all voice. An echo leftover is a few words in a long chunk with
44
+ * little voice (510 ms of voice in 3.4 s, 15 %); a single "stop" from the user is almost all voice.
45
+ */
46
+ export declare const MIN_INTERRUPT_VOICE_RATIO = 0.3;
47
+ /**
48
+ * What listen returns when the user cut the AI off, so the AI knows the user did not hear everything it said.
49
+ */
50
+ export declare const INTERRUPTED = "(the user interrupted you while you were speaking)";
51
+ export interface EarOptions {
52
+ openMic: OpenMic;
53
+ transcriber: Transcriber;
54
+ /**
55
+ * Checks that a sentence has human voice before Whisper: over noise, Whisper makes up sentences (from the
56
+ * subtitles it was trained on). Works for any language.
57
+ */
58
+ speechCheck?: SpeechCheck;
59
+ vad?: VadOptions;
60
+ /**
61
+ * Decides whether a pause ends the turn (Smart Turn). With it, the sentence closes after 0.3 s of silence
62
+ * if the user has finished; without it, always after 1.5 s.
63
+ */
64
+ turnCheck?: TurnCheck;
65
+ /** A user sentence just closed while the AI listens, to let the user know they were heard. */
66
+ onPhrase?: () => void;
67
+ /** The user started talking while the AI talks: it must go quiet. */
68
+ onBargeIn?: () => void;
69
+ log?: Log;
70
+ }
71
+ /**
72
+ * Continuous listening. The microphone stays open from the first listen, also while the AI talks, so the user
73
+ * can answer or interrupt at once. Each long pause closes a sentence, which is transcribed and stored; listen
74
+ * returns it whole. Whatever matches what the AI just said is its own echo from the speakers and is
75
+ * discarded.
76
+ */
77
+ export declare class Ear {
78
+ private readonly options;
79
+ private mic;
80
+ private vad;
81
+ private leftover;
82
+ /**
83
+ * Understood text not collected by listen yet. `fromUser`: it started while the AI was quiet, so it is not
84
+ * an echo leftover.
85
+ */
86
+ private heard;
87
+ private failure;
88
+ private waiters;
89
+ private transcribing;
90
+ private pending;
91
+ private speaking;
92
+ private said;
93
+ /** Bumped on close, so transcriptions in progress are discarded. */
94
+ private generation;
95
+ private idleTimer;
96
+ private heardAt;
97
+ /** Bumped on every interrupt, so the waiting listen returns. */
98
+ private interrupts;
99
+ /** Bumped on every closed sentence, so a Smart Turn answer for a previous sentence is ignored. */
100
+ private utteranceId;
101
+ /** Smart Turn is deciding: it is not asked again, since each pass delays Silero and Whisper. */
102
+ private checkingTurn;
103
+ private gate;
104
+ /** The current sentence started while the AI was talking, so it is almost always its echo. */
105
+ private startedWithAI;
106
+ /** The user cut the AI off; the AI is told on the next listen. */
107
+ private interruptedAI;
108
+ constructor(options: EarOptions);
109
+ /**
110
+ * Everything the user said since the last call, whole: waits for them to stop talking and for the rest to be
111
+ * transcribed. It never gets stuck with endless background voice (a TV, a video): with something already understood,
112
+ * it returns at most SETTLE_MS later, and after maxWaitMs it returns whatever there is (or nothing). Anything still
113
+ * playing is not lost: it goes to the next listen.
114
+ *
115
+ * If another tool interrupts it, it returns without consuming what was heard, which is kept for the next listen. If
116
+ * the AI does not listen again soon, the conversation is over and the microphone closes. The AI is told it was cut off
117
+ * once, even if what cut it off never became a sentence.
118
+ */
119
+ next(maxWaitMs?: number): Promise<string>;
120
+ /**
121
+ * A 30 ms chunk about to play from the speaker (its level and when it plays). `first` starts a sentence.
122
+ */
123
+ playing(level: number, at: number, first?: boolean): void;
124
+ /**
125
+ * The AI is about to say this text. Returns the function to call when it finishes: it closes the current
126
+ * sentence, which is its echo from the speakers, so it does not merge with what the user says next (if they
127
+ * merged, the echo filter would also discard the user's words).
128
+ */
129
+ saying(text: string): () => void;
130
+ /**
131
+ * Another tool arrives (speak, stop or another listen): the waiting listen returns now, without consuming
132
+ * what was heard. This way the AI can speak even if an earlier listen is still waiting.
133
+ */
134
+ interrupt(): void;
135
+ /** Ends the conversation: closes the microphone and clears everything pending. */
136
+ stop(): void;
137
+ /** If the microphone closes by itself, the next listen opens it again. */
138
+ private open;
139
+ /**
140
+ * No noise is learned while the AI talks: after a minute of reading, its echo passed for noise and the user's "stop"
141
+ * no longer crossed the threshold.
142
+ */
143
+ private onAudio;
144
+ /**
145
+ * The user talks over the AI: it goes quiet, and the user's sentence starts here (what came before was the AI's
146
+ * voice, which the echo filter would discard along with the user's words).
147
+ */
148
+ private bargeIn;
149
+ /**
150
+ * 0.3 s pause: Smart Turn tells whether the user has finished. If so and they are still quiet, the sentence closes
151
+ * now instead of after 1.5 s. If they started talking again meanwhile, the answer is ignored. While the AI talks,
152
+ * what is heard is almost always its echo: the sentence closes when it stops talking.
153
+ */
154
+ private checkTurn;
155
+ /** Closes the current sentence and prepares the next one; the detector keeps the room noise level. */
156
+ private endUtterance;
157
+ private log;
158
+ private aiSpeaking;
159
+ /**
160
+ * Sentences are transcribed in order, one after another. A sentence that started while the AI was talking is almost
161
+ * always its echo, and is dropped if nearly all its words are the AI's, however short. If it is not the echo, the user
162
+ * is talking over the AI (their "stop" was not in what the AI said): this is the second way to cut the AI off, in case
163
+ * the microphone level check did not fire.
164
+ */
165
+ private transcribe;
166
+ /**
167
+ * With fewer than `minWords` words it is never echo (a user's "yes"); 1 for what started while the AI was
168
+ * talking.
169
+ */
170
+ private isEcho;
171
+ private wake;
172
+ }
173
+ //# sourceMappingURL=ear.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ear.d.ts","sourceRoot":"","sources":["../src/ear.ts"],"names":[],"mappings":"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"}
package/dist/ear.js ADDED
@@ -0,0 +1,388 @@
1
+ import { silent } from './log.js';
2
+ import { EchoGate } from './barge-in.js';
3
+ import { TURN_COMPLETE } from './turn.js';
4
+ import { FRAME_SAMPLES, VoiceDetector } from './vad.js';
5
+ export const MAX_WAIT_MS = 60_000;
6
+ /**
7
+ * What the AI said may come back through the microphone (echo) up to this long after it finished saying it.
8
+ */
9
+ export const ECHO_WINDOW_MS = 10_000;
10
+ /** A sentence is echo if at least this share of its words came from what the AI said… */
11
+ export const ECHO_OVERLAP = 0.7;
12
+ /** …and it has at least this many words (a user's "yes" or "ok" is never echo). */
13
+ export const ECHO_MIN_WORDS = 3;
14
+ /**
15
+ * If the AI does not call listen again within this time, the conversation is over and the microphone closes.
16
+ */
17
+ export const IDLE_CLOSE_MS = 30_000;
18
+ /** A "sentence" with less voice energy than this is a knock or a noise and is not transcribed. */
19
+ export const MIN_VOICED_MS = 300;
20
+ /**
21
+ * With something already understood, listen waits at most this long for the speaker to stop (it may be a TV).
22
+ */
23
+ export const SETTLE_MS = 3_000;
24
+ /**
25
+ * At every 0.3 s pause (10 blocks) Smart Turn is asked whether the user has finished. If not, the 1.5 s limit
26
+ * applies.
27
+ */
28
+ export const PAUSE_FRAMES = 10;
29
+ /**
30
+ * Smart Turn only closes a sentence early when it has at least this much voice, so it does not split a short
31
+ * question in pieces.
32
+ */
33
+ export const MIN_TURN_VOICED_MS = 1_000;
34
+ /**
35
+ * When the user cuts the AI off, their sentence starts this much earlier (17 blocks, 0.5 s); what came before
36
+ * was the AI.
37
+ */
38
+ const BARGE_KEEP_FRAMES = 17;
39
+ /**
40
+ * A sentence that started while the AI was talking and is not its echo only cuts the AI off with at least
41
+ * this much voice. Echo leftovers are chunks with little voice (330 ms in 3 s) that Whisper sometimes
42
+ * transcribes as other words; a user saying "stop, stop" had 750 ms.
43
+ */
44
+ export const MIN_INTERRUPT_VOICED_MS = 500;
45
+ /**
46
+ * …and, under 3 words, it must be almost all voice. An echo leftover is a few words in a long chunk with
47
+ * little voice (510 ms of voice in 3.4 s, 15 %); a single "stop" from the user is almost all voice.
48
+ */
49
+ export const MIN_INTERRUPT_VOICE_RATIO = 0.3;
50
+ /**
51
+ * Whether it is a leftover of the AI's echo, even if Whisper transcribed it with words the AI did not say.
52
+ */
53
+ function echoLeftover(text, voicedMs, audioMs) {
54
+ const words = (text.match(/\p{L}+/gu) ?? []).length;
55
+ return voicedMs < MIN_INTERRUPT_VOICED_MS || (words < 3 && voicedMs / audioMs < MIN_INTERRUPT_VOICE_RATIO);
56
+ }
57
+ /**
58
+ * What listen returns when the user cut the AI off, so the AI knows the user did not hear everything it said.
59
+ */
60
+ export const INTERRUPTED = '(the user interrupted you while you were speaking)';
61
+ const rms = (frame) => {
62
+ let sum = 0;
63
+ for (const sample of frame)
64
+ sum += sample * sample;
65
+ return Math.sqrt(sum / frame.length);
66
+ };
67
+ /** Whisper sometimes makes up an endlessly repeated word over noise or music ("yes, yes, yes…"). */
68
+ function isRepetition(text) {
69
+ const all = text.toLowerCase().match(/\p{L}+/gu) ?? [];
70
+ return all.length >= 8 && new Set(all).size / all.length < 0.25;
71
+ }
72
+ /** Words without accents or capitals, to compare what was heard with what was said. */
73
+ function words(text) {
74
+ const plain = text.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
75
+ return new Set(plain.match(/[a-z0-9ñ]+/g) ?? []);
76
+ }
77
+ /**
78
+ * Continuous listening. The microphone stays open from the first listen, also while the AI talks, so the user
79
+ * can answer or interrupt at once. Each long pause closes a sentence, which is transcribed and stored; listen
80
+ * returns it whole. Whatever matches what the AI just said is its own echo from the speakers and is
81
+ * discarded.
82
+ */
83
+ export class Ear {
84
+ options;
85
+ mic;
86
+ vad;
87
+ leftover = Buffer.alloc(0);
88
+ /**
89
+ * Understood text not collected by listen yet. `fromUser`: it started while the AI was quiet, so it is not
90
+ * an echo leftover.
91
+ */
92
+ heard = [];
93
+ failure;
94
+ waiters = [];
95
+ transcribing = Promise.resolve();
96
+ pending = 0;
97
+ speaking = false;
98
+ said = [];
99
+ /** Bumped on close, so transcriptions in progress are discarded. */
100
+ generation = 0;
101
+ idleTimer;
102
+ heardAt = 0;
103
+ /** Bumped on every interrupt, so the waiting listen returns. */
104
+ interrupts = 0;
105
+ /** Bumped on every closed sentence, so a Smart Turn answer for a previous sentence is ignored. */
106
+ utteranceId = 0;
107
+ /** Smart Turn is deciding: it is not asked again, since each pass delays Silero and Whisper. */
108
+ checkingTurn = false;
109
+ gate = new EchoGate();
110
+ /** The current sentence started while the AI was talking, so it is almost always its echo. */
111
+ startedWithAI = false;
112
+ /** The user cut the AI off; the AI is told on the next listen. */
113
+ interruptedAI = false;
114
+ constructor(options) {
115
+ this.options = options;
116
+ this.vad = new VoiceDetector(options.vad);
117
+ }
118
+ /**
119
+ * Everything the user said since the last call, whole: waits for them to stop talking and for the rest to be
120
+ * transcribed. It never gets stuck with endless background voice (a TV, a video): with something already understood,
121
+ * it returns at most SETTLE_MS later, and after maxWaitMs it returns whatever there is (or nothing). Anything still
122
+ * playing is not lost: it goes to the next listen.
123
+ *
124
+ * If another tool interrupts it, it returns without consuming what was heard, which is kept for the next listen. If
125
+ * the AI does not listen again soon, the conversation is over and the microphone closes. The AI is told it was cut off
126
+ * once, even if what cut it off never became a sentence.
127
+ */
128
+ async next(maxWaitMs = MAX_WAIT_MS) {
129
+ clearTimeout(this.idleTimer);
130
+ this.open();
131
+ const interrupts = this.interrupts;
132
+ let timedOut = false;
133
+ const timer = setTimeout(() => {
134
+ timedOut = true;
135
+ this.wake();
136
+ }, maxWaitMs);
137
+ let settleTimer;
138
+ const busy = () => this.speaking || this.pending > 0;
139
+ while (!this.failure && !timedOut && interrupts === this.interrupts) {
140
+ if (this.heard.length) {
141
+ const left = this.heardAt + SETTLE_MS - Date.now();
142
+ if (!busy() || left <= 0)
143
+ break;
144
+ settleTimer ??= setTimeout(() => this.wake(), left);
145
+ }
146
+ await new Promise((resolve) => this.waiters.push(resolve));
147
+ }
148
+ clearTimeout(timer);
149
+ clearTimeout(settleTimer);
150
+ if (interrupts !== this.interrupts)
151
+ return '';
152
+ this.idleTimer = setTimeout(() => this.stop(), IDLE_CLOSE_MS);
153
+ if (this.failure) {
154
+ const reason = this.failure;
155
+ this.failure = undefined;
156
+ throw new Error(reason);
157
+ }
158
+ let text = this.heard.map((h) => h.text).join(' ');
159
+ this.heard = [];
160
+ if (text && this.interruptedAI)
161
+ text = `${INTERRUPTED} ${text}`;
162
+ this.interruptedAI = false;
163
+ return text;
164
+ }
165
+ /**
166
+ * A 30 ms chunk about to play from the speaker (its level and when it plays). `first` starts a sentence.
167
+ */
168
+ playing(level, at, first = false) {
169
+ this.gate.play(level, at, first);
170
+ }
171
+ /**
172
+ * The AI is about to say this text. Returns the function to call when it finishes: it closes the current
173
+ * sentence, which is its echo from the speakers, so it does not merge with what the user says next (if they
174
+ * merged, the echo filter would also discard the user's words).
175
+ */
176
+ saying(text) {
177
+ const said = { words: words(text), until: Infinity };
178
+ this.said.push(said);
179
+ return () => {
180
+ said.until = Math.max(Number.isFinite(said.until) ? said.until : 0, Date.now() + ECHO_WINDOW_MS);
181
+ if (!said.cut && this.vad.current === 'speaking')
182
+ this.endUtterance();
183
+ };
184
+ }
185
+ /**
186
+ * Another tool arrives (speak, stop or another listen): the waiting listen returns now, without consuming
187
+ * what was heard. This way the AI can speak even if an earlier listen is still waiting.
188
+ */
189
+ interrupt() {
190
+ this.interrupts++;
191
+ this.wake();
192
+ }
193
+ /** Ends the conversation: closes the microphone and clears everything pending. */
194
+ stop() {
195
+ clearTimeout(this.idleTimer);
196
+ this.mic?.stop();
197
+ this.mic = undefined;
198
+ this.generation++;
199
+ this.utteranceId++;
200
+ this.gate = new EchoGate();
201
+ this.interruptedAI = false;
202
+ this.heard = [];
203
+ this.said = [];
204
+ this.speaking = false;
205
+ this.leftover = Buffer.alloc(0);
206
+ this.vad = new VoiceDetector(this.options.vad);
207
+ this.wake();
208
+ }
209
+ /** If the microphone closes by itself, the next listen opens it again. */
210
+ open() {
211
+ if (this.mic)
212
+ return;
213
+ const mic = this.options.openMic();
214
+ this.mic = mic;
215
+ mic.onData((chunk) => this.onAudio(chunk));
216
+ mic.onExit((reason) => {
217
+ if (this.mic !== mic)
218
+ return;
219
+ this.mic = undefined;
220
+ this.failure = `Cannot use the microphone: ${reason}`;
221
+ this.wake();
222
+ });
223
+ }
224
+ /**
225
+ * No noise is learned while the AI talks: after a minute of reading, its echo passed for noise and the user's "stop"
226
+ * no longer crossed the threshold.
227
+ */
228
+ onAudio(chunk) {
229
+ if (!this.mic)
230
+ return;
231
+ const bytes = Buffer.concat([this.leftover, chunk]);
232
+ const frameBytes = FRAME_SAMPLES * 2;
233
+ let offset = 0;
234
+ for (; offset + frameBytes <= bytes.length; offset += frameBytes) {
235
+ const frame = new Float32Array(FRAME_SAMPLES);
236
+ for (let i = 0; i < FRAME_SAMPLES; i++)
237
+ frame[i] = bytes.readInt16LE(offset + i * 2) / 32_768;
238
+ if (this.aiSpeaking() && this.gate.mic(rms(frame), Date.now(), this.vad.threshold))
239
+ this.bargeIn();
240
+ const was = this.speaking;
241
+ const state = this.vad.push(frame, !this.aiSpeaking());
242
+ this.speaking = state === 'speaking';
243
+ if (this.speaking && !was)
244
+ this.startedWithAI = this.aiSpeaking();
245
+ if (state === 'done')
246
+ this.endUtterance();
247
+ else if (state === 'speaking' && this.vad.quietFrames === PAUSE_FRAMES)
248
+ this.checkTurn();
249
+ }
250
+ this.leftover = bytes.subarray(offset);
251
+ }
252
+ /**
253
+ * The user talks over the AI: it goes quiet, and the user's sentence starts here (what came before was the AI's
254
+ * voice, which the echo filter would discard along with the user's words).
255
+ */
256
+ bargeIn() {
257
+ this.log('the user is talking: cutting the AI off');
258
+ for (const said of this.said) {
259
+ if (said.until !== Infinity)
260
+ continue;
261
+ said.cut = true;
262
+ said.until = Date.now() + ECHO_WINDOW_MS;
263
+ }
264
+ this.interruptedAI = true;
265
+ this.startedWithAI = false;
266
+ if (this.vad.current === 'speaking')
267
+ this.vad.keepLast(BARGE_KEEP_FRAMES);
268
+ this.options.onBargeIn?.();
269
+ }
270
+ /**
271
+ * 0.3 s pause: Smart Turn tells whether the user has finished. If so and they are still quiet, the sentence closes
272
+ * now instead of after 1.5 s. If they started talking again meanwhile, the answer is ignored. While the AI talks,
273
+ * what is heard is almost always its echo: the sentence closes when it stops talking.
274
+ */
275
+ checkTurn() {
276
+ const turnCheck = this.options.turnCheck;
277
+ if (!turnCheck || this.checkingTurn || this.vad.voicedMs < MIN_TURN_VOICED_MS || this.aiSpeaking())
278
+ return;
279
+ this.checkingTurn = true;
280
+ const id = this.utteranceId;
281
+ const started = Date.now();
282
+ turnCheck.complete(this.vad.utterance()).then((probability) => {
283
+ const still = id === this.utteranceId && this.vad.current === 'speaking' && this.vad.quietFrames >= PAUSE_FRAMES;
284
+ const verdict = probability > TURN_COMPLETE ? 'finished' : 'just a pause';
285
+ this.log(`Smart Turn: ${verdict} (${probability.toFixed(2)}, ${Date.now() - started} ms)${still ? '' : ' · outdated'}`);
286
+ if (still && probability > TURN_COMPLETE)
287
+ this.endUtterance();
288
+ }, (error) => this.log(`Smart Turn failed: ${error.message}`)).finally(() => (this.checkingTurn = false));
289
+ }
290
+ /** Closes the current sentence and prepares the next one; the detector keeps the room noise level. */
291
+ endUtterance() {
292
+ const audio = this.vad.utterance();
293
+ const voiced = this.vad.voicedMs;
294
+ const fromUser = !this.startedWithAI;
295
+ this.vad.next();
296
+ this.utteranceId++;
297
+ this.speaking = false;
298
+ const ms = Math.round((audio.length / 16_000) * 1000);
299
+ if (voiced >= MIN_VOICED_MS) {
300
+ this.log(`sentence closed: ${ms} ms of audio, ${voiced} ms of voice${this.aiSpeaking() ? ' (while the AI was talking)' : ''}`);
301
+ if (this.waiters.length && !this.aiSpeaking())
302
+ this.options.onPhrase?.();
303
+ this.transcribe(audio, fromUser, voiced, ms);
304
+ }
305
+ else {
306
+ this.log(`stray noise dropped: ${ms} ms, ${voiced} ms of voice`);
307
+ this.wake();
308
+ }
309
+ }
310
+ log(message) {
311
+ (this.options.log ?? silent)(`[ear] ${message}`);
312
+ }
313
+ aiSpeaking() {
314
+ return this.said.some((said) => said.until === Infinity);
315
+ }
316
+ /**
317
+ * Sentences are transcribed in order, one after another. A sentence that started while the AI was talking is almost
318
+ * always its echo, and is dropped if nearly all its words are the AI's, however short. If it is not the echo, the user
319
+ * is talking over the AI (their "stop" was not in what the AI said): this is the second way to cut the AI off, in case
320
+ * the microphone level check did not fire.
321
+ */
322
+ transcribe(audio, fromUser, voiced = Infinity, audioMs = Infinity) {
323
+ const generation = this.generation;
324
+ this.pending++;
325
+ this.transcribing = this.transcribing.then(async () => {
326
+ try {
327
+ if (this.options.speechCheck && !(await this.options.speechCheck.hasSpeech(audio))) {
328
+ this.log('Silero: not human voice → not transcribed');
329
+ return;
330
+ }
331
+ const text = (await this.options.transcriber.transcribe(audio)).trim();
332
+ if (generation !== this.generation)
333
+ return this.log(`dropped (the microphone closed meanwhile): «${text}»`);
334
+ if (!text)
335
+ return this.log('Whisper returned no text');
336
+ if (this.isEcho(text, fromUser ? ECHO_MIN_WORDS : 1))
337
+ return this.log(`AI echo dropped: «${text}»`);
338
+ if (isRepetition(text))
339
+ return this.log(`endless repetition dropped: «${text}»`);
340
+ if (!fromUser && echoLeftover(text, voiced, audioMs))
341
+ return this.log(`AI echo leftover dropped (${voiced} ms of voice in ${audioMs} ms): «${text}»`);
342
+ this.log(`heard: «${text}»`);
343
+ if (!fromUser) {
344
+ this.log('the user talked over the AI (by what they said)');
345
+ if (this.aiSpeaking())
346
+ this.bargeIn();
347
+ else
348
+ this.interruptedAI = true;
349
+ }
350
+ if (!this.heard.length)
351
+ this.heardAt = Date.now();
352
+ this.heard.push({ text, fromUser: true });
353
+ }
354
+ catch (error) {
355
+ this.failure = `Could not transcribe the sentence: ${error instanceof Error ? error.message : String(error)}`;
356
+ }
357
+ finally {
358
+ this.pending--;
359
+ this.wake();
360
+ }
361
+ });
362
+ }
363
+ /**
364
+ * With fewer than `minWords` words it is never echo (a user's "yes"); 1 for what started while the AI was
365
+ * talking.
366
+ */
367
+ isEcho(text, minWords = ECHO_MIN_WORDS) {
368
+ const now = Date.now();
369
+ this.said = this.said.filter((said) => said.until > now);
370
+ const heard = words(text);
371
+ if (heard.size < minWords)
372
+ return false;
373
+ return this.said.some((said) => {
374
+ let shared = 0;
375
+ for (const word of heard)
376
+ if (said.words.has(word))
377
+ shared++;
378
+ return shared / heard.size >= ECHO_OVERLAP;
379
+ });
380
+ }
381
+ wake() {
382
+ const waiters = this.waiters;
383
+ this.waiters = [];
384
+ for (const waiter of waiters)
385
+ waiter();
386
+ }
387
+ }
388
+ //# sourceMappingURL=ear.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ear.js","sourceRoot":"","sources":["../src/ear.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Logs what happens to each sentence. Without --verbose it logs nothing. */
2
+ export type Log = (message: string) => void;
3
+ export declare const silent: Log;
4
+ /** To stderr (stdout is the MCP channel) and to the file, with the time. */
5
+ export declare function fileLog(file: string): Log;
6
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAGA,6EAA6E;AAC7E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5C,eAAO,MAAM,MAAM,EAAE,GAAc,CAAC;AAEpC,4EAA4E;AAC5E,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAOzC"}
package/dist/log.js ADDED
@@ -0,0 +1,13 @@
1
+ import { appendFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ export const silent = () => { };
4
+ /** To stderr (stdout is the MCP channel) and to the file, with the time. */
5
+ export function fileLog(file) {
6
+ mkdirSync(dirname(file), { recursive: true });
7
+ return (message) => {
8
+ const line = `${new Date().toISOString().slice(11, 23)} ${message}`;
9
+ console.error(line);
10
+ appendFileSync(file, line + '\n');
11
+ };
12
+ }
13
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,MAAM,CAAC,MAAM,MAAM,GAAQ,GAAG,EAAE,GAAE,CAAC,CAAC;AAEpC,4EAA4E;AAC5E,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC;QACpE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC,CAAC;AACJ,CAAC"}
package/dist/mic.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /** The sample rate Whisper expects; the microphone is recorded at it directly. */
2
+ export declare const SAMPLE_RATE = 16000;
3
+ /** The open microphone: 16-bit PCM, mono, 16 kHz. */
4
+ export interface Mic {
5
+ onData(listener: (chunk: Buffer) => void): void;
6
+ onExit(listener: (reason: string) => void): void;
7
+ stop(): void;
8
+ }
9
+ export type OpenMic = () => Mic;
10
+ /** How ffmpeg opens the microphone on each operating system. */
11
+ export declare function inputArgs(platform: NodeJS.Platform, device: string | undefined): string[];
12
+ /** On Windows the microphone must be named: the first one ffmpeg lists. */
13
+ export declare function findWindowsMic(ffmpeg: string): string | undefined;
14
+ /** The computer microphone through ffmpeg. It is only open while listening. */
15
+ export declare function createFfmpegMic({ ffmpeg, device }: {
16
+ ffmpeg: string;
17
+ device?: string;
18
+ }): OpenMic;
19
+ //# sourceMappingURL=mic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mic.d.ts","sourceRoot":"","sources":["../src/mic.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,eAAO,MAAM,WAAW,QAAS,CAAC;AAElC,qDAAqD;AACrD,MAAM,WAAW,GAAG;IAClB,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAChD,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IACjD,IAAI,IAAI,IAAI,CAAC;CACd;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC;AAEhC,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAIzF;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAwBhG"}