whisper-windows-mcp 1.7.0 → 1.8.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 +232 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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 });
|
|
@@ -373,7 +490,7 @@ function getFiles(dir, recursive) {
|
|
|
373
490
|
// ---------------------------------------------------------------------------
|
|
374
491
|
// MCP Server
|
|
375
492
|
// ---------------------------------------------------------------------------
|
|
376
|
-
const server = new Server({ name: "whisper-windows-mcp", version: "1.
|
|
493
|
+
const server = new Server({ name: "whisper-windows-mcp", version: "1.8.0" }, { capabilities: { tools: {} } });
|
|
377
494
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
378
495
|
tools: [
|
|
379
496
|
{
|
|
@@ -459,6 +576,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
459
576
|
description: "Verify whisper-cli.exe, model, and FFmpeg are all available. Run this first if anything fails.",
|
|
460
577
|
inputSchema: { type: "object", properties: {} },
|
|
461
578
|
},
|
|
579
|
+
{
|
|
580
|
+
name: "start_batch",
|
|
581
|
+
description: "Start an automated sequential batch transcription of all untranscribed files in a folder. " +
|
|
582
|
+
"Scans for files without a matching .txt, sorts by duration (shortest first), " +
|
|
583
|
+
"and processes them one at a time as background jobs. " +
|
|
584
|
+
"Each file is validated after completion — empty or suspiciously short outputs are flagged. " +
|
|
585
|
+
"Returns a batch ID to use with check_batch_progress.",
|
|
586
|
+
inputSchema: {
|
|
587
|
+
type: "object",
|
|
588
|
+
properties: {
|
|
589
|
+
folder_path: { type: "string", description: "Absolute Windows path to the folder." },
|
|
590
|
+
language: { type: "string", description: "Language code. Defaults to en.", default: "en" },
|
|
591
|
+
threads: { type: "number", description: `CPU threads. Defaults to ${WHISPER_THREADS} of ${SYSTEM_THREADS}.` },
|
|
592
|
+
},
|
|
593
|
+
required: ["folder_path"],
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: "check_batch_progress",
|
|
598
|
+
description: "Check the status of a batch started with start_batch. " +
|
|
599
|
+
"Automatically advances to the next file when the current one finishes. " +
|
|
600
|
+
"Returns overall progress, current file, failed files, and elapsed time. " +
|
|
601
|
+
"Call repeatedly until the batch shows as complete.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
batch_id: { type: "string", description: "Batch ID returned by start_batch." },
|
|
606
|
+
},
|
|
607
|
+
required: ["batch_id"],
|
|
608
|
+
},
|
|
609
|
+
},
|
|
462
610
|
{
|
|
463
611
|
name: "analyze_media",
|
|
464
612
|
description: "Analyze one or more media files using FFprobe before transcribing. " +
|
|
@@ -720,6 +868,88 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
720
868
|
}
|
|
721
869
|
}
|
|
722
870
|
// -------------------------------------------------------------------------
|
|
871
|
+
// start_batch
|
|
872
|
+
// -------------------------------------------------------------------------
|
|
873
|
+
if (name === "start_batch") {
|
|
874
|
+
const folderPath = args?.folder_path;
|
|
875
|
+
const language = args?.language || "en";
|
|
876
|
+
const threads = Math.min(SYSTEM_THREADS, Math.max(1, Math.round(args?.threads || WHISPER_THREADS)));
|
|
877
|
+
if (!folderPath)
|
|
878
|
+
return { content: [{ type: "text", text: "folder_path is required." }], isError: true };
|
|
879
|
+
if (!existsSync(folderPath))
|
|
880
|
+
return { content: [{ type: "text", text: `Folder not found: ${folderPath}` }], isError: true };
|
|
881
|
+
const configError = validatePaths();
|
|
882
|
+
if (configError)
|
|
883
|
+
return { content: [{ type: "text", text: configError }], isError: true };
|
|
884
|
+
if (await isWhisperRunning()) {
|
|
885
|
+
return { content: [{ type: "text", text: "A transcription is already running. Wait for it to finish before starting a batch." }], isError: true };
|
|
886
|
+
}
|
|
887
|
+
// Scan for untranscribed files
|
|
888
|
+
const allFiles = getFiles(folderPath, false);
|
|
889
|
+
const untranscribed = allFiles.filter(f => !existsSync(f.replace(/\.[^.]+$/, ".txt")));
|
|
890
|
+
if (untranscribed.length === 0) {
|
|
891
|
+
return { content: [{ type: "text", text: `✅ All files in ${folderPath} are already transcribed. Nothing to do.` }] };
|
|
892
|
+
}
|
|
893
|
+
// Probe durations for sorting
|
|
894
|
+
const batchFiles = [];
|
|
895
|
+
for (const f of untranscribed) {
|
|
896
|
+
const info = await probeFile(f);
|
|
897
|
+
batchFiles.push({
|
|
898
|
+
filePath: f,
|
|
899
|
+
fileName: basename(f),
|
|
900
|
+
durationSec: info?.durationSec ?? 0,
|
|
901
|
+
status: "pending",
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
batchFiles.sort((a, b) => a.durationSec - b.durationSec);
|
|
905
|
+
ensureJobsDir();
|
|
906
|
+
const batchId = `batch_${Date.now()}`;
|
|
907
|
+
const batchPath = join(JOBS_DIR, `${batchId}.batch.json`);
|
|
908
|
+
const state = {
|
|
909
|
+
batchId,
|
|
910
|
+
batchPath,
|
|
911
|
+
folder: folderPath,
|
|
912
|
+
startTime: new Date().toISOString(),
|
|
913
|
+
files: batchFiles,
|
|
914
|
+
currentIndex: 0,
|
|
915
|
+
status: "running",
|
|
916
|
+
model: WHISPER_MODEL,
|
|
917
|
+
language,
|
|
918
|
+
threads,
|
|
919
|
+
};
|
|
920
|
+
writeFileSync(batchPath, JSON.stringify(state, null, 2), "utf8");
|
|
921
|
+
await spawnNextBatchJob(state);
|
|
922
|
+
const totalDuration = batchFiles.reduce((acc, f) => acc + f.durationSec, 0);
|
|
923
|
+
return {
|
|
924
|
+
content: [{
|
|
925
|
+
type: "text",
|
|
926
|
+
text: `⏳ Batch started!\n\n` +
|
|
927
|
+
`Batch ID: ${batchId}\n` +
|
|
928
|
+
`Folder: ${folderPath}\n` +
|
|
929
|
+
`Files to process: ${batchFiles.length}\n` +
|
|
930
|
+
`Total audio: ${formatDuration(totalDuration)}\n` +
|
|
931
|
+
`Est. GPU time: ${estimateTime(totalDuration, hasVulkanDll())}\n\n` +
|
|
932
|
+
`First file: ${batchFiles[0].fileName}\n\n` +
|
|
933
|
+
`Call check_batch_progress with batch_id="${batchId}" to monitor.`,
|
|
934
|
+
}],
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
// -------------------------------------------------------------------------
|
|
938
|
+
// check_batch_progress
|
|
939
|
+
// -------------------------------------------------------------------------
|
|
940
|
+
if (name === "check_batch_progress") {
|
|
941
|
+
const batchId = args?.batch_id;
|
|
942
|
+
if (!batchId)
|
|
943
|
+
return { content: [{ type: "text", text: "batch_id is required." }], isError: true };
|
|
944
|
+
try {
|
|
945
|
+
const result = await readBatchProgress(batchId);
|
|
946
|
+
return { content: [{ type: "text", text: result }] };
|
|
947
|
+
}
|
|
948
|
+
catch (err) {
|
|
949
|
+
return { content: [{ type: "text", text: `Error reading batch progress:\n\n${err?.message || String(err)}` }], isError: true };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// -------------------------------------------------------------------------
|
|
723
953
|
// generate_subtitles
|
|
724
954
|
// -------------------------------------------------------------------------
|
|
725
955
|
if (name === "generate_subtitles") {
|
|
@@ -834,7 +1064,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
834
1064
|
async function main() {
|
|
835
1065
|
const transport = new StdioServerTransport();
|
|
836
1066
|
await server.connect(transport);
|
|
837
|
-
console.error(`whisper-windows-mcp v1.
|
|
1067
|
+
console.error(`whisper-windows-mcp v1.8.0 running | threads: ${WHISPER_THREADS}/${SYSTEM_THREADS}`);
|
|
838
1068
|
}
|
|
839
1069
|
main().catch((err) => {
|
|
840
1070
|
console.error("Fatal error:", err);
|
package/package.json
CHANGED