@acpfx/stt-deepgram 0.2.0 → 0.2.3

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024-2026 acpfx contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @acpfx/stt-deepgram
2
+
3
+ Speech-to-text via Deepgram streaming API. Streams partial transcriptions in real time with configurable VAD and endpointing.
4
+
5
+ ## Usage
6
+
7
+ This package is a pipeline node for [@acpfx/cli](../orchestrator/README.md). See the CLI package for installation and usage.
8
+
9
+ Requires a `DEEPGRAM_API_KEY` environment variable.
10
+
11
+ ## Manifest
12
+
13
+ - **Consumes:** `audio.chunk`
14
+ - **Emits:** `speech.partial`, `speech.final`, `speech.pause`, `lifecycle.ready`, `lifecycle.done`, `control.error`
15
+
16
+ ## Settings
17
+
18
+ | Name | Type | Default | Description |
19
+ |------|------|---------|-------------|
20
+ | `language` | string | `en` | Language code |
21
+ | `model` | string | `nova-3` | Deepgram model name |
22
+ | `utteranceEndMs` | number | `1000` | Silence ms before utterance end |
23
+ | `endpointing` | number | `300` | VAD endpointing threshold in ms |
24
+ | `apiKey` | string | | Overrides `DEEPGRAM_API_KEY` env var |
25
+
26
+ ## Pipeline Example
27
+
28
+ ```yaml
29
+ nodes:
30
+ stt:
31
+ use: "@acpfx/stt-deepgram"
32
+ settings: { language: en, model: nova-3 }
33
+ outputs: [bridge]
34
+ env:
35
+ DEEPGRAM_API_KEY: ${DEEPGRAM_API_KEY}
36
+ ```
37
+
38
+ ## External Links
39
+
40
+ - [Deepgram](https://deepgram.com) -- Speech AI platform
41
+ - [Deepgram Developer Docs](https://developers.deepgram.com) -- API reference and guides
42
+
43
+ ## License
44
+
45
+ ISC
package/dist/index.js ADDED
@@ -0,0 +1,287 @@
1
+ // ../node-sdk/src/index.ts
2
+ import { createInterface } from "node:readline";
3
+
4
+ // ../core/src/config.ts
5
+ import { parse as parseYaml } from "yaml";
6
+
7
+ // ../core/src/manifest.ts
8
+ import { readFileSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+ import { z as z2 } from "zod";
11
+
12
+ // ../core/src/acpfx-flags.ts
13
+ import { z } from "zod";
14
+ var SetupCheckResponseSchema = z.object({
15
+ needed: z.boolean(),
16
+ description: z.string().optional()
17
+ });
18
+ var SetupProgressSchema = z.discriminatedUnion("type", [
19
+ z.object({
20
+ type: z.literal("progress"),
21
+ message: z.string(),
22
+ pct: z.number().optional()
23
+ }),
24
+ z.object({ type: z.literal("complete"), message: z.string() }),
25
+ z.object({ type: z.literal("error"), message: z.string() })
26
+ ]);
27
+ var UnsupportedFlagResponseSchema = z.object({
28
+ unsupported: z.boolean(),
29
+ flag: z.string()
30
+ });
31
+
32
+ // ../core/src/manifest.ts
33
+ var ArgumentTypeSchema = z2.enum(["string", "number", "boolean"]);
34
+ var ManifestArgumentSchema = z2.object({
35
+ type: ArgumentTypeSchema,
36
+ default: z2.unknown().optional(),
37
+ description: z2.string().optional(),
38
+ required: z2.boolean().optional(),
39
+ enum: z2.array(z2.unknown()).optional()
40
+ });
41
+ var ManifestEnvFieldSchema = z2.object({
42
+ required: z2.boolean().optional(),
43
+ description: z2.string().optional()
44
+ });
45
+ var NodeManifestSchema = z2.object({
46
+ name: z2.string(),
47
+ description: z2.string().optional(),
48
+ consumes: z2.array(z2.string()),
49
+ emits: z2.array(z2.string()),
50
+ arguments: z2.record(z2.string(), ManifestArgumentSchema).optional(),
51
+ additional_arguments: z2.boolean().optional(),
52
+ env: z2.record(z2.string(), ManifestEnvFieldSchema).optional()
53
+ });
54
+ function handleAcpfxFlags(manifestPath) {
55
+ const acpfxFlag = process.argv.find((a) => a.startsWith("--acpfx-"));
56
+ const legacyManifest = process.argv.includes("--manifest");
57
+ if (!acpfxFlag && !legacyManifest) return;
58
+ const flag = acpfxFlag ?? "--acpfx-manifest";
59
+ switch (flag) {
60
+ case "--acpfx-manifest":
61
+ printManifest(manifestPath);
62
+ break;
63
+ case "--acpfx-setup-check":
64
+ process.stdout.write(JSON.stringify({ needed: false }) + "\n");
65
+ process.exit(0);
66
+ break;
67
+ default:
68
+ process.stdout.write(
69
+ JSON.stringify({ unsupported: true, flag }) + "\n"
70
+ );
71
+ process.exit(0);
72
+ }
73
+ }
74
+ function handleManifestFlag(manifestPath) {
75
+ handleAcpfxFlags(manifestPath);
76
+ }
77
+ function printManifest(manifestPath) {
78
+ if (!manifestPath) {
79
+ const script = process.argv[1];
80
+ const scriptDir = dirname(script);
81
+ const scriptBase = script.replace(/\.[^.]+$/, "");
82
+ const colocated = `${scriptBase}.manifest.json`;
83
+ try {
84
+ readFileSync(colocated);
85
+ manifestPath = colocated;
86
+ } catch {
87
+ manifestPath = join(scriptDir, "manifest.json");
88
+ }
89
+ }
90
+ try {
91
+ const content = readFileSync(manifestPath, "utf8");
92
+ process.stdout.write(content.trim() + "\n");
93
+ process.exit(0);
94
+ } catch (err) {
95
+ process.stderr.write(`Failed to read manifest: ${err}
96
+ `);
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ // ../node-sdk/src/index.ts
102
+ var NODE_NAME = process.env.ACPFX_NODE_NAME ?? "unknown";
103
+ function emit(event) {
104
+ process.stdout.write(JSON.stringify(event) + "\n");
105
+ }
106
+ function log(level, message) {
107
+ emit({ type: "log", level, component: NODE_NAME, message });
108
+ }
109
+ log.info = (message) => log("info", message);
110
+ log.warn = (message) => log("warn", message);
111
+ log.error = (message) => log("error", message);
112
+ log.debug = (message) => log("debug", message);
113
+ function onEvent(handler) {
114
+ const rl = createInterface({ input: process.stdin });
115
+ rl.on("line", (line) => {
116
+ if (!line.trim()) return;
117
+ try {
118
+ const event = JSON.parse(line);
119
+ handler(event);
120
+ } catch {
121
+ }
122
+ });
123
+ return rl;
124
+ }
125
+
126
+ // src/index.ts
127
+ handleManifestFlag();
128
+ var WS_URL = "wss://api.deepgram.com/v1/listen";
129
+ var settings = JSON.parse(process.env.ACPFX_SETTINGS || "{}");
130
+ var API_KEY = settings.apiKey ?? process.env.DEEPGRAM_API_KEY ?? "";
131
+ var LANGUAGE = settings.language ?? "en";
132
+ var MODEL = settings.model ?? "nova-3";
133
+ var UTTERANCE_END_MS = settings.utteranceEndMs ?? 1e3;
134
+ var ENDPOINTING = settings.endpointing ?? 300;
135
+ var TRACK_ID = "stt";
136
+ if (!API_KEY) {
137
+ log.error("No API key. Set DEEPGRAM_API_KEY or settings.apiKey");
138
+ process.exit(1);
139
+ }
140
+ var ws = null;
141
+ var connected = false;
142
+ var lastFinalText = "";
143
+ var pendingText = "";
144
+ async function connectWebSocket() {
145
+ const url = `${WS_URL}?model=${MODEL}&language=${encodeURIComponent(LANGUAGE)}&encoding=linear16&sample_rate=16000&channels=1&interim_results=true&punctuate=true&smart_format=true&utterance_end_ms=${UTTERANCE_END_MS}&endpointing=${ENDPOINTING}&vad_events=true`;
146
+ ws = new WebSocket(url, ["token", API_KEY]);
147
+ await new Promise((resolve, reject) => {
148
+ ws.addEventListener(
149
+ "open",
150
+ () => {
151
+ connected = true;
152
+ log.info("Connected to Deepgram STT");
153
+ resolve();
154
+ },
155
+ { once: true }
156
+ );
157
+ ws.addEventListener(
158
+ "error",
159
+ () => {
160
+ reject(new Error("WebSocket connection failed"));
161
+ },
162
+ { once: true }
163
+ );
164
+ });
165
+ ws.addEventListener("message", (event) => {
166
+ try {
167
+ const data = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
168
+ const msg = JSON.parse(data);
169
+ handleServerMessage(msg);
170
+ } catch {
171
+ }
172
+ });
173
+ ws.addEventListener("error", (event) => {
174
+ log.error(`WebSocket error: ${event.message ?? "unknown"}`);
175
+ emit({
176
+ type: "control.error",
177
+ component: "stt-deepgram",
178
+ message: "STT WebSocket error",
179
+ fatal: false
180
+ });
181
+ });
182
+ ws.addEventListener("close", (event) => {
183
+ log.info(`WebSocket closed (code=${event.code})`);
184
+ connected = false;
185
+ });
186
+ }
187
+ function handleServerMessage(msg) {
188
+ const type = msg.type;
189
+ if (type === "UtteranceEnd") {
190
+ if (pendingText) {
191
+ emit({
192
+ type: "speech.pause",
193
+ trackId: TRACK_ID,
194
+ pendingText,
195
+ silenceMs: UTTERANCE_END_MS
196
+ });
197
+ pendingText = "";
198
+ }
199
+ return;
200
+ }
201
+ if (type === "SpeechStarted") {
202
+ return;
203
+ }
204
+ if (type === "Results") {
205
+ const channel = msg.channel;
206
+ const alternatives = channel?.alternatives ?? [];
207
+ if (alternatives.length === 0) return;
208
+ const transcript = alternatives[0].transcript ?? "";
209
+ const isFinal = msg.is_final === true;
210
+ const speechFinal = msg.speech_final === true;
211
+ if (!transcript) return;
212
+ if (isFinal) {
213
+ lastFinalText = transcript;
214
+ pendingText = pendingText ? pendingText + " " + transcript : transcript;
215
+ emit({
216
+ type: "speech.final",
217
+ trackId: TRACK_ID,
218
+ text: transcript,
219
+ confidence: alternatives[0].confidence ?? void 0
220
+ });
221
+ if (speechFinal) {
222
+ emit({
223
+ type: "speech.pause",
224
+ trackId: TRACK_ID,
225
+ pendingText,
226
+ silenceMs: ENDPOINTING
227
+ });
228
+ pendingText = "";
229
+ }
230
+ } else {
231
+ emit({
232
+ type: "speech.partial",
233
+ trackId: TRACK_ID,
234
+ text: transcript
235
+ });
236
+ }
237
+ }
238
+ }
239
+ function sendAudio(base64Pcm) {
240
+ if (!ws || !connected) return;
241
+ const pcm = Buffer.from(base64Pcm, "base64");
242
+ try {
243
+ ws.send(pcm);
244
+ } catch {
245
+ }
246
+ }
247
+ function closeWebSocket() {
248
+ connected = false;
249
+ if (ws) {
250
+ try {
251
+ ws.send(JSON.stringify({ type: "CloseStream" }));
252
+ ws.close();
253
+ } catch {
254
+ }
255
+ ws = null;
256
+ }
257
+ }
258
+ async function main() {
259
+ await connectWebSocket();
260
+ emit({ type: "lifecycle.ready", component: "stt-deepgram" });
261
+ const rl = onEvent((event) => {
262
+ if (event.type === "audio.chunk") {
263
+ if (!connected) {
264
+ connectWebSocket().then(() => {
265
+ sendAudio(event.data);
266
+ }).catch(() => {
267
+ });
268
+ } else {
269
+ sendAudio(event.data);
270
+ }
271
+ } else if (event.type === "control.interrupt") {
272
+ }
273
+ });
274
+ rl.on("close", () => {
275
+ closeWebSocket();
276
+ emit({ type: "lifecycle.done", component: "stt-deepgram" });
277
+ process.exit(0);
278
+ });
279
+ process.on("SIGTERM", () => {
280
+ closeWebSocket();
281
+ process.exit(0);
282
+ });
283
+ }
284
+ main().catch((err) => {
285
+ log.error(`Fatal: ${err.message}`);
286
+ process.exit(1);
287
+ });
@@ -0,0 +1 @@
1
+ {"name":"stt-deepgram","description":"Speech-to-text via Deepgram streaming API","consumes":["audio.chunk"],"emits":["speech.partial","speech.final","speech.pause","lifecycle.ready","lifecycle.done","control.error"],"arguments":{"language":{"type":"string","default":"en","description":"Language code for transcription"},"model":{"type":"string","default":"nova-3","description":"Deepgram model name"},"utteranceEndMs":{"type":"number","default":1000,"description":"Milliseconds of silence before utterance end"},"endpointing":{"type":"number","default":300,"description":"VAD endpointing threshold in ms"},"apiKey":{"type":"string","description":"Deepgram API key (overrides DEEPGRAM_API_KEY env var)"}},"env":{"DEEPGRAM_API_KEY":{"required":true,"description":"Deepgram API key for STT"}}}
package/manifest.yaml CHANGED
@@ -9,3 +9,27 @@ emits:
9
9
  - lifecycle.ready
10
10
  - lifecycle.done
11
11
  - control.error
12
+ arguments:
13
+ language:
14
+ type: string
15
+ default: "en"
16
+ description: "Language code for transcription"
17
+ model:
18
+ type: string
19
+ default: "nova-3"
20
+ description: "Deepgram model name"
21
+ utteranceEndMs:
22
+ type: number
23
+ default: 1000
24
+ description: "Milliseconds of silence before utterance end"
25
+ endpointing:
26
+ type: number
27
+ default: 300
28
+ description: "VAD endpointing threshold in ms"
29
+ apiKey:
30
+ type: string
31
+ description: "Deepgram API key (overrides DEEPGRAM_API_KEY env var)"
32
+ env:
33
+ DEEPGRAM_API_KEY:
34
+ required: true
35
+ description: "Deepgram API key for STT"
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@acpfx/stt-deepgram",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "acpfx-stt-deepgram": "./dist/index.js"
7
7
  },
8
8
  "main": "./dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "manifest.yaml"
12
+ ],
9
13
  "dependencies": {
10
- "@acpfx/core": "0.2.0",
11
- "@acpfx/node-sdk": "0.2.0"
14
+ "@acpfx/core": "0.4.0",
15
+ "@acpfx/node-sdk": "0.3.0"
12
16
  },
13
17
  "scripts": {
14
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external"
18
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external && node ../../scripts/copy-manifest.js"
15
19
  }
16
20
  }
package/CHANGELOG.md DELETED
@@ -1,21 +0,0 @@
1
- # @acpfx/stt-deepgram
2
-
3
- ## 0.2.0
4
-
5
- ### Minor Changes
6
-
7
- - d757640: Initial release: type-safe contracts, Rust orchestrator, manifest-driven event filtering
8
-
9
- - Rust schema crate as canonical event type source of truth with codegen to TypeScript + Zod
10
- - Node manifests (manifest.yaml) declaring consumes/emits contracts
11
- - Orchestrator event filtering: nodes only receive declared events
12
- - Rust orchestrator with ratatui TUI (--ui flag)
13
- - node-sdk with structured logging helpers
14
- - CI/CD with GitHub Actions and changesets
15
- - Platform-specific npm packages for Rust binaries (esbuild-style distribution)
16
-
17
- ### Patch Changes
18
-
19
- - Updated dependencies [d757640]
20
- - @acpfx/core@0.2.0
21
- - @acpfx/node-sdk@0.2.0
package/src/index.ts DELETED
@@ -1,244 +0,0 @@
1
- /**
2
- * stt-deepgram node — Deepgram Nova-3 Realtime STT with UtteranceEnd detection.
3
- *
4
- * Reads audio.chunk events from stdin, streams to Deepgram WebSocket,
5
- * emits speech.partial, speech.final, and speech.pause events.
6
- *
7
- * Uses UtteranceEnd for end-of-turn detection — analyzes word timing gaps,
8
- * ignores non-speech audio (won't false-trigger on SFX sounds).
9
- *
10
- * Settings (via ACPFX_SETTINGS):
11
- * language?: string — language code (default: "en")
12
- * apiKey?: string — Deepgram API key (falls back to DEEPGRAM_API_KEY env)
13
- * model?: string — STT model (default: "nova-3")
14
- * utteranceEndMs?: number — ms gap for utterance end (default: 1000)
15
- * endpointing?: number — VAD endpointing ms (default: 300)
16
- */
17
-
18
- import { emit, log, onEvent, handleManifestFlag } from "@acpfx/node-sdk";
19
-
20
- handleManifestFlag();
21
-
22
- const WS_URL = "wss://api.deepgram.com/v1/listen";
23
-
24
- type Settings = {
25
- language?: string;
26
- apiKey?: string;
27
- model?: string;
28
- utteranceEndMs?: number;
29
- endpointing?: number;
30
- };
31
-
32
- const settings: Settings = JSON.parse(process.env.ACPFX_SETTINGS || "{}");
33
- const API_KEY = settings.apiKey ?? process.env.DEEPGRAM_API_KEY ?? "";
34
- const LANGUAGE = settings.language ?? "en";
35
- const MODEL = settings.model ?? "nova-3";
36
- const UTTERANCE_END_MS = settings.utteranceEndMs ?? 1000;
37
- const ENDPOINTING = settings.endpointing ?? 300;
38
- const TRACK_ID = "stt";
39
-
40
- if (!API_KEY) {
41
- log.error("No API key. Set DEEPGRAM_API_KEY or settings.apiKey");
42
- process.exit(1);
43
- }
44
-
45
- let ws: WebSocket | null = null;
46
- let connected = false;
47
- let lastFinalText = "";
48
- let pendingText = "";
49
-
50
-
51
- async function connectWebSocket(): Promise<void> {
52
- const url =
53
- `${WS_URL}?model=${MODEL}` +
54
- `&language=${encodeURIComponent(LANGUAGE)}` +
55
- `&encoding=linear16` +
56
- `&sample_rate=16000` +
57
- `&channels=1` +
58
- `&interim_results=true` +
59
- `&punctuate=true` +
60
- `&smart_format=true` +
61
- `&utterance_end_ms=${UTTERANCE_END_MS}` +
62
- `&endpointing=${ENDPOINTING}` +
63
- `&vad_events=true`;
64
-
65
- ws = new WebSocket(url, ["token", API_KEY]);
66
-
67
- await new Promise<void>((resolve, reject) => {
68
- ws!.addEventListener(
69
- "open",
70
- () => {
71
- connected = true;
72
- log.info("Connected to Deepgram STT");
73
- resolve();
74
- },
75
- { once: true },
76
- );
77
-
78
- ws!.addEventListener(
79
- "error",
80
- () => {
81
- reject(new Error("WebSocket connection failed"));
82
- },
83
- { once: true },
84
- );
85
- });
86
-
87
- ws.addEventListener("message", (event: MessageEvent) => {
88
- try {
89
- const data =
90
- typeof event.data === "string"
91
- ? event.data
92
- : Buffer.from(event.data as ArrayBuffer).toString("utf-8");
93
- const msg = JSON.parse(data);
94
- handleServerMessage(msg);
95
- } catch {
96
- // ignore parse errors
97
- }
98
- });
99
-
100
- ws.addEventListener("error", (event: Event) => {
101
- log.error(`WebSocket error: ${(event as ErrorEvent).message ?? "unknown"}`);
102
- emit({
103
- type: "control.error",
104
- component: "stt-deepgram",
105
- message: "STT WebSocket error",
106
- fatal: false,
107
- });
108
- });
109
-
110
- ws.addEventListener("close", (event: CloseEvent) => {
111
- log.info(`WebSocket closed (code=${event.code})`);
112
- connected = false;
113
- });
114
- }
115
-
116
- function handleServerMessage(msg: Record<string, unknown>): void {
117
- const type = msg.type as string | undefined;
118
-
119
- // UtteranceEnd — speaker finished their turn (word-timing based, ignores noise)
120
- if (type === "UtteranceEnd") {
121
- if (pendingText) {
122
- emit({
123
- type: "speech.pause",
124
- trackId: TRACK_ID,
125
- pendingText,
126
- silenceMs: UTTERANCE_END_MS,
127
- });
128
- pendingText = "";
129
- }
130
- return;
131
- }
132
-
133
- // SpeechStarted — VAD detected speech beginning
134
- if (type === "SpeechStarted") {
135
- return;
136
- }
137
-
138
- // Transcription result
139
- if (type === "Results") {
140
- const channel = msg.channel as Record<string, unknown> | undefined;
141
- const alternatives = (channel?.alternatives as Array<Record<string, unknown>>) ?? [];
142
- if (alternatives.length === 0) return;
143
-
144
- const transcript = (alternatives[0].transcript as string) ?? "";
145
- const isFinal = msg.is_final === true;
146
- const speechFinal = msg.speech_final === true;
147
-
148
- if (!transcript) return;
149
-
150
- if (isFinal) {
151
- // Clear stale partial timer — proper final arrived
152
- // Final transcript for this segment
153
- lastFinalText = transcript;
154
- pendingText = transcript;
155
-
156
- emit({
157
- type: "speech.final",
158
- trackId: TRACK_ID,
159
- text: transcript,
160
- confidence: (alternatives[0].confidence as number) ?? undefined,
161
- });
162
-
163
- // If speech_final (endpointing detected silence), also emit pause
164
- if (speechFinal) {
165
- emit({
166
- type: "speech.pause",
167
- trackId: TRACK_ID,
168
- pendingText: transcript,
169
- silenceMs: ENDPOINTING,
170
- });
171
- pendingText = "";
172
- }
173
- } else {
174
- // Interim result — partial transcript
175
- emit({
176
- type: "speech.partial",
177
- trackId: TRACK_ID,
178
- text: transcript,
179
- });
180
- }
181
- }
182
- }
183
-
184
- function sendAudio(base64Pcm: string): void {
185
- if (!ws || !connected) return;
186
- const pcm = Buffer.from(base64Pcm, "base64");
187
- try {
188
- ws.send(pcm);
189
- } catch {
190
- // WebSocket may have closed
191
- }
192
- }
193
-
194
- function closeWebSocket(): void {
195
- connected = false;
196
- if (ws) {
197
- try {
198
- // Send close message per Deepgram protocol
199
- ws.send(JSON.stringify({ type: "CloseStream" }));
200
- ws.close();
201
- } catch {
202
- // ignore
203
- }
204
- ws = null;
205
- }
206
- }
207
-
208
- // --- Main ---
209
-
210
- async function main(): Promise<void> {
211
- await connectWebSocket();
212
-
213
- emit({ type: "lifecycle.ready", component: "stt-deepgram" });
214
-
215
- const rl = onEvent((event) => {
216
- if (event.type === "audio.chunk") {
217
- if (!connected) {
218
- connectWebSocket().then(() => {
219
- sendAudio(event.data as string);
220
- }).catch(() => {});
221
- } else {
222
- sendAudio(event.data as string);
223
- }
224
- } else if (event.type === "control.interrupt") {
225
- // Don't close WebSocket — STT should keep listening for barge-in.
226
- }
227
- });
228
-
229
- rl.on("close", () => {
230
- closeWebSocket();
231
- emit({ type: "lifecycle.done", component: "stt-deepgram" });
232
- process.exit(0);
233
- });
234
-
235
- process.on("SIGTERM", () => {
236
- closeWebSocket();
237
- process.exit(0);
238
- });
239
- }
240
-
241
- main().catch((err) => {
242
- log.error(`Fatal: ${err.message}`);
243
- process.exit(1);
244
- });