@drakulavich/kesha-voice-kit 1.0.0-beta.5 → 1.0.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/README.md CHANGED
@@ -1,13 +1,19 @@
1
- # Kesha Voice Kit
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="Kesha Voice Kit" width="200">
3
+ </p>
2
4
 
3
- [![CI](https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/@drakulavich/kesha-voice-kit)](https://www.npmjs.com/package/@drakulavich/kesha-voice-kit)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
- [![Bun](https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun)](https://bun.sh)
5
+ <h1 align="center">Kesha Voice Kit</h1>
7
6
 
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.
7
+ <p align="center">
8
+ <a href="https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml"><img src="https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
9
+ <a href="https://www.npmjs.com/package/@drakulavich/kesha-voice-kit"><img src="https://img.shields.io/npm/v/@drakulavich/kesha-voice-kit" alt="npm version"></a>
10
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
11
+ <a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun" alt="Bun"></a>
12
+ </p>
9
13
 
10
- - **Speech-to-text** — 25 languages, ~18x faster than Whisper on Apple Silicon
14
+ <p align="center"><b>Open-source voice toolkit.</b> Optimized for Apple Silicon (CoreML), works on any platform (ONNX fallback).<br>A collection of small, fast, open-source audio models — packaged as CLI tools and an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill for LLM agents.</p>
15
+
16
+ - **Speech-to-text** — 25 languages, ~19x faster than Whisper on Apple Silicon, ~2.5x on CPU
11
17
  - **Language detection** — 107 languages from audio, text language via NLLanguageRecognizer
12
18
  - **Rust engine** — single 20MB binary, no ffmpeg, no Python, no native Node addons
13
19
  - **OpenClaw-ready** — plug into your LLM agent as a voice processing skill
@@ -22,44 +28,17 @@ kesha audio.ogg # transcript to stdout
22
28
 
23
29
  ## OpenClaw Integration
24
30
 
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.
31
+ Kesha Voice Kit ships as a plugin for [OpenClaw](https://github.com/openclaw/openclaw) — give your LLM agent ears. No API keys, everything runs locally on your machine.
26
32
 
27
- ### Voice message processing
33
+ **Install the plugin** (requires the `kesha` CLI — see [Quick Start](#quick-start)):
28
34
 
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
- }
35
+ ```bash
36
+ openclaw plugins install @drakulavich/kesha-voice-kit
48
37
  ```
49
38
 
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
- ```
39
+ OpenClaw fetches the package from npm, auto-detects [`openclaw.plugin.json`](./openclaw.plugin.json), and wires `kesha --json {{MediaPath}}` as the audio transcription backend under `tools.media.audio`. Manage it afterwards with `openclaw plugins list`, `openclaw plugins disable kesha-voice-kit`, or `openclaw plugins uninstall kesha-voice-kit`.
61
40
 
62
- JSON output includes detected language — your agent knows what language the user spoke and can respond accordingly:
41
+ Your agent receives a voice message in Telegram/WhatsApp/Slack. Kesha transcribes it locally, detects the language, and returns structured JSON:
63
42
 
64
43
  ```json
65
44
  [{
@@ -70,6 +49,8 @@ JSON output includes detected language — your agent knows what language the us
70
49
  }]
71
50
  ```
72
51
 
52
+ The agent knows what was said and in what language — and can respond accordingly.
53
+
73
54
  ## CLI Tools
74
55
 
75
56
  ```bash
@@ -91,7 +72,7 @@ Kesha Voice Kit bundles open-source models optimized for on-device inference:
91
72
 
92
73
  | Model | Task | Size | Source |
93
74
  |---|---|---|---|
94
- | NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~86MB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
75
+ | NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~2.5GB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
95
76
  | SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
96
77
  | Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
97
78
 
@@ -99,20 +80,16 @@ All models run through `kesha-engine` — a Rust binary using [FluidAudio](https
99
80
 
100
81
  ## Performance
101
82
 
102
- > **~18x faster than Whisper** on Apple Silicon
83
+ > **~19x faster than Whisper** on Apple Silicon, **~2.5x faster** on CPU
103
84
 
104
- <details>
105
- <summary>MacBook Pro M3 Pro — 10 Russian voice messages</summary>
85
+ Compared against Whisper `large-v3-turbo` — all engines auto-detect language.
106
86
 
107
- ```
108
- faster-whisper (CPU): 35.3s ██████████████████████████████████████
109
- Kesha (CoreML): 1.9s ██
110
- ```
87
+ ![Benchmark: openai-whisper vs faster-whisper vs Kesha Voice Kit](assets/benchmark.svg)
111
88
 
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** |
89
+ <details>
90
+ <summary>Full results with per-file breakdown</summary>
91
+
92
+ See [BENCHMARK.md](BENCHMARK.md) — includes Russian (real voice messages) and English transcription results with all four engines.
116
93
 
117
94
  </details>
118
95
 
@@ -157,7 +134,6 @@ const text = await transcribe("audio.ogg"); // transcribe
157
134
 
158
135
  - [Bun](https://bun.sh) >= 1.3
159
136
  - macOS arm64, Linux x64, or Windows x64
160
- - ~200MB disk (engine + models)
161
137
 
162
138
  ## Contributing
163
139
 
package/SKILL.md ADDED
@@ -0,0 +1,54 @@
1
+ # Kesha Voice Kit
2
+
3
+ Local speech-to-text for voice messages. Runs entirely on your machine — no API keys, no cloud.
4
+
5
+ ## What it does
6
+
7
+ When a voice message arrives (Telegram, WhatsApp, Slack), Kesha transcribes it and returns JSON with the transcript and detected language:
8
+
9
+ ```json
10
+ [{
11
+ "file": "voice.ogg",
12
+ "text": "Привет, как дела?",
13
+ "lang": "ru",
14
+ "textLanguage": { "code": "ru", "confidence": 0.99 }
15
+ }]
16
+ ```
17
+
18
+ Use the `lang` field to detect which language the user spoke and respond accordingly.
19
+
20
+ ## Setup
21
+
22
+ After installing the plugin, run once to download the engine and models:
23
+
24
+ ```bash
25
+ kesha install
26
+ ```
27
+
28
+ ## Plain text mode
29
+
30
+ If you don't need language detection, switch to plain text output in your config:
31
+
32
+ ```json
33
+ {
34
+ "type": "cli",
35
+ "command": "kesha",
36
+ "args": ["{{MediaPath}}"]
37
+ }
38
+ ```
39
+
40
+ This returns just the transcript text without JSON wrapping.
41
+
42
+ ## Available flags
43
+
44
+ - `--json` — JSON output with language detection (default in this plugin)
45
+ - `--verbose` — show language detection details (audio + text language with confidence)
46
+ - `--lang <code>` — warn if detected language differs from expected (e.g. `--lang en`)
47
+
48
+ ## Supported languages
49
+
50
+ Speech-to-text: Bulgarian, Croatian, Czech, Danish, Dutch, English, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Spanish, Swedish, Ukrainian.
51
+
52
+ ## Performance
53
+
54
+ ~19x faster than Whisper on Apple Silicon (CoreML), ~2.5x faster on CPU (ONNX).
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "kesha-voice-kit",
3
+ "displayName": "Kesha Voice Kit",
4
+ "description": "Local speech-to-text for voice messages. 25 languages, ~19x faster than Whisper on Apple Silicon. Drop-in replacement for openai-whisper.",
5
+ "configSchema": {},
6
+ "configPatch": {
7
+ "tools": {
8
+ "media": {
9
+ "audio": {
10
+ "enabled": true,
11
+ "models": [
12
+ {
13
+ "type": "cli",
14
+ "command": "kesha",
15
+ "args": ["--json", "{{MediaPath}}"]
16
+ }
17
+ ]
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.2",
4
4
  "description": "Open-source voice toolkit for Apple Silicon. Speech-to-text, language detection. 25 languages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,13 +16,17 @@
16
16
  "src/",
17
17
  "package.json",
18
18
  "tsconfig.json",
19
+ "openclaw.plugin.json",
20
+ "SKILL.md",
21
+ "scripts/postinstall.cjs",
19
22
  "LICENSE",
20
23
  "README.md"
21
24
  ],
22
25
  "scripts": {
23
26
  "test": "bun test",
24
27
  "test:unit": "bun test tests/unit/",
25
- "test:integration": "bun test tests/integration/"
28
+ "test:integration": "bun test tests/integration/",
29
+ "postinstall": "node scripts/postinstall.cjs"
26
30
  },
27
31
  "keywords": [
28
32
  "asr",
@@ -56,6 +60,7 @@
56
60
  },
57
61
  "dependencies": {
58
62
  "citty": "^0.2.2",
63
+ "fastest-levenshtein": "^1.0.16",
59
64
  "picocolors": "^1.1.1",
60
65
  "tinyld": "^1.3.4"
61
66
  }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ const { execSync } = require("child_process");
3
+
4
+ try {
5
+ execSync("bun --version", { stdio: "ignore" });
6
+ } catch {
7
+ console.warn(
8
+ "\n⚠️ Kesha Voice Kit requires Bun runtime.\n" +
9
+ " Install Bun: curl -fsSL https://bun.sh/install | bash\n" +
10
+ " Then run: kesha install\n"
11
+ );
12
+ }
package/src/cli.ts CHANGED
@@ -8,6 +8,7 @@ import { detectAudioLanguageEngine, detectTextLanguageEngine } from "./engine";
8
8
  import type { LangDetectResult } from "./engine";
9
9
  import { log } from "./log";
10
10
  import { showStatus } from "./status";
11
+ import { suggestCommand } from "./suggest-command";
11
12
 
12
13
  export function detectLanguage(text: string): string {
13
14
  if (!text) return "";
@@ -20,6 +21,8 @@ export function checkLanguageMismatch(expected: string | undefined, detected: st
20
21
  }
21
22
 
22
23
  interface InstallCommandArgs {
24
+ coreml: boolean;
25
+ onnx: boolean;
23
26
  "no-cache": boolean;
24
27
  }
25
28
 
@@ -32,9 +35,19 @@ interface MainCommandArgs {
32
35
 
33
36
  const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
34
37
 
35
- async function performInstall(noCache: boolean) {
38
+ function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined {
39
+ if (coreml && onnx) {
40
+ log.error('Choose only one backend: "--coreml" or "--onnx".');
41
+ process.exit(1);
42
+ }
43
+ if (coreml) return "coreml";
44
+ if (onnx) return "onnx";
45
+ return undefined;
46
+ }
47
+
48
+ async function performInstall(noCache: boolean, backend?: string) {
36
49
  try {
37
- await downloadEngine(noCache);
50
+ await downloadEngine(noCache, backend);
38
51
  } catch (err: unknown) {
39
52
  const message = err instanceof Error ? err.message : String(err);
40
53
  log.error(message);
@@ -48,6 +61,16 @@ export const installCommand = defineCommand({
48
61
  description: "Download inference engine and models",
49
62
  },
50
63
  args: {
64
+ coreml: {
65
+ type: "boolean",
66
+ description: "Force CoreML backend (macOS arm64)",
67
+ default: false,
68
+ },
69
+ onnx: {
70
+ type: "boolean",
71
+ description: "Force ONNX backend",
72
+ default: false,
73
+ },
51
74
  "no-cache": {
52
75
  type: "boolean",
53
76
  description: "Re-download even if cached",
@@ -55,7 +78,8 @@ export const installCommand = defineCommand({
55
78
  },
56
79
  },
57
80
  async run({ args }: { args: InstallCommandArgs }) {
58
- await performInstall(args["no-cache"]);
81
+ const backend = resolveBackendFlag(args.coreml, args.onnx);
82
+ await performInstall(args["no-cache"], backend);
59
83
  },
60
84
  });
61
85
 
@@ -109,12 +133,15 @@ export const mainCommand = defineCommand({
109
133
 
110
134
  for (const file of files) {
111
135
  try {
136
+ // Run audio lang-id and transcription concurrently
137
+ const [audioResult, text] = await Promise.all([
138
+ wantsLangId ? detectAudioLanguageEngine(file) : Promise.resolve(null),
139
+ transcribe(file),
140
+ ]);
141
+
112
142
  let audioLanguage: LangDetectResult | undefined;
113
- if (wantsLangId) {
114
- const audioResult = await detectAudioLanguageEngine(file);
115
- if (audioResult && audioResult.code) {
116
- audioLanguage = audioResult;
117
- }
143
+ if (audioResult && audioResult.code) {
144
+ audioLanguage = audioResult;
118
145
  }
119
146
 
120
147
  if (audioLanguage && args.lang && audioLanguage.confidence > 0.8) {
@@ -122,9 +149,7 @@ export const mainCommand = defineCommand({
122
149
  if (mismatch) log.warn(`${file}: ${mismatch} (from audio)`);
123
150
  }
124
151
 
125
- const text = await transcribe(file);
126
-
127
- const tinyldLang = detectLanguage(text);
152
+ const tinyldLang = wantsLangId ? detectLanguage(text) : "";
128
153
  let textLanguage: LangDetectResult | undefined;
129
154
 
130
155
  if (wantsLangId) {
@@ -165,6 +190,8 @@ export const mainCommand = defineCommand({
165
190
  },
166
191
  });
167
192
 
193
+ const SUBCOMMANDS = ["install", "status"];
194
+
168
195
  export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
169
196
  const [firstArg, ...restArgs] = rawArgs;
170
197
 
@@ -178,6 +205,16 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
178
205
  return;
179
206
  }
180
207
 
208
+ // Check for unknown subcommands (non-flag, non-file-path args)
209
+ if (firstArg && !firstArg.startsWith("-") && !firstArg.includes(".") && !firstArg.includes("/")) {
210
+ const suggestion = suggestCommand(firstArg, SUBCOMMANDS);
211
+ if (suggestion && suggestion !== firstArg) {
212
+ log.error(`unknown command '${firstArg}'`);
213
+ log.warn(`(Did you mean ${suggestion}?)`);
214
+ process.exit(1);
215
+ }
216
+ }
217
+
181
218
  await runMain(mainCommand, { rawArgs });
182
219
  }
183
220
 
@@ -1,6 +1,6 @@
1
1
  import { dirname } from "path";
2
2
  import { existsSync, mkdirSync, chmodSync } from "fs";
3
- import { getEngineBinPath } from "./engine";
3
+ import { getEngineBinPath, getEngineCapabilities } from "./engine";
4
4
  import { log } from "./log";
5
5
  import { streamResponseToFile } from "./progress";
6
6
 
@@ -17,7 +17,7 @@ function getEngineBinaryName(): string {
17
17
  throw new Error(`Unsupported platform: ${platform} ${arch}`);
18
18
  }
19
19
 
20
- export async function downloadEngine(noCache = false): Promise<string> {
20
+ export async function downloadEngine(noCache = false, backend?: string): Promise<string> {
21
21
  const binPath = getEngineBinPath();
22
22
 
23
23
  if (!noCache && existsSync(binPath)) {
@@ -50,18 +50,29 @@ export async function downloadEngine(noCache = false): Promise<string> {
50
50
  log.success("Engine binary downloaded.");
51
51
  }
52
52
 
53
+ if (backend) {
54
+ const caps = await getEngineCapabilities();
55
+ if (caps && caps.backend !== backend) {
56
+ throw new Error(
57
+ `Requested backend "${backend}" is not available: the installed engine for this platform uses "${caps.backend}".\n Fix: omit --${backend} to use the auto-detected backend, or run on a platform that ships the "${backend}" build.`,
58
+ );
59
+ }
60
+ }
61
+
53
62
  log.progress("Installing models...");
54
- const proc = Bun.spawnSync([binPath, "install", ...(noCache ? ["--no-cache"] : [])], {
63
+ const installArgs = ["install", ...(noCache ? ["--no-cache"] : [])];
64
+ const proc = Bun.spawnSync([binPath, ...installArgs], {
55
65
  stdout: "pipe",
56
66
  stderr: "pipe",
57
67
  });
58
68
 
59
- if (proc.stderr.toString()) {
60
- process.stderr.write(proc.stderr.toString());
69
+ const stderr = proc.stderr.toString();
70
+ if (stderr) {
71
+ process.stderr.write(stderr);
61
72
  }
62
73
 
63
74
  if (proc.exitCode !== 0) {
64
- const detail = proc.stderr.toString().trim();
75
+ const detail = stderr.trim();
65
76
  throw new Error(detail ? `Failed to install models: ${detail}` : "Failed to install models");
66
77
  }
67
78
 
package/src/engine.ts CHANGED
@@ -7,8 +7,10 @@ export interface LangDetectResult {
7
7
  confidence: number;
8
8
  }
9
9
 
10
+ const ENGINE_BIN_PATH = join(homedir(), ".cache", "kesha", "engine", "bin", "kesha-engine");
11
+
10
12
  export function getEngineBinPath(): string {
11
- return join(homedir(), ".cache", "kesha", "engine", "bin", "kesha-engine");
13
+ return ENGINE_BIN_PATH;
12
14
  }
13
15
 
14
16
  export function isEngineInstalled(): boolean {
package/src/lib.ts CHANGED
@@ -5,6 +5,9 @@ import { downloadEngine } from "./engine-install";
5
5
  export type { TranscribeOptions };
6
6
  export { downloadEngine as downloadModel };
7
7
 
8
+ /** @deprecated Use `downloadModel` instead. */
9
+ export const downloadCoreML = downloadEngine;
10
+
8
11
  export async function transcribe(
9
12
  audioPath: string,
10
13
  options: TranscribeOptions = {},
@@ -0,0 +1,21 @@
1
+ import { closest, distance } from "fastest-levenshtein";
2
+
3
+ export function suggestCommand(input: string, commands: string[]): string | null {
4
+ if (!input || commands.length === 0) return null;
5
+
6
+ const lowerInput = input.toLowerCase();
7
+ const lowerCommands = commands.map((c) => c.toLowerCase());
8
+
9
+ const exactIdx = lowerCommands.indexOf(lowerInput);
10
+ if (exactIdx !== -1) return commands[exactIdx];
11
+
12
+ const match = closest(lowerInput, lowerCommands);
13
+ const dist = distance(lowerInput, match);
14
+
15
+ // Threshold: distance <= 3 AND distance <= 40% of target length
16
+ const maxDist = Math.min(3, Math.ceil(match.length * 0.4));
17
+ if (dist > maxDist) return null;
18
+
19
+ const idx = lowerCommands.indexOf(match);
20
+ return commands[idx];
21
+ }
@@ -1,92 +0,0 @@
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
- }