@sudocode-ai/local-server 0.1.17 → 0.1.18-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/better-sqlite3-loader.d.ts +9 -0
  2. package/dist/better-sqlite3-loader.d.ts.map +1 -0
  3. package/dist/better-sqlite3-loader.js +24 -0
  4. package/dist/better-sqlite3-loader.js.map +1 -0
  5. package/dist/execution/executors/agent-executor-wrapper.d.ts +6 -0
  6. package/dist/execution/executors/agent-executor-wrapper.d.ts.map +1 -1
  7. package/dist/execution/executors/agent-executor-wrapper.js +75 -4
  8. package/dist/execution/executors/agent-executor-wrapper.js.map +1 -1
  9. package/dist/execution/executors/executor-factory.d.ts +9 -6
  10. package/dist/execution/executors/executor-factory.d.ts.map +1 -1
  11. package/dist/execution/executors/executor-factory.js +6 -5
  12. package/dist/execution/executors/executor-factory.js.map +1 -1
  13. package/dist/execution/worktree/config.js +1 -1
  14. package/dist/execution/worktree/config.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/public/assets/index-B1p5HV93.css +1 -0
  19. package/dist/public/assets/index-qqIsBBjJ.js +3836 -0
  20. package/dist/public/assets/index-qqIsBBjJ.js.map +1 -0
  21. package/dist/public/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
  22. package/dist/public/index.html +2 -2
  23. package/dist/public/kokoro-test.html +197 -0
  24. package/dist/routes/config.d.ts.map +1 -1
  25. package/dist/routes/config.js +39 -0
  26. package/dist/routes/config.js.map +1 -1
  27. package/dist/routes/executions.d.ts.map +1 -1
  28. package/dist/routes/executions.js +9 -0
  29. package/dist/routes/executions.js.map +1 -1
  30. package/dist/routes/voice.d.ts +12 -0
  31. package/dist/routes/voice.d.ts.map +1 -0
  32. package/dist/routes/voice.js +387 -0
  33. package/dist/routes/voice.js.map +1 -0
  34. package/dist/routes/workflows.d.ts.map +1 -1
  35. package/dist/routes/workflows.js +2 -1
  36. package/dist/routes/workflows.js.map +1 -1
  37. package/dist/services/db.d.ts +1 -1
  38. package/dist/services/db.d.ts.map +1 -1
  39. package/dist/services/db.js +2 -2
  40. package/dist/services/db.js.map +1 -1
  41. package/dist/services/execution-service.d.ts +8 -0
  42. package/dist/services/execution-service.d.ts.map +1 -1
  43. package/dist/services/execution-service.js +27 -2
  44. package/dist/services/execution-service.js.map +1 -1
  45. package/dist/services/narration-service.d.ts +304 -0
  46. package/dist/services/narration-service.d.ts.map +1 -0
  47. package/dist/services/narration-service.js +729 -0
  48. package/dist/services/narration-service.js.map +1 -0
  49. package/dist/services/stt-providers/index.d.ts +21 -0
  50. package/dist/services/stt-providers/index.d.ts.map +1 -0
  51. package/dist/services/stt-providers/index.js +32 -0
  52. package/dist/services/stt-providers/index.js.map +1 -0
  53. package/dist/services/stt-providers/openai-whisper.d.ts +66 -0
  54. package/dist/services/stt-providers/openai-whisper.d.ts.map +1 -0
  55. package/dist/services/stt-providers/openai-whisper.js +137 -0
  56. package/dist/services/stt-providers/openai-whisper.js.map +1 -0
  57. package/dist/services/stt-providers/whisper-local.d.ts +64 -0
  58. package/dist/services/stt-providers/whisper-local.d.ts.map +1 -0
  59. package/dist/services/stt-providers/whisper-local.js +166 -0
  60. package/dist/services/stt-providers/whisper-local.js.map +1 -0
  61. package/dist/services/stt-service.d.ts +160 -0
  62. package/dist/services/stt-service.d.ts.map +1 -0
  63. package/dist/services/stt-service.js +246 -0
  64. package/dist/services/stt-service.js.map +1 -0
  65. package/dist/services/tts-providers/browser-tts.d.ts +64 -0
  66. package/dist/services/tts-providers/browser-tts.d.ts.map +1 -0
  67. package/dist/services/tts-providers/browser-tts.js +89 -0
  68. package/dist/services/tts-providers/browser-tts.js.map +1 -0
  69. package/dist/services/tts-providers/index.d.ts +20 -0
  70. package/dist/services/tts-providers/index.d.ts.map +1 -0
  71. package/dist/services/tts-providers/index.js +31 -0
  72. package/dist/services/tts-providers/index.js.map +1 -0
  73. package/dist/services/tts-service.d.ts +190 -0
  74. package/dist/services/tts-service.d.ts.map +1 -0
  75. package/dist/services/tts-service.js +296 -0
  76. package/dist/services/tts-service.js.map +1 -0
  77. package/dist/services/tts-sidecar-manager.d.ts +276 -0
  78. package/dist/services/tts-sidecar-manager.d.ts.map +1 -0
  79. package/dist/services/tts-sidecar-manager.js +665 -0
  80. package/dist/services/tts-sidecar-manager.js.map +1 -0
  81. package/dist/services/websocket.d.ts +31 -1
  82. package/dist/services/websocket.d.ts.map +1 -1
  83. package/dist/services/websocket.js +149 -0
  84. package/dist/services/websocket.js.map +1 -1
  85. package/dist/services/worktree-sync-service.d.ts +17 -2
  86. package/dist/services/worktree-sync-service.d.ts.map +1 -1
  87. package/dist/services/worktree-sync-service.js +103 -6
  88. package/dist/services/worktree-sync-service.js.map +1 -1
  89. package/dist/utils/voice-config.d.ts +26 -0
  90. package/dist/utils/voice-config.d.ts.map +1 -0
  91. package/dist/utils/voice-config.js +48 -0
  92. package/dist/utils/voice-config.js.map +1 -0
  93. package/dist/workers/execution-worker.js +12 -4
  94. package/dist/workers/execution-worker.js.map +1 -1
  95. package/dist/workflow/base-workflow-engine.d.ts +3 -1
  96. package/dist/workflow/base-workflow-engine.d.ts.map +1 -1
  97. package/dist/workflow/base-workflow-engine.js.map +1 -1
  98. package/dist/workflow/engines/orchestrator-engine.d.ts +5 -1
  99. package/dist/workflow/engines/orchestrator-engine.d.ts.map +1 -1
  100. package/dist/workflow/engines/orchestrator-engine.js +4 -1
  101. package/dist/workflow/engines/orchestrator-engine.js.map +1 -1
  102. package/dist/workflow/engines/sequential-engine.d.ts +9 -2
  103. package/dist/workflow/engines/sequential-engine.d.ts.map +1 -1
  104. package/dist/workflow/engines/sequential-engine.js +102 -22
  105. package/dist/workflow/engines/sequential-engine.js.map +1 -1
  106. package/dist/workflow/workflow-engine.d.ts +8 -1
  107. package/dist/workflow/workflow-engine.d.ts.map +1 -1
  108. package/package.json +13 -4
  109. package/dist/public/assets/index-D4AKx6EO.css +0 -1
  110. package/dist/public/assets/index-DorQqwGV.js +0 -927
  111. package/dist/public/assets/index-DorQqwGV.js.map +0 -1
