klaudio 0.11.3 → 0.12.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.
- package/package.json +1 -1
- package/src/cli.js +26 -1
- package/src/notify.js +3 -2
- package/src/player.js +1 -1
- package/src/tts.js +13 -6
package/package.json
CHANGED
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/notify.js
CHANGED
|
@@ -65,6 +65,7 @@ function notifyWindows(title, body) {
|
|
|
65
65
|
const toastXml = `<toast${toastAttrs}><visual><binding template="ToastGeneric"><text>${safeTitle}</text><text>${safeBody}</text></binding></visual></toast>`;
|
|
66
66
|
|
|
67
67
|
// PowerShell script: show WinRT toast notification
|
|
68
|
+
// Use -EncodedCommand to avoid all escaping issues with special chars
|
|
68
69
|
const ps = `\
|
|
69
70
|
[void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
|
|
70
71
|
[void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime]
|
|
@@ -73,8 +74,8 @@ $x.LoadXml('${toastXml.replace(/'/g, "''")}')
|
|
|
73
74
|
$t = [Windows.UI.Notifications.ToastNotification]::new($x)
|
|
74
75
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${appId}').Show($t)`;
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-
|
|
77
|
+
const encoded = Buffer.from(ps, "utf16le").toString("base64");
|
|
78
|
+
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
|
|
78
79
|
windowsHide: true,
|
|
79
80
|
detached: true,
|
|
80
81
|
stdio: "ignore",
|
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 =
|
|
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
|
-
|
|
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 {
|