whisper-windows-mcp 1.5.0 → 1.6.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/dist/index.js CHANGED
@@ -95,6 +95,60 @@ function hasVulkanDll() {
95
95
  const whisperDir = dirname(WHISPER_CLI_PATH);
96
96
  return existsSync(join(whisperDir, "ggml-vulkan.dll"));
97
97
  }
98
+ async function probeFile(filePath) {
99
+ try {
100
+ const { stdout } = await execFileAsync(FFMPEG_PATH.replace(/ffmpeg(\.exe)?$/i, "ffprobe$1").replace(/ffmpeg$/i, "ffprobe"), [
101
+ "-v", "quiet",
102
+ "-print_format", "json",
103
+ "-show_format",
104
+ "-show_streams",
105
+ filePath,
106
+ ], { windowsHide: true, maxBuffer: 10 * 1024 * 1024 });
107
+ const data = JSON.parse(stdout);
108
+ const fmt = data.format ?? {};
109
+ const streams = data.streams ?? [];
110
+ const audioStream = streams.find((s) => s.codec_type === "audio");
111
+ const durationSec = parseFloat(fmt.duration ?? "0") || 0;
112
+ const sizeMb = parseInt(fmt.size ?? "0", 10) / (1024 * 1024);
113
+ const bitrate = Math.round(parseInt(fmt.bit_rate ?? "0", 10) / 1000);
114
+ const codec = audioStream?.codec_name ?? fmt.format_name?.split(",")[0] ?? "unknown";
115
+ return {
116
+ filePath,
117
+ fileName: basename(filePath),
118
+ durationSec,
119
+ sizeMb,
120
+ codec,
121
+ bitrate,
122
+ };
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ function formatDuration(sec) {
129
+ if (!sec)
130
+ return "?:??";
131
+ const h = Math.floor(sec / 3600);
132
+ const m = Math.floor((sec % 3600) / 60);
133
+ const s = Math.floor(sec % 60);
134
+ if (h > 0)
135
+ return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
136
+ return `${m}:${String(s).padStart(2, "0")}`;
137
+ }
138
+ function estimateTime(durationSec, gpu) {
139
+ if (!durationSec)
140
+ return "?";
141
+ // CPU: ~1.5x realtime on Ryzen 7 2700x with medium.en
142
+ // GPU: ~0.12x realtime on Vega 56 via Vulkan with medium.en (~8x faster than CPU)
143
+ const ratio = gpu ? 0.12 : 1.5;
144
+ const estSec = Math.round(durationSec * ratio);
145
+ if (estSec < 60)
146
+ return `~${estSec}s`;
147
+ return `~${Math.round(estSec / 60)}m`;
148
+ }
149
+ function padEnd(str, len) {
150
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
151
+ }
98
152
  function needsConversion(filePath) {
99
153
  return !NATIVE_EXTENSIONS.includes(extname(filePath).toLowerCase());
100
154
  }
@@ -186,7 +240,7 @@ function getFiles(dir, recursive) {
186
240
  // ---------------------------------------------------------------------------
187
241
  // MCP Server
188
242
  // ---------------------------------------------------------------------------
189
- const server = new Server({ name: "whisper-windows-mcp", version: "1.5.0" }, { capabilities: { tools: {} } });
243
+ const server = new Server({ name: "whisper-windows-mcp", version: "1.6.0" }, { capabilities: { tools: {} } });
190
244
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
191
245
  tools: [
192
246
  {
@@ -258,6 +312,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
258
312
  description: "Verify whisper-cli.exe, model, and FFmpeg are all available. Run this first if anything fails.",
259
313
  inputSchema: { type: "object", properties: {} },
260
314
  },
315
+ {
316
+ name: "analyze_media",
317
+ description: "Analyze one or more media files using FFprobe before transcribing. " +
318
+ "For a single file: returns duration, size, codec, and estimated transcription time on CPU and GPU. " +
319
+ "For a folder: scans all supported media files and returns a sorted table with the same info for each. " +
320
+ "Use this to plan batch work, estimate how long transcription will take, or check what's already been transcribed.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: {
324
+ path: {
325
+ type: "string",
326
+ description: "Absolute Windows path to a single file or a folder.",
327
+ },
328
+ sort_by: {
329
+ type: "string",
330
+ enum: ["duration", "name", "size"],
331
+ description: "For folder scans: sort order. Defaults to duration (shortest first).",
332
+ default: "duration",
333
+ },
334
+ },
335
+ required: ["path"],
336
+ },
337
+ },
261
338
  {
262
339
  name: "check_system",
263
340
  description: "Detect GPU hardware and verify Vulkan acceleration is available. " +
@@ -297,6 +374,98 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
297
374
  };
298
375
  }
299
376
  // -------------------------------------------------------------------------
377
+ // analyze_media
378
+ // -------------------------------------------------------------------------
379
+ if (name === "analyze_media") {
380
+ const targetPath = args?.path;
381
+ const sortBy = args?.sort_by || "duration";
382
+ if (!targetPath)
383
+ return { content: [{ type: "text", text: "path is required." }], isError: true };
384
+ if (!existsSync(targetPath))
385
+ return { content: [{ type: "text", text: `Path not found: ${targetPath}` }], isError: true };
386
+ // Check ffprobe is available
387
+ const ffprobePath = FFMPEG_PATH.replace(/ffmpeg(\.exe)?$/i, "ffprobe$1").replace(/ffmpeg$/i, "ffprobe");
388
+ try {
389
+ await execFileAsync(ffprobePath, ["-version"], { windowsHide: true });
390
+ }
391
+ catch {
392
+ return {
393
+ content: [{ type: "text", text: "ffprobe not found. FFprobe is bundled with FFmpeg — make sure FFmpeg is installed and in your PATH." }],
394
+ isError: true,
395
+ };
396
+ }
397
+ const vulkan = hasVulkanDll();
398
+ // Single file
399
+ const { statSync } = await import("fs");
400
+ const stat = statSync(targetPath);
401
+ if (stat.isFile()) {
402
+ const info = await probeFile(targetPath);
403
+ if (!info) {
404
+ return { content: [{ type: "text", text: `Could not probe file: ${targetPath}\nMake sure it is a supported media file.` }], isError: true };
405
+ }
406
+ const txtPath = targetPath.replace(/\.[^.]+$/, ".txt");
407
+ const transcribed = existsSync(txtPath) ? "✅ already transcribed" : "⬜ not yet transcribed";
408
+ return {
409
+ content: [{
410
+ type: "text",
411
+ text: `📄 ${info.fileName}\n` +
412
+ `${"─".repeat(40)}\n` +
413
+ `Duration: ${formatDuration(info.durationSec)}\n` +
414
+ `Size: ${info.sizeMb.toFixed(1)} MB\n` +
415
+ `Codec: ${info.codec}\n` +
416
+ `Bitrate: ${info.bitrate} kbps\n` +
417
+ `Status: ${transcribed}\n\n` +
418
+ `Estimated transcription time:\n` +
419
+ ` CPU: ${estimateTime(info.durationSec, false)}\n` +
420
+ ` GPU: ${estimateTime(info.durationSec, true)}${vulkan ? "" : " (⚠️ Vulkan not detected — GPU estimate may not apply)"}`,
421
+ }],
422
+ };
423
+ }
424
+ // Folder scan
425
+ const files = getFiles(targetPath, false);
426
+ if (files.length === 0) {
427
+ return { content: [{ type: "text", text: `No supported media files found in: ${targetPath}` }], isError: true };
428
+ }
429
+ const results = [];
430
+ for (const f of files) {
431
+ const info = await probeFile(f);
432
+ if (info)
433
+ results.push(info);
434
+ }
435
+ // Sort
436
+ if (sortBy === "name")
437
+ results.sort((a, b) => a.fileName.localeCompare(b.fileName));
438
+ else if (sortBy === "size")
439
+ results.sort((a, b) => a.sizeMb - b.sizeMb);
440
+ else
441
+ results.sort((a, b) => a.durationSec - b.durationSec); // duration default
442
+ const totalDuration = results.reduce((acc, r) => acc + r.durationSec, 0);
443
+ const totalSize = results.reduce((acc, r) => acc + r.sizeMb, 0);
444
+ const transcribedCount = results.filter(r => existsSync(r.filePath.replace(/\.[^.]+$/, ".txt"))).length;
445
+ const header = `${padEnd("File", 36)} ${padEnd("Duration", 8)} ${padEnd("Size", 8)} ${padEnd("CPU est", 8)} ${padEnd("GPU est", 8)} Status\n` +
446
+ `${"─".repeat(90)}\n`;
447
+ const rows = results.map(r => {
448
+ const done = existsSync(r.filePath.replace(/\.[^.]+$/, ".txt")) ? "✅" : "⬜";
449
+ return (`${padEnd(r.fileName, 36)} ` +
450
+ `${padEnd(formatDuration(r.durationSec), 8)} ` +
451
+ `${padEnd(r.sizeMb.toFixed(1) + " MB", 8)} ` +
452
+ `${padEnd(estimateTime(r.durationSec, false), 8)} ` +
453
+ `${padEnd(estimateTime(r.durationSec, true), 8)} ` +
454
+ done);
455
+ }).join("\n");
456
+ const summary = `\n${"─".repeat(90)}\n` +
457
+ `${results.length} files | Total duration: ${formatDuration(totalDuration)} | Total size: ${totalSize.toFixed(1)} MB\n` +
458
+ `Transcribed: ${transcribedCount}/${results.length}\n` +
459
+ `Total est. CPU time: ${estimateTime(totalDuration, false)} | Total est. GPU time: ${estimateTime(totalDuration, true)}` +
460
+ (vulkan ? "" : "\n⚠️ ggml-vulkan.dll not detected — GPU estimates may not apply");
461
+ return {
462
+ content: [{
463
+ type: "text",
464
+ text: `Media analysis: ${targetPath}\n\n${header}${rows}${summary}`,
465
+ }],
466
+ };
467
+ }
468
+ // -------------------------------------------------------------------------
300
469
  // check_system
301
470
  // -------------------------------------------------------------------------
302
471
  if (name === "check_system") {
@@ -476,7 +645,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
476
645
  async function main() {
477
646
  const transport = new StdioServerTransport();
478
647
  await server.connect(transport);
479
- console.error(`whisper-windows-mcp v1.5.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
648
+ console.error(`whisper-windows-mcp v1.6.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
480
649
  }
481
650
  main().catch((err) => {
482
651
  console.error("Fatal error:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whisper-windows-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Windows-native MCP server for local audio transcription using whisper.cpp with Vulkan GPU acceleration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,193 +0,0 @@
1
- # whisper-windows-mcp — Roadmap
2
-
3
- Current version is a functional early release. It works, but it has known limitations that make it unsuitable for production use with long files or batch workflows. This document tracks what needs to be fixed and what's planned.
4
-
5
- ---
6
-
7
- ## Known Issues (Current Version)
8
-
9
- ### 1. No Process Lock
10
- Multiple `whisper-cli.exe` instances can be spawned concurrently if Claude retries a timed-out request. This maxes CPU and degrades or breaks both transcriptions. **This is the most critical bug.**
11
-
12
- ### 2. No Progress Visibility
13
- The user has no indicator that transcription is happening or how far along it is, short of watching Task Manager or listening to their CPU fan. This is not acceptable UX for a public tool.
14
-
15
- ### 3. 4-Minute Claude MCP Timeout
16
- The Claude web client cuts MCP connections after ~4 minutes. Whisper continues running in the background after the timeout fires, but Claude can't confirm completion. For files longer than ~2-3 minutes of audio on CPU, this timeout is frequently hit.
17
-
18
- ### 4. No GPU Autodetection
19
- The bundled `whisper-cli.exe` binary is CPU-only. AMD GPUs are not detected or used. NVIDIA GPUs may work depending on the binary source, but there's no verification at startup.
20
-
21
- ### 5. High CPU Load
22
- Sustained 50-55% CPU utilization during transcription over multi-hour sessions puts significant strain on hardware. GPU acceleration is the primary fix, but binary selection matters too.
23
-
24
- ### 6. Background Batch Non-Functional
25
- The background batch mode (intended to open a CMD window with progress) does not work as designed and was cut from the current release.
26
-
27
- ### 7. No File Pre-Analysis
28
- No way to know file duration or size before processing starts. Can't sort a queue by length or estimate how long a job will take.
29
-
30
- ---
31
-
32
- ## Roadmap
33
-
34
- ### Priority 1 — GPU Acceleration
35
-
36
- **Problem:** The current `whisper-cli.exe` binary is CPU-only.
37
-
38
- **Solution options (in order of recommendation):**
39
-
40
- - **Vulkan backend** — Compile whisper.cpp with Vulkan support. Works with AMD, NVIDIA, and Intel GPUs on Windows without vendor-specific SDKs. Best cross-compatibility.
41
- - **ROCm backend** — AMD-native but Windows ROCm support is limited and complex to install.
42
- - **Switch to faster-whisper** — Python-based implementation with GPU support via CuBLAS/ROCm. Often faster than whisper.cpp for equivalent accuracy. Requires Python environment.
43
-
44
- **GPU autodetection:** On startup, check for available GPU via `dxdiag` or `wmic path win32_VideoController` and report to the user. If a supported GPU is found but the binary doesn't use it, surface a warning with instructions.
45
-
46
- ---
47
-
48
- ### Priority 2 — Process Lock
49
-
50
- **Problem:** Retrying a timed-out transcription spawns a second `whisper-cli.exe` while the first is still running.
51
-
52
- **Solution:** Before spawning any process, check for existing instances:
53
-
54
- ```batch
55
- tasklist /FI "IMAGENAME eq whisper-cli.exe" /NH
56
- ```
57
-
58
- If found, return an error: `"Transcription already in progress. Wait for the current job to complete before starting another."` Do not spawn. Period.
59
-
60
- This is a small code change with major impact.
61
-
62
- ---
63
-
64
- ### Priority 3 — File Pre-Analysis Tool
65
-
66
- **Problem:** No duration/size info before processing. Can't sort intelligently or estimate time.
67
-
68
- **Solution:** New tool `analyze_media` using FFprobe (already bundled with FFmpeg):
69
-
70
- ```
71
- ffprobe -v quiet -print_format json -show_format -show_streams <file>
72
- ```
73
-
74
- Returns: duration in seconds, file size, codec, bitrate.
75
-
76
- For a folder scan, returns a sorted table:
77
-
78
- ```
79
- filename | duration | size | est. time (CPU) | est. time (GPU)
80
- 2026-01-11 10-11-42.mp4 | 0:35 | 42 MB | ~1 min | ~10 sec
81
- 2026-03-16 06-56-43.mp4 | 1:17 | 98 MB | ~3 min | ~20 sec
82
- ...
83
- ```
84
-
85
- Throughput estimates are calibrated from actual completed jobs (logged internally) and refined over time.
86
-
87
- ---
88
-
89
- ### Priority 4 — Progress Visibility
90
-
91
- **Problem:** No way to know how far along transcription is without Task Manager.
92
-
93
- **Solution:** `whisper-cli.exe` outputs segment timestamps to stderr as it processes (e.g. `[00:01:30 --> 00:01:35]`). The MCP should:
94
-
95
- 1. Pipe stderr from the child process to a log file during processing
96
- 2. Expose a `check_progress` tool that reads the log and returns:
97
- - Last completed timestamp
98
- - Percentage complete (last timestamp / total duration)
99
- - Estimated time remaining
100
- - Whether the process is still running (check PID)
101
-
102
- No changes needed to `whisper-cli` itself — only changes to how the MCP monitors it.
103
-
104
- ---
105
-
106
- ### Priority 5 — Timeout Workaround (Detached Process Architecture)
107
-
108
- **Problem:** The 4-minute Claude MCP timeout kills the connection before long transcriptions finish.
109
-
110
- **Solution:** Rearchitect transcription to use fully detached background processes instead of blocking child processes.
111
-
112
- Flow:
113
- 1. `transcribe_audio` called → spawn whisper as **detached process** → write `job.json` (PID, source file, start time, expected duration, output path, log path) → **return immediately** with job ID
114
- 2. `check_progress` called → read `job.json` → check if PID is still alive → read last N lines of log → return status + percentage
115
- 3. When `check_progress` returns "complete" → read and return transcript
116
-
117
- The transcribe call **never blocks**. It always returns in under a second. The timeout problem disappears entirely.
118
-
119
- This is the correct architecture for any long-running MCP tool.
120
-
121
- ---
122
-
123
- ### Priority 6 — Sequential Batch with Validation
124
-
125
- **Problem:** Background batch mode is non-functional. No post-processing validation.
126
-
127
- **Solution:** Rebuild batch mode using the detached process architecture (Priority 5):
128
-
129
- 1. `transcribe_batch` → runs `analyze_media` on folder → sorts by duration ascending → processes one file at a time using detached process
130
- 2. After each file completes, validate: .txt exists + is non-empty + line count is proportional to duration (blank = likely failed)
131
- 3. Flag any suspect outputs for re-run
132
- 4. Write progress to `batch_progress.log`
133
- 5. `check_batch_progress` returns: files done, files remaining, current file, overall ETA, any failed files
134
-
135
- ---
136
-
137
- ### Priority 7 — Multi-Language Support and Translation
138
-
139
- `whisper-cli` already supports `--language` and `--translate` natively. Expose these properly in the MCP:
140
-
141
- - `language` parameter: auto-detect (default) or specify (e.g. `ja`, `es`, `de`)
142
- - `translate_to_english`: boolean flag — uses whisper's built-in translation model
143
- - For dual output (native + translated): two whisper passes, two output files
144
-
145
- **Example use case:** Japanese DVD rip → Japanese SRT + English SRT simultaneously. Whisper handles abbreviated subject-drop Japanese better than literal translators because it was trained on natural speech.
146
-
147
- ---
148
-
149
- ### Priority 8 — Filename-Based References Throughout
150
-
151
- **Problem:** When Claude references transcripts by positional index ("file 2", "file 11"), the user has no frame of reference. The index means nothing outside the tool call context.
152
-
153
- **Solution:** All tool outputs — status messages, batch listings, progress reports, analysis results — must reference the **full source filename** at all times. Never use positional indices as the primary identifier.
154
-
155
- Bad: `"File 11 is complete."`
156
- Good: `"2025-09-01 03-15-13.mp4 — complete. Transcript saved to 2025-09-01 03-15-13.txt"`
157
-
158
- When Claude ingests transcripts for analysis, each content block should be tagged with its source filename so quotes can always be traced back to their origin without ambiguity.
159
-
160
- ---
161
-
162
- ### Priority 9 — HWID / System Diagnostics Tool
163
-
164
- New tool: `check_system`
165
-
166
- Returns:
167
- - GPU vendor and model
168
- - VRAM available
169
- - Whether a GPU-accelerated whisper binary is available and configured
170
- - Recommended whisper model size for available hardware
171
- - Estimated throughput (tokens/sec) based on hardware profile
172
-
173
- This makes setup easier for new users and helps diagnose configuration problems without needing to open Task Manager or Device Manager.
174
-
175
- ---
176
-
177
- ## Design Principles
178
-
179
- **Minimize Claude API usage.** Every MCP tool call consumes from the user's usage limit. Free-tier users have a hard cap. The entire transcription workflow — scan, analyze, queue, run, validate — should require fewer than 20 Claude interactions for a 60-file batch.
180
-
181
- **One whisper instance at all times.** Never spawn a second process while one is running. Enforce this unconditionally.
182
-
183
- **Local-first, private by default.** No audio leaves the machine. No cloud APIs required. This is a feature, not a limitation.
184
-
185
- **Works for free-tier users.** Courtroom transcription, documentary research, foreign film subtitling — these are real use cases for people who can't afford cloud transcription services. The tool should serve them.
186
-
187
- ---
188
-
189
- ## Contributing
190
-
191
- If you've worked out GPU acceleration for AMD (ROCm or Vulkan) or NVIDIA on Windows, please open an issue or PR — it's the most wanted feature and the implementation details for Windows are non-trivial.
192
-
193
- Pull requests welcome for any of the above priorities. Check existing issues before starting work.