@framers/agentos 0.13.77 → 0.13.79

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.
Files changed (45) hide show
  1. package/dist/agents/agency/AgentCommunicationBus.d.ts +11 -1
  2. package/dist/agents/agency/AgentCommunicationBus.d.ts.map +1 -1
  3. package/dist/agents/agency/AgentCommunicationBus.js +15 -3
  4. package/dist/agents/agency/AgentCommunicationBus.js.map +1 -1
  5. package/dist/agents/agency/IAgentCommunicationBus.d.ts +5 -1
  6. package/dist/agents/agency/IAgentCommunicationBus.d.ts.map +1 -1
  7. package/dist/io/voice-pipeline/AcousticEndpointDetector.d.ts +2 -0
  8. package/dist/io/voice-pipeline/AcousticEndpointDetector.d.ts.map +1 -1
  9. package/dist/io/voice-pipeline/AcousticEndpointDetector.js +1 -0
  10. package/dist/io/voice-pipeline/AcousticEndpointDetector.js.map +1 -1
  11. package/dist/io/voice-pipeline/HeuristicEndpointDetector.d.ts +2 -0
  12. package/dist/io/voice-pipeline/HeuristicEndpointDetector.d.ts.map +1 -1
  13. package/dist/io/voice-pipeline/HeuristicEndpointDetector.js +4 -0
  14. package/dist/io/voice-pipeline/HeuristicEndpointDetector.js.map +1 -1
  15. package/dist/io/voice-pipeline/ListenSession.d.ts +111 -0
  16. package/dist/io/voice-pipeline/ListenSession.d.ts.map +1 -0
  17. package/dist/io/voice-pipeline/ListenSession.js +242 -0
  18. package/dist/io/voice-pipeline/ListenSession.js.map +1 -0
  19. package/dist/io/voice-pipeline/ReconnectingSTT.d.ts +90 -0
  20. package/dist/io/voice-pipeline/ReconnectingSTT.d.ts.map +1 -0
  21. package/dist/io/voice-pipeline/ReconnectingSTT.js +318 -0
  22. package/dist/io/voice-pipeline/ReconnectingSTT.js.map +1 -0
  23. package/dist/io/voice-pipeline/VoicePipelineOrchestrator.d.ts +4 -0
  24. package/dist/io/voice-pipeline/VoicePipelineOrchestrator.d.ts.map +1 -1
  25. package/dist/io/voice-pipeline/VoicePipelineOrchestrator.js +11 -0
  26. package/dist/io/voice-pipeline/VoicePipelineOrchestrator.js.map +1 -1
  27. package/dist/io/voice-pipeline/env-constructor.d.ts +17 -9
  28. package/dist/io/voice-pipeline/env-constructor.d.ts.map +1 -1
  29. package/dist/io/voice-pipeline/env-constructor.js +6 -2
  30. package/dist/io/voice-pipeline/env-constructor.js.map +1 -1
  31. package/dist/io/voice-pipeline/index.d.ts +6 -0
  32. package/dist/io/voice-pipeline/index.d.ts.map +1 -1
  33. package/dist/io/voice-pipeline/index.js +7 -0
  34. package/dist/io/voice-pipeline/index.js.map +1 -1
  35. package/dist/io/voice-pipeline/providers/StreamingSTTChain.d.ts +9 -5
  36. package/dist/io/voice-pipeline/providers/StreamingSTTChain.d.ts.map +1 -1
  37. package/dist/io/voice-pipeline/providers/StreamingSTTChain.js +44 -8
  38. package/dist/io/voice-pipeline/providers/StreamingSTTChain.js.map +1 -1
  39. package/dist/io/voice-pipeline/types.d.ts +6 -0
  40. package/dist/io/voice-pipeline/types.d.ts.map +1 -1
  41. package/dist/orchestration/runtime/GraphRuntime.d.ts +4 -1
  42. package/dist/orchestration/runtime/GraphRuntime.d.ts.map +1 -1
  43. package/dist/orchestration/runtime/GraphRuntime.js +38 -16
  44. package/dist/orchestration/runtime/GraphRuntime.js.map +1 -1
  45. package/package.json +1 -1
