@drakulavich/kesha-voice-kit 1.24.7 → 1.25.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/README.md +1 -0
- package/completions/kesha.bash +1 -1
- package/completions/kesha.fish +1 -0
- package/completions/kesha.zsh +2 -1
- package/man/kesha.1 +3 -0
- package/model-plan.json +43 -0
- package/package.json +4 -3
- package/src/cli/status.ts +15 -2
- package/src/engine.ts +14 -1
- package/src/install-plan.ts +25 -64
- package/src/status.ts +110 -47
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ kesha --json --timestamps audio.ogg # JSON with timestamped segments
|
|
|
57
57
|
kesha --toon audio.ogg # compact LLM-friendly TOON
|
|
58
58
|
kesha status # show installed backend info
|
|
59
59
|
kesha status --disk # + recursive cache disk usage
|
|
60
|
+
kesha status --json # machine-readable, for scripts
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
Multiple files get `head`-style headers; stdout is the transcript, stderr is errors — pipe-friendly:
|
package/completions/kesha.bash
CHANGED
|
@@ -28,7 +28,7 @@ _kesha_completion() {
|
|
|
28
28
|
record) opts="--help -h --out --max-seconds --debug" ;;
|
|
29
29
|
say) opts="--help -h --voice --lang --out --rate --list-voices --ssml --format --bitrate --sample-rate --no-expand-abbrev --verbose --debug" ;;
|
|
30
30
|
stats) opts="--help -h --format" ;;
|
|
31
|
-
status) opts="--help -h --disk" ;;
|
|
31
|
+
status) opts="--help -h --disk --json" ;;
|
|
32
32
|
support-bundle) opts="--help -h --output --include-logs" ;;
|
|
33
33
|
*) opts="--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad --quiet --no-color" ;;
|
|
34
34
|
esac
|
package/completions/kesha.fish
CHANGED
|
@@ -86,6 +86,7 @@ complete -c kesha -n '__fish_seen_subcommand_from stats' -l format -r -d 'Export
|
|
|
86
86
|
complete -c kesha -n '__fish_seen_subcommand_from status' -l help -d 'Show help'
|
|
87
87
|
complete -c kesha -n '__fish_seen_subcommand_from status' -s h -d 'Show help'
|
|
88
88
|
complete -c kesha -n '__fish_seen_subcommand_from status' -l disk -d 'Include recursive cache disk usage'
|
|
89
|
+
complete -c kesha -n '__fish_seen_subcommand_from status' -l json -d 'Output status as JSON'
|
|
89
90
|
complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l help -d 'Show help'
|
|
90
91
|
complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -s h -d 'Show help'
|
|
91
92
|
complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l output -r -d 'Write archive to this .tar.gz path'
|
package/completions/kesha.zsh
CHANGED
|
@@ -121,7 +121,8 @@ _kesha() {
|
|
|
121
121
|
status)
|
|
122
122
|
_arguments '--help[Show help]' \
|
|
123
123
|
'-h[Show help]' \
|
|
124
|
-
'--disk[Include recursive cache disk usage]'
|
|
124
|
+
'--disk[Include recursive cache disk usage]' \
|
|
125
|
+
'--json[Output status as JSON]'
|
|
125
126
|
;;
|
|
126
127
|
support-bundle)
|
|
127
128
|
_arguments '--help[Show help]' \
|
package/man/kesha.1
CHANGED
package/model-plan.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"asr": [
|
|
3
|
+
{ "relPath": "models/parakeet-tdt-v3/encoder-model.onnx", "sizeBytes": 41770866 },
|
|
4
|
+
{ "relPath": "models/parakeet-tdt-v3/encoder-model.onnx.data", "sizeBytes": 2435420160 },
|
|
5
|
+
{ "relPath": "models/parakeet-tdt-v3/decoder_joint-model.onnx", "sizeBytes": 72520893 },
|
|
6
|
+
{ "relPath": "models/parakeet-tdt-v3/nemo128.onnx", "sizeBytes": 139764 },
|
|
7
|
+
{ "relPath": "models/parakeet-tdt-v3/vocab.txt", "sizeBytes": 93939 }
|
|
8
|
+
],
|
|
9
|
+
"langId": [
|
|
10
|
+
{ "relPath": "models/lang-id-ecapa/lang-id-ecapa.onnx", "sizeBytes": 759814 },
|
|
11
|
+
{ "relPath": "models/lang-id-ecapa/lang-id-ecapa.onnx.data", "sizeBytes": 85327872 },
|
|
12
|
+
{ "relPath": "models/lang-id-ecapa/labels.json", "sizeBytes": 646 }
|
|
13
|
+
],
|
|
14
|
+
"vad": [
|
|
15
|
+
{ "relPath": "models/silero-vad/silero_vad.onnx", "sizeBytes": 2327524 }
|
|
16
|
+
],
|
|
17
|
+
"g2pCharsiu": [
|
|
18
|
+
{ "relPath": "models/g2p/byt5-tiny/encoder_model.onnx", "sizeBytes": 12478704 },
|
|
19
|
+
{ "relPath": "models/g2p/byt5-tiny/decoder_model.onnx", "sizeBytes": 11983268 },
|
|
20
|
+
{ "relPath": "models/g2p/byt5-tiny/decoder_with_past_model.onnx", "sizeBytes": 5427260 }
|
|
21
|
+
],
|
|
22
|
+
"kokoroGraph": { "relPath": "models/kokoro-82m/model.onnx", "sizeBytes": 325532387 },
|
|
23
|
+
"kokoroVoices": {
|
|
24
|
+
"en": { "relPath": "models/kokoro-82m/voices/am_michael.bin", "sizeBytes": 522240 },
|
|
25
|
+
"es": { "relPath": "models/kokoro-82m/voices/em_alex.bin", "sizeBytes": 522240 },
|
|
26
|
+
"fr": { "relPath": "models/kokoro-82m/voices/ff_siwis.bin", "sizeBytes": 522240 },
|
|
27
|
+
"it": { "relPath": "models/kokoro-82m/voices/im_nicola.bin", "sizeBytes": 522240 },
|
|
28
|
+
"pt": { "relPath": "models/kokoro-82m/voices/pm_alex.bin", "sizeBytes": 522240 }
|
|
29
|
+
},
|
|
30
|
+
"voskRu": [
|
|
31
|
+
{ "relPath": "models/vosk-ru/model.onnx", "sizeBytes": 179314533 },
|
|
32
|
+
{ "relPath": "models/vosk-ru/dictionary", "sizeBytes": 101431118 },
|
|
33
|
+
{ "relPath": "models/vosk-ru/config.json", "sizeBytes": 1518 },
|
|
34
|
+
{ "relPath": "models/vosk-ru/bert/model.onnx", "sizeBytes": 654361598 },
|
|
35
|
+
{ "relPath": "models/vosk-ru/bert/vocab.txt", "sizeBytes": 1780720 }
|
|
36
|
+
],
|
|
37
|
+
"diarize": [
|
|
38
|
+
{ "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Manifest.json", "sizeBytes": 617 },
|
|
39
|
+
{ "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/model.mlmodel", "sizeBytes": 7080357 },
|
|
40
|
+
{ "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/weights/0-weight.bin", "sizeBytes": 5930400 },
|
|
41
|
+
{ "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/weights/1-weight.bin", "sizeBytes": 232161600 }
|
|
42
|
+
]
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Give your tools a voice — speech to text and back, 25 languages, up to ~19× faster than Whisper. On your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"completions/",
|
|
16
16
|
"man/",
|
|
17
17
|
"src/",
|
|
18
|
+
"model-plan.json",
|
|
18
19
|
"package.json",
|
|
19
20
|
"tsconfig.json",
|
|
20
21
|
"openclaw.plugin.json",
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"scripts": {
|
|
44
45
|
"test": "bun test --timeout 30000",
|
|
45
46
|
"test:watch": "bun test --timeout 30000 --watch",
|
|
46
|
-
"test:unit": "bun test tests/unit/",
|
|
47
|
+
"test:unit": "bun test --timeout 30000 tests/unit/",
|
|
47
48
|
"test:cli-fast": "bun test --timeout 30000 tests/unit/ tests/integration/cli-contracts.test.ts tests/integration/e2e-cli.test.ts tests/integration/say.test.ts",
|
|
48
49
|
"test:integration": "bun test --timeout 30000 tests/integration/",
|
|
49
50
|
"coverage:ts": "bun run test:cli-fast --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage/ts",
|
|
@@ -92,7 +93,7 @@
|
|
|
92
93
|
"dependencies": {
|
|
93
94
|
"@clack/prompts": "^1.5.0",
|
|
94
95
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
95
|
-
"@toon-format/toon": "^
|
|
96
|
+
"@toon-format/toon": "^4.1.0",
|
|
96
97
|
"citty": "^0.2.2",
|
|
97
98
|
"fastest-levenshtein": "^1.0.16",
|
|
98
99
|
"picocolors": "^1.1.1",
|
package/src/cli/status.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
-
import {
|
|
2
|
+
import { collectStatus, renderStatus } from "../status";
|
|
3
3
|
|
|
4
4
|
interface StatusCommandArgs {
|
|
5
5
|
disk: boolean;
|
|
6
|
+
json: boolean;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export const statusCommand = defineCommand({
|
|
@@ -16,8 +17,20 @@ export const statusCommand = defineCommand({
|
|
|
16
17
|
description: "Include recursive cache disk usage",
|
|
17
18
|
default: false,
|
|
18
19
|
},
|
|
20
|
+
json: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
description: "Output status as JSON",
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
19
25
|
},
|
|
20
26
|
async run({ args }: { args: StatusCommandArgs }) {
|
|
21
|
-
await
|
|
27
|
+
const report = await collectStatus({ disk: args.disk });
|
|
28
|
+
if (args.json) {
|
|
29
|
+
// `log.*` writes to stdout, so the payload must be the only thing printed —
|
|
30
|
+
// the setup hint ships inside it instead of on stderr (#647).
|
|
31
|
+
console.log(JSON.stringify(report, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
renderStatus(report);
|
|
22
35
|
},
|
|
23
36
|
});
|
package/src/engine.ts
CHANGED
|
@@ -364,10 +364,23 @@ export async function getEngineCapabilities(): Promise<EngineCapabilities | null
|
|
|
364
364
|
const { stdout, exitCode } = await runEngine(["--capabilities-json"]);
|
|
365
365
|
if (exitCode !== 0) return null;
|
|
366
366
|
try {
|
|
367
|
-
const capabilities = JSON.parse(stdout)
|
|
367
|
+
const capabilities = parseCapabilities(JSON.parse(stdout));
|
|
368
|
+
if (!capabilities) return null;
|
|
368
369
|
cachedEngineCapabilities = { binPath, mtime, capabilities };
|
|
369
370
|
return capabilities;
|
|
370
371
|
} catch {
|
|
371
372
|
return null;
|
|
372
373
|
}
|
|
373
374
|
}
|
|
375
|
+
|
|
376
|
+
// A non-null return has to mean "the engine described itself" — callers reach
|
|
377
|
+
// straight for `.features.join()` and `.backend` (#647). Only the fields those
|
|
378
|
+
// callers consume are checked, so a newer engine adding fields still validates.
|
|
379
|
+
function parseCapabilities(parsed: unknown): EngineCapabilities | null {
|
|
380
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
381
|
+
const { protocolVersion, backend, features } = parsed as Record<string, unknown>;
|
|
382
|
+
if (typeof protocolVersion !== "number") return null;
|
|
383
|
+
if (typeof backend !== "string") return null;
|
|
384
|
+
if (!Array.isArray(features) || features.some((f) => typeof f !== "string")) return null;
|
|
385
|
+
return parsed as EngineCapabilities;
|
|
386
|
+
}
|
package/src/install-plan.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
fluidKokoroCachePath,
|
|
12
12
|
isDarwinArm64,
|
|
13
13
|
} from "./fluid-kokoro-cache";
|
|
14
|
+
import modelPlan from "../model-plan.json" with { type: "json" };
|
|
14
15
|
|
|
15
16
|
export interface InstallPlanOptions {
|
|
16
17
|
noCache?: boolean;
|
|
@@ -26,6 +27,17 @@ interface PlanFile {
|
|
|
26
27
|
sizeBytes: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
interface ModelPlan {
|
|
31
|
+
asr: PlanFile[];
|
|
32
|
+
langId: PlanFile[];
|
|
33
|
+
vad: PlanFile[];
|
|
34
|
+
g2pCharsiu: PlanFile[];
|
|
35
|
+
kokoroGraph: PlanFile;
|
|
36
|
+
kokoroVoices: Record<string, PlanFile>;
|
|
37
|
+
voskRu: PlanFile[];
|
|
38
|
+
diarize: PlanFile[];
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
interface PlanComponent {
|
|
30
42
|
name: string;
|
|
31
43
|
source: string;
|
|
@@ -45,46 +57,19 @@ interface ReleaseAssetSpec {
|
|
|
45
57
|
sizeBytes: number;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{ relPath: "models/lang-id-ecapa/labels.json", sizeBytes: 646 },
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
const VAD_FILES: PlanFile[] = [
|
|
65
|
-
{ relPath: "models/silero-vad/silero_vad.onnx", sizeBytes: 2_327_524 },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
// klebster CharsiuG2P byt5-tiny ONNX export (CC-BY 4.0 — see NOTICES.md). Multilingual G2P for es/fr/it/pt (#212).
|
|
69
|
-
const G2P_CHARSIU_FILES: PlanFile[] = [
|
|
70
|
-
{ relPath: "models/g2p/byt5-tiny/encoder_model.onnx", sizeBytes: 12_478_704 },
|
|
71
|
-
{ relPath: "models/g2p/byt5-tiny/decoder_model.onnx", sizeBytes: 11_983_268 },
|
|
72
|
-
{ relPath: "models/g2p/byt5-tiny/decoder_with_past_model.onnx", sizeBytes: 5_427_260 },
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
// Kokoro ONNX graph (shared by all Kokoro languages on non-darwin builds).
|
|
76
|
-
const KOKORO_GRAPH_FILE: PlanFile = { relPath: "models/kokoro-82m/model.onnx", sizeBytes: 325_532_387 };
|
|
77
|
-
|
|
78
|
-
// Multilingual voice packs for es/fr/it/pt (#212).
|
|
79
|
-
// em_alex (es, male), ff_siwis (fr, female — only French voice in Kokoro v1.0),
|
|
80
|
-
// im_nicola (it, male), pm_alex (pt, male).
|
|
81
|
-
const KOKORO_VOICE_FILES: Record<string, PlanFile> = {
|
|
82
|
-
en: { relPath: "models/kokoro-82m/voices/am_michael.bin", sizeBytes: 522_240 },
|
|
83
|
-
es: { relPath: "models/kokoro-82m/voices/em_alex.bin", sizeBytes: 522_240 },
|
|
84
|
-
fr: { relPath: "models/kokoro-82m/voices/ff_siwis.bin", sizeBytes: 522_240 },
|
|
85
|
-
it: { relPath: "models/kokoro-82m/voices/im_nicola.bin", sizeBytes: 522_240 },
|
|
86
|
-
pt: { relPath: "models/kokoro-82m/voices/pm_alex.bin", sizeBytes: 522_240 },
|
|
87
|
-
};
|
|
60
|
+
// Shared plan metadata keeps `kesha install --plan` usable before the engine
|
|
61
|
+
// exists. Rust tests guard these cache paths against its pinned manifests.
|
|
62
|
+
const plan: ModelPlan = modelPlan;
|
|
63
|
+
const {
|
|
64
|
+
asr: ASR_FILES,
|
|
65
|
+
langId: LANG_ID_FILES,
|
|
66
|
+
vad: VAD_FILES,
|
|
67
|
+
g2pCharsiu: G2P_CHARSIU_FILES,
|
|
68
|
+
kokoroGraph: KOKORO_GRAPH_FILE,
|
|
69
|
+
kokoroVoices: KOKORO_VOICE_FILES,
|
|
70
|
+
voskRu: VOSK_RU_FILES,
|
|
71
|
+
diarize: DIARIZE_FILES,
|
|
72
|
+
} = plan;
|
|
88
73
|
|
|
89
74
|
function kokoroPlanFiles(langs: string[]): PlanFile[] {
|
|
90
75
|
const files: PlanFile[] = [KOKORO_GRAPH_FILE];
|
|
@@ -95,30 +80,6 @@ function kokoroPlanFiles(langs: string[]): PlanFile[] {
|
|
|
95
80
|
return files;
|
|
96
81
|
}
|
|
97
82
|
|
|
98
|
-
const VOSK_RU_FILES: PlanFile[] = [
|
|
99
|
-
{ relPath: "models/vosk-ru/model.onnx", sizeBytes: 179_314_533 },
|
|
100
|
-
{ relPath: "models/vosk-ru/dictionary", sizeBytes: 101_431_118 },
|
|
101
|
-
{ relPath: "models/vosk-ru/config.json", sizeBytes: 1_518 },
|
|
102
|
-
{ relPath: "models/vosk-ru/bert/model.onnx", sizeBytes: 654_361_598 },
|
|
103
|
-
{ relPath: "models/vosk-ru/bert/vocab.txt", sizeBytes: 1_780_720 },
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
const DIARIZE_FILES: PlanFile[] = [
|
|
107
|
-
{ relPath: "models/diarize/SortformerNvidiaLow_v2.mlpackage/Manifest.json", sizeBytes: 617 },
|
|
108
|
-
{
|
|
109
|
-
relPath: "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
|
110
|
-
sizeBytes: 7_080_357,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
relPath: "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/weights/0-weight.bin",
|
|
114
|
-
sizeBytes: 5_930_400,
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
relPath: "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/weights/1-weight.bin",
|
|
118
|
-
sizeBytes: 232_161_600,
|
|
119
|
-
},
|
|
120
|
-
];
|
|
121
|
-
|
|
122
83
|
// The sidecar list (and the asset-name → installed-filename mapping) lives in
|
|
123
84
|
// SIDECARS in engine-install.ts; only the release-asset sizes are pinned here,
|
|
124
85
|
// like the model tables above.
|
package/src/status.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isEngineInstalled,
|
|
5
|
+
getEngineBinPath,
|
|
6
|
+
getEngineCapabilities,
|
|
7
|
+
type EngineCapabilities,
|
|
8
|
+
} from "./engine";
|
|
4
9
|
import { installHint } from "./install-hint";
|
|
5
10
|
import { log } from "./log";
|
|
11
|
+
import { packageVersion } from "./package-info";
|
|
6
12
|
import { keshaCacheDir } from "./paths";
|
|
7
13
|
import { fluidKokoroCacheInfo } from "./fluid-kokoro-cache";
|
|
8
14
|
import { dirSizeBytes } from "./diagnostic-paths";
|
|
@@ -35,8 +41,60 @@ export interface ShowStatusOptions {
|
|
|
35
41
|
disk?: boolean;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
export interface StatusDiskComponent {
|
|
45
|
+
label: string;
|
|
46
|
+
sizeBytes: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StatusDiskUsage {
|
|
50
|
+
cachePath: string;
|
|
51
|
+
components: StatusDiskComponent[];
|
|
52
|
+
componentTotalBytes: number;
|
|
53
|
+
totalBytes: number;
|
|
54
|
+
fluidKokoro: { path: string; sizeBytes: number } | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Machine-readable `kesha status` payload (#647). Contract: every key is always
|
|
59
|
+
* present — absent values are null (or `[]` for voices), never omitted, so a
|
|
60
|
+
* consumer never distinguishes a missing key from a null value. `engine.installed`
|
|
61
|
+
* reports only that a binary exists; usability additionally requires
|
|
62
|
+
* `engine.capabilities` to be non-null.
|
|
63
|
+
*/
|
|
64
|
+
export interface StatusReport {
|
|
65
|
+
cliVersion: string;
|
|
66
|
+
engine: {
|
|
67
|
+
installed: boolean;
|
|
68
|
+
path: string;
|
|
69
|
+
capabilities: EngineCapabilities | null;
|
|
70
|
+
};
|
|
71
|
+
voices: string[];
|
|
72
|
+
runtime: { bun: string; platform: string; arch: string };
|
|
73
|
+
modelMirror: string | null;
|
|
74
|
+
hint: string | null;
|
|
75
|
+
disk: StatusDiskUsage | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function collectStatus(options: ShowStatusOptions = {}): Promise<StatusReport> {
|
|
79
|
+
const path = getEngineBinPath();
|
|
80
|
+
const installed = isEngineInstalled();
|
|
81
|
+
const capabilities = installed ? await getEngineCapabilities().catch(() => null) : null;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
cliVersion: packageVersion,
|
|
85
|
+
engine: { installed, path, capabilities },
|
|
86
|
+
voices: installed ? listInstalledVoices() : [],
|
|
87
|
+
runtime: { bun: Bun.version, platform: process.platform, arch: process.arch },
|
|
88
|
+
modelMirror: activeModelMirror(),
|
|
89
|
+
hint: installed
|
|
90
|
+
? null
|
|
91
|
+
: `Run \`${installHint()}\` to download the engine and models.`,
|
|
92
|
+
// Absent engine means no disk walk, matching the human path (#647).
|
|
93
|
+
disk: installed && options.disk ? collectDiskUsage(path) : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function logEngineCapabilities(caps: EngineCapabilities | null): void {
|
|
40
98
|
if (caps) {
|
|
41
99
|
log.info(formatStatusLine("Backend", caps.backend, true));
|
|
42
100
|
log.info(formatStatusLine("Protocol", `v${caps.protocolVersion}`, true));
|
|
@@ -46,8 +104,7 @@ async function logEngineCapabilities(): Promise<void> {
|
|
|
46
104
|
}
|
|
47
105
|
}
|
|
48
106
|
|
|
49
|
-
function logInstalledVoices(): void {
|
|
50
|
-
const voices = listInstalledVoices();
|
|
107
|
+
function logInstalledVoices(voices: string[]): void {
|
|
51
108
|
if (voices.length === 0) return;
|
|
52
109
|
log.info("TTS voices:");
|
|
53
110
|
for (const v of voices) {
|
|
@@ -56,40 +113,37 @@ function logInstalledVoices(): void {
|
|
|
56
113
|
log.info("");
|
|
57
114
|
}
|
|
58
115
|
|
|
59
|
-
export
|
|
60
|
-
const
|
|
61
|
-
const installed = isEngineInstalled();
|
|
116
|
+
export function renderStatus(report: StatusReport): void {
|
|
117
|
+
const { installed, path, capabilities } = report.engine;
|
|
62
118
|
|
|
63
119
|
log.info("Engine:");
|
|
64
|
-
log.info(formatStatusLine("Binary", installed ?
|
|
120
|
+
log.info(formatStatusLine("Binary", installed ? path : null, installed));
|
|
65
121
|
|
|
66
122
|
if (installed) {
|
|
67
|
-
|
|
123
|
+
logEngineCapabilities(capabilities);
|
|
68
124
|
}
|
|
69
125
|
log.info("");
|
|
70
126
|
|
|
71
|
-
log.info(formatStatusLine("Runtime", `Bun ${
|
|
72
|
-
log.info(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
log.info(formatStatusLine("Runtime", `Bun ${report.runtime.bun}`, true));
|
|
128
|
+
log.info(
|
|
129
|
+
formatStatusLine("Platform", `${report.runtime.platform} ${report.runtime.arch}`, true),
|
|
130
|
+
);
|
|
131
|
+
if (report.modelMirror) {
|
|
132
|
+
log.info(formatStatusLine("Mirror", report.modelMirror, true));
|
|
76
133
|
}
|
|
77
134
|
log.info("");
|
|
78
135
|
|
|
79
136
|
if (installed) {
|
|
80
|
-
logInstalledVoices();
|
|
81
|
-
|
|
82
|
-
if (options.disk) {
|
|
83
|
-
showDiskUsage(binPath);
|
|
84
|
-
}
|
|
137
|
+
logInstalledVoices(report.voices);
|
|
138
|
+
if (report.disk) showDiskUsage(report.disk);
|
|
85
139
|
}
|
|
86
140
|
|
|
87
|
-
if (
|
|
88
|
-
log.warn(
|
|
89
|
-
return;
|
|
141
|
+
if (report.hint) {
|
|
142
|
+
log.warn(report.hint);
|
|
90
143
|
}
|
|
91
144
|
}
|
|
92
145
|
|
|
146
|
+
|
|
93
147
|
function buildDiskComponents(cache: string, engineDir: string): Array<{ label: string; path: string }> {
|
|
94
148
|
return [
|
|
95
149
|
{ label: "Engine", path: engineDir },
|
|
@@ -101,11 +155,11 @@ function buildDiskComponents(cache: string, engineDir: string): Array<{ label: s
|
|
|
101
155
|
];
|
|
102
156
|
}
|
|
103
157
|
|
|
104
|
-
function logDiskRows(rows:
|
|
158
|
+
function logDiskRows(rows: StatusDiskComponent[], total: number, componentTotal: number): void {
|
|
105
159
|
const labelWidth = Math.max(...rows.map((r) => r.label.length), "Total".length);
|
|
106
160
|
for (const r of rows) {
|
|
107
161
|
const pad = " ".repeat(labelWidth - r.label.length + 2);
|
|
108
|
-
log.info(` ${r.label}:${pad}${humanBytes(r.
|
|
162
|
+
log.info(` ${r.label}:${pad}${humanBytes(r.sizeBytes)}`);
|
|
109
163
|
}
|
|
110
164
|
const totalPad = " ".repeat(labelWidth - "Total".length + 2);
|
|
111
165
|
log.info(` ${pc.bold("Total")}:${totalPad}${pc.bold(humanBytes(total))}`);
|
|
@@ -115,42 +169,51 @@ function logDiskRows(rows: Array<{ label: string; size: number }>, total: number
|
|
|
115
169
|
}
|
|
116
170
|
}
|
|
117
171
|
|
|
118
|
-
function logFluidKokoroCache(): void {
|
|
119
|
-
|
|
120
|
-
if (!fluidKokoro.exists || fluidKokoro.sizeBytes <= 0) return;
|
|
172
|
+
function logFluidKokoroCache(fluidKokoro: StatusDiskUsage["fluidKokoro"]): void {
|
|
173
|
+
if (!fluidKokoro) return;
|
|
121
174
|
log.info("");
|
|
122
175
|
log.info(`External caches (not included in Kesha total):`);
|
|
123
176
|
log.info(` FluidAudio Kokoro: ${humanBytes(fluidKokoro.sizeBytes)} (${fluidKokoro.path})`);
|
|
124
177
|
}
|
|
125
178
|
|
|
126
|
-
function
|
|
179
|
+
function collectDiskUsage(binPath: string): StatusDiskUsage {
|
|
127
180
|
const cache = keshaCacheDir();
|
|
128
181
|
// Two levels up from the binary (`<cache>/engine/bin/`) so future engine-root siblings are counted.
|
|
129
182
|
const engineDir = join(binPath, "..", "..");
|
|
130
183
|
|
|
131
|
-
const components =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const size = dirSizeBytes(c.path);
|
|
136
|
-
if (size > 0) rows.push({ label: c.label, size });
|
|
184
|
+
const components: StatusDiskComponent[] = [];
|
|
185
|
+
for (const c of buildDiskComponents(cache, engineDir)) {
|
|
186
|
+
const sizeBytes = dirSizeBytes(c.path);
|
|
187
|
+
if (sizeBytes > 0) components.push({ label: c.label, sizeBytes });
|
|
137
188
|
}
|
|
138
189
|
|
|
139
|
-
if (rows.length === 0) return;
|
|
140
|
-
|
|
141
190
|
// Sum cache root + engine dir separately so `KESHA_ENGINE_BIN` overrides outside the cache are still counted.
|
|
142
|
-
const componentTotal = rows.reduce((n, r) => n + r.size, 0);
|
|
143
191
|
const cacheTotal = dirSizeBytes(cache);
|
|
144
|
-
const engineOutsideCache = engineDir.startsWith(cache)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
192
|
+
const engineOutsideCache = engineDir.startsWith(cache) ? 0 : dirSizeBytes(engineDir);
|
|
193
|
+
const fluidKokoro = fluidKokoroCacheInfo();
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
cachePath: cache,
|
|
197
|
+
components,
|
|
198
|
+
componentTotalBytes: components.reduce((n, c) => n + c.sizeBytes, 0),
|
|
199
|
+
totalBytes: cacheTotal + engineOutsideCache,
|
|
200
|
+
fluidKokoro:
|
|
201
|
+
fluidKokoro.exists && fluidKokoro.sizeBytes > 0
|
|
202
|
+
? { path: fluidKokoro.path, sizeBytes: fluidKokoro.sizeBytes }
|
|
203
|
+
: null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function showDiskUsage(disk: StatusDiskUsage): void {
|
|
208
|
+
if (disk.components.length === 0) return;
|
|
209
|
+
|
|
210
|
+
log.info(`Disk usage (${disk.cachePath}):`);
|
|
211
|
+
logDiskRows(disk.components, disk.totalBytes, disk.componentTotalBytes);
|
|
212
|
+
logFluidKokoroCache(disk.fluidKokoro);
|
|
152
213
|
log.info("");
|
|
153
|
-
log.info(
|
|
214
|
+
log.info(
|
|
215
|
+
pc.dim(` To reset cache: rm -rf ${disk.cachePath} — next \`kesha install\` re-downloads.`),
|
|
216
|
+
);
|
|
154
217
|
log.info("");
|
|
155
218
|
}
|
|
156
219
|
|