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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tts.js +48 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klaudio",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Add sound effects to your coding sessions — play sounds when tasks complete, notifications arrive, and more",
5
5
  "type": "module",
6
6
  "bin": {
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
- await speakKokoro(text, voice);
282
- return;
283
- } catch {
284
- // Kokoro unavailable — fall through
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
- // macOS: use built-in `say`
288
- if (platform() === "darwin") {
289
- return speakMacOS(text);
290
- }
315
+ // macOS: use built-in `say`
316
+ if (platform() === "darwin") {
317
+ return speakMacOS(text);
318
+ }
291
319
 
292
- // Fallback: Piper
293
- return speakPiper(text, onProgress);
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 };