@kuralle-syrinx/deepgram 4.1.0 → 4.3.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 +26 -6
- package/src/flux.ts +198 -155
- package/src/stt.ts +568 -481
- 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 -1257
- package/src/tts.test.ts +0 -359
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/deepgram",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"
|
|
5
|
+
"description": "Deepgram adapters for Syrinx — nova-3 STT, Flux turn-aware STT (semantic end-of-turn), and Aura TTS",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"voice",
|
|
8
|
+
"voice-agent",
|
|
9
|
+
"speech",
|
|
10
|
+
"syrinx",
|
|
11
|
+
"deepgram",
|
|
12
|
+
"speech-to-text",
|
|
13
|
+
"text-to-speech"
|
|
14
|
+
],
|
|
6
15
|
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/kuralle/syrinx#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/kuralle/syrinx.git",
|
|
20
|
+
"directory": "packages/deepgram"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/kuralle/syrinx/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
7
26
|
"main": "./src/index.ts",
|
|
8
27
|
"types": "./src/index.ts",
|
|
9
28
|
"exports": {
|
|
@@ -12,14 +31,15 @@
|
|
|
12
31
|
"./tts": "./src/tts.ts"
|
|
13
32
|
},
|
|
14
33
|
"dependencies": {
|
|
15
|
-
"@kuralle-syrinx/core": "4.
|
|
16
|
-
"@kuralle-syrinx/
|
|
34
|
+
"@kuralle-syrinx/stt-core": "4.3.0",
|
|
35
|
+
"@kuralle-syrinx/core": "4.3.0",
|
|
36
|
+
"@kuralle-syrinx/ws": "4.3.0"
|
|
17
37
|
},
|
|
18
38
|
"devDependencies": {
|
|
19
39
|
"@types/ws": "^8.5.0",
|
|
20
40
|
"typescript": "^5.7.0",
|
|
21
|
-
"vitest": "^2.
|
|
22
|
-
"ws": "^8.
|
|
41
|
+
"vitest": "^3.2.6",
|
|
42
|
+
"ws": "^8.21.0"
|
|
23
43
|
},
|
|
24
44
|
"scripts": {
|
|
25
45
|
"typecheck": "tsc --noEmit",
|
package/src/flux.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// is the semantic end-of-turn path for the edge cascade, where local ONNX
|
|
8
8
|
// endpointers (smart-turn) cannot run.
|
|
9
9
|
//
|
|
10
|
-
// TurnInfo state machine → bus mapping:
|
|
10
|
+
// TurnInfo state machine → bus mapping (via stt-core SttEvent):
|
|
11
11
|
// StartOfTurn → vad.speech_started (barge-in signal; Flux recommends it)
|
|
12
12
|
// Update → stt.interim
|
|
13
13
|
// EagerEndOfTurn → eos.interim (speculative-generation trigger)
|
|
@@ -20,19 +20,29 @@
|
|
|
20
20
|
// when no TurnResumed intervened, so a speculative result keyed on the eager
|
|
21
21
|
// transcript can be committed as-is.
|
|
22
22
|
|
|
23
|
-
import type { PipelineBus } from "@kuralle-syrinx/core";
|
|
24
23
|
import {
|
|
25
24
|
Route,
|
|
25
|
+
categorizeSttError,
|
|
26
|
+
isRecoverable,
|
|
27
|
+
type AudioFormat,
|
|
28
|
+
type PipelineBus,
|
|
26
29
|
type PluginConfig,
|
|
27
30
|
type SttErrorPacket,
|
|
31
|
+
type SttReconfigure,
|
|
32
|
+
type SttReconfigurePartial,
|
|
28
33
|
type VoicePlugin,
|
|
29
|
-
categorizeSttError,
|
|
30
|
-
isRecoverable,
|
|
31
34
|
optionalStringConfig,
|
|
32
35
|
readProviderRetryConfig,
|
|
33
36
|
requireStringConfig,
|
|
34
37
|
} from "@kuralle-syrinx/core";
|
|
35
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
defaultNodeSocketFactory,
|
|
40
|
+
startStreamingSttSession,
|
|
41
|
+
type SttEvent,
|
|
42
|
+
type SttWireProtocol,
|
|
43
|
+
type StreamingSttSession,
|
|
44
|
+
} from "@kuralle-syrinx/stt-core";
|
|
45
|
+
import type { SocketData, SocketFactory } from "@kuralle-syrinx/ws";
|
|
36
46
|
|
|
37
47
|
interface TurnInfoMessage {
|
|
38
48
|
readonly type: "TurnInfo";
|
|
@@ -49,6 +59,111 @@ function meanWordConfidence(words: TurnInfoMessage["words"]): number {
|
|
|
49
59
|
return sum / words.length;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
type MetricSink = (name: string, value: string) => void;
|
|
63
|
+
|
|
64
|
+
class DeepgramFluxSttWireProtocol implements SttWireProtocol {
|
|
65
|
+
constructor(
|
|
66
|
+
private readonly speechStartedEvents: boolean,
|
|
67
|
+
private readonly onMetric: MetricSink,
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
encodeFinalize(_contextId: string): readonly SocketData[] {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
encodeClose(): readonly SocketData[] {
|
|
75
|
+
return [JSON.stringify({ type: "CloseStream" })];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
encodeReconfigure(partial: SttReconfigurePartial): readonly SocketData[] {
|
|
79
|
+
const thresholds: Record<string, number> = {};
|
|
80
|
+
if (partial.eotThreshold !== undefined) thresholds["eot_threshold"] = partial.eotThreshold;
|
|
81
|
+
if (partial.eagerEotThreshold !== undefined) {
|
|
82
|
+
thresholds["eager_eot_threshold"] = partial.eagerEotThreshold;
|
|
83
|
+
}
|
|
84
|
+
if (partial.eotTimeoutMs !== undefined) thresholds["eot_timeout_ms"] = partial.eotTimeoutMs;
|
|
85
|
+
|
|
86
|
+
const configure: Record<string, unknown> = { type: "Configure" };
|
|
87
|
+
if (Object.keys(thresholds).length > 0) configure["thresholds"] = thresholds;
|
|
88
|
+
if (partial.keyterms !== undefined) configure["keyterms"] = partial.keyterms;
|
|
89
|
+
if (partial.languageHints !== undefined) configure["language_hints"] = partial.languageHints;
|
|
90
|
+
|
|
91
|
+
return [JSON.stringify(configure)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
decode(data: SocketData, _isBinary: boolean): readonly SttEvent[] {
|
|
95
|
+
if (typeof data !== "string") return [];
|
|
96
|
+
let msg: Record<string, unknown>;
|
|
97
|
+
try {
|
|
98
|
+
msg = JSON.parse(data) as Record<string, unknown>;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
type: "error",
|
|
103
|
+
error: new Error(
|
|
104
|
+
`Deepgram Flux sent malformed JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (msg["type"] === "Error") {
|
|
111
|
+
const description =
|
|
112
|
+
typeof msg["description"] === "string" ? msg["description"] : JSON.stringify(msg);
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
type: "error",
|
|
116
|
+
error: new Error(`Deepgram Flux provider error: ${description}`),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
// Ack/nack for a mid-stream Configure (dynamic reconfigure). Surface as metrics so the
|
|
121
|
+
// conversation-state biasing that actuates reconfigure can observe that it took effect.
|
|
122
|
+
if (msg["type"] === "ConfigureSuccess") {
|
|
123
|
+
this.onMetric("stt.flux.configure_success", "1");
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
if (msg["type"] === "ConfigureFailure") {
|
|
127
|
+
const description =
|
|
128
|
+
typeof msg["description"] === "string" ? msg["description"] : JSON.stringify(msg);
|
|
129
|
+
this.onMetric("stt.flux.configure_failure", description);
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
if (msg["type"] !== "TurnInfo") return [];
|
|
133
|
+
|
|
134
|
+
const info = msg as unknown as TurnInfoMessage;
|
|
135
|
+
const transcript = (info.transcript ?? "").trim();
|
|
136
|
+
|
|
137
|
+
switch (info.event) {
|
|
138
|
+
case "StartOfTurn":
|
|
139
|
+
if (!this.speechStartedEvents) return [];
|
|
140
|
+
return [{ type: "speech_started" }];
|
|
141
|
+
case "Update":
|
|
142
|
+
if (!transcript) return [];
|
|
143
|
+
return [{ type: "interim", contextId: "", text: transcript }];
|
|
144
|
+
case "EagerEndOfTurn":
|
|
145
|
+
if (!transcript) return [];
|
|
146
|
+
return [{ type: "eos_interim", text: transcript }];
|
|
147
|
+
case "TurnResumed":
|
|
148
|
+
return [{ type: "eos_retracted" }];
|
|
149
|
+
case "EndOfTurn":
|
|
150
|
+
if (!transcript) return [];
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
type: "final",
|
|
154
|
+
contextId: "",
|
|
155
|
+
text: transcript,
|
|
156
|
+
confidence: meanWordConfidence(info.words),
|
|
157
|
+
language: "en",
|
|
158
|
+
speechFinal: true,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
default:
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
52
167
|
export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
53
168
|
readonly endpointingCapability = {
|
|
54
169
|
owner: "provider_stt" as const,
|
|
@@ -58,7 +173,7 @@ export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
|
58
173
|
};
|
|
59
174
|
|
|
60
175
|
private bus: PipelineBus | null = null;
|
|
61
|
-
private
|
|
176
|
+
private session: StreamingSttSession | null = null;
|
|
62
177
|
private model = "flux-general-en";
|
|
63
178
|
private endpointUrl = "wss://api.deepgram.com/v2/listen";
|
|
64
179
|
private sampleRate = 16000;
|
|
@@ -68,11 +183,11 @@ export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
|
68
183
|
private keyterms: readonly string[] = [];
|
|
69
184
|
private languageHints: readonly string[] = [];
|
|
70
185
|
private speechStartedEvents = true;
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
private
|
|
186
|
+
private encoding = "linear16";
|
|
187
|
+
/** Open-ended Flux listen query knobs (profanity_filter, tag, mip_opt_out, …). */
|
|
188
|
+
private queryParams: Record<string, unknown> | undefined;
|
|
189
|
+
private apiKey = "";
|
|
74
190
|
private currentContextId = "";
|
|
75
|
-
private disposers: Array<() => void> = [];
|
|
76
191
|
|
|
77
192
|
constructor(private readonly socketFactory?: SocketFactory) {}
|
|
78
193
|
|
|
@@ -86,7 +201,7 @@ export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
|
86
201
|
this.eagerEotThreshold = config["eager_eot_threshold"] as number | undefined;
|
|
87
202
|
this.eotTimeoutMs = (config["eot_timeout_ms"] as number) ?? 5000;
|
|
88
203
|
this.speechStartedEvents = (config["speech_started_events"] as boolean) ?? true;
|
|
89
|
-
|
|
204
|
+
const emitEosOnFinal = (config["emit_eos_on_final"] as boolean) ?? true;
|
|
90
205
|
{
|
|
91
206
|
const raw = config["keyterm"];
|
|
92
207
|
this.keyterms = Array.isArray(raw)
|
|
@@ -103,13 +218,33 @@ export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
|
103
218
|
? [raw]
|
|
104
219
|
: [];
|
|
105
220
|
}
|
|
221
|
+
this.encoding = optionalStringConfig(config, "encoding") ?? "linear16";
|
|
222
|
+
this.queryParams = readPlainObject(config["query_params"]);
|
|
106
223
|
|
|
107
|
-
const
|
|
108
|
-
|
|
224
|
+
const audioFormat: AudioFormat = {
|
|
225
|
+
encoding: "pcm_s16le",
|
|
226
|
+
sampleRateHz: this.sampleRate,
|
|
227
|
+
channels: 1,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
this.session = await startStreamingSttSession(bus, {
|
|
231
|
+
protocol: new DeepgramFluxSttWireProtocol(this.speechStartedEvents, (name, value) => {
|
|
232
|
+
bus.push(Route.Background, {
|
|
233
|
+
kind: "metric.conversation",
|
|
234
|
+
contextId: "",
|
|
235
|
+
timestampMs: Date.now(),
|
|
236
|
+
name,
|
|
237
|
+
value,
|
|
238
|
+
});
|
|
239
|
+
}),
|
|
240
|
+
provider: { name: "deepgram", model: this.model, region: "global" },
|
|
241
|
+
format: audioFormat,
|
|
242
|
+
language: "en",
|
|
243
|
+
emitEosOnFinal,
|
|
109
244
|
url: () => {
|
|
110
245
|
const params = new URLSearchParams({
|
|
111
246
|
model: this.model,
|
|
112
|
-
encoding:
|
|
247
|
+
encoding: this.encoding,
|
|
113
248
|
sample_rate: String(this.sampleRate),
|
|
114
249
|
eot_threshold: String(this.eotThreshold),
|
|
115
250
|
eot_timeout_ms: String(this.eotTimeoutMs),
|
|
@@ -119,151 +254,52 @@ export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
|
119
254
|
});
|
|
120
255
|
for (const term of this.keyterms) params.append("keyterm", term);
|
|
121
256
|
for (const hint of this.languageHints) params.append("language_hint", hint);
|
|
257
|
+
applyQueryParams(params, this.queryParams);
|
|
122
258
|
const separator = this.endpointUrl.includes("?") ? "&" : "?";
|
|
123
259
|
return `${this.endpointUrl}${separator}${params.toString()}`;
|
|
124
260
|
},
|
|
125
261
|
headers: { Authorization: `Token ${this.apiKey}` },
|
|
126
|
-
socketFactory,
|
|
262
|
+
socketFactory: this.socketFactory ?? (await defaultNodeSocketFactory()),
|
|
127
263
|
retry: readProviderRetryConfig(config),
|
|
128
264
|
replayBufferSize: (config["replay_buffer_size"] as number) ?? 64,
|
|
129
|
-
|
|
130
|
-
this.pushMetric(this.currentContextId, `stt.flux.reconnect_replay_${event}`, String(count));
|
|
131
|
-
},
|
|
132
|
-
// No provider keepalive text message: the Flux v2 protocol keeps the turn
|
|
133
|
-
// model fed by continuous audio; transports stream silence frames between
|
|
134
|
-
// utterances, so an idle socket means the call itself has gone quiet.
|
|
135
|
-
onMessage: (data) => {
|
|
136
|
-
if (typeof data === "string") this.handleProviderMessage(data);
|
|
137
|
-
},
|
|
138
|
-
onConnectionLost: (err) => {
|
|
139
|
-
this.emitError(this.currentContextId, err);
|
|
140
|
-
},
|
|
265
|
+
metricPrefix: "stt.flux",
|
|
141
266
|
});
|
|
142
|
-
|
|
267
|
+
}
|
|
143
268
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}),
|
|
156
|
-
bus.on("turn.change", (pkt: unknown) => {
|
|
157
|
-
const tc = pkt as { contextId: string };
|
|
158
|
-
this.currentContextId = tc.contextId;
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
269
|
+
/**
|
|
270
|
+
* Mid-stream reconfigure of keyterms / end-of-turn thresholds via the Flux v2 `Configure`
|
|
271
|
+
* control message — no socket restart (Deepgram "on-the-fly configuration"). Instance state is
|
|
272
|
+
* updated too, so a reconnect replays the current config (single source of truth with `url()`).
|
|
273
|
+
* `keyterms` REPLACES the list (Flux semantics), not merges. The provider acks with
|
|
274
|
+
* ConfigureSuccess / ConfigureFailure (surfaced as metrics in the wire protocol).
|
|
275
|
+
* `language` (hard switch) is ignored: Flux's model is language-specific (flux-general-en) —
|
|
276
|
+
* use `languageHints` to bias.
|
|
277
|
+
*/
|
|
278
|
+
get sttReconfigure(): SttReconfigure {
|
|
279
|
+
return this;
|
|
161
280
|
}
|
|
162
281
|
|
|
163
|
-
|
|
164
|
-
|
|
282
|
+
reconfigure(partial: SttReconfigurePartial): void {
|
|
283
|
+
if (partial.keyterms !== undefined) this.keyterms = partial.keyterms;
|
|
284
|
+
if (partial.eotThreshold !== undefined) this.eotThreshold = partial.eotThreshold;
|
|
285
|
+
if (partial.eagerEotThreshold !== undefined) this.eagerEotThreshold = partial.eagerEotThreshold;
|
|
286
|
+
if (partial.eotTimeoutMs !== undefined) this.eotTimeoutMs = partial.eotTimeoutMs;
|
|
287
|
+
if (partial.languageHints !== undefined) this.languageHints = partial.languageHints;
|
|
288
|
+
|
|
165
289
|
try {
|
|
166
|
-
|
|
290
|
+
this.session?.reconfigure(partial);
|
|
167
291
|
} catch (err) {
|
|
168
|
-
this.emitError(
|
|
169
|
-
this.currentContextId,
|
|
170
|
-
new Error(`Deepgram Flux sent malformed JSON: ${err instanceof Error ? err.message : String(err)}`),
|
|
171
|
-
);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (msg["type"] === "Error") {
|
|
176
|
-
const description = typeof msg["description"] === "string" ? msg["description"] : JSON.stringify(msg);
|
|
177
|
-
this.emitError(this.currentContextId, new Error(`Deepgram Flux provider error: ${description}`));
|
|
178
|
-
return;
|
|
292
|
+
this.emitError(err instanceof Error ? err : new Error(String(err)));
|
|
179
293
|
}
|
|
180
|
-
if (msg["type"] !== "TurnInfo") return;
|
|
181
|
-
|
|
182
|
-
const info = msg as unknown as TurnInfoMessage;
|
|
183
|
-
const contextId = this.currentContextId;
|
|
184
|
-
const transcript = (info.transcript ?? "").trim();
|
|
185
|
-
|
|
186
|
-
switch (info.event) {
|
|
187
|
-
case "StartOfTurn": {
|
|
188
|
-
if (!this.speechStartedEvents) return;
|
|
189
|
-
this.bus?.push(Route.Main, {
|
|
190
|
-
kind: "vad.speech_started",
|
|
191
|
-
contextId,
|
|
192
|
-
timestampMs: Date.now(),
|
|
193
|
-
confidence: 1,
|
|
194
|
-
});
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
case "Update": {
|
|
198
|
-
if (!transcript) return;
|
|
199
|
-
this.bus?.push(Route.Main, {
|
|
200
|
-
kind: "stt.interim",
|
|
201
|
-
contextId,
|
|
202
|
-
timestampMs: Date.now(),
|
|
203
|
-
text: transcript,
|
|
204
|
-
});
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
case "EagerEndOfTurn": {
|
|
208
|
-
if (!transcript) return;
|
|
209
|
-
this.bus?.push(Route.Main, {
|
|
210
|
-
kind: "eos.interim",
|
|
211
|
-
contextId,
|
|
212
|
-
timestampMs: Date.now(),
|
|
213
|
-
text: transcript,
|
|
214
|
-
});
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
case "TurnResumed": {
|
|
218
|
-
this.bus?.push(Route.Main, {
|
|
219
|
-
kind: "eos.retracted",
|
|
220
|
-
contextId,
|
|
221
|
-
timestampMs: Date.now(),
|
|
222
|
-
});
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
case "EndOfTurn": {
|
|
226
|
-
if (!transcript) return;
|
|
227
|
-
this.bus?.push(Route.Main, {
|
|
228
|
-
kind: "stt.result",
|
|
229
|
-
contextId,
|
|
230
|
-
timestampMs: Date.now(),
|
|
231
|
-
text: transcript,
|
|
232
|
-
confidence: meanWordConfidence(info.words),
|
|
233
|
-
language: "en",
|
|
234
|
-
provider: { name: "deepgram", model: this.model, region: "global" },
|
|
235
|
-
});
|
|
236
|
-
if (this.emitEosOnFinal) {
|
|
237
|
-
this.bus?.push(Route.Main, {
|
|
238
|
-
kind: "eos.turn_complete",
|
|
239
|
-
contextId,
|
|
240
|
-
timestampMs: Date.now(),
|
|
241
|
-
text: transcript,
|
|
242
|
-
transcripts: [],
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private pushMetric(contextId: string, name: string, value: string): void {
|
|
251
|
-
this.bus?.push(Route.Background, {
|
|
252
|
-
kind: "metric.conversation",
|
|
253
|
-
contextId,
|
|
254
|
-
timestampMs: Date.now(),
|
|
255
|
-
name,
|
|
256
|
-
value,
|
|
257
|
-
});
|
|
258
294
|
}
|
|
259
295
|
|
|
260
|
-
private emitError(
|
|
296
|
+
private emitError(err: Error): void {
|
|
261
297
|
const category = categorizeSttError(err);
|
|
262
298
|
const packet: SttErrorPacket = {
|
|
263
299
|
kind: "stt.error",
|
|
264
|
-
contextId,
|
|
300
|
+
contextId: this.currentContextId,
|
|
265
301
|
timestampMs: Date.now(),
|
|
266
|
-
component: "stt"
|
|
302
|
+
component: "stt",
|
|
267
303
|
category,
|
|
268
304
|
cause: err,
|
|
269
305
|
isRecoverable: isRecoverable(category),
|
|
@@ -272,23 +308,30 @@ export class DeepgramFluxSTTPlugin implements VoicePlugin {
|
|
|
272
308
|
}
|
|
273
309
|
|
|
274
310
|
async close(): Promise<void> {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (this.conn.isReady) {
|
|
278
|
-
try {
|
|
279
|
-
this.conn.send(JSON.stringify({ type: "CloseStream" }));
|
|
280
|
-
} catch {
|
|
281
|
-
// Socket already going away — CloseStream is best-effort.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
await this.conn.close();
|
|
285
|
-
this.conn = null;
|
|
286
|
-
}
|
|
311
|
+
await this.session?.dispose();
|
|
312
|
+
this.session = null;
|
|
287
313
|
this.bus = null;
|
|
288
314
|
}
|
|
289
315
|
}
|
|
290
316
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
317
|
+
function readPlainObject(value: unknown): Record<string, unknown> | undefined {
|
|
318
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
319
|
+
return value as Record<string, unknown>;
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function applyQueryParams(params: URLSearchParams, extra: Record<string, unknown> | undefined): void {
|
|
325
|
+
if (!extra) return;
|
|
326
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
327
|
+
if (value === undefined || value === null) continue;
|
|
328
|
+
if (Array.isArray(value)) {
|
|
329
|
+
for (const item of value) {
|
|
330
|
+
if (item === undefined || item === null) continue;
|
|
331
|
+
params.append(key, String(item));
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
params.set(key, String(value));
|
|
336
|
+
}
|
|
294
337
|
}
|