@getpaseo/server 0.1.84 → 0.1.86

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 (60) hide show
  1. package/dist/scripts/supervisor-entrypoint.js +1 -0
  2. package/dist/server/server/agent/agent-manager.d.ts +4 -0
  3. package/dist/server/server/agent/agent-manager.js +23 -0
  4. package/dist/server/server/agent/agent-metadata-generator.d.ts +9 -0
  5. package/dist/server/server/agent/agent-metadata-generator.js +11 -2
  6. package/dist/server/server/agent/agent-response-loop.d.ts +1 -1
  7. package/dist/server/server/agent/agent-response-loop.js +3 -13
  8. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  9. package/dist/server/server/agent/create-agent/create.js +7 -0
  10. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +0 -1
  11. package/dist/server/server/agent/import-sessions.d.ts +3 -0
  12. package/dist/server/server/agent/import-sessions.js +11 -0
  13. package/dist/server/server/agent/mcp-server.d.ts +0 -1
  14. package/dist/server/server/agent/mcp-server.js +0 -4
  15. package/dist/server/server/agent/providers/claude/agent.d.ts +2 -1
  16. package/dist/server/server/agent/providers/claude/agent.js +70 -0
  17. package/dist/server/server/agent/providers/claude/feature-definitions.d.ts +8 -0
  18. package/dist/server/server/agent/providers/claude/feature-definitions.js +36 -0
  19. package/dist/server/server/agent/providers/claude/models.js +19 -5
  20. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +6 -3
  21. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +5 -0
  22. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +62 -0
  23. package/dist/server/server/agent/structured-generation-providers.d.ts +29 -0
  24. package/dist/server/server/agent/structured-generation-providers.js +192 -0
  25. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +0 -2
  26. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +0 -1
  27. package/dist/server/server/bootstrap.d.ts +7 -0
  28. package/dist/server/server/bootstrap.js +11 -2
  29. package/dist/server/server/config.js +1 -0
  30. package/dist/server/server/daemon-config-store.js +46 -6
  31. package/dist/server/server/daemon-worker.js +1 -0
  32. package/dist/server/server/file-explorer/service.js +4 -4
  33. package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -6
  34. package/dist/server/server/paseo-worktree-archive-service.js +13 -33
  35. package/dist/server/server/persisted-config.d.ts +77 -22
  36. package/dist/server/server/persisted-config.js +13 -0
  37. package/dist/server/server/schedule/service.d.ts +1 -0
  38. package/dist/server/server/schedule/service.js +15 -0
  39. package/dist/server/server/session.d.ts +3 -2
  40. package/dist/server/server/session.js +77 -25
  41. package/dist/server/server/speech/providers/local/runtime.js +52 -133
  42. package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +9 -2
  43. package/dist/server/server/speech/providers/local/sherpa/model-catalog.js +7 -0
  44. package/dist/server/server/speech/providers/local/worker-bytes.d.ts +4 -0
  45. package/dist/server/server/speech/providers/local/worker-bytes.js +9 -0
  46. package/dist/server/server/speech/providers/local/worker-client.d.ts +80 -0
  47. package/dist/server/server/speech/providers/local/worker-client.js +438 -0
  48. package/dist/server/server/speech/providers/local/worker-process.d.ts +2 -0
  49. package/dist/server/server/speech/providers/local/worker-process.js +270 -0
  50. package/dist/server/server/speech/providers/local/worker-protocol.d.ts +95 -0
  51. package/dist/server/server/speech/providers/local/worker-protocol.js +2 -0
  52. package/dist/server/server/websocket-server.js +2 -0
  53. package/dist/server/server/worktree-branch-name-generator.d.ts +9 -0
  54. package/dist/server/server/worktree-branch-name-generator.js +11 -2
  55. package/dist/server/utils/worktree.d.ts +1 -1
  56. package/dist/server/utils/worktree.js +2 -2
  57. package/dist/src/server/persisted-config.js +13 -0
  58. package/package.json +5 -10
  59. package/dist/server/utils/branch-slug.d.ts +0 -14
  60. package/dist/server/utils/branch-slug.js +0 -49
