@drakulavich/kesha-voice-kit 1.0.0-beta.5
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/LICENSE +21 -0
- package/README.md +168 -0
- package/bin/kesha.js +4 -0
- package/package.json +62 -0
- package/src/benchmark-report.ts +92 -0
- package/src/cli.ts +230 -0
- package/src/engine-install.ts +70 -0
- package/src/engine.ts +83 -0
- package/src/lang-id.ts +2 -0
- package/src/lib.ts +17 -0
- package/src/log.ts +9 -0
- package/src/models.ts +6 -0
- package/src/progress.ts +80 -0
- package/src/status.ts +43 -0
- package/src/transcribe.ts +20 -0
- package/tsconfig.json +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 drakulavich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Kesha Voice Kit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@drakulavich/kesha-voice-kit)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://bun.sh)
|
|
7
|
+
|
|
8
|
+
**Open-source voice toolkit for Apple Silicon.** A collection of small, fast, open-source audio models — packaged as CLI tools and an [OpenClaw](https://github.com/nicekid1/OpenClaw) skill for LLM agents.
|
|
9
|
+
|
|
10
|
+
- **Speech-to-text** — 25 languages, ~18x faster than Whisper on Apple Silicon
|
|
11
|
+
- **Language detection** — 107 languages from audio, text language via NLLanguageRecognizer
|
|
12
|
+
- **Rust engine** — single 20MB binary, no ffmpeg, no Python, no native Node addons
|
|
13
|
+
- **OpenClaw-ready** — plug into your LLM agent as a voice processing skill
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun install -g @drakulavich/kesha-voice-kit
|
|
19
|
+
kesha install # downloads engine + models
|
|
20
|
+
kesha audio.ogg # transcript to stdout
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## OpenClaw Integration
|
|
24
|
+
|
|
25
|
+
Kesha Voice Kit is built as a skill for [OpenClaw](https://github.com/nicekid1/OpenClaw) — give your LLM agent ears. No API keys, everything runs locally on your machine.
|
|
26
|
+
|
|
27
|
+
### Voice message processing
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"tools": {
|
|
32
|
+
"media": {
|
|
33
|
+
"audio": {
|
|
34
|
+
"enabled": true,
|
|
35
|
+
"models": [
|
|
36
|
+
{
|
|
37
|
+
"type": "cli",
|
|
38
|
+
"command": "kesha",
|
|
39
|
+
"args": ["{{MediaPath}}"],
|
|
40
|
+
"timeoutSeconds": 120
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"echoTranscript": false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Your agent receives a voice message in Telegram/WhatsApp/Slack. OpenClaw pipes it through Kesha, feeds the transcript to the LLM. The user speaks, the agent understands.
|
|
51
|
+
|
|
52
|
+
### Language-aware processing
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"type": "cli",
|
|
57
|
+
"command": "kesha",
|
|
58
|
+
"args": ["--json", "{{MediaPath}}"]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
JSON output includes detected language — your agent knows what language the user spoke and can respond accordingly:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
[{
|
|
66
|
+
"file": "voice.ogg",
|
|
67
|
+
"text": "Привет, как дела?",
|
|
68
|
+
"lang": "ru",
|
|
69
|
+
"textLanguage": { "code": "ru", "confidence": 0.99 }
|
|
70
|
+
}]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CLI Tools
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
kesha install # download engine and models
|
|
77
|
+
kesha audio.ogg # transcribe
|
|
78
|
+
kesha --json audio.ogg # JSON output with language info
|
|
79
|
+
kesha --verbose audio.ogg # show language detection details
|
|
80
|
+
kesha --lang en audio.ogg # warn if detected language differs
|
|
81
|
+
kesha status # show installed backend info
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Stdout: transcript. Stderr: errors. Pipe-friendly.
|
|
85
|
+
|
|
86
|
+
**Also available as `parakeet` command** (backward-compatible alias).
|
|
87
|
+
|
|
88
|
+
## What's Inside
|
|
89
|
+
|
|
90
|
+
Kesha Voice Kit bundles open-source models optimized for on-device inference:
|
|
91
|
+
|
|
92
|
+
| Model | Task | Size | Source |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~86MB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
|
|
95
|
+
| SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
|
|
96
|
+
| Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
|
|
97
|
+
|
|
98
|
+
All models run through `kesha-engine` — a Rust binary using [FluidAudio](https://github.com/FluidInference/FluidAudio) (CoreML) on Apple Silicon and [ort](https://github.com/pykeio/ort) (ONNX Runtime) on other platforms.
|
|
99
|
+
|
|
100
|
+
## Performance
|
|
101
|
+
|
|
102
|
+
> **~18x faster than Whisper** on Apple Silicon
|
|
103
|
+
|
|
104
|
+
<details>
|
|
105
|
+
<summary>MacBook Pro M3 Pro — 10 Russian voice messages</summary>
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
faster-whisper (CPU): 35.3s ██████████████████████████████████████
|
|
109
|
+
Kesha (CoreML): 1.9s ██
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| | faster-whisper | Kesha | Speedup |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| Apple Silicon (CoreML) | 35.3s | **1.9s** | **~18x** |
|
|
115
|
+
| Linux CI (ONNX) | 79.2s | **45.4s** | **~1.7x** |
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
## Supported Audio Formats
|
|
120
|
+
|
|
121
|
+
Built-in audio decoding via [symphonia](https://github.com/pdeljanov/Symphonia) — no ffmpeg required:
|
|
122
|
+
|
|
123
|
+
| Format | Extension |
|
|
124
|
+
|---|---|
|
|
125
|
+
| WAV | `.wav` |
|
|
126
|
+
| MP3 | `.mp3` |
|
|
127
|
+
| OGG Vorbis/Opus | `.ogg`, `.opus` |
|
|
128
|
+
| FLAC | `.flac` |
|
|
129
|
+
| AAC / M4A | `.aac`, `.m4a` |
|
|
130
|
+
|
|
131
|
+
## Supported Languages
|
|
132
|
+
|
|
133
|
+
**Speech-to-text (25):** :bulgaria: Bulgarian, :croatia: Croatian, :czech_republic: Czech, :denmark: Danish, :netherlands: Dutch, :gb: English, :estonia: Estonian, :finland: Finnish, :fr: French, :de: German, :greece: Greek, :hungary: Hungarian, :it: Italian, :latvia: Latvian, :lithuania: Lithuanian, :malta: Maltese, :poland: Polish, :portugal: Portuguese, :romania: Romanian, :ru: Russian, :slovakia: Slovak, :slovenia: Slovenian, :es: Spanish, :sweden: Swedish, :ukraine: Ukrainian
|
|
134
|
+
|
|
135
|
+
**Audio language detection (107):** Full list at [speechbrain/lang-id-voxlingua107-ecapa](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa)
|
|
136
|
+
|
|
137
|
+
## Architecture
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
kesha audio.ogg
|
|
141
|
+
→ kesha-engine (Rust binary)
|
|
142
|
+
├── Apple Silicon? → FluidAudio (CoreML / Neural Engine)
|
|
143
|
+
└── Other? → ort (ONNX Runtime / CPU)
|
|
144
|
+
→ transcript to stdout
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Programmatic API
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { transcribe, downloadModel } from "@drakulavich/kesha-voice-kit/core";
|
|
151
|
+
|
|
152
|
+
await downloadModel(); // install engine + models
|
|
153
|
+
const text = await transcribe("audio.ogg"); // transcribe
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Requirements
|
|
157
|
+
|
|
158
|
+
- [Bun](https://bun.sh) >= 1.3
|
|
159
|
+
- macOS arm64, Linux x64, or Windows x64
|
|
160
|
+
- ~200MB disk (engine + models)
|
|
161
|
+
|
|
162
|
+
## Contributing
|
|
163
|
+
|
|
164
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
Made with 💛🩵 Published under MIT License.
|
package/bin/kesha.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
|
+
"description": "Open-source voice toolkit for Apple Silicon. Speech-to-text, language detection. 25 languages.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kesha": "bin/kesha.js",
|
|
8
|
+
"parakeet": "bin/kesha.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/cli.ts",
|
|
12
|
+
"./core": "./src/lib.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"src/",
|
|
17
|
+
"package.json",
|
|
18
|
+
"tsconfig.json",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"test:unit": "bun test tests/unit/",
|
|
25
|
+
"test:integration": "bun test tests/integration/"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"asr",
|
|
29
|
+
"speech-to-text",
|
|
30
|
+
"transcription",
|
|
31
|
+
"voice",
|
|
32
|
+
"kesha",
|
|
33
|
+
"multilingual",
|
|
34
|
+
"apple-silicon",
|
|
35
|
+
"coreml",
|
|
36
|
+
"bun",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"author": "drakulavich",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/drakulavich/kesha-voice-kit.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/drakulavich/kesha-voice-kit#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/drakulavich/kesha-voice-kit/issues"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"bun": ">=1.3.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "latest",
|
|
54
|
+
"fast-xml-parser": "^5.5.10",
|
|
55
|
+
"typescript": "^6.0.2"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"citty": "^0.2.2",
|
|
59
|
+
"picocolors": "^1.1.1",
|
|
60
|
+
"tinyld": "^1.3.4"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface BenchmarkSystemInfo {
|
|
2
|
+
os: string;
|
|
3
|
+
arch: string;
|
|
4
|
+
chip: string;
|
|
5
|
+
ram: string;
|
|
6
|
+
backend: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BenchmarkResult {
|
|
10
|
+
time: number;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BenchmarkSummary {
|
|
15
|
+
whisper_total: number;
|
|
16
|
+
parakeet_total: number;
|
|
17
|
+
speedup: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BenchmarkReport {
|
|
21
|
+
markdown: string;
|
|
22
|
+
summary: BenchmarkSummary;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function roundToTenths(value: number): number {
|
|
26
|
+
return Math.round(value * 10) / 10;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createBenchmarkSummary(
|
|
30
|
+
whisperResults: BenchmarkResult[],
|
|
31
|
+
parakeetResults: BenchmarkResult[],
|
|
32
|
+
): BenchmarkSummary {
|
|
33
|
+
if (whisperResults.length !== parakeetResults.length) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Benchmark result count mismatch: faster-whisper=${whisperResults.length}, parakeet=${parakeetResults.length}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const whisperTotal = roundToTenths(
|
|
40
|
+
whisperResults.reduce((total, result) => total + result.time, 0),
|
|
41
|
+
);
|
|
42
|
+
const parakeetTotal = roundToTenths(
|
|
43
|
+
parakeetResults.reduce((total, result) => total + result.time, 0),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
whisper_total: whisperTotal,
|
|
48
|
+
parakeet_total: parakeetTotal,
|
|
49
|
+
speedup: parakeetTotal > 0 ? roundToTenths(whisperTotal / parakeetTotal) : 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderBenchmarkReport(args: {
|
|
54
|
+
date: string;
|
|
55
|
+
version: string;
|
|
56
|
+
system: BenchmarkSystemInfo;
|
|
57
|
+
whisperResults: BenchmarkResult[];
|
|
58
|
+
parakeetResults: BenchmarkResult[];
|
|
59
|
+
}): BenchmarkReport {
|
|
60
|
+
const { date, version, system, whisperResults, parakeetResults } = args;
|
|
61
|
+
const summary = createBenchmarkSummary(whisperResults, parakeetResults);
|
|
62
|
+
|
|
63
|
+
const lines: string[] = [
|
|
64
|
+
"",
|
|
65
|
+
`**Date:** ${date}`,
|
|
66
|
+
`**Version:** v${version}`,
|
|
67
|
+
`**Runner:** ${system.os} ${system.arch} (${system.chip}, ${system.ram} RAM)`,
|
|
68
|
+
`**Backend:** ${system.backend}`,
|
|
69
|
+
"",
|
|
70
|
+
`| # | faster-whisper | Kesha (${system.backend}) | faster-whisper Transcript | Kesha Transcript |`,
|
|
71
|
+
"|---|---------|----------|--------------------|---------------------|",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < whisperResults.length; i++) {
|
|
75
|
+
const whisper = whisperResults[i];
|
|
76
|
+
const parakeet = parakeetResults[i];
|
|
77
|
+
lines.push(
|
|
78
|
+
`| ${i + 1} | ${whisper.time}s | ${parakeet.time}s | ${whisper.text} | ${parakeet.text} |`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push(
|
|
83
|
+
`| **Total** | **${summary.whisper_total}s** | **${summary.parakeet_total}s** | | |`,
|
|
84
|
+
);
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push(`**Kesha is ~${summary.speedup}x faster.**`);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
markdown: lines.join("\n"),
|
|
90
|
+
summary,
|
|
91
|
+
};
|
|
92
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
|
+
import { detect } from "tinyld";
|
|
5
|
+
import { transcribe } from "./lib";
|
|
6
|
+
import { downloadEngine } from "./engine-install";
|
|
7
|
+
import { detectAudioLanguageEngine, detectTextLanguageEngine } from "./engine";
|
|
8
|
+
import type { LangDetectResult } from "./engine";
|
|
9
|
+
import { log } from "./log";
|
|
10
|
+
import { showStatus } from "./status";
|
|
11
|
+
|
|
12
|
+
export function detectLanguage(text: string): string {
|
|
13
|
+
if (!text) return "";
|
|
14
|
+
return detect(text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function checkLanguageMismatch(expected: string | undefined, detected: string): string | null {
|
|
18
|
+
if (!expected || !detected || expected === detected) return null;
|
|
19
|
+
return `warning: expected language "${expected}" but detected "${detected}"`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InstallCommandArgs {
|
|
23
|
+
"no-cache": boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MainCommandArgs {
|
|
27
|
+
_: string[];
|
|
28
|
+
json: boolean;
|
|
29
|
+
verbose: boolean;
|
|
30
|
+
lang?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
34
|
+
|
|
35
|
+
async function performInstall(noCache: boolean) {
|
|
36
|
+
try {
|
|
37
|
+
await downloadEngine(noCache);
|
|
38
|
+
} catch (err: unknown) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
log.error(message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const installCommand = defineCommand({
|
|
46
|
+
meta: {
|
|
47
|
+
name: "install",
|
|
48
|
+
description: "Download inference engine and models",
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
"no-cache": {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
description: "Re-download even if cached",
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async run({ args }: { args: InstallCommandArgs }) {
|
|
58
|
+
await performInstall(args["no-cache"]);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const statusCommand = defineCommand({
|
|
63
|
+
meta: {
|
|
64
|
+
name: "status",
|
|
65
|
+
description: "Show backend installation status",
|
|
66
|
+
},
|
|
67
|
+
async run() {
|
|
68
|
+
await showStatus();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const mainCommand = defineCommand({
|
|
73
|
+
meta: {
|
|
74
|
+
name: "kesha",
|
|
75
|
+
version: pkg.version,
|
|
76
|
+
description:
|
|
77
|
+
"Kesha Voice Kit — open-source voice toolkit for Apple Silicon.\n" +
|
|
78
|
+
" Run 'kesha install [--no-cache]' to download engine and models.\n" +
|
|
79
|
+
" Run 'kesha status' to inspect installed backend.",
|
|
80
|
+
},
|
|
81
|
+
args: {
|
|
82
|
+
json: {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
description: "Output results as JSON",
|
|
85
|
+
default: false,
|
|
86
|
+
},
|
|
87
|
+
verbose: {
|
|
88
|
+
type: "boolean",
|
|
89
|
+
description: "Show language detection details",
|
|
90
|
+
default: false,
|
|
91
|
+
},
|
|
92
|
+
lang: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Expected language code (ISO 639-1), warn if mismatch",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
async run({ args }: { args: MainCommandArgs }) {
|
|
98
|
+
const files = args._;
|
|
99
|
+
|
|
100
|
+
if (files.length === 0) {
|
|
101
|
+
log.info("Usage: kesha <audio_file> [audio_file ...]\n kesha install [--no-cache]\n kesha status");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let hasError = false;
|
|
106
|
+
const results: TranscribeResult[] = [];
|
|
107
|
+
|
|
108
|
+
const wantsLangId = !!(args.lang || args.verbose || args.json);
|
|
109
|
+
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
try {
|
|
112
|
+
let audioLanguage: LangDetectResult | undefined;
|
|
113
|
+
if (wantsLangId) {
|
|
114
|
+
const audioResult = await detectAudioLanguageEngine(file);
|
|
115
|
+
if (audioResult && audioResult.code) {
|
|
116
|
+
audioLanguage = audioResult;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (audioLanguage && args.lang && audioLanguage.confidence > 0.8) {
|
|
121
|
+
const mismatch = checkLanguageMismatch(args.lang, audioLanguage.code);
|
|
122
|
+
if (mismatch) log.warn(`${file}: ${mismatch} (from audio)`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const text = await transcribe(file);
|
|
126
|
+
|
|
127
|
+
const tinyldLang = detectLanguage(text);
|
|
128
|
+
let textLanguage: LangDetectResult | undefined;
|
|
129
|
+
|
|
130
|
+
if (wantsLangId) {
|
|
131
|
+
const engineTextResult = await detectTextLanguageEngine(text);
|
|
132
|
+
if (engineTextResult && engineTextResult.code) {
|
|
133
|
+
textLanguage = engineTextResult;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lang = textLanguage?.code || tinyldLang;
|
|
138
|
+
|
|
139
|
+
const mismatchWarning = checkLanguageMismatch(args.lang, lang);
|
|
140
|
+
if (mismatchWarning) log.warn(`${file}: ${mismatchWarning}`);
|
|
141
|
+
|
|
142
|
+
results.push({
|
|
143
|
+
file,
|
|
144
|
+
text,
|
|
145
|
+
lang,
|
|
146
|
+
audioLanguage,
|
|
147
|
+
textLanguage: textLanguage ?? (tinyldLang ? { code: tinyldLang, confidence: 0 } : undefined),
|
|
148
|
+
});
|
|
149
|
+
} catch (err: unknown) {
|
|
150
|
+
hasError = true;
|
|
151
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
+
log.error(`${file}: ${message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (args.json) {
|
|
157
|
+
process.stdout.write(formatJsonOutput(results));
|
|
158
|
+
} else if (args.verbose) {
|
|
159
|
+
process.stdout.write(formatVerboseOutput(results));
|
|
160
|
+
} else {
|
|
161
|
+
process.stdout.write(formatTextOutput(results));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (hasError) process.exit(1);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
169
|
+
const [firstArg, ...restArgs] = rawArgs;
|
|
170
|
+
|
|
171
|
+
if (firstArg === "install") {
|
|
172
|
+
await runMain(installCommand, { rawArgs: restArgs });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (firstArg === "status") {
|
|
177
|
+
await runMain(statusCommand, { rawArgs: restArgs });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await runMain(mainCommand, { rawArgs });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type TranscribeResult = {
|
|
185
|
+
file: string;
|
|
186
|
+
text: string;
|
|
187
|
+
lang: string;
|
|
188
|
+
audioLanguage?: LangDetectResult;
|
|
189
|
+
textLanguage?: LangDetectResult;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export function formatTextOutput(results: TranscribeResult[]): string {
|
|
193
|
+
if (results.length === 1) {
|
|
194
|
+
return results[0].text + "\n";
|
|
195
|
+
}
|
|
196
|
+
return results
|
|
197
|
+
.map((r, i) => (i > 0 ? "\n" : "") + `=== ${r.file} ===\n${r.text}\n`)
|
|
198
|
+
.join("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function formatVerboseOutput(results: TranscribeResult[]): string {
|
|
202
|
+
return results
|
|
203
|
+
.map((r, i) => {
|
|
204
|
+
const lines: string[] = [];
|
|
205
|
+
if (results.length > 1) {
|
|
206
|
+
if (i > 0) lines.push("");
|
|
207
|
+
lines.push(`=== ${r.file} ===`);
|
|
208
|
+
}
|
|
209
|
+
if (r.audioLanguage) {
|
|
210
|
+
lines.push(`Audio language: ${r.audioLanguage.code} (confidence: ${r.audioLanguage.confidence.toFixed(2)})`);
|
|
211
|
+
}
|
|
212
|
+
const textLang = r.textLanguage ?? (r.lang ? { code: r.lang, confidence: 0 } : null);
|
|
213
|
+
if (textLang) {
|
|
214
|
+
const confStr = textLang.confidence > 0 ? ` (confidence: ${textLang.confidence.toFixed(2)})` : "";
|
|
215
|
+
lines.push(`Text language: ${textLang.code}${confStr}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push("---");
|
|
218
|
+
lines.push(r.text);
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
})
|
|
221
|
+
.join("\n") + "\n";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function formatJsonOutput(results: TranscribeResult[]): string {
|
|
225
|
+
return JSON.stringify(results, null, 2) + "\n";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (import.meta.main) {
|
|
229
|
+
await runCli();
|
|
230
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { existsSync, mkdirSync, chmodSync } from "fs";
|
|
3
|
+
import { getEngineBinPath } from "./engine";
|
|
4
|
+
import { log } from "./log";
|
|
5
|
+
import { streamResponseToFile } from "./progress";
|
|
6
|
+
|
|
7
|
+
const GITHUB_REPO = "drakulavich/kesha-voice-kit";
|
|
8
|
+
|
|
9
|
+
function getEngineBinaryName(): string {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
|
|
13
|
+
if (platform === "darwin" && arch === "arm64") return "kesha-engine-darwin-arm64";
|
|
14
|
+
if (platform === "linux" && arch === "x64") return "kesha-engine-linux-x64";
|
|
15
|
+
if (platform === "win32" && arch === "x64") return "kesha-engine-windows-x64.exe";
|
|
16
|
+
|
|
17
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function downloadEngine(noCache = false): Promise<string> {
|
|
21
|
+
const binPath = getEngineBinPath();
|
|
22
|
+
|
|
23
|
+
if (!noCache && existsSync(binPath)) {
|
|
24
|
+
log.success("Engine binary already installed.");
|
|
25
|
+
} else {
|
|
26
|
+
const binaryName = getEngineBinaryName();
|
|
27
|
+
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
28
|
+
const version = typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
29
|
+
const url = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
|
|
30
|
+
|
|
31
|
+
mkdirSync(dirname(binPath), { recursive: true });
|
|
32
|
+
|
|
33
|
+
let res: Response;
|
|
34
|
+
try {
|
|
35
|
+
res = await fetch(url, { redirect: "follow" });
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Failed to fetch engine binary: ${e instanceof Error ? e.message : e}\n Fix: Check your network connection and try again`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Failed to download engine binary (HTTP ${res.status})\n Fix: Check https://github.com/${GITHUB_REPO}/releases for available versions`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await streamResponseToFile(res, binPath, "kesha-engine binary");
|
|
49
|
+
chmodSync(binPath, 0o755);
|
|
50
|
+
log.success("Engine binary downloaded.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log.progress("Installing models...");
|
|
54
|
+
const proc = Bun.spawnSync([binPath, "install", ...(noCache ? ["--no-cache"] : [])], {
|
|
55
|
+
stdout: "pipe",
|
|
56
|
+
stderr: "pipe",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (proc.stderr.toString()) {
|
|
60
|
+
process.stderr.write(proc.stderr.toString());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (proc.exitCode !== 0) {
|
|
64
|
+
const detail = proc.stderr.toString().trim();
|
|
65
|
+
throw new Error(detail ? `Failed to install models: ${detail}` : "Failed to install models");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log.success("Backend installed successfully.");
|
|
69
|
+
return binPath;
|
|
70
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
|
|
5
|
+
export interface LangDetectResult {
|
|
6
|
+
code: string;
|
|
7
|
+
confidence: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getEngineBinPath(): string {
|
|
11
|
+
return join(homedir(), ".cache", "kesha", "engine", "bin", "kesha-engine");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isEngineInstalled(): boolean {
|
|
15
|
+
return existsSync(getEngineBinPath());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function runEngine(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
19
|
+
const binPath = getEngineBinPath();
|
|
20
|
+
const proc = Bun.spawn([binPath, ...args], {
|
|
21
|
+
stdout: "pipe",
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
26
|
+
new Response(proc.stdout).text(),
|
|
27
|
+
new Response(proc.stderr).text(),
|
|
28
|
+
proc.exited,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function transcribeEngine(audioPath: string): Promise<string> {
|
|
35
|
+
const { stdout, stderr, exitCode } = await runEngine(["transcribe", audioPath]);
|
|
36
|
+
if (exitCode !== 0) {
|
|
37
|
+
throw new Error(stderr || `kesha-engine exited with code ${exitCode}`);
|
|
38
|
+
}
|
|
39
|
+
return stdout;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseLangResult(stdout: string): LangDetectResult | null {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(stdout);
|
|
45
|
+
if (typeof parsed.code !== "string" || typeof parsed.confidence !== "number") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { code: parsed.code, confidence: parsed.confidence };
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function detectAudioLanguageEngine(audioPath: string): Promise<LangDetectResult | null> {
|
|
55
|
+
if (!isEngineInstalled()) return null;
|
|
56
|
+
const { stdout, exitCode } = await runEngine(["detect-lang", audioPath]);
|
|
57
|
+
if (exitCode !== 0) return null;
|
|
58
|
+
return parseLangResult(stdout);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function detectTextLanguageEngine(text: string): Promise<LangDetectResult | null> {
|
|
62
|
+
if (!isEngineInstalled()) return null;
|
|
63
|
+
const { stdout, exitCode } = await runEngine(["detect-text-lang", text]);
|
|
64
|
+
if (exitCode !== 0) return null;
|
|
65
|
+
return parseLangResult(stdout);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EngineCapabilities {
|
|
69
|
+
protocolVersion: number;
|
|
70
|
+
backend: string;
|
|
71
|
+
features: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getEngineCapabilities(): Promise<EngineCapabilities | null> {
|
|
75
|
+
if (!isEngineInstalled()) return null;
|
|
76
|
+
const { stdout, exitCode } = await runEngine(["--capabilities-json"]);
|
|
77
|
+
if (exitCode !== 0) return null;
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(stdout) as EngineCapabilities;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/lang-id.ts
ADDED
package/src/lib.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { transcribe as internalTranscribe, type TranscribeOptions } from "./transcribe";
|
|
3
|
+
import { downloadEngine } from "./engine-install";
|
|
4
|
+
|
|
5
|
+
export type { TranscribeOptions };
|
|
6
|
+
export { downloadEngine as downloadModel };
|
|
7
|
+
|
|
8
|
+
export async function transcribe(
|
|
9
|
+
audioPath: string,
|
|
10
|
+
options: TranscribeOptions = {},
|
|
11
|
+
): Promise<string> {
|
|
12
|
+
if (!existsSync(audioPath)) {
|
|
13
|
+
throw new Error(`File not found: ${audioPath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return internalTranscribe(audioPath, { ...options, silent: true });
|
|
17
|
+
}
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
|
|
3
|
+
export const log = {
|
|
4
|
+
info: (msg: string) => console.log(msg),
|
|
5
|
+
success: (msg: string) => console.log(pc.green(msg)),
|
|
6
|
+
progress: (msg: string) => console.log(pc.cyan(msg)),
|
|
7
|
+
warn: (msg: string) => console.error(pc.yellow(msg)),
|
|
8
|
+
error: (msg: string) => console.error(pc.red(msg)),
|
|
9
|
+
};
|
package/src/models.ts
ADDED
package/src/progress.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { log } from "./log";
|
|
2
|
+
|
|
3
|
+
const BAR_WIDTH = 20;
|
|
4
|
+
|
|
5
|
+
export function formatBytes(bytes: number): string {
|
|
6
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatProgressBar(label: string, downloaded: number, total: number): string {
|
|
10
|
+
const pct = total <= 0 ? 0 : Math.min(100, Math.floor((downloaded / total) * 100));
|
|
11
|
+
const filled = Math.round((pct / 100) * BAR_WIDTH);
|
|
12
|
+
const empty = BAR_WIDTH - filled;
|
|
13
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
14
|
+
return `${label} [${bar}] ${pct}% ${formatBytes(downloaded)}/${formatBytes(total)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function streamResponseToFile(
|
|
18
|
+
res: Response,
|
|
19
|
+
destPath: string,
|
|
20
|
+
label: string,
|
|
21
|
+
): Promise<number> {
|
|
22
|
+
if (!res.body) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Download failed: empty response for ${label}\n Fix: Try again — the server may be temporarily unavailable`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const totalBytes = Number(res.headers.get("content-length") || 0);
|
|
29
|
+
const progress = createProgressBar(label, totalBytes);
|
|
30
|
+
|
|
31
|
+
const writer = Bun.file(destPath).writer();
|
|
32
|
+
let bytes = 0;
|
|
33
|
+
try {
|
|
34
|
+
for await (const chunk of res.body) {
|
|
35
|
+
writer.write(chunk);
|
|
36
|
+
bytes += chunk.length;
|
|
37
|
+
progress.update(chunk.length);
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
writer.end();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
progress.finish();
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createProgressBar(label: string, totalBytes: number): {
|
|
48
|
+
update(downloadedBytes: number): void;
|
|
49
|
+
finish(): void;
|
|
50
|
+
} {
|
|
51
|
+
const isTTY = process.stderr.isTTY;
|
|
52
|
+
|
|
53
|
+
if (!isTTY || totalBytes <= 0) {
|
|
54
|
+
const sizeInfo = totalBytes > 0 ? ` (${formatBytes(totalBytes)})` : "";
|
|
55
|
+
log.progress(`Downloading ${label}${sizeInfo}...`);
|
|
56
|
+
return {
|
|
57
|
+
update() {},
|
|
58
|
+
finish() {
|
|
59
|
+
log.success(`Downloaded ${label} ✓`);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let current = 0;
|
|
65
|
+
let lastPct = -1;
|
|
66
|
+
return {
|
|
67
|
+
update(downloadedBytes: number) {
|
|
68
|
+
current += downloadedBytes;
|
|
69
|
+
const pct = totalBytes > 0 ? Math.floor((current / totalBytes) * 100) : 0;
|
|
70
|
+
if (pct === lastPct) return;
|
|
71
|
+
lastPct = pct;
|
|
72
|
+
const line = formatProgressBar(label, current, totalBytes);
|
|
73
|
+
process.stderr.write(`\r${line}`);
|
|
74
|
+
},
|
|
75
|
+
finish() {
|
|
76
|
+
const line = formatProgressBar(label, totalBytes, totalBytes);
|
|
77
|
+
process.stderr.write(`\r${line}\n`);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isEngineInstalled, getEngineBinPath, getEngineCapabilities } from "./engine";
|
|
2
|
+
import { log } from "./log";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
export function formatStatusLine(
|
|
6
|
+
label: string,
|
|
7
|
+
path: string | null,
|
|
8
|
+
installed: boolean,
|
|
9
|
+
missingLabel = "not installed",
|
|
10
|
+
): string {
|
|
11
|
+
const status = installed ? pc.green("✓") : pc.red(`✗ ${missingLabel}`);
|
|
12
|
+
const pathStr = path ?? "";
|
|
13
|
+
const padding = " ".repeat(Math.max(1, 50 - label.length - pathStr.length));
|
|
14
|
+
return ` ${label}:${pathStr ? ` ${pathStr}` : ""}${padding}${status}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function showStatus(): Promise<void> {
|
|
18
|
+
const binPath = getEngineBinPath();
|
|
19
|
+
const installed = isEngineInstalled();
|
|
20
|
+
|
|
21
|
+
log.info("Engine:");
|
|
22
|
+
log.info(formatStatusLine("Binary", installed ? binPath : null, installed));
|
|
23
|
+
|
|
24
|
+
if (installed) {
|
|
25
|
+
const caps = await getEngineCapabilities();
|
|
26
|
+
if (caps) {
|
|
27
|
+
log.info(formatStatusLine("Backend", caps.backend, true));
|
|
28
|
+
log.info(formatStatusLine("Protocol", `v${caps.protocolVersion}`, true));
|
|
29
|
+
log.info(formatStatusLine("Features", caps.features.join(", "), true));
|
|
30
|
+
} else {
|
|
31
|
+
log.info(formatStatusLine("Capabilities", null, false, "probe failed"));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
log.info("");
|
|
35
|
+
|
|
36
|
+
log.info(formatStatusLine("Runtime", `Bun ${Bun.version}`, true));
|
|
37
|
+
log.info(formatStatusLine("Platform", `${process.platform} ${process.arch}`, true));
|
|
38
|
+
log.info("");
|
|
39
|
+
|
|
40
|
+
if (!installed) {
|
|
41
|
+
log.warn('Run "kesha install" to download the engine and models.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isEngineInstalled, transcribeEngine } from "./engine";
|
|
2
|
+
|
|
3
|
+
export interface TranscribeOptions {
|
|
4
|
+
silent?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function transcribe(audioPath: string, _opts: TranscribeOptions = {}): Promise<string> {
|
|
8
|
+
if (!isEngineInstalled()) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Error: No transcription backend is installed\n\n" +
|
|
11
|
+
"╔══════════════════════════════════════════════════════════╗\n" +
|
|
12
|
+
"║ Please run the following command to get started: ║\n" +
|
|
13
|
+
"║ ║\n" +
|
|
14
|
+
"║ bunx @drakulavich/kesha-voice-kit install ║\n" +
|
|
15
|
+
"╚══════════════════════════════════════════════════════════╝",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return transcribeEngine(audioPath);
|
|
20
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"types": ["bun-types"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "."
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
13
|
+
}
|