@livekit/agents 0.6.4 → 0.7.1

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.
Files changed (184) hide show
  1. package/dist/cli.cjs +8 -0
  2. package/dist/cli.cjs.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +8 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.cjs +6 -1
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/inference_runner.cjs +38 -0
  13. package/dist/inference_runner.cjs.map +1 -0
  14. package/dist/inference_runner.d.ts +11 -0
  15. package/dist/inference_runner.d.ts.map +1 -0
  16. package/dist/inference_runner.js +14 -0
  17. package/dist/inference_runner.js.map +1 -0
  18. package/dist/ipc/index.cjs +23 -0
  19. package/dist/ipc/index.cjs.map +1 -0
  20. package/dist/ipc/index.d.ts +2 -0
  21. package/dist/ipc/index.d.ts.map +1 -0
  22. package/dist/ipc/index.js +2 -0
  23. package/dist/ipc/index.js.map +1 -0
  24. package/dist/ipc/inference_executor.cjs +17 -0
  25. package/dist/ipc/inference_executor.cjs.map +1 -0
  26. package/dist/ipc/inference_executor.d.ts +4 -0
  27. package/dist/ipc/inference_executor.d.ts.map +1 -0
  28. package/dist/ipc/inference_executor.js +1 -0
  29. package/dist/ipc/inference_executor.js.map +1 -0
  30. package/dist/ipc/inference_proc_executor.cjs +97 -0
  31. package/dist/ipc/inference_proc_executor.cjs.map +1 -0
  32. package/dist/ipc/inference_proc_executor.d.ts +23 -0
  33. package/dist/ipc/inference_proc_executor.d.ts.map +1 -0
  34. package/dist/ipc/inference_proc_executor.js +72 -0
  35. package/dist/ipc/inference_proc_executor.js.map +1 -0
  36. package/dist/ipc/inference_proc_lazy_main.cjs +92 -0
  37. package/dist/ipc/inference_proc_lazy_main.cjs.map +1 -0
  38. package/dist/ipc/inference_proc_lazy_main.d.ts +2 -0
  39. package/dist/ipc/inference_proc_lazy_main.d.ts.map +1 -0
  40. package/dist/ipc/inference_proc_lazy_main.js +69 -0
  41. package/dist/ipc/inference_proc_lazy_main.js.map +1 -0
  42. package/dist/ipc/job_executor.cjs +8 -7
  43. package/dist/ipc/job_executor.cjs.map +1 -1
  44. package/dist/ipc/job_executor.d.ts +14 -15
  45. package/dist/ipc/job_executor.d.ts.map +1 -1
  46. package/dist/ipc/job_executor.js +7 -6
  47. package/dist/ipc/job_executor.js.map +1 -1
  48. package/dist/ipc/job_proc_executor.cjs +108 -0
  49. package/dist/ipc/job_proc_executor.cjs.map +1 -0
  50. package/dist/ipc/job_proc_executor.d.ts +19 -0
  51. package/dist/ipc/job_proc_executor.d.ts.map +1 -0
  52. package/dist/ipc/job_proc_executor.js +83 -0
  53. package/dist/ipc/job_proc_executor.js.map +1 -0
  54. package/dist/ipc/{job_main.cjs → job_proc_lazy_main.cjs} +46 -36
  55. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -0
  56. package/dist/ipc/job_proc_lazy_main.d.ts +2 -0
  57. package/dist/ipc/job_proc_lazy_main.d.ts.map +1 -0
  58. package/dist/ipc/{job_main.js → job_proc_lazy_main.js} +46 -11
  59. package/dist/ipc/job_proc_lazy_main.js.map +1 -0
  60. package/dist/ipc/message.cjs.map +1 -1
  61. package/dist/ipc/message.d.ts +17 -0
  62. package/dist/ipc/message.d.ts.map +1 -1
  63. package/dist/ipc/proc_pool.cjs +30 -4
  64. package/dist/ipc/proc_pool.cjs.map +1 -1
  65. package/dist/ipc/proc_pool.d.ts +5 -1
  66. package/dist/ipc/proc_pool.d.ts.map +1 -1
  67. package/dist/ipc/proc_pool.js +30 -4
  68. package/dist/ipc/proc_pool.js.map +1 -1
  69. package/dist/ipc/{proc_job_executor.cjs → supervised_proc.cjs} +57 -45
  70. package/dist/ipc/supervised_proc.cjs.map +1 -0
  71. package/dist/ipc/supervised_proc.d.ts +30 -0
  72. package/dist/ipc/supervised_proc.d.ts.map +1 -0
  73. package/dist/ipc/{proc_job_executor.js → supervised_proc.js} +53 -31
  74. package/dist/ipc/supervised_proc.js.map +1 -0
  75. package/dist/job.cjs +18 -1
  76. package/dist/job.cjs.map +1 -1
  77. package/dist/job.d.ts +9 -1
  78. package/dist/job.d.ts.map +1 -1
  79. package/dist/job.js +17 -1
  80. package/dist/job.js.map +1 -1
  81. package/dist/multimodal/agent_playout.cjs +18 -16
  82. package/dist/multimodal/agent_playout.cjs.map +1 -1
  83. package/dist/multimodal/agent_playout.d.ts +4 -4
  84. package/dist/multimodal/agent_playout.d.ts.map +1 -1
  85. package/dist/multimodal/agent_playout.js +18 -16
  86. package/dist/multimodal/agent_playout.js.map +1 -1
  87. package/dist/multimodal/multimodal_agent.cjs +12 -8
  88. package/dist/multimodal/multimodal_agent.cjs.map +1 -1
  89. package/dist/multimodal/multimodal_agent.d.ts.map +1 -1
  90. package/dist/multimodal/multimodal_agent.js +13 -9
  91. package/dist/multimodal/multimodal_agent.js.map +1 -1
  92. package/dist/pipeline/agent_output.cjs +22 -4
  93. package/dist/pipeline/agent_output.cjs.map +1 -1
  94. package/dist/pipeline/agent_output.d.ts +4 -2
  95. package/dist/pipeline/agent_output.d.ts.map +1 -1
  96. package/dist/pipeline/agent_output.js +22 -4
  97. package/dist/pipeline/agent_output.js.map +1 -1
  98. package/dist/pipeline/agent_playout.cjs +9 -3
  99. package/dist/pipeline/agent_playout.cjs.map +1 -1
  100. package/dist/pipeline/agent_playout.d.ts +4 -2
  101. package/dist/pipeline/agent_playout.d.ts.map +1 -1
  102. package/dist/pipeline/agent_playout.js +9 -3
  103. package/dist/pipeline/agent_playout.js.map +1 -1
  104. package/dist/pipeline/human_input.cjs +6 -0
  105. package/dist/pipeline/human_input.cjs.map +1 -1
  106. package/dist/pipeline/human_input.d.ts +3 -1
  107. package/dist/pipeline/human_input.d.ts.map +1 -1
  108. package/dist/pipeline/human_input.js +6 -0
  109. package/dist/pipeline/human_input.js.map +1 -1
  110. package/dist/pipeline/pipeline_agent.cjs +79 -12
  111. package/dist/pipeline/pipeline_agent.cjs.map +1 -1
  112. package/dist/pipeline/pipeline_agent.d.ts +8 -0
  113. package/dist/pipeline/pipeline_agent.d.ts.map +1 -1
  114. package/dist/pipeline/pipeline_agent.js +79 -12
  115. package/dist/pipeline/pipeline_agent.js.map +1 -1
  116. package/dist/stt/stream_adapter.cjs +16 -4
  117. package/dist/stt/stream_adapter.cjs.map +1 -1
  118. package/dist/stt/stream_adapter.d.ts.map +1 -1
  119. package/dist/stt/stream_adapter.js +16 -4
  120. package/dist/stt/stream_adapter.js.map +1 -1
  121. package/dist/tokenize/basic/basic.cjs +2 -0
  122. package/dist/tokenize/basic/basic.cjs.map +1 -1
  123. package/dist/tokenize/basic/basic.d.ts +2 -0
  124. package/dist/tokenize/basic/basic.d.ts.map +1 -1
  125. package/dist/tokenize/basic/basic.js +1 -0
  126. package/dist/tokenize/basic/basic.js.map +1 -1
  127. package/dist/tokenize/basic/index.cjs +2 -0
  128. package/dist/tokenize/basic/index.cjs.map +1 -1
  129. package/dist/tokenize/basic/index.d.ts +1 -1
  130. package/dist/tokenize/basic/index.d.ts.map +1 -1
  131. package/dist/tokenize/basic/index.js +8 -1
  132. package/dist/tokenize/basic/index.js.map +1 -1
  133. package/dist/tokenize/token_stream.cjs +5 -3
  134. package/dist/tokenize/token_stream.cjs.map +1 -1
  135. package/dist/tokenize/token_stream.d.ts.map +1 -1
  136. package/dist/tokenize/token_stream.js +5 -3
  137. package/dist/tokenize/token_stream.js.map +1 -1
  138. package/dist/transcription.cjs +203 -86
  139. package/dist/transcription.cjs.map +1 -1
  140. package/dist/transcription.d.ts +24 -17
  141. package/dist/transcription.d.ts.map +1 -1
  142. package/dist/transcription.js +201 -85
  143. package/dist/transcription.js.map +1 -1
  144. package/dist/worker.cjs +42 -9
  145. package/dist/worker.cjs.map +1 -1
  146. package/dist/worker.d.ts +5 -1
  147. package/dist/worker.d.ts.map +1 -1
  148. package/dist/worker.js +42 -9
  149. package/dist/worker.js.map +1 -1
  150. package/package.json +3 -3
  151. package/src/cli.ts +9 -0
  152. package/src/index.ts +3 -1
  153. package/src/inference_runner.ts +19 -0
  154. package/src/ipc/index.ts +5 -0
  155. package/src/ipc/inference_executor.ts +7 -0
  156. package/src/ipc/inference_proc_executor.ts +93 -0
  157. package/src/ipc/inference_proc_lazy_main.ts +90 -0
  158. package/src/ipc/job_executor.ts +15 -17
  159. package/src/ipc/job_proc_executor.ts +112 -0
  160. package/src/ipc/{job_main.ts → job_proc_lazy_main.ts} +52 -14
  161. package/src/ipc/message.ts +14 -1
  162. package/src/ipc/proc_pool.ts +33 -3
  163. package/src/ipc/{proc_job_executor.ts → supervised_proc.ts} +77 -29
  164. package/src/job.ts +21 -0
  165. package/src/multimodal/agent_playout.ts +19 -18
  166. package/src/multimodal/multimodal_agent.ts +13 -9
  167. package/src/pipeline/agent_output.ts +36 -5
  168. package/src/pipeline/agent_playout.ts +10 -1
  169. package/src/pipeline/human_input.ts +8 -0
  170. package/src/pipeline/pipeline_agent.ts +96 -11
  171. package/src/stt/stream_adapter.ts +17 -5
  172. package/src/tokenize/basic/basic.ts +2 -0
  173. package/src/tokenize/basic/index.ts +7 -1
  174. package/src/tokenize/token_stream.ts +6 -3
  175. package/src/transcription.ts +270 -96
  176. package/src/worker.ts +42 -5
  177. package/dist/ipc/job_main.cjs.map +0 -1
  178. package/dist/ipc/job_main.d.ts +0 -8
  179. package/dist/ipc/job_main.d.ts.map +0 -1
  180. package/dist/ipc/job_main.js.map +0 -1
  181. package/dist/ipc/proc_job_executor.cjs.map +0 -1
  182. package/dist/ipc/proc_job_executor.d.ts +0 -15
  183. package/dist/ipc/proc_job_executor.d.ts.map +0 -1
  184. package/dist/ipc/proc_job_executor.js.map +0 -1
