whisper-windows-mcp 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +364 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import { execFile } from "child_process";
|
|
11
|
-
import { existsSync, unlinkSync, readdirSync, writeFileSync, readFileSync } from "fs";
|
|
10
|
+
import { execFile, spawn } from "child_process";
|
|
11
|
+
import { existsSync, unlinkSync, readdirSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, statSync, } from "fs";
|
|
12
12
|
import { cpus, tmpdir } from "os";
|
|
13
13
|
import { join, extname, basename, dirname } from "path";
|
|
14
14
|
import { promisify } from "util";
|
|
@@ -53,6 +53,139 @@ async function isWhisperRunning() {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Background job architecture (Priorities 4 + 5)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
const JOBS_DIR = join(tmpdir(), "whisper-mcp-jobs");
|
|
60
|
+
function ensureJobsDir() {
|
|
61
|
+
mkdirSync(JOBS_DIR, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
async function spawnDetached(filePath, model, language, threads) {
|
|
64
|
+
ensureJobsDir();
|
|
65
|
+
const jobId = `job_${Date.now()}`;
|
|
66
|
+
const logPath = join(JOBS_DIR, `${jobId}.log`);
|
|
67
|
+
const jobPath = join(JOBS_DIR, `${jobId}.json`);
|
|
68
|
+
// Convert to WAV first if needed (fast, blocking — keeps things simple)
|
|
69
|
+
let transcribeFrom = filePath;
|
|
70
|
+
let isTmp = false;
|
|
71
|
+
if (needsConversion(filePath)) {
|
|
72
|
+
transcribeFrom = await convertToWav(filePath);
|
|
73
|
+
isTmp = true;
|
|
74
|
+
}
|
|
75
|
+
// Build args — use -otxt so whisper writes outputfile.txt itself
|
|
76
|
+
const outputBase = filePath.replace(/\.[^.]+$/, "");
|
|
77
|
+
const outputPath = outputBase + ".txt";
|
|
78
|
+
const args = [
|
|
79
|
+
"-m", model,
|
|
80
|
+
"-f", transcribeFrom,
|
|
81
|
+
"-l", language,
|
|
82
|
+
"-t", String(threads),
|
|
83
|
+
"-otxt",
|
|
84
|
+
"-of", outputBase,
|
|
85
|
+
];
|
|
86
|
+
// Spawn detached, redirect both stdout+stderr to log file
|
|
87
|
+
const logFd = openSync(logPath, "w");
|
|
88
|
+
const child = spawn(WHISPER_CLI_PATH, args, {
|
|
89
|
+
detached: true,
|
|
90
|
+
stdio: ["ignore", logFd, logFd],
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
});
|
|
93
|
+
closeSync(logFd);
|
|
94
|
+
child.unref();
|
|
95
|
+
const pid = child.pid ?? 0;
|
|
96
|
+
const job = {
|
|
97
|
+
jobId,
|
|
98
|
+
pid,
|
|
99
|
+
sourceFile: filePath,
|
|
100
|
+
transcribeFrom,
|
|
101
|
+
isTmp,
|
|
102
|
+
outputPath,
|
|
103
|
+
logPath,
|
|
104
|
+
jobPath,
|
|
105
|
+
startTime: new Date().toISOString(),
|
|
106
|
+
model,
|
|
107
|
+
language,
|
|
108
|
+
threads,
|
|
109
|
+
durationSec: 0,
|
|
110
|
+
status: "running",
|
|
111
|
+
};
|
|
112
|
+
writeFileSync(jobPath, JSON.stringify(job, null, 2), "utf8");
|
|
113
|
+
return { jobId, pid };
|
|
114
|
+
}
|
|
115
|
+
async function isPidRunning(pid) {
|
|
116
|
+
try {
|
|
117
|
+
const { stdout } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/NH"], { windowsHide: true });
|
|
118
|
+
return stdout.toLowerCase().includes("whisper-cli.exe");
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function parseLastTimestamp(logContent) {
|
|
125
|
+
// whisper outputs: [00:01:30.000 --> 00:01:35.000] text
|
|
126
|
+
const re = /\[(\d{2}):(\d{2}):(\d{2})\.\d{3} -->/g;
|
|
127
|
+
let lastSec = 0;
|
|
128
|
+
let m;
|
|
129
|
+
while ((m = re.exec(logContent)) !== null) {
|
|
130
|
+
const sec = parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10);
|
|
131
|
+
if (sec > lastSec)
|
|
132
|
+
lastSec = sec;
|
|
133
|
+
}
|
|
134
|
+
return lastSec;
|
|
135
|
+
}
|
|
136
|
+
async function readJobProgress(jobId) {
|
|
137
|
+
const jobPath = join(JOBS_DIR, `${jobId}.json`);
|
|
138
|
+
if (!existsSync(jobPath)) {
|
|
139
|
+
return `❌ Job not found: ${jobId}\n\nThe job file may have been deleted or the ID is incorrect.`;
|
|
140
|
+
}
|
|
141
|
+
const job = JSON.parse(readFileSync(jobPath, "utf8"));
|
|
142
|
+
// Read log
|
|
143
|
+
let logContent = "";
|
|
144
|
+
if (existsSync(job.logPath)) {
|
|
145
|
+
logContent = readFileSync(job.logPath, "utf8");
|
|
146
|
+
}
|
|
147
|
+
const lastSec = parseLastTimestamp(logContent);
|
|
148
|
+
const isRunning = await isPidRunning(job.pid);
|
|
149
|
+
const outputExists = existsSync(job.outputPath);
|
|
150
|
+
// Completed
|
|
151
|
+
if (!isRunning && outputExists) {
|
|
152
|
+
job.status = "complete";
|
|
153
|
+
writeFileSync(job.jobPath, JSON.stringify(job, null, 2), "utf8");
|
|
154
|
+
// Clean up tmp wav if present
|
|
155
|
+
if (job.isTmp && existsSync(job.transcribeFrom)) {
|
|
156
|
+
try {
|
|
157
|
+
unlinkSync(job.transcribeFrom);
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
const transcript = readFileSync(job.outputPath, "utf8").trim();
|
|
162
|
+
return (`✅ Complete!\n\n` +
|
|
163
|
+
`Source: ${basename(job.sourceFile)}\n` +
|
|
164
|
+
`Output: ${job.outputPath}\n\n` +
|
|
165
|
+
`Preview:\n${transcript.slice(0, 600)}${transcript.length > 600 ? "..." : ""}`);
|
|
166
|
+
}
|
|
167
|
+
// Failed
|
|
168
|
+
if (!isRunning && !outputExists) {
|
|
169
|
+
job.status = "failed";
|
|
170
|
+
writeFileSync(job.jobPath, JSON.stringify(job, null, 2), "utf8");
|
|
171
|
+
const lastLines = logContent.split(/\r?\n/).filter(l => l.trim()).slice(-5).join("\n");
|
|
172
|
+
return (`❌ Failed or cancelled.\n\n` +
|
|
173
|
+
`Source: ${basename(job.sourceFile)}\n` +
|
|
174
|
+
`No output found at: ${job.outputPath}\n\n` +
|
|
175
|
+
`Last log output:\n${lastLines || "(empty)"}`);
|
|
176
|
+
}
|
|
177
|
+
// Still running
|
|
178
|
+
const elapsed = Math.round((Date.now() - new Date(job.startTime).getTime()) / 1000);
|
|
179
|
+
const progressLine = lastSec > 0
|
|
180
|
+
? `Last segment: ${formatDuration(lastSec)}`
|
|
181
|
+
: "Starting up...";
|
|
182
|
+
return (`⏳ In progress...\n\n` +
|
|
183
|
+
`Source: ${basename(job.sourceFile)}\n` +
|
|
184
|
+
`Job ID: ${jobId}\n` +
|
|
185
|
+
`Elapsed: ${formatDuration(elapsed)}\n` +
|
|
186
|
+
`${progressLine}\n\n` +
|
|
187
|
+
`Call check_progress with this job ID to get an update.`);
|
|
188
|
+
}
|
|
56
189
|
async function detectGpus() {
|
|
57
190
|
try {
|
|
58
191
|
const { stdout } = await execFileAsync("wmic", ["path", "win32_VideoController", "get", "name,AdapterRAM", "/format:csv"], { windowsHide: true });
|
|
@@ -95,6 +228,60 @@ function hasVulkanDll() {
|
|
|
95
228
|
const whisperDir = dirname(WHISPER_CLI_PATH);
|
|
96
229
|
return existsSync(join(whisperDir, "ggml-vulkan.dll"));
|
|
97
230
|
}
|
|
231
|
+
async function probeFile(filePath) {
|
|
232
|
+
try {
|
|
233
|
+
const { stdout } = await execFileAsync(FFMPEG_PATH.replace(/ffmpeg(\.exe)?$/i, "ffprobe$1").replace(/ffmpeg$/i, "ffprobe"), [
|
|
234
|
+
"-v", "quiet",
|
|
235
|
+
"-print_format", "json",
|
|
236
|
+
"-show_format",
|
|
237
|
+
"-show_streams",
|
|
238
|
+
filePath,
|
|
239
|
+
], { windowsHide: true, maxBuffer: 10 * 1024 * 1024 });
|
|
240
|
+
const data = JSON.parse(stdout);
|
|
241
|
+
const fmt = data.format ?? {};
|
|
242
|
+
const streams = data.streams ?? [];
|
|
243
|
+
const audioStream = streams.find((s) => s.codec_type === "audio");
|
|
244
|
+
const durationSec = parseFloat(fmt.duration ?? "0") || 0;
|
|
245
|
+
const sizeMb = parseInt(fmt.size ?? "0", 10) / (1024 * 1024);
|
|
246
|
+
const bitrate = Math.round(parseInt(fmt.bit_rate ?? "0", 10) / 1000);
|
|
247
|
+
const codec = audioStream?.codec_name ?? fmt.format_name?.split(",")[0] ?? "unknown";
|
|
248
|
+
return {
|
|
249
|
+
filePath,
|
|
250
|
+
fileName: basename(filePath),
|
|
251
|
+
durationSec,
|
|
252
|
+
sizeMb,
|
|
253
|
+
codec,
|
|
254
|
+
bitrate,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function formatDuration(sec) {
|
|
262
|
+
if (!sec)
|
|
263
|
+
return "?:??";
|
|
264
|
+
const h = Math.floor(sec / 3600);
|
|
265
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
266
|
+
const s = Math.floor(sec % 60);
|
|
267
|
+
if (h > 0)
|
|
268
|
+
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
269
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
270
|
+
}
|
|
271
|
+
function estimateTime(durationSec, gpu) {
|
|
272
|
+
if (!durationSec)
|
|
273
|
+
return "?";
|
|
274
|
+
// CPU: ~1.5x realtime on Ryzen 7 2700x with medium.en
|
|
275
|
+
// GPU: ~0.12x realtime on Vega 56 via Vulkan with medium.en (~8x faster than CPU)
|
|
276
|
+
const ratio = gpu ? 0.12 : 1.5;
|
|
277
|
+
const estSec = Math.round(durationSec * ratio);
|
|
278
|
+
if (estSec < 60)
|
|
279
|
+
return `~${estSec}s`;
|
|
280
|
+
return `~${Math.round(estSec / 60)}m`;
|
|
281
|
+
}
|
|
282
|
+
function padEnd(str, len) {
|
|
283
|
+
return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
|
|
284
|
+
}
|
|
98
285
|
function needsConversion(filePath) {
|
|
99
286
|
return !NATIVE_EXTENSIONS.includes(extname(filePath).toLowerCase());
|
|
100
287
|
}
|
|
@@ -186,7 +373,7 @@ function getFiles(dir, recursive) {
|
|
|
186
373
|
// ---------------------------------------------------------------------------
|
|
187
374
|
// MCP Server
|
|
188
375
|
// ---------------------------------------------------------------------------
|
|
189
|
-
const server = new Server({ name: "whisper-windows-mcp", version: "1.
|
|
376
|
+
const server = new Server({ name: "whisper-windows-mcp", version: "1.7.0" }, { capabilities: { tools: {} } });
|
|
190
377
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
191
378
|
tools: [
|
|
192
379
|
{
|
|
@@ -195,8 +382,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
195
382
|
"Natively supports mp3 and wav. Automatically converts mp4, mkv, avi, mov, " +
|
|
196
383
|
"webm, m4a, flac, ogg etc. via FFmpeg — no manual conversion needed. " +
|
|
197
384
|
"Can output plain text, timestamps, JSON, or SRT subtitle files. " +
|
|
198
|
-
"
|
|
199
|
-
"use
|
|
385
|
+
"For files that may take more than 4 minutes, set background=true to run as a detached job " +
|
|
386
|
+
"and use check_progress to monitor it.",
|
|
200
387
|
inputSchema: {
|
|
201
388
|
type: "object",
|
|
202
389
|
properties: {
|
|
@@ -210,10 +397,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
210
397
|
},
|
|
211
398
|
threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
|
|
212
399
|
save_to_file: { type: "boolean", description: "Save transcript as .txt next to the source file.", default: false },
|
|
400
|
+
background: { type: "boolean", description: "Run as a detached background job. Returns a job ID immediately. Use check_progress to monitor. Recommended for files over 10 minutes.", default: false },
|
|
213
401
|
},
|
|
214
402
|
required: ["file_path"],
|
|
215
403
|
},
|
|
216
404
|
},
|
|
405
|
+
{
|
|
406
|
+
name: "check_progress",
|
|
407
|
+
description: "Check the status of a background transcription job started with transcribe_audio (background=true). " +
|
|
408
|
+
"Returns current progress, elapsed time, last processed timestamp, and the transcript when complete. " +
|
|
409
|
+
"Call this repeatedly until the job shows as complete or failed.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
job_id: { type: "string", description: "Job ID returned by transcribe_audio when background=true." },
|
|
414
|
+
},
|
|
415
|
+
required: ["job_id"],
|
|
416
|
+
},
|
|
417
|
+
},
|
|
217
418
|
{
|
|
218
419
|
name: "transcribe_batch",
|
|
219
420
|
description: "Transcribe multiple audio/video files in a folder interactively, one file at a time. " +
|
|
@@ -258,6 +459,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
258
459
|
description: "Verify whisper-cli.exe, model, and FFmpeg are all available. Run this first if anything fails.",
|
|
259
460
|
inputSchema: { type: "object", properties: {} },
|
|
260
461
|
},
|
|
462
|
+
{
|
|
463
|
+
name: "analyze_media",
|
|
464
|
+
description: "Analyze one or more media files using FFprobe before transcribing. " +
|
|
465
|
+
"For a single file: returns duration, size, codec, and estimated transcription time on CPU and GPU. " +
|
|
466
|
+
"For a folder: scans all supported media files and returns a sorted table with the same info for each. " +
|
|
467
|
+
"Use this to plan batch work, estimate how long transcription will take, or check what's already been transcribed.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
path: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "Absolute Windows path to a single file or a folder.",
|
|
474
|
+
},
|
|
475
|
+
sort_by: {
|
|
476
|
+
type: "string",
|
|
477
|
+
enum: ["duration", "name", "size"],
|
|
478
|
+
description: "For folder scans: sort order. Defaults to duration (shortest first).",
|
|
479
|
+
default: "duration",
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
required: ["path"],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
261
485
|
{
|
|
262
486
|
name: "check_system",
|
|
263
487
|
description: "Detect GPU hardware and verify Vulkan acceleration is available. " +
|
|
@@ -297,6 +521,97 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
297
521
|
};
|
|
298
522
|
}
|
|
299
523
|
// -------------------------------------------------------------------------
|
|
524
|
+
// analyze_media
|
|
525
|
+
// -------------------------------------------------------------------------
|
|
526
|
+
if (name === "analyze_media") {
|
|
527
|
+
const targetPath = args?.path;
|
|
528
|
+
const sortBy = args?.sort_by || "duration";
|
|
529
|
+
if (!targetPath)
|
|
530
|
+
return { content: [{ type: "text", text: "path is required." }], isError: true };
|
|
531
|
+
if (!existsSync(targetPath))
|
|
532
|
+
return { content: [{ type: "text", text: `Path not found: ${targetPath}` }], isError: true };
|
|
533
|
+
// Check ffprobe is available
|
|
534
|
+
const ffprobePath = FFMPEG_PATH.replace(/ffmpeg(\.exe)?$/i, "ffprobe$1").replace(/ffmpeg$/i, "ffprobe");
|
|
535
|
+
try {
|
|
536
|
+
await execFileAsync(ffprobePath, ["-version"], { windowsHide: true });
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: "text", text: "ffprobe not found. FFprobe is bundled with FFmpeg — make sure FFmpeg is installed and in your PATH." }],
|
|
541
|
+
isError: true,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const vulkan = hasVulkanDll();
|
|
545
|
+
// Single file
|
|
546
|
+
const stat = statSync(targetPath);
|
|
547
|
+
if (stat.isFile()) {
|
|
548
|
+
const info = await probeFile(targetPath);
|
|
549
|
+
if (!info) {
|
|
550
|
+
return { content: [{ type: "text", text: `Could not probe file: ${targetPath}\nMake sure it is a supported media file.` }], isError: true };
|
|
551
|
+
}
|
|
552
|
+
const txtPath = targetPath.replace(/\.[^.]+$/, ".txt");
|
|
553
|
+
const transcribed = existsSync(txtPath) ? "✅ already transcribed" : "⬜ not yet transcribed";
|
|
554
|
+
return {
|
|
555
|
+
content: [{
|
|
556
|
+
type: "text",
|
|
557
|
+
text: `📄 ${info.fileName}\n` +
|
|
558
|
+
`${"─".repeat(40)}\n` +
|
|
559
|
+
`Duration: ${formatDuration(info.durationSec)}\n` +
|
|
560
|
+
`Size: ${info.sizeMb.toFixed(1)} MB\n` +
|
|
561
|
+
`Codec: ${info.codec}\n` +
|
|
562
|
+
`Bitrate: ${info.bitrate} kbps\n` +
|
|
563
|
+
`Status: ${transcribed}\n\n` +
|
|
564
|
+
`Estimated transcription time:\n` +
|
|
565
|
+
` CPU: ${estimateTime(info.durationSec, false)}\n` +
|
|
566
|
+
` GPU: ${estimateTime(info.durationSec, true)}${vulkan ? "" : " (⚠️ Vulkan not detected — GPU estimate may not apply)"}`,
|
|
567
|
+
}],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
// Folder scan
|
|
571
|
+
const files = getFiles(targetPath, false);
|
|
572
|
+
if (files.length === 0) {
|
|
573
|
+
return { content: [{ type: "text", text: `No supported media files found in: ${targetPath}` }], isError: true };
|
|
574
|
+
}
|
|
575
|
+
const results = [];
|
|
576
|
+
for (const f of files) {
|
|
577
|
+
const info = await probeFile(f);
|
|
578
|
+
if (info)
|
|
579
|
+
results.push(info);
|
|
580
|
+
}
|
|
581
|
+
// Sort
|
|
582
|
+
if (sortBy === "name")
|
|
583
|
+
results.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
584
|
+
else if (sortBy === "size")
|
|
585
|
+
results.sort((a, b) => a.sizeMb - b.sizeMb);
|
|
586
|
+
else
|
|
587
|
+
results.sort((a, b) => a.durationSec - b.durationSec); // duration default
|
|
588
|
+
const totalDuration = results.reduce((acc, r) => acc + r.durationSec, 0);
|
|
589
|
+
const totalSize = results.reduce((acc, r) => acc + r.sizeMb, 0);
|
|
590
|
+
const transcribedCount = results.filter(r => existsSync(r.filePath.replace(/\.[^.]+$/, ".txt"))).length;
|
|
591
|
+
const header = `${padEnd("File", 36)} ${padEnd("Duration", 8)} ${padEnd("Size", 8)} ${padEnd("CPU est", 8)} ${padEnd("GPU est", 8)} Status\n` +
|
|
592
|
+
`${"─".repeat(90)}\n`;
|
|
593
|
+
const rows = results.map(r => {
|
|
594
|
+
const done = existsSync(r.filePath.replace(/\.[^.]+$/, ".txt")) ? "✅" : "⬜";
|
|
595
|
+
return (`${padEnd(r.fileName, 36)} ` +
|
|
596
|
+
`${padEnd(formatDuration(r.durationSec), 8)} ` +
|
|
597
|
+
`${padEnd(r.sizeMb.toFixed(1) + " MB", 8)} ` +
|
|
598
|
+
`${padEnd(estimateTime(r.durationSec, false), 8)} ` +
|
|
599
|
+
`${padEnd(estimateTime(r.durationSec, true), 8)} ` +
|
|
600
|
+
done);
|
|
601
|
+
}).join("\n");
|
|
602
|
+
const summary = `\n${"─".repeat(90)}\n` +
|
|
603
|
+
`${results.length} files | Total duration: ${formatDuration(totalDuration)} | Total size: ${totalSize.toFixed(1)} MB\n` +
|
|
604
|
+
`Transcribed: ${transcribedCount}/${results.length}\n` +
|
|
605
|
+
`Total est. CPU time: ${estimateTime(totalDuration, false)} | Total est. GPU time: ${estimateTime(totalDuration, true)}` +
|
|
606
|
+
(vulkan ? "" : "\n⚠️ ggml-vulkan.dll not detected — GPU estimates may not apply");
|
|
607
|
+
return {
|
|
608
|
+
content: [{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: `Media analysis: ${targetPath}\n\n${header}${rows}${summary}`,
|
|
611
|
+
}],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// -------------------------------------------------------------------------
|
|
300
615
|
// check_system
|
|
301
616
|
// -------------------------------------------------------------------------
|
|
302
617
|
if (name === "check_system") {
|
|
@@ -341,6 +656,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
341
656
|
const outputFormat = (args?.output_format || "text");
|
|
342
657
|
const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round(args?.threads || WHISPER_THREADS)));
|
|
343
658
|
const saveToFile = args?.save_to_file || false;
|
|
659
|
+
const background = args?.background || false;
|
|
344
660
|
if (!filePath)
|
|
345
661
|
return { content: [{ type: "text", text: "file_path is required." }], isError: true };
|
|
346
662
|
if (!existsSync(filePath))
|
|
@@ -348,6 +664,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
348
664
|
const configError = validatePaths();
|
|
349
665
|
if (configError)
|
|
350
666
|
return { content: [{ type: "text", text: configError }], isError: true };
|
|
667
|
+
// Background mode — detached process, returns immediately
|
|
668
|
+
if (background) {
|
|
669
|
+
if (await isWhisperRunning()) {
|
|
670
|
+
return {
|
|
671
|
+
content: [{ type: "text", text: "Transcription already in progress. Wait for the current job to finish before starting another." }],
|
|
672
|
+
isError: true,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
const { jobId, pid } = await spawnDetached(filePath, model, language, threads);
|
|
677
|
+
return {
|
|
678
|
+
content: [{
|
|
679
|
+
type: "text",
|
|
680
|
+
text: `⏳ Background transcription started.\n\n` +
|
|
681
|
+
`Source: ${basename(filePath)}\n` +
|
|
682
|
+
`Job ID: ${jobId}\n` +
|
|
683
|
+
`PID: ${pid}\n\n` +
|
|
684
|
+
`Call check_progress with job_id="${jobId}" to monitor progress.\n` +
|
|
685
|
+
`Output will be saved to: ${filePath.replace(/\.[^.]+$/, ".txt")}`,
|
|
686
|
+
}],
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
catch (err) {
|
|
690
|
+
return { content: [{ type: "text", text: `Failed to start background job:\n\n${err?.message || String(err)}` }], isError: true };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// Blocking mode (default)
|
|
351
694
|
try {
|
|
352
695
|
const result = await transcribeSingle(filePath, model, language, outputFormat, threads, saveToFile);
|
|
353
696
|
let response = result.text;
|
|
@@ -362,6 +705,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
362
705
|
}
|
|
363
706
|
}
|
|
364
707
|
// -------------------------------------------------------------------------
|
|
708
|
+
// check_progress
|
|
709
|
+
// -------------------------------------------------------------------------
|
|
710
|
+
if (name === "check_progress") {
|
|
711
|
+
const jobId = args?.job_id;
|
|
712
|
+
if (!jobId)
|
|
713
|
+
return { content: [{ type: "text", text: "job_id is required." }], isError: true };
|
|
714
|
+
try {
|
|
715
|
+
const result = await readJobProgress(jobId);
|
|
716
|
+
return { content: [{ type: "text", text: result }] };
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
return { content: [{ type: "text", text: `Error reading job progress:\n\n${err?.message || String(err)}` }], isError: true };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// -------------------------------------------------------------------------
|
|
365
723
|
// generate_subtitles
|
|
366
724
|
// -------------------------------------------------------------------------
|
|
367
725
|
if (name === "generate_subtitles") {
|
|
@@ -476,7 +834,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
476
834
|
async function main() {
|
|
477
835
|
const transport = new StdioServerTransport();
|
|
478
836
|
await server.connect(transport);
|
|
479
|
-
console.error(`whisper-windows-mcp v1.
|
|
837
|
+
console.error(`whisper-windows-mcp v1.7.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
|
|
480
838
|
}
|
|
481
839
|
main().catch((err) => {
|
|
482
840
|
console.error("Fatal error:", err);
|
package/package.json
CHANGED