@@ -0,0 +1,665 @@
1
+ /**
2
+ * TTS Sidecar Manager Service
3
+ *
4
+ * Manages the lifecycle of the Python Kokoro TTS sidecar process.
5
+ * Handles installation, startup, health checks, and graceful shutdown.
6
+ *
7
+ * The sidecar communicates via JSON-lines protocol over stdin/stdout:
8
+ * - stdin: {"id": "req-123", "type": "generate", "text": "Hello", "voice": "af_heart", "speed": 1.0}
9
+ * - stdout: {"id": "req-123", "type": "audio", "chunk": "<base64>", "index": 0}
10
+ *
11
+ * Audio format: mono, 24kHz, float32 PCM (base64 encoded)
12
+ */
13
+ import { spawn, execFile } from "child_process";
14
+ import { promisify } from "util";
15
+ import { EventEmitter } from "events";
16
+ import * as fs from "fs/promises";
17
+ import * as path from "path";
18
+ import * as os from "os";
19
+ const execFileAsync = promisify(execFile);
20
+ // =============================================================================
21
+ // Constants
22
+ // =============================================================================
23
+ /** Health check interval in milliseconds */
24
+ const HEALTH_CHECK_INTERVAL_MS = 30_000;
25
+ /** Minimum restart delay */
26
+ const MIN_RESTART_DELAY_MS = 1_000;
27
+ /** Maximum restart delay */
28
+ const MAX_RESTART_DELAY_MS = 30_000;
29
+ /** Restart delay multiplier for exponential backoff */
30
+ const RESTART_DELAY_MULTIPLIER = 2;
31
+ /** Timeout for graceful shutdown before SIGKILL */
32
+ const SHUTDOWN_TIMEOUT_MS = 5_000;
33
+ /** Timeout for waiting for ready signal */
34
+ const READY_TIMEOUT_MS = 30_000;
35
+ /** Model file URLs */
36
+ const MODEL_URL = "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx";
37
+ const VOICES_URL = "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin";
38
+ // =============================================================================
39
+ // TTSSidecarManager
40
+ // =============================================================================
41
+ /**
42
+ * Manages the Python Kokoro TTS sidecar process lifecycle.
43
+ *
44
+ * Features:
45
+ * - Lazy initialization (only installs/starts when first TTS request arrives)
46
+ * - Platform-specific ONNX runtime detection
47
+ * - Health monitoring with auto-restart on crash
48
+ * - Exponential backoff for restart attempts
49
+ * - Graceful shutdown with timeout
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const manager = getTTSSidecarManager();
54
+ *
55
+ * manager.on('audio', (response) => {
56
+ * console.log('Received audio chunk:', response.index);
57
+ * });
58
+ *
59
+ * await manager.ensureReady();
60
+ * await manager.generate({
61
+ * id: 'req-1',
62
+ * text: 'Hello, world!',
63
+ * voice: 'af_heart',
64
+ * });
65
+ * ```
66
+ */
67
+ export class TTSSidecarManager extends EventEmitter {
68
+ state = "idle";
69
+ process = null;
70
+ healthCheckTimer = null;
71
+ restartDelay = MIN_RESTART_DELAY_MS;
72
+ restartTimer = null;
73
+ lineBuffer = "";
74
+ readyPromise = null;
75
+ readyResolve = null;
76
+ readyReject = null;
77
+ shuttingDown = false;
78
+ consecutiveHealthFailures = 0;
79
+ maxHealthFailures = 3;
80
+ constructor() {
81
+ super();
82
+ }
83
+ // ===========================================================================
84
+ // Public API
85
+ // ===========================================================================
86
+ /**
87
+ * Get the current state of the sidecar manager.
88
+ */
89
+ getState() {
90
+ return this.state;
91
+ }
92
+ /**
93
+ * Get the cross-platform TTS directory path.
94
+ *
95
+ * - Windows: %APPDATA%/sudocode/tts
96
+ * - macOS/Linux: ~/.config/sudocode/tts
97
+ */
98
+ getTTSDirectory() {
99
+ if (process.platform === "win32") {
100
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
101
+ return path.join(appData, "sudocode", "tts");
102
+ }
103
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
104
+ return path.join(configHome, "sudocode", "tts");
105
+ }
106
+ /**
107
+ * Get the path to the Python virtual environment.
108
+ */
109
+ getVenvPath() {
110
+ return path.join(this.getTTSDirectory(), "venv");
111
+ }
112
+ /**
113
+ * Get the path to the Python executable in the venv.
114
+ */
115
+ getPythonPath() {
116
+ const venvPath = this.getVenvPath();
117
+ if (process.platform === "win32") {
118
+ return path.join(venvPath, "Scripts", "python.exe");
119
+ }
120
+ return path.join(venvPath, "bin", "python");
121
+ }
122
+ /**
123
+ * Get the path to the models directory.
124
+ */
125
+ getModelsPath() {
126
+ return path.join(this.getTTSDirectory(), "models");
127
+ }
128
+ /**
129
+ * Get the path to the sidecar Python script.
130
+ */
131
+ getSidecarScriptPath() {
132
+ // The sidecar script is located relative to this module
133
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
134
+ return path.join(thisDir, "..", "tts", "sidecar.py");
135
+ }
136
+ /**
137
+ * Check if the TTS environment is installed.
138
+ */
139
+ async isInstalled() {
140
+ const venvPath = this.getVenvPath();
141
+ const pythonPath = this.getPythonPath();
142
+ const modelsPath = this.getModelsPath();
143
+ try {
144
+ // Check venv exists
145
+ await fs.access(venvPath);
146
+ await fs.access(pythonPath);
147
+ // Check model files exist
148
+ const modelPath = path.join(modelsPath, "kokoro-v1.0.onnx");
149
+ const voicesPath = path.join(modelsPath, "voices-v1.0.bin");
150
+ await fs.access(modelPath);
151
+ await fs.access(voicesPath);
152
+ // Try to verify kokoro-onnx is installed
153
+ try {
154
+ await execFileAsync(pythonPath, ["-c", "import kokoro_onnx"], {
155
+ timeout: 10_000,
156
+ });
157
+ }
158
+ catch {
159
+ return {
160
+ installed: false,
161
+ venvPath,
162
+ pythonPath,
163
+ error: "kokoro-onnx package not installed",
164
+ };
165
+ }
166
+ return {
167
+ installed: true,
168
+ venvPath,
169
+ pythonPath,
170
+ };
171
+ }
172
+ catch {
173
+ return {
174
+ installed: false,
175
+ error: "TTS environment not installed",
176
+ };
177
+ }
178
+ }
179
+ /**
180
+ * Detect the appropriate ONNX runtime for the current platform.
181
+ *
182
+ * Note: This is a synchronous method that does a quick filesystem check
183
+ * for NVIDIA drivers on Linux. For most platforms, no I/O is performed.
184
+ */
185
+ detectOnnxRuntime() {
186
+ const platform = process.platform;
187
+ const arch = process.arch;
188
+ // Apple Silicon
189
+ if (platform === "darwin" && arch === "arm64") {
190
+ return "onnxruntime-silicon";
191
+ }
192
+ // NVIDIA GPU on Linux
193
+ if (platform === "linux") {
194
+ try {
195
+ // Check for NVIDIA driver - use sync fs for simplicity
196
+ // since this is a one-time check during install
197
+ const fsSync = require("node:fs");
198
+ const nvidiaProcPath = "/proc/driver/nvidia";
199
+ const stats = fsSync.statSync(nvidiaProcPath);
200
+ if (stats.isDirectory()) {
201
+ return "onnxruntime-gpu";
202
+ }
203
+ }
204
+ catch {
205
+ // Not an NVIDIA system
206
+ }
207
+ }
208
+ // Windows with DirectML
209
+ if (platform === "win32") {
210
+ return "onnxruntime-directml";
211
+ }
212
+ // Fallback to CPU
213
+ return "onnxruntime";
214
+ }
215
+ /**
216
+ * Install the TTS environment (venv, kokoro-onnx, appropriate ONNX runtime, model files).
217
+ *
218
+ * @throws Error if installation fails
219
+ */
220
+ async install() {
221
+ if (this.state !== "idle" && this.state !== "error") {
222
+ throw new Error(`Cannot install while in state: ${this.state}`);
223
+ }
224
+ this.setState("installing");
225
+ const ttsDir = this.getTTSDirectory();
226
+ const venvPath = this.getVenvPath();
227
+ const modelsPath = this.getModelsPath();
228
+ const pythonPath = this.getPythonPath();
229
+ const onnxRuntime = this.detectOnnxRuntime();
230
+ try {
231
+ // Create directories
232
+ await fs.mkdir(ttsDir, { recursive: true });
233
+ await fs.mkdir(modelsPath, { recursive: true });
234
+ // Find system Python
235
+ const systemPython = await this.findSystemPython();
236
+ // Create virtual environment
237
+ console.log("[tts-sidecar] Creating Python virtual environment...");
238
+ await execFileAsync(systemPython, ["-m", "venv", venvPath], {
239
+ timeout: 60_000,
240
+ });
241
+ // Upgrade pip
242
+ console.log("[tts-sidecar] Upgrading pip...");
243
+ await execFileAsync(pythonPath, ["-m", "pip", "install", "--upgrade", "pip"], { timeout: 120_000 });
244
+ // Install kokoro-onnx and appropriate ONNX runtime
245
+ console.log(`[tts-sidecar] Installing kokoro-onnx and ${onnxRuntime}...`);
246
+ await execFileAsync(pythonPath, ["-m", "pip", "install", "kokoro-onnx", onnxRuntime], { timeout: 300_000 });
247
+ // Download model files
248
+ console.log("[tts-sidecar] Downloading model files...");
249
+ await this.downloadFile(MODEL_URL, path.join(modelsPath, "kokoro-v1.0.onnx"));
250
+ await this.downloadFile(VOICES_URL, path.join(modelsPath, "voices-v1.0.bin"));
251
+ console.log("[tts-sidecar] Installation complete");
252
+ this.setState("idle");
253
+ }
254
+ catch (error) {
255
+ console.error("[tts-sidecar] Installation failed:", error);
256
+ this.setState("error");
257
+ throw error;
258
+ }
259
+ }
260
+ /**
261
+ * Start the sidecar process.
262
+ *
263
+ * @throws Error if start fails or ready signal not received
264
+ */
265
+ async start() {
266
+ if (this.state === "ready") {
267
+ return; // Already running
268
+ }
269
+ if (this.state === "starting") {
270
+ // Wait for existing start to complete
271
+ if (this.readyPromise) {
272
+ await this.readyPromise;
273
+ return;
274
+ }
275
+ }
276
+ if (this.state !== "idle" && this.state !== "error") {
277
+ throw new Error(`Cannot start while in state: ${this.state}`);
278
+ }
279
+ this.setState("starting");
280
+ this.shuttingDown = false;
281
+ const pythonPath = this.getPythonPath();
282
+ const sidecarPath = this.getSidecarScriptPath();
283
+ const modelsPath = this.getModelsPath();
284
+ // Create promise for ready signal
285
+ this.readyPromise = new Promise((resolve, reject) => {
286
+ this.readyResolve = resolve;
287
+ this.readyReject = reject;
288
+ });
289
+ try {
290
+ // Spawn the sidecar process
291
+ this.process = spawn(pythonPath, [sidecarPath], {
292
+ env: {
293
+ ...process.env,
294
+ KOKORO_MODELS_DIR: modelsPath,
295
+ },
296
+ stdio: ["pipe", "pipe", "pipe"],
297
+ });
298
+ // Handle stdout (JSON-lines responses)
299
+ this.process.stdout?.on("data", (data) => {
300
+ this.handleStdout(data);
301
+ });
302
+ // Handle stderr (logs)
303
+ this.process.stderr?.on("data", (data) => {
304
+ const message = data.toString().trim();
305
+ if (message) {
306
+ console.log(`[kokoro-sidecar] ${message}`);
307
+ }
308
+ });
309
+ // Handle process exit
310
+ this.process.on("exit", (code, signal) => {
311
+ this.handleProcessExit(code, signal);
312
+ });
313
+ // Handle process error
314
+ this.process.on("error", (error) => {
315
+ console.error("[tts-sidecar] Process error:", error);
316
+ this.handleProcessExit(1, null);
317
+ });
318
+ // Wait for ready signal with timeout
319
+ const timeoutPromise = new Promise((_, reject) => {
320
+ setTimeout(() => {
321
+ reject(new Error("Timeout waiting for sidecar ready signal"));
322
+ }, READY_TIMEOUT_MS);
323
+ });
324
+ await Promise.race([this.readyPromise, timeoutPromise]);
325
+ // Start health checks
326
+ this.startHealthChecks();
327
+ }
328
+ catch (error) {
329
+ console.error("[tts-sidecar] Start failed:", error);
330
+ this.cleanup();
331
+ this.setState("error");
332
+ throw error;
333
+ }
334
+ }
335
+ /**
336
+ * Ensure the sidecar is ready for TTS generation.
337
+ * Installs if necessary, then starts the sidecar.
338
+ */
339
+ async ensureReady() {
340
+ if (this.state === "ready") {
341
+ return;
342
+ }
343
+ const status = await this.isInstalled();
344
+ if (!status.installed) {
345
+ await this.install();
346
+ }
347
+ await this.start();
348
+ }
349
+ /**
350
+ * Generate TTS audio from text.
351
+ *
352
+ * @param request - The TTS generation request
353
+ * @throws Error if sidecar is not ready
354
+ */
355
+ async generate(request) {
356
+ if (this.state !== "ready") {
357
+ throw new Error(`Sidecar not ready, current state: ${this.state}`);
358
+ }
359
+ if (!this.process?.stdin?.writable) {
360
+ throw new Error("Sidecar stdin not writable");
361
+ }
362
+ const message = JSON.stringify({
363
+ id: request.id,
364
+ type: "generate",
365
+ text: request.text,
366
+ voice: request.voice || "af_heart",
367
+ speed: request.speed || 1.0,
368
+ });
369
+ this.process.stdin.write(message + "\n");
370
+ }
371
+ /**
372
+ * Gracefully shutdown the sidecar process.
373
+ *
374
+ * Sends shutdown command, waits for graceful exit.
375
+ * If timeout expires, sends SIGKILL.
376
+ */
377
+ async shutdown() {
378
+ if (this.state === "idle" || this.state === "shutdown") {
379
+ return;
380
+ }
381
+ this.setState("shutdown");
382
+ this.shuttingDown = true;
383
+ // Stop health checks
384
+ this.stopHealthChecks();
385
+ // Clear restart timer
386
+ if (this.restartTimer) {
387
+ clearTimeout(this.restartTimer);
388
+ this.restartTimer = null;
389
+ }
390
+ if (!this.process) {
391
+ this.setState("idle");
392
+ return;
393
+ }
394
+ // Try graceful shutdown
395
+ if (this.process.stdin?.writable) {
396
+ const shutdownMessage = JSON.stringify({ type: "shutdown" });
397
+ this.process.stdin.write(shutdownMessage + "\n");
398
+ }
399
+ // Wait for graceful exit with timeout
400
+ await new Promise((resolve) => {
401
+ const timeout = setTimeout(() => {
402
+ // Force kill if still running
403
+ if (this.process) {
404
+ console.log("[tts-sidecar] Forcing shutdown with SIGKILL");
405
+ this.process.kill("SIGKILL");
406
+ }
407
+ resolve();
408
+ }, SHUTDOWN_TIMEOUT_MS);
409
+ if (this.process) {
410
+ this.process.once("exit", () => {
411
+ clearTimeout(timeout);
412
+ resolve();
413
+ });
414
+ }
415
+ else {
416
+ clearTimeout(timeout);
417
+ resolve();
418
+ }
419
+ });
420
+ this.cleanup();
421
+ this.setState("idle");
422
+ }
423
+ // ===========================================================================
424
+ // Private Methods
425
+ // ===========================================================================
426
+ /**
427
+ * Set the current state and emit state event.
428
+ */
429
+ setState(newState) {
430
+ if (this.state !== newState) {
431
+ this.state = newState;
432
+ this.emit("state", newState);
433
+ }
434
+ }
435
+ /**
436
+ * Find the system Python interpreter.
437
+ */
438
+ async findSystemPython() {
439
+ const candidates = process.platform === "win32"
440
+ ? ["python", "python3", "py"]
441
+ : ["python3", "python"];
442
+ for (const candidate of candidates) {
443
+ try {
444
+ const { stdout } = await execFileAsync(candidate, ["--version"], {
445
+ timeout: 5_000,
446
+ });
447
+ // Ensure it's Python 3.8+
448
+ const versionMatch = stdout.match(/Python (\d+)\.(\d+)/);
449
+ if (versionMatch) {
450
+ const major = parseInt(versionMatch[1], 10);
451
+ const minor = parseInt(versionMatch[2], 10);
452
+ if (major >= 3 && minor >= 8) {
453
+ return candidate;
454
+ }
455
+ }
456
+ }
457
+ catch {
458
+ // Try next candidate
459
+ }
460
+ }
461
+ throw new Error("Python 3.8+ not found. Please install Python from https://python.org");
462
+ }
463
+ /**
464
+ * Download a file from URL to disk.
465
+ */
466
+ async downloadFile(url, destPath) {
467
+ // Use native fetch (Node 18+)
468
+ const response = await fetch(url);
469
+ if (!response.ok) {
470
+ throw new Error(`Failed to download ${url}: ${response.statusText}`);
471
+ }
472
+ const arrayBuffer = await response.arrayBuffer();
473
+ await fs.writeFile(destPath, Buffer.from(arrayBuffer));
474
+ }
475
+ /**
476
+ * Handle stdout data from the sidecar process.
477
+ * Parses JSON-lines and emits appropriate events.
478
+ */
479
+ handleStdout(data) {
480
+ this.lineBuffer += data.toString();
481
+ // Process complete lines
482
+ let newlineIndex;
483
+ while ((newlineIndex = this.lineBuffer.indexOf("\n")) !== -1) {
484
+ const line = this.lineBuffer.slice(0, newlineIndex).trim();
485
+ this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
486
+ if (!line)
487
+ continue;
488
+ try {
489
+ const response = JSON.parse(line);
490
+ this.handleResponse(response);
491
+ }
492
+ catch (error) {
493
+ console.error("[tts-sidecar] Failed to parse response:", line, error);
494
+ }
495
+ }
496
+ }
497
+ /**
498
+ * Handle a parsed response from the sidecar.
499
+ */
500
+ handleResponse(response) {
501
+ // Emit generic response event
502
+ this.emit("response", response);
503
+ switch (response.type) {
504
+ case "ready":
505
+ this.setState("ready");
506
+ this.restartDelay = MIN_RESTART_DELAY_MS;
507
+ this.consecutiveHealthFailures = 0;
508
+ if (this.readyResolve) {
509
+ this.readyResolve();
510
+ this.readyResolve = null;
511
+ this.readyReject = null;
512
+ this.readyPromise = null;
513
+ }
514
+ break;
515
+ case "audio":
516
+ this.emit("audio", response);
517
+ break;
518
+ case "done":
519
+ this.emit("done", response);
520
+ break;
521
+ case "error":
522
+ this.emit("error", response);
523
+ break;
524
+ case "pong":
525
+ // Health check response - reset failure counter
526
+ this.consecutiveHealthFailures = 0;
527
+ break;
528
+ }
529
+ }
530
+ /**
531
+ * Handle sidecar process exit.
532
+ */
533
+ handleProcessExit(code, signal) {
534
+ console.log(`[tts-sidecar] Process exited with code ${code}, signal ${signal}`);
535
+ // Reject any pending ready promise
536
+ if (this.readyReject) {
537
+ this.readyReject(new Error(`Sidecar exited unexpectedly: code=${code}, signal=${signal}`));
538
+ this.readyResolve = null;
539
+ this.readyReject = null;
540
+ this.readyPromise = null;
541
+ }
542
+ this.cleanup();
543
+ // Auto-restart if not shutting down
544
+ if (!this.shuttingDown && this.state !== "shutdown") {
545
+ this.setState("error");
546
+ this.scheduleRestart();
547
+ }
548
+ }
549
+ /**
550
+ * Clean up resources.
551
+ */
552
+ cleanup() {
553
+ this.stopHealthChecks();
554
+ this.process = null;
555
+ this.lineBuffer = "";
556
+ }
557
+ /**
558
+ * Schedule a restart with exponential backoff.
559
+ */
560
+ scheduleRestart() {
561
+ if (this.restartTimer) {
562
+ return; // Already scheduled
563
+ }
564
+ console.log(`[tts-sidecar] Scheduling restart in ${this.restartDelay}ms`);
565
+ this.restartTimer = setTimeout(async () => {
566
+ this.restartTimer = null;
567
+ try {
568
+ await this.start();
569
+ }
570
+ catch (error) {
571
+ console.error("[tts-sidecar] Restart failed:", error);
572
+ // Increase delay for next attempt (exponential backoff)
573
+ this.restartDelay = Math.min(this.restartDelay * RESTART_DELAY_MULTIPLIER, MAX_RESTART_DELAY_MS);
574
+ }
575
+ }, this.restartDelay);
576
+ }
577
+ /**
578
+ * Start the health check timer.
579
+ */
580
+ startHealthChecks() {
581
+ if (this.healthCheckTimer) {
582
+ return;
583
+ }
584
+ this.healthCheckTimer = setInterval(() => {
585
+ this.performHealthCheck();
586
+ }, HEALTH_CHECK_INTERVAL_MS);
587
+ }
588
+ /**
589
+ * Stop the health check timer.
590
+ */
591
+ stopHealthChecks() {
592
+ if (this.healthCheckTimer) {
593
+ clearInterval(this.healthCheckTimer);
594
+ this.healthCheckTimer = null;
595
+ }
596
+ }
597
+ /**
598
+ * Perform a health check by sending a ping.
599
+ */
600
+ performHealthCheck() {
601
+ if (this.state !== "ready") {
602
+ return;
603
+ }
604
+ if (!this.process?.stdin?.writable) {
605
+ console.warn("[tts-sidecar] Health check failed: stdin not writable");
606
+ this.consecutiveHealthFailures++;
607
+ this.checkHealthFailures();
608
+ return;
609
+ }
610
+ try {
611
+ const pingMessage = JSON.stringify({
612
+ id: "health",
613
+ type: "ping",
614
+ });
615
+ this.process.stdin.write(pingMessage + "\n");
616
+ }
617
+ catch (error) {
618
+ console.warn("[tts-sidecar] Health check failed:", error);
619
+ this.consecutiveHealthFailures++;
620
+ this.checkHealthFailures();
621
+ }
622
+ }
623
+ /**
624
+ * Check if consecutive health failures exceed threshold.
625
+ */
626
+ checkHealthFailures() {
627
+ if (this.consecutiveHealthFailures >= this.maxHealthFailures) {
628
+ console.error(`[tts-sidecar] Too many consecutive health failures (${this.consecutiveHealthFailures}), restarting`);
629
+ // Force process termination
630
+ if (this.process) {
631
+ this.process.kill("SIGKILL");
632
+ }
633
+ }
634
+ }
635
+ }
636
+ // =============================================================================
637
+ // Singleton Instance
638
+ // =============================================================================
639
+ /**
640
+ * Global TTS sidecar manager instance (singleton).
641
+ * Lazy-initialized on first use.
642
+ */
643
+ let sidecarManagerInstance = null;
644
+ /**
645
+ * Get or create the global TTS sidecar manager instance.
646
+ *
647
+ * @returns The TTS sidecar manager instance
648
+ */
649
+ export function getTTSSidecarManager() {
650
+ if (!sidecarManagerInstance) {
651
+ sidecarManagerInstance = new TTSSidecarManager();
652
+ }
653
+ return sidecarManagerInstance;
654
+ }
655
+ /**
656
+ * Reset the global TTS sidecar manager instance (for testing).
657
+ * Shuts down any running sidecar before resetting.
658
+ */
659
+ export async function resetTTSSidecarManager() {
660
+ if (sidecarManagerInstance) {
661
+ await sidecarManagerInstance.shutdown();
662
+ sidecarManagerInstance = null;
663
+ }
664
+ }
665
+ //# sourceMappingURL=tts-sidecar-manager.js.map