@framers/agentos-ext-streaming-stt-deepgram 0.2.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.
@@ -0,0 +1,462 @@
1
+ /**
2
+ * @file DeepgramStreamSession.ts
3
+ * @description Active streaming STT session backed by the Deepgram WebSocket API.
4
+ *
5
+ * {@link DeepgramStreamSession} implements the {@link StreamingSTTSession} interface
6
+ * (which extends `EventEmitter`) and adds Deepgram-specific convenience events:
7
+ *
8
+ * | Event | Payload | Description |
9
+ * |-----------------------|----------------------|-------------------------------------------------------|
10
+ * | `'transcript'` | `TranscriptEvent` | Every hypothesis (interim + final, per base interface)|
11
+ * | `'interim_transcript'`| `TranscriptEvent` | Non-final hypothesis only |
12
+ * | `'final_transcript'` | `TranscriptEvent` | Final (stable) hypothesis |
13
+ * | `'speech_start'` | — | First non-empty word received in this utterance |
14
+ * | `'speech_end'` | — | Deepgram `speech_final` flag fired |
15
+ * | `'error'` | `Error` | Unrecoverable provider error |
16
+ * | `'close'` | — | Session fully terminated |
17
+ *
18
+ * ### Auto-reconnect
19
+ * On an unexpected WebSocket close (code ≠ 1000/1001), the session buffers
20
+ * incoming audio frames (up to 5 s worth at 16 kHz / 512 samples per frame)
21
+ * and reattempts the connection with exponential back-off starting at 100 ms,
22
+ * doubling each attempt up to a cap of 5 000 ms.
23
+ *
24
+ * @module streaming-stt-deepgram/DeepgramStreamSession
25
+ */
26
+
27
+ import { EventEmitter } from 'node:events';
28
+ import WebSocket from 'ws';
29
+ import type { DeepgramStreamingConfig } from './types.js';
30
+ import { extractSpeakerFromWords, mapDeepgramWord } from './DeepgramDiarizationAdapter.js';
31
+ import type { DeepgramWord } from './DeepgramDiarizationAdapter.js';
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Voice pipeline type imports (shapes only — no runtime dep on agentos at test time)
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Minimal AudioFrame shape — mirrors packages/agentos/src/voice-pipeline/types.ts */
38
+ export interface AudioFrame {
39
+ samples: Float32Array;
40
+ sampleRate: number;
41
+ timestamp: number;
42
+ speakerHint?: string;
43
+ }
44
+
45
+ /** Minimal TranscriptWord shape. */
46
+ export interface TranscriptWord {
47
+ word: string;
48
+ start: number;
49
+ end: number;
50
+ confidence: number;
51
+ speaker?: string;
52
+ }
53
+
54
+ /** Minimal TranscriptEvent shape. */
55
+ export interface TranscriptEvent {
56
+ text: string;
57
+ confidence: number;
58
+ words: TranscriptWord[];
59
+ isFinal: boolean;
60
+ durationMs?: number;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Constants
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /** Deepgram WebSocket entry point for streaming recognition. */
68
+ const DEEPGRAM_WS_BASE = 'wss://api.deepgram.com/v1/listen';
69
+
70
+ /** Maximum number of audio frames to buffer during reconnect (~5 s at 16 kHz/512). */
71
+ const MAX_BUFFER_FRAMES = 500;
72
+
73
+ /** Initial reconnect back-off delay in milliseconds. */
74
+ const BACKOFF_INITIAL_MS = 100;
75
+
76
+ /** Maximum reconnect back-off delay in milliseconds. */
77
+ const BACKOFF_MAX_MS = 5_000;
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Helpers
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Convert a Float32Array of normalised PCM samples to a 16-bit signed integer
85
+ * PCM Buffer (little-endian), as required by the Deepgram `linear16` encoding.
86
+ *
87
+ * Samples are clamped to [-1, 1] before scaling to prevent wrap-around on
88
+ * values outside the normalised range.
89
+ *
90
+ * @param samples - Float32 PCM input, each sample in [-1, 1].
91
+ * @returns Node.js `Buffer` of Int16 samples (2 bytes each, little-endian).
92
+ */
93
+ function float32ToInt16Buffer(samples: Float32Array): Buffer {
94
+ const buf = Buffer.allocUnsafe(samples.length * 2);
95
+ for (let i = 0; i < samples.length; i++) {
96
+ // Clamp to [-1, 1] then scale to the Int16 range.
97
+ const clamped = Math.max(-1, Math.min(1, samples[i]!));
98
+ buf.writeInt16LE(Math.round(clamped * 0x7fff), i * 2);
99
+ }
100
+ return buf;
101
+ }
102
+
103
+ /**
104
+ * Build the Deepgram WebSocket URL from the resolved configuration.
105
+ *
106
+ * @param cfg - Resolved Deepgram streaming configuration.
107
+ * @returns Full `wss://` URL with query parameters.
108
+ */
109
+ function buildWsUrl(cfg: Required<Omit<DeepgramStreamingConfig, 'keywords' | 'endpointing'>> & DeepgramStreamingConfig): string {
110
+ const params = new URLSearchParams({
111
+ model: cfg.model,
112
+ language: cfg.language,
113
+ punctuate: String(cfg.punctuate),
114
+ interim_results: String(cfg.interimResults),
115
+ diarize: String(cfg.diarize),
116
+ encoding: 'linear16',
117
+ sample_rate: '16000',
118
+ channels: '1',
119
+ });
120
+
121
+ if (cfg.keywords && cfg.keywords.length > 0) {
122
+ for (const kw of cfg.keywords) {
123
+ params.append('keywords', kw);
124
+ }
125
+ }
126
+
127
+ if (cfg.endpointing !== false && cfg.endpointing !== undefined) {
128
+ params.set('endpointing', String(cfg.endpointing));
129
+ }
130
+
131
+ return `${DEEPGRAM_WS_BASE}?${params.toString()}`;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Main class
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Active streaming STT session backed by a Deepgram WebSocket connection.
140
+ *
141
+ * Construct via {@link DeepgramStreamingSTT.startSession} rather than directly.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const session = new DeepgramStreamSession({ apiKey: process.env.DEEPGRAM_API_KEY! });
146
+ *
147
+ * session.on('interim_transcript', (evt) => process.stdout.write(`\r${evt.text}`));
148
+ * session.on('final_transcript', (evt) => console.log(`FINAL: ${evt.text}`));
149
+ * session.on('speech_end', () => console.log('utterance ended'));
150
+ *
151
+ * // Push audio frames from a microphone source:
152
+ * microphone.on('frame', (frame) => session.pushAudio(frame));
153
+ *
154
+ * // Signal end of utterance:
155
+ * await session.flush();
156
+ * session.close();
157
+ * ```
158
+ */
159
+ export class DeepgramStreamSession extends EventEmitter {
160
+ // -------------------------------------------------------------------------
161
+ // Private state
162
+ // -------------------------------------------------------------------------
163
+
164
+ /** Resolved configuration with all defaults applied. */
165
+ private readonly cfg: DeepgramStreamingConfig & {
166
+ model: string;
167
+ language: string;
168
+ punctuate: boolean;
169
+ interimResults: boolean;
170
+ diarize: boolean;
171
+ };
172
+
173
+ /** Current WebSocket connection (replaced on reconnect). */
174
+ private ws: WebSocket | null = null;
175
+
176
+ /** `true` once the first non-empty transcript arrives in this utterance. */
177
+ private speechStarted = false;
178
+
179
+ /** `true` after {@link close} is called — prevents reconnect attempts. */
180
+ private closed = false;
181
+
182
+ /**
183
+ * Ring buffer of audio frames accumulated while the WebSocket is
184
+ * reconnecting. Capped at {@link MAX_BUFFER_FRAMES} to bound memory.
185
+ */
186
+ private reconnectBuffer: AudioFrame[] = [];
187
+
188
+ /** Current reconnect back-off delay in milliseconds. */
189
+ private backoffMs = BACKOFF_INITIAL_MS;
190
+
191
+ /** Active reconnect timer handle. */
192
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
193
+
194
+ /** `true` while the WebSocket is open and ready to accept binary data. */
195
+ private wsReady = false;
196
+
197
+ // -------------------------------------------------------------------------
198
+ // Constructor
199
+ // -------------------------------------------------------------------------
200
+
201
+ /**
202
+ * @param config - Deepgram streaming configuration. `apiKey` is required;
203
+ * all other fields have sensible defaults.
204
+ */
205
+ constructor(config: DeepgramStreamingConfig) {
206
+ super();
207
+
208
+ // Apply defaults.
209
+ this.cfg = {
210
+ model: 'nova-2',
211
+ language: 'en-US',
212
+ punctuate: true,
213
+ interimResults: true,
214
+ diarize: false,
215
+ ...config,
216
+ };
217
+
218
+ this.connect();
219
+ }
220
+
221
+ // -------------------------------------------------------------------------
222
+ // StreamingSTTSession interface
223
+ // -------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Push a raw PCM audio frame into the recognition stream.
227
+ *
228
+ * Converts Float32 samples to Int16 linear PCM and transmits them as binary
229
+ * over the WebSocket. Frames received while the socket is reconnecting are
230
+ * queued in {@link reconnectBuffer} (up to {@link MAX_BUFFER_FRAMES}).
231
+ *
232
+ * @param frame - Audio frame with normalised Float32 samples at any sample rate.
233
+ * Frames are sent as-is; resample to 16 kHz before calling if
234
+ * the source differs from the Deepgram session's `sample_rate`.
235
+ */
236
+ pushAudio(frame: AudioFrame): void {
237
+ if (this.closed) return;
238
+
239
+ const pcmBuffer = float32ToInt16Buffer(frame.samples);
240
+
241
+ if (this.wsReady && this.ws) {
242
+ this.ws.send(pcmBuffer);
243
+ } else {
244
+ // Buffer frames during reconnect — drop the oldest when full.
245
+ if (this.reconnectBuffer.length >= MAX_BUFFER_FRAMES) {
246
+ this.reconnectBuffer.shift();
247
+ }
248
+ this.reconnectBuffer.push(frame);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Signal end-of-utterance to Deepgram.
254
+ *
255
+ * Sends the JSON `{"type":"CloseStream"}` control message, instructing
256
+ * Deepgram to flush any buffered audio and emit a final transcript before
257
+ * closing the connection from their side.
258
+ *
259
+ * @returns A promise that resolves immediately after the message is enqueued.
260
+ */
261
+ async flush(): Promise<void> {
262
+ if (this.ws && this.wsReady) {
263
+ this.ws.send(JSON.stringify({ type: 'CloseStream' }));
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Immediately terminate the session without waiting for a final result.
269
+ *
270
+ * Cancels any pending reconnect timer, marks the session as closed, and
271
+ * terminates the underlying WebSocket. After `close()` no further events
272
+ * are emitted.
273
+ */
274
+ close(): void {
275
+ this.closed = true;
276
+
277
+ if (this.reconnectTimer !== null) {
278
+ clearTimeout(this.reconnectTimer);
279
+ this.reconnectTimer = null;
280
+ }
281
+
282
+ if (this.ws) {
283
+ this.ws.terminate();
284
+ this.ws = null;
285
+ }
286
+
287
+ this.wsReady = false;
288
+ this.emit('close');
289
+ }
290
+
291
+ // -------------------------------------------------------------------------
292
+ // Internal — WebSocket lifecycle
293
+ // -------------------------------------------------------------------------
294
+
295
+ /**
296
+ * Open (or reopen) the Deepgram WebSocket connection.
297
+ *
298
+ * Attaches `open`, `message`, `error`, and `close` handlers. On successful
299
+ * open the reconnect buffer is drained in order.
300
+ */
301
+ private connect(): void {
302
+ if (this.closed) return;
303
+
304
+ const url = buildWsUrl(this.cfg as Parameters<typeof buildWsUrl>[0]);
305
+
306
+ const socket = new WebSocket(url, {
307
+ headers: {
308
+ Authorization: `Token ${this.cfg.apiKey}`,
309
+ },
310
+ });
311
+
312
+ this.ws = socket;
313
+
314
+ socket.on('open', () => {
315
+ this.wsReady = true;
316
+ this.backoffMs = BACKOFF_INITIAL_MS; // reset on successful connect
317
+
318
+ // Drain buffered frames accumulated during the reconnect window.
319
+ const buffered = this.reconnectBuffer.splice(0);
320
+ for (const frame of buffered) {
321
+ const pcm = float32ToInt16Buffer(frame.samples);
322
+ socket.send(pcm);
323
+ }
324
+ });
325
+
326
+ socket.on('message', (data: WebSocket.RawData) => {
327
+ try {
328
+ const msg = JSON.parse(data.toString()) as DeepgramMessage;
329
+ this.handleMessage(msg);
330
+ } catch {
331
+ // Ignore malformed frames — Deepgram occasionally sends keep-alive
332
+ // empty strings or non-JSON control bytes.
333
+ }
334
+ });
335
+
336
+ socket.on('error', (err: Error) => {
337
+ this.emit('error', err);
338
+ });
339
+
340
+ socket.on('close', (code: number) => {
341
+ this.wsReady = false;
342
+ this.ws = null;
343
+
344
+ // Normal closure (1000 = Normal, 1001 = Going Away) or explicit close().
345
+ const isNormal = code === 1000 || code === 1001;
346
+ if (isNormal || this.closed) {
347
+ if (!this.closed) {
348
+ // Deepgram closed normally after a CloseStream — emit session close.
349
+ this.emit('close');
350
+ }
351
+ return;
352
+ }
353
+
354
+ // Unexpected drop — schedule reconnect with exponential back-off.
355
+ this.scheduleReconnect();
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Schedule a reconnect attempt after the current {@link backoffMs} delay,
361
+ * then double the delay for the next attempt (capped at {@link BACKOFF_MAX_MS}).
362
+ */
363
+ private scheduleReconnect(): void {
364
+ if (this.closed) return;
365
+
366
+ const delay = this.backoffMs;
367
+ this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
368
+
369
+ this.reconnectTimer = setTimeout(() => {
370
+ this.reconnectTimer = null;
371
+ this.connect();
372
+ }, delay);
373
+ }
374
+
375
+ // -------------------------------------------------------------------------
376
+ // Internal — message parsing
377
+ // -------------------------------------------------------------------------
378
+
379
+ /**
380
+ * Process a parsed Deepgram WebSocket message and emit the appropriate events.
381
+ *
382
+ * Deepgram sends two primary result types:
383
+ * - `Results` — transcript hypotheses with `is_final` and `speech_final` flags.
384
+ * - `Metadata` — session-level metadata (ignored here).
385
+ *
386
+ * @param msg - Parsed Deepgram JSON message.
387
+ */
388
+ private handleMessage(msg: DeepgramMessage): void {
389
+ if (msg.type !== 'Results') return;
390
+
391
+ const alt = msg.channel?.alternatives?.[0];
392
+ if (!alt) return;
393
+
394
+ const text = alt.transcript ?? '';
395
+ const confidence = alt.confidence ?? 0;
396
+ const rawWords: DeepgramWord[] = alt.words ?? [];
397
+
398
+ const words = rawWords.map(mapDeepgramWord);
399
+
400
+ // Determine majority speaker when diarization is enabled.
401
+ const majoritySpeaker = this.cfg.diarize
402
+ ? extractSpeakerFromWords(rawWords)
403
+ : undefined;
404
+
405
+ // Suppress empty interim frames — Deepgram emits these as silence padding.
406
+ if (text.trim() === '' && !msg.is_final) return;
407
+
408
+ // Emit 'speech_start' on the first non-empty transcript in this utterance.
409
+ if (!this.speechStarted && text.trim() !== '') {
410
+ this.speechStarted = true;
411
+ this.emit('speech_start');
412
+ }
413
+
414
+ const event: TranscriptEvent = {
415
+ text,
416
+ confidence,
417
+ words,
418
+ isFinal: msg.is_final ?? false,
419
+ durationMs: msg.duration !== undefined ? Math.round(msg.duration * 1000) : undefined,
420
+ };
421
+
422
+ // Attach majority speaker to the event as a convenience property.
423
+ if (majoritySpeaker) {
424
+ (event as TranscriptEvent & { speaker?: string }).speaker = majoritySpeaker;
425
+ }
426
+
427
+ // Base interface event — always emitted for both interim and final results.
428
+ this.emit('transcript', event);
429
+
430
+ if (msg.is_final) {
431
+ this.emit('final_transcript', event);
432
+
433
+ // Reset speech-started tracker for the next utterance.
434
+ this.speechStarted = false;
435
+
436
+ if (msg.speech_final) {
437
+ this.emit('speech_end');
438
+ }
439
+ } else {
440
+ this.emit('interim_transcript', event);
441
+ }
442
+ }
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Internal Deepgram message shapes
447
+ // ---------------------------------------------------------------------------
448
+
449
+ /** Parsed shape of a Deepgram WebSocket `Results` message. */
450
+ interface DeepgramMessage {
451
+ type: string;
452
+ is_final?: boolean;
453
+ speech_final?: boolean;
454
+ duration?: number;
455
+ channel?: {
456
+ alternatives?: Array<{
457
+ transcript?: string;
458
+ confidence?: number;
459
+ words?: DeepgramWord[];
460
+ }>;
461
+ };
462
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file DeepgramStreamingSTT.ts
3
+ * @description {@link IStreamingSTT} factory implementation backed by Deepgram.
4
+ *
5
+ * {@link DeepgramStreamingSTT} is a thin factory that creates
6
+ * {@link DeepgramStreamSession} instances on demand. It holds no persistent
7
+ * state other than the API key and a reference count used to implement
8
+ * {@link isStreaming}.
9
+ *
10
+ * @module streaming-stt-deepgram/DeepgramStreamingSTT
11
+ */
12
+
13
+ import { DeepgramStreamSession } from './DeepgramStreamSession.js';
14
+ import type { DeepgramStreamingConfig } from './types.js';
15
+
16
+ /** Minimal re-export of the StreamingSTTConfig shape used by this adapter. */
17
+ export interface StreamingSTTConfig {
18
+ language?: string;
19
+ interimResults?: boolean;
20
+ punctuate?: boolean;
21
+ profanityFilter?: boolean;
22
+ providerOptions?: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * {@link IStreamingSTT}-compatible factory for Deepgram streaming sessions.
27
+ *
28
+ * Instantiate once per agent and reuse for the lifetime of the voice pipeline.
29
+ * Each call to {@link startSession} opens an independent WebSocket connection
30
+ * to Deepgram so that multiple concurrent sessions are supported.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const stt = new DeepgramStreamingSTT(process.env.DEEPGRAM_API_KEY!);
35
+ * const session = await stt.startSession({ language: 'fr-FR' });
36
+ *
37
+ * session.on('final_transcript', (evt) => console.log(evt.text));
38
+ * microphone.on('frame', (f) => session.pushAudio(f));
39
+ * ```
40
+ */
41
+ export class DeepgramStreamingSTT {
42
+ /**
43
+ * Stable provider identifier used by the voice pipeline to select between
44
+ * registered STT implementations.
45
+ */
46
+ readonly providerId = 'deepgram-streaming';
47
+
48
+ /** `true` while at least one session has been created and not yet closed. */
49
+ get isStreaming(): boolean {
50
+ return this._activeSessions > 0;
51
+ }
52
+
53
+ /** Count of sessions opened but not yet closed. */
54
+ private _activeSessions = 0;
55
+
56
+ /**
57
+ * @param apiKey - Deepgram API key. Passed through to every session.
58
+ */
59
+ constructor(private readonly apiKey: string) {}
60
+
61
+ /**
62
+ * Open a new streaming recognition session.
63
+ *
64
+ * Provider-specific options can be forwarded via `config.providerOptions`
65
+ * under the following keys:
66
+ * - `model` — Deepgram model name (default `'nova-2'`).
67
+ * - `diarize` — Enable speaker diarization (default `false`).
68
+ * - `keywords` — Custom keyword list (default `[]`).
69
+ * - `endpointing` — Native endpointing duration ms, or `false`.
70
+ *
71
+ * @param config - Generic session config merged with Deepgram defaults.
72
+ * @returns A ready-to-use {@link DeepgramStreamSession}.
73
+ */
74
+ async startSession(config?: StreamingSTTConfig): Promise<DeepgramStreamSession> {
75
+ const provOpts = (config?.providerOptions ?? {}) as Partial<DeepgramStreamingConfig>;
76
+
77
+ const sessionConfig: DeepgramStreamingConfig = {
78
+ apiKey: this.apiKey,
79
+ language: config?.language ?? provOpts.language,
80
+ punctuate: config?.punctuate ?? provOpts.punctuate,
81
+ interimResults: config?.interimResults ?? provOpts.interimResults,
82
+ model: provOpts.model,
83
+ diarize: provOpts.diarize,
84
+ keywords: provOpts.keywords,
85
+ endpointing: provOpts.endpointing,
86
+ };
87
+
88
+ const session = new DeepgramStreamSession(sessionConfig);
89
+
90
+ this._activeSessions++;
91
+ session.once('close', () => {
92
+ this._activeSessions = Math.max(0, this._activeSessions - 1);
93
+ });
94
+
95
+ return session;
96
+ }
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Deepgram Streaming STT extension pack.
4
+ *
5
+ * This module exports the main {@link createDeepgramStreamingSTT} factory
6
+ * function and the {@link createExtensionPack} bridge function that conforms
7
+ * to the AgentOS manifest factory convention.
8
+ *
9
+ * ### Usage (direct)
10
+ * ```ts
11
+ * import { createDeepgramStreamingSTT } from '@framers/agentos-ext-streaming-stt-deepgram';
12
+ *
13
+ * const stt = createDeepgramStreamingSTT(process.env.DEEPGRAM_API_KEY!);
14
+ * const session = await stt.startSession({ language: 'en-US' });
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-streaming-stt-deepgram" }] }
20
+ * ```
21
+ *
22
+ * @module streaming-stt-deepgram
23
+ */
24
+
25
+ import { DeepgramStreamingSTT } from './DeepgramStreamingSTT.js';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types mirrored locally to avoid a hard runtime dependency on @framers/agentos
29
+ // in environments that load this pack before agentos is available.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Subset of ExtensionDescriptor required by this pack. */
33
+ interface ExtensionDescriptor {
34
+ id: string;
35
+ kind: string;
36
+ payload: unknown;
37
+ enableByDefault?: boolean;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+
41
+ /** Subset of ExtensionPack required by this pack. */
42
+ interface ExtensionPack {
43
+ id: string;
44
+ descriptors: ExtensionDescriptor[];
45
+ }
46
+
47
+ /** Subset of ExtensionPackContext required by this pack. */
48
+ interface ExtensionPackContext {
49
+ getSecret?: (id: string) => string | undefined;
50
+ options?: Record<string, unknown>;
51
+ }
52
+
53
+ /** Kind constant matching packages/agentos/src/extensions/types.ts line ~410. */
54
+ const EXTENSION_KIND_STREAMING_STT = 'streaming-stt-provider';
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Factory
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Create a standalone {@link DeepgramStreamingSTT} instance.
62
+ *
63
+ * Use this when composing the provider programmatically outside of the
64
+ * AgentOS extension system.
65
+ *
66
+ * @param apiKey - Deepgram API key.
67
+ * @returns Configured {@link DeepgramStreamingSTT}.
68
+ */
69
+ export function createDeepgramStreamingSTT(apiKey: string): DeepgramStreamingSTT {
70
+ return new DeepgramStreamingSTT(apiKey);
71
+ }
72
+
73
+ /**
74
+ * AgentOS manifest factory function.
75
+ *
76
+ * Reads the `DEEPGRAM_API_KEY` secret from the context and returns an
77
+ * {@link ExtensionPack} containing a single `streaming-stt-provider`
78
+ * descriptor backed by {@link DeepgramStreamingSTT}.
79
+ *
80
+ * @param context - Pack context supplied by the extension manager.
81
+ * @returns A fully configured {@link ExtensionPack}.
82
+ */
83
+ export function createExtensionPack(context: ExtensionPackContext): ExtensionPack {
84
+ const apiKey = context.getSecret?.('DEEPGRAM_API_KEY') ?? '';
85
+ const stt = new DeepgramStreamingSTT(apiKey);
86
+
87
+ return {
88
+ id: 'streaming-stt-deepgram',
89
+ descriptors: [
90
+ {
91
+ id: 'deepgram-streaming-stt',
92
+ kind: EXTENSION_KIND_STREAMING_STT,
93
+ payload: stt,
94
+ enableByDefault: true,
95
+ metadata: { providerId: 'deepgram-streaming' },
96
+ },
97
+ ],
98
+ };
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Re-exports
103
+ // ---------------------------------------------------------------------------
104
+
105
+ export * from './types.js';
106
+ export { DeepgramStreamingSTT } from './DeepgramStreamingSTT.js';
107
+ export { DeepgramStreamSession } from './DeepgramStreamSession.js';
108
+ export {
109
+ extractSpeakerFromWords,
110
+ mapDeepgramWord,
111
+ } from './DeepgramDiarizationAdapter.js';
112
+ export type { DeepgramWord } from './DeepgramDiarizationAdapter.js';