vidistill 0.5.4 → 0.6.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 +29 -0
- package/dist/index.js +265 -54
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -104,6 +104,35 @@ vidistill rename-speakers ./vidistill-output/my-meeting/ --rename "Steven Kang"
|
|
|
104
104
|
vidistill rename-speakers ./vidistill-output/my-meeting/ --merge "K Iphone" "Kristian"
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
## MCP Server
|
|
108
|
+
|
|
109
|
+
vidistill can run as an MCP server, letting AI coding tools (Claude Code, Cursor, etc.) analyze videos and read output directly.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
vidistill mcp
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
To configure in Claude Code, add to `~/.claude/claude_code_config.json`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"vidistill": {
|
|
121
|
+
"command": "npx",
|
|
122
|
+
"args": ["vidistill", "mcp"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Tools exposed:**
|
|
129
|
+
|
|
130
|
+
- `analyze_video` — run the full pipeline on a URL or file, returns output dir + summary
|
|
131
|
+
- `get_transcript` — read transcript from an existing output dir, with optional time range filtering
|
|
132
|
+
- `get_code` — read extracted code files from an existing output dir
|
|
133
|
+
|
|
134
|
+
Requires `GEMINI_API_KEY` set as environment variable or in `~/.vidistill/config.json`.
|
|
135
|
+
|
|
107
136
|
## How It Works
|
|
108
137
|
|
|
109
138
|
Supported video formats: MP4, MOV, WebM, MKV, AVI, MPEG, FLV, WMV, 3GPP. Supported audio formats: MP3, AAC, WAV, FLAC, OGG, M4A.
|
package/dist/index.js
CHANGED
|
@@ -648,22 +648,22 @@ function normalizeYouTubeUrl(url) {
|
|
|
648
648
|
return `https://www.youtube.com/watch?v=${id}`;
|
|
649
649
|
}
|
|
650
650
|
function fetchYtDlpDuration(url) {
|
|
651
|
-
return new Promise((
|
|
651
|
+
return new Promise((resolve3) => {
|
|
652
652
|
execFile("yt-dlp", ["--dump-json", "--no-download", url], { timeout: 15e3 }, (err, stdout) => {
|
|
653
653
|
if (err) {
|
|
654
|
-
|
|
654
|
+
resolve3(void 0);
|
|
655
655
|
return;
|
|
656
656
|
}
|
|
657
657
|
try {
|
|
658
658
|
const data = JSON.parse(stdout);
|
|
659
659
|
const dur = data["duration"];
|
|
660
660
|
if (typeof dur === "number" && dur > 0) {
|
|
661
|
-
|
|
661
|
+
resolve3(dur);
|
|
662
662
|
} else {
|
|
663
|
-
|
|
663
|
+
resolve3(void 0);
|
|
664
664
|
}
|
|
665
665
|
} catch {
|
|
666
|
-
|
|
666
|
+
resolve3(void 0);
|
|
667
667
|
}
|
|
668
668
|
});
|
|
669
669
|
});
|
|
@@ -906,7 +906,7 @@ var _require = createRequire(import.meta.url);
|
|
|
906
906
|
var ffmpeg = _require("fluent-ffmpeg");
|
|
907
907
|
var BYTES_PER_SECOND = 5e5;
|
|
908
908
|
function ffprobeAsync(filePath) {
|
|
909
|
-
return new Promise((
|
|
909
|
+
return new Promise((resolve3, reject) => {
|
|
910
910
|
ffmpeg.ffprobe(filePath, (err, data) => {
|
|
911
911
|
if (err) {
|
|
912
912
|
reject(err);
|
|
@@ -917,7 +917,7 @@ function ffprobeAsync(filePath) {
|
|
|
917
917
|
reject(new Error("ffprobe returned no duration"));
|
|
918
918
|
return;
|
|
919
919
|
}
|
|
920
|
-
|
|
920
|
+
resolve3(duration);
|
|
921
921
|
});
|
|
922
922
|
});
|
|
923
923
|
}
|
|
@@ -5274,17 +5274,228 @@ async function runDistill(args) {
|
|
|
5274
5274
|
}
|
|
5275
5275
|
|
|
5276
5276
|
// src/commands/mcp.ts
|
|
5277
|
-
import {
|
|
5277
|
+
import { resolve as resolve2, join as join5, basename as basename4, extname as extname3 } from "path";
|
|
5278
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5279
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
5280
|
+
var DEFAULT_OUTPUT = "./vidistill-output/";
|
|
5281
|
+
async function resolveApiKeyNonInteractive() {
|
|
5282
|
+
const envKey = process.env["GEMINI_API_KEY"];
|
|
5283
|
+
if (envKey && envKey.trim().length > 0) return envKey.trim();
|
|
5284
|
+
const config = await loadConfig();
|
|
5285
|
+
if (config?.apiKey && config.apiKey.trim().length > 0) return config.apiKey.trim();
|
|
5286
|
+
throw new Error("GEMINI_API_KEY not set. Set it as an environment variable or in ~/.vidistill/config.json");
|
|
5287
|
+
}
|
|
5288
|
+
async function analyzeVideo(input, context, lang) {
|
|
5289
|
+
const apiKey = await resolveApiKeyNonInteractive();
|
|
5290
|
+
const resolved = resolveInput(input);
|
|
5291
|
+
const client = new GeminiClient(apiKey);
|
|
5292
|
+
let fileUri;
|
|
5293
|
+
let mimeType;
|
|
5294
|
+
let duration;
|
|
5295
|
+
let videoTitle;
|
|
5296
|
+
if (resolved.type === "youtube") {
|
|
5297
|
+
const result = await handleYouTube(resolved.value, client);
|
|
5298
|
+
fileUri = result.fileUri;
|
|
5299
|
+
mimeType = result.mimeType;
|
|
5300
|
+
try {
|
|
5301
|
+
duration = await detectDuration({
|
|
5302
|
+
ytDlpDuration: result.duration,
|
|
5303
|
+
geminiDuration: result.duration
|
|
5304
|
+
});
|
|
5305
|
+
} catch (err) {
|
|
5306
|
+
process.stderr.write(`Duration detection failed, using 600s fallback: ${err instanceof Error ? err.message : String(err)}
|
|
5307
|
+
`);
|
|
5308
|
+
duration = 600;
|
|
5309
|
+
}
|
|
5310
|
+
const videoId = extractVideoId(resolved.value);
|
|
5311
|
+
videoTitle = videoId != null ? `youtube-${videoId}` : resolved.value;
|
|
5312
|
+
} else {
|
|
5313
|
+
const result = await handleLocalFile(resolved.value, client);
|
|
5314
|
+
fileUri = result.fileUri;
|
|
5315
|
+
mimeType = result.mimeType;
|
|
5316
|
+
duration = await detectDuration({
|
|
5317
|
+
filePath: resolved.value,
|
|
5318
|
+
geminiDuration: result.duration
|
|
5319
|
+
});
|
|
5320
|
+
videoTitle = basename4(resolved.value, extname3(resolved.value));
|
|
5321
|
+
}
|
|
5322
|
+
const model = MODELS.flash;
|
|
5323
|
+
const outputDir = resolve2(DEFAULT_OUTPUT);
|
|
5324
|
+
const slug = slugify(videoTitle);
|
|
5325
|
+
const finalOutputDir = `${outputDir}/${slug}`;
|
|
5326
|
+
const rateLimiter = new RateLimiter();
|
|
5327
|
+
const pipelineResult = await runPipeline({
|
|
5328
|
+
client,
|
|
5329
|
+
fileUri,
|
|
5330
|
+
mimeType,
|
|
5331
|
+
duration,
|
|
5332
|
+
model,
|
|
5333
|
+
context,
|
|
5334
|
+
lang,
|
|
5335
|
+
rateLimiter
|
|
5336
|
+
});
|
|
5337
|
+
await generateOutput({
|
|
5338
|
+
pipelineResult,
|
|
5339
|
+
outputDir,
|
|
5340
|
+
videoTitle,
|
|
5341
|
+
source: input,
|
|
5342
|
+
duration,
|
|
5343
|
+
model,
|
|
5344
|
+
processingTimeMs: 0
|
|
5345
|
+
});
|
|
5346
|
+
let summary = "Analysis complete.";
|
|
5347
|
+
const synthesisPath = join5(finalOutputDir, "raw", "synthesis.json");
|
|
5348
|
+
const synthesis = await readJsonFile(synthesisPath);
|
|
5349
|
+
if (synthesis?.overview) {
|
|
5350
|
+
summary = synthesis.overview;
|
|
5351
|
+
}
|
|
5352
|
+
return { outputDir: finalOutputDir, summary };
|
|
5353
|
+
}
|
|
5354
|
+
async function getTranscript(outputDir, startTime, endTime) {
|
|
5355
|
+
const absDir = resolve2(outputDir);
|
|
5356
|
+
if (!existsSync4(absDir)) {
|
|
5357
|
+
throw new Error("Not a vidistill output directory");
|
|
5358
|
+
}
|
|
5359
|
+
const rawDir = join5(absDir, "raw");
|
|
5360
|
+
if (!existsSync4(rawDir)) {
|
|
5361
|
+
throw new Error("No extracted data found");
|
|
5362
|
+
}
|
|
5363
|
+
const files = await readdir2(rawDir);
|
|
5364
|
+
const pass1Files = files.filter((f) => /^pass1-seg\d+\.json$/.test(f)).sort();
|
|
5365
|
+
if (pass1Files.length === 0) {
|
|
5366
|
+
throw new Error("No extracted data found");
|
|
5367
|
+
}
|
|
5368
|
+
const lines = [];
|
|
5369
|
+
for (const file of pass1Files) {
|
|
5370
|
+
const data = await readJsonFile(join5(rawDir, file));
|
|
5371
|
+
if (data?.transcript_entries == null) continue;
|
|
5372
|
+
for (const entry of data.transcript_entries) {
|
|
5373
|
+
if (startTime != null || endTime != null) {
|
|
5374
|
+
const ts = parseTimestamp(entry.timestamp);
|
|
5375
|
+
if (startTime != null && ts < startTime) continue;
|
|
5376
|
+
if (endTime != null && ts > endTime) continue;
|
|
5377
|
+
}
|
|
5378
|
+
const speaker = entry.speaker ? `${entry.speaker}: ` : "";
|
|
5379
|
+
lines.push(`[${entry.timestamp}] ${speaker}${entry.text}`);
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
return lines.join("\n");
|
|
5383
|
+
}
|
|
5384
|
+
async function getCode(outputDir) {
|
|
5385
|
+
const absDir = resolve2(outputDir);
|
|
5386
|
+
if (!existsSync4(absDir)) {
|
|
5387
|
+
throw new Error("Not a vidistill output directory");
|
|
5388
|
+
}
|
|
5389
|
+
const codeDir = join5(absDir, "code");
|
|
5390
|
+
if (!existsSync4(codeDir)) {
|
|
5391
|
+
return [];
|
|
5392
|
+
}
|
|
5393
|
+
const files = await readdir2(codeDir);
|
|
5394
|
+
const results = [];
|
|
5395
|
+
for (const file of files) {
|
|
5396
|
+
const content = await readFile3(join5(codeDir, file), "utf8");
|
|
5397
|
+
results.push({ filename: file, content });
|
|
5398
|
+
}
|
|
5399
|
+
return results;
|
|
5400
|
+
}
|
|
5278
5401
|
async function run(_args) {
|
|
5279
|
-
|
|
5402
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5403
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5404
|
+
const z = await import("zod");
|
|
5405
|
+
const server = new McpServer({
|
|
5406
|
+
name: "vidistill",
|
|
5407
|
+
version: "1.0.0"
|
|
5408
|
+
});
|
|
5409
|
+
server.registerTool(
|
|
5410
|
+
"analyze_video",
|
|
5411
|
+
{
|
|
5412
|
+
title: "Analyze Video",
|
|
5413
|
+
description: "Run the full vidistill pipeline on a video URL or local file. Returns the output directory and a summary.",
|
|
5414
|
+
inputSchema: z.object({
|
|
5415
|
+
input: z.string().describe("YouTube URL or local file path"),
|
|
5416
|
+
context: z.string().optional().describe('Optional context about the video (e.g. "CS lecture", "product demo")'),
|
|
5417
|
+
lang: z.string().optional().describe("Output language")
|
|
5418
|
+
})
|
|
5419
|
+
},
|
|
5420
|
+
async ({ input, context, lang }) => {
|
|
5421
|
+
try {
|
|
5422
|
+
const result = await analyzeVideo(input, context, lang);
|
|
5423
|
+
return {
|
|
5424
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
5425
|
+
};
|
|
5426
|
+
} catch (err) {
|
|
5427
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5428
|
+
return {
|
|
5429
|
+
content: [{ type: "text", text: message }],
|
|
5430
|
+
isError: true
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
);
|
|
5435
|
+
server.registerTool(
|
|
5436
|
+
"get_transcript",
|
|
5437
|
+
{
|
|
5438
|
+
title: "Get Transcript",
|
|
5439
|
+
description: "Read transcript from an existing vidistill output directory. Optionally filter by time range.",
|
|
5440
|
+
inputSchema: z.object({
|
|
5441
|
+
outputDir: z.string().describe("Path to a vidistill output directory"),
|
|
5442
|
+
startTime: z.number().optional().describe("Start time in seconds to filter from"),
|
|
5443
|
+
endTime: z.number().optional().describe("End time in seconds to filter to")
|
|
5444
|
+
})
|
|
5445
|
+
},
|
|
5446
|
+
async ({ outputDir, startTime, endTime }) => {
|
|
5447
|
+
try {
|
|
5448
|
+
const text4 = await getTranscript(outputDir, startTime, endTime);
|
|
5449
|
+
return {
|
|
5450
|
+
content: [{ type: "text", text: text4 }]
|
|
5451
|
+
};
|
|
5452
|
+
} catch (err) {
|
|
5453
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5454
|
+
return {
|
|
5455
|
+
content: [{ type: "text", text: message }],
|
|
5456
|
+
isError: true
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
);
|
|
5461
|
+
server.registerTool(
|
|
5462
|
+
"get_code",
|
|
5463
|
+
{
|
|
5464
|
+
title: "Get Code",
|
|
5465
|
+
description: "Read code files from an existing vidistill output directory.",
|
|
5466
|
+
inputSchema: z.object({
|
|
5467
|
+
outputDir: z.string().describe("Path to a vidistill output directory")
|
|
5468
|
+
})
|
|
5469
|
+
},
|
|
5470
|
+
async ({ outputDir }) => {
|
|
5471
|
+
try {
|
|
5472
|
+
const files = await getCode(outputDir);
|
|
5473
|
+
return {
|
|
5474
|
+
content: [{ type: "text", text: JSON.stringify(files) }]
|
|
5475
|
+
};
|
|
5476
|
+
} catch (err) {
|
|
5477
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5478
|
+
return {
|
|
5479
|
+
content: [{ type: "text", text: message }],
|
|
5480
|
+
isError: true
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
);
|
|
5485
|
+
const transport = new StdioServerTransport();
|
|
5486
|
+
await server.connect(transport);
|
|
5487
|
+
process.on("SIGINT", async () => {
|
|
5488
|
+
await server.close();
|
|
5489
|
+
process.exit(0);
|
|
5490
|
+
});
|
|
5280
5491
|
}
|
|
5281
5492
|
|
|
5282
5493
|
// src/commands/rename-speakers.ts
|
|
5283
|
-
import { join as
|
|
5284
|
-
import { log as
|
|
5494
|
+
import { join as join6 } from "path";
|
|
5495
|
+
import { log as log9, text as text3, isCancel as isCancel3, cancel as cancel4 } from "@clack/prompts";
|
|
5285
5496
|
|
|
5286
5497
|
// src/cli/speaker-naming.ts
|
|
5287
|
-
import { log as
|
|
5498
|
+
import { log as log8, text as text2, confirm as confirm2, isCancel as isCancel2, cancel as cancel3 } from "@clack/prompts";
|
|
5288
5499
|
async function detectAndPromptMerges(mapping) {
|
|
5289
5500
|
const byName = /* @__PURE__ */ new Map();
|
|
5290
5501
|
for (const [label, name] of Object.entries(mapping)) {
|
|
@@ -5323,7 +5534,7 @@ async function detectAndPromptMerges(mapping) {
|
|
|
5323
5534
|
async function collectSpeakersFromRaw(rawDir) {
|
|
5324
5535
|
const speakerEntries = /* @__PURE__ */ new Map();
|
|
5325
5536
|
for (let n = 0; n < 1e3; n++) {
|
|
5326
|
-
const pass1 = await readJsonFile(
|
|
5537
|
+
const pass1 = await readJsonFile(join6(rawDir, `pass1-seg${n}.json`));
|
|
5327
5538
|
if (pass1 == null) break;
|
|
5328
5539
|
for (const info of pass1.speaker_summary) {
|
|
5329
5540
|
if (!info.speaker_id) continue;
|
|
@@ -5440,22 +5651,22 @@ function formatNameList(names) {
|
|
|
5440
5651
|
return names.map((n) => `"${n.replace(/"/g, '\\"')}"`).join(", ");
|
|
5441
5652
|
}
|
|
5442
5653
|
async function runList(outputDir) {
|
|
5443
|
-
const metadataPath =
|
|
5654
|
+
const metadataPath = join6(outputDir, "metadata.json");
|
|
5444
5655
|
const metadata = await readJsonFile(metadataPath);
|
|
5445
5656
|
if (metadata == null) {
|
|
5446
|
-
|
|
5657
|
+
log9.error("Not a vidistill output directory");
|
|
5447
5658
|
return;
|
|
5448
5659
|
}
|
|
5449
|
-
const rawDir =
|
|
5660
|
+
const rawDir = join6(outputDir, "raw");
|
|
5450
5661
|
const speakers = await collectSpeakersFromRaw(rawDir);
|
|
5451
5662
|
const speakerMapping = metadata.speakerMapping ?? {};
|
|
5452
5663
|
if (speakers.length === 0 && Object.keys(speakerMapping).length === 0) {
|
|
5453
|
-
|
|
5664
|
+
log9.info("No speakers found.");
|
|
5454
5665
|
return;
|
|
5455
5666
|
}
|
|
5456
5667
|
const groups = groupSpeakersByExistingMapping(speakers, speakerMapping);
|
|
5457
5668
|
if (groups.length === 0) {
|
|
5458
|
-
|
|
5669
|
+
log9.info("No speakers found.");
|
|
5459
5670
|
return;
|
|
5460
5671
|
}
|
|
5461
5672
|
const lines = groups.map((group, idx) => {
|
|
@@ -5464,21 +5675,21 @@ async function runList(outputDir) {
|
|
|
5464
5675
|
const labelsStr = group.labels.join(", ");
|
|
5465
5676
|
return `${String(num)}. ${displayName} (${labelsStr}, ${String(group.totalEntries)} entries)`;
|
|
5466
5677
|
});
|
|
5467
|
-
|
|
5678
|
+
log9.info(lines.join("\n"));
|
|
5468
5679
|
}
|
|
5469
5680
|
async function runRename(outputDir, oldName, newName) {
|
|
5470
5681
|
if (newName.trim().length === 0) {
|
|
5471
|
-
|
|
5682
|
+
log9.error("New name cannot be empty. Use the interactive prompt to clear a mapping.");
|
|
5472
5683
|
return;
|
|
5473
5684
|
}
|
|
5474
|
-
const metadataPath =
|
|
5685
|
+
const metadataPath = join6(outputDir, "metadata.json");
|
|
5475
5686
|
const metadata = await readJsonFile(metadataPath);
|
|
5476
5687
|
if (metadata == null) {
|
|
5477
|
-
|
|
5688
|
+
log9.error("Not a vidistill output directory");
|
|
5478
5689
|
return;
|
|
5479
5690
|
}
|
|
5480
5691
|
const speakerMapping = { ...metadata.speakerMapping ?? {} };
|
|
5481
|
-
const rawDir =
|
|
5692
|
+
const rawDir = join6(outputDir, "raw");
|
|
5482
5693
|
const speakers = await collectSpeakersFromRaw(rawDir);
|
|
5483
5694
|
const matchingKeys = [];
|
|
5484
5695
|
const directKey = speakers.find((s) => s.label === oldName);
|
|
@@ -5495,18 +5706,18 @@ async function runRename(outputDir, oldName, newName) {
|
|
|
5495
5706
|
const currentNames = Object.values(speakerMapping);
|
|
5496
5707
|
const unmappedLabels = speakers.filter((s) => speakerMapping[s.label] == null).map((s) => s.label);
|
|
5497
5708
|
const allNames = [.../* @__PURE__ */ new Set([...currentNames, ...unmappedLabels])];
|
|
5498
|
-
|
|
5709
|
+
log9.error(`No speaker named "${oldName}" found. Current speakers: ${formatNameList(allNames)}`);
|
|
5499
5710
|
return;
|
|
5500
5711
|
}
|
|
5501
5712
|
if (matchingKeys.length > 1) {
|
|
5502
|
-
|
|
5713
|
+
log9.error(
|
|
5503
5714
|
`Multiple speakers named "${oldName}" (${matchingKeys.join(", ")}). Use SPEAKER_XX label to specify which one.`
|
|
5504
5715
|
);
|
|
5505
5716
|
return;
|
|
5506
5717
|
}
|
|
5507
5718
|
const key = matchingKeys[0];
|
|
5508
5719
|
speakerMapping[key] = newName;
|
|
5509
|
-
|
|
5720
|
+
log9.info("Re-rendering output files with updated speaker names...");
|
|
5510
5721
|
const result = await reRenderWithSpeakerMapping({
|
|
5511
5722
|
outputDir,
|
|
5512
5723
|
speakerMapping,
|
|
@@ -5514,20 +5725,20 @@ async function runRename(outputDir, oldName, newName) {
|
|
|
5514
5725
|
});
|
|
5515
5726
|
if (result.errors.length > 0) {
|
|
5516
5727
|
for (const err of result.errors) {
|
|
5517
|
-
|
|
5728
|
+
log9.error(err);
|
|
5518
5729
|
}
|
|
5519
5730
|
}
|
|
5520
|
-
|
|
5731
|
+
log9.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
|
|
5521
5732
|
}
|
|
5522
5733
|
async function runMerge(outputDir, sourceName, targetName) {
|
|
5523
|
-
const metadataPath =
|
|
5734
|
+
const metadataPath = join6(outputDir, "metadata.json");
|
|
5524
5735
|
const metadata = await readJsonFile(metadataPath);
|
|
5525
5736
|
if (metadata == null) {
|
|
5526
|
-
|
|
5737
|
+
log9.error("Not a vidistill output directory");
|
|
5527
5738
|
return;
|
|
5528
5739
|
}
|
|
5529
5740
|
const speakerMapping = { ...metadata.speakerMapping ?? {} };
|
|
5530
|
-
const rawDir =
|
|
5741
|
+
const rawDir = join6(outputDir, "raw");
|
|
5531
5742
|
const speakers = await collectSpeakersFromRaw(rawDir);
|
|
5532
5743
|
function findKeys(name) {
|
|
5533
5744
|
const directKey = speakers.find((s) => s.label === name);
|
|
@@ -5552,19 +5763,19 @@ async function runMerge(outputDir, sourceName, targetName) {
|
|
|
5552
5763
|
const targetKeys = findKeys(targetName);
|
|
5553
5764
|
if (sourceKeys.length === 0) {
|
|
5554
5765
|
const currentNames = buildCurrentNames(speakers, speakerMapping);
|
|
5555
|
-
|
|
5766
|
+
log9.error(`No speaker named "${sourceName}" found. Current speakers: ${formatNameList(currentNames)}`);
|
|
5556
5767
|
return;
|
|
5557
5768
|
}
|
|
5558
5769
|
if (targetKeys.length === 0) {
|
|
5559
5770
|
const currentNames = buildCurrentNames(speakers, speakerMapping);
|
|
5560
|
-
|
|
5771
|
+
log9.error(`No speaker named "${targetName}" found. Current speakers: ${formatNameList(currentNames)}`);
|
|
5561
5772
|
return;
|
|
5562
5773
|
}
|
|
5563
5774
|
const resolvedTargetName = speakerMapping[targetKeys[0]] ?? targetName;
|
|
5564
5775
|
for (const key of sourceKeys) {
|
|
5565
5776
|
speakerMapping[key] = resolvedTargetName;
|
|
5566
5777
|
}
|
|
5567
|
-
|
|
5778
|
+
log9.info("Re-rendering output files with updated speaker names...");
|
|
5568
5779
|
const result = await reRenderWithSpeakerMapping({
|
|
5569
5780
|
outputDir,
|
|
5570
5781
|
speakerMapping,
|
|
@@ -5572,10 +5783,10 @@ async function runMerge(outputDir, sourceName, targetName) {
|
|
|
5572
5783
|
});
|
|
5573
5784
|
if (result.errors.length > 0) {
|
|
5574
5785
|
for (const err of result.errors) {
|
|
5575
|
-
|
|
5786
|
+
log9.error(err);
|
|
5576
5787
|
}
|
|
5577
5788
|
}
|
|
5578
|
-
|
|
5789
|
+
log9.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
|
|
5579
5790
|
}
|
|
5580
5791
|
function buildCurrentNames(speakers, speakerMapping) {
|
|
5581
5792
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5592,11 +5803,11 @@ function buildCurrentNames(speakers, speakerMapping) {
|
|
|
5592
5803
|
async function run2(args) {
|
|
5593
5804
|
const { outputDir, list, rename, merge, error } = parseArgs(args);
|
|
5594
5805
|
if (error != null) {
|
|
5595
|
-
|
|
5806
|
+
log9.error(error);
|
|
5596
5807
|
return;
|
|
5597
5808
|
}
|
|
5598
5809
|
if (outputDir == null || outputDir.trim() === "") {
|
|
5599
|
-
|
|
5810
|
+
log9.error('Usage: vidistill rename-speakers <output-dir> [--list] [--rename "old" "new"] [--merge "source" "target"]');
|
|
5600
5811
|
return;
|
|
5601
5812
|
}
|
|
5602
5813
|
if (list) {
|
|
@@ -5611,25 +5822,25 @@ async function run2(args) {
|
|
|
5611
5822
|
await runMerge(outputDir, merge[0], merge[1]);
|
|
5612
5823
|
return;
|
|
5613
5824
|
}
|
|
5614
|
-
const metadataPath =
|
|
5825
|
+
const metadataPath = join6(outputDir, "metadata.json");
|
|
5615
5826
|
const metadata = await readJsonFile(metadataPath);
|
|
5616
5827
|
if (metadata == null) {
|
|
5617
|
-
|
|
5828
|
+
log9.error("Not a vidistill output directory");
|
|
5618
5829
|
return;
|
|
5619
5830
|
}
|
|
5620
|
-
const rawDir =
|
|
5621
|
-
const peopleExtraction = await readJsonFile(
|
|
5831
|
+
const rawDir = join6(outputDir, "raw");
|
|
5832
|
+
const peopleExtraction = await readJsonFile(join6(rawDir, "pass3b-people.json"));
|
|
5622
5833
|
if (peopleExtraction == null) {
|
|
5623
|
-
|
|
5834
|
+
log9.info("No speakers detected in this video");
|
|
5624
5835
|
return;
|
|
5625
5836
|
}
|
|
5626
5837
|
const speakers = await collectSpeakersFromRaw(rawDir);
|
|
5627
5838
|
if (speakers.length === 0) {
|
|
5628
|
-
|
|
5839
|
+
log9.info("No speakers detected in this video");
|
|
5629
5840
|
return;
|
|
5630
5841
|
}
|
|
5631
5842
|
const existingMapping = metadata.speakerMapping ?? {};
|
|
5632
|
-
|
|
5843
|
+
log9.info(
|
|
5633
5844
|
`${String(speakers.length)} speaker${speakers.length === 1 ? "" : "s"} found. Enter names (or press Enter to keep current).`
|
|
5634
5845
|
);
|
|
5635
5846
|
const groups = groupSpeakersByExistingMapping(speakers, existingMapping);
|
|
@@ -5676,7 +5887,7 @@ async function run2(args) {
|
|
|
5676
5887
|
return;
|
|
5677
5888
|
}
|
|
5678
5889
|
const { mapping: finalMapping, declinedMerges } = mergeResult;
|
|
5679
|
-
|
|
5890
|
+
log9.info("Re-rendering output files with updated speaker names...");
|
|
5680
5891
|
const result = await reRenderWithSpeakerMapping({
|
|
5681
5892
|
outputDir,
|
|
5682
5893
|
speakerMapping: finalMapping,
|
|
@@ -5684,15 +5895,15 @@ async function run2(args) {
|
|
|
5684
5895
|
});
|
|
5685
5896
|
if (result.errors.length > 0) {
|
|
5686
5897
|
for (const err of result.errors) {
|
|
5687
|
-
|
|
5898
|
+
log9.error(err);
|
|
5688
5899
|
}
|
|
5689
5900
|
}
|
|
5690
|
-
|
|
5901
|
+
log9.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
|
|
5691
5902
|
}
|
|
5692
5903
|
|
|
5693
5904
|
// src/cli/index.ts
|
|
5694
|
-
var version = "0.
|
|
5695
|
-
var
|
|
5905
|
+
var version = "0.6.0";
|
|
5906
|
+
var DEFAULT_OUTPUT2 = "./vidistill-output/";
|
|
5696
5907
|
var SUBCOMMANDS = {
|
|
5697
5908
|
mcp: run,
|
|
5698
5909
|
"rename-speakers": run2
|
|
@@ -5718,9 +5929,9 @@ Commands: ${Object.keys(SUBCOMMANDS).join(", ")}`
|
|
|
5718
5929
|
},
|
|
5719
5930
|
output: {
|
|
5720
5931
|
type: "string",
|
|
5721
|
-
description: `Output directory for generated notes (default: ${
|
|
5932
|
+
description: `Output directory for generated notes (default: ${DEFAULT_OUTPUT2})`,
|
|
5722
5933
|
alias: "o",
|
|
5723
|
-
default:
|
|
5934
|
+
default: DEFAULT_OUTPUT2
|
|
5724
5935
|
},
|
|
5725
5936
|
lang: {
|
|
5726
5937
|
type: "string",
|
|
@@ -5744,11 +5955,11 @@ Commands: ${Object.keys(SUBCOMMANDS).join(", ")}`
|
|
|
5744
5955
|
lang: args.lang
|
|
5745
5956
|
});
|
|
5746
5957
|
} catch (err) {
|
|
5747
|
-
const { log:
|
|
5958
|
+
const { log: log10 } = await import("@clack/prompts");
|
|
5748
5959
|
const { default: pc4 } = await import("picocolors");
|
|
5749
5960
|
const raw = err instanceof Error ? err.message : String(err);
|
|
5750
5961
|
const message = raw.split("\n")[0].slice(0, 200);
|
|
5751
|
-
|
|
5962
|
+
log10.error(pc4.red(message));
|
|
5752
5963
|
process.exit(1);
|
|
5753
5964
|
}
|
|
5754
5965
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vidistill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Video intelligence distiller — extract structured notes, transcripts, and insights from any video using Gemini",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,11 +31,13 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "1.0.1",
|
|
33
33
|
"@google/genai": "^1.40.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
34
35
|
"citty": "^0.1.6",
|
|
35
36
|
"figlet": "^1.8.0",
|
|
36
37
|
"fluent-ffmpeg": "^2.1.3",
|
|
37
38
|
"picocolors": "^1.1.1",
|
|
38
|
-
"ytdlp-nodejs": "^2.2.0"
|
|
39
|
+
"ytdlp-nodejs": "^2.2.0",
|
|
40
|
+
"zod": "^4.3.6"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@types/figlet": "^1.7.0",
|