klaudio 0.11.3 → 0.11.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klaudio",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
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/cli.js CHANGED
@@ -137,7 +137,7 @@ const NavHint = ({ back = true, extra = "" }) =>
137
137
  );
138
138
 
139
139
  // ── Screen: Scope ───────────────────────────────────────────────
140
- const ScopeScreen = ({ onNext, onMusic, onUpdate, tts, onToggleTts, outdatedReasons }) => {
140
+ const ScopeScreen = ({ onNext, onMusic, onUpdate, tts, onToggleTts, voice, hasKokoro, onCycleVoice, outdatedReasons }) => {
141
141
  const isOutdated = outdatedReasons && outdatedReasons.length > 0;
142
142
  const items = [
143
143
  ...(isOutdated ? [{ label: "⬆ Apply updates", value: "_update" }] : []),
@@ -146,7 +146,9 @@ const ScopeScreen = ({ onNext, onMusic, onUpdate, tts, onToggleTts, outdatedReas
146
146
  { label: "🎵 Play game music while you code", value: "_music" },
147
147
  ];
148
148
  const [sel, setSel] = useState(0);
149
+ const [previewing, setPreviewing] = useState(false);
149
150
  const GAP_AT = (isOutdated ? 1 : 0) + 2; // visual gap before music
151
+ const voiceInfo = hasKokoro ? KOKORO_VOICES.find((v) => v.id === voice) : null;
150
152
 
151
153
  useInput((input, key) => {
152
154
  if (input === "k" || key.upArrow) {
@@ -155,6 +157,15 @@ const ScopeScreen = ({ onNext, onMusic, onUpdate, tts, onToggleTts, outdatedReas
155
157
  setSel((i) => Math.min(items.length - 1, i + 1));
156
158
  } else if (input === "t") {
157
159
  onToggleTts();
160
+ } else if (input === "v" && tts && hasKokoro) {
161
+ onCycleVoice();
162
+ // Preview the new voice
163
+ setPreviewing(true);
164
+ const nextIdx = (KOKORO_VOICES.findIndex((x) => x.id === voice) + 1) % KOKORO_VOICES.length;
165
+ const nextVoice = KOKORO_VOICES[nextIdx];
166
+ import("../src/tts.js").then(({ speak }) =>
167
+ speak(`Hi, I'm ${nextVoice.name}`, { voice: nextVoice.id })
168
+ ).finally(() => setPreviewing(false));
158
169
  } else if (key.return) {
159
170
  const v = items[sel].value;
160
171
  if (v === "_update") onUpdate();
@@ -188,6 +199,14 @@ const ScopeScreen = ({ onNext, onMusic, onUpdate, tts, onToggleTts, outdatedReas
188
199
  ),
189
200
  h(Text, { dimColor: true }, " (t to toggle)"),
190
201
  ),
202
+ tts && voiceInfo ? h(Box, { marginLeft: 4 },
203
+ h(Text, { color: ACCENT },
204
+ previewing
205
+ ? `🎙 Voice: ${voiceInfo.name} (previewing...)`
206
+ : `🎙 Voice: ${voiceInfo.name} (${voiceInfo.gender}, ${voiceInfo.accent})`,
207
+ ),
208
+ h(Text, { dimColor: true }, " (v to change & preview)"),
209
+ ) : null,
191
210
  );
192
211
  };
193
212
 
@@ -1594,8 +1613,14 @@ const InstallApp = () => {
1594
1613
  case SCREEN.SCOPE:
1595
1614
  return h(ScopeScreen, {
1596
1615
  tts,
1616
+ voice,
1617
+ hasKokoro,
1597
1618
  outdatedReasons,
1598
1619
  onToggleTts: () => setTts((v) => !v),
1620
+ onCycleVoice: () => setVoice((v) => {
1621
+ const idx = KOKORO_VOICES.findIndex((x) => x.id === v);
1622
+ return KOKORO_VOICES[(idx + 1) % KOKORO_VOICES.length].id;
1623
+ }),
1599
1624
  onNext: (s) => {
1600
1625
  setScope(s);
1601
1626
  // Refresh sounds/outdated for the selected scope
package/src/player.js CHANGED
@@ -434,7 +434,7 @@ export async function handlePlayCommand(args) {
434
434
  .trim();
435
435
  // Build summary: include sentences up to ~25 words max.
436
436
  // Short next sentences (<4 chars, e.g. version numbers) are always included.
437
- const MAX_WORDS = 25;
437
+ const MAX_WORDS = 50;
438
438
  // Split on sentence-ending punctuation, but not periods between digits (0.8.4)
439
439
  // or inside filenames (auth.js). A period is "sentence-ending" only if followed
440
440
  // by a space+letter, end-of-string, or another sentence-end mark.
package/src/tts.js CHANGED
@@ -317,7 +317,14 @@ async function speakPiper(text, onProgress) {
317
317
 
318
318
  function speakMacOS(text) {
319
319
  return new Promise((resolve) => {
320
- execFile("say", ["-v", "Daniel", text], { timeout: 15000 }, () => resolve());
320
+ // Try Samantha (high quality US English), fall back to default
321
+ execFile("say", ["-v", "Samantha", text], { timeout: 15000 }, (err) => {
322
+ if (err) {
323
+ execFile("say", [text], { timeout: 15000 }, () => resolve());
324
+ } else {
325
+ resolve();
326
+ }
327
+ });
321
328
  });
322
329
  }
323
330
 
@@ -367,11 +374,6 @@ export async function speak(text, options = {}) {
367
374
  ? { voice: null, onProgress: options } // backwards compat: speak(text, onProgress)
368
375
  : options;
369
376
 
370
- // macOS: prefer built-in `say` (Kokoro ONNX has threading issues on macOS)
371
- if (platform() === "darwin") {
372
- return speakMacOS(text);
373
- }
374
-
375
377
  // Try Kokoro first (works on all platforms, best quality)
376
378
  try {
377
379
  await speakKokoro(text, voice);
@@ -380,6 +382,11 @@ export async function speak(text, options = {}) {
380
382
  // Kokoro unavailable — fall through
381
383
  }
382
384
 
385
+ // macOS: use built-in `say`
386
+ if (platform() === "darwin") {
387
+ return speakMacOS(text);
388
+ }
389
+
383
390
  // Fallback: Piper
384
391
  return speakPiper(text, onProgress);
385
392
  } finally {