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.
- package/README.md +338 -229
- package/dist/index.js +345 -14
- 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) —
|
|
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
|
|
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
|
|
16
|
-
- *"
|
|
17
|
-
- *"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Requirements
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
C:\whisper\Release\
|
|
45
|
-
C:\whisper\Release\ggml
|
|
46
|
-
C:\whisper\Release\ggml.dll
|
|
47
|
-
C:\whisper\Release\ggml-
|
|
48
|
-
C:\whisper\Release\
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
##
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
|
203
|
-
|
|
204
|
-
| `
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
445
|
-
"
|
|
446
|
-
"
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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