whisper-windows-mcp 1.7.0 → 1.9.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 (3) hide show
  1. package/README.md +338 -229
  2. package/dist/index.js +345 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,229 +1,338 @@
1
- # whisper-windows-mcp
2
-
3
- A Windows-native MCP (Model Context Protocol) server that lets Claude Desktop transcribe audio files locally using [whisper.cpp](https://github.com/ggml-org/whisper.cpp) — no internet connection required, no data sent to the cloud.
4
-
5
- > **Why does this exist?**
6
- > The popular `whisper-mcp` package was built for macOS and assumes a Unix environment. It does not work on Windows. This package was written specifically for Windows users who want the same functionality.
7
-
8
- ---
9
-
10
- ## What you can do with it
11
-
12
- Once installed, you can say things like this directly in Claude Desktop:
13
-
14
- - *"Transcribe C:\Users\Me\Downloads\meeting.mp3"*
15
- - *"Transcribe this recording and give me a summary"*
16
- - *"Transcribe with timestamps so I can find specific moments"*
17
- - *"Generate subtitles for this video"*
18
-
19
- Everything runs on your own machine. No audio ever leaves your computer.
20
-
21
- ---
22
-
23
- ## Requirements
24
-
25
- Before installing this package, you need three things set up on your Windows machine:
26
-
27
- 1. **Node.js 18 or later** — [download from nodejs.org](https://nodejs.org)
28
- 2. **whisper.cpp binaries** — the actual transcription engine (see Step 1 below)
29
- 3. **A Whisper model file** — the AI model that does the transcription (see Step 2 below)
30
-
31
- ---
32
-
33
- ## Step 1 — Install whisper.cpp binaries
34
-
35
- ### Option A — Pre-built Vulkan release (recommended)
36
-
37
- Download `whisper-vulkan-win-x64.zip` from the [releases page](https://github.com/eviscerations/whisper-windows-mcp/releases).
38
-
39
- This is a custom-compiled build with **Vulkan GPU acceleration** enabled. It works with AMD, NVIDIA, and Intel GPUs on Windows — no vendor-specific SDK required.
40
-
41
- Extract the zip to `C:\whisper\Release\`. You should end up with these files:
42
-
43
- ```
44
- C:\whisper\Release\whisper-cli.exe
45
- C:\whisper\Release\ggml-vulkan.dll
46
- C:\whisper\Release\ggml.dll
47
- C:\whisper\Release\ggml-base.dll
48
- C:\whisper\Release\ggml-cpu.dll
49
- C:\whisper\Release\whisper.dll
50
- ```
51
-
52
- **GPU acceleration is automatic** — if a supported GPU is present, whisper.cpp will use it. No additional configuration needed.
53
-
54
- ### Option B — Build from source
55
-
56
- If you prefer to compile your own binary (advanced users):
57
-
58
- **Prerequisites:** Git, CMake, Visual Studio Build Tools 2022+ with "Desktop development with C++", Vulkan SDK from [lunarg.com](https://vulkan.lunarg.com/sdk/home#windows).
59
-
60
- ```
61
- git clone https://github.com/ggml-org/whisper.cpp
62
- cd whisper.cpp
63
- cmake -B build -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release
64
- cmake --build build --config Release --target whisper-cli
65
- ```
66
-
67
- Copy the resulting binaries from `build\bin\Release\` to `C:\whisper\Release\`.
68
-
69
- > **Note:** The default whisper.cpp Windows release on GitHub does not include a Vulkan build. You must either use the pre-built release above or compile from source with `-DGGML_VULKAN=ON`.
70
-
71
- ---
72
-
73
- ## Step 2 — Download a Whisper model
74
-
75
- Models are downloaded from Hugging Face. Choose one based on your needs:
76
-
77
- | Model | File size | Speed | Accuracy | Recommended for |
78
- |---|---|---|---|---|
79
- | `ggml-tiny.en.bin` | 75 MB | Very fast | Basic | Quick tests |
80
- | `ggml-base.en.bin` | 142 MB | Fast | Good | Everyday use |
81
- | `ggml-small.en.bin` | 466 MB | Moderate | Better | Important recordings |
82
- | `ggml-medium.en.bin` | 1.5 GB | Fast on GPU | Very good | Best quality |
83
- | `ggml-large-v3.bin` | 2.9 GB | Fast on GPU | Excellent | Maximum accuracy |
84
-
85
- **For most people, `base.en` or `small.en` is the best starting point.** With GPU acceleration, `medium.en` and `large-v3` become practical for everyday use.
86
-
87
- Download your chosen model from:
88
-
89
- ```
90
- https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin
91
- ```
92
-
93
- Save it to `C:\whisper\models\` — create that folder if it doesn't exist.
94
-
95
- ---
96
-
97
- ## Step 3 — Install this MCP server
98
-
99
- ```
100
- npm install -g whisper-windows-mcp
101
- ```
102
-
103
- Or use `npx` directly in your config (see Step 4).
104
-
105
- ---
106
-
107
- ## Step 4 — Configure Claude Desktop
108
-
109
- 1. Open Claude Desktop
110
- 2. Go to **Settings → Developer → Edit Config**
111
- 3. Add the whisper-windows-mcp entry:
112
-
113
- ```json
114
- {
115
- "mcpServers": {
116
- "whisper": {
117
- "command": "npx",
118
- "args": ["-y", "whisper-windows-mcp"],
119
- "env": {
120
- "WHISPER_CLI_PATH": "C:\\whisper\\Release\\whisper-cli.exe",
121
- "WHISPER_MODEL": "C:\\whisper\\models\\ggml-base.en.bin"
122
- }
123
- }
124
- }
125
- }
126
- ```
127
-
128
- > **Important:** If your `claude_desktop_config.json` already has other content, add the `"mcpServers"` block inside the existing `{}` — don't replace the whole file.
129
-
130
- 4. Save the file and fully restart Claude Desktop.
131
- 5. Go to **Settings → Developer** — you should see **whisper** listed with a green **running** badge.
132
-
133
- ---
134
-
135
- ## Step 5 — Test it
136
-
137
- In Claude Desktop, type:
138
-
139
- > *"Can you check your whisper config?"*
140
-
141
- Claude will use the `check_config` tool to verify everything is set up correctly. Then try a transcription:
142
-
143
- > *"Please transcribe C:\Users\YourName\Downloads\recording.mp3"*
144
-
145
- ---
146
-
147
- ## GPU acceleration
148
-
149
- The pre-built Vulkan release enables GPU acceleration automatically. No flags or configuration required — whisper.cpp detects your GPU at startup and uses it if available.
150
-
151
- **Confirmed working:** AMD Radeon RX Vega 56 (GCN 5th gen), and any GPU with Vulkan 1.0+ support.
152
-
153
- **Performance comparison with medium.en model:**
154
-
155
- | Hardware | ~5 min audio file |
156
- |---|---|
157
- | CPU only (Ryzen 7 2700x) | ~8–12 minutes |
158
- | GPU (Vega 56 via Vulkan) | ~20–40 seconds |
159
-
160
- GPU utilization during transcription is typically 15–30% — efficient bursts, not sustained load.
161
-
162
- ---
163
-
164
- ## Output formats
165
-
166
- - **text** (default) — plain transcript
167
- - **timestamps** — transcript with `[00:00:00 --> 00:00:05]` time codes
168
- - **json** — structured output
169
- - **srt** — subtitle file saved next to the source file
170
-
171
- ---
172
-
173
- ## Full config example
174
-
175
- ```json
176
- {
177
- "preferences": {
178
- "coworkWebSearchEnabled": true
179
- },
180
- "mcpServers": {
181
- "whisper": {
182
- "command": "npx",
183
- "args": ["-y", "whisper-windows-mcp"],
184
- "env": {
185
- "WHISPER_CLI_PATH": "C:\\whisper\\Release\\whisper-cli.exe",
186
- "WHISPER_MODEL": "C:\\whisper\\models\\ggml-medium.en.bin"
187
- }
188
- }
189
- }
190
- }
191
- ```
192
-
193
- Config file location: `C:\Users\YourUsername\AppData\Roaming\Claude\claude_desktop_config.json`
194
-
195
- ---
196
-
197
- ## Optional environment variables
198
-
199
- | Variable | Description |
200
- |---|---|
201
- | `WHISPER_CLI_PATH` | Path to whisper-cli.exe (required) |
202
- | `WHISPER_MODEL` | Path to model .bin file (required) |
203
- | `WHISPER_THREADS` | CPU thread count override |
204
- | `FFMPEG_PATH` | Path to ffmpeg if not in system PATH |
205
-
206
- ---
207
-
208
- ## Troubleshooting
209
-
210
- See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed solutions.
211
-
212
- Quick checklist:
213
- - Paths in config use **double backslashes** (`C:\\whisper\\...`)
214
- - `whisper-cli.exe` exists at the path specified
215
- - The model `.bin` file exists at the path specified
216
- - Claude Desktop was fully restarted after editing config
217
- - The whisper server shows **running** in Settings → Developer
218
-
219
- ---
220
-
221
- ## License
222
-
223
- MIT — free to use, modify, and distribute.
224
-
225
- ---
226
-
227
- ## Contributing
228
-
229
- Pull requests welcome. See [ROADMAP.md](ROADMAP.md) for planned features.
1
+ # whisper-windows-mcp
2
+
3
+ A Windows-native MCP (Model Context Protocol) server that lets Claude Desktop transcribe audio and video files locally using [whisper.cpp](https://github.com/ggml-org/whisper.cpp) — with GPU acceleration, multilingual support, and batch processing. No internet connection required. No audio ever leaves your machine.
4
+
5
+ > **Why does this exist?**
6
+ > The popular `whisper-mcp` package was built for macOS and assumes a Unix environment. It does not work on Windows. This package was written specifically for Windows users who want local AI transcription integrated with Claude Desktop.
7
+
8
+ ---
9
+
10
+ ## What you can do with it
11
+
12
+ Once installed, you can say things like this directly in Claude Desktop:
13
+
14
+ - *"Transcribe C:\Users\Me\Downloads\meeting.mp3"*
15
+ - *"Transcribe this folder of recordings and save each as a text file"*
16
+ - *"Generate Japanese and English subtitles for this video"*
17
+ - *"Start a batch transcription of everything in this folder"*
18
+ - *"How long will it take to transcribe these files?"*
19
+ - *"Check if GPU acceleration is working"*
20
+
21
+ ---
22
+
23
+ ## Requirements
24
+
25
+ 1. **Node.js 18 or later** — [nodejs.org](https://nodejs.org)
26
+ 2. **whisper.cpp binaries with Vulkan GPU support** — see Step 1
27
+ 3. **A Whisper model file** — see Step 2
28
+ 4. **FFmpeg** — required for video files and non-WAV/MP3 audio
29
+
30
+ ---
31
+
32
+ ## Step 1 — Install whisper.cpp binaries
33
+
34
+ ### Option A — Pre-built Vulkan release (recommended)
35
+
36
+ Download `whisper-vulkan-win-x64.zip` from the [releases page](https://github.com/eviscerations/whisper-windows-mcp/releases/tag/v1.4.0).
37
+
38
+ This is a custom-compiled build with **Vulkan GPU acceleration** enabled. Works with AMD, NVIDIA, and Intel GPUs — no vendor-specific SDK required.
39
+
40
+ Extract to `C:\whisper\Release\`. You should end up with:
41
+
42
+ ```
43
+ C:\whisper\Release\whisper-cli.exe
44
+ C:\whisper\Release\ggml-vulkan.dll
45
+ C:\whisper\Release\ggml.dll
46
+ C:\whisper\Release\ggml-base.dll
47
+ C:\whisper\Release\ggml-cpu.dll
48
+ C:\whisper\Release\whisper.dll
49
+ ```
50
+
51
+ GPU acceleration is automatic — no additional configuration needed.
52
+
53
+ ### Option B — Build from source
54
+
55
+ Requires: Git, CMake, Visual Studio Build Tools 2022+ with "Desktop development with C++", Vulkan SDK from [lunarg.com](https://vulkan.lunarg.com/sdk/home#windows).
56
+
57
+ ```
58
+ git clone https://github.com/ggml-org/whisper.cpp
59
+ cd whisper.cpp
60
+ cmake -B build -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release
61
+ cmake --build build --config Release --target whisper-cli
62
+ ```
63
+
64
+ Copy the binaries from `build\bin\Release\` to `C:\whisper\Release\`.
65
+
66
+ > **Note:** The official whisper.cpp Windows releases on GitHub do not include a Vulkan build. You must use the pre-built release above or compile from source with `-DGGML_VULKAN=ON`.
67
+
68
+ ---
69
+
70
+ ## Step 2 — Download a Whisper model
71
+
72
+ | Model | Size | Speed | Accuracy | Best for |
73
+ |---|---|---|---|---|
74
+ | `ggml-tiny.en.bin` | 75 MB | Very fast | Basic | Quick tests |
75
+ | `ggml-base.en.bin` | 142 MB | Fast | Good | Everyday English |
76
+ | `ggml-small.en.bin` | 466 MB | Moderate | Better | Important recordings |
77
+ | `ggml-medium.en.bin` | 1.5 GB | Fast on GPU | Very good | Best quality English |
78
+ | `ggml-large-v3.bin` | 2.9 GB | Fast on GPU | Excellent | Multilingual, best accuracy |
79
+
80
+ For **English-only** use: `base.en` or `medium.en` are the best starting points.
81
+ For **multilingual** use (auto-detect, foreign language, translation): use `large-v3` for best results.
82
+
83
+ Download from Hugging Face:
84
+ ```
85
+ https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.en.bin
86
+ ```
87
+
88
+ Save to `C:\whisper\models\`.
89
+
90
+ ---
91
+
92
+ ## Step 3 — Install FFmpeg
93
+
94
+ FFmpeg is required for video files and non-native audio formats.
95
+
96
+ Install via winget:
97
+ ```
98
+ winget install ffmpeg
99
+ ```
100
+
101
+ Or download from [ffmpeg.org](https://ffmpeg.org/download.html) and add to your PATH.
102
+
103
+ Verify:
104
+ ```
105
+ ffmpeg -version
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Step 4 — Install this MCP server
111
+
112
+ ```
113
+ npm install -g whisper-windows-mcp
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Step 5 — Configure Claude Desktop
119
+
120
+ Open Claude Desktop → Settings → Developer → Edit Config.
121
+
122
+ Add the `whisper` entry:
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "whisper": {
128
+ "command": "npx",
129
+ "args": ["-y", "whisper-windows-mcp"],
130
+ "env": {
131
+ "WHISPER_CLI_PATH": "C:\\whisper\\Release\\whisper-cli.exe",
132
+ "WHISPER_MODEL": "C:\\whisper\\models\\ggml-medium.en.bin"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Config file location: `C:\Users\YourName\AppData\Roaming\Claude\claude_desktop_config.json`
140
+
141
+ > Use **double backslashes** in all paths.
142
+
143
+ Save and **fully restart** Claude Desktop. You should see **whisper** listed with a green running badge in Settings → Developer.
144
+
145
+ ---
146
+
147
+ ## Step 6 — Verify your setup
148
+
149
+ In Claude Desktop, ask:
150
+
151
+ > *"Check your whisper config"*
152
+
153
+ Then:
154
+
155
+ > *"Check your system hardware"*
156
+
157
+ This confirms your GPU is detected and Vulkan acceleration is active.
158
+
159
+ ---
160
+
161
+ ## Available tools
162
+
163
+ ### `transcribe_audio`
164
+ Transcribe a single file. Supports blocking (default) or background mode for long files.
165
+
166
+ | Parameter | Description |
167
+ |---|---|
168
+ | `file_path` | Absolute path to the file (required) |
169
+ | `language` | Language code (`en`, `ja`, `es`, etc.) or `auto` to detect. Default: `en` |
170
+ | `output_format` | `text` (default), `timestamps`, `json`, or `srt` |
171
+ | `save_to_file` | Save transcript as .txt next to the source file |
172
+ | `background` | Run as detached job — returns a job ID immediately. Use `check_progress` to monitor. Recommended for files over 10 minutes. |
173
+ | `threads` | CPU thread override |
174
+
175
+ ---
176
+
177
+ ### `check_progress`
178
+ Monitor a background transcription job started with `transcribe_audio` (background=true).
179
+
180
+ Returns elapsed time, last processed timestamp, percentage, and the full transcript when complete.
181
+
182
+ | Parameter | Description |
183
+ |---|---|
184
+ | `job_id` | Job ID returned by `transcribe_audio` |
185
+
186
+ ---
187
+
188
+ ### `start_batch`
189
+ Automated sequential batch transcription of all untranscribed files in a folder. Sorts by duration (shortest first), processes one at a time as background jobs, validates each output.
190
+
191
+ | Parameter | Description |
192
+ |---|---|
193
+ | `folder_path` | Path to folder (required) |
194
+ | `language` | Language code. Default: `en` |
195
+ | `threads` | CPU thread override |
196
+
197
+ ---
198
+
199
+ ### `check_batch_progress`
200
+ Monitor a running batch. Automatically advances to the next file when the current one finishes. Returns overall progress, current file with timestamp, ETA, and any failed files.
201
+
202
+ | Parameter | Description |
203
+ |---|---|
204
+ | `batch_id` | Batch ID returned by `start_batch` |
205
+
206
+ ---
207
+
208
+ ### `transcribe_batch` (interactive)
209
+ Process files one at a time with a preview and confirmation before each. Useful when you want to review as you go.
210
+
211
+ | Parameter | Description |
212
+ |---|---|
213
+ | `folder_path` | Path to folder (required) |
214
+ | `file_index` | Which file to process (1-based). Omit to list files first. |
215
+ | `language` | Language code. Default: `en` |
216
+ | `recursive` | Include subfolders |
217
+
218
+ ---
219
+
220
+ ### `generate_subtitles`
221
+ Generate SRT subtitle files. Supports automatic language detection and English translation output.
222
+
223
+ | Parameter | Description |
224
+ |---|---|
225
+ | `file_path` | Path to file (required) |
226
+ | `language` | Language code or `auto` to detect. Default: `en` |
227
+ | `translate_to_english` | Also generate an English translation `.en.srt`. Only applies when source is not English. |
228
+ | `threads` | CPU thread override |
229
+
230
+ When both native and translation are requested, two files are saved next to the source:
231
+ - `filename.ja.srt` — original language
232
+ - `filename.en.srt` — English translation
233
+
234
+ > Whisper's built-in translation only translates **to English**. For other target languages, translate the .srt file contents separately.
235
+
236
+ ---
237
+
238
+ ### `analyze_media`
239
+ Analyze files before committing to transcription. Returns duration, size, codec, and estimated transcription time on CPU and GPU. For folders, shows all files in a sortable table with transcription status.
240
+
241
+ | Parameter | Description |
242
+ |---|---|
243
+ | `path` | Path to a single file or folder (required) |
244
+ | `sort_by` | For folders: `duration` (default), `name`, or `size` |
245
+
246
+ ---
247
+
248
+ ### `check_config`
249
+ Verify whisper-cli.exe, the model file, and FFmpeg are all accessible. Run this first if anything is failing.
250
+
251
+ ---
252
+
253
+ ### `check_system`
254
+ Detect GPU hardware and verify Vulkan acceleration is available. Reports GPU name, VRAM, whether `ggml-vulkan.dll` is present, and recommends the best model size for your hardware.
255
+
256
+ ---
257
+
258
+ ## Supported formats
259
+
260
+ | Type | Formats |
261
+ |---|---|
262
+ | Native (no conversion) | `mp3`, `wav` |
263
+ | Video (auto-converted via FFmpeg) | `mp4`, `mkv`, `avi`, `mov`, `webm`, `flv`, `wmv`, `m4v`, `ts`, `3gp` |
264
+ | Audio (auto-converted via FFmpeg) | `m4a`, `ogg`, `flac` |
265
+
266
+ ---
267
+
268
+ ## GPU acceleration
269
+
270
+ The pre-built Vulkan release enables GPU acceleration automatically. Tested on AMD Radeon RX Vega 56 (GCN 5th gen). Any GPU with Vulkan 1.0+ support should work, including NVIDIA and Intel Arc.
271
+
272
+ **Performance comparison (medium.en model, ~5 minute audio file):**
273
+
274
+ | Hardware | Time |
275
+ |---|---|
276
+ | CPU only (Ryzen 7 2700x, 8 threads) | 8–12 minutes |
277
+ | GPU (Vega 56 via Vulkan) | 20–40 seconds |
278
+
279
+ GPU utilization during transcription is typically 15–20%, dropping back to idle between files. CPU stays around 15%.
280
+
281
+ ---
282
+
283
+ ## Multilingual support
284
+
285
+ Whisper can auto-detect the spoken language and transcribe in that language. The built-in translation model translates **to English only**.
286
+
287
+ For best multilingual accuracy, use the `large-v3` model. English-specific models (`*.en.bin`) cannot detect or transcribe other languages.
288
+
289
+ **Example — foreign language video with subtitles:**
290
+ 1. Ask Claude to generate subtitles with `language=auto` and `translate_to_english=true`
291
+ 2. Whisper detects the language and generates a native-language SRT
292
+ 3. A second pass generates an English translation SRT
293
+ 4. Load either file in VLC via Subtitle → Add Subtitle File
294
+
295
+ ---
296
+
297
+ ## Designed for free-tier users
298
+
299
+ This tool is built to minimize Claude API interactions. The entire transcription workflow — scan, analyze, queue, run, validate — is designed to require as few Claude interactions as possible. Heavy lifting is done locally on your machine.
300
+
301
+ ---
302
+
303
+ ## Optional environment variables
304
+
305
+ | Variable | Description |
306
+ |---|---|
307
+ | `WHISPER_CLI_PATH` | Path to whisper-cli.exe (required) |
308
+ | `WHISPER_MODEL` | Path to model .bin file (required) |
309
+ | `WHISPER_THREADS` | CPU thread count override |
310
+ | `FFMPEG_PATH` | Path to ffmpeg if not in system PATH |
311
+
312
+ ---
313
+
314
+ ## Troubleshooting
315
+
316
+ See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed solutions.
317
+
318
+ Quick checklist:
319
+ - Paths in config use **double backslashes** (`C:\\whisper\\...`)
320
+ - `whisper-cli.exe` exists at the configured path
321
+ - Model `.bin` file exists at the configured path
322
+ - FFmpeg is installed and in PATH (`ffmpeg -version` works)
323
+ - Claude Desktop was fully restarted after editing config
324
+ - Whisper shows **running** in Settings → Developer
325
+
326
+ ---
327
+
328
+ ## License
329
+
330
+ MIT
331
+
332
+ ---
333
+
334
+ ## Contributing
335
+
336
+ Pull requests welcome. See [ROADMAP.md](ROADMAP.md) for planned features.
337
+
338
+ 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.
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ const WHISPER_THREADS = parseInt(process.env.WHISPER_THREADS ?? String(DEFAULT_T
25
25
  const SUPPORTED_EXTENSIONS = [
26
26
  ".mp3", ".wav",
27
27
  ".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v",
28
- ".m4a", ".ogg", ".flac",
28
+ ".m4a", ".ogg", ".flac", ".3gp", ".ts",
29
29
  ];
30
30
  const NATIVE_EXTENSIONS = [".mp3", ".wav"];
31
31
  // ---------------------------------------------------------------------------
@@ -186,6 +186,123 @@ async function readJobProgress(jobId) {
186
186
  `${progressLine}\n\n` +
187
187
  `Call check_progress with this job ID to get an update.`);
188
188
  }
189
+ function validateTranscript(txtPath, durationSec) {
190
+ if (!existsSync(txtPath))
191
+ return { valid: false, reason: "output file missing" };
192
+ const content = readFileSync(txtPath, "utf8").trim();
193
+ if (!content)
194
+ return { valid: false, reason: "output file is empty" };
195
+ const lines = content.split(/\n/).filter(l => l.trim()).length;
196
+ const minExpected = Math.max(1, Math.floor(durationSec / 30));
197
+ if (lines < minExpected) {
198
+ return { valid: false, reason: `only ${lines} line(s) for ${Math.round(durationSec)}s audio (expected ≥${minExpected})` };
199
+ }
200
+ return { valid: true };
201
+ }
202
+ async function spawnNextBatchJob(state) {
203
+ for (let i = state.currentIndex; i < state.files.length; i++) {
204
+ if (state.files[i].status === "pending") {
205
+ state.currentIndex = i;
206
+ state.files[i].status = "running";
207
+ const f = state.files[i];
208
+ const { jobId } = await spawnDetached(f.filePath, state.model, state.language, state.threads);
209
+ state.files[i].jobId = jobId;
210
+ writeFileSync(state.batchPath, JSON.stringify(state, null, 2), "utf8");
211
+ return;
212
+ }
213
+ }
214
+ // Nothing left to run
215
+ state.status = "complete";
216
+ writeFileSync(state.batchPath, JSON.stringify(state, null, 2), "utf8");
217
+ }
218
+ async function readBatchProgress(batchId) {
219
+ const batchPath = join(JOBS_DIR, `${batchId}.batch.json`);
220
+ if (!existsSync(batchPath)) {
221
+ return `❌ Batch not found: ${batchId}\n\nThe batch file may have been deleted or the ID is incorrect.`;
222
+ }
223
+ const state = JSON.parse(readFileSync(batchPath, "utf8"));
224
+ // Check current running job
225
+ const running = state.files.find(f => f.status === "running");
226
+ if (running && running.jobId) {
227
+ const jobPath = join(JOBS_DIR, `${running.jobId}.json`);
228
+ if (existsSync(jobPath)) {
229
+ const job = JSON.parse(readFileSync(jobPath, "utf8"));
230
+ const isRunning = await isPidRunning(job.pid);
231
+ const outputExists = existsSync(job.outputPath);
232
+ if (!isRunning) {
233
+ // Job finished — validate and advance
234
+ const validation = validateTranscript(job.outputPath, running.durationSec);
235
+ if (outputExists && validation.valid) {
236
+ running.status = "complete";
237
+ }
238
+ else {
239
+ running.status = "failed";
240
+ running.failReason = validation.reason ?? "no output file";
241
+ }
242
+ // Advance to next
243
+ state.currentIndex = state.files.indexOf(running) + 1;
244
+ if (state.files.some(f => f.status === "pending")) {
245
+ await spawnNextBatchJob(state);
246
+ }
247
+ else {
248
+ state.status = "complete";
249
+ writeFileSync(batchPath, JSON.stringify(state, null, 2), "utf8");
250
+ }
251
+ }
252
+ else {
253
+ // Still running — update state file without advancing
254
+ writeFileSync(batchPath, JSON.stringify(state, null, 2), "utf8");
255
+ }
256
+ }
257
+ }
258
+ else if (state.status !== "complete" && state.files.some(f => f.status === "pending")) {
259
+ // No running job but pending files exist — advance
260
+ await spawnNextBatchJob(state);
261
+ }
262
+ // Build status report
263
+ const done = state.files.filter(f => f.status === "complete").length;
264
+ const failed = state.files.filter(f => f.status === "failed");
265
+ const pending = state.files.filter(f => f.status === "pending").length;
266
+ const currentRunning = state.files.find(f => f.status === "running");
267
+ const total = state.files.length;
268
+ const elapsed = Math.round((Date.now() - new Date(state.startTime).getTime()) / 1000);
269
+ let report = `Batch: ${batchId}\n`;
270
+ report += `Folder: ${state.folder}\n`;
271
+ report += `${"─".repeat(50)}\n`;
272
+ report += `Progress: ${done}/${total} complete`;
273
+ if (failed.length > 0)
274
+ report += ` | ${failed.length} failed`;
275
+ if (pending > 0)
276
+ report += ` | ${pending} remaining`;
277
+ report += `\nElapsed: ${formatDuration(elapsed)}\n`;
278
+ if (currentRunning) {
279
+ report += `\nCurrently processing: ${currentRunning.fileName}`;
280
+ if (currentRunning.jobId) {
281
+ const jobPath = join(JOBS_DIR, `${currentRunning.jobId}.json`);
282
+ if (existsSync(jobPath)) {
283
+ const job = JSON.parse(readFileSync(jobPath, "utf8"));
284
+ const logContent = existsSync(job.logPath) ? readFileSync(job.logPath, "utf8") : "";
285
+ const lastSec = parseLastTimestamp(logContent);
286
+ if (lastSec > 0)
287
+ report += ` (${formatDuration(lastSec)} / ${formatDuration(currentRunning.durationSec)})`;
288
+ }
289
+ }
290
+ }
291
+ if (failed.length > 0) {
292
+ report += `\n\n⚠️ Failed files:\n`;
293
+ for (const f of failed) {
294
+ report += ` ❌ ${f.fileName} — ${f.failReason ?? "unknown reason"}\n`;
295
+ }
296
+ report += `\nRe-run failed files with transcribe_audio individually.`;
297
+ }
298
+ if (state.status === "complete") {
299
+ report = `✅ Batch complete!\n\n` + report;
300
+ }
301
+ else {
302
+ report += `\n\nCall check_batch_progress again to update.`;
303
+ }
304
+ return report;
305
+ }
189
306
  async function detectGpus() {
190
307
  try {
191
308
  const { stdout } = await execFileAsync("wmic", ["path", "win32_VideoController", "get", "name,AdapterRAM", "/format:csv"], { windowsHide: true });
@@ -296,8 +413,11 @@ async function convertToWav(inputPath) {
296
413
  ], { windowsHide: true });
297
414
  return tmpFile;
298
415
  }
299
- function buildArgs(filePath, model, language, outputFormat, threads) {
300
- const args = ["-m", model, "-f", filePath, "-l", language, "-t", String(threads)];
416
+ function buildArgs(filePath, model, language, outputFormat, threads, translate = false) {
417
+ const lang = language === "auto" ? "auto" : language;
418
+ const args = ["-m", model, "-f", filePath, "-l", lang, "-t", String(threads)];
419
+ if (translate)
420
+ args.push("--translate");
301
421
  if (outputFormat === "srt") {
302
422
  args.push("-osrt", "-of", filePath.replace(/\.[^.]+$/, ""));
303
423
  }
@@ -309,6 +429,49 @@ function buildArgs(filePath, model, language, outputFormat, threads) {
309
429
  }
310
430
  return args;
311
431
  }
432
+ /**
433
+ * Detect the language of a file by running a short whisper probe.
434
+ * Runs whisper on the first 30 seconds only (--duration 30000ms).
435
+ * Returns the detected language code (e.g. "ja", "en") or null on failure.
436
+ */
437
+ async function detectLanguage(wavPath, model, threads) {
438
+ try {
439
+ const { stdout, stderr } = await execFileAsync(WHISPER_CLI_PATH, [
440
+ "-m", model, "-f", wavPath,
441
+ "-l", "auto",
442
+ "-t", String(threads),
443
+ "--no-timestamps",
444
+ "--duration", "30000",
445
+ ], { maxBuffer: 10 * 1024 * 1024, windowsHide: true });
446
+ const output = stdout + stderr;
447
+ // whisper outputs: "auto-detected language: ja (p = 0.98)"
448
+ const m = output.match(/auto-detected language:\s*([a-z]{2,3})/i);
449
+ return m ? m[1].toLowerCase() : null;
450
+ }
451
+ catch {
452
+ return null;
453
+ }
454
+ }
455
+ /**
456
+ * Run a single whisper SRT pass and move the output to destSrt.
457
+ * Returns the destSrt path.
458
+ */
459
+ async function runSrtPass(transcribeFrom, destSrt, model, language, threads, translate = false) {
460
+ const args = buildArgs(transcribeFrom, model, language, "srt", threads, translate);
461
+ await execFileAsync(WHISPER_CLI_PATH, args, {
462
+ maxBuffer: 100 * 1024 * 1024,
463
+ windowsHide: true,
464
+ });
465
+ const tmpSrt = transcribeFrom.replace(/\.[^.]+$/, ".srt");
466
+ if (existsSync(tmpSrt)) {
467
+ writeFileSync(destSrt, readFileSync(tmpSrt, "utf8"));
468
+ try {
469
+ unlinkSync(tmpSrt);
470
+ }
471
+ catch { }
472
+ }
473
+ return destSrt;
474
+ }
312
475
  async function transcribeSingle(filePath, model, language, outputFormat, threads, saveToFile = false) {
313
476
  // ---- Priority 2: Process lock ----
314
477
  // Never spawn a second whisper-cli.exe while one is already running.
@@ -373,7 +536,7 @@ function getFiles(dir, recursive) {
373
536
  // ---------------------------------------------------------------------------
374
537
  // MCP Server
375
538
  // ---------------------------------------------------------------------------
376
- const server = new Server({ name: "whisper-windows-mcp", version: "1.7.0" }, { capabilities: { tools: {} } });
539
+ const server = new Server({ name: "whisper-windows-mcp", version: "1.9.0" }, { capabilities: { tools: {} } });
377
540
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
378
541
  tools: [
379
542
  {
@@ -389,7 +552,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
389
552
  properties: {
390
553
  file_path: { type: "string", description: "Absolute Windows path, e.g. C:\\Users\\You\\Downloads\\recording.mp4" },
391
554
  model: { type: "string", description: "Override model path. Leave blank to use WHISPER_MODEL." },
392
- language: { type: "string", description: "Language code, e.g. en, es, fr. Defaults to en.", default: "en" },
555
+ language: { type: "string", description: "Language code (e.g. en, ja, es, fr) or 'auto' to detect automatically. Defaults to en.", default: "en" },
393
556
  output_format: {
394
557
  type: "string", enum: ["text", "timestamps", "json", "srt"],
395
558
  description: "text = plain (default), timestamps = with time codes, json = structured, srt = subtitle file saved next to source.",
@@ -441,14 +604,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
441
604
  },
442
605
  {
443
606
  name: "generate_subtitles",
444
- description: "Generate an SRT subtitle file for an audio or video file. " +
445
- "Saved next to the source file. Load in VLC via Subtitle → Add Subtitle File. " +
446
- "Supports all the same formats as transcribe_audio.",
607
+ description: "Generate subtitle files for an audio or video file using whisper.cpp. " +
608
+ "Set language='auto' to detect the spoken language automatically. " +
609
+ "Set translate_to_english=true to also generate an English translation subtitle file. " +
610
+ "When both are requested, two .srt files are saved: one in the original language (e.g. film.ja.srt) " +
611
+ "and one English translation (film.en.srt). " +
612
+ "Load in VLC via Subtitle → Add Subtitle File. " +
613
+ "Supports all standard formats plus .3gp and .ts.",
447
614
  inputSchema: {
448
615
  type: "object",
449
616
  properties: {
450
617
  file_path: { type: "string", description: "Absolute Windows path to the file." },
451
- language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
618
+ language: {
619
+ type: "string",
620
+ description: "Language code (e.g. ja, es, fr, de) or 'auto' to detect automatically. Defaults to en.",
621
+ default: "en",
622
+ },
623
+ translate_to_english: {
624
+ type: "boolean",
625
+ description: "Also generate an English translation .srt alongside the native language .srt. Only applies when language is not 'en'.",
626
+ default: false,
627
+ },
452
628
  threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
453
629
  },
454
630
  required: ["file_path"],
@@ -459,6 +635,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
459
635
  description: "Verify whisper-cli.exe, model, and FFmpeg are all available. Run this first if anything fails.",
460
636
  inputSchema: { type: "object", properties: {} },
461
637
  },
638
+ {
639
+ name: "start_batch",
640
+ description: "Start an automated sequential batch transcription of all untranscribed files in a folder. " +
641
+ "Scans for files without a matching .txt, sorts by duration (shortest first), " +
642
+ "and processes them one at a time as background jobs. " +
643
+ "Each file is validated after completion — empty or suspiciously short outputs are flagged. " +
644
+ "Returns a batch ID to use with check_batch_progress.",
645
+ inputSchema: {
646
+ type: "object",
647
+ properties: {
648
+ folder_path: { type: "string", description: "Absolute Windows path to the folder." },
649
+ language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
650
+ threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
651
+ },
652
+ required: ["folder_path"],
653
+ },
654
+ },
655
+ {
656
+ name: "check_batch_progress",
657
+ description: "Check the status of a batch started with start_batch. " +
658
+ "Automatically advances to the next file when the current one finishes. " +
659
+ "Returns overall progress, current file, failed files, and elapsed time. " +
660
+ "Call repeatedly until the batch shows as complete.",
661
+ inputSchema: {
662
+ type: "object",
663
+ properties: {
664
+ batch_id: { type: "string", description: "Batch ID returned by start_batch." },
665
+ },
666
+ required: ["batch_id"],
667
+ },
668
+ },
462
669
  {
463
670
  name: "analyze_media",
464
671
  description: "Analyze one or more media files using FFprobe before transcribing. " +
@@ -720,11 +927,94 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
720
927
  }
721
928
  }
722
929
  // -------------------------------------------------------------------------
930
+ // start_batch
931
+ // -------------------------------------------------------------------------
932
+ if (name === "start_batch") {
933
+ const folderPath = args?.folder_path;
934
+ const language = args?.language || "en";
935
+ const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round(args?.threads || WHISPER_THREADS)));
936
+ if (!folderPath)
937
+ return { content: [{ type: "text", text: "folder_path is required." }], isError: true };
938
+ if (!existsSync(folderPath))
939
+ return { content: [{ type: "text", text: `Folder not found: ${folderPath}` }], isError: true };
940
+ const configError = validatePaths();
941
+ if (configError)
942
+ return { content: [{ type: "text", text: configError }], isError: true };
943
+ if (await isWhisperRunning()) {
944
+ return { content: [{ type: "text", text: "A transcription is already running. Wait for it to finish before starting a batch." }], isError: true };
945
+ }
946
+ // Scan for untranscribed files
947
+ const allFiles = getFiles(folderPath, false);
948
+ const untranscribed = allFiles.filter(f => !existsSync(f.replace(/\.[^.]+$/, ".txt")));
949
+ if (untranscribed.length === 0) {
950
+ return { content: [{ type: "text", text: `✅ All files in ${folderPath} are already transcribed. Nothing to do.` }] };
951
+ }
952
+ // Probe durations for sorting
953
+ const batchFiles = [];
954
+ for (const f of untranscribed) {
955
+ const info = await probeFile(f);
956
+ batchFiles.push({
957
+ filePath: f,
958
+ fileName: basename(f),
959
+ durationSec: info?.durationSec ?? 0,
960
+ status: "pending",
961
+ });
962
+ }
963
+ batchFiles.sort((a, b) => a.durationSec - b.durationSec);
964
+ ensureJobsDir();
965
+ const batchId = `batch_${Date.now()}`;
966
+ const batchPath = join(JOBS_DIR, `${batchId}.batch.json`);
967
+ const state = {
968
+ batchId,
969
+ batchPath,
970
+ folder: folderPath,
971
+ startTime: new Date().toISOString(),
972
+ files: batchFiles,
973
+ currentIndex: 0,
974
+ status: "running",
975
+ model: WHISPER_MODEL,
976
+ language,
977
+ threads,
978
+ };
979
+ writeFileSync(batchPath, JSON.stringify(state, null, 2), "utf8");
980
+ await spawnNextBatchJob(state);
981
+ const totalDuration = batchFiles.reduce((acc, f) => acc + f.durationSec, 0);
982
+ return {
983
+ content: [{
984
+ type: "text",
985
+ text: `⏳ Batch started!\n\n` +
986
+ `Batch ID: ${batchId}\n` +
987
+ `Folder: ${folderPath}\n` +
988
+ `Files to process: ${batchFiles.length}\n` +
989
+ `Total audio: ${formatDuration(totalDuration)}\n` +
990
+ `Est. GPU time: ${estimateTime(totalDuration, hasVulkanDll())}\n\n` +
991
+ `First file: ${batchFiles[0].fileName}\n\n` +
992
+ `Call check_batch_progress with batch_id="${batchId}" to monitor.`,
993
+ }],
994
+ };
995
+ }
996
+ // -------------------------------------------------------------------------
997
+ // check_batch_progress
998
+ // -------------------------------------------------------------------------
999
+ if (name === "check_batch_progress") {
1000
+ const batchId = args?.batch_id;
1001
+ if (!batchId)
1002
+ return { content: [{ type: "text", text: "batch_id is required." }], isError: true };
1003
+ try {
1004
+ const result = await readBatchProgress(batchId);
1005
+ return { content: [{ type: "text", text: result }] };
1006
+ }
1007
+ catch (err) {
1008
+ return { content: [{ type: "text", text: `Error reading batch progress:\n\n${err?.message || String(err)}` }], isError: true };
1009
+ }
1010
+ }
1011
+ // -------------------------------------------------------------------------
723
1012
  // generate_subtitles
724
1013
  // -------------------------------------------------------------------------
725
1014
  if (name === "generate_subtitles") {
726
1015
  const filePath = args?.file_path;
727
1016
  const language = args?.language || "en";
1017
+ const translateToEnglish = args?.translate_to_english || false;
728
1018
  const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round(args?.threads || WHISPER_THREADS)));
729
1019
  if (!filePath)
730
1020
  return { content: [{ type: "text", text: "file_path is required." }], isError: true };
@@ -733,15 +1023,56 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
733
1023
  const configError = validatePaths();
734
1024
  if (configError)
735
1025
  return { content: [{ type: "text", text: configError }], isError: true };
1026
+ if (await isWhisperRunning()) {
1027
+ return { content: [{ type: "text", text: "Transcription already in progress. Wait for it to finish first." }], isError: true };
1028
+ }
736
1029
  try {
737
- const result = await transcribeSingle(filePath, WHISPER_MODEL, language, "srt", threads, false);
1030
+ // Convert to WAV if needed
1031
+ let transcribeFrom = filePath;
1032
+ let tmpFile = null;
1033
+ if (needsConversion(filePath)) {
1034
+ tmpFile = await convertToWav(filePath);
1035
+ transcribeFrom = tmpFile;
1036
+ }
1037
+ const baseNoExt = filePath.replace(/\.[^.]+$/, "");
1038
+ // Auto-detect language if requested
1039
+ let detectedLang = language;
1040
+ if (language === "auto") {
1041
+ const detected = await detectLanguage(transcribeFrom, WHISPER_MODEL, threads);
1042
+ detectedLang = detected ?? "en";
1043
+ }
1044
+ const results = [];
1045
+ // Pass 1 — native language SRT
1046
+ const nativeSrt = language === "en" || detectedLang === "en"
1047
+ ? `${baseNoExt}.srt`
1048
+ : `${baseNoExt}.${detectedLang}.srt`;
1049
+ await runSrtPass(transcribeFrom, nativeSrt, WHISPER_MODEL, detectedLang, threads, false);
1050
+ results.push(`✅ Native (${detectedLang}): ${nativeSrt}`);
1051
+ // Pass 2 — English translation SRT (only if language isn't already English)
1052
+ if (translateToEnglish && detectedLang !== "en") {
1053
+ const englishSrt = `${baseNoExt}.en.srt`;
1054
+ await runSrtPass(transcribeFrom, englishSrt, WHISPER_MODEL, detectedLang, threads, true);
1055
+ results.push(`✅ English translation: ${englishSrt}`);
1056
+ }
1057
+ // Clean up temp WAV
1058
+ if (tmpFile && existsSync(tmpFile))
1059
+ try {
1060
+ unlinkSync(tmpFile);
1061
+ }
1062
+ catch { }
1063
+ const langNote = language === "auto"
1064
+ ? `Auto-detected language: ${detectedLang}\n\n`
1065
+ : "";
738
1066
  return {
739
1067
  content: [{
740
1068
  type: "text",
741
- text: `✅ Subtitle file generated!\n\n` +
742
- `Saved to: ${result.srtPath}\n\n` +
1069
+ text: `✅ Subtitle file(s) generated!\n\n` +
1070
+ langNote +
1071
+ results.join("\n") + "\n\n" +
743
1072
  `To use in VLC: Subtitle → Add Subtitle File → select the .srt file.\n` +
744
- `Works in any video player that supports external subtitles.`,
1073
+ `Works in any video player that supports external subtitles.\n\n` +
1074
+ `Note: whisper's built-in translation only translates to English. ` +
1075
+ `For other target languages, translate the .srt file contents separately.`,
745
1076
  }],
746
1077
  };
747
1078
  }
@@ -834,7 +1165,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
834
1165
  async function main() {
835
1166
  const transport = new StdioServerTransport();
836
1167
  await server.connect(transport);
837
- console.error(`whisper-windows-mcp v1.7.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
1168
+ console.error(`whisper-windows-mcp v1.9.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
838
1169
  }
839
1170
  main().catch((err) => {
840
1171
  console.error("Fatal error:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whisper-windows-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.9.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": {