whisper-windows-mcp 1.4.0 → 1.4.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/package.json CHANGED
@@ -1,38 +1,38 @@
1
- {
2
- "name": "whisper-windows-mcp",
3
- "version": "1.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
- },
13
- "keywords": [
14
- "mcp",
15
- "whisper",
16
- "windows",
17
- "transcription",
18
- "speech-to-text",
19
- "audio",
20
- "claude",
21
- "model-context-protocol",
22
- "whisper-cpp",
23
- "vulkan",
24
- "gpu"
25
- ],
26
- "author": "eviscerations",
27
- "license": "MIT",
28
- "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.0.0"
30
- },
31
- "devDependencies": {
32
- "typescript": "^5.0.0",
33
- "@types/node": "^20.0.0"
34
- },
35
- "engines": {
36
- "node": ">=18"
37
- }
38
- }
1
+ {
2
+ "name": "whisper-windows-mcp",
3
+ "version": "1.4.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
+ "claude",
21
+ "model-context-protocol",
22
+ "whisper-cpp",
23
+ "vulkan",
24
+ "gpu"
25
+ ],
26
+ "author": "eviscerations",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.0.0",
33
+ "@types/node": "^20.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }
package/FUNDING.yml DELETED
@@ -1,3 +0,0 @@
1
- github: eviscerations
2
- ko_fi:
3
- patreon:
package/ROADMAP.md DELETED
@@ -1,129 +0,0 @@
1
- # whisper-windows-mcp — Roadmap
2
-
3
- Current version is v1.4.0. GPU acceleration via Vulkan is now working. This document tracks what has been completed and what remains.
4
-
5
- ---
6
-
7
- ## Completed
8
-
9
- ### ✅ Priority 1 — GPU Acceleration (v1.4.0)
10
-
11
- Compiled whisper.cpp from source with `-DGGML_VULKAN=ON` using Visual Studio Build Tools 2026 and Vulkan SDK 1.4.341.1. Pre-built Vulkan binaries are now distributed as a release asset (`whisper-vulkan-win-x64.zip`).
12
-
13
- **Results:** AMD Radeon RX Vega 56 at ~16% GPU utilization, 1.2GB VRAM, CPU at ~15% during transcription. A ~5 minute file that previously took 8–12 minutes on CPU now completes in 20–40 seconds.
14
-
15
- The official whisper.cpp Windows releases do not include a Vulkan build ([issue #3673](https://github.com/ggml-org/whisper.cpp/issues/3673)). The pre-built release in this repo fills that gap for AMD and Intel GPU users.
16
-
17
- ### ✅ Priority 2 — Process Lock (v1.3.1)
18
-
19
- Added `isWhisperRunning()` check using `tasklist /FI` before any transcription spawn. If `whisper-cli.exe` is already running, returns a clear error with Task Manager instructions rather than spawning a second competing process.
20
-
21
- ---
22
-
23
- ## Known Issues (Remaining)
24
-
25
- ### 3. 4-Minute Claude MCP Timeout
26
- 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. With GPU acceleration this is less frequently hit, but still a concern for very long files or large models.
27
-
28
- ### 5. No Progress Visibility
29
- The user has no indicator that transcription is happening or how far along it is. whisper-cli.exe outputs segment timestamps to stderr as it processes — the MCP should pipe and expose these.
30
-
31
- ### 6. Background Batch Non-Functional
32
- A background batch mode was attempted in a previous version but stripped due to no visible feedback. Needs a proper detached process architecture with job state files.
33
-
34
- ### 7. No File Pre-Analysis
35
- No way to know file duration or size before processing starts.
36
-
37
- ---
38
-
39
- ## Roadmap
40
-
41
- ### Priority 3 — File Pre-Analysis Tool
42
-
43
- New tool `analyze_media` using FFprobe:
44
-
45
- ```
46
- ffprobe -v quiet -print_format json -show_format -show_streams <file>
47
- ```
48
-
49
- Returns: duration, file size, codec, bitrate, estimated transcription time (CPU and GPU).
50
-
51
- ---
52
-
53
- ### Priority 4 — Progress Visibility
54
-
55
- whisper-cli.exe outputs segment timestamps to stderr (e.g. `[00:01:30 --> 00:01:35]`). The MCP should:
56
-
57
- 1. Pipe stderr to a log file during processing
58
- 2. Expose a `check_progress` tool returning last timestamp, percentage complete, estimated time remaining, and whether the process is still running
59
-
60
- ---
61
-
62
- ### Priority 5 — Timeout Workaround (Detached Process Architecture)
63
-
64
- Rearchitect transcription to use fully detached background processes:
65
-
66
- 1. `transcribe_audio` → spawn whisper as detached process → write `job.json` → return immediately with job ID
67
- 2. `check_progress` → read `job.json` → check PID → read log → return status + percentage
68
- 3. When complete → read and return transcript
69
-
70
- This eliminates the 4-minute timeout problem entirely and unlocks Priority 6.
71
-
72
- ---
73
-
74
- ### Priority 6 — Sequential Batch with Validation
75
-
76
- Rebuild batch mode on top of Priority 5:
77
-
78
- 1. `transcribe_batch` → runs `analyze_media` on folder → sorts by duration → processes one file at a time using detached process
79
- 2. After each file: validate .txt exists, is non-empty, line count proportional to duration
80
- 3. Flag suspect outputs for re-run
81
- 4. `check_batch_progress` returns: files done, remaining, current file, ETA, any failed files
82
-
83
- ---
84
-
85
- ### Priority 7 — Multi-Language Support and Translation
86
-
87
- Expose `--language` and `--translate` flags properly:
88
-
89
- - `language` parameter: auto-detect (default) or specify (`ja`, `es`, `de`, etc.)
90
- - `translate_to_english`: boolean — uses whisper's built-in translation model
91
- - Dual output: two whisper passes, two output files
92
-
93
- ---
94
-
95
- ### Priority 8 — Filename-Based References Throughout
96
-
97
- All tool outputs must reference the full source filename at all times. Never use positional indices as the primary identifier.
98
-
99
- ---
100
-
101
- ### Priority 9 — System Diagnostics Tool
102
-
103
- New tool `check_system`:
104
-
105
- - GPU vendor and model (via `wmic path win32_VideoController`)
106
- - Whether `ggml-vulkan.dll` is present alongside `whisper-cli.exe`
107
- - Recommended model size for available VRAM
108
- - Estimated throughput based on hardware profile
109
- - Actionable guidance if GPU binary is missing
110
-
111
- ---
112
-
113
- ## Design Principles
114
-
115
- **Minimize Claude API usage.** The entire transcription workflow should require fewer than 20 Claude interactions for a 60-file batch.
116
-
117
- **One whisper instance at all times.** Never spawn a second process while one is running.
118
-
119
- **Local-first, private by default.** No audio leaves the machine. No cloud APIs required.
120
-
121
- **Works for free-tier users.** Courtroom transcription, documentary research, foreign film subtitling — the tool should serve people who can't afford cloud transcription services.
122
-
123
- ---
124
-
125
- ## Contributing
126
-
127
- Pull requests welcome for any of the above priorities. Check existing issues before starting work.
128
-
129
- If you've tested GPU acceleration on hardware not listed above, please open an issue with your results — GPU model, VRAM, model size, and observed throughput.
@@ -1,172 +0,0 @@
1
- # Troubleshooting
2
-
3
- This document covers common issues and their solutions, including workarounds for known limitations.
4
-
5
- ---
6
-
7
- ## "whisper-cli not found" error
8
-
9
- **Symptom:** Claude reports `whisper-cli not found` or suggests running `brew install whisper-cpp`.
10
-
11
- The `brew install` suggestion is a macOS instruction and does not apply to Windows. This error means the MCP server cannot locate `whisper-cli.exe`.
12
-
13
- **Fix:** Make sure `WHISPER_CLI_PATH` in your `claude_desktop_config.json` points to the exact location of `whisper-cli.exe`, using double backslashes:
14
-
15
- ```json
16
- "WHISPER_CLI_PATH": "C:\\whisper\\Release\\whisper-cli.exe"
17
- ```
18
-
19
- Verify the file exists by navigating to it in File Explorer.
20
-
21
- ---
22
-
23
- ## "File not found" when transcribing
24
-
25
- 1. **Wrong path format** — when typing paths directly in Claude chat, use normal single backslashes: `C:\Users\Me\Downloads\audio.mp3`
26
- 2. **File doesn't exist** — double-check in File Explorer
27
- 3. **Spaces in path** — paths with spaces work fine, just make sure you give Claude the complete path
28
-
29
- ---
30
-
31
- ## Video files fail with FFmpeg error
32
-
33
- **Symptom:** Transcribing an MP4 or other video file fails with an FFmpeg-related error.
34
-
35
- **Fix:** Make sure FFmpeg is installed and in your system PATH.
36
-
37
- 1. Download FFmpeg from [ffmpeg.org/download.html](https://ffmpeg.org/download.html)
38
- 2. Extract and note the location of `ffmpeg.exe` (usually in a `bin` subfolder)
39
- 3. Add that folder to your system PATH:
40
- - **Win + S** → search **Environment Variables** → open it
41
- - Under **User Variables**, select **Path** → **Edit** → **New**
42
- - Add the path to the folder containing `ffmpeg.exe`
43
- - Click OK on all dialogs, then restart Claude Desktop
44
-
45
- ✅ Verify FFmpeg is working: open Command Prompt and run `ffmpeg -version`
46
-
47
- Alternatively, add `FFMPEG_PATH` to your config pointing directly to `ffmpeg.exe`:
48
- ```json
49
- "FFMPEG_PATH": "C:\\ffmpeg\\bin\\ffmpeg.exe"
50
- ```
51
-
52
- ---
53
-
54
- ## Whisper server shows as "error" in Settings → Developer
55
-
56
- 1. Make sure your `claude_desktop_config.json` is valid JSON — a missing comma or bracket breaks it. Paste it into [jsonlint.com](https://jsonlint.com) to check.
57
- 2. Make sure Node.js is installed: open Command Prompt and run `node --version`
58
- 3. Fully quit Claude Desktop (check Task Manager for any remaining Claude processes) and reopen it
59
-
60
- ---
61
-
62
- ## Transcription runs but produces garbled text
63
-
64
- - **Wrong language:** Specify the language when asking Claude — *"Transcribe this in English"* or add `"language": "en"` in the call
65
- - **Poor audio quality:** Whisper handles most accents well but very low bitrate or heavily distorted audio will reduce accuracy
66
- - **Try a larger model:** `medium.en` is significantly more accurate than `base.en`, at the cost of speed
67
-
68
- ---
69
-
70
- ## Transcription is very slow
71
-
72
- This is expected on CPU. Rough estimates for a 1-hour file:
73
-
74
- | Model | Time |
75
- |---|---|
76
- | base.en | 10–20 min |
77
- | medium.en | 35–60 min |
78
-
79
- To speed up, increase the thread count by adding to your config:
80
- ```json
81
- "WHISPER_THREADS": "12"
82
- ```
83
-
84
- Use up to the number of logical cores your CPU has (check Task Manager → Performance → CPU for your core count).
85
-
86
- ---
87
-
88
- ## Cancelling a long transcription
89
-
90
- **Known limitation:** Clicking the stop/cancel button in Claude Desktop while a transcription is running will return control to Claude but does NOT stop the underlying whisper-cli process. It continues running in the background consuming CPU.
91
-
92
- **To actually stop it:**
93
- 1. Open Task Manager (Ctrl + Shift + Esc)
94
- 2. Go to the **Details** tab
95
- 3. Find `whisper-cli.exe` and right-click → **End Process Tree**
96
-
97
- ---
98
-
99
- ## Running large overnight batch jobs (unattended)
100
-
101
- **Known limitation:** Claude Desktop has a connection timeout that prevents long-running batch operations through the MCP server. For large unattended batch jobs (many files, or long recordings), use whisper-cli directly from the command line instead.
102
-
103
- **Single file from command line:**
104
- ```
105
- "C:\whisper\Release\whisper-cli.exe" -m "C:\whisper\models\ggml-medium.en.bin" -f "C:\path\to\audio.mp3" --no-timestamps -t 8 > "C:\path\to\output.txt"
106
- ```
107
-
108
- **Batch folder using PowerShell (run from PowerShell window):**
109
- ```powershell
110
- $folder = "C:\path\to\your\folder"
111
- $whisper = "C:\whisper\Release\whisper-cli.exe"
112
- $model = "C:\whisper\models\ggml-medium.en.bin"
113
-
114
- Get-ChildItem $folder -Include *.mp4,*.mp3,*.mkv,*.wav -Recurse | ForEach-Object {
115
- $out = $_.FullName -replace '\.[^.]+$', '.txt'
116
- Write-Host "Processing: $($_.Name)"
117
- & $whisper -m $model -f $_.FullName --no-timestamps -t 8 | Out-File -FilePath $out -Encoding utf8
118
- Write-Host "Done: $out"
119
- }
120
- Write-Host "Batch complete."
121
- ```
122
-
123
- Save this as a `.ps1` file and run it from PowerShell, or paste it directly into a PowerShell window. You can let it run overnight — it will process each file sequentially and save a `.txt` next to each source file.
124
-
125
- > **Note:** If PowerShell blocks the script with an execution policy error, run: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`
126
-
127
- ---
128
-
129
- ## Claude Desktop fails to install or update (HRESULT errors)
130
-
131
- This is a Windows MSIX packaging issue, not related to this MCP server. Steps to resolve:
132
-
133
- 1. Fully uninstall Claude Desktop via **Settings → Apps → Apps & features**
134
- 2. Open Task Manager and end any remaining Claude or Cowork processes
135
- 3. Delete `C:\Users\YourUsername\AppData\Local\AnthropicClaude` if it exists
136
- 4. **Restart your computer** — some package registrations persist in memory until reboot
137
- 5. Run the installer again after reboot
138
-
139
- ---
140
-
141
- ## Double backslashes in JSON — why?
142
-
143
- In JSON, the backslash `\` is an escape character. To represent a literal backslash (as used in Windows paths), you must write `\\`. So:
144
-
145
- | Windows path | In JSON config |
146
- |---|---|
147
- | `C:\whisper\Release\whisper-cli.exe` | `"C:\\whisper\\Release\\whisper-cli.exe"` |
148
-
149
- This only applies inside the JSON config file. When typing paths into Claude in the chat, use normal single backslashes.
150
-
151
- ---
152
-
153
- ## Config file location
154
-
155
- ```
156
- C:\Users\YourUsername\AppData\Roaming\Claude\claude_desktop_config.json
157
- ```
158
-
159
- The `AppData` folder is hidden by default. To show hidden folders: File Explorer → **View → Show → Hidden items**.
160
-
161
- You can also reach the config via Claude Desktop → **Settings → Developer → Edit Config**.
162
-
163
- ---
164
-
165
- ## Still stuck?
166
-
167
- Open an issue at [github.com/eviscerations/whisper-windows-mcp/issues](https://github.com/eviscerations/whisper-windows-mcp/issues) and include:
168
-
169
- - Your Windows version
170
- - Your `claude_desktop_config.json` (remove personal info)
171
- - The MCP server log from **Settings → Developer → View Logs**
172
- - What you've already tried
package/src/index.ts DELETED
@@ -1,446 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * whisper-windows-mcp
4
- * A Windows-native MCP server for local audio transcription using whisper.cpp
5
- * https://github.com/eviscerations/whisper-windows-mcp
6
- */
7
-
8
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
- import {
11
- CallToolRequestSchema,
12
- ListToolsRequestSchema,
13
- } from "@modelcontextprotocol/sdk/types.js";
14
- import { execFile } from "child_process";
15
- import {
16
- existsSync, unlinkSync, readdirSync,
17
- writeFileSync, readFileSync
18
- } from "fs";
19
- import { cpus, tmpdir } from "os";
20
- import { join, extname, basename } from "path";
21
- import { promisify } from "util";
22
-
23
- const execFileAsync = promisify(execFile);
24
-
25
- // ---------------------------------------------------------------------------
26
- // Configuration
27
- // ---------------------------------------------------------------------------
28
- const WHISPER_CLI_PATH =
29
- process.env.WHISPER_CLI_PATH ?? "C:\\whisper\\Release\\whisper-cli.exe";
30
- const WHISPER_MODEL =
31
- process.env.WHISPER_MODEL ?? "C:\\whisper\\models\\ggml-base.en.bin";
32
- const FFMPEG_PATH =
33
- process.env.FFMPEG_PATH ?? "ffmpeg";
34
-
35
- const SYSTEM_THREADS = cpus().length;
36
- const DEFAULT_THREADS = Math.max(2, Math.floor(SYSTEM_THREADS / 2));
37
- const WHISPER_THREADS = parseInt(process.env.WHISPER_THREADS ?? String(DEFAULT_THREADS), 10);
38
-
39
- const SUPPORTED_EXTENSIONS = [
40
- ".mp3", ".wav",
41
- ".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v",
42
- ".m4a", ".ogg", ".flac",
43
- ];
44
- const NATIVE_EXTENSIONS = [".mp3", ".wav"];
45
-
46
- // ---------------------------------------------------------------------------
47
- // Helpers
48
- // ---------------------------------------------------------------------------
49
- function validatePaths(): string | null {
50
- if (!existsSync(WHISPER_CLI_PATH))
51
- return `whisper-cli.exe not found at: ${WHISPER_CLI_PATH}\nCheck WHISPER_CLI_PATH in claude_desktop_config.json`;
52
- if (!existsSync(WHISPER_MODEL))
53
- return `Whisper model not found at: ${WHISPER_MODEL}\nCheck WHISPER_MODEL in claude_desktop_config.json`;
54
- return null;
55
- }
56
-
57
- /**
58
- * Check whether a whisper-cli.exe process is already running.
59
- * Uses tasklist /FI which is available on all Windows versions.
60
- * Returns true if found, false if not (or if tasklist itself fails).
61
- */
62
- async function isWhisperRunning(): Promise<boolean> {
63
- try {
64
- const { stdout } = await execFileAsync(
65
- "tasklist",
66
- ["/FI", "IMAGENAME eq whisper-cli.exe", "/NH"],
67
- { windowsHide: true }
68
- );
69
- return stdout.toLowerCase().includes("whisper-cli.exe");
70
- } catch {
71
- // If tasklist fails for any reason, assume safe to proceed
72
- return false;
73
- }
74
- }
75
-
76
- function needsConversion(filePath: string): boolean {
77
- return !NATIVE_EXTENSIONS.includes(extname(filePath).toLowerCase());
78
- }
79
-
80
- function isSupportedFile(filePath: string): boolean {
81
- return SUPPORTED_EXTENSIONS.includes(extname(filePath).toLowerCase());
82
- }
83
-
84
- async function convertToWav(inputPath: string): Promise<string> {
85
- const tmpFile = join(tmpdir(), `whisper_tmp_${Date.now()}.wav`);
86
- await execFileAsync(FFMPEG_PATH, [
87
- "-y", "-i", inputPath,
88
- "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", tmpFile,
89
- ], { windowsHide: true });
90
- return tmpFile;
91
- }
92
-
93
- type OutputFormat = "text" | "timestamps" | "json" | "srt";
94
-
95
- function buildArgs(
96
- filePath: string, model: string, language: string,
97
- outputFormat: OutputFormat, threads: number
98
- ): string[] {
99
- const args = ["-m", model, "-f", filePath, "-l", language, "-t", String(threads)];
100
- if (outputFormat === "srt") {
101
- args.push("-osrt", "-of", filePath.replace(/\.[^.]+$/, ""));
102
- } else if (outputFormat === "json") {
103
- args.push("-oj");
104
- } else if (outputFormat === "text") {
105
- args.push("--no-timestamps");
106
- }
107
- return args;
108
- }
109
-
110
- async function transcribeSingle(
111
- filePath: string, model: string, language: string,
112
- outputFormat: OutputFormat, threads: number, saveToFile = false
113
- ): Promise<{ text: string; srtPath?: string; savedTo?: string }> {
114
-
115
- // ---- Priority 2: Process lock ----
116
- // Never spawn a second whisper-cli.exe while one is already running.
117
- if (await isWhisperRunning()) {
118
- throw new Error(
119
- "Transcription already in progress.\n\n" +
120
- "whisper-cli.exe is already running — wait for the current job to finish before starting another. " +
121
- "If you believe this is wrong (e.g. a previous job crashed and left a stale process), " +
122
- "open Task Manager, find whisper-cli.exe under Details, and end the task."
123
- );
124
- }
125
-
126
- let transcribeFrom = filePath;
127
- let tmpFile: string | null = null;
128
-
129
- if (needsConversion(filePath)) {
130
- tmpFile = await convertToWav(filePath);
131
- transcribeFrom = tmpFile;
132
- }
133
-
134
- try {
135
- const cliArgs = buildArgs(transcribeFrom, model, language, outputFormat, threads);
136
- const { stdout, stderr } = await execFileAsync(WHISPER_CLI_PATH, cliArgs, {
137
- maxBuffer: 100 * 1024 * 1024,
138
- windowsHide: true,
139
- });
140
-
141
- const output = (stdout || stderr || "").trim();
142
-
143
- if (outputFormat === "srt") {
144
- const tmpSrt = transcribeFrom.replace(/\.[^.]+$/, ".srt");
145
- const destSrt = filePath.replace(/\.[^.]+$/, ".srt");
146
- if (tmpFile && existsSync(tmpSrt)) {
147
- writeFileSync(destSrt, readFileSync(tmpSrt, "utf8"));
148
- try { unlinkSync(tmpSrt); } catch { }
149
- }
150
- return { text: output, srtPath: destSrt };
151
- }
152
-
153
- if (saveToFile) {
154
- const txtPath = filePath.replace(/\.[^.]+$/, ".txt");
155
- writeFileSync(txtPath, output, "utf8");
156
- return { text: output, savedTo: txtPath };
157
- }
158
-
159
- return { text: output };
160
- } finally {
161
- if (tmpFile && existsSync(tmpFile)) try { unlinkSync(tmpFile); } catch { }
162
- }
163
- }
164
-
165
- function getFiles(dir: string, recursive: boolean): string[] {
166
- const entries = readdirSync(dir, { withFileTypes: true });
167
- const files: string[] = [];
168
- for (const entry of entries) {
169
- const fullPath = join(dir, entry.name);
170
- if (entry.isDirectory() && recursive) files.push(...getFiles(fullPath, true));
171
- else if (entry.isFile() && isSupportedFile(entry.name)) files.push(fullPath);
172
- }
173
- return files;
174
- }
175
-
176
- // ---------------------------------------------------------------------------
177
- // MCP Server
178
- // ---------------------------------------------------------------------------
179
- const server = new Server(
180
- { name: "whisper-windows-mcp", version: "1.4.0" },
181
- { capabilities: { tools: {} } }
182
- );
183
-
184
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
185
- tools: [
186
- {
187
- name: "transcribe_audio",
188
- description:
189
- "Transcribe a single audio or video file using whisper.cpp on Windows. " +
190
- "Natively supports mp3 and wav. Automatically converts mp4, mkv, avi, mov, " +
191
- "webm, m4a, flac, ogg etc. via FFmpeg — no manual conversion needed. " +
192
- "Can output plain text, timestamps, JSON, or SRT subtitle files. " +
193
- "WARNING: files over 30 minutes cannot be cancelled cleanly from Claude — " +
194
- "use Task Manager to kill the process if needed.",
195
- inputSchema: {
196
- type: "object",
197
- properties: {
198
- file_path: { type: "string", description: "Absolute Windows path, e.g. C:\\Users\\You\\Downloads\\recording.mp4" },
199
- model: { type: "string", description: "Override model path. Leave blank to use WHISPER_MODEL." },
200
- language: { type: "string", description: "Language code, e.g. en, es, fr. Defaults to en.", default: "en" },
201
- output_format: {
202
- type: "string", enum: ["text", "timestamps", "json", "srt"],
203
- description: "text = plain (default), timestamps = with time codes, json = structured, srt = subtitle file saved next to source.",
204
- default: "text",
205
- },
206
- threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
207
- save_to_file: { type: "boolean", description: "Save transcript as .txt next to the source file.", default: false },
208
- },
209
- required: ["file_path"],
210
- },
211
- },
212
- {
213
- name: "transcribe_batch",
214
- description:
215
- "Transcribe multiple audio/video files in a folder interactively, one file at a time. " +
216
- "Shows a preview of each transcript and waits for confirmation before continuing. " +
217
- "Saves each transcript as a .txt file next to its source. " +
218
- "Files already transcribed (with matching .txt) are shown as done and skipped. " +
219
- "Supported formats: mp3, wav, mp4, mkv, avi, mov, webm, m4a, flac, ogg. " +
220
- "NOTE: For large unattended batch jobs, use whisper-cli.exe directly from the command line " +
221
- "— see TROUBLESHOOTING.md for the command syntax.",
222
- inputSchema: {
223
- type: "object",
224
- properties: {
225
- folder_path: { type: "string", description: "Absolute Windows path to the folder." },
226
- file_index: {
227
- type: "number",
228
- description: "Which file to process (1-based). Omit to list files first.",
229
- },
230
- language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
231
- threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
232
- recursive: { type: "boolean", description: "Include subfolders. Defaults to false.", default: false },
233
- },
234
- required: ["folder_path"],
235
- },
236
- },
237
- {
238
- name: "generate_subtitles",
239
- description:
240
- "Generate an SRT subtitle file for an audio or video file. " +
241
- "Saved next to the source file. Load in VLC via Subtitle → Add Subtitle File. " +
242
- "Supports all the same formats as transcribe_audio.",
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- file_path: { type: "string", description: "Absolute Windows path to the file." },
247
- language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
248
- threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
249
- },
250
- required: ["file_path"],
251
- },
252
- },
253
- {
254
- name: "check_config",
255
- description: "Verify whisper-cli.exe, model, and FFmpeg are all available. Run this first if anything fails.",
256
- inputSchema: { type: "object", properties: {} },
257
- },
258
- ],
259
- }));
260
-
261
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
262
- const { name, arguments: args } = request.params;
263
-
264
- // -------------------------------------------------------------------------
265
- // check_config
266
- // -------------------------------------------------------------------------
267
- if (name === "check_config") {
268
- const error = validatePaths();
269
- if (error) return { content: [{ type: "text", text: `❌ Configuration error:\n\n${error}` }], isError: true };
270
-
271
- let ffmpegStatus = "✅ Found";
272
- try { await execFileAsync(FFMPEG_PATH, ["-version"], { windowsHide: true }); }
273
- catch { ffmpegStatus = "⚠️ Not found — video/non-MP3 formats require FFmpeg in PATH"; }
274
-
275
- return {
276
- content: [{
277
- type: "text",
278
- text:
279
- `✅ Configuration looks good!\n\n` +
280
- `whisper-cli: ${WHISPER_CLI_PATH}\n` +
281
- `Model: ${WHISPER_MODEL}\n` +
282
- `Threads: ${WHISPER_THREADS} of ${SYSTEM_THREADS} logical cores\n` +
283
- `FFmpeg: ${ffmpegStatus}\n\n` +
284
- `Optional env vars: WHISPER_THREADS, FFMPEG_PATH`,
285
- }],
286
- };
287
- }
288
-
289
- // -------------------------------------------------------------------------
290
- // transcribe_audio
291
- // -------------------------------------------------------------------------
292
- if (name === "transcribe_audio") {
293
- const filePath = args?.file_path as string;
294
- const model = (args?.model as string) || WHISPER_MODEL;
295
- const language = (args?.language as string) || "en";
296
- const outputFormat = ((args?.output_format as string) || "text") as OutputFormat;
297
- const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round((args?.threads as number) || WHISPER_THREADS)));
298
- const saveToFile = (args?.save_to_file as boolean) || false;
299
-
300
- if (!filePath) return { content: [{ type: "text", text: "file_path is required." }], isError: true };
301
- if (!existsSync(filePath)) return { content: [{ type: "text", text: `File not found: ${filePath}` }], isError: true };
302
- const configError = validatePaths();
303
- if (configError) return { content: [{ type: "text", text: configError }], isError: true };
304
-
305
- try {
306
- const result = await transcribeSingle(filePath, model, language, outputFormat, threads, saveToFile);
307
- let response = result.text;
308
- if (result.savedTo) response += `\n\n[Transcript saved to: ${result.savedTo}]`;
309
- if (result.srtPath) response += `\n\n[SRT subtitle file saved to: ${result.srtPath}]`;
310
- return { content: [{ type: "text", text: response }] };
311
- } catch (err: any) {
312
- return { content: [{ type: "text", text: `Transcription failed:\n\n${err?.stderr || err?.message || String(err)}` }], isError: true };
313
- }
314
- }
315
-
316
- // -------------------------------------------------------------------------
317
- // generate_subtitles
318
- // -------------------------------------------------------------------------
319
- if (name === "generate_subtitles") {
320
- const filePath = args?.file_path as string;
321
- const language = (args?.language as string) || "en";
322
- const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round((args?.threads as number) || WHISPER_THREADS)));
323
-
324
- if (!filePath) return { content: [{ type: "text", text: "file_path is required." }], isError: true };
325
- if (!existsSync(filePath)) return { content: [{ type: "text", text: `File not found: ${filePath}` }], isError: true };
326
- const configError = validatePaths();
327
- if (configError) return { content: [{ type: "text", text: configError }], isError: true };
328
-
329
- try {
330
- const result = await transcribeSingle(filePath, WHISPER_MODEL, language, "srt", threads, false);
331
- return {
332
- content: [{
333
- type: "text",
334
- text:
335
- `✅ Subtitle file generated!\n\n` +
336
- `Saved to: ${result.srtPath}\n\n` +
337
- `To use in VLC: Subtitle → Add Subtitle File → select the .srt file.\n` +
338
- `Works in any video player that supports external subtitles.`,
339
- }],
340
- };
341
- } catch (err: any) {
342
- return { content: [{ type: "text", text: `Subtitle generation failed:\n\n${err?.stderr || err?.message || String(err)}` }], isError: true };
343
- }
344
- }
345
-
346
- // -------------------------------------------------------------------------
347
- // transcribe_batch (interactive only)
348
- // -------------------------------------------------------------------------
349
- if (name === "transcribe_batch") {
350
- const folderPath = args?.folder_path as string;
351
- const language = (args?.language as string) || "en";
352
- const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round((args?.threads as number) || WHISPER_THREADS)));
353
- const recursive = (args?.recursive as boolean) || false;
354
- const fileIndex = args?.file_index as number | undefined;
355
-
356
- if (!folderPath) return { content: [{ type: "text", text: "folder_path is required." }], isError: true };
357
- if (!existsSync(folderPath)) return { content: [{ type: "text", text: `Folder not found: ${folderPath}` }], isError: true };
358
- const configError = validatePaths();
359
- if (configError) return { content: [{ type: "text", text: configError }], isError: true };
360
-
361
- const files = getFiles(folderPath, recursive);
362
-
363
- if (files.length === 0) {
364
- return {
365
- content: [{
366
- type: "text",
367
- text: `No supported files found in: ${folderPath}\nSupported formats: ${SUPPORTED_EXTENSIONS.join(", ")}`,
368
- }],
369
- };
370
- }
371
-
372
- // No file_index: return file list
373
- if (fileIndex === undefined) {
374
- return {
375
- content: [{
376
- type: "text",
377
- text:
378
- `Found ${files.length} file(s) in: ${folderPath}\n\n` +
379
- files.map((f, i) => {
380
- const txtPath = f.replace(/\.[^.]+$/, ".txt");
381
- const done = existsSync(txtPath) ? " ✅ already done" : "";
382
- return ` ${i + 1}. ${basename(f)}${done}`;
383
- }).join("\n") +
384
- `\n\nTo start, say "transcribe file 1" (or any number). I'll process one file at a time and wait for your go-ahead before continuing.\n` +
385
- `\nFor large unattended batches, see the command line approach in TROUBLESHOOTING.md.`,
386
- }],
387
- };
388
- }
389
-
390
- // Process the requested file
391
- const idx = fileIndex - 1;
392
- if (idx < 0 || idx >= files.length) {
393
- return { content: [{ type: "text", text: `Invalid file number. Choose between 1 and ${files.length}.` }], isError: true };
394
- }
395
-
396
- const filePath = files[idx];
397
- const fileName = basename(filePath);
398
- const txtPath = filePath.replace(/\.[^.]+$/, ".txt");
399
-
400
- try {
401
- const result = await transcribeSingle(filePath, WHISPER_MODEL, language, "text", threads, true);
402
- const remaining = files.length - fileIndex;
403
- const nextMsg = remaining > 0
404
- ? `\n\n${remaining} file(s) remaining. Say "continue" or "transcribe file ${fileIndex + 1}" to proceed, or "stop" to finish.`
405
- : `\n\n✅ That was the last file. Batch complete!`;
406
-
407
- return {
408
- content: [{
409
- type: "text",
410
- text:
411
- `[${fileIndex}/${files.length}] ✅ ${fileName}\n\n` +
412
- `Saved to: ${txtPath}\n\n` +
413
- `Preview:\n${result.text.slice(0, 500)}${result.text.length > 500 ? "..." : ""}` +
414
- nextMsg,
415
- }],
416
- };
417
- } catch (err: any) {
418
- return {
419
- content: [{
420
- type: "text",
421
- text:
422
- `[${fileIndex}/${files.length}] ❌ Failed: ${fileName}\n\n` +
423
- `Error: ${err?.stderr || err?.message || String(err)}\n\n` +
424
- `Say "transcribe file ${fileIndex + 1}" to skip and continue.`,
425
- }],
426
- isError: true,
427
- };
428
- }
429
- }
430
-
431
- return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
432
- });
433
-
434
- // ---------------------------------------------------------------------------
435
- // Start
436
- // ---------------------------------------------------------------------------
437
- async function main() {
438
- const transport = new StdioServerTransport();
439
- await server.connect(transport);
440
- console.error(`whisper-windows-mcp v1.4.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
441
- }
442
-
443
- main().catch((err) => {
444
- console.error("Fatal error:", err);
445
- process.exit(1);
446
- });
package/src/index.ts.bak DELETED
@@ -1,446 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * whisper-windows-mcp
4
- * A Windows-native MCP server for local audio transcription using whisper.cpp
5
- * https://github.com/eviscerations/whisper-windows-mcp
6
- */
7
-
8
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
- import {
11
- CallToolRequestSchema,
12
- ListToolsRequestSchema,
13
- } from "@modelcontextprotocol/sdk/types.js";
14
- import { execFile } from "child_process";
15
- import {
16
- existsSync, unlinkSync, readdirSync,
17
- writeFileSync, readFileSync
18
- } from "fs";
19
- import { cpus, tmpdir } from "os";
20
- import { join, extname, basename } from "path";
21
- import { promisify } from "util";
22
-
23
- const execFileAsync = promisify(execFile);
24
-
25
- // ---------------------------------------------------------------------------
26
- // Configuration
27
- // ---------------------------------------------------------------------------
28
- const WHISPER_CLI_PATH =
29
- process.env.WHISPER_CLI_PATH ?? "C:\\whisper\\Release\\whisper-cli.exe";
30
- const WHISPER_MODEL =
31
- process.env.WHISPER_MODEL ?? "C:\\whisper\\models\\ggml-base.en.bin";
32
- const FFMPEG_PATH =
33
- process.env.FFMPEG_PATH ?? "ffmpeg";
34
-
35
- const SYSTEM_THREADS = cpus().length;
36
- const DEFAULT_THREADS = Math.max(2, Math.floor(SYSTEM_THREADS / 2));
37
- const WHISPER_THREADS = parseInt(process.env.WHISPER_THREADS ?? String(DEFAULT_THREADS), 10);
38
-
39
- const SUPPORTED_EXTENSIONS = [
40
- ".mp3", ".wav",
41
- ".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v",
42
- ".m4a", ".ogg", ".flac",
43
- ];
44
- const NATIVE_EXTENSIONS = [".mp3", ".wav"];
45
-
46
- // ---------------------------------------------------------------------------
47
- // Helpers
48
- // ---------------------------------------------------------------------------
49
- function validatePaths(): string | null {
50
- if (!existsSync(WHISPER_CLI_PATH))
51
- return `whisper-cli.exe not found at: ${WHISPER_CLI_PATH}\nCheck WHISPER_CLI_PATH in claude_desktop_config.json`;
52
- if (!existsSync(WHISPER_MODEL))
53
- return `Whisper model not found at: ${WHISPER_MODEL}\nCheck WHISPER_MODEL in claude_desktop_config.json`;
54
- return null;
55
- }
56
-
57
- /**
58
- * Check whether a whisper-cli.exe process is already running.
59
- * Uses tasklist /FI which is available on all Windows versions.
60
- * Returns true if found, false if not (or if tasklist itself fails).
61
- */
62
- async function isWhisperRunning(): Promise<boolean> {
63
- try {
64
- const { stdout } = await execFileAsync(
65
- "tasklist",
66
- ["/FI", "IMAGENAME eq whisper-cli.exe", "/NH"],
67
- { windowsHide: true }
68
- );
69
- return stdout.toLowerCase().includes("whisper-cli.exe");
70
- } catch {
71
- // If tasklist fails for any reason, assume safe to proceed
72
- return false;
73
- }
74
- }
75
-
76
- function needsConversion(filePath: string): boolean {
77
- return !NATIVE_EXTENSIONS.includes(extname(filePath).toLowerCase());
78
- }
79
-
80
- function isSupportedFile(filePath: string): boolean {
81
- return SUPPORTED_EXTENSIONS.includes(extname(filePath).toLowerCase());
82
- }
83
-
84
- async function convertToWav(inputPath: string): Promise<string> {
85
- const tmpFile = join(tmpdir(), `whisper_tmp_${Date.now()}.wav`);
86
- await execFileAsync(FFMPEG_PATH, [
87
- "-y", "-i", inputPath,
88
- "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", tmpFile,
89
- ], { windowsHide: true });
90
- return tmpFile;
91
- }
92
-
93
- type OutputFormat = "text" | "timestamps" | "json" | "srt";
94
-
95
- function buildArgs(
96
- filePath: string, model: string, language: string,
97
- outputFormat: OutputFormat, threads: number
98
- ): string[] {
99
- const args = ["-m", model, "-f", filePath, "-l", language, "-t", String(threads)];
100
- if (outputFormat === "srt") {
101
- args.push("-osrt", "-of", filePath.replace(/\.[^.]+$/, ""));
102
- } else if (outputFormat === "json") {
103
- args.push("-oj");
104
- } else if (outputFormat === "text") {
105
- args.push("--no-timestamps");
106
- }
107
- return args;
108
- }
109
-
110
- async function transcribeSingle(
111
- filePath: string, model: string, language: string,
112
- outputFormat: OutputFormat, threads: number, saveToFile = false
113
- ): Promise<{ text: string; srtPath?: string; savedTo?: string }> {
114
-
115
- // ---- Priority 2: Process lock ----
116
- // Never spawn a second whisper-cli.exe while one is already running.
117
- if (await isWhisperRunning()) {
118
- throw new Error(
119
- "Transcription already in progress.\n\n" +
120
- "whisper-cli.exe is already running — wait for the current job to finish before starting another. " +
121
- "If you believe this is wrong (e.g. a previous job crashed and left a stale process), " +
122
- "open Task Manager, find whisper-cli.exe under Details, and end the task."
123
- );
124
- }
125
-
126
- let transcribeFrom = filePath;
127
- let tmpFile: string | null = null;
128
-
129
- if (needsConversion(filePath)) {
130
- tmpFile = await convertToWav(filePath);
131
- transcribeFrom = tmpFile;
132
- }
133
-
134
- try {
135
- const cliArgs = buildArgs(transcribeFrom, model, language, outputFormat, threads);
136
- const { stdout, stderr } = await execFileAsync(WHISPER_CLI_PATH, cliArgs, {
137
- maxBuffer: 100 * 1024 * 1024,
138
- windowsHide: true,
139
- });
140
-
141
- const output = (stdout || stderr || "").trim();
142
-
143
- if (outputFormat === "srt") {
144
- const tmpSrt = transcribeFrom.replace(/\.[^.]+$/, ".srt");
145
- const destSrt = filePath.replace(/\.[^.]+$/, ".srt");
146
- if (tmpFile && existsSync(tmpSrt)) {
147
- writeFileSync(destSrt, readFileSync(tmpSrt, "utf8"));
148
- try { unlinkSync(tmpSrt); } catch { }
149
- }
150
- return { text: output, srtPath: destSrt };
151
- }
152
-
153
- if (saveToFile) {
154
- const txtPath = filePath.replace(/\.[^.]+$/, ".txt");
155
- writeFileSync(txtPath, output, "utf8");
156
- return { text: output, savedTo: txtPath };
157
- }
158
-
159
- return { text: output };
160
- } finally {
161
- if (tmpFile && existsSync(tmpFile)) try { unlinkSync(tmpFile); } catch { }
162
- }
163
- }
164
-
165
- function getFiles(dir: string, recursive: boolean): string[] {
166
- const entries = readdirSync(dir, { withFileTypes: true });
167
- const files: string[] = [];
168
- for (const entry of entries) {
169
- const fullPath = join(dir, entry.name);
170
- if (entry.isDirectory() && recursive) files.push(...getFiles(fullPath, true));
171
- else if (entry.isFile() && isSupportedFile(entry.name)) files.push(fullPath);
172
- }
173
- return files;
174
- }
175
-
176
- // ---------------------------------------------------------------------------
177
- // MCP Server
178
- // ---------------------------------------------------------------------------
179
- const server = new Server(
180
- { name: "whisper-windows-mcp", version: "1.3.1" },
181
- { capabilities: { tools: {} } }
182
- );
183
-
184
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
185
- tools: [
186
- {
187
- name: "transcribe_audio",
188
- description:
189
- "Transcribe a single audio or video file using whisper.cpp on Windows. " +
190
- "Natively supports mp3 and wav. Automatically converts mp4, mkv, avi, mov, " +
191
- "webm, m4a, flac, ogg etc. via FFmpeg — no manual conversion needed. " +
192
- "Can output plain text, timestamps, JSON, or SRT subtitle files. " +
193
- "WARNING: files over 30 minutes cannot be cancelled cleanly from Claude — " +
194
- "use Task Manager to kill the process if needed.",
195
- inputSchema: {
196
- type: "object",
197
- properties: {
198
- file_path: { type: "string", description: "Absolute Windows path, e.g. C:\\Users\\You\\Downloads\\recording.mp4" },
199
- model: { type: "string", description: "Override model path. Leave blank to use WHISPER_MODEL." },
200
- language: { type: "string", description: "Language code, e.g. en, es, fr. Defaults to en.", default: "en" },
201
- output_format: {
202
- type: "string", enum: ["text", "timestamps", "json", "srt"],
203
- description: "text = plain (default), timestamps = with time codes, json = structured, srt = subtitle file saved next to source.",
204
- default: "text",
205
- },
206
- threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
207
- save_to_file: { type: "boolean", description: "Save transcript as .txt next to the source file.", default: false },
208
- },
209
- required: ["file_path"],
210
- },
211
- },
212
- {
213
- name: "transcribe_batch",
214
- description:
215
- "Transcribe multiple audio/video files in a folder interactively, one file at a time. " +
216
- "Shows a preview of each transcript and waits for confirmation before continuing. " +
217
- "Saves each transcript as a .txt file next to its source. " +
218
- "Files already transcribed (with matching .txt) are shown as done and skipped. " +
219
- "Supported formats: mp3, wav, mp4, mkv, avi, mov, webm, m4a, flac, ogg. " +
220
- "NOTE: For large unattended batch jobs, use whisper-cli.exe directly from the command line " +
221
- "— see TROUBLESHOOTING.md for the command syntax.",
222
- inputSchema: {
223
- type: "object",
224
- properties: {
225
- folder_path: { type: "string", description: "Absolute Windows path to the folder." },
226
- file_index: {
227
- type: "number",
228
- description: "Which file to process (1-based). Omit to list files first.",
229
- },
230
- language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
231
- threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
232
- recursive: { type: "boolean", description: "Include subfolders. Defaults to false.", default: false },
233
- },
234
- required: ["folder_path"],
235
- },
236
- },
237
- {
238
- name: "generate_subtitles",
239
- description:
240
- "Generate an SRT subtitle file for an audio or video file. " +
241
- "Saved next to the source file. Load in VLC via Subtitle → Add Subtitle File. " +
242
- "Supports all the same formats as transcribe_audio.",
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- file_path: { type: "string", description: "Absolute Windows path to the file." },
247
- language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
248
- threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
249
- },
250
- required: ["file_path"],
251
- },
252
- },
253
- {
254
- name: "check_config",
255
- description: "Verify whisper-cli.exe, model, and FFmpeg are all available. Run this first if anything fails.",
256
- inputSchema: { type: "object", properties: {} },
257
- },
258
- ],
259
- }));
260
-
261
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
262
- const { name, arguments: args } = request.params;
263
-
264
- // -------------------------------------------------------------------------
265
- // check_config
266
- // -------------------------------------------------------------------------
267
- if (name === "check_config") {
268
- const error = validatePaths();
269
- if (error) return { content: [{ type: "text", text: `❌ Configuration error:\n\n${error}` }], isError: true };
270
-
271
- let ffmpegStatus = "✅ Found";
272
- try { await execFileAsync(FFMPEG_PATH, ["-version"], { windowsHide: true }); }
273
- catch { ffmpegStatus = "⚠️ Not found — video/non-MP3 formats require FFmpeg in PATH"; }
274
-
275
- return {
276
- content: [{
277
- type: "text",
278
- text:
279
- `✅ Configuration looks good!\n\n` +
280
- `whisper-cli: ${WHISPER_CLI_PATH}\n` +
281
- `Model: ${WHISPER_MODEL}\n` +
282
- `Threads: ${WHISPER_THREADS} of ${SYSTEM_THREADS} logical cores\n` +
283
- `FFmpeg: ${ffmpegStatus}\n\n` +
284
- `Optional env vars: WHISPER_THREADS, FFMPEG_PATH`,
285
- }],
286
- };
287
- }
288
-
289
- // -------------------------------------------------------------------------
290
- // transcribe_audio
291
- // -------------------------------------------------------------------------
292
- if (name === "transcribe_audio") {
293
- const filePath = args?.file_path as string;
294
- const model = (args?.model as string) || WHISPER_MODEL;
295
- const language = (args?.language as string) || "en";
296
- const outputFormat = ((args?.output_format as string) || "text") as OutputFormat;
297
- const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round((args?.threads as number) || WHISPER_THREADS)));
298
- const saveToFile = (args?.save_to_file as boolean) || false;
299
-
300
- if (!filePath) return { content: [{ type: "text", text: "file_path is required." }], isError: true };
301
- if (!existsSync(filePath)) return { content: [{ type: "text", text: `File not found: ${filePath}` }], isError: true };
302
- const configError = validatePaths();
303
- if (configError) return { content: [{ type: "text", text: configError }], isError: true };
304
-
305
- try {
306
- const result = await transcribeSingle(filePath, model, language, outputFormat, threads, saveToFile);
307
- let response = result.text;
308
- if (result.savedTo) response += `\n\n[Transcript saved to: ${result.savedTo}]`;
309
- if (result.srtPath) response += `\n\n[SRT subtitle file saved to: ${result.srtPath}]`;
310
- return { content: [{ type: "text", text: response }] };
311
- } catch (err: any) {
312
- return { content: [{ type: "text", text: `Transcription failed:\n\n${err?.stderr || err?.message || String(err)}` }], isError: true };
313
- }
314
- }
315
-
316
- // -------------------------------------------------------------------------
317
- // generate_subtitles
318
- // -------------------------------------------------------------------------
319
- if (name === "generate_subtitles") {
320
- const filePath = args?.file_path as string;
321
- const language = (args?.language as string) || "en";
322
- const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round((args?.threads as number) || WHISPER_THREADS)));
323
-
324
- if (!filePath) return { content: [{ type: "text", text: "file_path is required." }], isError: true };
325
- if (!existsSync(filePath)) return { content: [{ type: "text", text: `File not found: ${filePath}` }], isError: true };
326
- const configError = validatePaths();
327
- if (configError) return { content: [{ type: "text", text: configError }], isError: true };
328
-
329
- try {
330
- const result = await transcribeSingle(filePath, WHISPER_MODEL, language, "srt", threads, false);
331
- return {
332
- content: [{
333
- type: "text",
334
- text:
335
- `✅ Subtitle file generated!\n\n` +
336
- `Saved to: ${result.srtPath}\n\n` +
337
- `To use in VLC: Subtitle → Add Subtitle File → select the .srt file.\n` +
338
- `Works in any video player that supports external subtitles.`,
339
- }],
340
- };
341
- } catch (err: any) {
342
- return { content: [{ type: "text", text: `Subtitle generation failed:\n\n${err?.stderr || err?.message || String(err)}` }], isError: true };
343
- }
344
- }
345
-
346
- // -------------------------------------------------------------------------
347
- // transcribe_batch (interactive only)
348
- // -------------------------------------------------------------------------
349
- if (name === "transcribe_batch") {
350
- const folderPath = args?.folder_path as string;
351
- const language = (args?.language as string) || "en";
352
- const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round((args?.threads as number) || WHISPER_THREADS)));
353
- const recursive = (args?.recursive as boolean) || false;
354
- const fileIndex = args?.file_index as number | undefined;
355
-
356
- if (!folderPath) return { content: [{ type: "text", text: "folder_path is required." }], isError: true };
357
- if (!existsSync(folderPath)) return { content: [{ type: "text", text: `Folder not found: ${folderPath}` }], isError: true };
358
- const configError = validatePaths();
359
- if (configError) return { content: [{ type: "text", text: configError }], isError: true };
360
-
361
- const files = getFiles(folderPath, recursive);
362
-
363
- if (files.length === 0) {
364
- return {
365
- content: [{
366
- type: "text",
367
- text: `No supported files found in: ${folderPath}\nSupported formats: ${SUPPORTED_EXTENSIONS.join(", ")}`,
368
- }],
369
- };
370
- }
371
-
372
- // No file_index: return file list
373
- if (fileIndex === undefined) {
374
- return {
375
- content: [{
376
- type: "text",
377
- text:
378
- `Found ${files.length} file(s) in: ${folderPath}\n\n` +
379
- files.map((f, i) => {
380
- const txtPath = f.replace(/\.[^.]+$/, ".txt");
381
- const done = existsSync(txtPath) ? " ✅ already done" : "";
382
- return ` ${i + 1}. ${basename(f)}${done}`;
383
- }).join("\n") +
384
- `\n\nTo start, say "transcribe file 1" (or any number). I'll process one file at a time and wait for your go-ahead before continuing.\n` +
385
- `\nFor large unattended batches, see the command line approach in TROUBLESHOOTING.md.`,
386
- }],
387
- };
388
- }
389
-
390
- // Process the requested file
391
- const idx = fileIndex - 1;
392
- if (idx < 0 || idx >= files.length) {
393
- return { content: [{ type: "text", text: `Invalid file number. Choose between 1 and ${files.length}.` }], isError: true };
394
- }
395
-
396
- const filePath = files[idx];
397
- const fileName = basename(filePath);
398
- const txtPath = filePath.replace(/\.[^.]+$/, ".txt");
399
-
400
- try {
401
- const result = await transcribeSingle(filePath, WHISPER_MODEL, language, "text", threads, true);
402
- const remaining = files.length - fileIndex;
403
- const nextMsg = remaining > 0
404
- ? `\n\n${remaining} file(s) remaining. Say "continue" or "transcribe file ${fileIndex + 1}" to proceed, or "stop" to finish.`
405
- : `\n\n✅ That was the last file. Batch complete!`;
406
-
407
- return {
408
- content: [{
409
- type: "text",
410
- text:
411
- `[${fileIndex}/${files.length}] ✅ ${fileName}\n\n` +
412
- `Saved to: ${txtPath}\n\n` +
413
- `Preview:\n${result.text.slice(0, 500)}${result.text.length > 500 ? "..." : ""}` +
414
- nextMsg,
415
- }],
416
- };
417
- } catch (err: any) {
418
- return {
419
- content: [{
420
- type: "text",
421
- text:
422
- `[${fileIndex}/${files.length}] ❌ Failed: ${fileName}\n\n` +
423
- `Error: ${err?.stderr || err?.message || String(err)}\n\n` +
424
- `Say "transcribe file ${fileIndex + 1}" to skip and continue.`,
425
- }],
426
- isError: true,
427
- };
428
- }
429
- }
430
-
431
- return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
432
- });
433
-
434
- // ---------------------------------------------------------------------------
435
- // Start
436
- // ---------------------------------------------------------------------------
437
- async function main() {
438
- const transport = new StdioServerTransport();
439
- await server.connect(transport);
440
- console.error(`whisper-windows-mcp v1.3.1 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
441
- }
442
-
443
- main().catch((err) => {
444
- console.error("Fatal error:", err);
445
- process.exit(1);
446
- });
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ES2020",
5
- "moduleResolution": "node",
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "declaration": true
12
- },
13
- "include": ["src/**/*"],
14
- "exclude": ["node_modules", "dist"]
15
- }