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.
Files changed (57) hide show
  1. package/.github/workflows/ci.yml +24 -0
  2. package/.github/workflows/publish.yml +24 -0
  3. package/{LICENSE-COMMERCIAL.md → COMMERCIAL-LICENSE.md} +58 -58
  4. package/LICENSE +40 -40
  5. package/PRIVACY.es.md +194 -135
  6. package/PRIVACY.id.md +194 -135
  7. package/PRIVACY.ja.md +194 -135
  8. package/PRIVACY.ko.md +194 -135
  9. package/PRIVACY.md +194 -135
  10. package/PRIVACY.pl.md +194 -135
  11. package/PRIVACY.pt-BR.md +194 -135
  12. package/PRIVACY.ro.md +194 -135
  13. package/PRIVACY.uk.md +194 -135
  14. package/PRIVACY.vi.md +194 -135
  15. package/README.es.md +80 -48
  16. package/README.id.md +83 -40
  17. package/README.ja.md +106 -72
  18. package/README.ko.md +69 -37
  19. package/README.md +82 -39
  20. package/README.pl.md +83 -40
  21. package/README.pt-BR.md +77 -45
  22. package/README.ro.md +84 -41
  23. package/README.uk.md +83 -40
  24. package/README.vi.md +73 -41
  25. package/ROADMAP.es.md +131 -45
  26. package/ROADMAP.id.md +86 -89
  27. package/ROADMAP.ja.md +93 -108
  28. package/ROADMAP.ko.md +82 -82
  29. package/ROADMAP.pl.md +125 -41
  30. package/ROADMAP.pt-BR.md +87 -87
  31. package/ROADMAP.ro.md +123 -41
  32. package/ROADMAP.uk.md +82 -90
  33. package/ROADMAP.vi.md +87 -87
  34. package/SECURITY.es.md +76 -47
  35. package/SECURITY.id.md +76 -47
  36. package/SECURITY.ja.md +76 -47
  37. package/SECURITY.ko.md +76 -47
  38. package/SECURITY.md +33 -4
  39. package/SECURITY.pl.md +76 -47
  40. package/SECURITY.pt-BR.md +76 -47
  41. package/SECURITY.ro.md +76 -47
  42. package/SECURITY.uk.md +76 -47
  43. package/SECURITY.vi.md +76 -47
  44. package/TROUBLESHOOTING.es.md +325 -323
  45. package/TROUBLESHOOTING.id.md +349 -323
  46. package/TROUBLESHOOTING.ja.md +415 -286
  47. package/TROUBLESHOOTING.ko.md +325 -323
  48. package/TROUBLESHOOTING.pl.md +371 -323
  49. package/TROUBLESHOOTING.pt-BR.md +325 -323
  50. package/TROUBLESHOOTING.ro.md +371 -323
  51. package/TROUBLESHOOTING.uk.md +385 -323
  52. package/TROUBLESHOOTING.vi.md +325 -323
  53. package/dist/index.js +743 -289
  54. package/dist/lib.d.ts +37 -0
  55. package/dist/lib.js +123 -0
  56. package/package.json +46 -45
  57. 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.2.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
- "keywords": [
14
- "mcp",
15
- "whisper",
16
- "windows",
17
- "transcription",
18
- "speech-to-text",
19
- "audio",
20
- "video",
21
- "claude",
22
- "claude-desktop",
23
- "model-context-protocol",
24
- "whisper-cpp",
25
- "vulkan",
26
- "gpu",
27
- "local-ai",
28
- "batch-transcription",
29
- "subtitle",
30
- "srt",
31
- "multilingual"
32
- ],
33
- "author": "eviscerations",
34
- "license": "SEE LICENSE IN LICENSE",
35
- "dependencies": {
36
- "@modelcontextprotocol/sdk": "^1.0.0"
37
- },
38
- "devDependencies": {
39
- "typescript": "^5.0.0",
40
- "@types/node": "^20.0.0"
41
- },
42
- "engines": {
43
- "node": ">=18"
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()