@kuralle-syrinx/deepgram 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 +6 -5
- package/src/flux.ts +198 -155
- package/src/stt.ts +544 -495
- package/src/tts.ts +35 -5
- package/.handoff/finalize-contract-probe.test.ts +0 -193
- package/src/flux.test.ts +0 -227
- package/src/stt.test.ts +0 -1323
- package/src/tts.test.ts +0 -359
- package/tsconfig.json +0 -21
package/src/tts.ts
CHANGED
|
@@ -65,6 +65,8 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
65
65
|
private model = "aura-2-thalia-en";
|
|
66
66
|
private endpointUrl = "wss://api.deepgram.com/v1/speak";
|
|
67
67
|
private sampleRate = 24000;
|
|
68
|
+
private encoding = "linear16";
|
|
69
|
+
private container = "none";
|
|
68
70
|
private retryConfig: RetryConfig = readProviderRetryConfig({});
|
|
69
71
|
// Deepgram's speak socket has no per-message context id, but the engine
|
|
70
72
|
// synthesizes one turn at a time, so the audio streaming back belongs to the
|
|
@@ -81,6 +83,8 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
81
83
|
private finishTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
82
84
|
private disposers: Array<() => void> = [];
|
|
83
85
|
private audioFormat: AudioFormat = { encoding: "pcm_s16le", sampleRateHz: 24000, channels: 1 };
|
|
86
|
+
/** Synthesized character counts per contextId — billed on successful tts.end. */
|
|
87
|
+
private charactersByContextId = new Map<string, number>();
|
|
84
88
|
|
|
85
89
|
async initialize(bus: PipelineBus, config: PluginConfig): Promise<void> {
|
|
86
90
|
this.bus = bus;
|
|
@@ -88,6 +92,8 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
88
92
|
this.model = optionalStringConfig(config, "model") ?? this.model;
|
|
89
93
|
this.endpointUrl = optionalStringConfig(config, "endpoint_url") ?? this.endpointUrl;
|
|
90
94
|
this.sampleRate = readPositiveInteger(config["sample_rate"], this.sampleRate);
|
|
95
|
+
this.encoding = optionalStringConfig(config, "encoding") ?? this.encoding;
|
|
96
|
+
this.container = optionalStringConfig(config, "container") ?? this.container;
|
|
91
97
|
this.retryConfig = readProviderRetryConfig(config);
|
|
92
98
|
const finishTimeoutMs = readNonNegativeInteger(config["finish_timeout_ms"], 2000);
|
|
93
99
|
this.audioFormat = { encoding: "pcm_s16le", sampleRateHz: this.sampleRate, channels: 1 };
|
|
@@ -97,9 +103,9 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
97
103
|
url: () => {
|
|
98
104
|
const params = new URLSearchParams({
|
|
99
105
|
model: this.model,
|
|
100
|
-
encoding:
|
|
106
|
+
encoding: this.encoding,
|
|
101
107
|
sample_rate: String(this.sampleRate),
|
|
102
|
-
container:
|
|
108
|
+
container: this.container,
|
|
103
109
|
});
|
|
104
110
|
const separator = this.endpointUrl.includes("?") ? "&" : "?";
|
|
105
111
|
return `${this.endpointUrl}${separator}${params.toString()}`;
|
|
@@ -143,7 +149,11 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
143
149
|
if (this.cancelledContexts.has(contextId)) return;
|
|
144
150
|
this.activeContexts.add(contextId);
|
|
145
151
|
this.currentContextId = contextId;
|
|
146
|
-
if (!(await this.trySend(SPEAK(text), contextId)))
|
|
152
|
+
if (!(await this.trySend(SPEAK(text), contextId))) {
|
|
153
|
+
this.activeContexts.delete(contextId);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.charactersByContextId.set(contextId, (this.charactersByContextId.get(contextId) ?? 0) + text.length);
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
private async finishContext(contextId: string): Promise<void> {
|
|
@@ -166,6 +176,7 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
166
176
|
for (const dispose of this.disposers.splice(0)) dispose();
|
|
167
177
|
this.activeContexts.clear();
|
|
168
178
|
this.cancelledContexts.clear();
|
|
179
|
+
this.charactersByContextId.clear();
|
|
169
180
|
for (const timer of this.finishTimers.values()) clearTimeout(timer);
|
|
170
181
|
this.finishTimers.clear();
|
|
171
182
|
this.currentContextId = "";
|
|
@@ -178,7 +189,10 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
178
189
|
|
|
179
190
|
private async cancelActiveContexts(): Promise<void> {
|
|
180
191
|
const contextIds = [...this.activeContexts];
|
|
181
|
-
for (const contextId of contextIds)
|
|
192
|
+
for (const contextId of contextIds) {
|
|
193
|
+
boundedAdd(this.cancelledContexts, contextId, MAX_CANCELLED_CONTEXTS);
|
|
194
|
+
this.charactersByContextId.delete(contextId);
|
|
195
|
+
}
|
|
182
196
|
this.activeContexts.clear();
|
|
183
197
|
for (const contextId of contextIds) this.clearFinishTimeout(contextId);
|
|
184
198
|
this.currentContextId = "";
|
|
@@ -298,7 +312,10 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
298
312
|
private failActiveContexts(err: Error): void {
|
|
299
313
|
const contextIds = [...this.activeContexts];
|
|
300
314
|
this.activeContexts.clear();
|
|
301
|
-
for (const contextId of contextIds)
|
|
315
|
+
for (const contextId of contextIds) {
|
|
316
|
+
this.clearFinishTimeout(contextId);
|
|
317
|
+
this.charactersByContextId.delete(contextId);
|
|
318
|
+
}
|
|
302
319
|
this.currentContextId = "";
|
|
303
320
|
this.carry = EMPTY;
|
|
304
321
|
// A dropped socket voids any in-flight Clear/Cleared handshake: the server
|
|
@@ -330,6 +347,19 @@ export class DeepgramTTSPlugin implements VoicePlugin {
|
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
private emitEnd(contextId: string): void {
|
|
350
|
+
const characters = this.charactersByContextId.get(contextId) ?? 0;
|
|
351
|
+
this.charactersByContextId.delete(contextId);
|
|
352
|
+
if (characters > 0) {
|
|
353
|
+
this.bus?.push(Route.Background, {
|
|
354
|
+
kind: "usage.recorded",
|
|
355
|
+
contextId,
|
|
356
|
+
timestampMs: Date.now(),
|
|
357
|
+
stage: "tts",
|
|
358
|
+
provider: "deepgram",
|
|
359
|
+
model: this.model,
|
|
360
|
+
characters,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
333
363
|
this.bus?.push(Route.Main, {
|
|
334
364
|
kind: "tts.end",
|
|
335
365
|
contextId,
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { WebSocketServer, type WebSocket } from "ws";
|
|
3
|
-
import {
|
|
4
|
-
PipelineBusImpl,
|
|
5
|
-
Route,
|
|
6
|
-
type ConversationMetricPacket,
|
|
7
|
-
type SttErrorPacket,
|
|
8
|
-
type SttResultPacket,
|
|
9
|
-
} from "@kuralle-syrinx/core";
|
|
10
|
-
import { DeepgramSTTPlugin } from "../src/index.js";
|
|
11
|
-
|
|
12
|
-
async function createServer(onConnection: (socket: WebSocket) => void): Promise<{
|
|
13
|
-
url: string;
|
|
14
|
-
close: () => Promise<void>;
|
|
15
|
-
}> {
|
|
16
|
-
const server = await new Promise<WebSocketServer>((resolve) => {
|
|
17
|
-
let nextServer: WebSocketServer;
|
|
18
|
-
nextServer = new WebSocketServer({ port: 0 }, () => resolve(nextServer));
|
|
19
|
-
});
|
|
20
|
-
server.on("connection", onConnection);
|
|
21
|
-
const address = server.address();
|
|
22
|
-
if (!address || typeof address === "string") throw new Error("expected tcp address");
|
|
23
|
-
return {
|
|
24
|
-
url: `ws://127.0.0.1:${address.port}/listen`,
|
|
25
|
-
close: () => new Promise((resolve) => {
|
|
26
|
-
for (const client of server.clients) client.terminate();
|
|
27
|
-
server.close(() => resolve());
|
|
28
|
-
}),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function waitUntil(predicate: () => boolean): Promise<void> {
|
|
33
|
-
for (let i = 0; i < 100; i += 1) {
|
|
34
|
-
if (predicate()) return;
|
|
35
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
36
|
-
}
|
|
37
|
-
throw new Error("timed out waiting for probe condition");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("Deepgram finalize contract probe", () => {
|
|
41
|
-
it("keeps a delayed Finalize response correlated to the requested context after currentContextId moves", async () => {
|
|
42
|
-
const controlMessages: string[] = [];
|
|
43
|
-
const server = await createServer((socket) => {
|
|
44
|
-
socket.on("message", (data, isBinary) => {
|
|
45
|
-
if (isBinary) return;
|
|
46
|
-
const msg = JSON.parse(data.toString()) as { type?: string };
|
|
47
|
-
if (msg.type) controlMessages.push(msg.type);
|
|
48
|
-
if (msg.type !== "Finalize") return;
|
|
49
|
-
setTimeout(() => {
|
|
50
|
-
socket.send(JSON.stringify({
|
|
51
|
-
is_final: true,
|
|
52
|
-
speech_final: false,
|
|
53
|
-
from_finalize: true,
|
|
54
|
-
channel: { alternatives: [{ transcript: "old turn confirmed", confidence: 0.91 }] },
|
|
55
|
-
}));
|
|
56
|
-
}, 10);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
const bus = new PipelineBusImpl();
|
|
60
|
-
const started = bus.start();
|
|
61
|
-
const plugin = new DeepgramSTTPlugin();
|
|
62
|
-
const finals: SttResultPacket[] = [];
|
|
63
|
-
const errors: SttErrorPacket[] = [];
|
|
64
|
-
const metrics: ConversationMetricPacket[] = [];
|
|
65
|
-
bus.on("stt.result", (pkt) => finals.push(pkt as SttResultPacket));
|
|
66
|
-
bus.on("stt.error", (pkt) => errors.push(pkt as SttErrorPacket));
|
|
67
|
-
bus.on("metric.conversation", (pkt) => metrics.push(pkt as ConversationMetricPacket));
|
|
68
|
-
|
|
69
|
-
await plugin.initialize(bus, {
|
|
70
|
-
api_key: "test",
|
|
71
|
-
endpoint_url: server.url,
|
|
72
|
-
sample_rate: 16000,
|
|
73
|
-
emit_eos_on_final: false,
|
|
74
|
-
finalize_on_speech_final: false,
|
|
75
|
-
provider_finalize_timeout_ms: 40,
|
|
76
|
-
finalize_timeout_fallback: false,
|
|
77
|
-
});
|
|
78
|
-
bus.push(Route.Main, {
|
|
79
|
-
kind: "stt.audio",
|
|
80
|
-
contextId: "old-vad-fragment",
|
|
81
|
-
timestampMs: Date.now(),
|
|
82
|
-
audio: new Uint8Array(640),
|
|
83
|
-
});
|
|
84
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
85
|
-
|
|
86
|
-
bus.push(Route.Main, {
|
|
87
|
-
kind: "turn.change",
|
|
88
|
-
contextId: "new-vad-fragment",
|
|
89
|
-
previousContextId: "old-vad-fragment",
|
|
90
|
-
reason: "probe_vad_fragment_split",
|
|
91
|
-
timestampMs: Date.now(),
|
|
92
|
-
});
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
94
|
-
|
|
95
|
-
plugin.forceFinalize("old-vad-fragment");
|
|
96
|
-
await waitUntil(() => finals.length > 0);
|
|
97
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
98
|
-
|
|
99
|
-
expect(controlMessages).toContain("Finalize");
|
|
100
|
-
expect(finals).toEqual([
|
|
101
|
-
expect.objectContaining({
|
|
102
|
-
contextId: "old-vad-fragment",
|
|
103
|
-
text: "old turn confirmed",
|
|
104
|
-
}),
|
|
105
|
-
]);
|
|
106
|
-
expect(errors).toHaveLength(0);
|
|
107
|
-
expect(metrics).toEqual(expect.arrayContaining([
|
|
108
|
-
expect.objectContaining({
|
|
109
|
-
name: "stt_provider_final_segment",
|
|
110
|
-
contextId: "old-vad-fragment",
|
|
111
|
-
value: expect.stringContaining("\"fromFinalize\":true"),
|
|
112
|
-
}),
|
|
113
|
-
]));
|
|
114
|
-
expect(metrics).not.toEqual(expect.arrayContaining([
|
|
115
|
-
expect.objectContaining({
|
|
116
|
-
name: "stt_provider_finalize_timeout",
|
|
117
|
-
contextId: "old-vad-fragment",
|
|
118
|
-
}),
|
|
119
|
-
]));
|
|
120
|
-
|
|
121
|
-
await plugin.close();
|
|
122
|
-
bus.stop();
|
|
123
|
-
await started;
|
|
124
|
-
await server.close();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("releases provider-final text without speech_final/from_finalize and does not need timeout fallback", async () => {
|
|
128
|
-
const controlMessages: string[] = [];
|
|
129
|
-
const server = await createServer((socket) => {
|
|
130
|
-
socket.on("message", (data, isBinary) => {
|
|
131
|
-
if (isBinary) {
|
|
132
|
-
socket.send(JSON.stringify({
|
|
133
|
-
is_final: true,
|
|
134
|
-
speech_final: false,
|
|
135
|
-
channel: { alternatives: [{ transcript: "just want to complain again so", confidence: 0.84 }] },
|
|
136
|
-
}));
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const msg = JSON.parse(data.toString()) as { type?: string };
|
|
140
|
-
if (msg.type) controlMessages.push(msg.type);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
const bus = new PipelineBusImpl();
|
|
144
|
-
const started = bus.start();
|
|
145
|
-
const plugin = new DeepgramSTTPlugin();
|
|
146
|
-
const finals: SttResultPacket[] = [];
|
|
147
|
-
const errors: SttErrorPacket[] = [];
|
|
148
|
-
const metrics: ConversationMetricPacket[] = [];
|
|
149
|
-
bus.on("stt.result", (pkt) => finals.push(pkt as SttResultPacket));
|
|
150
|
-
bus.on("stt.error", (pkt) => errors.push(pkt as SttErrorPacket));
|
|
151
|
-
bus.on("metric.conversation", (pkt) => metrics.push(pkt as ConversationMetricPacket));
|
|
152
|
-
|
|
153
|
-
await plugin.initialize(bus, {
|
|
154
|
-
api_key: "test",
|
|
155
|
-
endpoint_url: server.url,
|
|
156
|
-
sample_rate: 16000,
|
|
157
|
-
emit_eos_on_final: false,
|
|
158
|
-
finalize_on_speech_final: false,
|
|
159
|
-
provider_finalize_timeout_ms: 20,
|
|
160
|
-
finalize_timeout_fallback: false,
|
|
161
|
-
});
|
|
162
|
-
bus.push(Route.Main, {
|
|
163
|
-
kind: "stt.audio",
|
|
164
|
-
contextId: "messy-live-turn",
|
|
165
|
-
timestampMs: Date.now(),
|
|
166
|
-
audio: new Uint8Array(640),
|
|
167
|
-
});
|
|
168
|
-
await waitUntil(() => metrics.some((m) => m.name === "stt_provider_final_segment"));
|
|
169
|
-
await waitUntil(() => finals.length > 0);
|
|
170
|
-
plugin.forceFinalize("messy-live-turn");
|
|
171
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
172
|
-
|
|
173
|
-
expect(controlMessages).toContain("Finalize");
|
|
174
|
-
expect(finals).toEqual([
|
|
175
|
-
expect.objectContaining({
|
|
176
|
-
contextId: "messy-live-turn",
|
|
177
|
-
text: "just want to complain again so",
|
|
178
|
-
}),
|
|
179
|
-
]);
|
|
180
|
-
expect(errors).toHaveLength(0);
|
|
181
|
-
expect(metrics).toEqual(expect.arrayContaining([
|
|
182
|
-
expect.objectContaining({ name: "stt_provider_final_segment" }),
|
|
183
|
-
]));
|
|
184
|
-
expect(metrics).not.toEqual(expect.arrayContaining([
|
|
185
|
-
expect.objectContaining({ name: "stt_provider_finalize_timeout" }),
|
|
186
|
-
]));
|
|
187
|
-
|
|
188
|
-
await plugin.close();
|
|
189
|
-
bus.stop();
|
|
190
|
-
await started;
|
|
191
|
-
await server.close();
|
|
192
|
-
});
|
|
193
|
-
});
|
package/src/flux.test.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { WebSocketServer, type WebSocket } from "ws";
|
|
5
|
-
import {
|
|
6
|
-
PipelineBusImpl,
|
|
7
|
-
Route,
|
|
8
|
-
type EndOfSpeechPacket,
|
|
9
|
-
type EndOfSpeechRetractedPacket,
|
|
10
|
-
type InterimEndOfSpeechPacket,
|
|
11
|
-
type SttInterimPacket,
|
|
12
|
-
type SttResultPacket,
|
|
13
|
-
} from "@kuralle-syrinx/core";
|
|
14
|
-
|
|
15
|
-
import { DeepgramFluxSTTPlugin } from "./flux.js";
|
|
16
|
-
|
|
17
|
-
let servers: WebSocketServer[] = [];
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await Promise.all(
|
|
21
|
-
servers.splice(0).map(
|
|
22
|
-
(server) =>
|
|
23
|
-
new Promise<void>((resolve) => {
|
|
24
|
-
for (const client of server.clients) client.terminate();
|
|
25
|
-
server.close(() => resolve());
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
interface LocalServer {
|
|
32
|
-
endpointUrl: string;
|
|
33
|
-
connectionUrls: string[];
|
|
34
|
-
sockets: WebSocket[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function createLocalServer(): Promise<LocalServer> {
|
|
38
|
-
const server = await new Promise<WebSocketServer>((resolve) => {
|
|
39
|
-
let nextServer: WebSocketServer;
|
|
40
|
-
nextServer = new WebSocketServer({ port: 0 }, () => resolve(nextServer));
|
|
41
|
-
});
|
|
42
|
-
servers.push(server);
|
|
43
|
-
const state: LocalServer = { endpointUrl: "", connectionUrls: [], sockets: [] };
|
|
44
|
-
server.on("connection", (socket, req) => {
|
|
45
|
-
state.connectionUrls.push(req.url ?? "");
|
|
46
|
-
state.sockets.push(socket);
|
|
47
|
-
});
|
|
48
|
-
const address = server.address();
|
|
49
|
-
if (!address || typeof address === "string") throw new Error("Expected TCP server address");
|
|
50
|
-
state.endpointUrl = `ws://127.0.0.1:${address.port}/v2/listen`;
|
|
51
|
-
return state;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function waitFor<T>(items: T[], count = 1): Promise<void> {
|
|
55
|
-
for (let i = 0; i < 100; i += 1) {
|
|
56
|
-
if (items.length >= count) return;
|
|
57
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function turnInfo(event: string, extra: Record<string, unknown> = {}): string {
|
|
62
|
-
return JSON.stringify({
|
|
63
|
-
type: "TurnInfo",
|
|
64
|
-
event,
|
|
65
|
-
turn_index: 0,
|
|
66
|
-
audio_window_start: 0,
|
|
67
|
-
audio_window_end: 0.6,
|
|
68
|
-
transcript: "",
|
|
69
|
-
words: [],
|
|
70
|
-
end_of_turn_confidence: 0.5,
|
|
71
|
-
...extra,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function startPlugin(
|
|
76
|
-
local: LocalServer,
|
|
77
|
-
config: Record<string, unknown> = {},
|
|
78
|
-
): Promise<{ bus: PipelineBusImpl; plugin: DeepgramFluxSTTPlugin; started: Promise<void> }> {
|
|
79
|
-
const bus = new PipelineBusImpl();
|
|
80
|
-
const started = bus.start();
|
|
81
|
-
const plugin = new DeepgramFluxSTTPlugin();
|
|
82
|
-
await plugin.initialize(bus, {
|
|
83
|
-
api_key: "test",
|
|
84
|
-
endpoint_url: local.endpointUrl,
|
|
85
|
-
sample_rate: 16000,
|
|
86
|
-
...config,
|
|
87
|
-
});
|
|
88
|
-
await waitFor(local.sockets);
|
|
89
|
-
// Feed one audio packet so the plugin learns the current contextId.
|
|
90
|
-
bus.push(Route.Main, {
|
|
91
|
-
kind: "stt.audio",
|
|
92
|
-
contextId: "turn-1",
|
|
93
|
-
timestampMs: Date.now(),
|
|
94
|
-
audio: new Uint8Array(320),
|
|
95
|
-
});
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
97
|
-
return { bus, plugin, started };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
describe("DeepgramFluxSTTPlugin", () => {
|
|
101
|
-
it("connects with Flux params and forwards keyterm + eager threshold", async () => {
|
|
102
|
-
const local = await createLocalServer();
|
|
103
|
-
const { bus, plugin, started } = await startPlugin(local, {
|
|
104
|
-
eot_threshold: 0.85,
|
|
105
|
-
eager_eot_threshold: 0.4,
|
|
106
|
-
eot_timeout_ms: 7000,
|
|
107
|
-
keyterm: ["Syrinx"],
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await plugin.close();
|
|
111
|
-
bus.stop();
|
|
112
|
-
await started;
|
|
113
|
-
|
|
114
|
-
const url = local.connectionUrls[0]!;
|
|
115
|
-
expect(url).toContain("model=flux-general-en");
|
|
116
|
-
expect(url).toContain("encoding=linear16");
|
|
117
|
-
expect(url).toContain("sample_rate=16000");
|
|
118
|
-
expect(url).toContain("eot_threshold=0.85");
|
|
119
|
-
expect(url).toContain("eager_eot_threshold=0.4");
|
|
120
|
-
expect(url).toContain("eot_timeout_ms=7000");
|
|
121
|
-
expect(url).toContain("keyterm=Syrinx");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("omits eager_eot_threshold by default (eager mode is opt-in)", async () => {
|
|
125
|
-
const local = await createLocalServer();
|
|
126
|
-
const { bus, plugin, started } = await startPlugin(local);
|
|
127
|
-
|
|
128
|
-
await plugin.close();
|
|
129
|
-
bus.stop();
|
|
130
|
-
await started;
|
|
131
|
-
|
|
132
|
-
const url = local.connectionUrls[0]!;
|
|
133
|
-
expect(url).toContain("eot_threshold=0.7");
|
|
134
|
-
expect(url).not.toContain("eager_eot_threshold=");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("maps TurnInfo events onto the bus: Update→stt.interim, EndOfTurn→stt.result+eos.turn_complete", async () => {
|
|
138
|
-
const local = await createLocalServer();
|
|
139
|
-
const { bus, plugin, started } = await startPlugin(local);
|
|
140
|
-
|
|
141
|
-
const interims: SttInterimPacket[] = [];
|
|
142
|
-
const results: SttResultPacket[] = [];
|
|
143
|
-
const turnCompletes: EndOfSpeechPacket[] = [];
|
|
144
|
-
bus.on("stt.interim", (pkt) => {
|
|
145
|
-
interims.push(pkt as SttInterimPacket);
|
|
146
|
-
});
|
|
147
|
-
bus.on("stt.result", (pkt) => {
|
|
148
|
-
results.push(pkt as SttResultPacket);
|
|
149
|
-
});
|
|
150
|
-
bus.on("eos.turn_complete", (pkt) => {
|
|
151
|
-
turnCompletes.push(pkt as EndOfSpeechPacket);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const socket = local.sockets[0]!;
|
|
155
|
-
socket.send(turnInfo("Update", { transcript: "what are" }));
|
|
156
|
-
socket.send(
|
|
157
|
-
turnInfo("EndOfTurn", {
|
|
158
|
-
transcript: "what are the lab fees",
|
|
159
|
-
words: [
|
|
160
|
-
{ word: "what", confidence: 0.99 },
|
|
161
|
-
{ word: "are", confidence: 0.97 },
|
|
162
|
-
],
|
|
163
|
-
end_of_turn_confidence: 0.91,
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
await waitFor(turnCompletes);
|
|
167
|
-
|
|
168
|
-
await plugin.close();
|
|
169
|
-
bus.stop();
|
|
170
|
-
await started;
|
|
171
|
-
|
|
172
|
-
expect(interims.map((p) => p.text)).toContain("what are");
|
|
173
|
-
expect(results).toHaveLength(1);
|
|
174
|
-
expect(results[0]!.text).toBe("what are the lab fees");
|
|
175
|
-
expect(results[0]!.confidence).toBeCloseTo(0.98, 2);
|
|
176
|
-
expect(turnCompletes).toEqual([
|
|
177
|
-
expect.objectContaining({ contextId: "turn-1", text: "what are the lab fees" }),
|
|
178
|
-
]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("emits eos.interim on EagerEndOfTurn and eos.retracted on TurnResumed", async () => {
|
|
182
|
-
const local = await createLocalServer();
|
|
183
|
-
const { bus, plugin, started } = await startPlugin(local, { eager_eot_threshold: 0.4 });
|
|
184
|
-
|
|
185
|
-
const eagers: InterimEndOfSpeechPacket[] = [];
|
|
186
|
-
const retractions: EndOfSpeechRetractedPacket[] = [];
|
|
187
|
-
bus.on("eos.interim", (pkt) => {
|
|
188
|
-
eagers.push(pkt as InterimEndOfSpeechPacket);
|
|
189
|
-
});
|
|
190
|
-
bus.on("eos.retracted", (pkt) => {
|
|
191
|
-
retractions.push(pkt as EndOfSpeechRetractedPacket);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const socket = local.sockets[0]!;
|
|
195
|
-
socket.send(turnInfo("EagerEndOfTurn", { transcript: "book a room", end_of_turn_confidence: 0.55 }));
|
|
196
|
-
await waitFor(eagers);
|
|
197
|
-
socket.send(turnInfo("TurnResumed"));
|
|
198
|
-
await waitFor(retractions);
|
|
199
|
-
|
|
200
|
-
await plugin.close();
|
|
201
|
-
bus.stop();
|
|
202
|
-
await started;
|
|
203
|
-
|
|
204
|
-
expect(eagers).toEqual([expect.objectContaining({ contextId: "turn-1", text: "book a room" })]);
|
|
205
|
-
expect(retractions).toEqual([expect.objectContaining({ contextId: "turn-1" })]);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("emits vad.speech_started on StartOfTurn (Flux owns barge-in signalling)", async () => {
|
|
209
|
-
const local = await createLocalServer();
|
|
210
|
-
const { bus, plugin, started } = await startPlugin(local);
|
|
211
|
-
|
|
212
|
-
const speechStarts: unknown[] = [];
|
|
213
|
-
bus.on("vad.speech_started", (pkt) => {
|
|
214
|
-
speechStarts.push(pkt);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const socket = local.sockets[0]!;
|
|
218
|
-
socket.send(turnInfo("StartOfTurn", { transcript: "hello" }));
|
|
219
|
-
await waitFor(speechStarts);
|
|
220
|
-
|
|
221
|
-
await plugin.close();
|
|
222
|
-
bus.stop();
|
|
223
|
-
await started;
|
|
224
|
-
|
|
225
|
-
expect(speechStarts.length).toBe(1);
|
|
226
|
-
});
|
|
227
|
-
});
|