@framers/agentos-ext-endpoint-semantic 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.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Framers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
package/SKILL.md ADDED
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: endpoint-semantic
3
+ description: Semantic endpoint detection — uses an LLM to classify whether the user's utterance is a complete thought, reducing false turn boundaries on mid-sentence pauses
4
+ category: voice
5
+ ---
6
+
7
+ # Semantic Endpoint Detector Extension Pack
8
+
9
+ LLM-powered turn-boundary detection for the AgentOS voice pipeline. Extends the heuristic detector's punctuation and backchannel rules with a fast LLM classifier that decides whether an ambiguous utterance is complete or still in progress.
10
+
11
+ ## How it works
12
+
13
+ 1. **Punctuation (immediate)** — If the final transcript ends with `.`, `?`, or `!`, `turn_complete` fires immediately with reason `punctuation`, identical to the heuristic detector.
14
+ 2. **Backchannel suppression** — Short acknowledgement phrases (`"uh huh"`, `"yeah"`, etc.) are emitted as `backchannel_detected` and do not advance the turn.
15
+ 3. **LLM classification (ambiguous)** — On silence without terminal punctuation, after `minSilenceBeforeCheckMs` (default 500 ms), a small LLM prompt is sent asking whether the utterance is a complete thought. Results are LRU-cached keyed on the first 100 characters of the transcript.
16
+ - `COMPLETE` → emit `turn_complete` with reason `semantic_model`.
17
+ - `INCOMPLETE` → keep waiting; eventual silence timeout acts as the final fallback.
18
+ - `TIMEOUT` (LLM call exceeds `timeoutMs`) → fall back to silence timeout.
19
+
20
+ ## Setup
21
+
22
+ No API key is bundled — supply an `llmCall` function when constructing the detector directly, or configure an LLM provider in the AgentOS runtime.
23
+
24
+ ## Configuration
25
+
26
+ ```json
27
+ {
28
+ "voice": {
29
+ "endpointing": "semantic",
30
+ "endpointingOptions": {
31
+ "model": "gpt-4o-mini",
32
+ "timeoutMs": 500,
33
+ "minSilenceBeforeCheckMs": 500,
34
+ "silenceTimeoutMs": 2000
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### Options
41
+
42
+ | Option | Type | Default | Description |
43
+ |---------------------------|----------|---------|---------------------------------------------------------------------|
44
+ | `model` | `string` | — | LLM model identifier forwarded to the runtime LLM provider |
45
+ | `timeoutMs` | `number` | `500` | Max ms to wait for the LLM before falling back to silence timeout |
46
+ | `minSilenceBeforeCheckMs` | `number` | `500` | Silence duration after speech_end before the LLM is queried |
47
+ | `silenceTimeoutMs` | `number` | `1500` | Hard silence fallback after speech_end if LLM returns INCOMPLETE |
48
+
49
+ ## Events
50
+
51
+ | Event | Payload | Description |
52
+ |-----------------------|----------------------|--------------------------------------------------------------|
53
+ | `turn_complete` | `TurnCompleteEvent` | User turn has ended; `reason` is `punctuation`, `semantic_model`, or `silence_timeout` |
54
+ | `backchannel_detected`| `{ text: string }` | A backchannel phrase was recognised; accumulation suppressed |
55
+
56
+ ## Programmatic usage
57
+
58
+ ```ts
59
+ import { createSemanticEndpointDetector } from '@framers/agentos-ext-endpoint-semantic';
60
+
61
+ const detector = createSemanticEndpointDetector(
62
+ async (prompt) => {
63
+ const res = await openai.chat.completions.create({
64
+ model: 'gpt-4o-mini',
65
+ messages: [{ role: 'user', content: prompt }],
66
+ max_tokens: 32,
67
+ });
68
+ return res.choices[0].message.content ?? '';
69
+ },
70
+ { timeoutMs: 400, minSilenceBeforeCheckMs: 600 }
71
+ );
72
+
73
+ detector.on('turn_complete', (evt) => console.log('Turn done:', evt.reason));
74
+ ```
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @file SemanticEndpointDetector.ts
3
+ * @description IEndpointDetector implementation that combines the heuristic
4
+ * detector's punctuation and backchannel rules with LLM-based turn-completeness
5
+ * classification for ambiguous cases.
6
+ *
7
+ * Detection strategy (in priority order):
8
+ * 1. **Backchannel** — recognised phrases emitted as `backchannel_detected`
9
+ * without advancing the turn.
10
+ * 2. **Terminal punctuation** — immediate `turn_complete` with reason
11
+ * `'punctuation'`.
12
+ * 3. **LLM classification** — invoked after `minSilenceBeforeCheckMs` of
13
+ * silence; `COMPLETE` fires `turn_complete` with reason `'semantic_model'`.
14
+ * `INCOMPLETE` defers to the hard silence timeout.
15
+ * 4. **Silence timeout** — hard fallback after `silenceTimeoutMs`, reason
16
+ * `'silence_timeout'`.
17
+ *
18
+ * @module endpoint-semantic/SemanticEndpointDetector
19
+ */
20
+ import { EventEmitter } from 'node:events';
21
+ import type { SemanticEndpointConfig } from './types.js';
22
+ /**
23
+ * Minimal VAD event shape required by this detector.
24
+ * Mirrors `VadEvent` from `@framers/agentos` voice-pipeline types.
25
+ */
26
+ interface VadEvent {
27
+ /** Type of the VAD transition. */
28
+ type: 'speech_start' | 'speech_end' | 'silence';
29
+ /** Unix epoch millisecond timestamp of the transition. */
30
+ timestamp: number;
31
+ /** Optional raw energy level (implementation-defined). */
32
+ energyLevel?: number;
33
+ /** Origin of the VAD event. */
34
+ source?: 'vad' | 'stt';
35
+ }
36
+ /**
37
+ * Minimal transcript event shape required by this detector.
38
+ * Mirrors `TranscriptEvent` from `@framers/agentos` voice-pipeline types.
39
+ */
40
+ interface TranscriptEvent {
41
+ /** Full transcript text for this event. */
42
+ text: string;
43
+ /** Aggregate confidence score in the range [0, 1]. */
44
+ confidence: number;
45
+ /** Word-level tokens (may be empty for interim events). */
46
+ words: unknown[];
47
+ /** `true` when this hypothesis is stable and will not be revised. */
48
+ isFinal: boolean;
49
+ /** Duration of the recognised speech segment, ms (final events only). */
50
+ durationMs?: number;
51
+ }
52
+ /**
53
+ * Minimal IEndpointDetector interface required by this pack.
54
+ * Mirrors `IEndpointDetector` from `@framers/agentos` voice-pipeline types.
55
+ */
56
+ interface IEndpointDetector extends EventEmitter {
57
+ readonly mode: 'acoustic' | 'heuristic' | 'semantic';
58
+ pushVadEvent(event: VadEvent): void;
59
+ pushTranscript(event: TranscriptEvent): void;
60
+ reset(): void;
61
+ }
62
+ /**
63
+ * Semantic endpoint detector that augments heuristic rules with LLM-based
64
+ * turn-completeness classification.
65
+ *
66
+ * ### Events emitted
67
+ * - `'turn_complete'` ({@link TurnCompleteEvent}) — user turn has ended.
68
+ * - `'backchannel_detected'` (`{ text: string }`) — backchannel phrase
69
+ * recognised; accumulation suppressed.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const detector = new SemanticEndpointDetector(
74
+ * async (prompt) => callMyLLM(prompt),
75
+ * { minSilenceBeforeCheckMs: 600, silenceTimeoutMs: 2000 }
76
+ * );
77
+ * detector.on('turn_complete', (e) => console.log(e.reason));
78
+ * ```
79
+ */
80
+ export declare class SemanticEndpointDetector extends EventEmitter implements IEndpointDetector {
81
+ /** Active detection strategy label. */
82
+ readonly mode: IEndpointDetector['mode'];
83
+ /** LLM-backed completeness classifier. */
84
+ private readonly classifier;
85
+ /** Resolved silence timeout (ms) — hard fallback. */
86
+ private readonly silenceTimeoutMs;
87
+ /** Silence duration (ms) before invoking the LLM classifier. */
88
+ private readonly minSilenceBeforeCheckMs;
89
+ /** Accumulated final transcript text for the current turn. */
90
+ private accumulatedText;
91
+ /** Whether VAD reports active speech. */
92
+ private speechActive;
93
+ /**
94
+ * Handle to the "check" timer — fires after `minSilenceBeforeCheckMs` and
95
+ * invokes the LLM classifier.
96
+ */
97
+ private checkTimer;
98
+ /**
99
+ * Handle to the hard silence-timeout timer — fires after `silenceTimeoutMs`.
100
+ */
101
+ private silenceTimer;
102
+ /** Wall-clock timestamp (ms) when speech began for the current turn. */
103
+ private turnStartMs;
104
+ /** Confidence score from the most recent final STT transcript. */
105
+ private lastConfidence;
106
+ /**
107
+ * @param llmCall — Async function that sends a prompt to an LLM and resolves
108
+ * with the raw response text.
109
+ * @param config — Optional configuration overrides.
110
+ */
111
+ constructor(llmCall: (prompt: string) => Promise<string>, config?: SemanticEndpointConfig);
112
+ /**
113
+ * Ingest a transcript event from the upstream STT session.
114
+ *
115
+ * Only final events (`isFinal: true`) affect state. Interim results are
116
+ * silently ignored as they are unstable and arrive at high frequency.
117
+ *
118
+ * Recognised backchannel phrases are emitted as `'backchannel_detected'`
119
+ * without being accumulated, preventing a subsequent `speech_end` from
120
+ * triggering `turn_complete` for an acknowledgement.
121
+ *
122
+ * @param transcript — Transcript event from the upstream STT session.
123
+ */
124
+ pushTranscript(transcript: TranscriptEvent): void;
125
+ /**
126
+ * Ingest a VAD (voice activity detection) event.
127
+ *
128
+ * - `speech_start` — marks the turn as active, cancels pending timers.
129
+ * - `speech_end` — if text has been accumulated, either fires immediately
130
+ * on terminal punctuation, or schedules the LLM check
131
+ * timer + hard silence-timeout timer.
132
+ * - `silence` — periodic heartbeats; no action required.
133
+ *
134
+ * @param event — VAD transition event.
135
+ */
136
+ pushVadEvent(event: VadEvent): void;
137
+ /**
138
+ * Cancel all pending timers and clear accumulated state so the detector is
139
+ * ready for the next turn. Should be called by the pipeline after each
140
+ * `turn_complete` event.
141
+ */
142
+ reset(): void;
143
+ /**
144
+ * Schedule the LLM completeness check after `minSilenceBeforeCheckMs`.
145
+ *
146
+ * If the LLM returns `COMPLETE`, `turn_complete` is emitted with reason
147
+ * `'semantic_model'` and both timers are cancelled. If the LLM returns
148
+ * `INCOMPLETE` or `TIMEOUT`, the check timer is done and the hard silence
149
+ * timer (already running) continues as the fallback.
150
+ *
151
+ * @param speechEndTimestamp — Timestamp of the triggering `speech_end` event.
152
+ */
153
+ private _startCheckTimer;
154
+ /**
155
+ * Start the hard silence-timeout timer. When it fires, `turn_complete` is
156
+ * emitted with reason `'silence_timeout'` regardless of the LLM result.
157
+ *
158
+ * @param speechEndTimestamp — Timestamp of the triggering `speech_end` event.
159
+ */
160
+ private _startSilenceTimer;
161
+ /**
162
+ * Emit `turn_complete` with the currently accumulated transcript and reset
163
+ * internal state so the detector is clean for the next turn.
164
+ *
165
+ * State is reset **before** emitting so that re-entrant listeners see a
166
+ * clean detector.
167
+ *
168
+ * @param reason — Semantic reason driving turn completion.
169
+ * @param speechEndTimestamp — Timestamp used to compute `durationMs`.
170
+ */
171
+ private _emitTurnComplete;
172
+ /**
173
+ * Cancel the LLM check timer without any other side effects.
174
+ */
175
+ private _clearCheckTimer;
176
+ /**
177
+ * Cancel the hard silence timer without any other side effects.
178
+ */
179
+ private _clearSilenceTimer;
180
+ /**
181
+ * Cancel both the check timer and the silence timer.
182
+ */
183
+ private _clearAllTimers;
184
+ }
185
+ export {};
186
+ //# sourceMappingURL=SemanticEndpointDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SemanticEndpointDetector.d.ts","sourceRoot":"","sources":["../src/SemanticEndpointDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAOzD;;;GAGG;AACH,UAAU,QAAQ;IAChB,kCAAkC;IAClC,IAAI,EAAE,cAAc,GAAG,YAAY,GAAG,SAAS,CAAC;IAChD,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CACxB;AAED;;;GAGG;AACH,UAAU,eAAe;IACvB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA6BD;;;GAGG;AACH,UAAU,iBAAkB,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;IACrD,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;IACpC,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;CACf;AAuCD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,wBAAyB,SAAQ,YAAa,YAAW,iBAAiB;IACrF,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAc;IAEtD,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAExD,qDAAqD;IACrD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IAMjD,8DAA8D;IAC9D,OAAO,CAAC,eAAe,CAAM;IAE7B,yCAAyC;IACzC,OAAO,CAAC,YAAY,CAAS;IAE7B;;;OAGG;IACH,OAAO,CAAC,UAAU,CAA8C;IAEhE;;OAEG;IACH,OAAO,CAAC,YAAY,CAA8C;IAElE,wEAAwE;IACxE,OAAO,CAAC,WAAW,CAAuB;IAE1C,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAK;IAM3B;;;;OAIG;gBAED,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EAC5C,MAAM,GAAE,sBAA2B;IAerC;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI;IAqBjD;;;;;;;;;;OAUG;IACH,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IA2CnC;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAYb;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IAoCxB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAOxB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;OAEG;IACH,OAAO,CAAC,eAAe;CAIxB"}
@@ -0,0 +1,308 @@
1
+ /**
2
+ * @file SemanticEndpointDetector.ts
3
+ * @description IEndpointDetector implementation that combines the heuristic
4
+ * detector's punctuation and backchannel rules with LLM-based turn-completeness
5
+ * classification for ambiguous cases.
6
+ *
7
+ * Detection strategy (in priority order):
8
+ * 1. **Backchannel** — recognised phrases emitted as `backchannel_detected`
9
+ * without advancing the turn.
10
+ * 2. **Terminal punctuation** — immediate `turn_complete` with reason
11
+ * `'punctuation'`.
12
+ * 3. **LLM classification** — invoked after `minSilenceBeforeCheckMs` of
13
+ * silence; `COMPLETE` fires `turn_complete` with reason `'semantic_model'`.
14
+ * `INCOMPLETE` defers to the hard silence timeout.
15
+ * 4. **Silence timeout** — hard fallback after `silenceTimeoutMs`, reason
16
+ * `'silence_timeout'`.
17
+ *
18
+ * @module endpoint-semantic/SemanticEndpointDetector
19
+ */
20
+ import { EventEmitter } from 'node:events';
21
+ import { TurnCompletenessClassifier } from './TurnCompletenessClassifier.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Constants
24
+ // ---------------------------------------------------------------------------
25
+ /** Default silence timeout (ms) — hard fallback if the LLM returns INCOMPLETE. */
26
+ const DEFAULT_SILENCE_TIMEOUT_MS = 1_500;
27
+ /** Default delay (ms) between speech_end and LLM invocation. */
28
+ const DEFAULT_MIN_SILENCE_BEFORE_CHECK_MS = 500;
29
+ /** Terminal punctuation that indicates sentence completion. */
30
+ const TERMINAL_PUNCTUATION = /[.?!]$/;
31
+ /**
32
+ * Normalised backchannel phrases that indicate acknowledgement without a full
33
+ * conversational turn. Compared after `.trim().toLowerCase()`.
34
+ */
35
+ const BACKCHANNEL_PHRASES = new Set([
36
+ 'uh huh',
37
+ 'yeah',
38
+ 'okay',
39
+ 'ok',
40
+ 'mm hmm',
41
+ 'mmhmm',
42
+ 'mhm',
43
+ 'mm-hmm',
44
+ 'right',
45
+ 'sure',
46
+ 'yep',
47
+ 'yup',
48
+ 'gotcha',
49
+ ]);
50
+ // ---------------------------------------------------------------------------
51
+ // Implementation
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Semantic endpoint detector that augments heuristic rules with LLM-based
55
+ * turn-completeness classification.
56
+ *
57
+ * ### Events emitted
58
+ * - `'turn_complete'` ({@link TurnCompleteEvent}) — user turn has ended.
59
+ * - `'backchannel_detected'` (`{ text: string }`) — backchannel phrase
60
+ * recognised; accumulation suppressed.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const detector = new SemanticEndpointDetector(
65
+ * async (prompt) => callMyLLM(prompt),
66
+ * { minSilenceBeforeCheckMs: 600, silenceTimeoutMs: 2000 }
67
+ * );
68
+ * detector.on('turn_complete', (e) => console.log(e.reason));
69
+ * ```
70
+ */
71
+ export class SemanticEndpointDetector extends EventEmitter {
72
+ /** Active detection strategy label. */
73
+ mode = 'semantic';
74
+ /** LLM-backed completeness classifier. */
75
+ classifier;
76
+ /** Resolved silence timeout (ms) — hard fallback. */
77
+ silenceTimeoutMs;
78
+ /** Silence duration (ms) before invoking the LLM classifier. */
79
+ minSilenceBeforeCheckMs;
80
+ // ---------------------------------------------------------------------------
81
+ // Mutable state (reset on each turn)
82
+ // ---------------------------------------------------------------------------
83
+ /** Accumulated final transcript text for the current turn. */
84
+ accumulatedText = '';
85
+ /** Whether VAD reports active speech. */
86
+ speechActive = false;
87
+ /**
88
+ * Handle to the "check" timer — fires after `minSilenceBeforeCheckMs` and
89
+ * invokes the LLM classifier.
90
+ */
91
+ checkTimer = null;
92
+ /**
93
+ * Handle to the hard silence-timeout timer — fires after `silenceTimeoutMs`.
94
+ */
95
+ silenceTimer = null;
96
+ /** Wall-clock timestamp (ms) when speech began for the current turn. */
97
+ turnStartMs = null;
98
+ /** Confidence score from the most recent final STT transcript. */
99
+ lastConfidence = 1;
100
+ // ---------------------------------------------------------------------------
101
+ // Constructor
102
+ // ---------------------------------------------------------------------------
103
+ /**
104
+ * @param llmCall — Async function that sends a prompt to an LLM and resolves
105
+ * with the raw response text.
106
+ * @param config — Optional configuration overrides.
107
+ */
108
+ constructor(llmCall, config = {}) {
109
+ super();
110
+ this.silenceTimeoutMs = config.silenceTimeoutMs ?? DEFAULT_SILENCE_TIMEOUT_MS;
111
+ this.minSilenceBeforeCheckMs =
112
+ config.minSilenceBeforeCheckMs ?? DEFAULT_MIN_SILENCE_BEFORE_CHECK_MS;
113
+ this.classifier = new TurnCompletenessClassifier(llmCall, config.timeoutMs);
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // IEndpointDetector — pushTranscript
117
+ // ---------------------------------------------------------------------------
118
+ /**
119
+ * Ingest a transcript event from the upstream STT session.
120
+ *
121
+ * Only final events (`isFinal: true`) affect state. Interim results are
122
+ * silently ignored as they are unstable and arrive at high frequency.
123
+ *
124
+ * Recognised backchannel phrases are emitted as `'backchannel_detected'`
125
+ * without being accumulated, preventing a subsequent `speech_end` from
126
+ * triggering `turn_complete` for an acknowledgement.
127
+ *
128
+ * @param transcript — Transcript event from the upstream STT session.
129
+ */
130
+ pushTranscript(transcript) {
131
+ if (!transcript.isFinal) {
132
+ return;
133
+ }
134
+ const text = transcript.text;
135
+ const normalised = text.trim().toLowerCase();
136
+ if (BACKCHANNEL_PHRASES.has(normalised)) {
137
+ this.emit('backchannel_detected', { text });
138
+ return;
139
+ }
140
+ this.accumulatedText = text;
141
+ this.lastConfidence = transcript.confidence;
142
+ }
143
+ // ---------------------------------------------------------------------------
144
+ // IEndpointDetector — pushVadEvent
145
+ // ---------------------------------------------------------------------------
146
+ /**
147
+ * Ingest a VAD (voice activity detection) event.
148
+ *
149
+ * - `speech_start` — marks the turn as active, cancels pending timers.
150
+ * - `speech_end` — if text has been accumulated, either fires immediately
151
+ * on terminal punctuation, or schedules the LLM check
152
+ * timer + hard silence-timeout timer.
153
+ * - `silence` — periodic heartbeats; no action required.
154
+ *
155
+ * @param event — VAD transition event.
156
+ */
157
+ pushVadEvent(event) {
158
+ switch (event.type) {
159
+ case 'speech_start': {
160
+ this.speechActive = true;
161
+ this._clearAllTimers();
162
+ if (this.turnStartMs === null) {
163
+ this.turnStartMs = event.timestamp;
164
+ }
165
+ break;
166
+ }
167
+ case 'speech_end': {
168
+ this.speechActive = false;
169
+ if (!this.accumulatedText) {
170
+ // No transcript available yet — nothing to evaluate.
171
+ break;
172
+ }
173
+ if (TERMINAL_PUNCTUATION.test(this.accumulatedText)) {
174
+ // Sentence-terminal punctuation: fire immediately, identical to the
175
+ // heuristic detector.
176
+ this._emitTurnComplete('punctuation', event.timestamp);
177
+ break;
178
+ }
179
+ // Ambiguous case: schedule the LLM check and the hard silence fallback.
180
+ this._startCheckTimer(event.timestamp);
181
+ this._startSilenceTimer(event.timestamp);
182
+ break;
183
+ }
184
+ case 'silence': {
185
+ // Periodic heartbeat — no action; timers already handle delayed firing.
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // IEndpointDetector — reset
192
+ // ---------------------------------------------------------------------------
193
+ /**
194
+ * Cancel all pending timers and clear accumulated state so the detector is
195
+ * ready for the next turn. Should be called by the pipeline after each
196
+ * `turn_complete` event.
197
+ */
198
+ reset() {
199
+ this._clearAllTimers();
200
+ this.accumulatedText = '';
201
+ this.speechActive = false;
202
+ this.turnStartMs = null;
203
+ this.lastConfidence = 1;
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Private helpers
207
+ // ---------------------------------------------------------------------------
208
+ /**
209
+ * Schedule the LLM completeness check after `minSilenceBeforeCheckMs`.
210
+ *
211
+ * If the LLM returns `COMPLETE`, `turn_complete` is emitted with reason
212
+ * `'semantic_model'` and both timers are cancelled. If the LLM returns
213
+ * `INCOMPLETE` or `TIMEOUT`, the check timer is done and the hard silence
214
+ * timer (already running) continues as the fallback.
215
+ *
216
+ * @param speechEndTimestamp — Timestamp of the triggering `speech_end` event.
217
+ */
218
+ _startCheckTimer(speechEndTimestamp) {
219
+ this._clearCheckTimer();
220
+ // Capture state at scheduling time so async re-entrancy cannot corrupt it.
221
+ const transcriptSnapshot = this.accumulatedText;
222
+ this.checkTimer = setTimeout(async () => {
223
+ this.checkTimer = null;
224
+ // Guard: if the user resumed speaking before this timer fired, bail out.
225
+ if (this.speechActive || this.accumulatedText !== transcriptSnapshot) {
226
+ return;
227
+ }
228
+ let result;
229
+ try {
230
+ result = await this.classifier.classify(transcriptSnapshot);
231
+ }
232
+ catch {
233
+ // Unexpected classifier error — let the silence timer handle it.
234
+ return;
235
+ }
236
+ if (result === 'COMPLETE') {
237
+ // Cancel the hard silence timer — LLM beat it.
238
+ this._clearSilenceTimer();
239
+ // Another guard: state may have changed while we awaited the LLM.
240
+ if (this.accumulatedText === transcriptSnapshot && !this.speechActive) {
241
+ this._emitTurnComplete('semantic_model', speechEndTimestamp);
242
+ }
243
+ }
244
+ // INCOMPLETE or TIMEOUT → do nothing; the hard silence timer continues.
245
+ }, this.minSilenceBeforeCheckMs);
246
+ }
247
+ /**
248
+ * Start the hard silence-timeout timer. When it fires, `turn_complete` is
249
+ * emitted with reason `'silence_timeout'` regardless of the LLM result.
250
+ *
251
+ * @param speechEndTimestamp — Timestamp of the triggering `speech_end` event.
252
+ */
253
+ _startSilenceTimer(speechEndTimestamp) {
254
+ this._clearSilenceTimer();
255
+ this.silenceTimer = setTimeout(() => {
256
+ this.silenceTimer = null;
257
+ this._clearCheckTimer();
258
+ this._emitTurnComplete('silence_timeout', speechEndTimestamp);
259
+ }, this.silenceTimeoutMs);
260
+ }
261
+ /**
262
+ * Emit `turn_complete` with the currently accumulated transcript and reset
263
+ * internal state so the detector is clean for the next turn.
264
+ *
265
+ * State is reset **before** emitting so that re-entrant listeners see a
266
+ * clean detector.
267
+ *
268
+ * @param reason — Semantic reason driving turn completion.
269
+ * @param speechEndTimestamp — Timestamp used to compute `durationMs`.
270
+ */
271
+ _emitTurnComplete(reason, speechEndTimestamp) {
272
+ const durationMs = this.turnStartMs !== null ? speechEndTimestamp - this.turnStartMs : 0;
273
+ const event = {
274
+ transcript: this.accumulatedText,
275
+ confidence: this.lastConfidence,
276
+ durationMs,
277
+ reason,
278
+ };
279
+ this.reset();
280
+ this.emit('turn_complete', event);
281
+ }
282
+ /**
283
+ * Cancel the LLM check timer without any other side effects.
284
+ */
285
+ _clearCheckTimer() {
286
+ if (this.checkTimer !== null) {
287
+ clearTimeout(this.checkTimer);
288
+ this.checkTimer = null;
289
+ }
290
+ }
291
+ /**
292
+ * Cancel the hard silence timer without any other side effects.
293
+ */
294
+ _clearSilenceTimer() {
295
+ if (this.silenceTimer !== null) {
296
+ clearTimeout(this.silenceTimer);
297
+ this.silenceTimer = null;
298
+ }
299
+ }
300
+ /**
301
+ * Cancel both the check timer and the silence timer.
302
+ */
303
+ _clearAllTimers() {
304
+ this._clearCheckTimer();
305
+ this._clearSilenceTimer();
306
+ }
307
+ }
308
+ //# sourceMappingURL=SemanticEndpointDetector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SemanticEndpointDetector.js","sourceRoot":"","sources":["../src/SemanticEndpointDetector.ts"],"names":[],"mappings":"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"}