@@ -0,0 +1,242 @@
1
+ /**
2
+ * @module voice-pipeline/ListenSession
3
+ *
4
+ * A session that listens and writes: a transport's audio into a streaming
5
+ * transcriber, each final (or each detector turn) into a sink as a numbered
6
+ * turn. No synthesis, no barge-in, no agent turn. Built for a notetaker, a
7
+ * meeting bot or a transcription job, where {@link VoicePipelineOrchestrator}
8
+ * is built for a conversation.
9
+ */
10
+ import { EventEmitter } from 'node:events';
11
+ import { ReconnectingSTT } from './ReconnectingSTT.js';
12
+ /**
13
+ * Starts listening. Emits `turn` (a {@link TranscriptTurn}), `interim` (a {@link TranscriptEvent}), `stt:reconnected`,
14
+ * `stt:gap`, `paused`, `resumed` and `ended` (`{ reason, error? }`).
15
+ *
16
+ * The transport's frames go to the transcriber. Each final, or with an endpoint detector each `turn_complete`, goes
17
+ * to the sink, one write at a time in arrival order; the sink numbers it. When `sinkQueueLimit` finals wait for the
18
+ * sink, the session emits `pause` on the transport and `paused` on itself, and `resume` and `resumed` once the queue
19
+ * is down to half or the session ends; a final is never dropped for a slow sink. The transcriber's `speech_start`
20
+ * and `speech_end` reach the detector as voice activity, as the orchestrator gives them. The session ends once, for the first
21
+ * {@link ListenEndReason}; it closes the transcriber and leaves the transport open, since the transport is the
22
+ * caller's. A transport error ends it with `transport_failed` and the error in `ended`. Its transport listeners
23
+ * are removed on every end. An empty detector turn uses the accumulated STT finals and their timing.
24
+ *
25
+ * @param transport - Where the audio comes from: the session listens for its `audio`, `close` and `error`.
26
+ * @param sink - Where the turns go; it assigns each turn's number and id.
27
+ * @param overrides - The transcriber, the run and the session's options.
28
+ * @returns The session, listening.
29
+ * @throws {Error} When the transcriber's first session fails to open.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const session = await startListenSession(transport, sink, {
34
+ * streamingSTT: new DeepgramStreamingSTT({ apiKey: process.env.DEEPGRAM_API_KEY! }),
35
+ * runId: 'run-1',
36
+ * });
37
+ * session.on('ended', ({ reason }: { reason: ListenEndReason }) => console.log(`listening ended: ${reason}`));
38
+ * await session.close('stopped');
39
+ * ```
40
+ */
41
+ export async function startListenSession(transport, sink, overrides) {
42
+ const stt = overrides.reconnect === false ? overrides.streamingSTT : new ReconnectingSTT(overrides.streamingSTT, typeof overrides.reconnect === 'object' ? overrides.reconnect : {});
43
+ const sttSession = await stt.startSession(overrides.sttConfig);
44
+ const detector = overrides.endpointDetector;
45
+ const queueLimit = overrides.sinkQueueLimit ?? 8;
46
+ const drainMs = overrides.drainMs ?? 250;
47
+ const emitter = new EventEmitter();
48
+ let state = 'listening';
49
+ let turns = 0;
50
+ let dropped = 0;
51
+ let reconnects = 0;
52
+ let paused = false;
53
+ let sinkFailed = false;
54
+ const pending = [];
55
+ const finals = [];
56
+ let speechEnded = false;
57
+ let writing = Promise.resolve();
58
+ let endPromise;
59
+ const accepting = () => state === 'listening' || state === 'draining';
60
+ const resume = () => {
61
+ if (!paused)
62
+ return;
63
+ paused = false;
64
+ transport.emit('resume');
65
+ emitter.emit('resumed');
66
+ };
67
+ const drainDetector = async () => {
68
+ if (!detector || finals.length === 0)
69
+ return;
70
+ await new Promise((resolve) => {
71
+ const done = () => {
72
+ clearTimeout(timer);
73
+ detector.off('turn_complete', done);
74
+ resolve();
75
+ };
76
+ // One timer tick lets a detector scheduled by the synthetic speech_end
77
+ // below emit at its bound before the fallback removes its listener.
78
+ const timer = setTimeout(done, (detector.turnCompletionTimeoutMs ?? 3250) + 1);
79
+ detector.on('turn_complete', done);
80
+ // A provider need not report speech_end when flushed. End the input so
81
+ // the detector can start its silence timer, without restarting a pending one.
82
+ if (!speechEnded)
83
+ detector.pushVadEvent({ type: 'speech_end', timestamp: Date.now(), source: 'stt' });
84
+ });
85
+ };
86
+ /** One end for every path. The writer calls it with `fromWriter` and never waits for its own chain. */
87
+ const end = (reason, error, fromWriter = false) => {
88
+ if (endPromise)
89
+ return endPromise;
90
+ // Reserve the end before cleanup emits resume: its listeners may call close again.
91
+ let resolveEnd = () => { };
92
+ let rejectEnd = () => { };
93
+ endPromise = new Promise((resolve, reject) => { resolveEnd = resolve; rejectEnd = reject; });
94
+ void (async () => {
95
+ if (reason === 'stopped') {
96
+ // the transcriber is flushed and its last finals are still written
97
+ state = 'draining';
98
+ try {
99
+ await sttSession.flush();
100
+ }
101
+ catch {
102
+ // the transcriber is gone; nothing to drain
103
+ }
104
+ await new Promise((r) => setTimeout(r, drainMs));
105
+ await drainDetector();
106
+ }
107
+ state = 'ending';
108
+ transport.off('audio', onAudio);
109
+ transport.off('close', onTransportClose);
110
+ detector?.off('turn_complete', onTurnComplete);
111
+ finals.length = 0;
112
+ if (reason === 'purged')
113
+ pending.length = 0;
114
+ resume();
115
+ sttSession.close();
116
+ detector?.reset();
117
+ if (!fromWriter)
118
+ await writing;
119
+ transport.off('error', onTransportError);
120
+ state = 'ended';
121
+ emitter.emit('ended', error ? { reason, error } : { reason });
122
+ })().then(resolveEnd, rejectEnd);
123
+ return endPromise;
124
+ };
125
+ const write = (text, event) => {
126
+ if (!accepting() || sinkFailed)
127
+ return;
128
+ const turn = { runId: overrides.runId, at: Date.now(), text, startMs: event.startMs, endMs: event.endMs, speaker: overrides.speaker?.(event) };
129
+ pending.push(turn);
130
+ if (pending.length >= queueLimit && !paused) {
131
+ paused = true;
132
+ transport.emit('pause');
133
+ emitter.emit('paused');
134
+ }
135
+ writing = writing.then(async () => {
136
+ const next = pending.shift();
137
+ if (!next || sinkFailed)
138
+ return;
139
+ try {
140
+ const stored = await sink.appendNext(next);
141
+ if (stored) {
142
+ turns += 1;
143
+ emitter.emit('turn', stored);
144
+ }
145
+ }
146
+ catch (err) {
147
+ sinkFailed = true;
148
+ pending.length = 0;
149
+ void end('sink_failed', err instanceof Error ? err : new Error(String(err)), true);
150
+ return;
151
+ }
152
+ if (paused && pending.length < Math.ceil(queueLimit / 2)) {
153
+ resume();
154
+ }
155
+ });
156
+ };
157
+ const onAudio = (frame) => {
158
+ if (state !== 'listening')
159
+ return;
160
+ if (paused)
161
+ dropped += 1; // the transport was asked to pause; a frame that still comes is counted, and the transcriber keeps its own buffer
162
+ sttSession.pushAudio(frame);
163
+ };
164
+ const onTransportClose = () => {
165
+ void end('transport_closed');
166
+ };
167
+ const onTransportError = (error) => {
168
+ void end('transport_failed', error);
169
+ };
170
+ const onTurnComplete = (turn) => {
171
+ if (!accepting())
172
+ return;
173
+ const accumulated = finals.splice(0);
174
+ if (!turn.transcript.trim()) {
175
+ const text = accumulated.map((event) => event.text).filter(Boolean).join(' ');
176
+ if (text)
177
+ write(text, {
178
+ text, confidence: accumulated[accumulated.length - 1].confidence,
179
+ words: accumulated.flatMap((event) => event.words), isFinal: true,
180
+ startMs: accumulated[0].startMs, endMs: accumulated[accumulated.length - 1].endMs,
181
+ });
182
+ return;
183
+ }
184
+ // a detector turn has its text and confidence, and no audio boundaries of its own
185
+ write(turn.transcript, { text: turn.transcript, confidence: turn.confidence, words: [], isFinal: true, durationMs: turn.durationMs });
186
+ };
187
+ sttSession.on('transcript', (event) => {
188
+ if (!accepting())
189
+ return;
190
+ if (detector) {
191
+ if (event.isFinal)
192
+ finals.push(event);
193
+ detector.pushTranscript(event);
194
+ if (!event.isFinal)
195
+ emitter.emit('interim', event);
196
+ return;
197
+ }
198
+ if (event.isFinal)
199
+ write(event.text, event);
200
+ else
201
+ emitter.emit('interim', event);
202
+ });
203
+ const onVad = (event) => {
204
+ if (!accepting())
205
+ return;
206
+ if (event.type === 'speech_start')
207
+ speechEnded = false;
208
+ else if (event.type === 'speech_end')
209
+ speechEnded = true;
210
+ detector?.pushVadEvent(event);
211
+ };
212
+ sttSession.on('vad', onVad);
213
+ // the transcriber's own speech events reach the detector as voice activity, as the orchestrator gives them
214
+ sttSession.on('speech_start', () => onVad({ type: 'speech_start', timestamp: Date.now(), source: 'stt' }));
215
+ sttSession.on('speech_end', () => onVad({ type: 'speech_end', timestamp: Date.now(), source: 'stt' }));
216
+ sttSession.on('stt:reconnected', (e) => {
217
+ reconnects += 1;
218
+ emitter.emit('stt:reconnected', e);
219
+ });
220
+ sttSession.on('stt:gap', (e) => emitter.emit('stt:gap', e));
221
+ sttSession.on('error', (err) => {
222
+ if (state === 'listening')
223
+ void end('stt_failed', err);
224
+ });
225
+ sttSession.on('close', () => {
226
+ if (state === 'listening')
227
+ void end('stt_failed', new Error('the transcriber closed'));
228
+ });
229
+ detector?.on('turn_complete', onTurnComplete);
230
+ transport.on('audio', onAudio);
231
+ transport.on('close', onTransportClose);
232
+ transport.on('error', onTransportError);
233
+ const session = Object.assign(emitter, {
234
+ runId: overrides.runId,
235
+ transport,
236
+ stats: () => ({ turns, dropped, reconnects }),
237
+ close: (reason = 'stopped') => end(reason),
238
+ });
239
+ Object.defineProperty(session, 'state', { get: () => state, enumerable: true });
240
+ return session;
241
+ }
242
+ //# sourceMappingURL=ListenSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListenSession.js","sourceRoot":"","sources":["../../../src/io/voice-pipeline/ListenSession.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import type { IStreamingSTT, StreamingSTTConfig, StreamingSTTSession } from './types.js';
2
+ /** Options of {@link ReconnectingSTT}. */
3
+ export interface ReconnectingSTTOptions {
4
+ /** How much audio is kept for replay. Default 3000. */
5
+ ringBufferCapacityMs?: number;
6
+ /** Replays below this are skipped. Default 400. */
7
+ minReplayMs?: number;
8
+ /** Attempts allowed in any sixty seconds before the session gives up. Default 3. */
9
+ maxAttemptsPerMinute?: number;
10
+ /** Frames queued while a reconnect runs; beyond it the oldest are dropped and counted. Default 2000 ms. */
11
+ queueCapacityMs?: number;
12
+ /** Milliseconds now, for the attempt window and the gap's length. Default `Date.now`. */
13
+ clock?: () => number;
14
+ }
15
+ /** Payload of a session's `stt:reconnected`: a new session of the provider took over. */
16
+ export interface SttReconnectedEvent {
17
+ /** The attempt's number among the attempts of the last sixty seconds, from 1. */
18
+ attempt: number;
19
+ /** Milliseconds of buffered audio replayed into the new session; 0 when there was less than `minReplayMs`. */
20
+ replayedMs: number;
21
+ }
22
+ /** Payload of a session's `stt:gap`, emitted right after `stt:reconnected`. */
23
+ export interface SttGapEvent {
24
+ /** The attempt's number, as in {@link SttReconnectedEvent}. */
25
+ attempt: number;
26
+ /**
27
+ * Milliseconds the reconnect took, by the options' clock. The audio pushed in that time was queued for the new
28
+ * session, less the oldest frames a full queue dropped (the session's `droppedFrames`).
29
+ */
30
+ lostMs: number;
31
+ }
32
+ /**
33
+ * An {@link IStreamingSTT} over one provider whose session is reconnected.
34
+ *
35
+ * An `error` or `close` of the provider's session that the caller did not ask
36
+ * for opens a new session of the same provider, at most
37
+ * `maxAttemptsPerMinute` times in any sixty seconds. The new session hears the
38
+ * buffered audio first (the last `ringBufferCapacityMs`, when there is at least
39
+ * `minReplayMs` of it), then the frames pushed while it opened, then live
40
+ * audio. Its `startMs` and `endMs` are moved onto one timeline, the audio
41
+ * pushed since the session began, and a final that repeats one already
42
+ * emitted over the same audio is dropped. Only finals with finite start and end
43
+ * bounds and a positive duration enter the dedupe history; others pass through.
44
+ * Queue overflow leaves a gap after the replay, preserving the original times
45
+ * of both replayed and retained audio. Each reconnect emits
46
+ * `stt:reconnected` ({@link SttReconnectedEvent}) and `stt:gap`
47
+ * ({@link SttGapEvent}). Past the cap the session emits `error`, then `close`.
48
+ *
49
+ * The session forwards `transcript`, `vad`, `speech_start`, `speech_end`,
50
+ * `usage` and `warning` from the provider's sessions; `usage` comes from every
51
+ * one of them, a replaced one too, since a session reports its audio as it
52
+ * closes. `flush()` during a reconnect waits for the replacement, replay and
53
+ * queued audio before flushing it. A close during that flush, or after it with
54
+ * no further provider activity or pushed audio, ends the stream without a
55
+ * reconnect. Activity after a completed flush makes a later close a failure.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const stt = new ReconnectingSTT(new DeepgramStreamingSTT({ apiKey: process.env.DEEPGRAM_API_KEY! }));
60
+ * const session = await stt.startSession({ language: 'en-US' });
61
+ * session.on('stt:gap', ({ lostMs }: SttGapEvent) => console.log(`the transcriber was away for ${lostMs} ms`));
62
+ * ```
63
+ */
64
+ export declare class ReconnectingSTT implements IStreamingSTT {
65
+ private readonly inner;
66
+ /** `reconnecting:` followed by the inner provider's id. */
67
+ readonly providerId: string;
68
+ private readonly capacityMs;
69
+ private readonly minReplayMs;
70
+ private readonly maxAttempts;
71
+ private readonly queueCapacityMs;
72
+ private readonly clock;
73
+ /**
74
+ * @param inner - The provider each session opens, again on every reconnect.
75
+ * @param options - The replay buffer, the attempt cap, the queue and the clock.
76
+ */
77
+ constructor(inner: IStreamingSTT, options?: ReconnectingSTTOptions);
78
+ /** Whether the inner provider has a session open. */
79
+ get isStreaming(): boolean;
80
+ /**
81
+ * Opens a session of the inner provider behind a session that reconnects it.
82
+ *
83
+ * @param config - Passed to the inner provider for the first session and for every reconnect.
84
+ * @returns The session, with a read-only `droppedFrames`: the queued frames dropped during reconnects so far.
85
+ * @throws {Error} When the first session fails to open; a session that fails to open on a reconnect is retried
86
+ * within the cap.
87
+ */
88
+ startSession(config?: StreamingSTTConfig): Promise<StreamingSTTSession>;
89
+ }
90
+ //# sourceMappingURL=ReconnectingSTT.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReconnectingSTT.d.ts","sourceRoot":"","sources":["../../../src/io/voice-pipeline/ReconnectingSTT.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAc,aAAa,EAAE,kBAAkB,EAAE,mBAAmB,EAAmB,MAAM,YAAY,CAAC;AAEtH,0CAA0C;AAC1C,MAAM,WAAW,sBAAsB;IACrC,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oFAAoF;IACpF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,2GAA2G;IAC3G,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,yFAAyF;AACzF,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,8GAA8G;IAC9G,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,eAAgB,YAAW,aAAa;IAavC,OAAO,CAAC,QAAQ,CAAC,KAAK;IAZlC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC;;;OAGG;gBAC0B,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,sBAA2B;IASvF,qDAAqD;IACrD,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;;;;;OAOG;IACG,YAAY,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CA6N9E"}
@@ -0,0 +1,318 @@
1
+ /**
2
+ * @module voice-pipeline/ReconnectingSTT
3
+ *
4
+ * One streaming transcriber, reconnected when its session errors or closes:
5
+ * the last seconds of audio are replayed into the new session before the
6
+ * frames that arrived meanwhile, a replayed duplicate is dropped, each new
7
+ * session's clock is moved onto the one session timeline, and the caller
8
+ * hears `stt:reconnected` and `stt:gap`. After the cap, `error` then `close`.
9
+ * A chain of several providers fails over instead ({@link StreamingSTTChain});
10
+ * this wraps a chain of one.
11
+ */
12
+ import { EventEmitter } from 'node:events';
13
+ import { AudioRingBuffer } from './AudioRingBuffer.js';
14
+ import { TranscriptDedupe } from './TranscriptDedupe.js';
15
+ const frameMs = (f) => (f.samples.length / f.sampleRate) * 1000;
16
+ /**
17
+ * An {@link IStreamingSTT} over one provider whose session is reconnected.
18
+ *
19
+ * An `error` or `close` of the provider's session that the caller did not ask
20
+ * for opens a new session of the same provider, at most
21
+ * `maxAttemptsPerMinute` times in any sixty seconds. The new session hears the
22
+ * buffered audio first (the last `ringBufferCapacityMs`, when there is at least
23
+ * `minReplayMs` of it), then the frames pushed while it opened, then live
24
+ * audio. Its `startMs` and `endMs` are moved onto one timeline, the audio
25
+ * pushed since the session began, and a final that repeats one already
26
+ * emitted over the same audio is dropped. Only finals with finite start and end
27
+ * bounds and a positive duration enter the dedupe history; others pass through.
28
+ * Queue overflow leaves a gap after the replay, preserving the original times
29
+ * of both replayed and retained audio. Each reconnect emits
30
+ * `stt:reconnected` ({@link SttReconnectedEvent}) and `stt:gap`
31
+ * ({@link SttGapEvent}). Past the cap the session emits `error`, then `close`.
32
+ *
33
+ * The session forwards `transcript`, `vad`, `speech_start`, `speech_end`,
34
+ * `usage` and `warning` from the provider's sessions; `usage` comes from every
35
+ * one of them, a replaced one too, since a session reports its audio as it
36
+ * closes. `flush()` during a reconnect waits for the replacement, replay and
37
+ * queued audio before flushing it. A close during that flush, or after it with
38
+ * no further provider activity or pushed audio, ends the stream without a
39
+ * reconnect. Activity after a completed flush makes a later close a failure.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const stt = new ReconnectingSTT(new DeepgramStreamingSTT({ apiKey: process.env.DEEPGRAM_API_KEY! }));
44
+ * const session = await stt.startSession({ language: 'en-US' });
45
+ * session.on('stt:gap', ({ lostMs }: SttGapEvent) => console.log(`the transcriber was away for ${lostMs} ms`));
46
+ * ```
47
+ */
48
+ export class ReconnectingSTT {
49
+ /**
50
+ * @param inner - The provider each session opens, again on every reconnect.
51
+ * @param options - The replay buffer, the attempt cap, the queue and the clock.
52
+ */
53
+ constructor(inner, options = {}) {
54
+ this.inner = inner;
55
+ this.providerId = `reconnecting:${inner.providerId}`;
56
+ this.capacityMs = options.ringBufferCapacityMs ?? 3000;
57
+ this.minReplayMs = options.minReplayMs ?? 400;
58
+ this.maxAttempts = options.maxAttemptsPerMinute ?? 3;
59
+ this.queueCapacityMs = options.queueCapacityMs ?? 2000;
60
+ this.clock = options.clock ?? Date.now;
61
+ }
62
+ /** Whether the inner provider has a session open. */
63
+ get isStreaming() {
64
+ return this.inner.isStreaming;
65
+ }
66
+ /**
67
+ * Opens a session of the inner provider behind a session that reconnects it.
68
+ *
69
+ * @param config - Passed to the inner provider for the first session and for every reconnect.
70
+ * @returns The session, with a read-only `droppedFrames`: the queued frames dropped during reconnects so far.
71
+ * @throws {Error} When the first session fails to open; a session that fails to open on a reconnect is retried
72
+ * within the cap.
73
+ */
74
+ async startSession(config) {
75
+ const facade = new EventEmitter();
76
+ const ring = new AudioRingBuffer({ capacityMs: this.capacityMs, sampleRate: 16000 });
77
+ const dedupe = new TranscriptDedupe();
78
+ const attempts = [];
79
+ const queue = [];
80
+ let queuedMs = 0;
81
+ let droppedQueuedMs = 0;
82
+ let dropped = 0;
83
+ let current = await this.inner.startSession(config);
84
+ // the connection generation: the dedupe skips observations of one provider id, so each session gets an id of its own
85
+ let generation = 0;
86
+ // where each session's clock 0 sits on the session timeline (the audio pushed by the caller since the start)
87
+ let pushedMs = 0;
88
+ let offsetMs = 0;
89
+ let reconnecting = false;
90
+ let reconnectPromise;
91
+ let closedByCaller = false;
92
+ let ended = false;
93
+ // The flush is the last operation until audio or provider activity follows it.
94
+ let flushed = false;
95
+ let flushing;
96
+ /** Ends the session once: `close`, after which nothing but `usage` is forwarded. */
97
+ const finish = () => {
98
+ if (ended)
99
+ return;
100
+ ended = true;
101
+ facade.emit('close');
102
+ };
103
+ const attach = (session, sessionGeneration, sessionOffsetMs, replayedMs = 0, gapMs = 0) => {
104
+ const providerId = `${this.inner.providerId}#${sessionGeneration}`;
105
+ const activity = () => {
106
+ if (session === current)
107
+ flushed = false;
108
+ };
109
+ // End bounds are exclusive: an utterance ending exactly at the replay
110
+ // boundary still belongs to the audio before the gap.
111
+ const mapTime = (time, end = false) => time + sessionOffsetMs + ((end ? time > replayedMs : time >= replayedMs) ? gapMs : 0);
112
+ session.on('transcript', (raw) => {
113
+ if (ended)
114
+ return;
115
+ activity();
116
+ const evt = {
117
+ ...raw,
118
+ ...(raw.startMs !== undefined ? { startMs: mapTime(raw.startMs) } : {}),
119
+ ...(raw.endMs !== undefined ? { endMs: mapTime(raw.endMs, true) } : {}),
120
+ };
121
+ if (evt.isFinal && evt.startMs !== undefined && evt.endMs !== undefined && Number.isFinite(evt.startMs) && Number.isFinite(evt.endMs) && evt.endMs > evt.startMs) {
122
+ const r = dedupe.evaluate({ provider: providerId, text: evt.text, audioStartMs: evt.startMs, audioEndMs: evt.endMs, isFinal: true });
123
+ if (r.isDuplicate)
124
+ return;
125
+ }
126
+ facade.emit('transcript', evt);
127
+ });
128
+ for (const name of ['vad', 'speech_start', 'speech_end']) {
129
+ session.on(name, (...args) => {
130
+ if (ended)
131
+ return;
132
+ activity();
133
+ facade.emit(name, ...args);
134
+ });
135
+ }
136
+ // from every session, a replaced or ended one too: a session reports the audio of its connections as it closes
137
+ session.on('usage', (usage) => {
138
+ activity();
139
+ facade.emit('usage', usage);
140
+ });
141
+ session.on('warning', (warning) => {
142
+ if (ended)
143
+ return;
144
+ activity();
145
+ // Listening here must not silence a warning: a provider session prints one nobody listens for, so the
146
+ // session prints it when its own caller does not listen either.
147
+ if (facade.listenerCount('warning') > 0)
148
+ facade.emit('warning', warning);
149
+ else
150
+ console.warn(`[${this.inner.providerId}] ${warning.message}`);
151
+ });
152
+ session.on('error', (err) => {
153
+ if (closedByCaller || session !== current)
154
+ return;
155
+ activity();
156
+ void reconnect(err);
157
+ });
158
+ session.on('close', () => {
159
+ if (closedByCaller || session !== current || reconnecting)
160
+ return;
161
+ if (flushed || flushing === session) {
162
+ // the provider sent its last final and ended its stream, as the caller's flush asked
163
+ finish();
164
+ return;
165
+ }
166
+ void reconnect(undefined);
167
+ });
168
+ };
169
+ const giveUp = (err) => {
170
+ if (ended)
171
+ return;
172
+ ended = true;
173
+ // the provider's session does not stay open behind a session that reports itself closed
174
+ try {
175
+ current.close();
176
+ }
177
+ catch {
178
+ // already gone
179
+ }
180
+ facade.emit('error', err);
181
+ facade.emit('close');
182
+ };
183
+ const runReconnect = async (err) => {
184
+ if (reconnecting || closedByCaller || ended)
185
+ return;
186
+ reconnecting = true;
187
+ const startedAt = this.clock();
188
+ const minuteAgo = startedAt - 60000;
189
+ while (attempts.length && attempts[0] < minuteAgo)
190
+ attempts.shift();
191
+ if (attempts.length >= this.maxAttempts) {
192
+ reconnecting = false;
193
+ giveUp(err ?? new Error(`${this.inner.providerId}: session ended after ${this.maxAttempts} reconnects in a minute`));
194
+ return;
195
+ }
196
+ attempts.push(startedAt);
197
+ const attempt = attempts.length;
198
+ try {
199
+ current.close();
200
+ }
201
+ catch {
202
+ // already gone
203
+ }
204
+ let next;
205
+ try {
206
+ next = await this.inner.startSession(config);
207
+ }
208
+ catch (startErr) {
209
+ reconnecting = false;
210
+ await runReconnect(startErr instanceof Error ? startErr : new Error(String(startErr)));
211
+ return;
212
+ }
213
+ if (closedByCaller) {
214
+ next.close();
215
+ reconnecting = false;
216
+ return;
217
+ }
218
+ const replay = ring.snapshot();
219
+ const bufferedMs = replay.reduce((duration, frame) => duration + frameMs(frame), 0);
220
+ const replayedMs = bufferedMs >= this.minReplayMs ? bufferedMs : 0;
221
+ // Clock zero belongs to the replay before the failure. Dropped queue
222
+ // audio shifts only the retained queue and the live audio after it.
223
+ generation += 1;
224
+ offsetMs = pushedMs - queuedMs - droppedQueuedMs - replayedMs;
225
+ current = next;
226
+ flushed = false;
227
+ attach(next, generation, offsetMs, replayedMs, droppedQueuedMs);
228
+ if (replayedMs > 0)
229
+ for (const f of replay)
230
+ next.pushAudio(f);
231
+ // After a gap the ring's old span and the retained queue are not contiguous;
232
+ // keep only the audio a later replay can place on one timeline.
233
+ if (droppedQueuedMs > 0)
234
+ ring.clear();
235
+ for (const f of queue.splice(0)) {
236
+ ring.push(f);
237
+ next.pushAudio(f);
238
+ }
239
+ queuedMs = 0;
240
+ droppedQueuedMs = 0;
241
+ reconnecting = false;
242
+ facade.emit('stt:reconnected', { attempt, replayedMs });
243
+ facade.emit('stt:gap', { attempt, lostMs: this.clock() - startedAt });
244
+ };
245
+ const reconnect = (err) => {
246
+ if (reconnecting || closedByCaller || ended)
247
+ return reconnectPromise ?? Promise.resolve();
248
+ reconnectPromise = runReconnect(err);
249
+ return reconnectPromise;
250
+ };
251
+ attach(current, generation, offsetMs);
252
+ const session = Object.assign(facade, {
253
+ pushAudio: (frame) => {
254
+ if (ended || closedByCaller)
255
+ return;
256
+ flushed = false;
257
+ flushing = undefined;
258
+ pushedMs += frameMs(frame);
259
+ if (reconnecting) {
260
+ queue.push(frame);
261
+ queuedMs += frameMs(frame);
262
+ while (queuedMs > this.queueCapacityMs && queue.length) {
263
+ const old = queue.shift();
264
+ queuedMs -= frameMs(old);
265
+ droppedQueuedMs += frameMs(old);
266
+ dropped += 1;
267
+ }
268
+ return;
269
+ }
270
+ ring.push(frame);
271
+ try {
272
+ current.pushAudio(frame);
273
+ }
274
+ catch (err) {
275
+ void reconnect(err instanceof Error ? err : new Error(String(err)));
276
+ }
277
+ },
278
+ /**
279
+ * Waits for any reconnect and its replay, then drains the adopted session. A close during the flush or
280
+ * before subsequent provider activity or audio ends the stream without reconnecting.
281
+ */
282
+ flush: async () => {
283
+ if (ended || closedByCaller)
284
+ return;
285
+ while (reconnecting && reconnectPromise)
286
+ await reconnectPromise;
287
+ if (ended || closedByCaller)
288
+ return;
289
+ const target = current;
290
+ flushed = true;
291
+ flushing = target;
292
+ try {
293
+ await target.flush();
294
+ }
295
+ finally {
296
+ if (flushing === target)
297
+ flushing = undefined;
298
+ }
299
+ },
300
+ /** Closes the live session and emits `close` once, from the facade itself: what the provider does after is ignored. */
301
+ close: () => {
302
+ if (closedByCaller)
303
+ return;
304
+ closedByCaller = true;
305
+ try {
306
+ current.close();
307
+ }
308
+ catch {
309
+ // already gone
310
+ }
311
+ finish();
312
+ },
313
+ });
314
+ Object.defineProperty(session, 'droppedFrames', { get: () => dropped, enumerable: true });
315
+ return session;
316
+ }
317
+ }
318
+ //# sourceMappingURL=ReconnectingSTT.js.map