whisper-windows-mcp 2.2.2 → 2.4.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/.github/workflows/ci.yml +24 -0
- package/.github/workflows/publish.yml +24 -0
- package/{LICENSE-COMMERCIAL.md → COMMERCIAL-LICENSE.md} +58 -58
- package/LICENSE +40 -40
- package/PRIVACY.es.md +194 -135
- package/PRIVACY.id.md +194 -135
- package/PRIVACY.ja.md +194 -135
- package/PRIVACY.ko.md +194 -135
- package/PRIVACY.md +194 -135
- package/PRIVACY.pl.md +194 -135
- package/PRIVACY.pt-BR.md +194 -135
- package/PRIVACY.ro.md +194 -135
- package/PRIVACY.uk.md +194 -135
- package/PRIVACY.vi.md +194 -135
- package/README.es.md +80 -48
- package/README.id.md +83 -40
- package/README.ja.md +106 -72
- package/README.ko.md +69 -37
- package/README.md +82 -39
- package/README.pl.md +83 -40
- package/README.pt-BR.md +77 -45
- package/README.ro.md +84 -41
- package/README.uk.md +83 -40
- package/README.vi.md +73 -41
- package/ROADMAP.es.md +131 -45
- package/ROADMAP.id.md +86 -89
- package/ROADMAP.ja.md +93 -108
- package/ROADMAP.ko.md +82 -82
- package/ROADMAP.pl.md +125 -41
- package/ROADMAP.pt-BR.md +87 -87
- package/ROADMAP.ro.md +123 -41
- package/ROADMAP.uk.md +82 -90
- package/ROADMAP.vi.md +87 -87
- package/SECURITY.es.md +76 -47
- package/SECURITY.id.md +76 -47
- package/SECURITY.ja.md +76 -47
- package/SECURITY.ko.md +76 -47
- package/SECURITY.md +33 -4
- package/SECURITY.pl.md +76 -47
- package/SECURITY.pt-BR.md +76 -47
- package/SECURITY.ro.md +76 -47
- package/SECURITY.uk.md +76 -47
- package/SECURITY.vi.md +76 -47
- package/TROUBLESHOOTING.es.md +325 -323
- package/TROUBLESHOOTING.id.md +349 -323
- package/TROUBLESHOOTING.ja.md +415 -286
- package/TROUBLESHOOTING.ko.md +325 -323
- package/TROUBLESHOOTING.pl.md +371 -323
- package/TROUBLESHOOTING.pt-BR.md +325 -323
- package/TROUBLESHOOTING.ro.md +371 -323
- package/TROUBLESHOOTING.uk.md +385 -323
- package/TROUBLESHOOTING.vi.md +325 -323
- package/dist/index.js +743 -289
- package/dist/lib.d.ts +37 -0
- package/dist/lib.js +123 -0
- package/package.json +46 -45
- package/patch_roadmaps.py +0 -72
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Coerce a tool arg to a finite number, or undefined if it isn't one (rejects NaN/Infinity from bad input). */
|
|
2
|
+
export declare function coerceNum(v: unknown): number | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Write JSON atomically (unique temp file + rename).
|
|
5
|
+
* The rename makes the swap atomic at the filesystem level so a reader in ANOTHER
|
|
6
|
+
* process never sees a half-written, torn file. The random temp suffix keeps two
|
|
7
|
+
* writing processes from colliding on a shared temp name.
|
|
8
|
+
* (Within a single Node process these writes are already serialized — writeFileSync
|
|
9
|
+
* is synchronous and cannot interleave with another call on the event loop.)
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeJsonAtomic(path: string, obj: unknown): void;
|
|
12
|
+
/** Estimate word count from transcript text. */
|
|
13
|
+
export declare function estimateWordCount(text: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Deterministic serialization with sorted keys: the SAME logical arguments produce the
|
|
16
|
+
* SAME string regardless of the order the JSON keys were emitted in.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stableStringify(value: unknown): string;
|
|
19
|
+
/** Stable identity for a single privacy-gated operation: tool name + the full argument set. */
|
|
20
|
+
export declare function opKeyFor(tool: string, args: unknown): string;
|
|
21
|
+
/**
|
|
22
|
+
* True if `child` resolves to a path strictly inside `parent` (not parent itself).
|
|
23
|
+
* Normalizes both paths and uses relative() so a sibling-prefix dir ("models-evil")
|
|
24
|
+
* cannot satisfy the check the way a naive startsWith(parent) would. Windows-safe
|
|
25
|
+
* (drive-letter case handled by resolve/relative). NOTE: this is path-string
|
|
26
|
+
* containment, not symlink-resolved — a junction inside parent is not followed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isInsideDir(child: string, parent: string): boolean;
|
|
29
|
+
/** Recover the timestamped transcript lines from a whisper-cli log (drops diagnostic noise). */
|
|
30
|
+
export declare function extractTranscriptFromLog(logContent: string): string;
|
|
31
|
+
/** Largest start-timestamp (in whole seconds) seen in a whisper-cli log — the progress signal. */
|
|
32
|
+
export declare function parseLastTimestamp(logContent: string): number;
|
|
33
|
+
/** Format seconds as M:SS, or H:MM:SS past an hour. Zero/falsy → "?:??". */
|
|
34
|
+
export declare function formatDuration(sec: number): string;
|
|
35
|
+
export declare function estimateSec(durationSec: number, gpu: boolean): number;
|
|
36
|
+
/** estimateSec as a short human string ("~45s" / "~6m"); "?" when the duration is unknown. */
|
|
37
|
+
export declare function estimateTime(durationSec: number, gpu: boolean): string;
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whisper-windows-mcp — pure logic library
|
|
3
|
+
*
|
|
4
|
+
* Side-effect-free (or fs-only) functions extracted from index.ts so they can be
|
|
5
|
+
* unit-tested directly. No MCP/server/process state lives here. index.ts imports
|
|
6
|
+
* these; the test suite (test/lib.test.mjs) imports the compiled dist/lib.js.
|
|
7
|
+
*/
|
|
8
|
+
import { resolve, relative, isAbsolute } from "path";
|
|
9
|
+
import { writeFileSync, renameSync } from "fs";
|
|
10
|
+
import { createHash, randomUUID } from "crypto";
|
|
11
|
+
/** Coerce a tool arg to a finite number, or undefined if it isn't one (rejects NaN/Infinity from bad input). */
|
|
12
|
+
export function coerceNum(v) {
|
|
13
|
+
// Only accept real numbers and numeric strings. Reject null / "" / booleans / arrays /
|
|
14
|
+
// objects — Number() would silently coerce null, "", and [] to 0, which is loose input
|
|
15
|
+
// we do not want reaching whisper-cli as a deliberate-looking 0.
|
|
16
|
+
if (typeof v !== "number" && typeof v !== "string")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (typeof v === "string" && v.trim() === "")
|
|
19
|
+
return undefined;
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return Number.isFinite(n) ? n : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Write JSON atomically (unique temp file + rename).
|
|
25
|
+
* The rename makes the swap atomic at the filesystem level so a reader in ANOTHER
|
|
26
|
+
* process never sees a half-written, torn file. The random temp suffix keeps two
|
|
27
|
+
* writing processes from colliding on a shared temp name.
|
|
28
|
+
* (Within a single Node process these writes are already serialized — writeFileSync
|
|
29
|
+
* is synchronous and cannot interleave with another call on the event loop.)
|
|
30
|
+
*/
|
|
31
|
+
export function writeJsonAtomic(path, obj) {
|
|
32
|
+
const tmp = `${path}.${randomUUID().slice(0, 8)}.tmp`;
|
|
33
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), "utf8");
|
|
34
|
+
renameSync(tmp, path);
|
|
35
|
+
}
|
|
36
|
+
/** Estimate word count from transcript text. */
|
|
37
|
+
export function estimateWordCount(text) {
|
|
38
|
+
return text.split(/\s+/).filter((w) => w.trim().length > 0).length;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Deterministic serialization with sorted keys: the SAME logical arguments produce the
|
|
42
|
+
* SAME string regardless of the order the JSON keys were emitted in.
|
|
43
|
+
*/
|
|
44
|
+
export function stableStringify(value) {
|
|
45
|
+
if (value === null || typeof value !== "object")
|
|
46
|
+
return JSON.stringify(value) ?? "null";
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
49
|
+
const obj = value;
|
|
50
|
+
const keys = Object.keys(obj).sort();
|
|
51
|
+
return "{" + keys.map(k => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
52
|
+
}
|
|
53
|
+
/** Stable identity for a single privacy-gated operation: tool name + the full argument set. */
|
|
54
|
+
export function opKeyFor(tool, args) {
|
|
55
|
+
return createHash("sha256").update(tool + "\0" + stableStringify(args)).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True if `child` resolves to a path strictly inside `parent` (not parent itself).
|
|
59
|
+
* Normalizes both paths and uses relative() so a sibling-prefix dir ("models-evil")
|
|
60
|
+
* cannot satisfy the check the way a naive startsWith(parent) would. Windows-safe
|
|
61
|
+
* (drive-letter case handled by resolve/relative). NOTE: this is path-string
|
|
62
|
+
* containment, not symlink-resolved — a junction inside parent is not followed.
|
|
63
|
+
*/
|
|
64
|
+
export function isInsideDir(child, parent) {
|
|
65
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
66
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
67
|
+
}
|
|
68
|
+
/** Recover the timestamped transcript lines from a whisper-cli log (drops diagnostic noise). */
|
|
69
|
+
export function extractTranscriptFromLog(logContent) {
|
|
70
|
+
return logContent
|
|
71
|
+
.split(/\r?\n/)
|
|
72
|
+
.filter((l) => /^\[\d{2}:\d{2}:\d{2}\.\d{3} --> /.test(l))
|
|
73
|
+
.join("\n");
|
|
74
|
+
}
|
|
75
|
+
/** Largest start-timestamp (in whole seconds) seen in a whisper-cli log — the progress signal. */
|
|
76
|
+
export function parseLastTimestamp(logContent) {
|
|
77
|
+
const re = /\[(\d{2}):(\d{2}):(\d{2})\.\d{3} -->/g;
|
|
78
|
+
let lastSec = 0;
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = re.exec(logContent)) !== null) {
|
|
81
|
+
const sec = parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10);
|
|
82
|
+
if (sec > lastSec)
|
|
83
|
+
lastSec = sec;
|
|
84
|
+
}
|
|
85
|
+
return lastSec;
|
|
86
|
+
}
|
|
87
|
+
/** Format seconds as M:SS, or H:MM:SS past an hour. Zero/falsy → "?:??". */
|
|
88
|
+
export function formatDuration(sec) {
|
|
89
|
+
if (!sec)
|
|
90
|
+
return "?:??";
|
|
91
|
+
const h = Math.floor(sec / 3600);
|
|
92
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
93
|
+
const s = Math.floor(sec % 60);
|
|
94
|
+
if (h > 0)
|
|
95
|
+
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
96
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Rough transcription-time estimate in SECONDS = a fixed model-LOAD cost + a per-second transcribe
|
|
100
|
+
* rate. The load cost is what actually drives timeouts: whisper-cli reloads the model on EVERY
|
|
101
|
+
* invocation (it is a one-shot CLI, no daemon), and loading large-v3 (2.9 GB) into a memory-
|
|
102
|
+
* constrained GPU was MEASURED at ~2 min on a Vega 56 — so a 7:29 file's whisper-cli ran 3:56 total
|
|
103
|
+
* (≈2 min load + ~2 min transcribe), tipping just past the 4-min MCP wall. Transcription speed was
|
|
104
|
+
* never the bottleneck; the reload tax was. Conservative so it does not under-predict; the real
|
|
105
|
+
* figures vary by model + hardware. (Calibration history: 0.12 then 0.40 then 0.60 single-ratio
|
|
106
|
+
* guesses all mis-modeled this as a realtime factor before the components were measured.)
|
|
107
|
+
*/
|
|
108
|
+
const MODEL_LOAD_SEC_GPU = 110; // fixed per-invocation model-load allowance, GPU (measured ~120s on a constrained 8 GB card)
|
|
109
|
+
const MODEL_LOAD_SEC_CPU = 30; // CPU loads from system RAM — far cheaper than a constrained VRAM upload
|
|
110
|
+
export function estimateSec(durationSec, gpu) {
|
|
111
|
+
if (!durationSec)
|
|
112
|
+
return 0;
|
|
113
|
+
const load = gpu ? MODEL_LOAD_SEC_GPU : MODEL_LOAD_SEC_CPU;
|
|
114
|
+
const transcribeRatio = gpu ? 0.3 : 1.5;
|
|
115
|
+
return Math.round(load + durationSec * transcribeRatio);
|
|
116
|
+
}
|
|
117
|
+
/** estimateSec as a short human string ("~45s" / "~6m"); "?" when the duration is unknown. */
|
|
118
|
+
export function estimateTime(durationSec, gpu) {
|
|
119
|
+
if (!durationSec)
|
|
120
|
+
return "?";
|
|
121
|
+
const est = estimateSec(durationSec, gpu);
|
|
122
|
+
return est < 60 ? `~${est}s` : `~${Math.round(est / 60)}m`;
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "whisper-windows-mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Windows-native MCP server for local audio transcription using whisper.cpp with Vulkan GPU acceleration",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"whisper-windows-mcp": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"start": "node dist/index.js"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"claude
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "whisper-windows-mcp",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Windows-native MCP server for local audio transcription using whisper.cpp with Vulkan GPU acceleration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"whisper-windows-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"test": "tsc && node --test"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"whisper",
|
|
17
|
+
"windows",
|
|
18
|
+
"transcription",
|
|
19
|
+
"speech-to-text",
|
|
20
|
+
"audio",
|
|
21
|
+
"video",
|
|
22
|
+
"claude",
|
|
23
|
+
"claude-desktop",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"whisper-cpp",
|
|
26
|
+
"vulkan",
|
|
27
|
+
"gpu",
|
|
28
|
+
"local-ai",
|
|
29
|
+
"batch-transcription",
|
|
30
|
+
"subtitle",
|
|
31
|
+
"srt",
|
|
32
|
+
"multilingual"
|
|
33
|
+
],
|
|
34
|
+
"author": "eviscerations",
|
|
35
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"@types/node": "^20.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/patch_roadmaps.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
patch_roadmaps.py
|
|
4
|
-
Adds new language entries to the multilingual documentation tracking section
|
|
5
|
-
in existing ROADMAP files. Run from the repo root. Outputs to ./patched_output/.
|
|
6
|
-
|
|
7
|
-
NOTE: ROADMAP.pl.md and ROADMAP.ro.md are NOT in the target list because those
|
|
8
|
-
files were generated fresh with the complete language list already included.
|
|
9
|
-
Only the previously-existing ROADMAPs need patching.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
NEW_ENTRIES = [
|
|
15
|
-
"**Polish (`*.pl.md`)** — `README.pl.md` / `TROUBLESHOOTING.pl.md` / `ROADMAP.pl.md` / `PRIVACY.pl.md` / `SECURITY.pl.md`",
|
|
16
|
-
"**Romanian (`*.ro.md`)** — `README.ro.md` / `TROUBLESHOOTING.ro.md` / `ROADMAP.ro.md` / `PRIVACY.ro.md` / `SECURITY.ro.md`",
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
# Only patch the previously-existing ROADMAPs.
|
|
20
|
-
# ROADMAP.pl.md and ROADMAP.ro.md are excluded — they were generated with
|
|
21
|
-
# the full language list already in their native languages.
|
|
22
|
-
ROADMAP_FILES = {
|
|
23
|
-
"ROADMAP.md": "Community contributions for other languages welcome.",
|
|
24
|
-
"ROADMAP.ja.md": "他の言語へのコミュニティ貢献を歓迎します。",
|
|
25
|
-
"ROADMAP.ko.md": "다른 언어로의 커뮤니티 기여를 환영합니다.",
|
|
26
|
-
"ROADMAP.vi.md": "Chào mừng đóng góp cộng đồng cho các ngôn ngữ khác.",
|
|
27
|
-
"ROADMAP.id.md": "Kontribusi komunitas untuk bahasa lain disambut.",
|
|
28
|
-
"ROADMAP.uk.md": "Внески спільноти для інших мов вітаються.",
|
|
29
|
-
"ROADMAP.pt-BR.md": "Contribuições da comunidade para outros idiomas são bem-vindas.",
|
|
30
|
-
"ROADMAP.es.md": "Las contribuciones de la comunidad para otros idiomas son bienvenidas.",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
OUTPUT_DIR = "patched_output"
|
|
34
|
-
|
|
35
|
-
def patch_roadmap(filename, marker, entries):
|
|
36
|
-
with open(filename, "r", encoding="utf-8") as f:
|
|
37
|
-
content = f.read()
|
|
38
|
-
if marker not in content:
|
|
39
|
-
print(f" WARNING: marker not found in {filename} — skipping")
|
|
40
|
-
return None
|
|
41
|
-
to_insert = [e for e in entries if e.split("(")[0].strip() not in content]
|
|
42
|
-
if not to_insert:
|
|
43
|
-
print(f" INFO: all entries already present in {filename} — skipping")
|
|
44
|
-
return content
|
|
45
|
-
insert_block = "\n".join(to_insert) + "\n\n"
|
|
46
|
-
return content.replace(marker, f"{insert_block}{marker}")
|
|
47
|
-
|
|
48
|
-
def main():
|
|
49
|
-
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
50
|
-
print(f"Output directory: {OUTPUT_DIR}/")
|
|
51
|
-
print(f"Processing {len(ROADMAP_FILES)} ROADMAP files...\n")
|
|
52
|
-
success = 0
|
|
53
|
-
for filename, marker in ROADMAP_FILES.items():
|
|
54
|
-
if not os.path.exists(filename):
|
|
55
|
-
print(f" MISSING: {filename} — skipping")
|
|
56
|
-
continue
|
|
57
|
-
print(f" Patching {filename}...")
|
|
58
|
-
patched = patch_roadmap(filename, marker, NEW_ENTRIES)
|
|
59
|
-
if patched is None:
|
|
60
|
-
continue
|
|
61
|
-
out_path = os.path.join(OUTPUT_DIR, filename)
|
|
62
|
-
with open(out_path, "w", encoding="utf-8") as f:
|
|
63
|
-
f.write(patched)
|
|
64
|
-
print(f" OK -> {out_path}")
|
|
65
|
-
success += 1
|
|
66
|
-
print(f"\nDone. {success}/{len(ROADMAP_FILES)} files patched.")
|
|
67
|
-
print(f"\nReview files in ./{OUTPUT_DIR}/ before replacing originals.")
|
|
68
|
-
print("To replace originals (cmd.exe):")
|
|
69
|
-
print(f" copy /Y {OUTPUT_DIR}\\ROADMAP*.md .")
|
|
70
|
-
|
|
71
|
-
if __name__ == "__main__":
|
|
72
|
-
main()
|