@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.
@@ -1,8 +1,11 @@
1
- import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
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
- mkdirSync(dirname(outputPath), { recursive: true });
184
- writeFileSync(outputPath, gzipSync(createTarArchive(files)));
185
-
186
- if (!existsSync(outputPath)) {
187
- throw new Error(`failed to create support bundle: ${outputPath}`);
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: statSync(outputPath).size,
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
- return encodeToon(errors === undefined ? results : { results, errors }) + "\n";
18
+ const payload = errors === undefined ? results : { results, errors };
19
+ return encodeToon(JSON.parse(JSON.stringify(payload))) + "\n";
19
20
  }
@@ -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 = await detectTextLanguageEngine(text, { signal: options.signal });
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 > engine default (`undefined`). A language
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
  }