whisper-windows-mcp 1.6.0 → 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 +196 -7
- 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 });
|
|
@@ -240,7 +373,7 @@ function getFiles(dir, recursive) {
|
|
|
240
373
|
// ---------------------------------------------------------------------------
|
|
241
374
|
// MCP Server
|
|
242
375
|
// ---------------------------------------------------------------------------
|
|
243
|
-
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: {} } });
|
|
244
377
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
245
378
|
tools: [
|
|
246
379
|
{
|
|
@@ -249,8 +382,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
249
382
|
"Natively supports mp3 and wav. Automatically converts mp4, mkv, avi, mov, " +
|
|
250
383
|
"webm, m4a, flac, ogg etc. via FFmpeg — no manual conversion needed. " +
|
|
251
384
|
"Can output plain text, timestamps, JSON, or SRT subtitle files. " +
|
|
252
|
-
"
|
|
253
|
-
"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.",
|
|
254
387
|
inputSchema: {
|
|
255
388
|
type: "object",
|
|
256
389
|
properties: {
|
|
@@ -264,10 +397,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
264
397
|
},
|
|
265
398
|
threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
|
|
266
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 },
|
|
267
401
|
},
|
|
268
402
|
required: ["file_path"],
|
|
269
403
|
},
|
|
270
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
|
+
},
|
|
271
418
|
{
|
|
272
419
|
name: "transcribe_batch",
|
|
273
420
|
description: "Transcribe multiple audio/video files in a folder interactively, one file at a time. " +
|
|
@@ -396,7 +543,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
396
543
|
}
|
|
397
544
|
const vulkan = hasVulkanDll();
|
|
398
545
|
// Single file
|
|
399
|
-
const { statSync } = await import("fs");
|
|
400
546
|
const stat = statSync(targetPath);
|
|
401
547
|
if (stat.isFile()) {
|
|
402
548
|
const info = await probeFile(targetPath);
|
|
@@ -510,6 +656,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
510
656
|
const outputFormat = (args?.output_format || "text");
|
|
511
657
|
const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round(args?.threads || WHISPER_THREADS)));
|
|
512
658
|
const saveToFile = args?.save_to_file || false;
|
|
659
|
+
const background = args?.background || false;
|
|
513
660
|
if (!filePath)
|
|
514
661
|
return { content: [{ type: "text", text: "file_path is required." }], isError: true };
|
|
515
662
|
if (!existsSync(filePath))
|
|
@@ -517,6 +664,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
517
664
|
const configError = validatePaths();
|
|
518
665
|
if (configError)
|
|
519
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)
|
|
520
694
|
try {
|
|
521
695
|
const result = await transcribeSingle(filePath, model, language, outputFormat, threads, saveToFile);
|
|
522
696
|
let response = result.text;
|
|
@@ -531,6 +705,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
531
705
|
}
|
|
532
706
|
}
|
|
533
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
|
+
// -------------------------------------------------------------------------
|
|
534
723
|
// generate_subtitles
|
|
535
724
|
// -------------------------------------------------------------------------
|
|
536
725
|
if (name === "generate_subtitles") {
|
|
@@ -645,7 +834,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
645
834
|
async function main() {
|
|
646
835
|
const transport = new StdioServerTransport();
|
|
647
836
|
await server.connect(transport);
|
|
648
|
-
console.error(`whisper-windows-mcp v1.
|
|
837
|
+
console.error(`whisper-windows-mcp v1.7.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
|
|
649
838
|
}
|
|
650
839
|
main().catch((err) => {
|
|
651
840
|
console.error("Fatal error:", err);
|
package/package.json
CHANGED