@workadventure/livekit-agent-plugin-fake-stt 0.1.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 +21 -0
- package/README.md +155 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +377 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WorkAdventure
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @workadventure/livekit-agent-plugin-fake-stt
|
|
2
|
+
|
|
3
|
+
Deterministic fake STT provider for `@livekit/agents`.
|
|
4
|
+
|
|
5
|
+
This plugin is designed for tests and local development where you need predictable
|
|
6
|
+
streaming STT events without calling external APIs.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- deterministic streaming event sequence
|
|
11
|
+
- no network calls, no credentials
|
|
12
|
+
- configurable scripts with interim/final steps
|
|
13
|
+
- per-step delays and deterministic jitter
|
|
14
|
+
- stable `seed` + `offset` controls
|
|
15
|
+
- optional `RECOGNITION_USAGE` events
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @workadventure/livekit-agent-plugin-fake-stt
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Peer requirements:
|
|
24
|
+
|
|
25
|
+
- `@livekit/agents`
|
|
26
|
+
- `@livekit/rtc-node`
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { stt } from '@livekit/agents';
|
|
32
|
+
import type { AudioFrame } from '@livekit/rtc-node';
|
|
33
|
+
import { STT } from '@workadventure/livekit-agent-plugin-fake-stt';
|
|
34
|
+
|
|
35
|
+
const fakeStt = new STT({
|
|
36
|
+
emitRecognitionUsage: true,
|
|
37
|
+
script: [['hello partial', 'hello final']],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const stream = fakeStt.stream();
|
|
41
|
+
const frame = {
|
|
42
|
+
data: new Int16Array(1600),
|
|
43
|
+
sampleRate: 16000,
|
|
44
|
+
channels: 1,
|
|
45
|
+
samplesPerChannel: 1600,
|
|
46
|
+
} as unknown as AudioFrame;
|
|
47
|
+
|
|
48
|
+
stream.pushFrame(frame);
|
|
49
|
+
stream.endInput();
|
|
50
|
+
|
|
51
|
+
for await (const event of stream) {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case stt.SpeechEventType.INTERIM_TRANSCRIPT:
|
|
54
|
+
case stt.SpeechEventType.FINAL_TRANSCRIPT:
|
|
55
|
+
console.log(event.alternatives?.[0]?.text ?? '');
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Event Model
|
|
62
|
+
|
|
63
|
+
For each utterance (audio frames up to `flush()` or `endInput()`), the stream emits:
|
|
64
|
+
|
|
65
|
+
1. `START_OF_SPEECH`
|
|
66
|
+
2. one or more `INTERIM_TRANSCRIPT` / `FINAL_TRANSCRIPT` steps from the configured script
|
|
67
|
+
3. `END_OF_SPEECH`
|
|
68
|
+
4. optional `RECOGNITION_USAGE`
|
|
69
|
+
|
|
70
|
+
Default script:
|
|
71
|
+
|
|
72
|
+
- Segment A: interim, interim, final
|
|
73
|
+
- Segment B: interim, final
|
|
74
|
+
|
|
75
|
+
Segments are reused in deterministic round-robin order.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { STT } from '@workadventure/livekit-agent-plugin-fake-stt';
|
|
81
|
+
|
|
82
|
+
const fakeStt = new STT({
|
|
83
|
+
language: 'en-US',
|
|
84
|
+
defaultStepDelayMs: 10,
|
|
85
|
+
jitterMs: 2,
|
|
86
|
+
seed: 1234,
|
|
87
|
+
offset: 1,
|
|
88
|
+
emitRecognitionUsage: true,
|
|
89
|
+
script: [
|
|
90
|
+
[
|
|
91
|
+
{ text: 'segment-1 partial', final: false, delayMs: 20 },
|
|
92
|
+
{ text: 'segment-1 final', final: true },
|
|
93
|
+
],
|
|
94
|
+
{
|
|
95
|
+
emitUsage: false,
|
|
96
|
+
steps: ['segment-2 partial', 'segment-2 final'],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `FakeSTTOptions`
|
|
103
|
+
|
|
104
|
+
- `script`: deterministic transcript script. Array of segments.
|
|
105
|
+
- `language`: default language on transcript alternatives.
|
|
106
|
+
- `defaultStepDelayMs`: delay used when a step has no explicit `delayMs`.
|
|
107
|
+
- `jitterMs`: deterministic signed jitter added to each step delay.
|
|
108
|
+
- `seed`: stable seed used for script offset and jitter generation.
|
|
109
|
+
- `offset`: deterministic script cursor offset.
|
|
110
|
+
- `emitRecognitionUsage`: emit usage events for each utterance.
|
|
111
|
+
- `sampleRate`: expected input sample rate for stream resampling behavior.
|
|
112
|
+
|
|
113
|
+
## Deterministic Testing Recipes
|
|
114
|
+
|
|
115
|
+
### Stable transcript snapshots
|
|
116
|
+
|
|
117
|
+
- set `defaultStepDelayMs: 0`
|
|
118
|
+
- set `jitterMs: 0`
|
|
119
|
+
- use explicit script text values
|
|
120
|
+
|
|
121
|
+
### Seeded timing simulation
|
|
122
|
+
|
|
123
|
+
- set `defaultStepDelayMs` and `jitterMs`
|
|
124
|
+
- pin `seed` so delay jitter stays reproducible in CI
|
|
125
|
+
|
|
126
|
+
### Script partitioning by utterance
|
|
127
|
+
|
|
128
|
+
- call `flush()` between user turns
|
|
129
|
+
- each flush/end advances to the next script segment
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
- `examples/basic-stream.mjs`
|
|
134
|
+
- `examples/seeded-script.mjs`
|
|
135
|
+
|
|
136
|
+
Build first, then run:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm run build
|
|
140
|
+
node examples/basic-stream.mjs
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm install
|
|
147
|
+
npm run lint
|
|
148
|
+
npm run typecheck
|
|
149
|
+
npm test
|
|
150
|
+
npm run build
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { stt, AudioBuffer, APIConnectOptions } from '@livekit/agents';
|
|
2
|
+
|
|
3
|
+
interface FakeSTTScriptStep {
|
|
4
|
+
text: string;
|
|
5
|
+
final?: boolean;
|
|
6
|
+
delayMs?: number;
|
|
7
|
+
confidence?: number;
|
|
8
|
+
language?: string;
|
|
9
|
+
}
|
|
10
|
+
type FakeSTTScriptStepInput = string | FakeSTTScriptStep;
|
|
11
|
+
interface FakeSTTScriptSegment {
|
|
12
|
+
steps: FakeSTTScriptStepInput[];
|
|
13
|
+
emitUsage?: boolean;
|
|
14
|
+
usageAudioDuration?: number;
|
|
15
|
+
}
|
|
16
|
+
type FakeSTTScriptSegmentInput = FakeSTTScriptSegment | FakeSTTScriptStepInput[];
|
|
17
|
+
interface FakeSTTOptions {
|
|
18
|
+
script?: FakeSTTScriptSegmentInput[];
|
|
19
|
+
language?: string;
|
|
20
|
+
defaultStepDelayMs?: number;
|
|
21
|
+
jitterMs?: number;
|
|
22
|
+
seed?: number;
|
|
23
|
+
offset?: number;
|
|
24
|
+
emitRecognitionUsage?: boolean;
|
|
25
|
+
sampleRate?: number;
|
|
26
|
+
silenceDurationMsToCommit?: number;
|
|
27
|
+
silenceAmplitudeThreshold?: number;
|
|
28
|
+
}
|
|
29
|
+
interface NormalizedStep {
|
|
30
|
+
text: string;
|
|
31
|
+
final: boolean;
|
|
32
|
+
delayMs?: number;
|
|
33
|
+
confidence?: number;
|
|
34
|
+
language?: string;
|
|
35
|
+
}
|
|
36
|
+
interface NormalizedSegment {
|
|
37
|
+
steps: NormalizedStep[];
|
|
38
|
+
emitUsage?: boolean;
|
|
39
|
+
usageAudioDuration?: number;
|
|
40
|
+
}
|
|
41
|
+
interface ResolvedOptions {
|
|
42
|
+
script: NormalizedSegment[];
|
|
43
|
+
language: string;
|
|
44
|
+
defaultStepDelayMs: number;
|
|
45
|
+
jitterMs: number;
|
|
46
|
+
seed: number;
|
|
47
|
+
initialOffset: number;
|
|
48
|
+
emitRecognitionUsage: boolean;
|
|
49
|
+
sampleRate: number;
|
|
50
|
+
silenceDurationMsToCommit: number;
|
|
51
|
+
silenceAmplitudeThreshold: number;
|
|
52
|
+
}
|
|
53
|
+
declare const DEFAULT_SCRIPT: FakeSTTScriptSegmentInput[];
|
|
54
|
+
declare class STT extends stt.STT {
|
|
55
|
+
#private;
|
|
56
|
+
readonly label = "fake.STT";
|
|
57
|
+
constructor(options?: FakeSTTOptions);
|
|
58
|
+
get options(): Readonly<ResolvedOptions>;
|
|
59
|
+
protected _recognize(frame: AudioBuffer, abortSignal?: AbortSignal): Promise<stt.SpeechEvent>;
|
|
60
|
+
stream(options?: {
|
|
61
|
+
connOptions?: APIConnectOptions;
|
|
62
|
+
}): SpeechStream;
|
|
63
|
+
}
|
|
64
|
+
declare class SpeechStream extends stt.SpeechStream {
|
|
65
|
+
#private;
|
|
66
|
+
readonly label = "fake.SpeechStream";
|
|
67
|
+
constructor(sttImpl: STT, options: ResolvedOptions, connOptions?: APIConnectOptions);
|
|
68
|
+
protected run(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { DEFAULT_SCRIPT, type FakeSTTOptions, type FakeSTTScriptSegment, type FakeSTTScriptSegmentInput, type FakeSTTScriptStep, type FakeSTTScriptStepInput, STT, SpeechStream };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { stt, mergeFrames } from '@livekit/agents';
|
|
2
|
+
|
|
3
|
+
// src/stt.ts
|
|
4
|
+
var DEFAULT_SCRIPT = [
|
|
5
|
+
[
|
|
6
|
+
{ text: "segment-a partial-1", final: false },
|
|
7
|
+
{ text: "segment-a partial-2", final: false },
|
|
8
|
+
{ text: "segment-a final", final: true }
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
{ text: "segment-b partial", final: false },
|
|
12
|
+
{ text: "segment-b final", final: true }
|
|
13
|
+
]
|
|
14
|
+
];
|
|
15
|
+
var DEFAULT_OPTIONS = {
|
|
16
|
+
language: "en-US",
|
|
17
|
+
defaultStepDelayMs: 0,
|
|
18
|
+
jitterMs: 0,
|
|
19
|
+
seed: 0,
|
|
20
|
+
offset: 0,
|
|
21
|
+
emitRecognitionUsage: false,
|
|
22
|
+
sampleRate: 16e3,
|
|
23
|
+
silenceDurationMsToCommit: 400,
|
|
24
|
+
silenceAmplitudeThreshold: 150
|
|
25
|
+
};
|
|
26
|
+
var STT = class extends stt.STT {
|
|
27
|
+
label = "fake.STT";
|
|
28
|
+
#options;
|
|
29
|
+
#recognizeCount = 0;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
super({
|
|
32
|
+
streaming: true,
|
|
33
|
+
interimResults: true,
|
|
34
|
+
alignedTranscript: false
|
|
35
|
+
});
|
|
36
|
+
this.#options = resolveOptions(options);
|
|
37
|
+
}
|
|
38
|
+
get options() {
|
|
39
|
+
return this.#options;
|
|
40
|
+
}
|
|
41
|
+
async _recognize(frame, abortSignal) {
|
|
42
|
+
if (abortSignal?.aborted) {
|
|
43
|
+
throw abortError();
|
|
44
|
+
}
|
|
45
|
+
const merged = mergeFrames(frame);
|
|
46
|
+
const duration = frameDurationSeconds(merged.samplesPerChannel, merged.sampleRate);
|
|
47
|
+
const segment = this.#segmentForRecognize();
|
|
48
|
+
const transcript = segment.steps.filter((step) => step.final).map((step) => step.text).join(" ").trim() || segment.steps[segment.steps.length - 1]?.text || "";
|
|
49
|
+
return {
|
|
50
|
+
type: stt.SpeechEventType.FINAL_TRANSCRIPT,
|
|
51
|
+
requestId: `fake-stt-recognize-${this.#recognizeCount - 1}`,
|
|
52
|
+
alternatives: [
|
|
53
|
+
{
|
|
54
|
+
text: transcript,
|
|
55
|
+
language: this.#options.language,
|
|
56
|
+
startTime: 0,
|
|
57
|
+
endTime: duration,
|
|
58
|
+
confidence: 1
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
stream(options) {
|
|
64
|
+
return new SpeechStream(this, this.#options, options?.connOptions);
|
|
65
|
+
}
|
|
66
|
+
#segmentForRecognize() {
|
|
67
|
+
const segment = getSegmentAt(
|
|
68
|
+
this.#options.script,
|
|
69
|
+
positiveModulo(
|
|
70
|
+
this.#options.initialOffset + this.#recognizeCount,
|
|
71
|
+
this.#options.script.length
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
this.#recognizeCount += 1;
|
|
75
|
+
return segment;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var SpeechStream = class _SpeechStream extends stt.SpeechStream {
|
|
79
|
+
label = "fake.SpeechStream";
|
|
80
|
+
#options;
|
|
81
|
+
#cursor;
|
|
82
|
+
#utteranceCount = 0;
|
|
83
|
+
#pendingAudioDuration = 0;
|
|
84
|
+
#hasPendingAudio = false;
|
|
85
|
+
#hasDetectedSpeech = false;
|
|
86
|
+
#pendingSilenceDurationMs = 0;
|
|
87
|
+
#processedAudioDuration = 0;
|
|
88
|
+
constructor(sttImpl, options, connOptions) {
|
|
89
|
+
super(sttImpl, options.sampleRate, connOptions);
|
|
90
|
+
this.#options = options;
|
|
91
|
+
this.#cursor = options.initialOffset;
|
|
92
|
+
}
|
|
93
|
+
async run() {
|
|
94
|
+
try {
|
|
95
|
+
for await (const item of this.input) {
|
|
96
|
+
if (this.abortSignal.aborted || this.closed) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (item === _SpeechStream.FLUSH_SENTINEL) {
|
|
100
|
+
await this.#emitPendingUtterance();
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const duration = frameDurationSeconds(item.samplesPerChannel, item.sampleRate);
|
|
104
|
+
if (duration <= 0) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
this.#hasPendingAudio = true;
|
|
108
|
+
this.#pendingAudioDuration += duration;
|
|
109
|
+
const frameDurationMs = duration * 1e3;
|
|
110
|
+
if (isSilentAudioFrame(item, this.#options.silenceAmplitudeThreshold)) {
|
|
111
|
+
if (this.#hasDetectedSpeech) {
|
|
112
|
+
this.#pendingSilenceDurationMs += frameDurationMs;
|
|
113
|
+
}
|
|
114
|
+
if (this.#hasDetectedSpeech && this.#pendingSilenceDurationMs >= this.#options.silenceDurationMsToCommit) {
|
|
115
|
+
await this.#emitPendingUtterance();
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
this.#hasDetectedSpeech = true;
|
|
120
|
+
this.#pendingSilenceDurationMs = 0;
|
|
121
|
+
}
|
|
122
|
+
await this.#emitPendingUtterance();
|
|
123
|
+
} finally {
|
|
124
|
+
this.closed = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async #emitPendingUtterance() {
|
|
128
|
+
if (!this.#hasPendingAudio || this.abortSignal.aborted || this.closed) {
|
|
129
|
+
this.#resetPendingAudio();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const utteranceIndex = this.#utteranceCount;
|
|
133
|
+
const requestId = `fake-stt-${utteranceIndex}`;
|
|
134
|
+
const segmentAudioDuration = this.#pendingAudioDuration;
|
|
135
|
+
const segmentStart = this.startTimeOffset + this.#processedAudioDuration;
|
|
136
|
+
const segment = this.#nextSegment();
|
|
137
|
+
this.#processedAudioDuration += segmentAudioDuration;
|
|
138
|
+
this.#utteranceCount += 1;
|
|
139
|
+
this.#resetPendingAudio();
|
|
140
|
+
if (!this.#putEvent({ type: stt.SpeechEventType.START_OF_SPEECH, requestId })) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const stepsCount = segment.steps.length;
|
|
144
|
+
for (const [stepIndex, step] of segment.steps.entries()) {
|
|
145
|
+
const delayMs = this.#computeDelay(step, utteranceIndex, stepIndex);
|
|
146
|
+
const continueEmission = await sleepWithAbort(delayMs, this.abortSignal);
|
|
147
|
+
if (!continueEmission || this.closed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const eventType = step.final ? stt.SpeechEventType.FINAL_TRANSCRIPT : stt.SpeechEventType.INTERIM_TRANSCRIPT;
|
|
151
|
+
const stepEnd = segmentStart + segmentAudioDuration * ((stepIndex + 1) / stepsCount);
|
|
152
|
+
const transcriptEvent = {
|
|
153
|
+
type: eventType,
|
|
154
|
+
requestId,
|
|
155
|
+
alternatives: [
|
|
156
|
+
{
|
|
157
|
+
text: step.text,
|
|
158
|
+
language: step.language ?? this.#options.language,
|
|
159
|
+
startTime: segmentStart,
|
|
160
|
+
endTime: stepEnd,
|
|
161
|
+
confidence: step.confidence ?? (step.final ? 0.95 : 0.65)
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
if (!this.#putEvent(transcriptEvent)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!this.#putEvent({ type: stt.SpeechEventType.END_OF_SPEECH, requestId })) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const shouldEmitUsage = segment.emitUsage ?? this.#options.emitRecognitionUsage;
|
|
173
|
+
if (!shouldEmitUsage) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.#putEvent({
|
|
177
|
+
type: stt.SpeechEventType.RECOGNITION_USAGE,
|
|
178
|
+
requestId,
|
|
179
|
+
recognitionUsage: {
|
|
180
|
+
audioDuration: segment.usageAudioDuration ?? segmentAudioDuration
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
#nextSegment() {
|
|
185
|
+
const segment = getSegmentAt(this.#options.script, this.#cursor);
|
|
186
|
+
this.#cursor = positiveModulo(this.#cursor + 1, this.#options.script.length);
|
|
187
|
+
return segment;
|
|
188
|
+
}
|
|
189
|
+
#computeDelay(step, utteranceIndex, stepIndex) {
|
|
190
|
+
const baseDelay = step.delayMs ?? this.#options.defaultStepDelayMs;
|
|
191
|
+
if (this.#options.jitterMs <= 0) {
|
|
192
|
+
return baseDelay;
|
|
193
|
+
}
|
|
194
|
+
const mixed = hash32(
|
|
195
|
+
toUint32(this.#options.seed) ^ toUint32((utteranceIndex + 1) * 2654435761) ^ toUint32((stepIndex + 1) * 2246822507)
|
|
196
|
+
);
|
|
197
|
+
const unit = mixed / 4294967295;
|
|
198
|
+
const jitter = Math.round((unit * 2 - 1) * this.#options.jitterMs);
|
|
199
|
+
return Math.max(0, baseDelay + jitter);
|
|
200
|
+
}
|
|
201
|
+
#putEvent(event) {
|
|
202
|
+
if (this.closed || this.abortSignal.aborted || this.queue.closed) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
this.queue.put(event);
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
#resetPendingAudio() {
|
|
213
|
+
this.#hasPendingAudio = false;
|
|
214
|
+
this.#hasDetectedSpeech = false;
|
|
215
|
+
this.#pendingAudioDuration = 0;
|
|
216
|
+
this.#pendingSilenceDurationMs = 0;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
function resolveOptions(options) {
|
|
220
|
+
const script = normalizeScript(options.script ?? DEFAULT_SCRIPT);
|
|
221
|
+
if (script.length === 0) {
|
|
222
|
+
throw new Error("Fake STT script must contain at least one segment");
|
|
223
|
+
}
|
|
224
|
+
const seed = asInteger(options.seed ?? DEFAULT_OPTIONS.seed);
|
|
225
|
+
const offset = asInteger(options.offset ?? DEFAULT_OPTIONS.offset);
|
|
226
|
+
const seedOffset = options.seed === void 0 ? 0 : positiveModulo(hash32(seed), script.length);
|
|
227
|
+
const initialOffset = positiveModulo(offset + seedOffset, script.length);
|
|
228
|
+
return {
|
|
229
|
+
script,
|
|
230
|
+
language: options.language ?? DEFAULT_OPTIONS.language,
|
|
231
|
+
defaultStepDelayMs: clampNumber(
|
|
232
|
+
options.defaultStepDelayMs ?? DEFAULT_OPTIONS.defaultStepDelayMs,
|
|
233
|
+
0
|
|
234
|
+
),
|
|
235
|
+
jitterMs: clampNumber(options.jitterMs ?? DEFAULT_OPTIONS.jitterMs, 0),
|
|
236
|
+
seed,
|
|
237
|
+
initialOffset,
|
|
238
|
+
emitRecognitionUsage: options.emitRecognitionUsage ?? DEFAULT_OPTIONS.emitRecognitionUsage,
|
|
239
|
+
sampleRate: clampNumber(options.sampleRate ?? DEFAULT_OPTIONS.sampleRate, 1),
|
|
240
|
+
silenceDurationMsToCommit: clampNumber(
|
|
241
|
+
options.silenceDurationMsToCommit ?? DEFAULT_OPTIONS.silenceDurationMsToCommit,
|
|
242
|
+
0
|
|
243
|
+
),
|
|
244
|
+
silenceAmplitudeThreshold: clampNumber(
|
|
245
|
+
options.silenceAmplitudeThreshold ?? DEFAULT_OPTIONS.silenceAmplitudeThreshold,
|
|
246
|
+
0
|
|
247
|
+
)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function normalizeScript(script) {
|
|
251
|
+
return script.map((segmentInput, segmentIndex) => {
|
|
252
|
+
const segment = Array.isArray(segmentInput) ? { steps: segmentInput } : segmentInput;
|
|
253
|
+
if (!segment.steps.length) {
|
|
254
|
+
throw new Error(`Fake STT segment at index ${segmentIndex} is empty`);
|
|
255
|
+
}
|
|
256
|
+
const steps = segment.steps.map(
|
|
257
|
+
(stepInput, stepIndex) => normalizeStep(stepInput, segmentIndex, stepIndex)
|
|
258
|
+
);
|
|
259
|
+
if (!steps.some((step) => step.final)) {
|
|
260
|
+
const last = steps[steps.length - 1];
|
|
261
|
+
if (!last) {
|
|
262
|
+
throw new Error(`Fake STT segment at index ${segmentIndex} is empty`);
|
|
263
|
+
}
|
|
264
|
+
last.final = true;
|
|
265
|
+
}
|
|
266
|
+
const normalized = { steps };
|
|
267
|
+
if (segment.emitUsage !== void 0) {
|
|
268
|
+
normalized.emitUsage = segment.emitUsage;
|
|
269
|
+
}
|
|
270
|
+
if (segment.usageAudioDuration !== void 0) {
|
|
271
|
+
normalized.usageAudioDuration = segment.usageAudioDuration;
|
|
272
|
+
}
|
|
273
|
+
return normalized;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function normalizeStep(stepInput, segmentIndex, stepIndex) {
|
|
277
|
+
const step = typeof stepInput === "string" ? { text: stepInput } : stepInput;
|
|
278
|
+
if (!step.text) {
|
|
279
|
+
throw new Error(`Fake STT step at segment ${segmentIndex}, index ${stepIndex} is missing text`);
|
|
280
|
+
}
|
|
281
|
+
const normalized = {
|
|
282
|
+
text: step.text,
|
|
283
|
+
final: Boolean(step.final)
|
|
284
|
+
};
|
|
285
|
+
if (step.delayMs !== void 0) {
|
|
286
|
+
normalized.delayMs = step.delayMs;
|
|
287
|
+
}
|
|
288
|
+
if (step.confidence !== void 0) {
|
|
289
|
+
normalized.confidence = step.confidence;
|
|
290
|
+
}
|
|
291
|
+
if (step.language !== void 0) {
|
|
292
|
+
normalized.language = step.language;
|
|
293
|
+
}
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
function frameDurationSeconds(samplesPerChannel, sampleRate) {
|
|
297
|
+
if (samplesPerChannel <= 0 || sampleRate <= 0) {
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
return samplesPerChannel / sampleRate;
|
|
301
|
+
}
|
|
302
|
+
function isSilentAudioFrame(frame, silenceAmplitudeThreshold) {
|
|
303
|
+
if (frame.samplesPerChannel <= 0) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
for (let i = 0; i < frame.data.length; i++) {
|
|
307
|
+
const sample = frame.data[i];
|
|
308
|
+
if (sample !== void 0 && Math.abs(sample) > silenceAmplitudeThreshold) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
function abortError() {
|
|
315
|
+
const err = new Error("The operation was aborted");
|
|
316
|
+
err.name = "AbortError";
|
|
317
|
+
return err;
|
|
318
|
+
}
|
|
319
|
+
function asInteger(value) {
|
|
320
|
+
if (!Number.isFinite(value)) {
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
return Math.trunc(value);
|
|
324
|
+
}
|
|
325
|
+
function clampNumber(value, minimum) {
|
|
326
|
+
if (!Number.isFinite(value)) {
|
|
327
|
+
return minimum;
|
|
328
|
+
}
|
|
329
|
+
return Math.max(minimum, value);
|
|
330
|
+
}
|
|
331
|
+
function positiveModulo(value, modulus) {
|
|
332
|
+
return (value % modulus + modulus) % modulus;
|
|
333
|
+
}
|
|
334
|
+
function toUint32(value) {
|
|
335
|
+
return value >>> 0;
|
|
336
|
+
}
|
|
337
|
+
function hash32(seed) {
|
|
338
|
+
let x = toUint32(seed) || 2654435769;
|
|
339
|
+
x ^= x << 13;
|
|
340
|
+
x ^= x >>> 17;
|
|
341
|
+
x ^= x << 5;
|
|
342
|
+
return toUint32(x);
|
|
343
|
+
}
|
|
344
|
+
function getSegmentAt(script, index) {
|
|
345
|
+
const segment = script[index];
|
|
346
|
+
if (!segment) {
|
|
347
|
+
throw new Error(`Missing script segment at index ${index}`);
|
|
348
|
+
}
|
|
349
|
+
return segment;
|
|
350
|
+
}
|
|
351
|
+
async function sleepWithAbort(ms, signal) {
|
|
352
|
+
if (ms <= 0) {
|
|
353
|
+
return !signal.aborted;
|
|
354
|
+
}
|
|
355
|
+
if (signal.aborted) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
return new Promise((resolve) => {
|
|
359
|
+
const timer = setTimeout(() => {
|
|
360
|
+
cleanup();
|
|
361
|
+
resolve(true);
|
|
362
|
+
}, ms);
|
|
363
|
+
const onAbort = () => {
|
|
364
|
+
cleanup();
|
|
365
|
+
resolve(false);
|
|
366
|
+
};
|
|
367
|
+
const cleanup = () => {
|
|
368
|
+
clearTimeout(timer);
|
|
369
|
+
signal.removeEventListener("abort", onAbort);
|
|
370
|
+
};
|
|
371
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export { DEFAULT_SCRIPT, STT, SpeechStream };
|
|
376
|
+
//# sourceMappingURL=index.js.map
|
|
377
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stt.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { mergeFrames, stt, type APIConnectOptions, type AudioBuffer } from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\n\nexport interface FakeSTTScriptStep {\n text: string;\n final?: boolean;\n delayMs?: number;\n confidence?: number;\n language?: string;\n}\n\nexport type FakeSTTScriptStepInput = string | FakeSTTScriptStep;\n\nexport interface FakeSTTScriptSegment {\n steps: FakeSTTScriptStepInput[];\n emitUsage?: boolean;\n usageAudioDuration?: number;\n}\n\nexport type FakeSTTScriptSegmentInput = FakeSTTScriptSegment | FakeSTTScriptStepInput[];\n\nexport interface FakeSTTOptions {\n script?: FakeSTTScriptSegmentInput[];\n language?: string;\n defaultStepDelayMs?: number;\n jitterMs?: number;\n seed?: number;\n offset?: number;\n emitRecognitionUsage?: boolean;\n sampleRate?: number;\n silenceDurationMsToCommit?: number;\n silenceAmplitudeThreshold?: number;\n}\n\ninterface NormalizedStep {\n text: string;\n final: boolean;\n delayMs?: number;\n confidence?: number;\n language?: string;\n}\n\ninterface NormalizedSegment {\n steps: NormalizedStep[];\n emitUsage?: boolean;\n usageAudioDuration?: number;\n}\n\ninterface ResolvedOptions {\n script: NormalizedSegment[];\n language: string;\n defaultStepDelayMs: number;\n jitterMs: number;\n seed: number;\n initialOffset: number;\n emitRecognitionUsage: boolean;\n sampleRate: number;\n silenceDurationMsToCommit: number;\n silenceAmplitudeThreshold: number;\n}\n\nconst DEFAULT_SCRIPT: FakeSTTScriptSegmentInput[] = [\n [\n { text: 'segment-a partial-1', final: false },\n { text: 'segment-a partial-2', final: false },\n { text: 'segment-a final', final: true },\n ],\n [\n { text: 'segment-b partial', final: false },\n { text: 'segment-b final', final: true },\n ],\n];\n\nconst DEFAULT_OPTIONS: Required<Omit<FakeSTTOptions, 'script'>> = {\n language: 'en-US',\n defaultStepDelayMs: 0,\n jitterMs: 0,\n seed: 0,\n offset: 0,\n emitRecognitionUsage: false,\n sampleRate: 16000,\n silenceDurationMsToCommit: 400,\n silenceAmplitudeThreshold: 150,\n};\n\nexport class STT extends stt.STT {\n readonly label = 'fake.STT';\n\n #options: ResolvedOptions;\n #recognizeCount = 0;\n\n constructor(options: FakeSTTOptions = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: false,\n });\n\n this.#options = resolveOptions(options);\n }\n\n get options(): Readonly<ResolvedOptions> {\n return this.#options;\n }\n\n protected async _recognize(\n frame: AudioBuffer,\n abortSignal?: AbortSignal\n ): Promise<stt.SpeechEvent> {\n if (abortSignal?.aborted) {\n throw abortError();\n }\n\n const merged = mergeFrames(frame);\n const duration = frameDurationSeconds(merged.samplesPerChannel, merged.sampleRate);\n const segment = this.#segmentForRecognize();\n const transcript =\n segment.steps\n .filter((step) => step.final)\n .map((step) => step.text)\n .join(' ')\n .trim() ||\n segment.steps[segment.steps.length - 1]?.text ||\n '';\n\n return {\n type: stt.SpeechEventType.FINAL_TRANSCRIPT,\n requestId: `fake-stt-recognize-${this.#recognizeCount - 1}`,\n alternatives: [\n {\n text: transcript,\n language: this.#options.language,\n startTime: 0,\n endTime: duration,\n confidence: 1,\n },\n ],\n };\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {\n return new SpeechStream(this, this.#options, options?.connOptions);\n }\n\n #segmentForRecognize(): NormalizedSegment {\n const segment = getSegmentAt(\n this.#options.script,\n positiveModulo(\n this.#options.initialOffset + this.#recognizeCount,\n this.#options.script.length\n )\n );\n\n this.#recognizeCount += 1;\n return segment;\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n readonly label = 'fake.SpeechStream';\n\n #options: ResolvedOptions;\n #cursor: number;\n #utteranceCount = 0;\n #pendingAudioDuration = 0;\n #hasPendingAudio = false;\n #hasDetectedSpeech = false;\n #pendingSilenceDurationMs = 0;\n #processedAudioDuration = 0;\n\n constructor(sttImpl: STT, options: ResolvedOptions, connOptions?: APIConnectOptions) {\n super(sttImpl, options.sampleRate, connOptions);\n this.#options = options;\n this.#cursor = options.initialOffset;\n }\n\n protected async run(): Promise<void> {\n try {\n for await (const item of this.input) {\n if (this.abortSignal.aborted || this.closed) {\n return;\n }\n\n if (item === SpeechStream.FLUSH_SENTINEL) {\n await this.#emitPendingUtterance();\n continue;\n }\n\n const duration = frameDurationSeconds(item.samplesPerChannel, item.sampleRate);\n if (duration <= 0) {\n continue;\n }\n\n this.#hasPendingAudio = true;\n this.#pendingAudioDuration += duration;\n\n const frameDurationMs = duration * 1000;\n if (isSilentAudioFrame(item, this.#options.silenceAmplitudeThreshold)) {\n if (this.#hasDetectedSpeech) {\n this.#pendingSilenceDurationMs += frameDurationMs;\n }\n\n if (\n this.#hasDetectedSpeech &&\n this.#pendingSilenceDurationMs >= this.#options.silenceDurationMsToCommit\n ) {\n await this.#emitPendingUtterance();\n }\n continue;\n }\n\n this.#hasDetectedSpeech = true;\n this.#pendingSilenceDurationMs = 0;\n }\n\n await this.#emitPendingUtterance();\n } finally {\n this.closed = true;\n }\n }\n\n async #emitPendingUtterance(): Promise<void> {\n if (!this.#hasPendingAudio || this.abortSignal.aborted || this.closed) {\n this.#resetPendingAudio();\n return;\n }\n\n const utteranceIndex = this.#utteranceCount;\n const requestId = `fake-stt-${utteranceIndex}`;\n const segmentAudioDuration = this.#pendingAudioDuration;\n const segmentStart = this.startTimeOffset + this.#processedAudioDuration;\n const segment = this.#nextSegment();\n\n this.#processedAudioDuration += segmentAudioDuration;\n this.#utteranceCount += 1;\n this.#resetPendingAudio();\n\n if (!this.#putEvent({ type: stt.SpeechEventType.START_OF_SPEECH, requestId })) {\n return;\n }\n\n const stepsCount = segment.steps.length;\n for (const [stepIndex, step] of segment.steps.entries()) {\n const delayMs = this.#computeDelay(step, utteranceIndex, stepIndex);\n const continueEmission = await sleepWithAbort(delayMs, this.abortSignal);\n if (!continueEmission || this.closed) {\n return;\n }\n\n const eventType = step.final\n ? stt.SpeechEventType.FINAL_TRANSCRIPT\n : stt.SpeechEventType.INTERIM_TRANSCRIPT;\n\n const stepEnd = segmentStart + segmentAudioDuration * ((stepIndex + 1) / stepsCount);\n const transcriptEvent: stt.SpeechEvent = {\n type: eventType,\n requestId,\n alternatives: [\n {\n text: step.text,\n language: step.language ?? this.#options.language,\n startTime: segmentStart,\n endTime: stepEnd,\n confidence: step.confidence ?? (step.final ? 0.95 : 0.65),\n },\n ],\n };\n\n if (!this.#putEvent(transcriptEvent)) {\n return;\n }\n }\n\n if (!this.#putEvent({ type: stt.SpeechEventType.END_OF_SPEECH, requestId })) {\n return;\n }\n\n const shouldEmitUsage = segment.emitUsage ?? this.#options.emitRecognitionUsage;\n if (!shouldEmitUsage) {\n return;\n }\n\n this.#putEvent({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId,\n recognitionUsage: {\n audioDuration: segment.usageAudioDuration ?? segmentAudioDuration,\n },\n });\n }\n\n #nextSegment(): NormalizedSegment {\n const segment = getSegmentAt(this.#options.script, this.#cursor);\n this.#cursor = positiveModulo(this.#cursor + 1, this.#options.script.length);\n return segment;\n }\n\n #computeDelay(step: NormalizedStep, utteranceIndex: number, stepIndex: number): number {\n const baseDelay = step.delayMs ?? this.#options.defaultStepDelayMs;\n if (this.#options.jitterMs <= 0) {\n return baseDelay;\n }\n\n const mixed = hash32(\n toUint32(this.#options.seed) ^\n toUint32((utteranceIndex + 1) * 0x9e3779b1) ^\n toUint32((stepIndex + 1) * 0x85ebca6b)\n );\n\n const unit = mixed / 0xffffffff;\n const jitter = Math.round((unit * 2 - 1) * this.#options.jitterMs);\n return Math.max(0, baseDelay + jitter);\n }\n\n #putEvent(event: stt.SpeechEvent): boolean {\n if (this.closed || this.abortSignal.aborted || this.queue.closed) {\n return false;\n }\n\n try {\n this.queue.put(event);\n return true;\n } catch {\n return false;\n }\n }\n\n #resetPendingAudio(): void {\n this.#hasPendingAudio = false;\n this.#hasDetectedSpeech = false;\n this.#pendingAudioDuration = 0;\n this.#pendingSilenceDurationMs = 0;\n }\n}\n\nfunction resolveOptions(options: FakeSTTOptions): ResolvedOptions {\n const script = normalizeScript(options.script ?? DEFAULT_SCRIPT);\n if (script.length === 0) {\n throw new Error('Fake STT script must contain at least one segment');\n }\n\n const seed = asInteger(options.seed ?? DEFAULT_OPTIONS.seed);\n const offset = asInteger(options.offset ?? DEFAULT_OPTIONS.offset);\n const seedOffset = options.seed === undefined ? 0 : positiveModulo(hash32(seed), script.length);\n const initialOffset = positiveModulo(offset + seedOffset, script.length);\n\n return {\n script,\n language: options.language ?? DEFAULT_OPTIONS.language,\n defaultStepDelayMs: clampNumber(\n options.defaultStepDelayMs ?? DEFAULT_OPTIONS.defaultStepDelayMs,\n 0\n ),\n jitterMs: clampNumber(options.jitterMs ?? DEFAULT_OPTIONS.jitterMs, 0),\n seed,\n initialOffset,\n emitRecognitionUsage: options.emitRecognitionUsage ?? DEFAULT_OPTIONS.emitRecognitionUsage,\n sampleRate: clampNumber(options.sampleRate ?? DEFAULT_OPTIONS.sampleRate, 1),\n silenceDurationMsToCommit: clampNumber(\n options.silenceDurationMsToCommit ?? DEFAULT_OPTIONS.silenceDurationMsToCommit,\n 0\n ),\n silenceAmplitudeThreshold: clampNumber(\n options.silenceAmplitudeThreshold ?? DEFAULT_OPTIONS.silenceAmplitudeThreshold,\n 0\n ),\n };\n}\n\nfunction normalizeScript(script: FakeSTTScriptSegmentInput[]): NormalizedSegment[] {\n return script.map((segmentInput, segmentIndex) => {\n const segment = Array.isArray(segmentInput) ? { steps: segmentInput } : segmentInput;\n if (!segment.steps.length) {\n throw new Error(`Fake STT segment at index ${segmentIndex} is empty`);\n }\n\n const steps = segment.steps.map((stepInput, stepIndex) =>\n normalizeStep(stepInput, segmentIndex, stepIndex)\n );\n if (!steps.some((step) => step.final)) {\n const last = steps[steps.length - 1];\n if (!last) {\n throw new Error(`Fake STT segment at index ${segmentIndex} is empty`);\n }\n last.final = true;\n }\n\n const normalized: NormalizedSegment = { steps };\n if (segment.emitUsage !== undefined) {\n normalized.emitUsage = segment.emitUsage;\n }\n if (segment.usageAudioDuration !== undefined) {\n normalized.usageAudioDuration = segment.usageAudioDuration;\n }\n return normalized;\n });\n}\n\nfunction normalizeStep(\n stepInput: FakeSTTScriptStepInput,\n segmentIndex: number,\n stepIndex: number\n): NormalizedStep {\n const step = typeof stepInput === 'string' ? { text: stepInput } : stepInput;\n\n if (!step.text) {\n throw new Error(`Fake STT step at segment ${segmentIndex}, index ${stepIndex} is missing text`);\n }\n\n const normalized: NormalizedStep = {\n text: step.text,\n final: Boolean(step.final),\n };\n\n if (step.delayMs !== undefined) {\n normalized.delayMs = step.delayMs;\n }\n if (step.confidence !== undefined) {\n normalized.confidence = step.confidence;\n }\n if (step.language !== undefined) {\n normalized.language = step.language;\n }\n\n return normalized;\n}\n\nfunction frameDurationSeconds(samplesPerChannel: number, sampleRate: number): number {\n if (samplesPerChannel <= 0 || sampleRate <= 0) {\n return 0;\n }\n\n return samplesPerChannel / sampleRate;\n}\n\nfunction isSilentAudioFrame(frame: AudioFrame, silenceAmplitudeThreshold: number): boolean {\n if (frame.samplesPerChannel <= 0) {\n return true;\n }\n\n for (let i = 0; i < frame.data.length; i++) {\n const sample = frame.data[i];\n if (sample !== undefined && Math.abs(sample) > silenceAmplitudeThreshold) {\n return false;\n }\n }\n\n return true;\n}\n\nfunction abortError(): Error {\n const err = new Error('The operation was aborted');\n err.name = 'AbortError';\n return err;\n}\n\nfunction asInteger(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.trunc(value);\n}\n\nfunction clampNumber(value: number, minimum: number): number {\n if (!Number.isFinite(value)) {\n return minimum;\n }\n return Math.max(minimum, value);\n}\n\nfunction positiveModulo(value: number, modulus: number): number {\n return ((value % modulus) + modulus) % modulus;\n}\n\nfunction toUint32(value: number): number {\n return value >>> 0;\n}\n\nfunction hash32(seed: number): number {\n let x = toUint32(seed) || 0x9e3779b9;\n x ^= x << 13;\n x ^= x >>> 17;\n x ^= x << 5;\n return toUint32(x);\n}\n\nfunction getSegmentAt(script: NormalizedSegment[], index: number): NormalizedSegment {\n const segment = script[index];\n if (!segment) {\n throw new Error(`Missing script segment at index ${index}`);\n }\n return segment;\n}\n\nasync function sleepWithAbort(ms: number, signal: AbortSignal): Promise<boolean> {\n if (ms <= 0) {\n return !signal.aborted;\n }\n\n if (signal.aborted) {\n return false;\n }\n\n return new Promise<boolean>((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n resolve(true);\n }, ms);\n\n const onAbort = () => {\n cleanup();\n resolve(false);\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n };\n\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nexport { DEFAULT_SCRIPT };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workadventure/livekit-agent-plugin-fake-stt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic fake STT provider for LiveKit Agents tests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/workadventure/livekit-agent-plugin-fake-stt.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/workadventure/livekit-agent-plugin-fake-stt#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/workadventure/livekit-agent-plugin-fake-stt/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"livekit",
|
|
20
|
+
"agents",
|
|
21
|
+
"stt",
|
|
22
|
+
"speech-to-text",
|
|
23
|
+
"testing",
|
|
24
|
+
"deterministic"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"clean": "rm -rf dist coverage",
|
|
43
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
44
|
+
"lint": "eslint src test examples eslint.config.js vitest.config.ts tsup.config.ts",
|
|
45
|
+
"format": "prettier --check .",
|
|
46
|
+
"format:write": "prettier --write .",
|
|
47
|
+
"test": "vitest run --coverage",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"prepack": "npm run clean && npm run build"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@livekit/agents": "^1.0.48",
|
|
53
|
+
"@livekit/rtc-node": "^0.13.24"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@eslint/js": "^9.39.1",
|
|
57
|
+
"@livekit/agents": "^1.0.48",
|
|
58
|
+
"@livekit/rtc-node": "^0.13.24",
|
|
59
|
+
"@types/node": "^24.10.1",
|
|
60
|
+
"@vitest/coverage-v8": "^4.0.0",
|
|
61
|
+
"eslint": "^9.39.1",
|
|
62
|
+
"globals": "^17.4.0",
|
|
63
|
+
"prettier": "^3.6.2",
|
|
64
|
+
"tsup": "^8.5.0",
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
|
+
"typescript-eslint": "^8.46.2",
|
|
67
|
+
"vitest": "^4.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|