@livekit/agents 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/.turbo/turbo-build.log +4 -0
- package/api-extractor.json +20 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator.d.ts +12 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +8 -0
- package/dist/generator.js.map +1 -0
- package/dist/http_server.d.ts +11 -0
- package/dist/http_server.d.ts.map +1 -0
- package/dist/http_server.js +45 -0
- package/dist/http_server.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/job_main.d.ts +5 -0
- package/dist/ipc/job_main.d.ts.map +1 -0
- package/dist/ipc/job_main.js +77 -0
- package/dist/ipc/job_main.js.map +1 -0
- package/dist/ipc/job_process.d.ts +22 -0
- package/dist/ipc/job_process.d.ts.map +1 -0
- package/dist/ipc/job_process.js +73 -0
- package/dist/ipc/job_process.js.map +1 -0
- package/dist/ipc/protocol.d.ts +40 -0
- package/dist/ipc/protocol.d.ts.map +1 -0
- package/dist/ipc/protocol.js +14 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/job_context.d.ts +16 -0
- package/dist/job_context.d.ts.map +1 -0
- package/dist/job_context.js +31 -0
- package/dist/job_context.js.map +1 -0
- package/dist/job_request.d.ts +42 -0
- package/dist/job_request.d.ts.map +1 -0
- package/dist/job_request.js +79 -0
- package/dist/job_request.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +13 -0
- package/dist/log.js.map +1 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +22 -0
- package/dist/plugin.js.map +1 -0
- package/dist/stt/index.d.ts +3 -0
- package/dist/stt/index.d.ts.map +1 -0
- package/dist/stt/index.js +6 -0
- package/dist/stt/index.js.map +1 -0
- package/dist/stt/stream_adapter.d.ts +28 -0
- package/dist/stt/stream_adapter.d.ts.map +1 -0
- package/dist/stt/stream_adapter.js +82 -0
- package/dist/stt/stream_adapter.js.map +1 -0
- package/dist/stt/stt.d.ts +34 -0
- package/dist/stt/stt.d.ts.map +1 -0
- package/dist/stt/stt.js +30 -0
- package/dist/stt/stt.js.map +1 -0
- package/dist/tokenize.d.ts +15 -0
- package/dist/tokenize.d.ts.map +1 -0
- package/dist/tokenize.js +12 -0
- package/dist/tokenize.js.map +1 -0
- package/dist/tts/index.d.ts +4 -0
- package/dist/tts/index.d.ts.map +1 -0
- package/dist/tts/index.js +7 -0
- package/dist/tts/index.js.map +1 -0
- package/dist/tts/stream_adapter.d.ts +26 -0
- package/dist/tts/stream_adapter.d.ts.map +1 -0
- package/dist/tts/stream_adapter.js +77 -0
- package/dist/tts/stream_adapter.js.map +1 -0
- package/dist/tts/tts.d.ts +37 -0
- package/dist/tts/tts.d.ts.map +1 -0
- package/dist/tts/tts.js +48 -0
- package/dist/tts/tts.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +29 -0
- package/dist/utils.js.map +1 -0
- package/dist/vad.d.ts +28 -0
- package/dist/vad.d.ts.map +1 -0
- package/dist/vad.js +14 -0
- package/dist/vad.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/worker.d.ts +84 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +296 -0
- package/dist/worker.js.map +1 -0
- package/package.json +31 -0
- package/src/cli.ts +79 -0
- package/src/generator.ts +18 -0
- package/src/http_server.ts +47 -0
- package/src/index.ts +18 -0
- package/src/ipc/job_main.ts +83 -0
- package/src/ipc/job_process.ts +96 -0
- package/src/ipc/protocol.ts +51 -0
- package/src/job_context.ts +49 -0
- package/src/job_request.ts +118 -0
- package/src/log.ts +13 -0
- package/src/plugin.ts +28 -0
- package/src/stt/index.ts +6 -0
- package/src/stt/stream_adapter.ts +104 -0
- package/src/stt/stt.ts +58 -0
- package/src/tokenize.ts +22 -0
- package/src/tts/index.ts +23 -0
- package/src/tts/stream_adapter.ts +92 -0
- package/src/tts/tts.ts +78 -0
- package/src/utils.ts +37 -0
- package/src/vad.ts +42 -0
- package/src/version.ts +5 -0
- package/src/worker.ts +384 -0
- package/tsconfig.json +10 -0
package/src/tts/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';
|
|
5
|
+
import {
|
|
6
|
+
ChunkedStream,
|
|
7
|
+
SynthesisEvent,
|
|
8
|
+
SynthesisEventType,
|
|
9
|
+
SynthesizeStream,
|
|
10
|
+
type SynthesizedAudio,
|
|
11
|
+
TTS,
|
|
12
|
+
} from './tts.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
TTS,
|
|
16
|
+
SynthesisEvent,
|
|
17
|
+
SynthesisEventType,
|
|
18
|
+
SynthesizedAudio,
|
|
19
|
+
SynthesizeStream,
|
|
20
|
+
StreamAdapter,
|
|
21
|
+
StreamAdapterWrapper,
|
|
22
|
+
ChunkedStream,
|
|
23
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { SentenceStream, SentenceTokenizer } from '../tokenize.js';
|
|
5
|
+
import { ChunkedStream, SynthesisEvent, SynthesisEventType, SynthesizeStream, TTS } from './tts.js';
|
|
6
|
+
|
|
7
|
+
export class StreamAdapterWrapper extends SynthesizeStream {
|
|
8
|
+
closed: boolean;
|
|
9
|
+
tts: TTS;
|
|
10
|
+
sentenceStream: SentenceStream;
|
|
11
|
+
eventQueue: (SynthesisEvent | undefined)[];
|
|
12
|
+
task: {
|
|
13
|
+
run: Promise<void>;
|
|
14
|
+
cancel: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
constructor(tts: TTS, sentenceStream: SentenceStream) {
|
|
18
|
+
super();
|
|
19
|
+
this.closed = false;
|
|
20
|
+
this.tts = tts;
|
|
21
|
+
this.sentenceStream = sentenceStream;
|
|
22
|
+
this.eventQueue = [];
|
|
23
|
+
this.task = {
|
|
24
|
+
run: new Promise((_, reject) => {
|
|
25
|
+
this.run(reject);
|
|
26
|
+
}),
|
|
27
|
+
cancel: () => {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async run(reject: (arg: Error) => void) {
|
|
32
|
+
while (!this.closed) {
|
|
33
|
+
this.task.cancel = () => {
|
|
34
|
+
this.closed = true;
|
|
35
|
+
reject(new Error('cancelled'));
|
|
36
|
+
};
|
|
37
|
+
for await (const sentence of this.sentenceStream) {
|
|
38
|
+
const audio = await this.tts.synthesize(sentence.text).then((data) => data.next());
|
|
39
|
+
if (!audio.done) {
|
|
40
|
+
this.eventQueue.push(new SynthesisEvent(SynthesisEventType.STARTED));
|
|
41
|
+
this.eventQueue.push(new SynthesisEvent(SynthesisEventType.AUDIO, audio.value));
|
|
42
|
+
this.eventQueue.push(new SynthesisEvent(SynthesisEventType.FINISHED));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pushText(token: string) {
|
|
49
|
+
this.sentenceStream.pushText(token);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async flush() {
|
|
53
|
+
await this.sentenceStream.flush();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
next(): IteratorResult<SynthesisEvent> {
|
|
57
|
+
const event = this.eventQueue.shift();
|
|
58
|
+
if (event) {
|
|
59
|
+
return { done: false, value: event };
|
|
60
|
+
} else {
|
|
61
|
+
return { done: true, value: undefined };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async close(): Promise<void> {
|
|
66
|
+
this.task.cancel();
|
|
67
|
+
try {
|
|
68
|
+
await this.task.run;
|
|
69
|
+
} finally {
|
|
70
|
+
this.eventQueue.push(undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class StreamAdapter extends TTS {
|
|
76
|
+
tts: TTS;
|
|
77
|
+
tokenizer: SentenceTokenizer;
|
|
78
|
+
|
|
79
|
+
constructor(tts: TTS, tokenizer: SentenceTokenizer) {
|
|
80
|
+
super(true);
|
|
81
|
+
this.tts = tts;
|
|
82
|
+
this.tokenizer = tokenizer;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
synthesize(text: string): Promise<ChunkedStream> {
|
|
86
|
+
return this.tts.synthesize(text);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stream() {
|
|
90
|
+
return new StreamAdapterWrapper(this.tts, this.tokenizer.stream(undefined));
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/tts/tts.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { AudioFrame } from '@livekit/rtc-node';
|
|
5
|
+
import { mergeFrames } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
export interface SynthesizedAudio {
|
|
8
|
+
text: string;
|
|
9
|
+
data: AudioFrame;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export enum SynthesisEventType {
|
|
13
|
+
STARTED = 0,
|
|
14
|
+
AUDIO = 1,
|
|
15
|
+
FINISHED = 2,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SynthesisEvent {
|
|
19
|
+
type: SynthesisEventType;
|
|
20
|
+
audio?: SynthesizedAudio;
|
|
21
|
+
|
|
22
|
+
constructor(type: SynthesisEventType, audio: SynthesizedAudio | undefined = undefined) {
|
|
23
|
+
this.type = type;
|
|
24
|
+
this.audio = audio;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export abstract class SynthesizeStream implements IterableIterator<SynthesisEvent> {
|
|
29
|
+
abstract pushText(token?: string): void;
|
|
30
|
+
|
|
31
|
+
markSegmentEnd() {
|
|
32
|
+
this.pushText(undefined);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
abstract close(wait: boolean): Promise<void>;
|
|
36
|
+
abstract next(): IteratorResult<SynthesisEvent>;
|
|
37
|
+
|
|
38
|
+
[Symbol.iterator](): SynthesizeStream {
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export abstract class TTS {
|
|
44
|
+
#streamingSupported: boolean;
|
|
45
|
+
|
|
46
|
+
constructor(streamingSupported: boolean) {
|
|
47
|
+
this.#streamingSupported = streamingSupported;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
abstract synthesize(text: string): Promise<ChunkedStream>;
|
|
51
|
+
|
|
52
|
+
abstract stream(): SynthesizeStream;
|
|
53
|
+
|
|
54
|
+
get streamingSupported(): boolean {
|
|
55
|
+
return this.#streamingSupported;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export abstract class ChunkedStream implements AsyncIterableIterator<SynthesizedAudio> {
|
|
60
|
+
async collect(): Promise<AudioFrame> {
|
|
61
|
+
const frames = [];
|
|
62
|
+
for await (const ev of this) {
|
|
63
|
+
frames.push(ev.data);
|
|
64
|
+
}
|
|
65
|
+
return mergeFrames(frames);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
abstract close(): Promise<void>;
|
|
69
|
+
abstract next(): Promise<IteratorResult<SynthesizedAudio>>;
|
|
70
|
+
|
|
71
|
+
[Symbol.iterator](): ChunkedStream {
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[Symbol.asyncIterator](): ChunkedStream {
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { AudioFrame } from '@livekit/rtc-node';
|
|
5
|
+
|
|
6
|
+
export type AudioBuffer = AudioFrame[] | AudioFrame;
|
|
7
|
+
|
|
8
|
+
export const mergeFrames = (buffer: AudioBuffer): AudioFrame => {
|
|
9
|
+
if (Array.isArray(buffer)) {
|
|
10
|
+
buffer = buffer as AudioFrame[];
|
|
11
|
+
if (buffer.length == 0) {
|
|
12
|
+
throw new TypeError('buffer is empty');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sampleRate = buffer[0].sampleRate;
|
|
16
|
+
const channels = buffer[0].channels;
|
|
17
|
+
let samplesPerChannel = 0;
|
|
18
|
+
let data = new Uint16Array();
|
|
19
|
+
|
|
20
|
+
for (const frame of buffer) {
|
|
21
|
+
if (frame.sampleRate !== sampleRate) {
|
|
22
|
+
throw new TypeError('sample rate mismatch');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (frame.channels !== channels) {
|
|
26
|
+
throw new TypeError('channel count mismatch');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
data = new Uint16Array([...data, ...frame.data]);
|
|
30
|
+
samplesPerChannel += frame.samplesPerChannel;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new AudioFrame(data, sampleRate, channels, samplesPerChannel);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return buffer;
|
|
37
|
+
};
|
package/src/vad.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { AudioFrame } from '@livekit/rtc-node';
|
|
5
|
+
|
|
6
|
+
export enum VADEventType {
|
|
7
|
+
START_OF_SPEECH = 1,
|
|
8
|
+
SPEAKING = 2,
|
|
9
|
+
END_OF_SPEECH = 3,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface VADEvent {
|
|
13
|
+
type: VADEventType;
|
|
14
|
+
samplesIndex: number;
|
|
15
|
+
duration: number;
|
|
16
|
+
speech: AudioFrame[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export abstract class VAD {
|
|
20
|
+
abstract stream({
|
|
21
|
+
minSpeakingDuration,
|
|
22
|
+
minSilenceDuration,
|
|
23
|
+
paddingDuration,
|
|
24
|
+
sampleRate,
|
|
25
|
+
maxBufferedSpeech,
|
|
26
|
+
}: {
|
|
27
|
+
minSpeakingDuration: number;
|
|
28
|
+
minSilenceDuration: number;
|
|
29
|
+
paddingDuration: number;
|
|
30
|
+
sampleRate: number;
|
|
31
|
+
maxBufferedSpeech: number;
|
|
32
|
+
}): VADStream;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export abstract class VADStream implements IterableIterator<VADEvent> {
|
|
36
|
+
abstract pushFrame(frame: AudioFrame): void;
|
|
37
|
+
abstract close(wait: boolean): Promise<void>;
|
|
38
|
+
abstract next(): IteratorResult<VADEvent>;
|
|
39
|
+
[Symbol.iterator](): VADStream {
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/version.ts
ADDED
package/src/worker.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import {
|
|
5
|
+
type AvailabilityRequest,
|
|
6
|
+
type Job,
|
|
7
|
+
type JobAssignment,
|
|
8
|
+
JobType,
|
|
9
|
+
ParticipantPermission,
|
|
10
|
+
ServerMessage,
|
|
11
|
+
WorkerMessage,
|
|
12
|
+
} from '@livekit/protocol';
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import { WebSocket } from 'ws';
|
|
17
|
+
import { HTTPServer } from './http_server.js';
|
|
18
|
+
import { JobProcess } from './ipc/job_process.js';
|
|
19
|
+
import { type AvailRes, JobRequest } from './job_request.js';
|
|
20
|
+
import type { AcceptData } from './job_request.js';
|
|
21
|
+
import { log } from './log.js';
|
|
22
|
+
import { version } from './version.js';
|
|
23
|
+
|
|
24
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
25
|
+
const ASSIGNMENT_TIMEOUT = 15 * 1000;
|
|
26
|
+
const LOAD_INTERVAL = 5 * 1000;
|
|
27
|
+
|
|
28
|
+
const cpuLoad = (): number =>
|
|
29
|
+
(os
|
|
30
|
+
.cpus()
|
|
31
|
+
.reduce(
|
|
32
|
+
(acc, x) => acc + x.times.idle / Object.values(x.times).reduce((acc, x) => acc + x, 0),
|
|
33
|
+
0,
|
|
34
|
+
) /
|
|
35
|
+
os.cpus().length) *
|
|
36
|
+
100;
|
|
37
|
+
|
|
38
|
+
class WorkerPermissions {
|
|
39
|
+
canPublish: boolean;
|
|
40
|
+
canSubscribe: boolean;
|
|
41
|
+
canPublishData: boolean;
|
|
42
|
+
canUpdateMetadata: boolean;
|
|
43
|
+
hidden: boolean;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
canPublish = true,
|
|
47
|
+
canSubscribe = true,
|
|
48
|
+
canPublishData = true,
|
|
49
|
+
canUpdateMetadata = true,
|
|
50
|
+
hidden = false,
|
|
51
|
+
) {
|
|
52
|
+
this.canPublish = canPublish;
|
|
53
|
+
this.canSubscribe = canSubscribe;
|
|
54
|
+
this.canPublishData = canPublishData;
|
|
55
|
+
this.canUpdateMetadata = canUpdateMetadata;
|
|
56
|
+
this.hidden = hidden;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class WorkerOptions {
|
|
61
|
+
requestFunc: (arg: JobRequest) => Promise<void>;
|
|
62
|
+
cpuLoadFunc: () => number;
|
|
63
|
+
namespace: string;
|
|
64
|
+
permissions: WorkerPermissions;
|
|
65
|
+
workerType: JobType;
|
|
66
|
+
maxRetry: number;
|
|
67
|
+
wsURL: string;
|
|
68
|
+
apiKey?: string;
|
|
69
|
+
apiSecret?: string;
|
|
70
|
+
host: string;
|
|
71
|
+
port: number;
|
|
72
|
+
|
|
73
|
+
constructor({
|
|
74
|
+
requestFunc,
|
|
75
|
+
cpuLoadFunc = cpuLoad,
|
|
76
|
+
namespace = 'default',
|
|
77
|
+
permissions = new WorkerPermissions(),
|
|
78
|
+
workerType = JobType.JT_PUBLISHER,
|
|
79
|
+
maxRetry = MAX_RECONNECT_ATTEMPTS,
|
|
80
|
+
wsURL = 'ws://localhost:7880',
|
|
81
|
+
apiKey = undefined,
|
|
82
|
+
apiSecret = undefined,
|
|
83
|
+
host = 'localhost',
|
|
84
|
+
port = 8081,
|
|
85
|
+
}: {
|
|
86
|
+
requestFunc: (arg: JobRequest) => Promise<void>;
|
|
87
|
+
cpuLoadFunc?: () => number;
|
|
88
|
+
namespace?: string;
|
|
89
|
+
permissions?: WorkerPermissions;
|
|
90
|
+
workerType?: JobType;
|
|
91
|
+
maxRetry?: number;
|
|
92
|
+
wsURL?: string;
|
|
93
|
+
apiKey?: string;
|
|
94
|
+
apiSecret?: string;
|
|
95
|
+
host?: string;
|
|
96
|
+
port?: number;
|
|
97
|
+
}) {
|
|
98
|
+
this.requestFunc = requestFunc;
|
|
99
|
+
this.cpuLoadFunc = cpuLoadFunc;
|
|
100
|
+
this.namespace = namespace;
|
|
101
|
+
this.permissions = permissions;
|
|
102
|
+
this.workerType = workerType;
|
|
103
|
+
this.maxRetry = maxRetry;
|
|
104
|
+
this.wsURL = wsURL;
|
|
105
|
+
this.apiKey = apiKey;
|
|
106
|
+
this.apiSecret = apiSecret;
|
|
107
|
+
this.host = host;
|
|
108
|
+
this.port = port;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class ActiveJob {
|
|
113
|
+
job: Job;
|
|
114
|
+
acceptData: AcceptData;
|
|
115
|
+
|
|
116
|
+
constructor(job: Job, acceptData: AcceptData) {
|
|
117
|
+
this.job = job;
|
|
118
|
+
this.acceptData = acceptData;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type AssignmentPair = {
|
|
123
|
+
// this string is the JSON string version of the JobAssignment.
|
|
124
|
+
// we keep it around to unpack it again in the child, because we can't pass Job directly.
|
|
125
|
+
raw: string;
|
|
126
|
+
asgn: JobAssignment;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
class PendingAssignment {
|
|
130
|
+
promise = new Promise<AssignmentPair>((resolve) => {
|
|
131
|
+
this.resolve = resolve; // oh, JavaScript.
|
|
132
|
+
});
|
|
133
|
+
resolve(arg: AssignmentPair) {
|
|
134
|
+
arg;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class Worker {
|
|
139
|
+
opts: WorkerOptions;
|
|
140
|
+
#id = 'unregistered';
|
|
141
|
+
session: WebSocket | undefined = undefined;
|
|
142
|
+
closed = false;
|
|
143
|
+
httpServer: HTTPServer;
|
|
144
|
+
logger = log.child({ version });
|
|
145
|
+
event = new EventEmitter();
|
|
146
|
+
pending: { [id: string]: { value: PendingAssignment } } = {};
|
|
147
|
+
processes: { [id: string]: { proc: JobProcess; activeJob: ActiveJob } } = {};
|
|
148
|
+
|
|
149
|
+
constructor(opts: WorkerOptions) {
|
|
150
|
+
opts.wsURL = opts.wsURL || process.env.LIVEKIT_URL || '';
|
|
151
|
+
opts.apiKey = opts.apiKey || process.env.LIVEKIT_API_KEY || '';
|
|
152
|
+
opts.apiSecret = opts.apiSecret || process.env.LIVEKIT_API_SECRET || '';
|
|
153
|
+
|
|
154
|
+
this.opts = opts;
|
|
155
|
+
this.httpServer = new HTTPServer(opts.host, opts.port);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get id(): string {
|
|
159
|
+
return this.#id;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async run() {
|
|
163
|
+
this.logger.info('starting worker');
|
|
164
|
+
|
|
165
|
+
if (this.opts.wsURL === '') throw new Error('--url is required, or set LIVEKIT_URL env var');
|
|
166
|
+
if (this.opts.apiKey === '')
|
|
167
|
+
throw new Error('--api-key is required, or set LIVEKIT_API_KEY env var');
|
|
168
|
+
if (this.opts.apiSecret === '')
|
|
169
|
+
throw new Error('--api-secret is required, or set LIVEKIT_API_SECRET env var');
|
|
170
|
+
|
|
171
|
+
const workerWS = async () => {
|
|
172
|
+
let retries = 0;
|
|
173
|
+
while (!this.closed) {
|
|
174
|
+
const token = new AccessToken(this.opts.apiKey, this.opts.apiSecret);
|
|
175
|
+
token.addGrant({ agent: true });
|
|
176
|
+
const jwt = await token.toJwt();
|
|
177
|
+
|
|
178
|
+
const url = new URL(this.opts.wsURL);
|
|
179
|
+
url.protocol = url.protocol.replace('http', 'ws');
|
|
180
|
+
this.session = new WebSocket(url + 'agent', {
|
|
181
|
+
headers: { authorization: 'Bearer ' + jwt },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
this.session!.on('open', resolve);
|
|
187
|
+
this.session!.on('error', (error) => reject(error));
|
|
188
|
+
this.session!.on('close', (code) => reject(`WebSocket returned ${code}`));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
this.runWS(this.session!);
|
|
192
|
+
return;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (this.closed) return;
|
|
195
|
+
if (retries >= this.opts.maxRetry) {
|
|
196
|
+
throw new Error(`failed to connect to LiveKit server after ${retries} attempts: ${e}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
retries++;
|
|
200
|
+
const delay = Math.min(retries * 2, 10);
|
|
201
|
+
|
|
202
|
+
this.logger.warn(
|
|
203
|
+
`failed to connect to LiveKit server, retrying in ${delay} seconds: ${e} (${retries}/${this.opts.maxRetry})`,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
await Promise.all([workerWS(), this.httpServer.run()]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
startProcess(job: Job, acceptData: AcceptData, raw: string) {
|
|
215
|
+
const proc = new JobProcess(job, acceptData, raw, this.opts.wsURL);
|
|
216
|
+
this.processes[job.id] = { proc, activeJob: new ActiveJob(job, acceptData) };
|
|
217
|
+
proc
|
|
218
|
+
.run()
|
|
219
|
+
.catch((e) => {
|
|
220
|
+
proc.logger.error(`error running job process ${proc.job.id}: ${e}`);
|
|
221
|
+
})
|
|
222
|
+
.finally(() => {
|
|
223
|
+
proc.clear();
|
|
224
|
+
delete this.processes[job.id];
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
runWS(ws: WebSocket) {
|
|
229
|
+
let closingWS = false;
|
|
230
|
+
|
|
231
|
+
const send = (msg: WorkerMessage) => {
|
|
232
|
+
if (closingWS) {
|
|
233
|
+
this.event.off('worker_msg', send);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
ws.send(msg.toBinary());
|
|
237
|
+
};
|
|
238
|
+
this.event.on('worker_msg', send);
|
|
239
|
+
|
|
240
|
+
ws.addEventListener('close', () => {
|
|
241
|
+
closingWS = true;
|
|
242
|
+
this.logger.error('worker connection closed unexpectedly');
|
|
243
|
+
this.close();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
ws.addEventListener('message', (event) => {
|
|
247
|
+
if (event.type !== 'message') {
|
|
248
|
+
this.logger.warn('unexpected message type: ' + event.type);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const msg = new ServerMessage();
|
|
253
|
+
msg.fromBinary(event.data as Uint8Array);
|
|
254
|
+
switch (msg.message.case) {
|
|
255
|
+
case 'register': {
|
|
256
|
+
this.#id = msg.message.value.workerId;
|
|
257
|
+
log
|
|
258
|
+
.child({ id: this.id, server_info: msg.message.value.serverInfo })
|
|
259
|
+
.info('registered worker');
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case 'availability': {
|
|
263
|
+
this.availability(msg.message.value);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'assignment': {
|
|
267
|
+
const job = msg.message.value.job!;
|
|
268
|
+
if (job.id in this.pending) {
|
|
269
|
+
const task = this.pending[job.id];
|
|
270
|
+
delete this.pending[job.id];
|
|
271
|
+
task.value.resolve({
|
|
272
|
+
asgn: msg.message.value,
|
|
273
|
+
raw: msg.toJsonString(),
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
log.child({ job }).warn('received assignment for unknown job ' + job.id);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
this.event.emit(
|
|
284
|
+
'worker_msg',
|
|
285
|
+
new WorkerMessage({
|
|
286
|
+
message: {
|
|
287
|
+
case: 'register',
|
|
288
|
+
value: {
|
|
289
|
+
type: this.opts.workerType,
|
|
290
|
+
namespace: this.opts.namespace,
|
|
291
|
+
allowedPermissions: new ParticipantPermission({
|
|
292
|
+
canPublish: this.opts.permissions.canPublish,
|
|
293
|
+
canSubscribe: this.opts.permissions.canSubscribe,
|
|
294
|
+
canPublishData: this.opts.permissions.canPublishData,
|
|
295
|
+
hidden: this.opts.permissions.hidden,
|
|
296
|
+
agent: true,
|
|
297
|
+
}),
|
|
298
|
+
version,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const loadMonitor = setInterval(() => {
|
|
305
|
+
if (closingWS) clearInterval(loadMonitor);
|
|
306
|
+
this.event.emit(
|
|
307
|
+
'worker_msg',
|
|
308
|
+
new WorkerMessage({
|
|
309
|
+
message: {
|
|
310
|
+
case: 'updateWorker',
|
|
311
|
+
value: {
|
|
312
|
+
load: cpuLoad(),
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
}, LOAD_INTERVAL);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async availability(msg: AvailabilityRequest) {
|
|
321
|
+
const tx = new EventEmitter();
|
|
322
|
+
const req = new JobRequest(msg.job!, tx);
|
|
323
|
+
|
|
324
|
+
tx.on('recv', async (av: AvailRes) => {
|
|
325
|
+
const msg = new WorkerMessage({
|
|
326
|
+
message: {
|
|
327
|
+
case: 'availability',
|
|
328
|
+
value: {
|
|
329
|
+
available: av.avail,
|
|
330
|
+
jobId: req.id,
|
|
331
|
+
participantIdentity: av.data?.identity,
|
|
332
|
+
participantName: av.data?.name,
|
|
333
|
+
participantMetadata: av.data?.metadata,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
this.pending[req.id] = { value: new PendingAssignment() };
|
|
339
|
+
this.event.emit('worker_msg', msg);
|
|
340
|
+
if (!av.avail) return;
|
|
341
|
+
|
|
342
|
+
const timer = setTimeout(() => {
|
|
343
|
+
log.child({ req }).warn(`assignment for job ${req.id} timed out`);
|
|
344
|
+
return;
|
|
345
|
+
}, ASSIGNMENT_TIMEOUT);
|
|
346
|
+
this.pending[req.id].value.promise.then(({ asgn, raw }) => {
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
this.startProcess(asgn!.job!, av.data!, raw);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
this.opts.requestFunc(req);
|
|
354
|
+
} catch (e) {
|
|
355
|
+
log.child({ req }).error(`user request handler for job ${req.id} failed`);
|
|
356
|
+
} finally {
|
|
357
|
+
if (!req.answered) {
|
|
358
|
+
log.child({ req }).error(`no answer for job ${req.id}, automatically rejecting the job`);
|
|
359
|
+
this.event.emit(
|
|
360
|
+
'worker_msg',
|
|
361
|
+
new WorkerMessage({
|
|
362
|
+
message: {
|
|
363
|
+
case: 'availability',
|
|
364
|
+
value: {
|
|
365
|
+
available: false,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async close() {
|
|
375
|
+
if (this.closed) return;
|
|
376
|
+
this.closed = true;
|
|
377
|
+
this.logger.debug('shutting down worker');
|
|
378
|
+
await this.httpServer.close();
|
|
379
|
+
for await (const value of Object.values(this.processes)) {
|
|
380
|
+
await value.proc.close();
|
|
381
|
+
}
|
|
382
|
+
this.session?.close();
|
|
383
|
+
}
|
|
384
|
+
}
|
package/tsconfig.json
ADDED