@drakulavich/kesha-voice-kit 2.0.0 → 2.2.0-beta.1
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/README.md +13 -13
- package/docs/product-positioning.md +1 -1
- package/package.json +15 -14
- package/src/cli/dispatch.ts +28 -10
- package/src/cli/init.ts +15 -50
- package/src/cli/install.ts +1 -1
- package/src/cli/logs.ts +3 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/options.ts +26 -5
- package/src/cli/say.ts +9 -6
- package/src/cli/stats.ts +8 -0
- package/src/cli/support-bundle.ts +12 -2
- package/src/cli.ts +0 -1
- package/src/doctor.ts +8 -2
- package/src/engine/events.ts +2 -1
- package/src/engine-install.ts +135 -63
- package/src/engine-version-marker.ts +8 -0
- package/src/engine.ts +36 -3
- package/src/log.ts +4 -0
- package/src/mcp/tools.ts +3 -1
- package/src/path-errnos.ts +11 -0
- package/src/progress.ts +30 -13
- package/src/support-bundle.ts +73 -8
- package/src/toon.ts +2 -1
- package/src/voice-routing.ts +16 -3
package/src/support-bundle.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, dirname, resolve } from "path";
|
|
1
|
+
import { lstatSync, mkdirSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "path";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
4
|
import { collectDoctorReport, formatDoctorReport } from "./doctor";
|
|
5
5
|
import { readDiagnosticLogTail } from "./diagnostic-log";
|
|
6
|
+
import { KeshaError } from "./engine/events";
|
|
7
|
+
import { errorMessage } from "./error-utils";
|
|
8
|
+
import { PATH_ERRNOS } from "./path-errnos";
|
|
6
9
|
|
|
7
10
|
interface SupportBundleOptions {
|
|
8
11
|
output?: string;
|
|
@@ -99,6 +102,67 @@ function createTarArchive(entries: TarEntry[]): Uint8Array {
|
|
|
99
102
|
return archive;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function existsAsLink(path: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
return lstatSync(path).isSymbolicLink();
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fileInTheWay(dir: string): string | null {
|
|
114
|
+
for (let d = dir; ; d = dirname(d)) {
|
|
115
|
+
try {
|
|
116
|
+
return statSync(d).isDirectory() ? null : d;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT" && existsAsLink(d)) return d;
|
|
119
|
+
if (dirname(d) === d) return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mkdirReason(dir: string, errno: string): string {
|
|
125
|
+
if (errno !== "EEXIST") return errno;
|
|
126
|
+
try {
|
|
127
|
+
statSync(dir);
|
|
128
|
+
return errno;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return (e as NodeJS.ErrnoException).code ?? errno;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function bundleWriteFailure(outputPath: string, e: unknown): unknown {
|
|
135
|
+
if ((e as NodeJS.ErrnoException).code === "EPIPE") return e;
|
|
136
|
+
const dir = dirname(outputPath);
|
|
137
|
+
const errno = mkdirReason(dir, (e as NodeJS.ErrnoException).code ?? "");
|
|
138
|
+
const blocker = fileInTheWay(dir);
|
|
139
|
+
if (blocker) {
|
|
140
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the support bundle to ${outputPath}: ${blocker} exists and is not a directory`, {
|
|
141
|
+
hint: `remove or rename the file blocking ${blocker}, or pass --output a path under a directory.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (errno === "EISDIR") {
|
|
145
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the support bundle to ${outputPath}: it is a directory (EISDIR)`, {
|
|
146
|
+
hint: `pass --output a .tar.gz file path, for example ${join(outputPath, "kesha-support-bundle.tar.gz")}.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const why = PATH_ERRNOS[errno];
|
|
150
|
+
if (why) {
|
|
151
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the support bundle to ${outputPath}: ${why} (${errno})`, {
|
|
152
|
+
hint: `pass --output a path in a directory you can write to.`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (!errno) return e;
|
|
156
|
+
if (errno === "ENOSPC") {
|
|
157
|
+
return new KeshaError("E_INTERNAL", `Cannot write the support bundle to ${outputPath}: no space left on its disk (ENOSPC)`, {
|
|
158
|
+
hint: `the disk is full: free space on the volume that holds ${dir}, or pass --output a path on another disk.`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return new KeshaError("E_INTERNAL", `Cannot write the support bundle to ${outputPath}: ${errorMessage(e)}`, {
|
|
162
|
+
hint: `resolve that filesystem error on ${dir}, or pass --output a path elsewhere.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
102
166
|
export async function createSupportBundle(
|
|
103
167
|
options: SupportBundleOptions = {},
|
|
104
168
|
): Promise<SupportBundleResult> {
|
|
@@ -180,16 +244,17 @@ export async function createSupportBundle(
|
|
|
180
244
|
);
|
|
181
245
|
}
|
|
182
246
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
247
|
+
const archive = gzipSync(createTarArchive(files));
|
|
248
|
+
try {
|
|
249
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
250
|
+
writeFileSync(outputPath, archive);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
throw bundleWriteFailure(outputPath, e);
|
|
188
253
|
}
|
|
189
254
|
|
|
190
255
|
return {
|
|
191
256
|
path: outputPath,
|
|
192
|
-
sizeBytes:
|
|
257
|
+
sizeBytes: archive.byteLength,
|
|
193
258
|
entries: files.map((file) => file.name),
|
|
194
259
|
};
|
|
195
260
|
}
|
package/src/toon.ts
CHANGED
|
@@ -15,5 +15,6 @@ export function formatToonOutput<T extends object, E extends object>(
|
|
|
15
15
|
results: T[],
|
|
16
16
|
errors?: E[],
|
|
17
17
|
): string {
|
|
18
|
-
|
|
18
|
+
const payload = errors === undefined ? results : { results, errors };
|
|
19
|
+
return encodeToon(JSON.parse(JSON.stringify(payload))) + "\n";
|
|
19
20
|
}
|
package/src/voice-routing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { detectTextLanguageEngine } from "./engine";
|
|
1
|
+
import { detectTextLanguageEngine, type LangDetectResult } from "./engine";
|
|
2
2
|
import { listVoiceIds } from "./synth";
|
|
3
3
|
|
|
4
4
|
/** The voice the Engine speaks with when `say` is given no `--voice` — mirrors `tts::voices::DEFAULT_VOICE_ID`. */
|
|
@@ -63,6 +63,12 @@ const NATIVE_SCRIPT_ROUTES: Record<string, NativeScriptRoute> = {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
const LATIN_LETTER = /[A-Za-z]/u;
|
|
66
|
+
const CYRILLIC = /\p{Script=Cyrillic}/u;
|
|
67
|
+
|
|
68
|
+
function mostlyCyrillicWords(text: string): boolean {
|
|
69
|
+
const words = text.split(/\s+/).filter((w) => /\p{L}/u.test(w));
|
|
70
|
+
return words.filter((w) => CYRILLIC.test(w)).length * 2 > words.length;
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
/** True when more of the text's letters are in `script` than in the Latin alphabet. */
|
|
68
74
|
function dominantScript(text: string, script: RegExp): boolean {
|
|
@@ -110,6 +116,7 @@ export interface ResolveSayVoiceOptions {
|
|
|
110
116
|
arch?: NodeJS.Architecture;
|
|
111
117
|
/** The engine's `say --list-voices` union; injected so the routing decision is testable without an engine. */
|
|
112
118
|
listVoices?: (signal?: AbortSignal) => Promise<string[]>;
|
|
119
|
+
detectLanguage?: (text: string, signal?: AbortSignal) => Promise<LangDetectResult | null>;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
function baseLangOf(code: string | undefined): string {
|
|
@@ -145,7 +152,9 @@ async function detectLang(
|
|
|
145
152
|
options: ResolveSayVoiceOptions,
|
|
146
153
|
): Promise<{ code?: string; confidence: number }> {
|
|
147
154
|
if (!text) return { confidence: 0 };
|
|
148
|
-
const detected =
|
|
155
|
+
const detected = options.detectLanguage
|
|
156
|
+
? await options.detectLanguage(text, options.signal)
|
|
157
|
+
: await detectTextLanguageEngine(text, { signal: options.signal });
|
|
149
158
|
return { code: detected?.code, confidence: detected?.confidence ?? 0 };
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -153,7 +162,8 @@ async function detectLang(
|
|
|
153
162
|
* Resolve the voice for a synthesis request. Precedence: explicit voice >
|
|
154
163
|
* explicit language hint (route by the stated language, skipping detection —
|
|
155
164
|
* also the path on Linux/Windows where text-language detection is unavailable) >
|
|
156
|
-
* macOS text-language auto-detection >
|
|
165
|
+
* macOS text-language auto-detection > Russian when detection is inconclusive and most
|
|
166
|
+
* words are Cyrillic (#1387) > engine default (`undefined`). A language
|
|
157
167
|
* hint the build has no voice for resolves to `undefined` (engine default)
|
|
158
168
|
* rather than re-running detection — the user stated the language explicitly.
|
|
159
169
|
*
|
|
@@ -170,6 +180,9 @@ export async function resolveSayVoice(
|
|
|
170
180
|
const { code, confidence } =
|
|
171
181
|
langHint !== undefined ? { code: langHint, confidence: 1 } : await detectLang(text, options);
|
|
172
182
|
const voice = pickVoiceForLang(code, confidence, options.platform, options.arch);
|
|
183
|
+
if (confidence < 0.5 && mostlyCyrillicWords(text)) {
|
|
184
|
+
return pickVoiceForLang("ru", 1, options.platform, options.arch);
|
|
185
|
+
}
|
|
173
186
|
if (confidence < 0.5) return voice;
|
|
174
187
|
return (await nativeScriptOverride(baseLangOf(code), text, options)) ?? voice;
|
|
175
188
|
}
|