klaudio 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/tts.js +48 -16
package/package.json
CHANGED
package/src/tts.js
CHANGED
|
@@ -260,9 +260,33 @@ function speakMacOS(text) {
|
|
|
260
260
|
|
|
261
261
|
// ── Public API ──────────────────────────────────────────────────
|
|
262
262
|
|
|
263
|
+
let speaking = false;
|
|
264
|
+
const TTS_LOCK = join(tmpdir(), ".klaudio-tts-lock");
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Try to acquire a cross-process TTS lock.
|
|
268
|
+
* Returns true if acquired, false if another process is speaking.
|
|
269
|
+
* Stale locks (>30s) are automatically cleaned up.
|
|
270
|
+
*/
|
|
271
|
+
async function acquireTTSLock() {
|
|
272
|
+
try {
|
|
273
|
+
const lockStat = await stat(TTS_LOCK);
|
|
274
|
+
if (Date.now() - lockStat.mtimeMs < 30000) return false; // fresh lock, skip
|
|
275
|
+
} catch { /* no lock file, good */ }
|
|
276
|
+
try {
|
|
277
|
+
await fsWriteFile(TTS_LOCK, String(process.pid), "utf-8");
|
|
278
|
+
return true;
|
|
279
|
+
} catch { return false; }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function releaseTTSLock() {
|
|
283
|
+
try { const { unlink } = await import("node:fs/promises"); await unlink(TTS_LOCK); } catch { /* ignore */ }
|
|
284
|
+
}
|
|
285
|
+
|
|
263
286
|
/**
|
|
264
287
|
* Speak text using the best available TTS engine.
|
|
265
288
|
* Priority: Kokoro (GPU/CPU) → Piper → macOS say
|
|
289
|
+
* Only one speak() call runs at a time — concurrent calls are skipped.
|
|
266
290
|
*
|
|
267
291
|
* @param {string} text - Text to speak
|
|
268
292
|
* @param {object} [options]
|
|
@@ -271,26 +295,34 @@ function speakMacOS(text) {
|
|
|
271
295
|
*/
|
|
272
296
|
export async function speak(text, options = {}) {
|
|
273
297
|
if (!text) return;
|
|
298
|
+
if (speaking) return; // in-process mutex
|
|
299
|
+
if (!await acquireTTSLock()) return; // cross-process mutex
|
|
300
|
+
speaking = true;
|
|
274
301
|
|
|
275
|
-
const { voice, onProgress } = typeof options === "function"
|
|
276
|
-
? { voice: null, onProgress: options } // backwards compat: speak(text, onProgress)
|
|
277
|
-
: options;
|
|
278
|
-
|
|
279
|
-
// Try Kokoro first (works on all platforms, best quality)
|
|
280
302
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
const { voice, onProgress } = typeof options === "function"
|
|
304
|
+
? { voice: null, onProgress: options } // backwards compat: speak(text, onProgress)
|
|
305
|
+
: options;
|
|
306
|
+
|
|
307
|
+
// Try Kokoro first (works on all platforms, best quality)
|
|
308
|
+
try {
|
|
309
|
+
await speakKokoro(text, voice);
|
|
310
|
+
return;
|
|
311
|
+
} catch {
|
|
312
|
+
// Kokoro unavailable — fall through
|
|
313
|
+
}
|
|
286
314
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
315
|
+
// macOS: use built-in `say`
|
|
316
|
+
if (platform() === "darwin") {
|
|
317
|
+
return speakMacOS(text);
|
|
318
|
+
}
|
|
291
319
|
|
|
292
|
-
|
|
293
|
-
|
|
320
|
+
// Fallback: Piper
|
|
321
|
+
return speakPiper(text, onProgress);
|
|
322
|
+
} finally {
|
|
323
|
+
speaking = false;
|
|
324
|
+
await releaseTTSLock();
|
|
325
|
+
}
|
|
294
326
|
}
|
|
295
327
|
|
|
296
328
|
export { KOKORO_PRESET_VOICES, KOKORO_VOICES, KOKORO_DEFAULT_VOICE };
|