@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 +23 -0
- package/SKILL.md +74 -0
- package/dist/SemanticEndpointDetector.d.ts +186 -0
- package/dist/SemanticEndpointDetector.d.ts.map +1 -0
- package/dist/SemanticEndpointDetector.js +308 -0
- package/dist/SemanticEndpointDetector.js.map +1 -0
- package/dist/TurnCompletenessClassifier.d.ts +82 -0
- package/dist/TurnCompletenessClassifier.d.ts.map +1 -0
- package/dist/TurnCompletenessClassifier.js +130 -0
- package/dist/TurnCompletenessClassifier.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/manifest.json +8 -0
- package/package.json +42 -0
- package/src/SemanticEndpointDetector.ts +440 -0
- package/src/TurnCompletenessClassifier.ts +157 -0
- package/src/index.ts +125 -0
- package/src/types.ts +53 -0
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":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AA8E7E,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,kFAAkF;AAClF,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEzC,gEAAgE;AAChE,MAAM,mCAAmC,GAAG,GAAG,CAAC;AAEhD,+DAA+D;AAC/D,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAEtC;;;GAGG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,QAAQ;IACR,MAAM;IACN,MAAM;IACN,IAAI;IACJ,QAAQ;IACR,OAAO;IACP,KAAK;IACL,QAAQ;IACR,OAAO;IACP,MAAM;IACN,KAAK;IACL,KAAK;IACL,QAAQ;CACT,CAAC,CAAC;AAEH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,wBAAyB,SAAQ,YAAY;IACxD,uCAAuC;IAC9B,IAAI,GAA8B,UAAU,CAAC;IAEtD,0CAA0C;IACzB,UAAU,CAA6B;IAExD,qDAAqD;IACpC,gBAAgB,CAAS;IAE1C,gEAAgE;IAC/C,uBAAuB,CAAS;IAEjD,8EAA8E;IAC9E,qCAAqC;IACrC,8EAA8E;IAE9E,8DAA8D;IACtD,eAAe,GAAG,EAAE,CAAC;IAE7B,yCAAyC;IACjC,YAAY,GAAG,KAAK,CAAC;IAE7B;;;OAGG;IACK,UAAU,GAAyC,IAAI,CAAC;IAEhE;;OAEG;IACK,YAAY,GAAyC,IAAI,CAAC;IAElE,wEAAwE;IAChE,WAAW,GAAkB,IAAI,CAAC;IAE1C,kEAAkE;IAC1D,cAAc,GAAG,CAAC,CAAC;IAE3B,8EAA8E;IAC9E,cAAc;IACd,8EAA8E;IAE9E;;;;OAIG;IACH,YACE,OAA4C,EAC5C,SAAiC,EAAE;QAEnC,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;QAC9E,IAAI,CAAC,uBAAuB;YAC1B,MAAM,CAAC,uBAAuB,IAAI,mCAAmC,CAAC;QAExE,IAAI,CAAC,UAAU,GAAG,IAAI,0BAA0B,CAAC,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IAC9E,CAAC;IAED,8EAA8E;IAC9E,qCAAqC;IACrC,8EAA8E;IAE9E;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,UAA2B;QACxC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAE7C,IAAI,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC;IAC9C,CAAC;IAED,8EAA8E;IAC9E,mCAAmC;IACnC,8EAA8E;IAE9E;;;;;;;;;;OAUG;IACH,YAAY,CAAC,KAAe;QAC1B,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,cAAc,CAAC,CAAC,CAAC;gBACpB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;gBACzB,IAAI,CAAC,eAAe,EAAE,CAAC;gBACvB,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;oBAC9B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC;gBACrC,CAAC;gBACD,MAAM;YACR,CAAC;YAED,KAAK,YAAY,CAAC,CAAC,CAAC;gBAClB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;gBAE1B,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;oBAC1B,qDAAqD;oBACrD,MAAM;gBACR,CAAC;gBAED,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;oBACpD,oEAAoE;oBACpE,sBAAsB;oBACtB,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;oBACvD,MAAM;gBACR,CAAC;gBAED,wEAAwE;gBACxE,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBACvC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBACzC,MAAM;YACR,CAAC;YAED,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,wEAAwE;gBACxE,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,4BAA4B;IAC5B,8EAA8E;IAE9E;;;;OAIG;IACH,KAAK;QACH,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC;QAC1B,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,8EAA8E;IAC9E,kBAAkB;IAClB,8EAA8E;IAE9E;;;;;;;;;OASG;IACK,gBAAgB,CAAC,kBAA0B;QACjD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAExB,2EAA2E;QAC3E,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,CAAC;QAEhD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YACtC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YAEvB,yEAAyE;YACzE,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,eAAe,KAAK,kBAAkB,EAAE,CAAC;gBACrE,OAAO;YACT,CAAC;YAED,IAAI,MAAmE,CAAC;YAExE,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;YAC9D,CAAC;YAAC,MAAM,CAAC;gBACP,iEAAiE;gBACjE,OAAO;YACT,CAAC;YAED,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;gBAC1B,+CAA+C;gBAC/C,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAE1B,kEAAkE;gBAClE,IAAI,IAAI,CAAC,eAAe,KAAK,kBAAkB,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;oBACtE,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;gBAC/D,CAAC;YACH,CAAC;YACD,wEAAwE;QAC1E,CAAC,EAAE,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACnC,CAAC;IAED;;;;;OAKG;IACK,kBAAkB,CAAC,kBAA0B;QACnD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAE1B,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;YAClC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACxB,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAAC;QAChE,CAAC,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;OASG;IACK,iBAAiB,CAAC,MAAsB,EAAE,kBAA0B;QAC1E,MAAM,UAAU,GACd,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QAExE,MAAM,KAAK,GAAsB;YAC/B,UAAU,EAAE,IAAI,CAAC,eAAe;YAChC,UAAU,EAAE,IAAI,CAAC,cAAc;YAC/B,UAAU;YACV,MAAM;SACP,CAAC;QAEF,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED;;OAEG;IACK,gBAAgB;QACtB,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;YAC7B,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;OAEG;IACK,kBAAkB;QACxB,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YAC/B,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAChC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;IAED;;OAEG;IACK,eAAe;QACrB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;CACF"}
|