@@ -0,0 +1,438 @@
1
+ import { fork } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { EventEmitter } from "node:events";
4
+ import { Readable } from "node:stream";
5
+ import { fileURLToPath } from "node:url";
6
+ import { applySherpaLoaderEnv } from "./sherpa/sherpa-runtime-env.js";
7
+ import { bufferToWorkerBytes, workerBytesToBuffer } from "./worker-bytes.js";
8
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
9
+ const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
10
+ const DEFAULT_LOCAL_SAMPLE_RATE = 16000;
11
+ function resolveWorkerUrl() {
12
+ const currentUrl = import.meta.url;
13
+ if (currentUrl.endsWith(".ts")) {
14
+ return new URL("./worker-process.ts", currentUrl);
15
+ }
16
+ return new URL("./worker-process.js", currentUrl);
17
+ }
18
+ function resolveWorkerExecArgv() {
19
+ if (!import.meta.url.endsWith(".ts")) {
20
+ return [];
21
+ }
22
+ const loaderUrl = new URL("../../../../terminal/terminal-ts-loader.mjs", import.meta.url).href;
23
+ const importSource = [
24
+ 'import { register } from "node:module";',
25
+ 'import { pathToFileURL } from "node:url";',
26
+ `register(${JSON.stringify(loaderUrl)}, pathToFileURL("./"));`,
27
+ ].join(" ");
28
+ return [
29
+ "--experimental-strip-types",
30
+ "--import",
31
+ `data:text/javascript,${encodeURIComponent(importSource)}`,
32
+ ];
33
+ }
34
+ function forkLocalSpeechWorker() {
35
+ const env = { ...process.env };
36
+ applySherpaLoaderEnv(env);
37
+ return fork(fileURLToPath(resolveWorkerUrl()), [], {
38
+ env,
39
+ execArgv: resolveWorkerExecArgv(),
40
+ serialization: "advanced",
41
+ stdio: ["ignore", "ignore", "inherit", "ipc"],
42
+ });
43
+ }
44
+ function isResponse(message) {
45
+ return message.type === "response";
46
+ }
47
+ export class LocalSpeechWorkerClient {
48
+ constructor(options) {
49
+ this.pendingRequests = new Map();
50
+ this.activeSessionIds = new Set();
51
+ this.sessionEmitters = new Map();
52
+ this.worker = null;
53
+ this.inFlightRequests = 0;
54
+ this.idleTimer = null;
55
+ this.config = options.config;
56
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
57
+ this.idleTtlMs = options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
58
+ this.forkWorker = options.forkWorker ?? forkLocalSpeechWorker;
59
+ }
60
+ async synthesizeSpeech(text) {
61
+ const result = await this.sendRequest({
62
+ type: "tts.synthesize",
63
+ config: this.config,
64
+ text,
65
+ });
66
+ return {
67
+ stream: Readable.from([workerBytesToBuffer(result.audio)]),
68
+ format: result.format,
69
+ };
70
+ }
71
+ transcribeVoice(audio, format) {
72
+ return this.sendRequest({
73
+ type: "stt.transcribe",
74
+ config: this.config,
75
+ model: "voice",
76
+ audio: bufferToWorkerBytes(audio),
77
+ format,
78
+ });
79
+ }
80
+ async createSession(kind, emitter) {
81
+ const sessionId = randomUUID();
82
+ this.activeSessionIds.add(sessionId);
83
+ this.sessionEmitters.set(sessionId, emitter);
84
+ try {
85
+ const result = await this.sendRequest({
86
+ type: "session.create",
87
+ config: this.config,
88
+ sessionId,
89
+ kind,
90
+ });
91
+ return { sessionId, requiredSampleRate: result.requiredSampleRate };
92
+ }
93
+ catch (err) {
94
+ this.activeSessionIds.delete(sessionId);
95
+ this.sessionEmitters.delete(sessionId);
96
+ this.scheduleIdleShutdownIfReady();
97
+ throw err;
98
+ }
99
+ }
100
+ appendSessionAudio(sessionId, audio) {
101
+ void this.sendRequest({
102
+ type: "session.append",
103
+ sessionId,
104
+ audio: bufferToWorkerBytes(audio),
105
+ }).catch((err) => {
106
+ this.emitSessionError(sessionId, err);
107
+ });
108
+ }
109
+ commitSession(sessionId) {
110
+ void this.sendRequest({ type: "session.commit", sessionId }).catch((err) => {
111
+ this.emitSessionError(sessionId, err);
112
+ });
113
+ }
114
+ clearSession(sessionId) {
115
+ void this.sendRequest({ type: "session.clear", sessionId }).catch((err) => {
116
+ this.emitSessionError(sessionId, err);
117
+ });
118
+ }
119
+ flushSession(sessionId) {
120
+ void this.sendRequest({ type: "session.flush", sessionId }).catch((err) => {
121
+ this.emitSessionError(sessionId, err);
122
+ });
123
+ }
124
+ resetSession(sessionId) {
125
+ void this.sendRequest({ type: "session.reset", sessionId }).catch((err) => {
126
+ this.emitSessionError(sessionId, err);
127
+ });
128
+ }
129
+ closeSession(sessionId) {
130
+ this.activeSessionIds.delete(sessionId);
131
+ this.sessionEmitters.delete(sessionId);
132
+ void this.sendRequest({ type: "session.close", sessionId }).catch(() => {
133
+ // Closing is best-effort; the parent already dropped the session.
134
+ });
135
+ this.scheduleIdleShutdownIfReady();
136
+ }
137
+ shutdown() {
138
+ this.clearIdleTimer();
139
+ this.rejectAllPending(new Error("Local speech worker shut down"));
140
+ this.activeSessionIds.clear();
141
+ this.sessionEmitters.clear();
142
+ const worker = this.worker;
143
+ this.worker = null;
144
+ if (worker && !worker.killed) {
145
+ try {
146
+ worker.disconnect();
147
+ }
148
+ catch {
149
+ // ignore
150
+ }
151
+ try {
152
+ worker.kill();
153
+ }
154
+ catch {
155
+ // ignore
156
+ }
157
+ }
158
+ }
159
+ sendRequest(input) {
160
+ const worker = this.ensureWorker();
161
+ const requestId = randomUUID();
162
+ const message = { ...input, requestId };
163
+ this.inFlightRequests++;
164
+ this.clearIdleTimer();
165
+ return new Promise((resolve, reject) => {
166
+ const timeout = setTimeout(() => {
167
+ this.pendingRequests.delete(requestId);
168
+ this.inFlightRequests = Math.max(0, this.inFlightRequests - 1);
169
+ this.scheduleIdleShutdownIfReady();
170
+ reject(new Error(`Local speech worker request timed out: ${input.type}`));
171
+ }, this.requestTimeoutMs);
172
+ this.pendingRequests.set(requestId, {
173
+ resolve: (value) => resolve(value),
174
+ reject,
175
+ timeout,
176
+ });
177
+ const sent = worker.send(message, (error) => {
178
+ if (!error) {
179
+ return;
180
+ }
181
+ const pending = this.pendingRequests.get(requestId);
182
+ if (!pending) {
183
+ return;
184
+ }
185
+ clearTimeout(pending.timeout);
186
+ this.pendingRequests.delete(requestId);
187
+ this.inFlightRequests = Math.max(0, this.inFlightRequests - 1);
188
+ this.scheduleIdleShutdownIfReady();
189
+ pending.reject(error);
190
+ });
191
+ if (!sent) {
192
+ const pending = this.pendingRequests.get(requestId);
193
+ if (pending) {
194
+ clearTimeout(pending.timeout);
195
+ this.pendingRequests.delete(requestId);
196
+ this.inFlightRequests = Math.max(0, this.inFlightRequests - 1);
197
+ this.scheduleIdleShutdownIfReady();
198
+ pending.reject(new Error("Local speech worker IPC channel is not writable"));
199
+ }
200
+ }
201
+ });
202
+ }
203
+ ensureWorker() {
204
+ if (this.worker && !this.worker.killed && this.worker.connected) {
205
+ return this.worker;
206
+ }
207
+ const worker = this.forkWorker();
208
+ this.worker = worker;
209
+ worker.on("message", (message) => this.handleWorkerMessage(message));
210
+ worker.on("exit", () => this.handleWorkerExit());
211
+ return worker;
212
+ }
213
+ handleWorkerMessage(message) {
214
+ if (isResponse(message)) {
215
+ const pending = this.pendingRequests.get(message.requestId);
216
+ if (!pending) {
217
+ return;
218
+ }
219
+ clearTimeout(pending.timeout);
220
+ this.pendingRequests.delete(message.requestId);
221
+ this.inFlightRequests = Math.max(0, this.inFlightRequests - 1);
222
+ this.scheduleIdleShutdownIfReady();
223
+ if (message.ok) {
224
+ pending.resolve(message.result);
225
+ }
226
+ else {
227
+ pending.reject(new Error(message.error));
228
+ }
229
+ return;
230
+ }
231
+ const emitter = this.sessionEmitters.get(message.sessionId);
232
+ if (!emitter) {
233
+ return;
234
+ }
235
+ switch (message.type) {
236
+ case "session.committed":
237
+ emitter.emit("committed", message.payload);
238
+ return;
239
+ case "session.transcript":
240
+ emitter.emit("transcript", message.payload);
241
+ return;
242
+ case "session.speech_started":
243
+ emitter.emit("speech_started");
244
+ return;
245
+ case "session.speech_stopped":
246
+ emitter.emit("speech_stopped");
247
+ return;
248
+ case "session.error":
249
+ emitter.emit("error", new Error(message.error));
250
+ return;
251
+ }
252
+ }
253
+ handleWorkerExit() {
254
+ this.worker = null;
255
+ this.clearIdleTimer();
256
+ this.rejectAllPending(new Error("Local speech worker exited"));
257
+ for (const [sessionId, emitter] of this.sessionEmitters) {
258
+ if (this.activeSessionIds.has(sessionId)) {
259
+ emitter.emit("error", new Error("Local speech worker exited"));
260
+ }
261
+ }
262
+ this.activeSessionIds.clear();
263
+ this.sessionEmitters.clear();
264
+ this.inFlightRequests = 0;
265
+ }
266
+ rejectAllPending(error) {
267
+ for (const [requestId, pending] of this.pendingRequests) {
268
+ clearTimeout(pending.timeout);
269
+ pending.reject(error);
270
+ this.pendingRequests.delete(requestId);
271
+ }
272
+ }
273
+ emitSessionError(sessionId, error) {
274
+ const emitter = this.sessionEmitters.get(sessionId);
275
+ if (!emitter) {
276
+ return;
277
+ }
278
+ emitter.emit("error", error instanceof Error ? error : new Error(String(error)));
279
+ }
280
+ scheduleIdleShutdownIfReady() {
281
+ if (!this.worker || this.inFlightRequests > 0 || this.activeSessionIds.size > 0) {
282
+ return;
283
+ }
284
+ this.clearIdleTimer();
285
+ this.idleTimer = setTimeout(() => {
286
+ if (this.inFlightRequests === 0 && this.activeSessionIds.size === 0) {
287
+ this.shutdown();
288
+ }
289
+ }, this.idleTtlMs);
290
+ }
291
+ clearIdleTimer() {
292
+ if (this.idleTimer) {
293
+ clearTimeout(this.idleTimer);
294
+ this.idleTimer = null;
295
+ }
296
+ }
297
+ }
298
+ export class WorkerBackedTextToSpeechProvider {
299
+ constructor(client) {
300
+ this.client = client;
301
+ }
302
+ synthesizeSpeech(text) {
303
+ return this.client.synthesizeSpeech(text);
304
+ }
305
+ }
306
+ export class WorkerBackedSpeechToTextProvider {
307
+ constructor(client, kind) {
308
+ this.client = client;
309
+ this.kind = kind;
310
+ this.id = "local";
311
+ }
312
+ createSession(_params) {
313
+ return new WorkerBackedTranscriptionSession(this.client, this.kind);
314
+ }
315
+ }
316
+ export class WorkerBackedTurnDetectionProvider {
317
+ constructor(client) {
318
+ this.client = client;
319
+ this.id = "local";
320
+ }
321
+ createSession(_params) {
322
+ return new WorkerBackedTurnDetectionSession(this.client);
323
+ }
324
+ }
325
+ class WorkerBackedTranscriptionSession extends EventEmitter {
326
+ constructor(client, kind) {
327
+ super();
328
+ this.client = client;
329
+ this.kind = kind;
330
+ this.requiredSampleRate = DEFAULT_LOCAL_SAMPLE_RATE;
331
+ this.connectedSessionId = null;
332
+ this.connecting = null;
333
+ }
334
+ async connect() {
335
+ if (this.connectedSessionId) {
336
+ return;
337
+ }
338
+ if (!this.connecting) {
339
+ this.connecting = this.connectRemoteSession();
340
+ }
341
+ await this.connecting;
342
+ }
343
+ async connectRemoteSession() {
344
+ try {
345
+ const result = await this.client.createSession(this.kind, this);
346
+ this.connectedSessionId = result.sessionId;
347
+ this.requiredSampleRate = result.requiredSampleRate;
348
+ }
349
+ finally {
350
+ this.connecting = null;
351
+ }
352
+ }
353
+ appendPcm16(pcm16le) {
354
+ const sessionId = this.connectedSessionId;
355
+ if (!sessionId) {
356
+ this.emit("error", new Error("Local STT session not connected"));
357
+ return;
358
+ }
359
+ this.client.appendSessionAudio(sessionId, pcm16le);
360
+ }
361
+ commit() {
362
+ const sessionId = this.connectedSessionId;
363
+ if (!sessionId) {
364
+ this.emit("error", new Error("Local STT session not connected"));
365
+ return;
366
+ }
367
+ this.client.commitSession(sessionId);
368
+ }
369
+ clear() {
370
+ const sessionId = this.connectedSessionId;
371
+ if (sessionId) {
372
+ this.client.clearSession(sessionId);
373
+ }
374
+ }
375
+ close() {
376
+ const sessionId = this.connectedSessionId;
377
+ this.connectedSessionId = null;
378
+ if (sessionId) {
379
+ this.client.closeSession(sessionId);
380
+ }
381
+ }
382
+ }
383
+ class WorkerBackedTurnDetectionSession extends EventEmitter {
384
+ constructor(client) {
385
+ super();
386
+ this.client = client;
387
+ this.requiredSampleRate = DEFAULT_LOCAL_SAMPLE_RATE;
388
+ this.connectedSessionId = null;
389
+ this.connecting = null;
390
+ }
391
+ async connect() {
392
+ if (this.connectedSessionId) {
393
+ return;
394
+ }
395
+ if (!this.connecting) {
396
+ this.connecting = this.connectRemoteSession();
397
+ }
398
+ await this.connecting;
399
+ }
400
+ async connectRemoteSession() {
401
+ try {
402
+ const result = await this.client.createSession("vad", this);
403
+ this.connectedSessionId = result.sessionId;
404
+ this.requiredSampleRate = result.requiredSampleRate;
405
+ }
406
+ finally {
407
+ this.connecting = null;
408
+ }
409
+ }
410
+ appendPcm16(pcm16le) {
411
+ const sessionId = this.connectedSessionId;
412
+ if (!sessionId) {
413
+ this.emit("error", new Error("Local turn-detection session not connected"));
414
+ return;
415
+ }
416
+ this.client.appendSessionAudio(sessionId, pcm16le);
417
+ }
418
+ flush() {
419
+ const sessionId = this.connectedSessionId;
420
+ if (sessionId) {
421
+ this.client.flushSession(sessionId);
422
+ }
423
+ }
424
+ reset() {
425
+ const sessionId = this.connectedSessionId;
426
+ if (sessionId) {
427
+ this.client.resetSession(sessionId);
428
+ }
429
+ }
430
+ close() {
431
+ const sessionId = this.connectedSessionId;
432
+ this.connectedSessionId = null;
433
+ if (sessionId) {
434
+ this.client.closeSession(sessionId);
435
+ }
436
+ }
437
+ }
438
+ //# sourceMappingURL=worker-client.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-process.d.ts.map
@@ -0,0 +1,270 @@
1
+ import pino from "pino";
2
+ import { getLocalSpeechModelDir } from "./models.js";
3
+ import { SherpaOfflineRecognizerEngine } from "./sherpa/sherpa-offline-recognizer.js";
4
+ import { SherpaOnnxParakeetSTT } from "./sherpa/sherpa-parakeet-stt.js";
5
+ import { SherpaParakeetRealtimeTranscriptionSession } from "./sherpa/sherpa-parakeet-realtime-session.js";
6
+ import { SherpaOnnxTTS } from "./sherpa/sherpa-tts.js";
7
+ import { ensureSileroVadModel, SherpaSileroTurnDetectionProvider, } from "./sherpa/silero-vad-provider.js";
8
+ import { bufferToWorkerBytes, workerBytesToBuffer } from "./worker-bytes.js";
9
+ process.title = "Paseo Voice";
10
+ const logger = pino({
11
+ level: process.env.PASEO_LOG_LEVEL ?? "info",
12
+ }).child({ module: "speech", component: "local-worker" });
13
+ const sttEngines = new Map();
14
+ const sttProviders = new Map();
15
+ const ttsProviders = new Map();
16
+ const sessions = new Map();
17
+ const unsubscribeBySessionId = new Map();
18
+ let ipcClosing = false;
19
+ function sendToParent(message) {
20
+ if (ipcClosing || !process.connected || !process.send) {
21
+ return;
22
+ }
23
+ try {
24
+ process.send(message, (error) => {
25
+ if (error) {
26
+ ipcClosing = true;
27
+ }
28
+ });
29
+ }
30
+ catch {
31
+ ipcClosing = true;
32
+ }
33
+ }
34
+ function sttModelId(config, model) {
35
+ return (model === "voice" ? config.voiceSttModel : config.dictationSttModel);
36
+ }
37
+ function ttsModelId(config) {
38
+ return config.voiceTtsModel;
39
+ }
40
+ function sttEngineKey(config, modelId) {
41
+ return `${config.modelsDir}:${modelId}`;
42
+ }
43
+ function ttsKey(config) {
44
+ return [
45
+ config.modelsDir,
46
+ config.voiceTtsModel,
47
+ config.voiceTtsSpeakerId ?? 0,
48
+ config.voiceTtsSpeed ?? 1,
49
+ ].join(":");
50
+ }
51
+ function getSttEngine(config, model) {
52
+ const modelId = sttModelId(config, model);
53
+ const key = sttEngineKey(config, modelId);
54
+ const existing = sttEngines.get(key);
55
+ if (existing) {
56
+ return existing;
57
+ }
58
+ const modelDir = getLocalSpeechModelDir(config.modelsDir, modelId);
59
+ const created = new SherpaOfflineRecognizerEngine({
60
+ model: {
61
+ kind: "nemo_transducer",
62
+ encoder: `${modelDir}/encoder.int8.onnx`,
63
+ decoder: `${modelDir}/decoder.int8.onnx`,
64
+ joiner: `${modelDir}/joiner.int8.onnx`,
65
+ tokens: `${modelDir}/tokens.txt`,
66
+ },
67
+ numThreads: 2,
68
+ debug: 0,
69
+ }, logger);
70
+ sttEngines.set(key, created);
71
+ return created;
72
+ }
73
+ function getSttProvider(config, model) {
74
+ const modelId = sttModelId(config, model);
75
+ const key = sttEngineKey(config, modelId);
76
+ const existing = sttProviders.get(key);
77
+ if (existing) {
78
+ return existing;
79
+ }
80
+ const created = new SherpaOnnxParakeetSTT({ engine: getSttEngine(config, model) }, logger);
81
+ sttProviders.set(key, created);
82
+ return created;
83
+ }
84
+ function getTtsProvider(config) {
85
+ const key = ttsKey(config);
86
+ const existing = ttsProviders.get(key);
87
+ if (existing) {
88
+ return existing;
89
+ }
90
+ const modelDir = getLocalSpeechModelDir(config.modelsDir, ttsModelId(config));
91
+ const created = new SherpaOnnxTTS({
92
+ preset: ttsModelId(config),
93
+ modelDir,
94
+ speakerId: config.voiceTtsSpeakerId,
95
+ speed: config.voiceTtsSpeed,
96
+ }, logger);
97
+ ttsProviders.set(key, created);
98
+ return created;
99
+ }
100
+ function cleanupSession(sessionId) {
101
+ const unsubscribe = unsubscribeBySessionId.get(sessionId);
102
+ if (unsubscribe) {
103
+ for (const fn of unsubscribe) {
104
+ try {
105
+ fn();
106
+ }
107
+ catch {
108
+ // ignore
109
+ }
110
+ }
111
+ }
112
+ unsubscribeBySessionId.delete(sessionId);
113
+ const session = sessions.get(sessionId);
114
+ sessions.delete(sessionId);
115
+ try {
116
+ session?.close();
117
+ }
118
+ catch {
119
+ // ignore
120
+ }
121
+ }
122
+ function trackTranscriptionSession(sessionId, session) {
123
+ session.on("committed", (payload) => {
124
+ sendToParent({ type: "session.committed", sessionId, payload });
125
+ });
126
+ session.on("transcript", (payload) => {
127
+ sendToParent({ type: "session.transcript", sessionId, payload });
128
+ });
129
+ session.on("error", (err) => {
130
+ sendToParent({
131
+ type: "session.error",
132
+ sessionId,
133
+ error: err instanceof Error ? err.message : String(err),
134
+ });
135
+ });
136
+ unsubscribeBySessionId.set(sessionId, []);
137
+ }
138
+ function trackTurnDetectionSession(sessionId, session) {
139
+ session.on("speech_started", () => {
140
+ sendToParent({ type: "session.speech_started", sessionId });
141
+ });
142
+ session.on("speech_stopped", () => {
143
+ sendToParent({ type: "session.speech_stopped", sessionId });
144
+ });
145
+ session.on("error", (err) => {
146
+ sendToParent({
147
+ type: "session.error",
148
+ sessionId,
149
+ error: err instanceof Error ? err.message : String(err),
150
+ });
151
+ });
152
+ unsubscribeBySessionId.set(sessionId, []);
153
+ }
154
+ async function createSession(message) {
155
+ cleanupSession(message.sessionId);
156
+ if (message.kind === "vad") {
157
+ let vadModelPath;
158
+ try {
159
+ vadModelPath = await ensureSileroVadModel(message.config.modelsDir, logger);
160
+ }
161
+ catch (err) {
162
+ logger.warn({ err }, "Failed to provision Silero VAD model, falling back to bundled");
163
+ }
164
+ const provider = new SherpaSileroTurnDetectionProvider({ modelPath: vadModelPath }, logger);
165
+ const session = provider.createSession({ logger });
166
+ trackTurnDetectionSession(message.sessionId, session);
167
+ await session.connect();
168
+ sessions.set(message.sessionId, session);
169
+ return { requiredSampleRate: session.requiredSampleRate };
170
+ }
171
+ const model = message.kind === "voiceStt" ? "voice" : "dictation";
172
+ const engine = getSttEngine(message.config, model);
173
+ const session = message.kind === "voiceStt"
174
+ ? getSttProvider(message.config, "voice").createSession({ logger })
175
+ : new SherpaParakeetRealtimeTranscriptionSession({ engine });
176
+ trackTranscriptionSession(message.sessionId, session);
177
+ await session.connect();
178
+ sessions.set(message.sessionId, session);
179
+ return { requiredSampleRate: session.requiredSampleRate };
180
+ }
181
+ function sendOk(requestId, result) {
182
+ sendToParent({ type: "response", requestId, ok: true, result });
183
+ }
184
+ function handleSessionRequest(message) {
185
+ if (message.type === "session.close") {
186
+ cleanupSession(message.sessionId);
187
+ sendOk(message.requestId);
188
+ return;
189
+ }
190
+ const session = sessions.get(message.sessionId);
191
+ switch (message.type) {
192
+ case "session.append":
193
+ session?.appendPcm16(workerBytesToBuffer(message.audio));
194
+ break;
195
+ case "session.commit":
196
+ if (session && "commit" in session) {
197
+ session.commit();
198
+ }
199
+ break;
200
+ case "session.clear":
201
+ if (session && "clear" in session) {
202
+ session.clear();
203
+ }
204
+ break;
205
+ case "session.flush":
206
+ if (session && "flush" in session) {
207
+ session.flush();
208
+ }
209
+ break;
210
+ case "session.reset":
211
+ if (session && "reset" in session) {
212
+ session.reset();
213
+ }
214
+ break;
215
+ }
216
+ sendOk(message.requestId);
217
+ }
218
+ async function handleRequest(message) {
219
+ if (message.type === "tts.synthesize") {
220
+ const result = await getTtsProvider(message.config).synthesizeSpeech(message.text);
221
+ const chunks = [];
222
+ for await (const chunk of result.stream) {
223
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
224
+ }
225
+ sendOk(message.requestId, {
226
+ audio: bufferToWorkerBytes(Buffer.concat(chunks)),
227
+ format: result.format,
228
+ });
229
+ return;
230
+ }
231
+ if (message.type === "stt.transcribe") {
232
+ const result = await getSttProvider(message.config, message.model).transcribeAudio(workerBytesToBuffer(message.audio), message.format);
233
+ sendOk(message.requestId, result);
234
+ return;
235
+ }
236
+ if (message.type === "session.create") {
237
+ const result = await createSession(message);
238
+ sendOk(message.requestId, result);
239
+ return;
240
+ }
241
+ handleSessionRequest(message);
242
+ }
243
+ process.on("message", (message) => {
244
+ void handleRequest(message).catch((error) => {
245
+ sendToParent({
246
+ type: "response",
247
+ requestId: message.requestId,
248
+ ok: false,
249
+ error: error instanceof Error ? error.message : "Local speech worker request failed",
250
+ });
251
+ });
252
+ });
253
+ process.once("disconnect", () => {
254
+ ipcClosing = true;
255
+ for (const sessionId of Array.from(sessions.keys())) {
256
+ cleanupSession(sessionId);
257
+ }
258
+ for (const tts of ttsProviders.values()) {
259
+ try {
260
+ tts.free();
261
+ }
262
+ catch {
263
+ // ignore
264
+ }
265
+ }
266
+ for (const engine of sttEngines.values()) {
267
+ engine.free();
268
+ }
269
+ });
270
+ //# sourceMappingURL=worker-process.js.map