@livekit/agents 0.7.2 → 0.7.4
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/dist/audio.cjs +1 -1
- package/dist/audio.cjs.map +1 -1
- package/dist/audio.js +1 -1
- package/dist/audio.js.map +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/constants.cjs +38 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.cjs +14 -14
- package/dist/index.cjs.map +1 -1
- package/dist/ipc/inference_proc_lazy_main.cjs +14 -27
- package/dist/ipc/inference_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/inference_proc_lazy_main.js +14 -5
- package/dist/ipc/inference_proc_lazy_main.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +23 -10
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +23 -10
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/ipc/supervised_proc.cjs +4 -5
- package/dist/ipc/supervised_proc.cjs.map +1 -1
- package/dist/ipc/supervised_proc.d.ts.map +1 -1
- package/dist/ipc/supervised_proc.js +4 -5
- package/dist/ipc/supervised_proc.js.map +1 -1
- package/dist/multimodal/agent_playout.cjs +1 -0
- package/dist/multimodal/agent_playout.cjs.map +1 -1
- package/dist/multimodal/agent_playout.js +1 -0
- package/dist/multimodal/agent_playout.js.map +1 -1
- package/dist/multimodal/multimodal_agent.cjs +36 -11
- package/dist/multimodal/multimodal_agent.cjs.map +1 -1
- package/dist/multimodal/multimodal_agent.d.ts +3 -2
- package/dist/multimodal/multimodal_agent.d.ts.map +1 -1
- package/dist/multimodal/multimodal_agent.js +40 -11
- package/dist/multimodal/multimodal_agent.js.map +1 -1
- package/dist/pipeline/agent_playout.cjs +1 -1
- package/dist/pipeline/agent_playout.cjs.map +1 -1
- package/dist/pipeline/agent_playout.d.ts.map +1 -1
- package/dist/pipeline/agent_playout.js +1 -1
- package/dist/pipeline/agent_playout.js.map +1 -1
- package/dist/pipeline/human_input.cjs +9 -2
- package/dist/pipeline/human_input.cjs.map +1 -1
- package/dist/pipeline/human_input.d.ts +2 -2
- package/dist/pipeline/human_input.d.ts.map +1 -1
- package/dist/pipeline/human_input.js +9 -2
- package/dist/pipeline/human_input.js.map +1 -1
- package/dist/pipeline/pipeline_agent.cjs +59 -37
- package/dist/pipeline/pipeline_agent.cjs.map +1 -1
- package/dist/pipeline/pipeline_agent.d.ts +3 -1
- package/dist/pipeline/pipeline_agent.d.ts.map +1 -1
- package/dist/pipeline/pipeline_agent.js +63 -37
- package/dist/pipeline/pipeline_agent.js.map +1 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +4 -4
- package/src/audio.ts +1 -1
- package/src/constants.ts +7 -0
- package/src/ipc/inference_proc_lazy_main.ts +21 -6
- package/src/ipc/job_proc_lazy_main.ts +27 -9
- package/src/ipc/supervised_proc.ts +5 -6
- package/src/multimodal/multimodal_agent.ts +43 -13
- package/src/pipeline/agent_playout.ts +1 -7
- package/src/pipeline/human_input.ts +17 -3
- package/src/pipeline/pipeline_agent.ts +79 -38
- package/src/worker.ts +1 -1
- package/dist/llm/function_context.test.d.ts +0 -2
- package/dist/llm/function_context.test.d.ts.map +0 -1
- package/dist/tokenize/tokenizer.test.d.ts +0 -2
- package/dist/tokenize/tokenizer.test.d.ts.map +0 -1
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { isAgent } from "../generator.js";
|
|
6
6
|
import { CurrentJobContext, JobContext, JobProcess } from "../job.js";
|
|
7
7
|
import { initializeLogger, log } from "../log.js";
|
|
8
|
+
import { Future } from "../utils.js";
|
|
8
9
|
import { defaultInitializeProcessFunc } from "../worker.js";
|
|
9
10
|
const ORPHANED_TIMEOUT = 15 * 1e3;
|
|
10
11
|
class PendingInference {
|
|
@@ -43,12 +44,14 @@ class InfClient {
|
|
|
43
44
|
return resp.data;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
const startJob = (proc, func, info, closeEvent, logger) => {
|
|
47
|
+
const startJob = (proc, func, info, closeEvent, logger, joinFuture) => {
|
|
47
48
|
let connect = false;
|
|
48
49
|
let shutdown = false;
|
|
49
50
|
const room = new Room();
|
|
50
51
|
room.on(RoomEvent.Disconnected, () => {
|
|
51
|
-
|
|
52
|
+
if (!shutdown) {
|
|
53
|
+
closeEvent.emit("close", false);
|
|
54
|
+
}
|
|
52
55
|
});
|
|
53
56
|
const onConnect = () => {
|
|
54
57
|
connect = true;
|
|
@@ -71,6 +74,7 @@ const startJob = (proc, func, info, closeEvent, logger) => {
|
|
|
71
74
|
func(ctx).finally(() => clearTimeout(unconnectedTimeout));
|
|
72
75
|
await once(closeEvent, "close").then((close) => {
|
|
73
76
|
logger.debug("shutting down");
|
|
77
|
+
shutdown = true;
|
|
74
78
|
process.send({ case: "exiting", value: { reason: close[1] } });
|
|
75
79
|
});
|
|
76
80
|
await room.disconnect();
|
|
@@ -79,15 +83,18 @@ const startJob = (proc, func, info, closeEvent, logger) => {
|
|
|
79
83
|
for (const callback of ctx.shutdownCallbacks) {
|
|
80
84
|
shutdownTasks.push(callback());
|
|
81
85
|
}
|
|
82
|
-
await Promise.all(shutdownTasks).catch(
|
|
86
|
+
await Promise.all(shutdownTasks).catch(
|
|
87
|
+
(error) => logger.error("error while shutting down the job", error)
|
|
88
|
+
);
|
|
83
89
|
process.send({ case: "done" });
|
|
84
90
|
logger.info("job completed.");
|
|
85
|
-
|
|
91
|
+
joinFuture.resolve();
|
|
86
92
|
});
|
|
87
93
|
return { ctx, task };
|
|
88
94
|
};
|
|
89
95
|
(async () => {
|
|
90
96
|
if (process.send) {
|
|
97
|
+
const join = new Future();
|
|
91
98
|
const moduleFile = process.argv[2];
|
|
92
99
|
const agent = await import(pathToFileURL(moduleFile).pathname).then((module) => {
|
|
93
100
|
const agent2 = module.default;
|
|
@@ -125,9 +132,9 @@ const startJob = (proc, func, info, closeEvent, logger) => {
|
|
|
125
132
|
const closeEvent = new EventEmitter();
|
|
126
133
|
const orphanedTimeout = setTimeout(() => {
|
|
127
134
|
logger.warn("job process orphaned, shutting down.");
|
|
128
|
-
|
|
135
|
+
join.resolve();
|
|
129
136
|
}, ORPHANED_TIMEOUT);
|
|
130
|
-
|
|
137
|
+
const messageHandler = (msg) => {
|
|
131
138
|
switch (msg.case) {
|
|
132
139
|
case "pingRequest": {
|
|
133
140
|
orphanedTimeout.refresh();
|
|
@@ -142,18 +149,24 @@ const startJob = (proc, func, info, closeEvent, logger) => {
|
|
|
142
149
|
throw new Error("job task already running");
|
|
143
150
|
}
|
|
144
151
|
logger = logger.child({ jobID: msg.value.runningJob.job.id });
|
|
145
|
-
job = startJob(proc, agent.entry, msg.value.runningJob, closeEvent, logger);
|
|
152
|
+
job = startJob(proc, agent.entry, msg.value.runningJob, closeEvent, logger, join);
|
|
146
153
|
logger.debug("job started");
|
|
147
154
|
break;
|
|
148
155
|
}
|
|
149
156
|
case "shutdownRequest": {
|
|
150
157
|
if (!job) {
|
|
151
|
-
|
|
158
|
+
join.resolve();
|
|
152
159
|
}
|
|
153
|
-
closeEvent.emit("close", "");
|
|
160
|
+
closeEvent.emit("close", "shutdownRequest");
|
|
161
|
+
clearTimeout(orphanedTimeout);
|
|
162
|
+
process.off("message", messageHandler);
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
|
-
}
|
|
165
|
+
};
|
|
166
|
+
process.on("message", messageHandler);
|
|
167
|
+
await join.await;
|
|
168
|
+
logger.info("Job process shutdown");
|
|
169
|
+
return process.exitCode;
|
|
157
170
|
}
|
|
158
171
|
})();
|
|
159
172
|
//# sourceMappingURL=job_proc_lazy_main.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ipc/job_proc_lazy_main.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { Room, RoomEvent } from '@livekit/rtc-node';\nimport { randomUUID } from 'node:crypto';\nimport { EventEmitter, once } from 'node:events';\nimport { pathToFileURL } from 'node:url';\nimport type { Logger } from 'pino';\nimport { type Agent, isAgent } from '../generator.js';\nimport { CurrentJobContext, JobContext, JobProcess, type RunningJobInfo } from '../job.js';\nimport { initializeLogger, log } from '../log.js';\nimport { defaultInitializeProcessFunc } from '../worker.js';\nimport type { InferenceExecutor } from './inference_executor.js';\nimport type { IPCMessage } from './message.js';\n\nconst ORPHANED_TIMEOUT = 15 * 1000;\n\ntype JobTask = {\n ctx: JobContext;\n task: Promise<void>;\n};\n\nclass PendingInference {\n promise = new Promise<{ requestId: string; data: unknown; error?: Error }>((resolve) => {\n this.resolve = resolve; // this is how JavaScript lets you resolve promises externally\n });\n resolve(arg: { requestId: string; data: unknown; error?: Error }) {\n arg; // useless call to counteract TypeScript E6133\n }\n}\n\nclass InfClient implements InferenceExecutor {\n #requests: { [id: string]: PendingInference } = {};\n\n constructor() {\n process.on('message', (msg: IPCMessage) => {\n switch (msg.case) {\n case 'inferenceResponse':\n const fut = this.#requests[msg.value.requestId];\n delete this.#requests[msg.value.requestId];\n if (!fut) {\n log().child({ resp: msg.value }).warn('received unexpected inference response');\n return;\n }\n fut.resolve(msg.value);\n break;\n }\n });\n }\n\n async doInference(method: string, data: unknown): Promise<unknown> {\n const requestId = 'inference_job_' + randomUUID;\n process.send!({ case: 'inferenceRequest', value: { requestId, method, data } });\n this.#requests[requestId] = new PendingInference();\n const resp = await this.#requests[requestId]!.promise;\n if (resp.error) {\n throw new Error(`inference of ${method} failed: ${resp.error.message}`);\n }\n return resp.data;\n }\n}\n\nconst startJob = (\n proc: JobProcess,\n func: (ctx: JobContext) => Promise<void>,\n info: RunningJobInfo,\n closeEvent: EventEmitter,\n logger: Logger,\n): JobTask => {\n let connect = false;\n let shutdown = false;\n\n const room = new Room();\n room.on(RoomEvent.Disconnected, () => {\n closeEvent.emit('close', false);\n });\n\n const onConnect = () => {\n connect = true;\n };\n const onShutdown = (reason: string) => {\n shutdown = true;\n closeEvent.emit('close', true, reason);\n };\n\n const ctx = new JobContext(proc, info, room, onConnect, onShutdown, new InfClient());\n new CurrentJobContext(ctx);\n\n const task = new Promise<void>(async () => {\n const unconnectedTimeout = setTimeout(() => {\n if (!(connect || shutdown)) {\n logger.warn(\n 'room not connect after job_entry was called after 10 seconds, ',\n 'did you forget to call ctx.connect()?',\n );\n }\n }, 10000);\n func(ctx).finally(() => clearTimeout(unconnectedTimeout));\n\n await once(closeEvent, 'close').then((close) => {\n logger.debug('shutting down');\n process.send!({ case: 'exiting', value: { reason: close[1] } });\n });\n\n await room.disconnect();\n logger.debug('disconnected from room');\n\n const shutdownTasks = [];\n for (const callback of ctx.shutdownCallbacks) {\n shutdownTasks.push(callback());\n }\n await Promise.all(shutdownTasks).catch(() => logger.error('error while shutting down the job'));\n\n process.send!({ case: 'done' });\n logger.info('job completed.');\n process.exit();\n });\n\n return { ctx, task };\n};\n\n(async () => {\n if (process.send) {\n // process.argv:\n // [0] `node'\n // [1] import.meta.filename\n // [2] import.meta.filename of function containing entry file\n const moduleFile = process.argv[2];\n const agent: Agent = await import(pathToFileURL(moduleFile!).pathname).then((module) => {\n const agent = module.default;\n if (agent === undefined || !isAgent(agent)) {\n throw new Error(`Unable to load agent: Missing or invalid default export in ${moduleFile}`);\n }\n return agent;\n });\n if (!agent.prewarm) {\n agent.prewarm = defaultInitializeProcessFunc;\n }\n\n // don't do anything on C-c\n // this is handled in cli, triggering a termination of all child processes at once.\n process.on('SIGINT', () => {\n logger.info('SIGINT received in job proc');\n });\n\n // don't do anything on SIGTERM\n // Render uses SIGTERM in autoscale, this ensures the processes are properly drained if needed\n process.on('SIGTERM', () => {\n logger.info('SIGTERM received in job proc');\n });\n\n await once(process, 'message').then(([msg]: IPCMessage[]) => {\n msg = msg!;\n if (msg.case !== 'initializeRequest') {\n throw new Error('first message must be InitializeRequest');\n }\n initializeLogger(msg.value.loggerOptions);\n });\n const proc = new JobProcess();\n let logger = log().child({ pid: proc.pid });\n\n process.on('unhandledRejection', (reason) => {\n logger.error(reason);\n });\n\n logger.debug('initializing job runner');\n agent.prewarm(proc);\n logger.debug('job runner initialized');\n process.send({ case: 'initializeResponse' });\n\n let job: JobTask | undefined = undefined;\n const closeEvent = new EventEmitter();\n\n const orphanedTimeout = setTimeout(() => {\n logger.warn('job process orphaned, shutting down.');\n process.exit();\n }, ORPHANED_TIMEOUT);\n\n process.on('message', (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pingRequest': {\n orphanedTimeout.refresh();\n process.send!({\n case: 'pongResponse',\n value: { lastTimestamp: msg.value.timestamp, timestamp: Date.now() },\n });\n break;\n }\n case 'startJobRequest': {\n if (job) {\n throw new Error('job task already running');\n }\n\n logger = logger.child({ jobID: msg.value.runningJob.job.id });\n\n job = startJob(proc, agent.entry, msg.value.runningJob, closeEvent, logger);\n logger.debug('job started');\n break;\n }\n case 'shutdownRequest': {\n if (!job) {\n break;\n }\n closeEvent.emit('close', '');\n }\n }\n });\n }\n})();\n"],"mappings":"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","names":["agent"]}
|
|
1
|
+
{"version":3,"sources":["../../src/ipc/job_proc_lazy_main.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { Room, RoomEvent } from '@livekit/rtc-node';\nimport { randomUUID } from 'node:crypto';\nimport { EventEmitter, once } from 'node:events';\nimport { pathToFileURL } from 'node:url';\nimport type { Logger } from 'pino';\nimport { type Agent, isAgent } from '../generator.js';\nimport { CurrentJobContext, JobContext, JobProcess, type RunningJobInfo } from '../job.js';\nimport { initializeLogger, log } from '../log.js';\nimport { Future } from '../utils.js';\nimport { defaultInitializeProcessFunc } from '../worker.js';\nimport type { InferenceExecutor } from './inference_executor.js';\nimport type { IPCMessage } from './message.js';\n\nconst ORPHANED_TIMEOUT = 15 * 1000;\n\ntype JobTask = {\n ctx: JobContext;\n task: Promise<void>;\n};\n\nclass PendingInference {\n promise = new Promise<{ requestId: string; data: unknown; error?: Error }>((resolve) => {\n this.resolve = resolve; // this is how JavaScript lets you resolve promises externally\n });\n resolve(arg: { requestId: string; data: unknown; error?: Error }) {\n arg; // useless call to counteract TypeScript E6133\n }\n}\n\nclass InfClient implements InferenceExecutor {\n #requests: { [id: string]: PendingInference } = {};\n\n constructor() {\n process.on('message', (msg: IPCMessage) => {\n switch (msg.case) {\n case 'inferenceResponse':\n const fut = this.#requests[msg.value.requestId];\n delete this.#requests[msg.value.requestId];\n if (!fut) {\n log().child({ resp: msg.value }).warn('received unexpected inference response');\n return;\n }\n fut.resolve(msg.value);\n break;\n }\n });\n }\n\n async doInference(method: string, data: unknown): Promise<unknown> {\n const requestId = 'inference_job_' + randomUUID;\n process.send!({ case: 'inferenceRequest', value: { requestId, method, data } });\n this.#requests[requestId] = new PendingInference();\n const resp = await this.#requests[requestId]!.promise;\n if (resp.error) {\n throw new Error(`inference of ${method} failed: ${resp.error.message}`);\n }\n return resp.data;\n }\n}\n\nconst startJob = (\n proc: JobProcess,\n func: (ctx: JobContext) => Promise<void>,\n info: RunningJobInfo,\n closeEvent: EventEmitter,\n logger: Logger,\n joinFuture: Future,\n): JobTask => {\n let connect = false;\n let shutdown = false;\n\n const room = new Room();\n room.on(RoomEvent.Disconnected, () => {\n if (!shutdown) {\n closeEvent.emit('close', false);\n }\n });\n\n const onConnect = () => {\n connect = true;\n };\n const onShutdown = (reason: string) => {\n shutdown = true;\n closeEvent.emit('close', true, reason);\n };\n\n const ctx = new JobContext(proc, info, room, onConnect, onShutdown, new InfClient());\n new CurrentJobContext(ctx);\n\n const task = new Promise<void>(async () => {\n const unconnectedTimeout = setTimeout(() => {\n if (!(connect || shutdown)) {\n logger.warn(\n 'room not connect after job_entry was called after 10 seconds, ',\n 'did you forget to call ctx.connect()?',\n );\n }\n }, 10000);\n func(ctx).finally(() => clearTimeout(unconnectedTimeout));\n\n await once(closeEvent, 'close').then((close) => {\n logger.debug('shutting down');\n shutdown = true;\n process.send!({ case: 'exiting', value: { reason: close[1] } });\n });\n\n await room.disconnect();\n logger.debug('disconnected from room');\n\n const shutdownTasks = [];\n for (const callback of ctx.shutdownCallbacks) {\n shutdownTasks.push(callback());\n }\n await Promise.all(shutdownTasks).catch((error) =>\n logger.error('error while shutting down the job', error),\n );\n\n process.send!({ case: 'done' });\n logger.info('job completed.');\n joinFuture.resolve();\n });\n\n return { ctx, task };\n};\n\n(async () => {\n if (process.send) {\n const join = new Future();\n\n // process.argv:\n // [0] `node'\n // [1] import.meta.filename\n // [2] import.meta.filename of function containing entry file\n const moduleFile = process.argv[2];\n const agent: Agent = await import(pathToFileURL(moduleFile!).pathname).then((module) => {\n const agent = module.default;\n if (agent === undefined || !isAgent(agent)) {\n throw new Error(`Unable to load agent: Missing or invalid default export in ${moduleFile}`);\n }\n return agent;\n });\n if (!agent.prewarm) {\n agent.prewarm = defaultInitializeProcessFunc;\n }\n\n // don't do anything on C-c\n // this is handled in cli, triggering a termination of all child processes at once.\n process.on('SIGINT', () => {\n logger.info('SIGINT received in job proc');\n });\n\n // don't do anything on SIGTERM\n // Render uses SIGTERM in autoscale, this ensures the processes are properly drained if needed\n process.on('SIGTERM', () => {\n logger.info('SIGTERM received in job proc');\n });\n\n await once(process, 'message').then(([msg]: IPCMessage[]) => {\n msg = msg!;\n if (msg.case !== 'initializeRequest') {\n throw new Error('first message must be InitializeRequest');\n }\n initializeLogger(msg.value.loggerOptions);\n });\n const proc = new JobProcess();\n let logger = log().child({ pid: proc.pid });\n\n process.on('unhandledRejection', (reason) => {\n logger.error(reason);\n });\n\n logger.debug('initializing job runner');\n agent.prewarm(proc);\n logger.debug('job runner initialized');\n process.send({ case: 'initializeResponse' });\n\n let job: JobTask | undefined = undefined;\n const closeEvent = new EventEmitter();\n\n const orphanedTimeout = setTimeout(() => {\n logger.warn('job process orphaned, shutting down.');\n join.resolve();\n }, ORPHANED_TIMEOUT);\n\n const messageHandler = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pingRequest': {\n orphanedTimeout.refresh();\n process.send!({\n case: 'pongResponse',\n value: { lastTimestamp: msg.value.timestamp, timestamp: Date.now() },\n });\n break;\n }\n case 'startJobRequest': {\n if (job) {\n throw new Error('job task already running');\n }\n\n logger = logger.child({ jobID: msg.value.runningJob.job.id });\n\n job = startJob(proc, agent.entry, msg.value.runningJob, closeEvent, logger, join);\n logger.debug('job started');\n break;\n }\n case 'shutdownRequest': {\n if (!job) {\n join.resolve();\n }\n closeEvent.emit('close', 'shutdownRequest');\n clearTimeout(orphanedTimeout);\n process.off('message', messageHandler);\n }\n }\n };\n\n process.on('message', messageHandler);\n\n await join.await;\n\n logger.info('Job process shutdown');\n return process.exitCode;\n }\n})();\n"],"mappings":"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","names":["agent"]}
|
|
@@ -106,7 +106,6 @@ class SupervisedProc {
|
|
|
106
106
|
case "done": {
|
|
107
107
|
this.#closing = true;
|
|
108
108
|
this.proc.off("message", listener);
|
|
109
|
-
this.#join.resolve();
|
|
110
109
|
break;
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -120,6 +119,9 @@ class SupervisedProc {
|
|
|
120
119
|
clearInterval(this.#memoryWatch);
|
|
121
120
|
this.#join.resolve();
|
|
122
121
|
});
|
|
122
|
+
this.proc.on("exit", () => {
|
|
123
|
+
this.#join.resolve();
|
|
124
|
+
});
|
|
123
125
|
this.mainTask(this.proc);
|
|
124
126
|
await this.#join.await;
|
|
125
127
|
}
|
|
@@ -157,13 +159,10 @@ class SupervisedProc {
|
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
159
161
|
this.#closing = true;
|
|
160
|
-
if (!this.#runningJob) {
|
|
161
|
-
this.proc.kill();
|
|
162
|
-
this.#join.resolve();
|
|
163
|
-
}
|
|
164
162
|
this.proc.send({ case: "shutdownRequest" });
|
|
165
163
|
const timer = setTimeout(() => {
|
|
166
164
|
this.#logger.error("job shutdown is taking too much time");
|
|
165
|
+
this.proc.kill();
|
|
167
166
|
}, this.#opts.closeTimeout);
|
|
168
167
|
await this.#join.await.then(() => {
|
|
169
168
|
clearTimeout(timer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryWatch?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n this.proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryWatch = setInterval(() => {\n const memoryMB = process.memoryUsage().heapUsed / (1024 * 1024);\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .error('process memory usage is high');\n }\n });\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n
|
|
1
|
+
{"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryWatch?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n this.proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryWatch = setInterval(() => {\n const memoryMB = process.memoryUsage().heapUsed / (1024 * 1024);\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .error('process memory usage is high');\n }\n });\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n break;\n }\n }\n };\n this.proc!.on('message', listener);\n this.proc!.on('error', (err) => {\n if (this.#closing) return;\n this.#logger\n .child({ err })\n .warn('job process exited unexpectedly; this likely means the error above caused a crash');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n clearInterval(this.#memoryWatch);\n this.#join.resolve();\n });\n\n this.proc!.on('exit', () => {\n this.#join.resolve();\n });\n\n this.mainTask(this.proc!);\n\n await this.#join.await;\n }\n\n async join() {\n if (!this.#started) {\n throw new Error('runner not started');\n }\n\n await this.#join.await;\n }\n\n async initialize() {\n const timer = setTimeout(() => {\n const err = new Error('runner initialization timed out');\n this.init.reject(err);\n throw err;\n }, this.#opts.initializeTimeout);\n this.proc!.send({\n case: 'initializeRequest',\n value: {\n loggerOptions,\n pingInterval: this.#opts.pingInterval,\n pingTimeout: this.#opts.pingTimeout,\n highPingThreshold: this.#opts.highPingThreshold,\n },\n });\n await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {\n clearTimeout(timer);\n if (msg!.case !== 'initializeResponse') {\n throw new Error('first message must be InitializeResponse');\n }\n });\n this.init.resolve();\n }\n\n async close() {\n if (!this.#started) {\n return;\n }\n this.#closing = true;\n\n this.proc!.send({ case: 'shutdownRequest' });\n\n const timer = setTimeout(() => {\n this.#logger.error('job shutdown is taking too much time');\n this.proc!.kill();\n }, this.#opts.closeTimeout);\n await this.#join.await.then(() => {\n clearTimeout(timer);\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n });\n }\n\n async launchJob(info: RunningJobInfo) {\n if (this.#runningJob) {\n throw new Error('executor already has a running job');\n }\n this.#runningJob = info;\n this.proc!.send({ case: 'startJobRequest', value: { runningJob: info } });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supervised_proc.d.ts","sourceRoot":"","sources":["../../src/ipc/supervised_proc.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,MAAM,WAAW,QAAQ;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,8BAAsB,cAAc;;IAKlC,IAAI,CAAC,EAAE,YAAY,CAAC;IAIpB,SAAS,CAAC,IAAI,SAAgB;gBAK5B,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM;IAa3B,QAAQ,CAAC,aAAa,IAAI,YAAY;IACtC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAErD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,cAAc,GAAG,SAAS,CAE3C;IAEK,KAAK;IAaL,GAAG;
|
|
1
|
+
{"version":3,"file":"supervised_proc.d.ts","sourceRoot":"","sources":["../../src/ipc/supervised_proc.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,MAAM,WAAW,QAAQ;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,8BAAsB,cAAc;;IAKlC,IAAI,CAAC,EAAE,YAAY,CAAC;IAIpB,SAAS,CAAC,IAAI,SAAgB;gBAK5B,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM;IAa3B,QAAQ,CAAC,aAAa,IAAI,YAAY;IACtC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAErD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,cAAc,GAAG,SAAS,CAE3C;IAEK,KAAK;IAaL,GAAG;IA2EH,IAAI;IAQJ,UAAU;IAwBV,KAAK;IAmBL,SAAS,CAAC,IAAI,EAAE,cAAc;CAOrC"}
|
|
@@ -83,7 +83,6 @@ class SupervisedProc {
|
|
|
83
83
|
case "done": {
|
|
84
84
|
this.#closing = true;
|
|
85
85
|
this.proc.off("message", listener);
|
|
86
|
-
this.#join.resolve();
|
|
87
86
|
break;
|
|
88
87
|
}
|
|
89
88
|
}
|
|
@@ -97,6 +96,9 @@ class SupervisedProc {
|
|
|
97
96
|
clearInterval(this.#memoryWatch);
|
|
98
97
|
this.#join.resolve();
|
|
99
98
|
});
|
|
99
|
+
this.proc.on("exit", () => {
|
|
100
|
+
this.#join.resolve();
|
|
101
|
+
});
|
|
100
102
|
this.mainTask(this.proc);
|
|
101
103
|
await this.#join.await;
|
|
102
104
|
}
|
|
@@ -134,13 +136,10 @@ class SupervisedProc {
|
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
138
|
this.#closing = true;
|
|
137
|
-
if (!this.#runningJob) {
|
|
138
|
-
this.proc.kill();
|
|
139
|
-
this.#join.resolve();
|
|
140
|
-
}
|
|
141
139
|
this.proc.send({ case: "shutdownRequest" });
|
|
142
140
|
const timer = setTimeout(() => {
|
|
143
141
|
this.#logger.error("job shutdown is taking too much time");
|
|
142
|
+
this.proc.kill();
|
|
144
143
|
}, this.#opts.closeTimeout);
|
|
145
144
|
await this.#join.await.then(() => {
|
|
146
145
|
clearTimeout(timer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryWatch?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n this.proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryWatch = setInterval(() => {\n const memoryMB = process.memoryUsage().heapUsed / (1024 * 1024);\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .error('process memory usage is high');\n }\n });\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n
|
|
1
|
+
{"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryWatch?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n this.proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryWatch = setInterval(() => {\n const memoryMB = process.memoryUsage().heapUsed / (1024 * 1024);\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .error('process memory usage is high');\n }\n });\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n break;\n }\n }\n };\n this.proc!.on('message', listener);\n this.proc!.on('error', (err) => {\n if (this.#closing) return;\n this.#logger\n .child({ err })\n .warn('job process exited unexpectedly; this likely means the error above caused a crash');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n clearInterval(this.#memoryWatch);\n this.#join.resolve();\n });\n\n this.proc!.on('exit', () => {\n this.#join.resolve();\n });\n\n this.mainTask(this.proc!);\n\n await this.#join.await;\n }\n\n async join() {\n if (!this.#started) {\n throw new Error('runner not started');\n }\n\n await this.#join.await;\n }\n\n async initialize() {\n const timer = setTimeout(() => {\n const err = new Error('runner initialization timed out');\n this.init.reject(err);\n throw err;\n }, this.#opts.initializeTimeout);\n this.proc!.send({\n case: 'initializeRequest',\n value: {\n loggerOptions,\n pingInterval: this.#opts.pingInterval,\n pingTimeout: this.#opts.pingTimeout,\n highPingThreshold: this.#opts.highPingThreshold,\n },\n });\n await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {\n clearTimeout(timer);\n if (msg!.case !== 'initializeResponse') {\n throw new Error('first message must be InitializeResponse');\n }\n });\n this.init.resolve();\n }\n\n async close() {\n if (!this.#started) {\n return;\n }\n this.#closing = true;\n\n this.proc!.send({ case: 'shutdownRequest' });\n\n const timer = setTimeout(() => {\n this.#logger.error('job shutdown is taking too much time');\n this.proc!.kill();\n }, this.#opts.closeTimeout);\n await this.#join.await.then(() => {\n clearTimeout(timer);\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n });\n }\n\n async launchJob(info: RunningJobInfo) {\n if (this.#runningJob) {\n throw new Error('executor already has a running job');\n }\n this.#runningJob = info;\n this.proc!.send({ case: 'startJobRequest', value: { runningJob: info } });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -23,6 +23,7 @@ __export(agent_playout_exports, {
|
|
|
23
23
|
proto: () => proto
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(agent_playout_exports);
|
|
26
|
+
var import_rtc_node = require("@livekit/rtc-node");
|
|
26
27
|
var import_node_events = require("node:events");
|
|
27
28
|
var import_audio = require("../audio.cjs");
|
|
28
29
|
var import_utils = require("../utils.cjs");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/multimodal/agent_playout.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { type AudioSource } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport { AudioByteStream } from '../audio.js';\nimport type { TextAudioSynchronizer } from '../transcription.js';\nimport { type AsyncIterableQueue, CancellablePromise, Future, gracefullyCancel } from '../utils.js';\n\nexport const proto = {};\n\nexport class PlayoutHandle extends EventEmitter {\n #audioSource: AudioSource;\n #sampleRate: number;\n #itemId: string;\n #contentIndex: number;\n /** @internal */\n synchronizer: TextAudioSynchronizer;\n /** @internal */\n doneFut: Future;\n /** @internal */\n intFut: Future;\n /** @internal */\n #interrupted: boolean;\n /** @internal */\n pushedDuration: number;\n /** @internal */\n totalPlayedTime: number | undefined; // Set when playout is done\n\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#sampleRate = sampleRate;\n this.#itemId = itemId;\n this.#contentIndex = contentIndex;\n this.synchronizer = synchronizer;\n this.doneFut = new Future();\n this.intFut = new Future();\n this.#interrupted = false;\n this.pushedDuration = 0;\n this.totalPlayedTime = undefined;\n }\n\n get itemId(): string {\n return this.#itemId;\n }\n\n get audioSamples(): number {\n if (this.totalPlayedTime !== undefined) {\n return Math.floor(this.totalPlayedTime * this.#sampleRate);\n }\n\n return Math.max(\n 0,\n Math.floor(\n (this.pushedDuration - this.#audioSource.queuedDuration) * (this.#sampleRate / 1000),\n ),\n );\n }\n\n get textChars(): number {\n return this.synchronizer.playedText.length;\n }\n\n get contentIndex(): number {\n return this.#contentIndex;\n }\n\n get interrupted(): boolean {\n return this.#interrupted;\n }\n\n get done(): boolean {\n return this.doneFut.done || this.#interrupted;\n }\n\n interrupt() {\n if (this.doneFut.done) return;\n this.intFut.resolve();\n this.#interrupted = true;\n }\n}\n\nexport class AgentPlayout extends EventEmitter {\n #audioSource: AudioSource;\n #playoutTask: CancellablePromise<void> | null;\n #sampleRate: number;\n #numChannels: number;\n #inFrameSize: number;\n #outFrameSize: number;\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n numChannels: number,\n inFrameSize: number,\n outFrameSize: number,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#playoutTask = null;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n this.#inFrameSize = inFrameSize;\n this.#outFrameSize = outFrameSize;\n }\n\n play(\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): PlayoutHandle {\n const handle = new PlayoutHandle(\n this.#audioSource,\n this.#sampleRate,\n itemId,\n contentIndex,\n synchronizer,\n );\n this.#playoutTask = this.#makePlayoutTask(this.#playoutTask, handle, textStream, audioStream);\n return handle;\n }\n\n #makePlayoutTask(\n oldTask: CancellablePromise<void> | null,\n handle: PlayoutHandle,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): CancellablePromise<void> {\n return new CancellablePromise<void>((resolve, reject, onCancel) => {\n let cancelled = false;\n onCancel(() => {\n cancelled = true;\n });\n\n (async () => {\n try {\n if (oldTask) {\n await gracefullyCancel(oldTask);\n }\n\n let firstFrame = true;\n\n const readText = () =>\n new CancellablePromise<void>((resolveText, rejectText, onCancelText) => {\n let cancelledText = false;\n onCancelText(() => {\n cancelledText = true;\n });\n\n (async () => {\n try {\n for await (const text of textStream) {\n if (cancelledText || cancelled) {\n break;\n }\n handle.synchronizer.pushText(text);\n }\n if (!cancelled) {\n handle.synchronizer.markTextSegmentEnd();\n }\n resolveText();\n } catch (error) {\n rejectText(error);\n }\n })();\n });\n\n const capture = () =>\n new CancellablePromise<void>((resolveCapture, rejectCapture, onCancelCapture) => {\n let cancelledCapture = false;\n onCancelCapture(() => {\n cancelledCapture = true;\n });\n\n (async () => {\n try {\n const samplesPerChannel = this.#outFrameSize;\n const bstream = new AudioByteStream(\n this.#sampleRate,\n this.#numChannels,\n samplesPerChannel,\n );\n\n for await (const frame of audioStream) {\n if (cancelledCapture || cancelled) {\n break;\n }\n if (firstFrame) {\n handle.synchronizer.segmentPlayoutStarted();\n this.emit('playout_started');\n firstFrame = false;\n }\n\n handle.synchronizer.pushAudio(frame);\n\n for (const f of bstream.write(frame.data.buffer)) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n }\n\n if (!cancelledCapture && !cancelled) {\n for (const f of bstream.flush()) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n\n handle.synchronizer.markAudioSegmentEnd();\n\n await this.#audioSource.waitForPlayout();\n }\n\n resolveCapture();\n } catch (error) {\n rejectCapture(error);\n }\n })();\n });\n\n const readTextTask = readText();\n const captureTask = capture();\n\n try {\n await Promise.race([captureTask, handle.intFut.await]);\n } finally {\n if (!captureTask.isCancelled) {\n await gracefullyCancel(captureTask);\n }\n\n if (!readTextTask.isCancelled) {\n await gracefullyCancel(readTextTask);\n }\n\n handle.totalPlayedTime = handle.pushedDuration - this.#audioSource.queuedDuration;\n\n if (handle.interrupted || captureTask.error) {\n this.#audioSource.clearQueue(); // make sure to remove any queued frames\n }\n\n if (!firstFrame) {\n this.emit('playout_stopped', handle.interrupted);\n }\n\n handle.doneFut.resolve();\n\n const isInterrupted = handle.interrupted || !!captureTask.error;\n await handle.synchronizer.close(isInterrupted);\n }\n\n resolve();\n } catch (error) {\n reject(error);\n }\n })();\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/multimodal/agent_playout.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { type AudioSource } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport { AudioByteStream } from '../audio.js';\nimport type { TextAudioSynchronizer } from '../transcription.js';\nimport { type AsyncIterableQueue, CancellablePromise, Future, gracefullyCancel } from '../utils.js';\n\nexport const proto = {};\n\nexport class PlayoutHandle extends EventEmitter {\n #audioSource: AudioSource;\n #sampleRate: number;\n #itemId: string;\n #contentIndex: number;\n /** @internal */\n synchronizer: TextAudioSynchronizer;\n /** @internal */\n doneFut: Future;\n /** @internal */\n intFut: Future;\n /** @internal */\n #interrupted: boolean;\n /** @internal */\n pushedDuration: number;\n /** @internal */\n totalPlayedTime: number | undefined; // Set when playout is done\n\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#sampleRate = sampleRate;\n this.#itemId = itemId;\n this.#contentIndex = contentIndex;\n this.synchronizer = synchronizer;\n this.doneFut = new Future();\n this.intFut = new Future();\n this.#interrupted = false;\n this.pushedDuration = 0;\n this.totalPlayedTime = undefined;\n }\n\n get itemId(): string {\n return this.#itemId;\n }\n\n get audioSamples(): number {\n if (this.totalPlayedTime !== undefined) {\n return Math.floor(this.totalPlayedTime * this.#sampleRate);\n }\n\n return Math.max(\n 0,\n Math.floor(\n (this.pushedDuration - this.#audioSource.queuedDuration) * (this.#sampleRate / 1000),\n ),\n );\n }\n\n get textChars(): number {\n return this.synchronizer.playedText.length;\n }\n\n get contentIndex(): number {\n return this.#contentIndex;\n }\n\n get interrupted(): boolean {\n return this.#interrupted;\n }\n\n get done(): boolean {\n return this.doneFut.done || this.#interrupted;\n }\n\n interrupt() {\n if (this.doneFut.done) return;\n this.intFut.resolve();\n this.#interrupted = true;\n }\n}\n\nexport class AgentPlayout extends EventEmitter {\n #audioSource: AudioSource;\n #playoutTask: CancellablePromise<void> | null;\n #sampleRate: number;\n #numChannels: number;\n #inFrameSize: number;\n #outFrameSize: number;\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n numChannels: number,\n inFrameSize: number,\n outFrameSize: number,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#playoutTask = null;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n this.#inFrameSize = inFrameSize;\n this.#outFrameSize = outFrameSize;\n }\n\n play(\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): PlayoutHandle {\n const handle = new PlayoutHandle(\n this.#audioSource,\n this.#sampleRate,\n itemId,\n contentIndex,\n synchronizer,\n );\n this.#playoutTask = this.#makePlayoutTask(this.#playoutTask, handle, textStream, audioStream);\n return handle;\n }\n\n #makePlayoutTask(\n oldTask: CancellablePromise<void> | null,\n handle: PlayoutHandle,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): CancellablePromise<void> {\n return new CancellablePromise<void>((resolve, reject, onCancel) => {\n let cancelled = false;\n onCancel(() => {\n cancelled = true;\n });\n\n (async () => {\n try {\n if (oldTask) {\n await gracefullyCancel(oldTask);\n }\n\n let firstFrame = true;\n\n const readText = () =>\n new CancellablePromise<void>((resolveText, rejectText, onCancelText) => {\n let cancelledText = false;\n onCancelText(() => {\n cancelledText = true;\n });\n\n (async () => {\n try {\n for await (const text of textStream) {\n if (cancelledText || cancelled) {\n break;\n }\n handle.synchronizer.pushText(text);\n }\n if (!cancelled) {\n handle.synchronizer.markTextSegmentEnd();\n }\n resolveText();\n } catch (error) {\n rejectText(error);\n }\n })();\n });\n\n const capture = () =>\n new CancellablePromise<void>((resolveCapture, rejectCapture, onCancelCapture) => {\n let cancelledCapture = false;\n onCancelCapture(() => {\n cancelledCapture = true;\n });\n\n (async () => {\n try {\n const samplesPerChannel = this.#outFrameSize;\n const bstream = new AudioByteStream(\n this.#sampleRate,\n this.#numChannels,\n samplesPerChannel,\n );\n\n for await (const frame of audioStream) {\n if (cancelledCapture || cancelled) {\n break;\n }\n if (firstFrame) {\n handle.synchronizer.segmentPlayoutStarted();\n this.emit('playout_started');\n firstFrame = false;\n }\n\n handle.synchronizer.pushAudio(frame);\n\n for (const f of bstream.write(frame.data.buffer)) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n }\n\n if (!cancelledCapture && !cancelled) {\n for (const f of bstream.flush()) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n\n handle.synchronizer.markAudioSegmentEnd();\n\n await this.#audioSource.waitForPlayout();\n }\n\n resolveCapture();\n } catch (error) {\n rejectCapture(error);\n }\n })();\n });\n\n const readTextTask = readText();\n const captureTask = capture();\n\n try {\n await Promise.race([captureTask, handle.intFut.await]);\n } finally {\n if (!captureTask.isCancelled) {\n await gracefullyCancel(captureTask);\n }\n\n if (!readTextTask.isCancelled) {\n await gracefullyCancel(readTextTask);\n }\n\n handle.totalPlayedTime = handle.pushedDuration - this.#audioSource.queuedDuration;\n\n if (handle.interrupted || captureTask.error) {\n this.#audioSource.clearQueue(); // make sure to remove any queued frames\n }\n\n if (!firstFrame) {\n this.emit('playout_stopped', handle.interrupted);\n }\n\n handle.doneFut.resolve();\n\n const isInterrupted = handle.interrupted || !!captureTask.error;\n await handle.synchronizer.close(isInterrupted);\n }\n\n resolve();\n } catch (error) {\n reject(error);\n }\n })();\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/multimodal/agent_playout.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { type AudioSource } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport { AudioByteStream } from '../audio.js';\nimport type { TextAudioSynchronizer } from '../transcription.js';\nimport { type AsyncIterableQueue, CancellablePromise, Future, gracefullyCancel } from '../utils.js';\n\nexport const proto = {};\n\nexport class PlayoutHandle extends EventEmitter {\n #audioSource: AudioSource;\n #sampleRate: number;\n #itemId: string;\n #contentIndex: number;\n /** @internal */\n synchronizer: TextAudioSynchronizer;\n /** @internal */\n doneFut: Future;\n /** @internal */\n intFut: Future;\n /** @internal */\n #interrupted: boolean;\n /** @internal */\n pushedDuration: number;\n /** @internal */\n totalPlayedTime: number | undefined; // Set when playout is done\n\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#sampleRate = sampleRate;\n this.#itemId = itemId;\n this.#contentIndex = contentIndex;\n this.synchronizer = synchronizer;\n this.doneFut = new Future();\n this.intFut = new Future();\n this.#interrupted = false;\n this.pushedDuration = 0;\n this.totalPlayedTime = undefined;\n }\n\n get itemId(): string {\n return this.#itemId;\n }\n\n get audioSamples(): number {\n if (this.totalPlayedTime !== undefined) {\n return Math.floor(this.totalPlayedTime * this.#sampleRate);\n }\n\n return Math.max(\n 0,\n Math.floor(\n (this.pushedDuration - this.#audioSource.queuedDuration) * (this.#sampleRate / 1000),\n ),\n );\n }\n\n get textChars(): number {\n return this.synchronizer.playedText.length;\n }\n\n get contentIndex(): number {\n return this.#contentIndex;\n }\n\n get interrupted(): boolean {\n return this.#interrupted;\n }\n\n get done(): boolean {\n return this.doneFut.done || this.#interrupted;\n }\n\n interrupt() {\n if (this.doneFut.done) return;\n this.intFut.resolve();\n this.#interrupted = true;\n }\n}\n\nexport class AgentPlayout extends EventEmitter {\n #audioSource: AudioSource;\n #playoutTask: CancellablePromise<void> | null;\n #sampleRate: number;\n #numChannels: number;\n #inFrameSize: number;\n #outFrameSize: number;\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n numChannels: number,\n inFrameSize: number,\n outFrameSize: number,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#playoutTask = null;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n this.#inFrameSize = inFrameSize;\n this.#outFrameSize = outFrameSize;\n }\n\n play(\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): PlayoutHandle {\n const handle = new PlayoutHandle(\n this.#audioSource,\n this.#sampleRate,\n itemId,\n contentIndex,\n synchronizer,\n );\n this.#playoutTask = this.#makePlayoutTask(this.#playoutTask, handle, textStream, audioStream);\n return handle;\n }\n\n #makePlayoutTask(\n oldTask: CancellablePromise<void> | null,\n handle: PlayoutHandle,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): CancellablePromise<void> {\n return new CancellablePromise<void>((resolve, reject, onCancel) => {\n let cancelled = false;\n onCancel(() => {\n cancelled = true;\n });\n\n (async () => {\n try {\n if (oldTask) {\n await gracefullyCancel(oldTask);\n }\n\n let firstFrame = true;\n\n const readText = () =>\n new CancellablePromise<void>((resolveText, rejectText, onCancelText) => {\n let cancelledText = false;\n onCancelText(() => {\n cancelledText = true;\n });\n\n (async () => {\n try {\n for await (const text of textStream) {\n if (cancelledText || cancelled) {\n break;\n }\n handle.synchronizer.pushText(text);\n }\n if (!cancelled) {\n handle.synchronizer.markTextSegmentEnd();\n }\n resolveText();\n } catch (error) {\n rejectText(error);\n }\n })();\n });\n\n const capture = () =>\n new CancellablePromise<void>((resolveCapture, rejectCapture, onCancelCapture) => {\n let cancelledCapture = false;\n onCancelCapture(() => {\n cancelledCapture = true;\n });\n\n (async () => {\n try {\n const samplesPerChannel = this.#outFrameSize;\n const bstream = new AudioByteStream(\n this.#sampleRate,\n this.#numChannels,\n samplesPerChannel,\n );\n\n for await (const frame of audioStream) {\n if (cancelledCapture || cancelled) {\n break;\n }\n if (firstFrame) {\n handle.synchronizer.segmentPlayoutStarted();\n this.emit('playout_started');\n firstFrame = false;\n }\n\n handle.synchronizer.pushAudio(frame);\n\n for (const f of bstream.write(frame.data.buffer)) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n }\n\n if (!cancelledCapture && !cancelled) {\n for (const f of bstream.flush()) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n\n handle.synchronizer.markAudioSegmentEnd();\n\n await this.#audioSource.waitForPlayout();\n }\n\n resolveCapture();\n } catch (error) {\n rejectCapture(error);\n }\n })();\n });\n\n const readTextTask = readText();\n const captureTask = capture();\n\n try {\n await Promise.race([captureTask, handle.intFut.await]);\n } finally {\n if (!captureTask.isCancelled) {\n await gracefullyCancel(captureTask);\n }\n\n if (!readTextTask.isCancelled) {\n await gracefullyCancel(readTextTask);\n }\n\n handle.totalPlayedTime = handle.pushedDuration - this.#audioSource.queuedDuration;\n\n if (handle.interrupted || captureTask.error) {\n this.#audioSource.clearQueue(); // make sure to remove any queued frames\n }\n\n if (!firstFrame) {\n this.emit('playout_stopped', handle.interrupted);\n }\n\n handle.doneFut.resolve();\n\n const isInterrupted = handle.interrupted || !!captureTask.error;\n await handle.synchronizer.close(isInterrupted);\n }\n\n resolve();\n } catch (error) {\n reject(error);\n }\n })();\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/multimodal/agent_playout.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { type AudioSource } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport { AudioByteStream } from '../audio.js';\nimport type { TextAudioSynchronizer } from '../transcription.js';\nimport { type AsyncIterableQueue, CancellablePromise, Future, gracefullyCancel } from '../utils.js';\n\nexport const proto = {};\n\nexport class PlayoutHandle extends EventEmitter {\n #audioSource: AudioSource;\n #sampleRate: number;\n #itemId: string;\n #contentIndex: number;\n /** @internal */\n synchronizer: TextAudioSynchronizer;\n /** @internal */\n doneFut: Future;\n /** @internal */\n intFut: Future;\n /** @internal */\n #interrupted: boolean;\n /** @internal */\n pushedDuration: number;\n /** @internal */\n totalPlayedTime: number | undefined; // Set when playout is done\n\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#sampleRate = sampleRate;\n this.#itemId = itemId;\n this.#contentIndex = contentIndex;\n this.synchronizer = synchronizer;\n this.doneFut = new Future();\n this.intFut = new Future();\n this.#interrupted = false;\n this.pushedDuration = 0;\n this.totalPlayedTime = undefined;\n }\n\n get itemId(): string {\n return this.#itemId;\n }\n\n get audioSamples(): number {\n if (this.totalPlayedTime !== undefined) {\n return Math.floor(this.totalPlayedTime * this.#sampleRate);\n }\n\n return Math.max(\n 0,\n Math.floor(\n (this.pushedDuration - this.#audioSource.queuedDuration) * (this.#sampleRate / 1000),\n ),\n );\n }\n\n get textChars(): number {\n return this.synchronizer.playedText.length;\n }\n\n get contentIndex(): number {\n return this.#contentIndex;\n }\n\n get interrupted(): boolean {\n return this.#interrupted;\n }\n\n get done(): boolean {\n return this.doneFut.done || this.#interrupted;\n }\n\n interrupt() {\n if (this.doneFut.done) return;\n this.intFut.resolve();\n this.#interrupted = true;\n }\n}\n\nexport class AgentPlayout extends EventEmitter {\n #audioSource: AudioSource;\n #playoutTask: CancellablePromise<void> | null;\n #sampleRate: number;\n #numChannels: number;\n #inFrameSize: number;\n #outFrameSize: number;\n constructor(\n audioSource: AudioSource,\n sampleRate: number,\n numChannels: number,\n inFrameSize: number,\n outFrameSize: number,\n ) {\n super();\n this.#audioSource = audioSource;\n this.#playoutTask = null;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n this.#inFrameSize = inFrameSize;\n this.#outFrameSize = outFrameSize;\n }\n\n play(\n itemId: string,\n contentIndex: number,\n synchronizer: TextAudioSynchronizer,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): PlayoutHandle {\n const handle = new PlayoutHandle(\n this.#audioSource,\n this.#sampleRate,\n itemId,\n contentIndex,\n synchronizer,\n );\n this.#playoutTask = this.#makePlayoutTask(this.#playoutTask, handle, textStream, audioStream);\n return handle;\n }\n\n #makePlayoutTask(\n oldTask: CancellablePromise<void> | null,\n handle: PlayoutHandle,\n textStream: AsyncIterableQueue<string>,\n audioStream: AsyncIterableQueue<AudioFrame>,\n ): CancellablePromise<void> {\n return new CancellablePromise<void>((resolve, reject, onCancel) => {\n let cancelled = false;\n onCancel(() => {\n cancelled = true;\n });\n\n (async () => {\n try {\n if (oldTask) {\n await gracefullyCancel(oldTask);\n }\n\n let firstFrame = true;\n\n const readText = () =>\n new CancellablePromise<void>((resolveText, rejectText, onCancelText) => {\n let cancelledText = false;\n onCancelText(() => {\n cancelledText = true;\n });\n\n (async () => {\n try {\n for await (const text of textStream) {\n if (cancelledText || cancelled) {\n break;\n }\n handle.synchronizer.pushText(text);\n }\n if (!cancelled) {\n handle.synchronizer.markTextSegmentEnd();\n }\n resolveText();\n } catch (error) {\n rejectText(error);\n }\n })();\n });\n\n const capture = () =>\n new CancellablePromise<void>((resolveCapture, rejectCapture, onCancelCapture) => {\n let cancelledCapture = false;\n onCancelCapture(() => {\n cancelledCapture = true;\n });\n\n (async () => {\n try {\n const samplesPerChannel = this.#outFrameSize;\n const bstream = new AudioByteStream(\n this.#sampleRate,\n this.#numChannels,\n samplesPerChannel,\n );\n\n for await (const frame of audioStream) {\n if (cancelledCapture || cancelled) {\n break;\n }\n if (firstFrame) {\n handle.synchronizer.segmentPlayoutStarted();\n this.emit('playout_started');\n firstFrame = false;\n }\n\n handle.synchronizer.pushAudio(frame);\n\n for (const f of bstream.write(frame.data.buffer)) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n }\n\n if (!cancelledCapture && !cancelled) {\n for (const f of bstream.flush()) {\n handle.pushedDuration += (f.samplesPerChannel / f.sampleRate) * 1000;\n await this.#audioSource.captureFrame(f);\n }\n\n handle.synchronizer.markAudioSegmentEnd();\n\n await this.#audioSource.waitForPlayout();\n }\n\n resolveCapture();\n } catch (error) {\n rejectCapture(error);\n }\n })();\n });\n\n const readTextTask = readText();\n const captureTask = capture();\n\n try {\n await Promise.race([captureTask, handle.intFut.await]);\n } finally {\n if (!captureTask.isCancelled) {\n await gracefullyCancel(captureTask);\n }\n\n if (!readTextTask.isCancelled) {\n await gracefullyCancel(readTextTask);\n }\n\n handle.totalPlayedTime = handle.pushedDuration - this.#audioSource.queuedDuration;\n\n if (handle.interrupted || captureTask.error) {\n this.#audioSource.clearQueue(); // make sure to remove any queued frames\n }\n\n if (!firstFrame) {\n this.emit('playout_stopped', handle.interrupted);\n }\n\n handle.doneFut.resolve();\n\n const isInterrupted = handle.interrupted || !!captureTask.error;\n await handle.synchronizer.close(isInterrupted);\n }\n\n resolve();\n } catch (error) {\n reject(error);\n }\n })();\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -37,6 +37,7 @@ module.exports = __toCommonJS(multimodal_agent_exports);
|
|
|
37
37
|
var import_rtc_node = require("@livekit/rtc-node");
|
|
38
38
|
var import_node_events = require("node:events");
|
|
39
39
|
var import_audio = require("../audio.cjs");
|
|
40
|
+
var import_constants = require("../constants.cjs");
|
|
40
41
|
var llm = __toESM(require("../llm/index.cjs"), 1);
|
|
41
42
|
var import_log = require("../log.cjs");
|
|
42
43
|
var import_transcription = require("../transcription.cjs");
|
|
@@ -59,13 +60,15 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
59
60
|
model,
|
|
60
61
|
chatCtx,
|
|
61
62
|
fncCtx,
|
|
62
|
-
maxTextResponseRetries = 5
|
|
63
|
+
maxTextResponseRetries = 5,
|
|
64
|
+
noiseCancellation
|
|
63
65
|
}) {
|
|
64
66
|
super();
|
|
65
67
|
this.model = model;
|
|
66
68
|
this.#chatCtx = chatCtx;
|
|
67
69
|
this.#fncCtx = fncCtx;
|
|
68
70
|
this.#maxTextResponseRetries = maxTextResponseRetries;
|
|
71
|
+
this.#noiseCancellation = noiseCancellation;
|
|
69
72
|
}
|
|
70
73
|
#participant = null;
|
|
71
74
|
#agentPublication = null;
|
|
@@ -77,6 +80,7 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
77
80
|
#session = null;
|
|
78
81
|
#fncCtx = void 0;
|
|
79
82
|
#chatCtx = void 0;
|
|
83
|
+
#noiseCancellation = void 0;
|
|
80
84
|
#_started = false;
|
|
81
85
|
#_pendingFunctionCalls = /* @__PURE__ */ new Set();
|
|
82
86
|
#_speaking = false;
|
|
@@ -196,8 +200,8 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
196
200
|
var _a2;
|
|
197
201
|
if (message.contentType === "text") return;
|
|
198
202
|
const synchronizer = new import_transcription.TextAudioSynchronizer(import_transcription.defaultTextSyncOptions);
|
|
199
|
-
synchronizer.on("textUpdated", (text) => {
|
|
200
|
-
this.#publishTranscription(
|
|
203
|
+
synchronizer.on("textUpdated", async (text) => {
|
|
204
|
+
await this.#publishTranscription(
|
|
201
205
|
this.room.localParticipant.identity,
|
|
202
206
|
this.#getLocalTrackSid(),
|
|
203
207
|
text.text,
|
|
@@ -234,23 +238,29 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
234
238
|
this.#textResponseRetries = 0;
|
|
235
239
|
}
|
|
236
240
|
});
|
|
237
|
-
this.#session.on("input_speech_committed", (ev) => {
|
|
241
|
+
this.#session.on("input_speech_committed", async (ev) => {
|
|
238
242
|
var _a2, _b;
|
|
239
243
|
const participantIdentity = (_a2 = this.linkedParticipant) == null ? void 0 : _a2.identity;
|
|
240
244
|
const trackSid = (_b = this.subscribedTrack) == null ? void 0 : _b.sid;
|
|
241
245
|
if (participantIdentity && trackSid) {
|
|
242
|
-
this.#publishTranscription(participantIdentity, trackSid, "\u2026", false, ev.itemId);
|
|
246
|
+
await this.#publishTranscription(participantIdentity, trackSid, "\u2026", false, ev.itemId);
|
|
243
247
|
} else {
|
|
244
248
|
this.#logger.error("Participant or track not set");
|
|
245
249
|
}
|
|
246
250
|
});
|
|
247
|
-
this.#session.on("input_speech_transcription_completed", (ev) => {
|
|
251
|
+
this.#session.on("input_speech_transcription_completed", async (ev) => {
|
|
248
252
|
var _a2, _b;
|
|
249
253
|
const transcription = ev.transcript;
|
|
250
254
|
const participantIdentity = (_a2 = this.linkedParticipant) == null ? void 0 : _a2.identity;
|
|
251
255
|
const trackSid = (_b = this.subscribedTrack) == null ? void 0 : _b.sid;
|
|
252
256
|
if (participantIdentity && trackSid) {
|
|
253
|
-
this.#publishTranscription(
|
|
257
|
+
await this.#publishTranscription(
|
|
258
|
+
participantIdentity,
|
|
259
|
+
trackSid,
|
|
260
|
+
transcription,
|
|
261
|
+
true,
|
|
262
|
+
ev.itemId
|
|
263
|
+
);
|
|
254
264
|
} else {
|
|
255
265
|
this.#logger.error("Participant or track not set");
|
|
256
266
|
}
|
|
@@ -261,7 +271,7 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
261
271
|
this.emit("user_speech_committed", userMsg);
|
|
262
272
|
this.#logger.child({ transcription }).debug("committed user speech");
|
|
263
273
|
});
|
|
264
|
-
this.#session.on("input_speech_started", (ev) => {
|
|
274
|
+
this.#session.on("input_speech_started", async (ev) => {
|
|
265
275
|
var _a2, _b;
|
|
266
276
|
this.emit("user_started_speaking");
|
|
267
277
|
if (this.#playingHandle && !this.#playingHandle.done) {
|
|
@@ -276,7 +286,7 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
276
286
|
const participantIdentity = (_a2 = this.linkedParticipant) == null ? void 0 : _a2.identity;
|
|
277
287
|
const trackSid = (_b = this.subscribedTrack) == null ? void 0 : _b.sid;
|
|
278
288
|
if (participantIdentity && trackSid) {
|
|
279
|
-
this.#publishTranscription(participantIdentity, trackSid, "\u2026", false, ev.itemId);
|
|
289
|
+
await this.#publishTranscription(participantIdentity, trackSid, "\u2026", false, ev.itemId);
|
|
280
290
|
}
|
|
281
291
|
});
|
|
282
292
|
this.#session.on("input_speech_stopped", (ev) => {
|
|
@@ -359,7 +369,12 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
359
369
|
};
|
|
360
370
|
this.subscribedTrack = track;
|
|
361
371
|
this.readMicroTask = new Promise((resolve, reject) => {
|
|
362
|
-
|
|
372
|
+
const audioStreamOptions = {
|
|
373
|
+
sampleRate: this.model.sampleRate,
|
|
374
|
+
numChannels: this.model.numChannels,
|
|
375
|
+
...this.#noiseCancellation ? { noiseCancellation: this.#noiseCancellation } : {}
|
|
376
|
+
};
|
|
377
|
+
readAudioStreamTask(new import_rtc_node.AudioStream(track, audioStreamOptions)).then(resolve).catch(reject);
|
|
363
378
|
});
|
|
364
379
|
}
|
|
365
380
|
#getLocalTrackSid() {
|
|
@@ -368,7 +383,7 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
368
383
|
}
|
|
369
384
|
return this.#localTrackSid;
|
|
370
385
|
}
|
|
371
|
-
#publishTranscription(participantIdentity, trackSid, text, isFinal, id) {
|
|
386
|
+
async #publishTranscription(participantIdentity, trackSid, text, isFinal, id) {
|
|
372
387
|
var _a;
|
|
373
388
|
this.#logger.debug(
|
|
374
389
|
`Publishing transcription ${participantIdentity} ${trackSid} ${text} ${isFinal} ${id}`
|
|
@@ -391,6 +406,16 @@ class MultimodalAgent extends import_node_events.EventEmitter {
|
|
|
391
406
|
}
|
|
392
407
|
]
|
|
393
408
|
});
|
|
409
|
+
const stream = await this.room.localParticipant.streamText({
|
|
410
|
+
topic: import_constants.TOPIC_TRANSCRIPTION,
|
|
411
|
+
senderIdentity: participantIdentity,
|
|
412
|
+
attributes: {
|
|
413
|
+
[import_constants.ATTRIBUTE_TRANSCRIPTION_TRACK_ID]: trackSid,
|
|
414
|
+
[import_constants.ATTRIBUTE_TRANSCRIPTION_FINAL]: isFinal.toString()
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
await stream.write(text);
|
|
418
|
+
await stream.close();
|
|
394
419
|
}
|
|
395
420
|
#updateState() {
|
|
396
421
|
let newState = "initializing";
|