@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { type IncomingMessage, type Server, type ServerResponse, createServer } from 'http';
|
|
5
|
+
|
|
6
|
+
const healthCheck = async (res: ServerResponse) => {
|
|
7
|
+
res.writeHead(200);
|
|
8
|
+
res.end('OK');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class HTTPServer {
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
app: Server;
|
|
15
|
+
|
|
16
|
+
constructor(host: string, port: number) {
|
|
17
|
+
this.host = host;
|
|
18
|
+
this.port = port;
|
|
19
|
+
|
|
20
|
+
this.app = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
21
|
+
if (req.url === '/') {
|
|
22
|
+
healthCheck(res);
|
|
23
|
+
} else {
|
|
24
|
+
res.writeHead(404);
|
|
25
|
+
res.end('not found');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async run(): Promise<void> {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this.app.listen(this.port, this.host, (err?: Error) => {
|
|
33
|
+
if (err) reject(err);
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async close(): Promise<void> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.app.close((err?: Error) => {
|
|
42
|
+
if (err) reject(err);
|
|
43
|
+
resolve();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import * as cli from './cli.js';
|
|
5
|
+
import * as stt from './stt/index.js';
|
|
6
|
+
import * as tts from './tts/index.js';
|
|
7
|
+
|
|
8
|
+
export * from './vad.js';
|
|
9
|
+
export * from './plugin.js';
|
|
10
|
+
export * from './version.js';
|
|
11
|
+
export * from './job_context.js';
|
|
12
|
+
export * from './job_request.js';
|
|
13
|
+
export * from './worker.js';
|
|
14
|
+
export * from './utils.js';
|
|
15
|
+
export * from './log.js';
|
|
16
|
+
export * from './generator.js';
|
|
17
|
+
|
|
18
|
+
export { cli, stt, tts };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { type JobAssignment, ServerMessage } from '@livekit/protocol';
|
|
5
|
+
import { Room } from '@livekit/rtc-node';
|
|
6
|
+
import { type ChildProcess, fork } from 'child_process';
|
|
7
|
+
import { EventEmitter, once } from 'events';
|
|
8
|
+
import { JobContext } from '../job_context.js';
|
|
9
|
+
import { log } from '../log.js';
|
|
10
|
+
import { IPC_MESSAGE, type JobMainArgs, type Message, type Ping } from './protocol.js';
|
|
11
|
+
|
|
12
|
+
export const runJob = (args: JobMainArgs): ChildProcess => {
|
|
13
|
+
return fork(import.meta.filename, [args.raw, args.entry, args.fallbackURL]);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (process.send) {
|
|
17
|
+
// process.argv:
|
|
18
|
+
// [0] `node'
|
|
19
|
+
// [1] import.meta.filename
|
|
20
|
+
// [2] proto.JobAssignment, serialized to JSON string
|
|
21
|
+
// [3] import.meta.filename of function containing entry file
|
|
22
|
+
// [4] fallback URL in case JobAssignment.url is empty
|
|
23
|
+
|
|
24
|
+
const msg = new ServerMessage();
|
|
25
|
+
msg.fromJsonString(process.argv[2]);
|
|
26
|
+
const args = msg.message.value as JobAssignment;
|
|
27
|
+
|
|
28
|
+
const room = new Room();
|
|
29
|
+
const closeEvent = new EventEmitter();
|
|
30
|
+
let shuttingDown = false;
|
|
31
|
+
let closed = false;
|
|
32
|
+
|
|
33
|
+
process.on('message', (msg: Message) => {
|
|
34
|
+
if (msg.type === IPC_MESSAGE.ShutdownRequest) {
|
|
35
|
+
shuttingDown = true;
|
|
36
|
+
closed = true;
|
|
37
|
+
closeEvent.emit('close');
|
|
38
|
+
} else if (msg.type === IPC_MESSAGE.Ping) {
|
|
39
|
+
process.send!({
|
|
40
|
+
type: IPC_MESSAGE.Pong,
|
|
41
|
+
lastTimestamp: (msg as Ping).timestamp,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// don't do anything on C-c
|
|
48
|
+
process.on('SIGINT', () => {});
|
|
49
|
+
|
|
50
|
+
const conn = room.connect(args.url || process.argv[4], args.token);
|
|
51
|
+
|
|
52
|
+
const start = () => {
|
|
53
|
+
if (room.isConnected && !closed) {
|
|
54
|
+
process.send!({ type: IPC_MESSAGE.StartJobResponse });
|
|
55
|
+
|
|
56
|
+
// here we import the file containing the exported entry function, and call it.
|
|
57
|
+
// the file must export default an Agent, usually using defineAgent().
|
|
58
|
+
import(process.argv[3]).then((agent) => {
|
|
59
|
+
agent.default.entry(new JobContext(closeEvent, args.job!, room));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
new Promise(() => {
|
|
65
|
+
conn
|
|
66
|
+
.then(() => {
|
|
67
|
+
if (!closed) start();
|
|
68
|
+
})
|
|
69
|
+
.catch((err) => {
|
|
70
|
+
if (!closed) process.send!({ type: IPC_MESSAGE.StartJobResponse, err });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await once(closeEvent, 'close');
|
|
75
|
+
log.debug('disconnecting from room');
|
|
76
|
+
await room.disconnect();
|
|
77
|
+
if (shuttingDown) {
|
|
78
|
+
process.send({ type: IPC_MESSAGE.ShutdownResponse });
|
|
79
|
+
} else {
|
|
80
|
+
process.send({ type: IPC_MESSAGE.UserExit });
|
|
81
|
+
}
|
|
82
|
+
process.exit();
|
|
83
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { Job } from '@livekit/protocol';
|
|
5
|
+
import type { ChildProcess } from 'child_process';
|
|
6
|
+
import { once } from 'events';
|
|
7
|
+
import type { Logger } from 'pino';
|
|
8
|
+
import type { AcceptData } from '../job_request.js';
|
|
9
|
+
import { log } from '../log.js';
|
|
10
|
+
import {
|
|
11
|
+
IPC_MESSAGE,
|
|
12
|
+
type JobMainArgs,
|
|
13
|
+
type Message,
|
|
14
|
+
type Pong,
|
|
15
|
+
type StartJobResponse,
|
|
16
|
+
} from './protocol.js';
|
|
17
|
+
|
|
18
|
+
const START_TIMEOUT = 90 * 1000;
|
|
19
|
+
const PING_INTERVAL = 5 * 1000;
|
|
20
|
+
const PING_TIMEOUT = 90 * 1000;
|
|
21
|
+
const HIGH_PING_THRESHOLD = 10;
|
|
22
|
+
|
|
23
|
+
export class JobProcess {
|
|
24
|
+
#job: Job;
|
|
25
|
+
args: JobMainArgs;
|
|
26
|
+
logger: Logger;
|
|
27
|
+
process?: ChildProcess;
|
|
28
|
+
startTimeout?: ReturnType<typeof setTimeout>;
|
|
29
|
+
pongTimeout?: ReturnType<typeof setTimeout>;
|
|
30
|
+
pingInterval?: ReturnType<typeof setInterval>;
|
|
31
|
+
|
|
32
|
+
constructor(job: Job, acceptData: AcceptData, raw: string, fallbackURL: string) {
|
|
33
|
+
this.#job = job;
|
|
34
|
+
this.args = { entry: acceptData.entry, raw, fallbackURL };
|
|
35
|
+
this.logger = log.child({ job_id: this.#job.id });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get job(): Job {
|
|
39
|
+
return this.#job;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async close() {
|
|
43
|
+
this.logger.debug('closing job process');
|
|
44
|
+
await this.clear();
|
|
45
|
+
this.process!.send({ type: IPC_MESSAGE.ShutdownRequest });
|
|
46
|
+
await once(this.process!, 'disconnect');
|
|
47
|
+
this.logger.info('job process closed');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async clear() {
|
|
51
|
+
clearTimeout(this.startTimeout);
|
|
52
|
+
clearTimeout(this.pongTimeout);
|
|
53
|
+
clearInterval(this.pingInterval);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async run() {
|
|
57
|
+
let resp: StartJobResponse | undefined = undefined;
|
|
58
|
+
|
|
59
|
+
this.startTimeout = setTimeout(() => {
|
|
60
|
+
if (resp === undefined) {
|
|
61
|
+
this.logger.error('process start timed out, killing job');
|
|
62
|
+
this.process?.kill();
|
|
63
|
+
this.clear();
|
|
64
|
+
}
|
|
65
|
+
}, START_TIMEOUT);
|
|
66
|
+
|
|
67
|
+
this.pingInterval = setInterval(() => {
|
|
68
|
+
this.process?.send({ type: IPC_MESSAGE.Ping, timestamp: Date.now() });
|
|
69
|
+
}, PING_INTERVAL);
|
|
70
|
+
|
|
71
|
+
this.pongTimeout = setTimeout(() => {
|
|
72
|
+
this.logger.error('job ping timed out, killing job');
|
|
73
|
+
this.process?.kill();
|
|
74
|
+
this.clear();
|
|
75
|
+
}, PING_TIMEOUT);
|
|
76
|
+
|
|
77
|
+
this.process = await import('./job_main.js').then((main) => main.runJob(this.args));
|
|
78
|
+
|
|
79
|
+
this.process.on('message', (msg: Message) => {
|
|
80
|
+
if (msg.type === IPC_MESSAGE.StartJobResponse) {
|
|
81
|
+
resp = msg as StartJobResponse;
|
|
82
|
+
} else if (msg.type === IPC_MESSAGE.Pong) {
|
|
83
|
+
const delay = Date.now() - (msg as Pong).timestamp;
|
|
84
|
+
if (delay > HIGH_PING_THRESHOLD) {
|
|
85
|
+
this.logger.warn(`job is unresponsive (${delay}ms delay)`);
|
|
86
|
+
}
|
|
87
|
+
this.pongTimeout?.refresh();
|
|
88
|
+
} else if (msg.type === IPC_MESSAGE.UserExit || msg.type === IPC_MESSAGE.ShutdownResponse) {
|
|
89
|
+
this.logger.debug('job exiting');
|
|
90
|
+
this.clear();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await once(this.process, 'disconnect');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
export type JobMainArgs = {
|
|
6
|
+
entry: string;
|
|
7
|
+
raw: string;
|
|
8
|
+
fallbackURL: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface Message {
|
|
12
|
+
type: IPC_MESSAGE;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StartJobResponse extends Message {
|
|
16
|
+
type: IPC_MESSAGE.StartJobResponse;
|
|
17
|
+
err?: Error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Ping extends Message {
|
|
21
|
+
type: IPC_MESSAGE.Ping;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Pong extends Message {
|
|
26
|
+
type: IPC_MESSAGE.Pong;
|
|
27
|
+
lastTimestamp: number;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ShutdownRequest extends Message {
|
|
32
|
+
type: IPC_MESSAGE.ShutdownRequest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ShutdownResponse extends Message {
|
|
36
|
+
type: IPC_MESSAGE.ShutdownResponse;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UserExit extends Message {
|
|
40
|
+
type: IPC_MESSAGE.UserExit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum IPC_MESSAGE {
|
|
44
|
+
StartJobRequest,
|
|
45
|
+
StartJobResponse,
|
|
46
|
+
Ping,
|
|
47
|
+
Pong,
|
|
48
|
+
ShutdownRequest,
|
|
49
|
+
ShutdownResponse,
|
|
50
|
+
UserExit,
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { Job } from '@livekit/protocol';
|
|
5
|
+
import type { LocalParticipant, RemoteParticipant, Room } from '@livekit/rtc-node';
|
|
6
|
+
import type { EventEmitter } from 'events';
|
|
7
|
+
|
|
8
|
+
export class JobContext {
|
|
9
|
+
#job: Job;
|
|
10
|
+
#room: Room;
|
|
11
|
+
#publisher?: RemoteParticipant;
|
|
12
|
+
tx: EventEmitter;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
tx: EventEmitter,
|
|
16
|
+
job: Job,
|
|
17
|
+
room: Room,
|
|
18
|
+
publisher: RemoteParticipant | undefined = undefined,
|
|
19
|
+
) {
|
|
20
|
+
this.#job = job;
|
|
21
|
+
this.#room = room;
|
|
22
|
+
this.#publisher = publisher;
|
|
23
|
+
this.tx = tx;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get id(): string {
|
|
27
|
+
return this.#job.id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get job(): Job {
|
|
31
|
+
return this.#job;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get room(): Room {
|
|
35
|
+
return this.#room;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get publisher(): RemoteParticipant | undefined {
|
|
39
|
+
return this.#publisher;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get agent(): LocalParticipant | undefined {
|
|
43
|
+
return this.#room.localParticipant;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async shutdown() {
|
|
47
|
+
this.tx.emit('close');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { Job, ParticipantInfo, Room } from '@livekit/protocol';
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
import { log } from './log.js';
|
|
7
|
+
|
|
8
|
+
class AnsweredError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'AnsweredError';
|
|
12
|
+
this.message = 'request already answered';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum AutoDisconnect {
|
|
17
|
+
ROOM_EMPTY,
|
|
18
|
+
PUBLISHER_LEFT,
|
|
19
|
+
NONE,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export enum AutoSubscribe {
|
|
23
|
+
SUBSCRIBE_ALL,
|
|
24
|
+
SUBSCRIBE_NONE,
|
|
25
|
+
VIDEO_ONLY,
|
|
26
|
+
AUDIO_ONLY,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AcceptData = {
|
|
30
|
+
entry: string; // filename
|
|
31
|
+
autoSubscribe: AutoSubscribe;
|
|
32
|
+
autoDisconnect: AutoDisconnect;
|
|
33
|
+
name: string;
|
|
34
|
+
identity: string;
|
|
35
|
+
metadata: string;
|
|
36
|
+
assign: EventEmitter;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AvailRes = {
|
|
40
|
+
avail: boolean;
|
|
41
|
+
data?: AcceptData;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export class JobRequest {
|
|
45
|
+
#job: Job;
|
|
46
|
+
#answered = false;
|
|
47
|
+
tx: EventEmitter;
|
|
48
|
+
logger = log.child({ job: this.job });
|
|
49
|
+
|
|
50
|
+
constructor(job: Job, tx: EventEmitter) {
|
|
51
|
+
this.#job = job;
|
|
52
|
+
this.tx = tx;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get id(): string {
|
|
56
|
+
return this.#job.id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get job(): Job {
|
|
60
|
+
return this.#job;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get room(): Room | undefined {
|
|
64
|
+
return this.#job.room;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get publisher(): ParticipantInfo | undefined {
|
|
68
|
+
return this.#job.participant;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get answered(): boolean {
|
|
72
|
+
return this.#answered;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async reject() {
|
|
76
|
+
if (this.#answered) {
|
|
77
|
+
throw new AnsweredError();
|
|
78
|
+
}
|
|
79
|
+
this.#answered = true;
|
|
80
|
+
this.tx.emit('recv', { avail: false, data: undefined } as AvailRes);
|
|
81
|
+
this.logger.info('rejected job', this.id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async accept(
|
|
85
|
+
entry: string,
|
|
86
|
+
autoSubscribe: AutoSubscribe = AutoSubscribe.SUBSCRIBE_ALL,
|
|
87
|
+
autoDisconnect: AutoDisconnect = AutoDisconnect.ROOM_EMPTY,
|
|
88
|
+
name: string = '',
|
|
89
|
+
identity: string = '',
|
|
90
|
+
metadata: string = '',
|
|
91
|
+
) {
|
|
92
|
+
if (this.#answered) {
|
|
93
|
+
throw new AnsweredError();
|
|
94
|
+
}
|
|
95
|
+
this.#answered = true;
|
|
96
|
+
|
|
97
|
+
const assign = new EventEmitter();
|
|
98
|
+
assign.on('error', (e) => {
|
|
99
|
+
throw e;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (identity === '') identity = 'agent-' + this.id;
|
|
103
|
+
|
|
104
|
+
const data: AcceptData = {
|
|
105
|
+
entry,
|
|
106
|
+
autoSubscribe,
|
|
107
|
+
autoDisconnect,
|
|
108
|
+
name,
|
|
109
|
+
identity,
|
|
110
|
+
metadata,
|
|
111
|
+
assign,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.tx.emit('recv', { avail: true, data } as AvailRes);
|
|
115
|
+
|
|
116
|
+
this.logger.info('accepted job', this.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/log.ts
ADDED
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
export abstract class Plugin {
|
|
6
|
+
registeredPlugins: Plugin[] = [];
|
|
7
|
+
#title: string;
|
|
8
|
+
#version: string;
|
|
9
|
+
|
|
10
|
+
constructor(title: string, version: string) {
|
|
11
|
+
this.#title = title;
|
|
12
|
+
this.#version = version;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static registerPlugins(plugin: Plugin) {
|
|
16
|
+
plugin.registeredPlugins.push(plugin);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
abstract downloadFiles(): void;
|
|
20
|
+
|
|
21
|
+
get title(): string {
|
|
22
|
+
return this.#title;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get version(): string {
|
|
26
|
+
return this.#version;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/stt/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { AudioFrame } from '@livekit/rtc-node';
|
|
5
|
+
import { type AudioBuffer, mergeFrames } from '../utils.js';
|
|
6
|
+
import { VADEventType, type VADStream } from '../vad.js';
|
|
7
|
+
import { STT, SpeechEvent, SpeechEventType, SpeechStream } from './stt.js';
|
|
8
|
+
|
|
9
|
+
export class StreamAdapterWrapper extends SpeechStream {
|
|
10
|
+
closed: boolean;
|
|
11
|
+
stt: STT;
|
|
12
|
+
vadStream: VADStream;
|
|
13
|
+
eventQueue: (SpeechEvent | undefined)[];
|
|
14
|
+
language?: string;
|
|
15
|
+
task: {
|
|
16
|
+
run: Promise<void>;
|
|
17
|
+
cancel: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
constructor(stt: STT, vadStream: VADStream, language: string | undefined = undefined) {
|
|
21
|
+
super();
|
|
22
|
+
this.closed = false;
|
|
23
|
+
this.stt = stt;
|
|
24
|
+
this.vadStream = vadStream;
|
|
25
|
+
this.eventQueue = [];
|
|
26
|
+
this.language = language;
|
|
27
|
+
this.task = {
|
|
28
|
+
run: new Promise((_, reject) => {
|
|
29
|
+
this.run(reject);
|
|
30
|
+
}),
|
|
31
|
+
cancel: () => {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async run(reject: (arg: Error) => void) {
|
|
36
|
+
this.task.cancel = () => {
|
|
37
|
+
this.closed = true;
|
|
38
|
+
reject(new Error('cancelled'));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const event of this.vadStream) {
|
|
42
|
+
if (event.type == VADEventType.START_OF_SPEECH) {
|
|
43
|
+
const startEvent = new SpeechEvent(SpeechEventType.START_OF_SPEECH);
|
|
44
|
+
this.eventQueue.push(startEvent);
|
|
45
|
+
} else if (event.type == VADEventType.END_OF_SPEECH) {
|
|
46
|
+
const mergedFrames = mergeFrames(event.speech);
|
|
47
|
+
const endEvent = await this.stt.recognize(mergedFrames, this.language);
|
|
48
|
+
this.eventQueue.push(endEvent);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.eventQueue.push(undefined);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pushFrame(frame: AudioFrame) {
|
|
56
|
+
if (this.closed) {
|
|
57
|
+
throw new TypeError('cannot push frame to closed stream');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.vadStream.pushFrame(frame);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async close(wait: boolean = true): Promise<void> {
|
|
64
|
+
this.closed = true;
|
|
65
|
+
|
|
66
|
+
if (!wait) {
|
|
67
|
+
this.task.cancel();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this.vadStream.close(wait);
|
|
71
|
+
await this.task.run;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
next(): IteratorResult<SpeechEvent> {
|
|
75
|
+
const item = this.eventQueue.shift();
|
|
76
|
+
if (item) {
|
|
77
|
+
return { done: false, value: item };
|
|
78
|
+
} else {
|
|
79
|
+
return { done: true, value: undefined };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class StreamAdapter extends STT {
|
|
85
|
+
stt: STT;
|
|
86
|
+
vadStream: VADStream;
|
|
87
|
+
|
|
88
|
+
constructor(stt: STT, vadStream: VADStream) {
|
|
89
|
+
super(true);
|
|
90
|
+
this.stt = stt;
|
|
91
|
+
this.vadStream = vadStream;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async recognize(
|
|
95
|
+
buffer: AudioBuffer,
|
|
96
|
+
language: string | undefined = undefined,
|
|
97
|
+
): Promise<SpeechEvent> {
|
|
98
|
+
return await this.stt.recognize(buffer, language);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
stream(language: string | undefined = undefined) {
|
|
102
|
+
return new StreamAdapterWrapper(this.stt, this.vadStream, language);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/stt/stt.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { AudioFrame } from '@livekit/rtc-node';
|
|
5
|
+
import type { AudioBuffer } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
export enum SpeechEventType {
|
|
8
|
+
START_OF_SPEECH = 0,
|
|
9
|
+
INTERIM_TRANSCRIPT = 1,
|
|
10
|
+
FINAL_TRANSCRIPT = 2,
|
|
11
|
+
END_OF_SPEECH = 3,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SpeechData {
|
|
15
|
+
language: string;
|
|
16
|
+
text: string;
|
|
17
|
+
startTime: number;
|
|
18
|
+
endTime: number;
|
|
19
|
+
confidence: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SpeechEvent {
|
|
23
|
+
type: SpeechEventType;
|
|
24
|
+
alternatives: SpeechData[];
|
|
25
|
+
|
|
26
|
+
constructor(type: SpeechEventType, alternatives: SpeechData[] = []) {
|
|
27
|
+
this.type = type;
|
|
28
|
+
this.alternatives = alternatives;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export abstract class SpeechStream implements IterableIterator<SpeechEvent> {
|
|
33
|
+
abstract pushFrame(token: AudioFrame): void;
|
|
34
|
+
|
|
35
|
+
abstract close(wait: boolean): Promise<void>;
|
|
36
|
+
|
|
37
|
+
abstract next(): IteratorResult<SpeechEvent>;
|
|
38
|
+
|
|
39
|
+
[Symbol.iterator](): SpeechStream {
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export abstract class STT {
|
|
45
|
+
#streamingSupported: boolean;
|
|
46
|
+
|
|
47
|
+
constructor(streamingSupported: boolean) {
|
|
48
|
+
this.#streamingSupported = streamingSupported;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
abstract recognize(buffer: AudioBuffer, language?: string): Promise<SpeechEvent>;
|
|
52
|
+
|
|
53
|
+
abstract stream(language: string | undefined): SpeechStream;
|
|
54
|
+
|
|
55
|
+
get streamingSupported(): boolean {
|
|
56
|
+
return this.#streamingSupported;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/tokenize.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
export interface SegmentedSentence {
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class SentenceTokenizer {
|
|
10
|
+
abstract tokenize(text: string, language?: string): SegmentedSentence[];
|
|
11
|
+
abstract stream(language: string | undefined): SentenceStream;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export abstract class SentenceStream implements IterableIterator<SegmentedSentence> {
|
|
15
|
+
abstract pushText(text: string): void;
|
|
16
|
+
abstract flush(): Promise<void>;
|
|
17
|
+
async close(): Promise<void> {}
|
|
18
|
+
abstract next(): IteratorResult<SegmentedSentence>;
|
|
19
|
+
[Symbol.iterator](): SentenceStream {
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
}
|