@drakulavich/kesha-voice-kit 1.3.0 → 1.4.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.
- package/NOTICES.md +67 -0
- package/README.md +40 -6
- package/SKILL.md +70 -24
- package/package.json +5 -3
- package/src/cli/dispatch.ts +40 -0
- package/src/cli/install.ts +108 -0
- package/src/cli/main.ts +177 -0
- package/src/cli/say.ts +119 -0
- package/src/cli/status.ts +12 -0
- package/src/cli.ts +19 -463
- package/src/engine-install.ts +37 -5
- package/src/engine.ts +19 -2
- package/src/format.ts +57 -0
- package/src/lib.ts +14 -0
- package/src/say.ts +1 -1
- package/src/status.ts +102 -1
- package/src/toon.ts +15 -0
- package/src/transcribe.ts +7 -3
- package/src/types.ts +18 -0
- package/src/models.ts +0 -6
package/src/engine-install.ts
CHANGED
|
@@ -76,14 +76,29 @@ async function downloadAVSpeechSidecar(binPath: string, engineVersion: string):
|
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// Keep the best-effort contract: streamResponseToFile throws on an empty
|
|
80
|
+
// body and can fail mid-stream, and chmodSync can throw EPERM. Without
|
|
81
|
+
// this catch a stream/chmod failure would propagate through the tail
|
|
82
|
+
// `await sidecarPromise` in downloadEngine — converting a successful
|
|
83
|
+
// engine install into a thrown exception after log.success already
|
|
84
|
+
// announced it, which is exactly the regression the fetch/404 branches
|
|
85
|
+
// above protect against.
|
|
86
|
+
try {
|
|
87
|
+
await streamResponseToFile(res, sidecarPath, "say-avspeech sidecar");
|
|
88
|
+
chmodSync(sidecarPath, 0o755);
|
|
89
|
+
log.success("AVSpeech sidecar installed (macOS voices available).");
|
|
90
|
+
} catch (e) {
|
|
91
|
+
log.warn(
|
|
92
|
+
`AVSpeech sidecar install failed (${e instanceof Error ? e.message : e}); macos-* voices unavailable.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
export interface InstallOptions {
|
|
85
|
-
/** Also install Kokoro
|
|
98
|
+
/** Also install Kokoro + Piper + ONNX G2P TTS models. */
|
|
86
99
|
tts?: boolean;
|
|
100
|
+
/** Also install Silero VAD model for long-audio preprocessing. */
|
|
101
|
+
vad?: boolean;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
export async function downloadEngine(
|
|
@@ -128,16 +143,32 @@ export async function downloadEngine(
|
|
|
128
143
|
|
|
129
144
|
mkdirSync(dirname(binPath), { recursive: true });
|
|
130
145
|
|
|
146
|
+
// Kick off the sidecar fetch concurrently with the engine fetch. Both
|
|
147
|
+
// target github.com release assets on independent paths with no data
|
|
148
|
+
// dependency, so overlapping the HTTP round-trips saves ~15-30s on a
|
|
149
|
+
// cold install. Sidecar is best-effort (404 on older engines, warn +
|
|
150
|
+
// continue) so a failure doesn't cascade into the engine path.
|
|
151
|
+
const sidecarPromise = downloadAVSpeechSidecar(binPath, engineVersion);
|
|
152
|
+
|
|
131
153
|
let res: Response;
|
|
132
154
|
try {
|
|
133
155
|
res = await fetch(url, { redirect: "follow" });
|
|
134
156
|
} catch (e) {
|
|
157
|
+
// Attach a no-op rejection handler to the sidecar promise as
|
|
158
|
+
// defense-in-depth. Today it can't reject (downloadAVSpeechSidecar
|
|
159
|
+
// catches all of its own errors), but if that contract ever drifts
|
|
160
|
+
// a bare dangling promise would surface as an unhandledRejection
|
|
161
|
+
// while we're throwing the engine error. Not waiting — the engine
|
|
162
|
+
// error is what the user needs to see now; sidecar's own logs
|
|
163
|
+
// will print whenever they finish.
|
|
164
|
+
sidecarPromise.catch(() => {});
|
|
135
165
|
throw new Error(
|
|
136
166
|
`Failed to fetch engine binary: ${e instanceof Error ? e.message : e}\n Fix: Check your network connection and try again`,
|
|
137
167
|
);
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
if (!res.ok) {
|
|
171
|
+
sidecarPromise.catch(() => {});
|
|
141
172
|
throw new Error(
|
|
142
173
|
`Failed to download engine binary (HTTP ${res.status})\n Fix: Check https://github.com/${GITHUB_REPO}/releases for available versions`,
|
|
143
174
|
);
|
|
@@ -147,7 +178,7 @@ export async function downloadEngine(
|
|
|
147
178
|
chmodSync(binPath, 0o755);
|
|
148
179
|
writeInstalledEngineVersion(binPath, engineVersion);
|
|
149
180
|
log.success(`Engine binary downloaded (v${engineVersion}).`);
|
|
150
|
-
await
|
|
181
|
+
await sidecarPromise;
|
|
151
182
|
}
|
|
152
183
|
|
|
153
184
|
if (backend) {
|
|
@@ -164,6 +195,7 @@ export async function downloadEngine(
|
|
|
164
195
|
"install",
|
|
165
196
|
...(noCache ? ["--no-cache"] : []),
|
|
166
197
|
...(options.tts ? ["--tts"] : []),
|
|
198
|
+
...(options.vad ? ["--vad"] : []),
|
|
167
199
|
];
|
|
168
200
|
const proc = Bun.spawnSync([binPath, ...installArgs], {
|
|
169
201
|
stdout: "pipe",
|
package/src/engine.ts
CHANGED
|
@@ -42,8 +42,25 @@ async function runEngine(args: string[]): Promise<{ stdout: string; stderr: stri
|
|
|
42
42
|
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/** VAD preprocessing selector.
|
|
46
|
+
* - `"auto"` (default): engine decides — VAD when audio ≥ 120 s and model installed
|
|
47
|
+
* - `"on"`: force VAD (requires `kesha install --vad`)
|
|
48
|
+
* - `"off"`: force full-file pass regardless of duration or install state
|
|
49
|
+
*/
|
|
50
|
+
export type VadMode = "auto" | "on" | "off";
|
|
51
|
+
|
|
52
|
+
export interface TranscribeEngineOptions {
|
|
53
|
+
vad?: VadMode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function transcribeEngine(
|
|
57
|
+
audioPath: string,
|
|
58
|
+
opts: TranscribeEngineOptions = {},
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
const args = ["transcribe", audioPath];
|
|
61
|
+
if (opts.vad === "on") args.push("--vad");
|
|
62
|
+
else if (opts.vad === "off") args.push("--no-vad");
|
|
63
|
+
const { stdout, stderr, exitCode } = await runEngine(args);
|
|
47
64
|
if (exitCode !== 0) {
|
|
48
65
|
throw new Error(stderr || `kesha-engine exited with code ${exitCode}`);
|
|
49
66
|
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { TranscribeResult } from "./types";
|
|
2
|
+
|
|
3
|
+
export function formatTextOutput(results: TranscribeResult[]): string {
|
|
4
|
+
if (results.length === 1) {
|
|
5
|
+
return results[0].text + "\n";
|
|
6
|
+
}
|
|
7
|
+
return results
|
|
8
|
+
.map((r, i) => (i > 0 ? "\n" : "") + `=== ${r.file} ===\n${r.text}\n`)
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatVerboseOutput(results: TranscribeResult[]): string {
|
|
13
|
+
return results
|
|
14
|
+
.map((r, i) => {
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
if (results.length > 1) {
|
|
17
|
+
if (i > 0) lines.push("");
|
|
18
|
+
lines.push(`=== ${r.file} ===`);
|
|
19
|
+
}
|
|
20
|
+
if (r.audioLanguage) {
|
|
21
|
+
lines.push(`Audio language: ${r.audioLanguage.code} (confidence: ${r.audioLanguage.confidence.toFixed(2)})`);
|
|
22
|
+
}
|
|
23
|
+
const textLang = r.textLanguage ?? (r.lang ? { code: r.lang, confidence: 0 } : null);
|
|
24
|
+
if (textLang) {
|
|
25
|
+
const confStr = textLang.confidence > 0 ? ` (confidence: ${textLang.confidence.toFixed(2)})` : "";
|
|
26
|
+
lines.push(`Text language: ${textLang.code}${confStr}`);
|
|
27
|
+
}
|
|
28
|
+
if (r.sttTimeMs !== undefined) {
|
|
29
|
+
lines.push(`STT time: ${r.sttTimeMs}ms`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("---");
|
|
32
|
+
lines.push(r.text);
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
})
|
|
35
|
+
.join("\n") + "\n";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatTranscriptOutput(results: TranscribeResult[]): string {
|
|
39
|
+
return results
|
|
40
|
+
.map((r, i) => {
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
if (results.length > 1) {
|
|
43
|
+
if (i > 0) lines.push("");
|
|
44
|
+
lines.push(`=== ${r.file} ===`);
|
|
45
|
+
}
|
|
46
|
+
lines.push(r.text);
|
|
47
|
+
const lang = r.textLanguage?.code || r.audioLanguage?.code || r.lang;
|
|
48
|
+
const confidence = r.textLanguage?.confidence ?? r.audioLanguage?.confidence;
|
|
49
|
+
if (lang) lines.push(`[lang: ${lang}${confidence != null ? `, confidence: ${confidence.toFixed(2)}` : ""}]`);
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
})
|
|
52
|
+
.join("\n") + "\n";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatJsonOutput(results: TranscribeResult[]): string {
|
|
56
|
+
return JSON.stringify(results, null, 2) + "\n";
|
|
57
|
+
}
|
package/src/lib.ts
CHANGED
|
@@ -6,6 +6,20 @@ export type { TranscribeOptions };
|
|
|
6
6
|
export { downloadEngine as downloadModel };
|
|
7
7
|
export { say, type SayOptions, SayError } from "./say";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Encode a `TranscribeResult[]` as TOON (#138). Same data shape as the
|
|
11
|
+
* `--json` / `--toon` CLI output; the CLI reads from stdin of a transcribe
|
|
12
|
+
* run, this helper is for programmatic callers that already have the array.
|
|
13
|
+
*/
|
|
14
|
+
export { formatToonOutput as toToon } from "./toon";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Output shape returned by `kesha --json` and the input shape expected by
|
|
18
|
+
* `toToon`. Lives in `./types` (since #179) so the public API stops
|
|
19
|
+
* reaching into the CLI-layer file.
|
|
20
|
+
*/
|
|
21
|
+
export type { TranscribeResult } from "./types";
|
|
22
|
+
|
|
9
23
|
/** Install Kokoro TTS models. Shorthand for `downloadModel({ tts: true })`. */
|
|
10
24
|
export async function downloadTts(noCache = false): Promise<void> {
|
|
11
25
|
await downloadEngine(noCache, undefined, { tts: true });
|
package/src/say.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface SayOptions {
|
|
|
10
10
|
text?: string;
|
|
11
11
|
/** Voice id, e.g. `en-af_heart`. Defaults to engine default. */
|
|
12
12
|
voice?: string;
|
|
13
|
-
/** Override the voice's default
|
|
13
|
+
/** Override the voice's default BCP 47 language code (e.g. `en-us`, `ru`). */
|
|
14
14
|
lang?: string;
|
|
15
15
|
/** Write audio to this path instead of returning bytes. */
|
|
16
16
|
out?: string;
|
package/src/status.ts
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
1
|
-
import { readdirSync } from "fs";
|
|
1
|
+
import { readdirSync, statSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { isEngineInstalled, getEngineBinPath, getEngineCapabilities } from "./engine";
|
|
5
5
|
import { log } from "./log";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
|
|
8
|
+
function humanBytes(bytes: number): string {
|
|
9
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
10
|
+
const units = ["KB", "MB", "GB"];
|
|
11
|
+
let n = bytes / 1024;
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
14
|
+
n /= 1024;
|
|
15
|
+
i++;
|
|
16
|
+
}
|
|
17
|
+
return `${n.toFixed(n >= 100 ? 0 : 1)} ${units[i]}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dirSizeBytes(path: string): number {
|
|
21
|
+
let total = 0;
|
|
22
|
+
try {
|
|
23
|
+
const st = statSync(path);
|
|
24
|
+
if (st.isFile()) return st.size;
|
|
25
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
26
|
+
const p = join(path, entry.name);
|
|
27
|
+
total += entry.isDirectory() ? dirSizeBytes(p) : statSync(p).size;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
/* missing path — component not installed */
|
|
31
|
+
}
|
|
32
|
+
return total;
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
export function formatStatusLine(
|
|
9
36
|
label: string,
|
|
10
37
|
path: string | null,
|
|
@@ -38,6 +65,10 @@ export async function showStatus(): Promise<void> {
|
|
|
38
65
|
|
|
39
66
|
log.info(formatStatusLine("Runtime", `Bun ${Bun.version}`, true));
|
|
40
67
|
log.info(formatStatusLine("Platform", `${process.platform} ${process.arch}`, true));
|
|
68
|
+
const mirror = activeModelMirror();
|
|
69
|
+
if (mirror) {
|
|
70
|
+
log.info(formatStatusLine("Mirror", mirror, true));
|
|
71
|
+
}
|
|
41
72
|
log.info("");
|
|
42
73
|
|
|
43
74
|
if (installed) {
|
|
@@ -49,6 +80,8 @@ export async function showStatus(): Promise<void> {
|
|
|
49
80
|
}
|
|
50
81
|
log.info("");
|
|
51
82
|
}
|
|
83
|
+
|
|
84
|
+
showDiskUsage(binPath);
|
|
52
85
|
}
|
|
53
86
|
|
|
54
87
|
if (!installed) {
|
|
@@ -56,10 +89,78 @@ export async function showStatus(): Promise<void> {
|
|
|
56
89
|
}
|
|
57
90
|
}
|
|
58
91
|
|
|
92
|
+
function showDiskUsage(binPath: string): void {
|
|
93
|
+
const cache = kesheCacheDir();
|
|
94
|
+
// Engine binary lives under `<cache>/engine/bin/` (managed by the TS CLI's
|
|
95
|
+
// engine-install) while all models live under `<cache>/models/` (managed by
|
|
96
|
+
// the Rust engine). Point at `engine/` (two levels up from the binary) so
|
|
97
|
+
// any future sibling files under that root (metadata, hooks, etc.) are
|
|
98
|
+
// counted too.
|
|
99
|
+
const engineDir = join(binPath, "..", "..");
|
|
100
|
+
|
|
101
|
+
const components: Array<{ label: string; path: string }> = [
|
|
102
|
+
{ label: "Engine", path: engineDir },
|
|
103
|
+
{ label: "ASR (Parakeet)", path: join(cache, "models/parakeet-tdt-v3") },
|
|
104
|
+
{ label: "Language ID", path: join(cache, "models/lang-id-ecapa") },
|
|
105
|
+
{ label: "VAD (Silero)", path: join(cache, "models/silero-vad") },
|
|
106
|
+
{ label: "TTS (Kokoro)", path: join(cache, "models/kokoro-82m") },
|
|
107
|
+
{ label: "TTS (Piper)", path: join(cache, "models/piper-ru") },
|
|
108
|
+
{ label: "TTS (G2P)", path: join(cache, "models/g2p") },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const rows: Array<{ label: string; size: number }> = [];
|
|
112
|
+
for (const c of components) {
|
|
113
|
+
const size = dirSizeBytes(c.path);
|
|
114
|
+
if (size > 0) rows.push({ label: c.label, size });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (rows.length === 0) return;
|
|
118
|
+
|
|
119
|
+
// Total counts everything under both the model cache root AND the engine
|
|
120
|
+
// dir (which may live outside the cache when `KESHA_ENGINE_BIN` overrides
|
|
121
|
+
// the default layout). That way the number matches what the `rm -rf` hint
|
|
122
|
+
// below would actually free, including any temp downloads or future
|
|
123
|
+
// components not in the per-row list.
|
|
124
|
+
const componentTotal = rows.reduce((n, r) => n + r.size, 0);
|
|
125
|
+
const cacheTotal = dirSizeBytes(cache);
|
|
126
|
+
const engineOutsideCache = engineDir.startsWith(cache)
|
|
127
|
+
? 0
|
|
128
|
+
: dirSizeBytes(engineDir);
|
|
129
|
+
const total = cacheTotal + engineOutsideCache;
|
|
130
|
+
|
|
131
|
+
log.info(`Disk usage (${cache}):`);
|
|
132
|
+
const labelWidth = Math.max(...rows.map((r) => r.label.length), "Total".length);
|
|
133
|
+
for (const r of rows) {
|
|
134
|
+
const pad = " ".repeat(labelWidth - r.label.length + 2);
|
|
135
|
+
log.info(` ${r.label}:${pad}${humanBytes(r.size)}`);
|
|
136
|
+
}
|
|
137
|
+
const totalPad = " ".repeat(labelWidth - "Total".length + 2);
|
|
138
|
+
log.info(` ${pc.bold("Total")}:${totalPad}${pc.bold(humanBytes(total))}`);
|
|
139
|
+
if (total > componentTotal) {
|
|
140
|
+
const other = total - componentTotal;
|
|
141
|
+
log.info(pc.dim(` (includes ${humanBytes(other)} of other cache files)`));
|
|
142
|
+
}
|
|
143
|
+
log.info("");
|
|
144
|
+
log.info(pc.dim(` To reset cache: rm -rf ${cache} — next \`kesha install\` re-downloads.`));
|
|
145
|
+
log.info("");
|
|
146
|
+
}
|
|
147
|
+
|
|
59
148
|
function kesheCacheDir(): string {
|
|
60
149
|
return process.env.KESHA_CACHE_DIR ?? join(homedir(), ".cache", "kesha");
|
|
61
150
|
}
|
|
62
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Read the effective `KESHA_MODEL_MIRROR` base URL (#121). Returns null when
|
|
154
|
+
* unset, empty, or whitespace. Matches the Rust side's `model_mirror()` in
|
|
155
|
+
* `rust/src/models.rs` — keeping them in lockstep lets `kesha status`
|
|
156
|
+
* surface the exact URL the engine will hit on the next `kesha install`.
|
|
157
|
+
*/
|
|
158
|
+
export function activeModelMirror(): string | null {
|
|
159
|
+
const raw = process.env.KESHA_MODEL_MIRROR ?? "";
|
|
160
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
161
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
63
164
|
function listInstalledVoices(): string[] {
|
|
64
165
|
const cache = kesheCacheDir();
|
|
65
166
|
const voices: string[] = [];
|
package/src/toon.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { encode as encodeToon } from "@toon-format/toon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TOON (#138) encoding of an array of transcription results. Same data shape
|
|
5
|
+
* as `formatJsonOutput`; lossless round-trip through `@toon-format/toon`'s
|
|
6
|
+
* `decode()`.
|
|
7
|
+
*
|
|
8
|
+
* Generic so this module stays independent of `TranscribeResult` (defined in
|
|
9
|
+
* `cli.ts`) and doesn't introduce a runtime cycle between `lib.ts` and
|
|
10
|
+
* `cli.ts`. Callers that have the concrete `TranscribeResult[]` type will
|
|
11
|
+
* still get a type-checked call via structural compatibility.
|
|
12
|
+
*/
|
|
13
|
+
export function formatToonOutput<T extends object>(results: T[]): string {
|
|
14
|
+
return encodeToon(results) + "\n";
|
|
15
|
+
}
|
package/src/transcribe.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { isEngineInstalled, transcribeEngine } from "./engine";
|
|
1
|
+
import { isEngineInstalled, transcribeEngine, type VadMode } from "./engine";
|
|
2
|
+
|
|
3
|
+
export type { VadMode };
|
|
2
4
|
|
|
3
5
|
export interface TranscribeOptions {
|
|
4
6
|
silent?: boolean;
|
|
7
|
+
/** Silero VAD preprocessing selector. Defaults to `"auto"`. */
|
|
8
|
+
vad?: VadMode;
|
|
5
9
|
}
|
|
6
10
|
|
|
7
|
-
export async function transcribe(audioPath: string,
|
|
11
|
+
export async function transcribe(audioPath: string, opts: TranscribeOptions = {}): Promise<string> {
|
|
8
12
|
if (!isEngineInstalled()) {
|
|
9
13
|
throw new Error(
|
|
10
14
|
"Error: No transcription backend is installed\n\n" +
|
|
@@ -16,5 +20,5 @@ export async function transcribe(audioPath: string, _opts: TranscribeOptions = {
|
|
|
16
20
|
);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
return transcribeEngine(audioPath);
|
|
23
|
+
return transcribeEngine(audioPath, { vad: opts.vad });
|
|
20
24
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LangDetectResult } from "./engine";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One row of `kesha --json` / `kesha --toon` output. Canonical output shape
|
|
5
|
+
* for programmatic callers of `transcribe(...)` + `toToon(...)` in the
|
|
6
|
+
* public API (`@drakulavich/kesha-voice-kit/core`).
|
|
7
|
+
*/
|
|
8
|
+
export type TranscribeResult = {
|
|
9
|
+
file: string;
|
|
10
|
+
text: string;
|
|
11
|
+
lang: string;
|
|
12
|
+
audioLanguage?: LangDetectResult;
|
|
13
|
+
textLanguage?: LangDetectResult;
|
|
14
|
+
/** Wall-clock time around the engine subprocess calls for this file, ms. See #139. */
|
|
15
|
+
sttTimeMs?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type { LangDetectResult };
|