@@ -44,12 +44,15 @@ export class BufferedTokenStream implements AsyncIterableIterator<TokenData> {
44
44
  if (this.#outBuf) this.#outBuf += ' ';
45
45
 
46
46
  const tok = tokens.shift()!;
47
- let tokText = tok as string;
48
- if (tok.length > 1 && typeof tok[1] === 'number') {
47
+ let tokText: string;
48
+ if (Array.isArray(tok)) {
49
49
  tokText = tok[0];
50
+ } else {
51
+ tokText = tok;
50
52
  }
51
53
 
52
54
  this.#outBuf += tokText;
55
+
53
56
  if (this.#outBuf.length >= this.#minTokenLength) {
54
57
  this.queue.put({ token: this.#outBuf, segmentId: this.#currentSegmentId });
55
58
  this.#outBuf = '';
@@ -76,7 +79,7 @@ export class BufferedTokenStream implements AsyncIterableIterator<TokenData> {
76
79
  if (tokens) {
77
80
  if (this.#outBuf) this.#outBuf += ' ';
78
81
 
79
- if (typeof tokens[0] !== 'string') {
82
+ if (Array.isArray(tokens[0])) {
80
83
  this.#outBuf += tokens.map((tok) => tok[0]).join(' ');
81
84
  } else {
82
85
  this.#outBuf += tokens.join(' ');
@@ -1,134 +1,308 @@
1
1
  // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
- import type { AudioFrame, Room } from '@livekit/rtc-node';
5
- import { log } from './log.js';
6
-
7
- export interface TranscriptionForwarder {
8
- start(): void;
9
- pushAudio(frame: AudioFrame): void;
10
- pushText(text: string): void;
11
- markTextComplete(): void;
12
- markAudioComplete(): void;
13
- close(interrupt: boolean): Promise<void>;
14
- currentCharacterIndex: number;
15
- text: string;
4
+ import { TranscriptionSegment } from '@livekit/protocol';
5
+ import { AudioFrame } from '@livekit/rtc-node';
6
+ import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { EventEmitter } from 'node:events';
9
+ import { basic } from './tokenize/index.js';
10
+ import type { SentenceStream, SentenceTokenizer } from './tokenize/tokenizer.js';
11
+ import { AsyncIterableQueue, Future } from './utils.js';
12
+
13
+ // standard speech rate in hyphens/ms
14
+ const STANDARD_SPEECH_RATE = 3830;
15
+
16
+ export interface TextSyncOptions {
17
+ language: string;
18
+ speed: number;
19
+ newSentenceDelay: number;
20
+ sentenceTokenizer: SentenceTokenizer;
21
+ hyphenateWord: (word: string) => string[];
22
+ splitWords: (words: string) => [string, number, number][];
23
+ }
24
+
25
+ export const defaultTextSyncOptions: TextSyncOptions = {
26
+ language: '',
27
+ speed: 1,
28
+ newSentenceDelay: 400,
29
+ sentenceTokenizer: new basic.SentenceTokenizer(),
30
+ hyphenateWord: basic.hyphenateWord,
31
+ splitWords: basic.splitWords,
32
+ };
33
+
34
+ interface AudioData {
35
+ pushedDuration: number;
36
+ done: boolean;
37
+ }
38
+
39
+ interface TextData {
40
+ sentenceStream: SentenceStream;
41
+ pushedText: string;
42
+ done: boolean;
43
+ forwardedHyphens: number;
44
+ forwardedSentences: number;
16
45
  }
17
46
 
18
- export class BasicTranscriptionForwarder implements TranscriptionForwarder {
19
- #room: Room;
20
- #participantIdentity: string;
21
- #trackSid: string;
22
- #currentText: string = '';
23
- #totalAudioDuration: number = 0;
24
- #currentPlayoutTime: number = 0;
25
- #DEFAULT_CHARS_PER_SECOND = 16;
26
- #charsPerSecond: number = this.#DEFAULT_CHARS_PER_SECOND;
27
- #messageId: string;
28
- #isRunning: boolean = false;
29
- #logger = log();
30
- currentCharacterIndex: number = 0;
31
-
32
- constructor(room: Room, participantIdentity: string, trackSid: string, messageId: string) {
33
- this.#room = room;
34
- this.#participantIdentity = participantIdentity;
35
- this.#trackSid = trackSid;
36
- this.#messageId = messageId;
47
+ type SyncCallbacks = {
48
+ textUpdated: (text: TranscriptionSegment) => void;
49
+ };
50
+
51
+ export class TextAudioSynchronizer extends (EventEmitter as new () => TypedEmitter<SyncCallbacks>) {
52
+ #opts: TextSyncOptions;
53
+ #speed: number;
54
+
55
+ #closed = false;
56
+ #interrupted = false;
57
+ #closeFut = new Future();
58
+
59
+ #playingSegIndex = -1;
60
+ #finishedSegIndex = -1;
61
+
62
+ #textQChanged = new AsyncIterableQueue<number>();
63
+ #textQ: (TextData | undefined)[] = [];
64
+ #audioQChanged = new AsyncIterableQueue<number>();
65
+ #audioQ: (AudioData | undefined)[] = [];
66
+
67
+ #playedText = '';
68
+ #task?: Promise<void>;
69
+
70
+ #audioData?: AudioData;
71
+ #textData?: TextData;
72
+
73
+ constructor(opts: TextSyncOptions) {
74
+ super();
75
+
76
+ this.#opts = opts;
77
+ this.#speed = opts.speed * STANDARD_SPEECH_RATE;
37
78
  }
38
79
 
39
- get text(): string {
40
- return this.#currentText;
80
+ pushAudio(frame: AudioFrame) {
81
+ this.#checkNotClosed();
82
+ if (!this.#audioData) {
83
+ this.#audioData = { pushedDuration: 0, done: false };
84
+ this.#audioQ.push(this.#audioData);
85
+ this.#audioQChanged.put(1);
86
+ }
87
+ this.#audioData.pushedDuration += frame.samplesPerChannel / frame.sampleRate;
41
88
  }
42
89
 
43
- start(): void {
44
- if (!this.#isRunning) {
45
- this.#isRunning = true;
46
- this.#startPublishingLoop().catch((error) => {
47
- this.#logger.error('Error in publishing loop:', error);
48
- this.#isRunning = false;
49
- });
90
+ pushText(text: string) {
91
+ this.#checkNotClosed();
92
+ if (!this.#textData) {
93
+ this.#textData = {
94
+ sentenceStream: this.#opts.sentenceTokenizer.stream(),
95
+ pushedText: '',
96
+ done: false,
97
+ forwardedHyphens: 0,
98
+ forwardedSentences: 0,
99
+ };
100
+ this.#textQ.push(this.#textData);
101
+ this.#textQChanged.put(1);
50
102
  }
103
+
104
+ this.#textData.pushedText += text;
105
+ this.#textData.sentenceStream.pushText(text);
51
106
  }
52
107
 
53
- pushAudio(frame: AudioFrame): void {
54
- this.#totalAudioDuration += frame.samplesPerChannel / frame.sampleRate;
108
+ markAudioSegmentEnd() {
109
+ this.#checkNotClosed();
110
+
111
+ if (!this.#audioData) {
112
+ // create empty audio data if none exists
113
+ this.pushAudio(new AudioFrame(new Int16Array(), 24000, 1, 0));
114
+ }
115
+
116
+ this.#audioData!.done = true;
117
+ this.#audioData = undefined;
55
118
  }
56
119
 
57
- pushText(text: string): void {
58
- this.#currentText += text;
120
+ markTextSegmentEnd() {
121
+ this.#checkNotClosed();
122
+
123
+ if (!this.#textData) {
124
+ this.pushText('');
125
+ }
126
+
127
+ this.#textData!.done = true;
128
+ this.#textData?.sentenceStream.flush();
129
+ this.#textData?.sentenceStream.close();
130
+ this.#textData = undefined;
59
131
  }
60
132
 
61
- #textIsComplete: boolean = false;
62
- #audioIsComplete: boolean = false;
133
+ segmentPlayoutStarted() {
134
+ this.#checkNotClosed();
135
+ this.#playingSegIndex++;
63
136
 
64
- markTextComplete(): void {
65
- this.#textIsComplete = true;
66
- this.#adjustTimingIfBothFinished();
137
+ if (!this.#task) {
138
+ this.#task = this.#mainLoop();
139
+ }
67
140
  }
68
141
 
69
- markAudioComplete(): void {
70
- this.#audioIsComplete = true;
71
- this.#adjustTimingIfBothFinished();
142
+ segmentPlayoutFinished() {
143
+ this.#checkNotClosed();
144
+ this.#finishedSegIndex++;
72
145
  }
73
146
 
74
- #adjustTimingIfBothFinished(): void {
75
- if (this.#textIsComplete && this.#audioIsComplete) {
76
- const actualDuration = this.#totalAudioDuration;
77
- if (actualDuration > 0 && this.#currentText.length > 0) {
78
- this.#charsPerSecond = this.#currentText.length / actualDuration;
79
- }
80
- }
147
+ get playedText(): string {
148
+ return this.#playedText;
81
149
  }
82
150
 
83
- #computeSleepInterval(): number {
84
- return Math.min(Math.max(1 / this.#charsPerSecond, 0.0625), 0.5);
151
+ async close(interrupt: boolean) {
152
+ if (this.#closed) {
153
+ return;
154
+ }
155
+ this.#closed = true;
156
+ this.#interrupted = interrupt;
157
+ this.#closeFut.resolve();
158
+
159
+ for (const textData of this.#textQ) {
160
+ textData?.sentenceStream.close();
161
+ }
162
+
163
+ this.#textQ.push(undefined);
164
+ this.#audioQ.push(undefined);
165
+ this.#textQChanged.put(1);
166
+ this.#audioQChanged.put(1);
167
+
168
+ await this.#task;
85
169
  }
86
170
 
87
- async #startPublishingLoop(): Promise<void> {
88
- this.#isRunning = true;
89
- let sleepInterval = this.#computeSleepInterval();
90
- let isComplete = false;
91
- while (this.#isRunning && !isComplete) {
92
- this.#currentPlayoutTime += sleepInterval;
93
- this.currentCharacterIndex = Math.floor(this.#currentPlayoutTime * this.#charsPerSecond);
94
- isComplete = this.#textIsComplete && this.currentCharacterIndex >= this.#currentText.length;
95
- await this.#publishTranscription(false);
96
- if (this.#isRunning && !isComplete) {
97
- sleepInterval = this.#computeSleepInterval();
98
- await new Promise((resolve) => setTimeout(resolve, sleepInterval * 1000));
171
+ async #mainLoop() {
172
+ let segIndex = 0;
173
+ let qDone = false;
174
+
175
+ while (!qDone) {
176
+ await this.#textQChanged.next();
177
+ await this.#audioQChanged.next();
178
+
179
+ while (this.#textQ.length && this.#audioQ.length) {
180
+ const textData = this.#textQ.pop();
181
+ const audioData = this.#audioQ.pop();
182
+
183
+ if (!(textData && audioData)) {
184
+ qDone = true;
185
+ break;
186
+ }
187
+
188
+ // wait for segment to start playing
189
+ while (!this.#closed) {
190
+ if (this.#playingSegIndex >= segIndex) break;
191
+ await this.#sleepIfNotClosed(125);
192
+ }
193
+
194
+ const sentenceStream = textData.sentenceStream;
195
+ const forwardStartTime = Date.now();
196
+
197
+ for await (const ev of sentenceStream) {
198
+ await this.#syncSentence(segIndex, forwardStartTime, textData, audioData, ev.token);
199
+ }
200
+
201
+ segIndex++;
99
202
  }
100
203
  }
204
+ }
101
205
 
102
- if (this.#isRunning) {
103
- this.close(false);
206
+ async #syncSentence(
207
+ segIndex: number,
208
+ segStartTime: number,
209
+ textData: TextData,
210
+ audioData: AudioData,
211
+ sentence: string,
212
+ ) {
213
+ let realSpeed: number | undefined;
214
+ if (audioData.pushedDuration > 0 && audioData.done) {
215
+ realSpeed = this.#calcHyphens(textData.pushedText).length / audioData.pushedDuration;
104
216
  }
105
- }
106
217
 
107
- async #publishTranscription(final: boolean): Promise<void> {
108
- const textToPublish = this.#currentText.slice(0, this.currentCharacterIndex);
109
- await this.#room.localParticipant?.publishTranscription({
110
- participantIdentity: this.#participantIdentity,
111
- trackSid: this.#trackSid,
112
- segments: [
113
- {
114
- text: textToPublish,
115
- final: final,
116
- id: this.#messageId,
218
+ const segId = 'SG_' + randomUUID();
219
+ const words = this.#opts.splitWords(sentence);
220
+ const processedWords: string[] = [];
221
+
222
+ const ogText = this.#playedText;
223
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
224
+ for (const [word, _, end] of words) {
225
+ if (segIndex <= this.#finishedSegIndex) break;
226
+ if (this.#interrupted) return;
227
+
228
+ const wordHyphens = this.#opts.hyphenateWord(word).length;
229
+ processedWords.push(word);
230
+
231
+ const elapsed = Date.now() - segStartTime;
232
+ const text = sentence.slice(0, end); // TODO: rstrip punctuations
233
+
234
+ let speed = this.#speed;
235
+ let delay: number;
236
+ if (realSpeed) {
237
+ speed = realSpeed;
238
+ const estimatedPausesMs = textData.forwardedSentences * this.#opts.newSentenceDelay;
239
+ const hyphPauses = estimatedPausesMs * speed;
240
+ const targetHyphens = Math.round(speed * elapsed);
241
+ const dt = targetHyphens - textData.forwardedHyphens - hyphPauses;
242
+ const toWaitHyphens = Math.max(0, wordHyphens - dt);
243
+ delay = toWaitHyphens / speed;
244
+ } else {
245
+ delay = wordHyphens / speed;
246
+ }
247
+
248
+ const firstDelay = Math.min(delay / 2, 2 / speed);
249
+ await this.#sleepIfNotClosed(firstDelay * 1000000);
250
+
251
+ this.emit(
252
+ 'textUpdated',
253
+ new TranscriptionSegment({
254
+ id: segId,
255
+ text: text,
117
256
  startTime: BigInt(0),
118
257
  endTime: BigInt(0),
119
- language: '',
120
- },
121
- ],
122
- });
258
+ final: false,
259
+ language: this.#opts.language,
260
+ }),
261
+ );
262
+
263
+ this.#playedText = `${ogText} ${text}`;
264
+ await this.#sleepIfNotClosed((delay - firstDelay) * 1000000);
265
+ textData.forwardedHyphens += wordHyphens;
266
+ }
267
+
268
+ this.emit(
269
+ 'textUpdated',
270
+ new TranscriptionSegment({
271
+ id: segId,
272
+ text: sentence,
273
+ startTime: BigInt(0),
274
+ endTime: BigInt(0),
275
+ final: true,
276
+ language: this.#opts.language,
277
+ }),
278
+ );
279
+
280
+ this.#playedText = `${ogText} ${sentence}`;
281
+
282
+ await this.#sleepIfNotClosed(this.#opts.newSentenceDelay);
283
+ textData.forwardedSentences++;
123
284
  }
124
285
 
125
- async close(interrupt: boolean): Promise<void> {
126
- this.#isRunning = false;
286
+ async #sleepIfNotClosed(delay: number) {
287
+ await Promise.race([
288
+ this.#closeFut.await,
289
+ new Promise((resolve) => setTimeout(resolve, delay)),
290
+ ]);
291
+ }
292
+
293
+ #calcHyphens(text: string): string[] {
294
+ const hyphens: string[] = [];
295
+ const words = this.#opts.splitWords(text);
296
+ for (const word of words) {
297
+ const n = this.#opts.hyphenateWord(word[0]);
298
+ hyphens.push(...n);
299
+ }
300
+ return hyphens;
301
+ }
127
302
 
128
- // Publish whatever we had as final
129
- if (!interrupt) {
130
- this.currentCharacterIndex = this.#currentText.length;
303
+ #checkNotClosed() {
304
+ if (this.#closed) {
305
+ throw new Error('TextAudioSynchronizer is closed');
131
306
  }
132
- await this.#publishTranscription(true);
133
307
  }
134
308
  }
package/src/worker.ts CHANGED
@@ -20,6 +20,8 @@ import { EventEmitter } from 'node:events';
20
20
  import os from 'node:os';
21
21
  import { WebSocket } from 'ws';
22
22
  import { HTTPServer } from './http_server.js';
23
+ import { InferenceRunner } from './inference_runner.js';
24
+ import { InferenceProcExecutor } from './ipc/inference_proc_executor.js';
23
25
  import { ProcPool } from './ipc/proc_pool.js';
24
26
  import type { JobAcceptArguments, JobProcess, RunningJobInfo } from './job.js';
25
27
  import { JobRequest } from './job.js';
@@ -159,6 +161,8 @@ export class WorkerOptions {
159
161
  port: number;
160
162
  logLevel: string;
161
163
  production: boolean;
164
+ jobMemoryWarnMB: number;
165
+ jobMemoryLimitMB: number;
162
166
 
163
167
  /** @param options */
164
168
  constructor({
@@ -180,6 +184,8 @@ export class WorkerOptions {
180
184
  port = undefined,
181
185
  logLevel = 'info',
182
186
  production = false,
187
+ jobMemoryWarnMB = 300,
188
+ jobMemoryLimitMB = 0,
183
189
  }: {
184
190
  /**
185
191
  * Path to a file that has {@link Agent} as a default export, dynamically imported later for
@@ -205,6 +211,8 @@ export class WorkerOptions {
205
211
  port?: number;
206
212
  logLevel?: string;
207
213
  production?: boolean;
214
+ jobMemoryWarnMB?: number;
215
+ jobMemoryLimitMB?: number;
208
216
  }) {
209
217
  this.agent = agent;
210
218
  if (!this.agent) {
@@ -227,6 +235,8 @@ export class WorkerOptions {
227
235
  this.port = port || Default.port(production);
228
236
  this.logLevel = logLevel;
229
237
  this.production = production;
238
+ this.jobMemoryWarnMB = jobMemoryWarnMB;
239
+ this.jobMemoryLimitMB = jobMemoryLimitMB;
230
240
  }
231
241
  }
232
242
 
@@ -264,6 +274,7 @@ export class Worker {
264
274
  #session: WebSocket | undefined = undefined;
265
275
  #httpServer: HTTPServer;
266
276
  #logger = log().child({ version });
277
+ #inferenceExecutor?: InferenceProcExecutor;
267
278
 
268
279
  /* @throws {@link MissingCredentialsError} if URL, API key or API secret are missing */
269
280
  constructor(opts: WorkerOptions) {
@@ -284,11 +295,27 @@ export class Worker {
284
295
  'API Secret is required: Set LIVEKIT_API_SECRET, run with --api-secret, or pass apiSecret in WorkerOptions',
285
296
  );
286
297
 
298
+ if (Object.entries(InferenceRunner.registeredRunners).length) {
299
+ this.#inferenceExecutor = new InferenceProcExecutor({
300
+ runners: InferenceRunner.registeredRunners,
301
+ initializeTimeout: 30000,
302
+ closeTimeout: 5000,
303
+ memoryWarnMB: 2000,
304
+ memoryLimitMB: 0,
305
+ pingInterval: 5000,
306
+ pingTimeout: 60000,
307
+ highPingThreshold: 2500,
308
+ });
309
+ }
310
+
287
311
  this.#procPool = new ProcPool(
288
312
  opts.agent,
289
313
  opts.numIdleProcesses,
290
314
  opts.initializeProcessTimeout,
291
315
  opts.shutdownProcessTimeout,
316
+ this.#inferenceExecutor,
317
+ opts.jobMemoryWarnMB,
318
+ opts.jobMemoryLimitMB,
292
319
  );
293
320
 
294
321
  this.#opts = opts;
@@ -301,6 +328,11 @@ export class Worker {
301
328
  throw new WorkerError('worker is already running');
302
329
  }
303
330
 
331
+ if (this.#inferenceExecutor) {
332
+ await this.#inferenceExecutor.start();
333
+ await this.#inferenceExecutor.initialize();
334
+ }
335
+
304
336
  this.#logger.info('starting worker');
305
337
  this.#closed = false;
306
338
  this.#procPool.start();
@@ -400,12 +432,16 @@ export class Worker {
400
432
  );
401
433
  };
402
434
 
403
- const timer = setTimeout(() => {
404
- throw new WorkerError('timed out draining');
405
- }, timeout);
406
- if (timeout === undefined) clearTimeout(timer);
435
+ let timer: NodeJS.Timeout | undefined;
436
+ if (timeout) {
437
+ timer = setTimeout(() => {
438
+ throw new WorkerError('timed out draining');
439
+ }, timeout);
440
+ }
407
441
  await joinJobs().then(() => {
408
- clearTimeout(timer);
442
+ if (timeout) {
443
+ clearTimeout(timer);
444
+ }
409
445
  });
410
446
  }
411
447
 
@@ -678,6 +714,7 @@ export class Worker {
678
714
 
679
715
  this.#closed = true;
680
716
 
717
+ await this.#inferenceExecutor?.close();
681
718
  await this.#procPool.close();
682
719
  await this.#httpServer.close();
683
720
  await Promise.allSettled(this.#tasks);
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/ipc/job_main.ts","../../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.43.7_@types+node@22.5.5__postcss@8.4.38_tsx@4.19.2_typescript@5.4.5/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { Room, RoomEvent } from '@livekit/rtc-node';\nimport type { ChildProcess } from 'node:child_process';\nimport { fork } from 'node:child_process';\nimport { EventEmitter, once } from 'node:events';\nimport { pathToFileURL } from 'node:url';\nimport type { Logger } from 'pino';\nimport { type Agent, isAgent } from '../generator.js';\nimport type { RunningJobInfo } from '../job.js';\nimport { JobContext } from '../job.js';\nimport { JobProcess } from '../job.js';\nimport { initializeLogger, log } from '../log.js';\nimport { defaultInitializeProcessFunc } from '../worker.js';\nimport type { IPCMessage } from './message.js';\n\nconst ORPHANED_TIMEOUT = 15 * 1000;\n\ntype StartArgs = {\n agentFile: string;\n // userArguments: unknown;\n};\n\ntype JobTask = {\n ctx: JobContext;\n task: Promise<void>;\n};\n\nexport const runProcess = (args: StartArgs): ChildProcess => {\n return fork(new URL(import.meta.url), [args.agentFile]);\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);\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 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\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 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('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","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":["import_job","module","agent"]}
@@ -1,8 +0,0 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- import type { ChildProcess } from 'node:child_process';
3
- type StartArgs = {
4
- agentFile: string;
5
- };
6
- export declare const runProcess: (args: StartArgs) => ChildProcess;
7
- export {};
8
- //# sourceMappingURL=job_main.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"job_main.d.ts","sourceRoot":"","sources":["../../src/ipc/job_main.ts"],"names":[],"mappings":";AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAevD,KAAK,SAAS,GAAG;IACf,SAAS,EAAE,MAAM,CAAC;CAEnB,CAAC;AAOF,eAAO,MAAM,UAAU,SAAU,SAAS,KAAG,YAE5C,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/ipc/job_main.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { Room, RoomEvent } from '@livekit/rtc-node';\nimport type { ChildProcess } from 'node:child_process';\nimport { fork } from 'node:child_process';\nimport { EventEmitter, once } from 'node:events';\nimport { pathToFileURL } from 'node:url';\nimport type { Logger } from 'pino';\nimport { type Agent, isAgent } from '../generator.js';\nimport type { RunningJobInfo } from '../job.js';\nimport { JobContext } from '../job.js';\nimport { JobProcess } from '../job.js';\nimport { initializeLogger, log } from '../log.js';\nimport { defaultInitializeProcessFunc } from '../worker.js';\nimport type { IPCMessage } from './message.js';\n\nconst ORPHANED_TIMEOUT = 15 * 1000;\n\ntype StartArgs = {\n agentFile: string;\n // userArguments: unknown;\n};\n\ntype JobTask = {\n ctx: JobContext;\n task: Promise<void>;\n};\n\nexport const runProcess = (args: StartArgs): ChildProcess => {\n return fork(new URL(import.meta.url), [args.agentFile]);\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);\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 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\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 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('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 +0,0 @@
1
- {"version":3,"sources":["../../src/ipc/proc_job_executor.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 { ProcOpts } from './job_executor.js';\nimport { JobExecutor } from './job_executor.js';\nimport type { IPCMessage } from './message.js';\n\nexport class ProcJobExecutor extends JobExecutor {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n #proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n #init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(agent: string, initializeTimeout: number, closeTimeout: number) {\n super();\n this.#opts = {\n agent,\n initializeTimeout,\n closeTimeout,\n };\n }\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 = await import('./job_main.js').then((m) =>\n m.runProcess({\n agentFile: this.#opts.agent,\n }),\n );\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.PING_INTERVAL);\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.PING_TIMEOUT);\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.HIGH_PING_THRESHOLD) {\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 this.#join.resolve();\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 this.#join.resolve();\n });\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({ case: 'initializeRequest', value: { loggerOptions } });\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 if (!this.#runningJob) {\n this.#proc!.kill();\n this.#join.resolve();\n }\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.#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,15 +0,0 @@
1
- import type { RunningJobInfo } from '../job.js';
2
- import { JobExecutor } from './job_executor.js';
3
- export declare class ProcJobExecutor extends JobExecutor {
4
- #private;
5
- constructor(agent: string, initializeTimeout: number, closeTimeout: number);
6
- get started(): boolean;
7
- get runningJob(): RunningJobInfo | undefined;
8
- start(): Promise<void>;
9
- run(): Promise<void>;
10
- join(): Promise<void>;
11
- initialize(): Promise<void>;
12
- close(): Promise<void>;
13
- launchJob(info: RunningJobInfo): Promise<void>;
14
- }
15
- //# sourceMappingURL=proc_job_executor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"proc_job_executor.d.ts","sourceRoot":"","sources":["../../src/ipc/proc_job_executor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,qBAAa,eAAgB,SAAQ,WAAW;;gBAYlC,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;IAS1E,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,cAAc,GAAG,SAAS,CAE3C;IAEK,KAAK;IAiBL,GAAG;IAmDH,IAAI;IAQJ,UAAU;IAgBV,KAAK;IAuBL,SAAS,CAAC,IAAI,EAAE,cAAc;CAOrC"}