@kuralle-syrinx/core 4.2.0 → 4.4.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/package.json +2 -2
- package/src/audio/alaw.ts +56 -0
- package/src/audio/g722.ts +329 -0
- package/src/audio/index.ts +12 -0
- package/src/audio/loudness.ts +87 -0
- package/src/idle-timeout.ts +1 -0
- package/src/index.ts +65 -3
- package/src/interaction-coordinator.ts +17 -2
- package/src/interaction-policy.ts +12 -0
- package/src/observability-observer.ts +8 -4
- package/src/observability.ts +30 -1
- package/src/packet-factories.ts +124 -3
- package/src/packets.ts +139 -1
- package/src/plugin-contract.ts +41 -0
- package/src/policies/rule-based.ts +17 -2
- package/src/pricing.ts +137 -0
- package/src/primary-speaker-gate.ts +23 -3
- package/src/reasoner.ts +28 -1
- package/src/spend-cap.ts +52 -0
- package/src/turn-arbiter.ts +34 -2
- package/src/voice-agent-session-util.ts +18 -0
- package/src/voice-agent-session.ts +469 -36
- package/src/voice-text.ts +69 -0
- package/src/audio/audio.test.ts +0 -285
- package/src/audio-envelope.test.ts +0 -167
- package/src/confidence-to-wait.test.ts +0 -21
- package/src/error-handler.test.ts +0 -56
- package/src/hedge-throwing-backend.test.ts +0 -46
- package/src/interaction-coordinator.test.ts +0 -425
- package/src/iu-ledger.test.ts +0 -276
- package/src/latency-filler.test.ts +0 -62
- package/src/observability-observer.test.ts +0 -245
- package/src/observability.test.ts +0 -85
- package/src/packet-factories.test.ts +0 -53
- package/src/pipeline-bus.g10.test.ts +0 -145
- package/src/pipeline-bus.test.ts +0 -211
- package/src/policies/defer.test.ts +0 -86
- package/src/policies/rule-based.test.ts +0 -269
- package/src/primary-speaker-gate.test.ts +0 -150
- package/src/provider-fallback.test.ts +0 -87
- package/src/reasoner-hedge.test.ts +0 -361
- package/src/reasoner-route.test.ts +0 -248
- package/src/reasoner.test.ts +0 -69
- package/src/retry.test.ts +0 -83
- package/src/route-throwing-spec.test.ts +0 -44
- package/src/tts-playout-clock.test.ts +0 -125
- package/src/turn-arbiter.characterization.test.ts +0 -477
- package/src/turn-arbiter.test.ts +0 -567
- package/src/voice-agent-session.test.ts +0 -3316
- package/src/voice-text.test.ts +0 -94
- package/tsconfig.json +0 -21
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// CR-09 Stage 0 — pins the implicit barge-in / turn-taking transition table on the
|
|
4
|
-
// session before extraction. Values are observed from driving the real session.
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from "vitest";
|
|
7
|
-
import { VoiceAgentSession } from "./voice-agent-session.js";
|
|
8
|
-
import { Route } from "./index.js";
|
|
9
|
-
import type {
|
|
10
|
-
InterruptTtsPacket,
|
|
11
|
-
TextToSpeechAudioPacket,
|
|
12
|
-
TextToSpeechEndPacket,
|
|
13
|
-
VadAudioPacket,
|
|
14
|
-
VadSpeechActivityPacket,
|
|
15
|
-
VadSpeechEndedPacket,
|
|
16
|
-
VadSpeechStartedPacket,
|
|
17
|
-
} from "./packets.js";
|
|
18
|
-
import {
|
|
19
|
-
BYSTANDER_SPEAKER_TONE_HZ,
|
|
20
|
-
PRIMARY_SPEAKER_TONE_HZ,
|
|
21
|
-
synthesizeTonePcm16,
|
|
22
|
-
} from "./primary-speaker-fixtures.js";
|
|
23
|
-
|
|
24
|
-
async function closeSession(session: VoiceAgentSession): Promise<void> {
|
|
25
|
-
if (session.state !== "closed") {
|
|
26
|
-
await session.close();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function enrollPrimarySpeaker(
|
|
31
|
-
session: VoiceAgentSession,
|
|
32
|
-
contextId = "user-enroll",
|
|
33
|
-
): Promise<void> {
|
|
34
|
-
const chunk = synthesizeTonePcm16({
|
|
35
|
-
frequencyHz: PRIMARY_SPEAKER_TONE_HZ,
|
|
36
|
-
durationMs: 32,
|
|
37
|
-
});
|
|
38
|
-
const t0 = Date.now();
|
|
39
|
-
session.bus.push(Route.Main, {
|
|
40
|
-
kind: "vad.speech_started",
|
|
41
|
-
contextId,
|
|
42
|
-
timestampMs: t0,
|
|
43
|
-
confidence: 0.99,
|
|
44
|
-
} satisfies VadSpeechStartedPacket);
|
|
45
|
-
for (let i = 0; i < 12; i += 1) {
|
|
46
|
-
session.bus.push(Route.Main, {
|
|
47
|
-
kind: "user.audio_received",
|
|
48
|
-
contextId,
|
|
49
|
-
timestampMs: t0 + i * 20,
|
|
50
|
-
audio: chunk,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
session.bus.push(Route.Main, {
|
|
54
|
-
kind: "vad.speech_ended",
|
|
55
|
-
contextId,
|
|
56
|
-
timestampMs: t0 + 300,
|
|
57
|
-
} satisfies VadSpeechEndedPacket);
|
|
58
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function armAssistantSpeaking(session: VoiceAgentSession, contextId = "assistant-turn"): void {
|
|
62
|
-
session.bus.push(Route.Main, {
|
|
63
|
-
kind: "tts.audio",
|
|
64
|
-
contextId,
|
|
65
|
-
timestampMs: Date.now(),
|
|
66
|
-
audio: new Uint8Array([1, 2, 3, 4]),
|
|
67
|
-
sampleRateHz: 16000,
|
|
68
|
-
} satisfies TextToSpeechAudioPacket);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function collectMetrics(session: VoiceAgentSession): {
|
|
72
|
-
names: string[];
|
|
73
|
-
pairs: Array<{ name: string; value: string }>;
|
|
74
|
-
} {
|
|
75
|
-
const names: string[] = [];
|
|
76
|
-
const pairs: Array<{ name: string; value: string }> = [];
|
|
77
|
-
session.bus.on("metric.conversation", (pkt) => {
|
|
78
|
-
const m = pkt as unknown as { name: string; value: string };
|
|
79
|
-
names.push(m.name);
|
|
80
|
-
pairs.push({ name: m.name, value: m.value });
|
|
81
|
-
});
|
|
82
|
-
return { names, pairs };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
describe("turn-taking transition table (CR-09 characterization)", () => {
|
|
86
|
-
it("suppresses short speech blip via interrupt.suppressed_short_speech", async () => {
|
|
87
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 280 });
|
|
88
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
89
|
-
const { names: metrics } = collectMetrics(session);
|
|
90
|
-
|
|
91
|
-
await session.start();
|
|
92
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
93
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
94
|
-
});
|
|
95
|
-
armAssistantSpeaking(session);
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
97
|
-
|
|
98
|
-
const t0 = 1000;
|
|
99
|
-
session.bus.push(Route.Main, {
|
|
100
|
-
kind: "vad.speech_started",
|
|
101
|
-
contextId: "user",
|
|
102
|
-
timestampMs: t0,
|
|
103
|
-
confidence: 0.99,
|
|
104
|
-
} satisfies VadSpeechStartedPacket);
|
|
105
|
-
session.bus.push(Route.Main, {
|
|
106
|
-
kind: "vad.speech_activity",
|
|
107
|
-
contextId: "user",
|
|
108
|
-
timestampMs: t0 + 90,
|
|
109
|
-
isAsync: true,
|
|
110
|
-
} satisfies VadSpeechActivityPacket);
|
|
111
|
-
session.bus.push(Route.Main, {
|
|
112
|
-
kind: "vad.speech_ended",
|
|
113
|
-
contextId: "user",
|
|
114
|
-
timestampMs: t0 + 130,
|
|
115
|
-
} satisfies VadSpeechEndedPacket);
|
|
116
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
117
|
-
|
|
118
|
-
expect(interrupts).toEqual([]);
|
|
119
|
-
expect(metrics).toContain("interrupt.suppressed_short_speech");
|
|
120
|
-
|
|
121
|
-
await closeSession(session);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("commits sustained speech via interrupt.committed_after_ms and interrupt.detected", async () => {
|
|
125
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 280 });
|
|
126
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
127
|
-
const { names: metrics, pairs } = collectMetrics(session);
|
|
128
|
-
|
|
129
|
-
await session.start();
|
|
130
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
131
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
132
|
-
});
|
|
133
|
-
armAssistantSpeaking(session);
|
|
134
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
135
|
-
|
|
136
|
-
const t0 = 2000;
|
|
137
|
-
session.bus.push(Route.Main, {
|
|
138
|
-
kind: "vad.speech_started",
|
|
139
|
-
contextId: "user",
|
|
140
|
-
timestampMs: t0,
|
|
141
|
-
confidence: 0.99,
|
|
142
|
-
} satisfies VadSpeechStartedPacket);
|
|
143
|
-
session.bus.push(Route.Main, {
|
|
144
|
-
kind: "vad.speech_activity",
|
|
145
|
-
contextId: "user",
|
|
146
|
-
timestampMs: t0 + 300,
|
|
147
|
-
isAsync: true,
|
|
148
|
-
} satisfies VadSpeechActivityPacket);
|
|
149
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
150
|
-
|
|
151
|
-
expect(interrupts).toEqual([
|
|
152
|
-
expect.objectContaining({ kind: "interrupt.tts", contextId: "assistant-turn" }),
|
|
153
|
-
]);
|
|
154
|
-
expect(metrics).toContain("interrupt.committed_after_ms");
|
|
155
|
-
expect(metrics).toContain("vaqi.interruption");
|
|
156
|
-
expect(metrics).toContain("interrupt.latency_ms");
|
|
157
|
-
expect(pairs).toContainEqual({ name: "interrupt.latency_ms", value: "300" });
|
|
158
|
-
|
|
159
|
-
await closeSession(session);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("suppresses non-primary speaker via interrupt.suppressed_non_primary", async () => {
|
|
163
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 280 });
|
|
164
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
165
|
-
const { names: metrics } = collectMetrics(session);
|
|
166
|
-
|
|
167
|
-
await session.start();
|
|
168
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
169
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
170
|
-
});
|
|
171
|
-
await enrollPrimarySpeaker(session);
|
|
172
|
-
armAssistantSpeaking(session);
|
|
173
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
174
|
-
|
|
175
|
-
const bystander = synthesizeTonePcm16({
|
|
176
|
-
frequencyHz: BYSTANDER_SPEAKER_TONE_HZ,
|
|
177
|
-
durationMs: 32,
|
|
178
|
-
});
|
|
179
|
-
const t0 = 3000;
|
|
180
|
-
session.bus.push(Route.Main, {
|
|
181
|
-
kind: "vad.speech_started",
|
|
182
|
-
contextId: "user-barge",
|
|
183
|
-
timestampMs: t0,
|
|
184
|
-
confidence: 0.99,
|
|
185
|
-
} satisfies VadSpeechStartedPacket);
|
|
186
|
-
for (let i = 0; i < 10; i += 1) {
|
|
187
|
-
session.bus.push(Route.Main, {
|
|
188
|
-
kind: "vad.audio",
|
|
189
|
-
contextId: "user-barge",
|
|
190
|
-
timestampMs: t0 + 20 + i * 30,
|
|
191
|
-
audio: bystander,
|
|
192
|
-
} satisfies VadAudioPacket);
|
|
193
|
-
}
|
|
194
|
-
session.bus.push(Route.Main, {
|
|
195
|
-
kind: "vad.speech_activity",
|
|
196
|
-
contextId: "user-barge",
|
|
197
|
-
timestampMs: t0 + 320,
|
|
198
|
-
isAsync: true,
|
|
199
|
-
} satisfies VadSpeechActivityPacket);
|
|
200
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
201
|
-
|
|
202
|
-
expect(interrupts).toEqual([]);
|
|
203
|
-
expect(metrics).toContain("interrupt.suppressed_non_primary");
|
|
204
|
-
|
|
205
|
-
await closeSession(session);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("cuts immediately when minInterruptionMs is 0 and no speaker profile", async () => {
|
|
209
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 0 });
|
|
210
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
211
|
-
const { pairs } = collectMetrics(session);
|
|
212
|
-
|
|
213
|
-
await session.start();
|
|
214
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
215
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
216
|
-
});
|
|
217
|
-
armAssistantSpeaking(session);
|
|
218
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
219
|
-
|
|
220
|
-
session.bus.push(Route.Main, {
|
|
221
|
-
kind: "vad.speech_started",
|
|
222
|
-
contextId: "user",
|
|
223
|
-
timestampMs: 4000,
|
|
224
|
-
confidence: 0.99,
|
|
225
|
-
} satisfies VadSpeechStartedPacket);
|
|
226
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
227
|
-
|
|
228
|
-
expect(interrupts).toEqual([
|
|
229
|
-
expect.objectContaining({ kind: "interrupt.tts", contextId: "assistant-turn" }),
|
|
230
|
-
]);
|
|
231
|
-
expect(pairs).toEqual([
|
|
232
|
-
{ name: "vaqi.interruption", value: "1" },
|
|
233
|
-
{ name: "interrupt.onset_to_logic_cancel_ms", value: expect.stringMatching(/^\d+$/) },
|
|
234
|
-
]);
|
|
235
|
-
|
|
236
|
-
await closeSession(session);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("resolves without cut when playout ended via interrupt.gate_resolved_after_tts_end", async () => {
|
|
240
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 280 });
|
|
241
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
242
|
-
const { names: metrics } = collectMetrics(session);
|
|
243
|
-
|
|
244
|
-
await session.start();
|
|
245
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
246
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
247
|
-
});
|
|
248
|
-
armAssistantSpeaking(session);
|
|
249
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
250
|
-
|
|
251
|
-
const t0 = 5000;
|
|
252
|
-
session.bus.push(Route.Main, {
|
|
253
|
-
kind: "vad.speech_started",
|
|
254
|
-
contextId: "user",
|
|
255
|
-
timestampMs: t0,
|
|
256
|
-
confidence: 0.99,
|
|
257
|
-
} satisfies VadSpeechStartedPacket);
|
|
258
|
-
session.bus.push(Route.Main, {
|
|
259
|
-
kind: "tts.end",
|
|
260
|
-
contextId: "assistant-turn",
|
|
261
|
-
timestampMs: t0 + 50,
|
|
262
|
-
} satisfies TextToSpeechEndPacket);
|
|
263
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
264
|
-
session.bus.push(Route.Main, {
|
|
265
|
-
kind: "vad.speech_activity",
|
|
266
|
-
contextId: "user",
|
|
267
|
-
timestampMs: t0 + 300,
|
|
268
|
-
isAsync: true,
|
|
269
|
-
} satisfies VadSpeechActivityPacket);
|
|
270
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
271
|
-
|
|
272
|
-
expect(interrupts).toEqual([]);
|
|
273
|
-
expect(metrics).toContain("interrupt.gate_resolved_after_tts_end");
|
|
274
|
-
|
|
275
|
-
await closeSession(session);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("defers immediate cut until vad.audio when speaker profile is enrolled", async () => {
|
|
279
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 0 });
|
|
280
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
281
|
-
const { names: metrics } = collectMetrics(session);
|
|
282
|
-
|
|
283
|
-
await session.start();
|
|
284
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
285
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
286
|
-
});
|
|
287
|
-
await enrollPrimarySpeaker(session);
|
|
288
|
-
armAssistantSpeaking(session);
|
|
289
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
290
|
-
|
|
291
|
-
const primary = synthesizeTonePcm16({
|
|
292
|
-
frequencyHz: PRIMARY_SPEAKER_TONE_HZ,
|
|
293
|
-
durationMs: 32,
|
|
294
|
-
});
|
|
295
|
-
const t0 = 6000;
|
|
296
|
-
session.bus.push(Route.Main, {
|
|
297
|
-
kind: "vad.speech_started",
|
|
298
|
-
contextId: "user-barge",
|
|
299
|
-
timestampMs: t0,
|
|
300
|
-
confidence: 0.99,
|
|
301
|
-
} satisfies VadSpeechStartedPacket);
|
|
302
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
303
|
-
|
|
304
|
-
expect(interrupts).toEqual([]);
|
|
305
|
-
expect(metrics).not.toContain("vaqi.interruption");
|
|
306
|
-
|
|
307
|
-
session.bus.push(Route.Main, {
|
|
308
|
-
kind: "vad.audio",
|
|
309
|
-
contextId: "user-barge",
|
|
310
|
-
timestampMs: t0 + 5,
|
|
311
|
-
audio: primary,
|
|
312
|
-
} satisfies VadAudioPacket);
|
|
313
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
314
|
-
|
|
315
|
-
expect(interrupts).toEqual([
|
|
316
|
-
expect.objectContaining({ kind: "interrupt.tts", contextId: "assistant-turn" }),
|
|
317
|
-
]);
|
|
318
|
-
expect(metrics).toContain("vaqi.interruption");
|
|
319
|
-
|
|
320
|
-
await closeSession(session);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("locks primary speaker profile on idle speech end", async () => {
|
|
324
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 280 });
|
|
325
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
326
|
-
const { names: metrics } = collectMetrics(session);
|
|
327
|
-
|
|
328
|
-
await session.start();
|
|
329
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
330
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
const enrollChunk = synthesizeTonePcm16({
|
|
334
|
-
frequencyHz: PRIMARY_SPEAKER_TONE_HZ,
|
|
335
|
-
durationMs: 32,
|
|
336
|
-
});
|
|
337
|
-
const t0 = 7000;
|
|
338
|
-
session.bus.push(Route.Main, {
|
|
339
|
-
kind: "vad.speech_started",
|
|
340
|
-
contextId: "user-first",
|
|
341
|
-
timestampMs: t0,
|
|
342
|
-
confidence: 0.99,
|
|
343
|
-
} satisfies VadSpeechStartedPacket);
|
|
344
|
-
for (let i = 0; i < 8; i += 1) {
|
|
345
|
-
session.bus.push(Route.Main, {
|
|
346
|
-
kind: "user.audio_received",
|
|
347
|
-
contextId: "user-first",
|
|
348
|
-
timestampMs: t0 + i * 20,
|
|
349
|
-
audio: enrollChunk,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
session.bus.push(Route.Main, {
|
|
353
|
-
kind: "vad.speech_ended",
|
|
354
|
-
contextId: "user-first",
|
|
355
|
-
timestampMs: t0 + 200,
|
|
356
|
-
} satisfies VadSpeechEndedPacket);
|
|
357
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
358
|
-
|
|
359
|
-
armAssistantSpeaking(session);
|
|
360
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
361
|
-
|
|
362
|
-
const bystander = synthesizeTonePcm16({
|
|
363
|
-
frequencyHz: BYSTANDER_SPEAKER_TONE_HZ,
|
|
364
|
-
durationMs: 32,
|
|
365
|
-
});
|
|
366
|
-
const t1 = 8000;
|
|
367
|
-
session.bus.push(Route.Main, {
|
|
368
|
-
kind: "vad.speech_started",
|
|
369
|
-
contextId: "user-barge",
|
|
370
|
-
timestampMs: t1,
|
|
371
|
-
confidence: 0.99,
|
|
372
|
-
} satisfies VadSpeechStartedPacket);
|
|
373
|
-
for (let i = 0; i < 10; i += 1) {
|
|
374
|
-
session.bus.push(Route.Main, {
|
|
375
|
-
kind: "vad.audio",
|
|
376
|
-
contextId: "user-barge",
|
|
377
|
-
timestampMs: t1 + 20 + i * 30,
|
|
378
|
-
audio: bystander,
|
|
379
|
-
} satisfies VadAudioPacket);
|
|
380
|
-
}
|
|
381
|
-
session.bus.push(Route.Main, {
|
|
382
|
-
kind: "vad.speech_activity",
|
|
383
|
-
contextId: "user-barge",
|
|
384
|
-
timestampMs: t1 + 320,
|
|
385
|
-
isAsync: true,
|
|
386
|
-
} satisfies VadSpeechActivityPacket);
|
|
387
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
388
|
-
|
|
389
|
-
expect(interrupts).toEqual([]);
|
|
390
|
-
expect(metrics).toContain("interrupt.suppressed_non_primary");
|
|
391
|
-
|
|
392
|
-
await closeSession(session);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it("ignores uplink audio before vad.speech_started when enrolling the primary speaker", async () => {
|
|
396
|
-
const session = new VoiceAgentSession({ plugins: {}, minInterruptionMs: 280 });
|
|
397
|
-
const interrupts: InterruptTtsPacket[] = [];
|
|
398
|
-
const { names: metrics } = collectMetrics(session);
|
|
399
|
-
|
|
400
|
-
await session.start();
|
|
401
|
-
session.bus.on("interrupt.tts", (pkt) => {
|
|
402
|
-
interrupts.push(pkt as InterruptTtsPacket);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
const bystander = synthesizeTonePcm16({
|
|
406
|
-
frequencyHz: BYSTANDER_SPEAKER_TONE_HZ,
|
|
407
|
-
durationMs: 32,
|
|
408
|
-
});
|
|
409
|
-
const primary = synthesizeTonePcm16({
|
|
410
|
-
frequencyHz: PRIMARY_SPEAKER_TONE_HZ,
|
|
411
|
-
durationMs: 32,
|
|
412
|
-
});
|
|
413
|
-
for (let i = 0; i < 20; i += 1) {
|
|
414
|
-
session.bus.push(Route.Main, {
|
|
415
|
-
kind: "user.audio_received",
|
|
416
|
-
contextId: "user-enroll",
|
|
417
|
-
timestampMs: 500 + i * 10,
|
|
418
|
-
audio: bystander,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const t0 = 1000;
|
|
423
|
-
session.bus.push(Route.Main, {
|
|
424
|
-
kind: "vad.speech_started",
|
|
425
|
-
contextId: "user-enroll",
|
|
426
|
-
timestampMs: t0,
|
|
427
|
-
confidence: 0.99,
|
|
428
|
-
} satisfies VadSpeechStartedPacket);
|
|
429
|
-
for (let i = 0; i < 12; i += 1) {
|
|
430
|
-
session.bus.push(Route.Main, {
|
|
431
|
-
kind: "user.audio_received",
|
|
432
|
-
contextId: "user-enroll",
|
|
433
|
-
timestampMs: t0 + 20 + i * 20,
|
|
434
|
-
audio: primary,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
session.bus.push(Route.Main, {
|
|
438
|
-
kind: "vad.speech_ended",
|
|
439
|
-
contextId: "user-enroll",
|
|
440
|
-
timestampMs: t0 + 300,
|
|
441
|
-
} satisfies VadSpeechEndedPacket);
|
|
442
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
443
|
-
|
|
444
|
-
armAssistantSpeaking(session);
|
|
445
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
446
|
-
|
|
447
|
-
const t1 = 2000;
|
|
448
|
-
session.bus.push(Route.Main, {
|
|
449
|
-
kind: "vad.speech_started",
|
|
450
|
-
contextId: "user-barge",
|
|
451
|
-
timestampMs: t1,
|
|
452
|
-
confidence: 0.99,
|
|
453
|
-
} satisfies VadSpeechStartedPacket);
|
|
454
|
-
for (let i = 0; i < 8; i += 1) {
|
|
455
|
-
session.bus.push(Route.Main, {
|
|
456
|
-
kind: "vad.audio",
|
|
457
|
-
contextId: "user-barge",
|
|
458
|
-
timestampMs: t1 + 20 + i * 30,
|
|
459
|
-
audio: primary,
|
|
460
|
-
} satisfies VadAudioPacket);
|
|
461
|
-
}
|
|
462
|
-
session.bus.push(Route.Main, {
|
|
463
|
-
kind: "vad.speech_activity",
|
|
464
|
-
contextId: "user-barge",
|
|
465
|
-
timestampMs: t1 + 320,
|
|
466
|
-
isAsync: true,
|
|
467
|
-
} satisfies VadSpeechActivityPacket);
|
|
468
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
469
|
-
|
|
470
|
-
expect(interrupts).toEqual([
|
|
471
|
-
expect.objectContaining({ kind: "interrupt.tts", contextId: "assistant-turn" }),
|
|
472
|
-
]);
|
|
473
|
-
expect(metrics).not.toContain("interrupt.suppressed_non_primary");
|
|
474
|
-
|
|
475
|
-
await closeSession(session);
|
|
476
|
-
});
|
|
477
|
-
});
|