vidistill 0.2.0 → 0.2.2
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 +306 -202
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __glob = (map) => (path) => {
|
|
3
|
+
var fn = map[path];
|
|
4
|
+
if (fn) return fn();
|
|
5
|
+
throw new Error("Module not found in bundle: " + path);
|
|
6
|
+
};
|
|
2
7
|
|
|
3
8
|
// src/cli/index.ts
|
|
9
|
+
import { createRequire as createRequire2 } from "module";
|
|
4
10
|
import { defineCommand, runMain } from "citty";
|
|
5
|
-
import { log as log10 } from "@clack/prompts";
|
|
6
|
-
import pc5 from "picocolors";
|
|
7
|
-
import { basename as basename3, extname as extname2, resolve } from "path";
|
|
8
11
|
|
|
9
12
|
// src/cli/ui.ts
|
|
10
13
|
import figlet from "figlet";
|
|
11
14
|
import pc from "picocolors";
|
|
12
|
-
import { intro,
|
|
15
|
+
import { intro, note } from "@clack/prompts";
|
|
13
16
|
function showLogo() {
|
|
14
17
|
const ascii = figlet.textSync("VIDISTILL", { font: "Big" });
|
|
15
18
|
console.log(pc.cyan(ascii));
|
|
@@ -17,17 +20,22 @@ function showLogo() {
|
|
|
17
20
|
function showIntro() {
|
|
18
21
|
intro(pc.dim("video intelligence distiller"));
|
|
19
22
|
}
|
|
20
|
-
function
|
|
23
|
+
function showConfigBox(config) {
|
|
21
24
|
const lines = [
|
|
22
|
-
`
|
|
23
|
-
`
|
|
24
|
-
`
|
|
25
|
+
`Video: ${config.input}`,
|
|
26
|
+
`Context: ${config.context ?? "(none)"}`,
|
|
27
|
+
`Output: ${config.output}`
|
|
25
28
|
];
|
|
26
|
-
|
|
29
|
+
note(lines.join("\n"), "Configuration");
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
// src/commands/distill.ts
|
|
33
|
+
import { log as log8, cancel as cancel2 } from "@clack/prompts";
|
|
34
|
+
import pc4 from "picocolors";
|
|
35
|
+
import { basename as basename3, extname as extname2, resolve } from "path";
|
|
36
|
+
|
|
29
37
|
// src/cli/prompts.ts
|
|
30
|
-
import { text, password, confirm, isCancel, cancel } from "@clack/prompts";
|
|
38
|
+
import { text, password, confirm, select, isCancel, cancel } from "@clack/prompts";
|
|
31
39
|
function handleCancel(value) {
|
|
32
40
|
if (isCancel(value)) {
|
|
33
41
|
cancel("Operation cancelled.");
|
|
@@ -39,7 +47,7 @@ async function promptVideoSource() {
|
|
|
39
47
|
message: "YouTube URL or local file path",
|
|
40
48
|
placeholder: "https://youtube.com/watch?v=...",
|
|
41
49
|
validate(input) {
|
|
42
|
-
if (input.trim().length === 0) {
|
|
50
|
+
if (!input || input.trim().length === 0) {
|
|
43
51
|
return "A video source is required.";
|
|
44
52
|
}
|
|
45
53
|
}
|
|
@@ -60,7 +68,7 @@ async function promptApiKey() {
|
|
|
60
68
|
const value = await password({
|
|
61
69
|
message: "Gemini API key",
|
|
62
70
|
validate(input) {
|
|
63
|
-
if (input.trim().length === 0) {
|
|
71
|
+
if (!input || input.trim().length === 0) {
|
|
64
72
|
return "An API key is required.";
|
|
65
73
|
}
|
|
66
74
|
}
|
|
@@ -76,12 +84,25 @@ async function promptSaveKey() {
|
|
|
76
84
|
handleCancel(value);
|
|
77
85
|
return value;
|
|
78
86
|
}
|
|
87
|
+
async function promptConfirmation() {
|
|
88
|
+
const value = await select({
|
|
89
|
+
message: "Ready to process?",
|
|
90
|
+
options: [
|
|
91
|
+
{ value: "start", label: "Start processing" },
|
|
92
|
+
{ value: "edit-video", label: "Edit video source" },
|
|
93
|
+
{ value: "edit-context", label: "Edit context" },
|
|
94
|
+
{ value: "cancel", label: "Cancel" }
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
handleCancel(value);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
79
100
|
|
|
80
101
|
// src/cli/config.ts
|
|
81
102
|
import { promises as fs } from "fs";
|
|
82
103
|
import { join } from "path";
|
|
83
104
|
import os from "os";
|
|
84
|
-
import { log
|
|
105
|
+
import { log } from "@clack/prompts";
|
|
85
106
|
import pc2 from "picocolors";
|
|
86
107
|
|
|
87
108
|
// src/gemini/client.ts
|
|
@@ -187,12 +208,12 @@ async function saveConfig(config) {
|
|
|
187
208
|
async function resolveApiKey() {
|
|
188
209
|
const envKey = process.env["GEMINI_API_KEY"];
|
|
189
210
|
if (envKey && envKey.trim().length > 0) {
|
|
190
|
-
|
|
211
|
+
log.info(pc2.dim("(using GEMINI_API_KEY from environment)"));
|
|
191
212
|
return envKey.trim();
|
|
192
213
|
}
|
|
193
214
|
const config = await loadConfig();
|
|
194
215
|
if (config?.apiKey && config.apiKey.trim().length > 0) {
|
|
195
|
-
|
|
216
|
+
log.info(pc2.dim("(using API key from ~/.vidistill/config.json)"));
|
|
196
217
|
return config.apiKey.trim();
|
|
197
218
|
}
|
|
198
219
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
@@ -200,9 +221,9 @@ async function resolveApiKey() {
|
|
|
200
221
|
const client = new GeminiClient(key);
|
|
201
222
|
const valid = await client.validateKey();
|
|
202
223
|
if (!valid) {
|
|
203
|
-
|
|
224
|
+
log.error(pc2.red("Invalid API key"));
|
|
204
225
|
if (attempt === MAX_ATTEMPTS) {
|
|
205
|
-
|
|
226
|
+
log.error(pc2.red("Maximum attempts reached. Exiting."));
|
|
206
227
|
process.exit(1);
|
|
207
228
|
}
|
|
208
229
|
continue;
|
|
@@ -218,64 +239,50 @@ async function resolveApiKey() {
|
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
// src/cli/progress.ts
|
|
221
|
-
import { spinner,
|
|
222
|
-
|
|
242
|
+
import { spinner, progress } from "@clack/prompts";
|
|
243
|
+
var PHASE_LABELS = {
|
|
244
|
+
pass0: "Understanding your video...",
|
|
245
|
+
pass1: "Extracting transcript...",
|
|
246
|
+
pass2: "Analyzing visuals...",
|
|
247
|
+
pass3a: "Reconstructing code...",
|
|
248
|
+
pass3b: "Identifying participants...",
|
|
249
|
+
pass3c: "Reading chat messages...",
|
|
250
|
+
pass3d: "Detecting insights...",
|
|
251
|
+
synthesis: "Synthesizing notes...",
|
|
252
|
+
output: "Writing output files..."
|
|
253
|
+
};
|
|
223
254
|
function createProgressDisplay() {
|
|
224
255
|
const s = spinner();
|
|
225
|
-
s.start(
|
|
256
|
+
s.start(PHASE_LABELS.pass0);
|
|
257
|
+
let progressBar = null;
|
|
258
|
+
let seenTotalSteps = false;
|
|
226
259
|
function update(status) {
|
|
227
|
-
const
|
|
228
|
-
const total = status.totalSegments;
|
|
260
|
+
const label = PHASE_LABELS[status.phase] ?? status.phase;
|
|
229
261
|
if (status.phase === "pass0") {
|
|
230
|
-
s.message(
|
|
231
|
-
|
|
232
|
-
s.message(`Pass 1: Transcript (${segNum}/${total} segments)`);
|
|
233
|
-
} else if (status.phase === "pass2") {
|
|
234
|
-
s.message(`Pass 2: Visual extraction (${segNum}/${total} segments)`);
|
|
235
|
-
} else if (status.phase === "pass3a") {
|
|
236
|
-
if (total > 1) {
|
|
237
|
-
s.message(`Reconstructing code (run ${segNum}/${total})...`);
|
|
238
|
-
} else {
|
|
239
|
-
s.message("Reconstructing code...");
|
|
240
|
-
}
|
|
241
|
-
} else if (status.phase === "pass3b") {
|
|
242
|
-
s.message("People extraction...");
|
|
243
|
-
} else if (status.phase === "pass3c") {
|
|
244
|
-
s.message(`Chat extraction (${segNum}/${total} segments)`);
|
|
245
|
-
} else if (status.phase === "pass3d") {
|
|
246
|
-
s.message(`Implicit signals (${segNum}/${total} segments)`);
|
|
247
|
-
} else if (status.phase === "synthesis") {
|
|
248
|
-
s.message("Synthesizing results...");
|
|
249
|
-
} else if (status.phase === "output") {
|
|
250
|
-
s.message("Generating output files...");
|
|
251
|
-
} else {
|
|
252
|
-
s.message(`${status.phase} (${segNum}/${total} segments)`);
|
|
262
|
+
s.message(label);
|
|
263
|
+
return;
|
|
253
264
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
function complete(result, elapsedMs) {
|
|
260
|
-
const elapsedSecs = Math.round(elapsedMs / 1e3);
|
|
261
|
-
const mins = Math.floor(elapsedSecs / 60);
|
|
262
|
-
const secs = elapsedSecs % 60;
|
|
263
|
-
const elapsed = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
264
|
-
const errorCount = result.errors.length;
|
|
265
|
-
if (errorCount > 0) {
|
|
266
|
-
s.stop(pc3.yellow("Pipeline complete (with errors)"));
|
|
267
|
-
} else {
|
|
268
|
-
s.stop(pc3.green("Pipeline complete"));
|
|
265
|
+
if (!seenTotalSteps && status.totalSteps != null) {
|
|
266
|
+
seenTotalSteps = true;
|
|
267
|
+
s.stop("");
|
|
268
|
+
progressBar = progress({ max: status.totalSteps });
|
|
269
269
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
log3.warn(`Errors: ${pc3.yellow(String(errorCount))}`);
|
|
270
|
+
if (progressBar != null) {
|
|
271
|
+
if (status.status === "done" && status.currentStep != null) {
|
|
272
|
+
progressBar.advance(1, label);
|
|
273
|
+
}
|
|
275
274
|
} else {
|
|
276
|
-
|
|
275
|
+
if (status.status === "done" && status.currentStep != null && status.totalSteps != null) {
|
|
276
|
+
s.message(`${label} (${status.currentStep}/${status.totalSteps})`);
|
|
277
|
+
} else {
|
|
278
|
+
s.message(label);
|
|
279
|
+
}
|
|
277
280
|
}
|
|
278
281
|
}
|
|
282
|
+
function onWait(_delayMs) {
|
|
283
|
+
}
|
|
284
|
+
function complete(_result, _elapsedMs) {
|
|
285
|
+
}
|
|
279
286
|
return { update, onWait, complete };
|
|
280
287
|
}
|
|
281
288
|
|
|
@@ -332,8 +339,8 @@ import { tmpdir } from "os";
|
|
|
332
339
|
import { join as join2 } from "path";
|
|
333
340
|
import { unlink } from "fs/promises";
|
|
334
341
|
import { YtDlp } from "ytdlp-nodejs";
|
|
335
|
-
import { log as
|
|
336
|
-
import
|
|
342
|
+
import { log as log2 } from "@clack/prompts";
|
|
343
|
+
import pc3 from "picocolors";
|
|
337
344
|
|
|
338
345
|
// src/gemini/models.ts
|
|
339
346
|
var MODELS = {
|
|
@@ -377,7 +384,7 @@ async function handleYouTube(url, client) {
|
|
|
377
384
|
});
|
|
378
385
|
return { fileUri: url, mimeType: "video/mp4", source: "direct" };
|
|
379
386
|
} catch (err) {
|
|
380
|
-
|
|
387
|
+
log2.warn(pc3.dim("Direct Gemini probe failed. Falling back to yt-dlp."));
|
|
381
388
|
}
|
|
382
389
|
const tempPath2 = await downloadWithYtDlp(url);
|
|
383
390
|
try {
|
|
@@ -437,7 +444,7 @@ import { existsSync as existsSync2, statSync, openSync, readSync, closeSync, unl
|
|
|
437
444
|
import * as childProc from "child_process";
|
|
438
445
|
import { tmpdir as tmpdir2 } from "os";
|
|
439
446
|
import { join as join3, extname, basename } from "path";
|
|
440
|
-
import { log as
|
|
447
|
+
import { log as log3 } from "@clack/prompts";
|
|
441
448
|
var GB = 1024 * 1024 * 1024;
|
|
442
449
|
var SIZE_3GB = 3 * GB;
|
|
443
450
|
var SIZE_2GB = 2 * GB;
|
|
@@ -533,7 +540,7 @@ function convertMkvToMp4(inputPath) {
|
|
|
533
540
|
}
|
|
534
541
|
function compressTo720p(inputPath) {
|
|
535
542
|
const output = tempPath(".mp4");
|
|
536
|
-
|
|
543
|
+
log3.info("Compressing video to 720p...");
|
|
537
544
|
try {
|
|
538
545
|
childProc.execFileSync(
|
|
539
546
|
"ffmpeg",
|
|
@@ -596,7 +603,7 @@ async function handleLocalFile(filePath, client) {
|
|
|
596
603
|
|
|
597
604
|
// src/input/duration.ts
|
|
598
605
|
import { createRequire } from "module";
|
|
599
|
-
import { log as
|
|
606
|
+
import { log as log4 } from "@clack/prompts";
|
|
600
607
|
var _require = createRequire(import.meta.url);
|
|
601
608
|
var ffmpeg = _require("fluent-ffmpeg");
|
|
602
609
|
var BYTES_PER_SECOND = 5e5;
|
|
@@ -625,7 +632,7 @@ async function detectDuration(source) {
|
|
|
625
632
|
const message = err instanceof Error ? err.message : String(err);
|
|
626
633
|
const isNotFound = /ENOENT|not found|no such file|spawn.*ffprobe|Cannot find/i.test(message);
|
|
627
634
|
if (isNotFound) {
|
|
628
|
-
|
|
635
|
+
log4.warn(
|
|
629
636
|
"ffprobe not found \u2014 video duration will be estimated. Install: brew install ffmpeg"
|
|
630
637
|
);
|
|
631
638
|
}
|
|
@@ -649,7 +656,7 @@ async function detectDuration(source) {
|
|
|
649
656
|
bytes = source.fileSize;
|
|
650
657
|
}
|
|
651
658
|
if (bytes !== void 0 && bytes > 0) {
|
|
652
|
-
|
|
659
|
+
log4.warn(
|
|
653
660
|
"Duration estimated from file size \u2014 segmentation may be inaccurate"
|
|
654
661
|
);
|
|
655
662
|
return Math.max(1, Math.round(bytes / BYTES_PER_SECOND));
|
|
@@ -658,7 +665,7 @@ async function detectDuration(source) {
|
|
|
658
665
|
}
|
|
659
666
|
|
|
660
667
|
// src/core/pipeline.ts
|
|
661
|
-
import { log as
|
|
668
|
+
import { log as log6 } from "@clack/prompts";
|
|
662
669
|
|
|
663
670
|
// src/constants/prompts.ts
|
|
664
671
|
var SYSTEM_INSTRUCTION_PASS_1 = `
|
|
@@ -1896,7 +1903,7 @@ async function runSynthesis(params) {
|
|
|
1896
1903
|
}
|
|
1897
1904
|
|
|
1898
1905
|
// src/core/strategy.ts
|
|
1899
|
-
var BASE_PASSES = ["transcript", "visual"
|
|
1906
|
+
var BASE_PASSES = ["transcript", "visual"];
|
|
1900
1907
|
function determineStrategy(profile) {
|
|
1901
1908
|
const passes = new Set(BASE_PASSES);
|
|
1902
1909
|
const { type, visualContent, audioContent, complexity, recommendations } = profile;
|
|
@@ -1936,6 +1943,7 @@ function determineStrategy(profile) {
|
|
|
1936
1943
|
}
|
|
1937
1944
|
const resolution = recommendations.resolution ?? "medium";
|
|
1938
1945
|
const segmentMinutes = complexity === "complex" && recommendations.segmentMinutes > 8 ? 8 : recommendations.segmentMinutes;
|
|
1946
|
+
passes.add("synthesis");
|
|
1939
1947
|
return {
|
|
1940
1948
|
passes: Array.from(passes),
|
|
1941
1949
|
resolution,
|
|
@@ -1991,7 +1999,7 @@ function createSegmentPlan(durationSeconds, options) {
|
|
|
1991
1999
|
}
|
|
1992
2000
|
|
|
1993
2001
|
// src/core/consensus.ts
|
|
1994
|
-
import { log as
|
|
2002
|
+
import { log as log5 } from "@clack/prompts";
|
|
1995
2003
|
function tokenize(content) {
|
|
1996
2004
|
const tokens = content.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
1997
2005
|
return new Set(tokens);
|
|
@@ -2069,7 +2077,7 @@ async function runCodeConsensus(params) {
|
|
|
2069
2077
|
successfulRuns.push(result);
|
|
2070
2078
|
} catch (e) {
|
|
2071
2079
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2072
|
-
|
|
2080
|
+
log5.warn(`consensus run ${i + 1}/${runs} failed: ${msg}`);
|
|
2073
2081
|
}
|
|
2074
2082
|
onProgress?.(i + 1, runs);
|
|
2075
2083
|
}
|
|
@@ -2293,7 +2301,7 @@ async function runPipeline(config) {
|
|
|
2293
2301
|
"pass0"
|
|
2294
2302
|
);
|
|
2295
2303
|
if (pass0Attempt.error !== null) {
|
|
2296
|
-
|
|
2304
|
+
log6.warn(pass0Attempt.error);
|
|
2297
2305
|
errors.push(pass0Attempt.error);
|
|
2298
2306
|
videoProfile = DEFAULT_PROFILE;
|
|
2299
2307
|
} else {
|
|
@@ -2301,8 +2309,6 @@ async function runPipeline(config) {
|
|
|
2301
2309
|
}
|
|
2302
2310
|
strategy = determineStrategy(videoProfile);
|
|
2303
2311
|
onProgress?.({ phase: "pass0", segment: 0, totalSegments: 1, status: "done" });
|
|
2304
|
-
log8.info(`Video type: ${videoProfile.type}`);
|
|
2305
|
-
log8.info(`Strategy: ${strategy.passes.join(" \u2192 ")}`);
|
|
2306
2312
|
const plan = createSegmentPlan(duration, {
|
|
2307
2313
|
segmentMinutes: strategy.segmentMinutes,
|
|
2308
2314
|
resolution: strategy.resolution
|
|
@@ -2311,6 +2317,10 @@ async function runPipeline(config) {
|
|
|
2311
2317
|
const resolution = plan.resolution;
|
|
2312
2318
|
const results = [];
|
|
2313
2319
|
const n = segments.length;
|
|
2320
|
+
const callsPerSegment = 2 + (strategy.passes.includes("chat") ? 1 : 0) + (strategy.passes.includes("implicit") ? 1 : 0);
|
|
2321
|
+
const postSegmentCalls = (strategy.passes.includes("people") ? 1 : 0) + (strategy.passes.includes("code") ? 3 : 0) + (strategy.passes.includes("synthesis") ? 1 : 0);
|
|
2322
|
+
const totalSteps = n * callsPerSegment + postSegmentCalls;
|
|
2323
|
+
let currentStep = 0;
|
|
2314
2324
|
let pass1RanOnce = false;
|
|
2315
2325
|
let pass2RanOnce = false;
|
|
2316
2326
|
let pass3cRanOnce = false;
|
|
@@ -2324,21 +2334,22 @@ async function runPipeline(config) {
|
|
|
2324
2334
|
break;
|
|
2325
2335
|
}
|
|
2326
2336
|
const segment = segments[i];
|
|
2327
|
-
onProgress?.({ phase: "pass1", segment: i, totalSegments: n, status: "running" });
|
|
2337
|
+
onProgress?.({ phase: "pass1", segment: i, totalSegments: n, status: "running", totalSteps });
|
|
2328
2338
|
let pass1 = null;
|
|
2329
2339
|
const pass1Attempt = await withRetry(
|
|
2330
2340
|
() => rateLimiter.execute(() => runTranscript({ client, fileUri, mimeType, segment, model, resolution }), { onWait }),
|
|
2331
2341
|
`segment ${i} pass1`
|
|
2332
2342
|
);
|
|
2333
2343
|
if (pass1Attempt.error !== null) {
|
|
2334
|
-
|
|
2344
|
+
log6.warn(pass1Attempt.error);
|
|
2335
2345
|
errors.push(pass1Attempt.error);
|
|
2336
2346
|
} else {
|
|
2337
2347
|
pass1 = pass1Attempt.result;
|
|
2338
2348
|
pass1RanOnce = true;
|
|
2339
2349
|
}
|
|
2340
|
-
|
|
2341
|
-
onProgress?.({ phase: "
|
|
2350
|
+
currentStep++;
|
|
2351
|
+
onProgress?.({ phase: "pass1", segment: i, totalSegments: n, status: "done", currentStep, totalSteps });
|
|
2352
|
+
onProgress?.({ phase: "pass2", segment: i, totalSegments: n, status: "running", totalSteps });
|
|
2342
2353
|
let pass2 = null;
|
|
2343
2354
|
const pass2Attempt = await withRetry(
|
|
2344
2355
|
() => rateLimiter.execute(
|
|
@@ -2356,16 +2367,17 @@ async function runPipeline(config) {
|
|
|
2356
2367
|
`segment ${i} pass2`
|
|
2357
2368
|
);
|
|
2358
2369
|
if (pass2Attempt.error !== null) {
|
|
2359
|
-
|
|
2370
|
+
log6.warn(pass2Attempt.error);
|
|
2360
2371
|
errors.push(pass2Attempt.error);
|
|
2361
2372
|
} else {
|
|
2362
2373
|
pass2 = pass2Attempt.result;
|
|
2363
2374
|
pass2RanOnce = true;
|
|
2364
2375
|
}
|
|
2365
|
-
|
|
2376
|
+
currentStep++;
|
|
2377
|
+
onProgress?.({ phase: "pass2", segment: i, totalSegments: n, status: "done", currentStep, totalSteps });
|
|
2366
2378
|
let pass3c;
|
|
2367
2379
|
if (strategy.passes.includes("chat")) {
|
|
2368
|
-
onProgress?.({ phase: "pass3c", segment: i, totalSegments: n, status: "running" });
|
|
2380
|
+
onProgress?.({ phase: "pass3c", segment: i, totalSegments: n, status: "running", totalSteps });
|
|
2369
2381
|
const pass3cAttempt = await withRetry(
|
|
2370
2382
|
() => rateLimiter.execute(
|
|
2371
2383
|
() => runChatExtraction({
|
|
@@ -2382,18 +2394,19 @@ async function runPipeline(config) {
|
|
|
2382
2394
|
`segment ${i} pass3c`
|
|
2383
2395
|
);
|
|
2384
2396
|
if (pass3cAttempt.error !== null) {
|
|
2385
|
-
|
|
2397
|
+
log6.warn(pass3cAttempt.error);
|
|
2386
2398
|
errors.push(pass3cAttempt.error);
|
|
2387
2399
|
pass3c = null;
|
|
2388
2400
|
} else {
|
|
2389
2401
|
pass3c = pass3cAttempt.result;
|
|
2390
2402
|
pass3cRanOnce = true;
|
|
2391
2403
|
}
|
|
2392
|
-
|
|
2404
|
+
currentStep++;
|
|
2405
|
+
onProgress?.({ phase: "pass3c", segment: i, totalSegments: n, status: "done", currentStep, totalSteps });
|
|
2393
2406
|
}
|
|
2394
2407
|
let pass3d;
|
|
2395
2408
|
if (strategy.passes.includes("implicit")) {
|
|
2396
|
-
onProgress?.({ phase: "pass3d", segment: i, totalSegments: n, status: "running" });
|
|
2409
|
+
onProgress?.({ phase: "pass3d", segment: i, totalSegments: n, status: "running", totalSteps });
|
|
2397
2410
|
const pass3dAttempt = await withRetry(
|
|
2398
2411
|
() => rateLimiter.execute(
|
|
2399
2412
|
() => runImplicitSignals({
|
|
@@ -2411,14 +2424,15 @@ async function runPipeline(config) {
|
|
|
2411
2424
|
`segment ${i} pass3d`
|
|
2412
2425
|
);
|
|
2413
2426
|
if (pass3dAttempt.error !== null) {
|
|
2414
|
-
|
|
2427
|
+
log6.warn(pass3dAttempt.error);
|
|
2415
2428
|
errors.push(pass3dAttempt.error);
|
|
2416
2429
|
pass3d = null;
|
|
2417
2430
|
} else {
|
|
2418
2431
|
pass3d = pass3dAttempt.result;
|
|
2419
2432
|
pass3dRanOnce = true;
|
|
2420
2433
|
}
|
|
2421
|
-
|
|
2434
|
+
currentStep++;
|
|
2435
|
+
onProgress?.({ phase: "pass3d", segment: i, totalSegments: n, status: "done", currentStep, totalSteps });
|
|
2422
2436
|
}
|
|
2423
2437
|
results.push({ index: segment.index, pass1, pass2, pass3c, pass3d });
|
|
2424
2438
|
}
|
|
@@ -2447,7 +2461,7 @@ async function runPipeline(config) {
|
|
|
2447
2461
|
const pass2Results = results.map((r) => r.pass2);
|
|
2448
2462
|
let peopleExtraction = null;
|
|
2449
2463
|
if (strategy.passes.includes("people")) {
|
|
2450
|
-
onProgress?.({ phase: "pass3b", segment: 0, totalSegments: 1, status: "running" });
|
|
2464
|
+
onProgress?.({ phase: "pass3b", segment: 0, totalSegments: 1, status: "running", totalSteps });
|
|
2451
2465
|
const pass3bAttempt = await withRetry(
|
|
2452
2466
|
() => rateLimiter.execute(
|
|
2453
2467
|
() => runPeopleExtraction({
|
|
@@ -2462,12 +2476,13 @@ async function runPipeline(config) {
|
|
|
2462
2476
|
"pass3b"
|
|
2463
2477
|
);
|
|
2464
2478
|
if (pass3bAttempt.error !== null) {
|
|
2465
|
-
|
|
2479
|
+
log6.warn(pass3bAttempt.error);
|
|
2466
2480
|
errors.push(pass3bAttempt.error);
|
|
2467
2481
|
} else {
|
|
2468
2482
|
peopleExtraction = pass3bAttempt.result;
|
|
2469
2483
|
}
|
|
2470
|
-
|
|
2484
|
+
currentStep++;
|
|
2485
|
+
onProgress?.({ phase: "pass3b", segment: 0, totalSegments: 1, status: "done", currentStep, totalSteps });
|
|
2471
2486
|
if (peopleExtraction !== null) passesRun.push("pass3b");
|
|
2472
2487
|
}
|
|
2473
2488
|
let codeReconstruction = null;
|
|
@@ -2491,13 +2506,14 @@ async function runPipeline(config) {
|
|
|
2491
2506
|
),
|
|
2492
2507
|
pass2Results,
|
|
2493
2508
|
onProgress: (run, total) => {
|
|
2494
|
-
onProgress?.({ phase: "pass3a", segment: run - 1, totalSegments: total, status: "running" });
|
|
2509
|
+
onProgress?.({ phase: "pass3a", segment: run - 1, totalSegments: total, status: "running", totalSteps });
|
|
2510
|
+
currentStep++;
|
|
2511
|
+
onProgress?.({ phase: "pass3a", segment: run - 1, totalSegments: total, status: "done", currentStep, totalSteps });
|
|
2495
2512
|
}
|
|
2496
2513
|
});
|
|
2497
|
-
onProgress?.({ phase: "pass3a", segment: consensusConfig.runs - 1, totalSegments: consensusConfig.runs, status: "done" });
|
|
2498
2514
|
if (consensusResult.runsCompleted === 0) {
|
|
2499
2515
|
const errMsg = "pass3a: all consensus runs failed";
|
|
2500
|
-
|
|
2516
|
+
log6.warn(errMsg);
|
|
2501
2517
|
errors.push(errMsg);
|
|
2502
2518
|
} else {
|
|
2503
2519
|
const validationResult = validateCodeReconstruction({
|
|
@@ -2513,13 +2529,12 @@ async function runPipeline(config) {
|
|
|
2513
2529
|
};
|
|
2514
2530
|
uncertainCodeFiles = validationResult.uncertain.map((f) => f.filename);
|
|
2515
2531
|
}
|
|
2516
|
-
log8.info(`Code: ${validationResult.confirmed.length} confirmed, ${validationResult.uncertain.length} uncertain, ${validationResult.rejected.length} rejected`);
|
|
2517
2532
|
}
|
|
2518
2533
|
if (codeReconstruction !== null) passesRun.push("pass3a");
|
|
2519
2534
|
}
|
|
2520
2535
|
let synthesisResult;
|
|
2521
2536
|
if (strategy.passes.includes("synthesis")) {
|
|
2522
|
-
onProgress?.({ phase: "synthesis", segment: 0, totalSegments: 1, status: "running" });
|
|
2537
|
+
onProgress?.({ phase: "synthesis", segment: 0, totalSegments: 1, status: "running", totalSteps });
|
|
2523
2538
|
const synthAttempt = await withRetry(
|
|
2524
2539
|
() => rateLimiter.execute(
|
|
2525
2540
|
() => runSynthesis({
|
|
@@ -2536,12 +2551,13 @@ async function runPipeline(config) {
|
|
|
2536
2551
|
"synthesis"
|
|
2537
2552
|
);
|
|
2538
2553
|
if (synthAttempt.error !== null) {
|
|
2539
|
-
|
|
2554
|
+
log6.warn(synthAttempt.error);
|
|
2540
2555
|
errors.push(synthAttempt.error);
|
|
2541
2556
|
} else {
|
|
2542
2557
|
synthesisResult = synthAttempt.result ?? void 0;
|
|
2543
2558
|
}
|
|
2544
|
-
|
|
2559
|
+
currentStep++;
|
|
2560
|
+
onProgress?.({ phase: "synthesis", segment: 0, totalSegments: 1, status: "done", currentStep, totalSteps });
|
|
2545
2561
|
if (synthesisResult !== void 0) passesRun.push("synthesis");
|
|
2546
2562
|
}
|
|
2547
2563
|
return {
|
|
@@ -2742,8 +2758,8 @@ function renderCodeEvent(block) {
|
|
|
2742
2758
|
lines.push("```");
|
|
2743
2759
|
return lines.join("\n");
|
|
2744
2760
|
}
|
|
2745
|
-
function renderVisualEvent(
|
|
2746
|
-
return `_[${
|
|
2761
|
+
function renderVisualEvent(note2) {
|
|
2762
|
+
return `_[${note2.timestamp}]_ **${note2.visual_type}:** ${note2.description}`;
|
|
2747
2763
|
}
|
|
2748
2764
|
function renderEvent(event) {
|
|
2749
2765
|
switch (event.kind) {
|
|
@@ -2774,8 +2790,8 @@ function writeCombined(params) {
|
|
|
2774
2790
|
for (const block of pass2.code_blocks) {
|
|
2775
2791
|
events.push({ timestamp: block.timestamp, kind: "code", segmentIndex: seg.index, data: block });
|
|
2776
2792
|
}
|
|
2777
|
-
for (const
|
|
2778
|
-
events.push({ timestamp:
|
|
2793
|
+
for (const note2 of pass2.visual_notes) {
|
|
2794
|
+
events.push({ timestamp: note2.timestamp, kind: "visual", segmentIndex: seg.index, data: note2 });
|
|
2779
2795
|
}
|
|
2780
2796
|
}
|
|
2781
2797
|
if (events.length === 0) {
|
|
@@ -3521,21 +3537,31 @@ async function generateOutput(params) {
|
|
|
3521
3537
|
}
|
|
3522
3538
|
|
|
3523
3539
|
// src/core/shutdown.ts
|
|
3524
|
-
import { log as
|
|
3540
|
+
import { log as log7 } from "@clack/prompts";
|
|
3525
3541
|
function createShutdownHandler(params) {
|
|
3526
3542
|
const { client, uploadedFileNames } = params;
|
|
3527
3543
|
let shuttingDown = false;
|
|
3528
3544
|
let handler = null;
|
|
3545
|
+
let forceHandler = null;
|
|
3546
|
+
let progressCurrentStep = 0;
|
|
3547
|
+
let progressTotalSteps = 0;
|
|
3548
|
+
let hasProgress = false;
|
|
3529
3549
|
const sigintHandler = () => {
|
|
3530
3550
|
if (shuttingDown) {
|
|
3531
3551
|
process.exit(1);
|
|
3532
3552
|
return;
|
|
3533
3553
|
}
|
|
3534
3554
|
shuttingDown = true;
|
|
3535
|
-
|
|
3555
|
+
if (hasProgress) {
|
|
3556
|
+
log7.warn(`Interrupted \u2014 progress saved (${progressCurrentStep}/${progressTotalSteps} steps)`);
|
|
3557
|
+
log7.info(`Resume: vidistill ${params.source} -o ${params.outputDir}/`);
|
|
3558
|
+
} else {
|
|
3559
|
+
log7.warn("Interrupted");
|
|
3560
|
+
}
|
|
3536
3561
|
const forceExitHandler = () => {
|
|
3537
3562
|
process.exit(1);
|
|
3538
3563
|
};
|
|
3564
|
+
forceHandler = forceExitHandler;
|
|
3539
3565
|
process.once("SIGINT", forceExitHandler);
|
|
3540
3566
|
const cleanupAndExit = async () => {
|
|
3541
3567
|
for (const fileName of uploadedFileNames) {
|
|
@@ -3564,21 +3590,168 @@ function createShutdownHandler(params) {
|
|
|
3564
3590
|
process.removeListener("SIGINT", handler);
|
|
3565
3591
|
handler = null;
|
|
3566
3592
|
}
|
|
3593
|
+
if (forceHandler !== null) {
|
|
3594
|
+
process.removeListener("SIGINT", forceHandler);
|
|
3595
|
+
forceHandler = null;
|
|
3596
|
+
}
|
|
3597
|
+
},
|
|
3598
|
+
setProgress(currentStep, totalSteps) {
|
|
3599
|
+
progressCurrentStep = currentStep;
|
|
3600
|
+
progressTotalSteps = totalSteps;
|
|
3601
|
+
hasProgress = true;
|
|
3567
3602
|
}
|
|
3568
3603
|
};
|
|
3569
3604
|
}
|
|
3570
3605
|
|
|
3606
|
+
// src/commands/distill.ts
|
|
3607
|
+
async function runDistill(args) {
|
|
3608
|
+
const apiKey = await resolveApiKey();
|
|
3609
|
+
let rawInput = args.input ?? await promptVideoSource();
|
|
3610
|
+
let context = args.context ?? await promptContext();
|
|
3611
|
+
const allFlagsProvided = args.input != null && args.context != null;
|
|
3612
|
+
if (!allFlagsProvided) {
|
|
3613
|
+
let confirmed = false;
|
|
3614
|
+
while (!confirmed) {
|
|
3615
|
+
showConfigBox({ input: rawInput, context, output: args.output });
|
|
3616
|
+
const choice = await promptConfirmation();
|
|
3617
|
+
switch (choice) {
|
|
3618
|
+
case "start":
|
|
3619
|
+
confirmed = true;
|
|
3620
|
+
break;
|
|
3621
|
+
case "edit-video":
|
|
3622
|
+
rawInput = await promptVideoSource();
|
|
3623
|
+
break;
|
|
3624
|
+
case "edit-context":
|
|
3625
|
+
context = await promptContext();
|
|
3626
|
+
break;
|
|
3627
|
+
case "cancel":
|
|
3628
|
+
cancel2("Cancelled.");
|
|
3629
|
+
process.exit(0);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
const resolved = resolveInput(rawInput);
|
|
3634
|
+
const client = new GeminiClient(apiKey);
|
|
3635
|
+
let fileUri;
|
|
3636
|
+
let mimeType;
|
|
3637
|
+
let duration;
|
|
3638
|
+
let videoTitle;
|
|
3639
|
+
let uploadedFileNames = [];
|
|
3640
|
+
if (resolved.type === "youtube") {
|
|
3641
|
+
const result = await handleYouTube(resolved.value, client);
|
|
3642
|
+
fileUri = result.fileUri;
|
|
3643
|
+
mimeType = result.mimeType;
|
|
3644
|
+
duration = await detectDuration({
|
|
3645
|
+
ytDlpDuration: result.duration,
|
|
3646
|
+
geminiDuration: result.duration
|
|
3647
|
+
});
|
|
3648
|
+
if (result.uploadedFileName != null) {
|
|
3649
|
+
uploadedFileNames = [result.uploadedFileName];
|
|
3650
|
+
}
|
|
3651
|
+
const videoId = extractVideoId(resolved.value);
|
|
3652
|
+
videoTitle = videoId != null ? `youtube-${videoId}` : resolved.value;
|
|
3653
|
+
} else {
|
|
3654
|
+
const result = await handleLocalFile(resolved.value, client);
|
|
3655
|
+
fileUri = result.fileUri;
|
|
3656
|
+
mimeType = result.mimeType;
|
|
3657
|
+
duration = await detectDuration({
|
|
3658
|
+
filePath: resolved.value,
|
|
3659
|
+
geminiDuration: result.duration
|
|
3660
|
+
});
|
|
3661
|
+
if (result.uploadedFileName != null) {
|
|
3662
|
+
uploadedFileNames = [result.uploadedFileName];
|
|
3663
|
+
}
|
|
3664
|
+
videoTitle = basename3(resolved.value, extname2(resolved.value));
|
|
3665
|
+
}
|
|
3666
|
+
const model = MODELS.flash;
|
|
3667
|
+
const outputDir = resolve(args.output);
|
|
3668
|
+
const slug = slugify(videoTitle);
|
|
3669
|
+
const finalOutputDir = `${outputDir}/${slug}`;
|
|
3670
|
+
const shutdownHandler = createShutdownHandler({
|
|
3671
|
+
client,
|
|
3672
|
+
uploadedFileNames,
|
|
3673
|
+
outputDir,
|
|
3674
|
+
videoTitle,
|
|
3675
|
+
source: rawInput,
|
|
3676
|
+
duration,
|
|
3677
|
+
model
|
|
3678
|
+
});
|
|
3679
|
+
shutdownHandler.register();
|
|
3680
|
+
const rateLimiter = new RateLimiter();
|
|
3681
|
+
const progress2 = createProgressDisplay();
|
|
3682
|
+
const startTime = Date.now();
|
|
3683
|
+
const pipelineResult = await runPipeline({
|
|
3684
|
+
client,
|
|
3685
|
+
fileUri,
|
|
3686
|
+
mimeType,
|
|
3687
|
+
duration,
|
|
3688
|
+
model,
|
|
3689
|
+
context,
|
|
3690
|
+
rateLimiter,
|
|
3691
|
+
onProgress: (status) => {
|
|
3692
|
+
progress2.update(status);
|
|
3693
|
+
if (status.currentStep != null && status.totalSteps != null) {
|
|
3694
|
+
shutdownHandler.setProgress(status.currentStep, status.totalSteps);
|
|
3695
|
+
}
|
|
3696
|
+
},
|
|
3697
|
+
onWait: (delayMs) => progress2.onWait(delayMs),
|
|
3698
|
+
isShuttingDown: () => shutdownHandler.isShuttingDown()
|
|
3699
|
+
});
|
|
3700
|
+
const elapsedMs = Date.now() - startTime;
|
|
3701
|
+
shutdownHandler.deregister();
|
|
3702
|
+
progress2.complete(pipelineResult, elapsedMs);
|
|
3703
|
+
if (pipelineResult.interrupted != null) {
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
const outputResult = await generateOutput({
|
|
3707
|
+
pipelineResult,
|
|
3708
|
+
outputDir,
|
|
3709
|
+
videoTitle,
|
|
3710
|
+
source: rawInput,
|
|
3711
|
+
duration,
|
|
3712
|
+
model,
|
|
3713
|
+
processingTimeMs: elapsedMs
|
|
3714
|
+
});
|
|
3715
|
+
const elapsedSecs = Math.round(elapsedMs / 1e3);
|
|
3716
|
+
const elapsedMins = Math.floor(elapsedSecs / 60);
|
|
3717
|
+
const remainSecs = elapsedSecs % 60;
|
|
3718
|
+
const elapsed = elapsedMins > 0 ? `${elapsedMins}m ${remainSecs}s` : `${remainSecs}s`;
|
|
3719
|
+
log8.success(`Done in ${elapsed}`);
|
|
3720
|
+
log8.info(`Output: ${finalOutputDir}/`);
|
|
3721
|
+
log8.info(pc4.dim("Open guide.md for an overview"));
|
|
3722
|
+
if (pipelineResult.codeReconstruction != null) {
|
|
3723
|
+
log8.info(pc4.dim("Tip: vidistill extract code <input> for code-only extraction next time"));
|
|
3724
|
+
} else if (pipelineResult.peopleExtraction?.participants != null && pipelineResult.peopleExtraction.participants.length > 1) {
|
|
3725
|
+
log8.info(pc4.dim("Tip: vidistill rename-speakers <dir> to assign real names"));
|
|
3726
|
+
} else {
|
|
3727
|
+
log8.info(pc4.dim('Tip: vidistill ask <dir> "your question" to query this video'));
|
|
3728
|
+
}
|
|
3729
|
+
if (outputResult.errors.length > 0) {
|
|
3730
|
+
log8.warn(`Output errors: ${pc4.yellow(String(outputResult.errors.length))}`);
|
|
3731
|
+
for (const err of outputResult.errors) {
|
|
3732
|
+
log8.warn(pc4.dim(` ${err}`));
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
// import("../commands/**/*.js") in src/cli/index.ts
|
|
3738
|
+
var globImport_commands_js = __glob({});
|
|
3739
|
+
|
|
3571
3740
|
// src/cli/index.ts
|
|
3741
|
+
var _require2 = createRequire2(import.meta.url);
|
|
3742
|
+
var { version } = _require2("../package.json");
|
|
3572
3743
|
var DEFAULT_OUTPUT = "./vidistill-output/";
|
|
3744
|
+
var SUBCOMMANDS = /* @__PURE__ */ new Set(["ask", "search", "extract", "mcp", "watch", "rename-speakers"]);
|
|
3573
3745
|
var main = defineCommand({
|
|
3574
3746
|
meta: {
|
|
3575
3747
|
name: "vidistill",
|
|
3576
|
-
|
|
3748
|
+
version,
|
|
3749
|
+
description: "Video Intelligence Distiller \u2014 turn video into structured notes\n\nCommands: ask, search, extract, mcp, watch, rename-speakers"
|
|
3577
3750
|
},
|
|
3578
3751
|
args: {
|
|
3579
3752
|
input: {
|
|
3580
3753
|
type: "positional",
|
|
3581
|
-
description: "YouTube URL
|
|
3754
|
+
description: "YouTube URL, local file path, or subcommand name",
|
|
3582
3755
|
required: false
|
|
3583
3756
|
},
|
|
3584
3757
|
context: {
|
|
@@ -3591,107 +3764,38 @@ var main = defineCommand({
|
|
|
3591
3764
|
description: `Output directory for generated notes (default: ${DEFAULT_OUTPUT})`,
|
|
3592
3765
|
alias: "o",
|
|
3593
3766
|
default: DEFAULT_OUTPUT
|
|
3767
|
+
},
|
|
3768
|
+
lang: {
|
|
3769
|
+
type: "string",
|
|
3770
|
+
description: "Output language",
|
|
3771
|
+
alias: "l"
|
|
3594
3772
|
}
|
|
3595
3773
|
},
|
|
3596
3774
|
async run({ args }) {
|
|
3597
3775
|
showLogo();
|
|
3598
3776
|
showIntro();
|
|
3777
|
+
const name = args.input;
|
|
3778
|
+
if (name != null && SUBCOMMANDS.has(name)) {
|
|
3779
|
+
const mod = await globImport_commands_js(`../commands/${name}.js`);
|
|
3780
|
+
if (typeof mod.run !== "function") {
|
|
3781
|
+
throw new Error(`Subcommand "${name}" does not export a run function`);
|
|
3782
|
+
}
|
|
3783
|
+
await mod.run(process.argv.slice(3));
|
|
3784
|
+
return;
|
|
3785
|
+
}
|
|
3599
3786
|
try {
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
const client = new GeminiClient(apiKey);
|
|
3606
|
-
let fileUri;
|
|
3607
|
-
let mimeType;
|
|
3608
|
-
let duration;
|
|
3609
|
-
let videoTitle;
|
|
3610
|
-
let uploadedFileNames = [];
|
|
3611
|
-
if (resolved.type === "youtube") {
|
|
3612
|
-
const result = await handleYouTube(resolved.value, client);
|
|
3613
|
-
fileUri = result.fileUri;
|
|
3614
|
-
mimeType = result.mimeType;
|
|
3615
|
-
duration = await detectDuration({
|
|
3616
|
-
ytDlpDuration: result.duration,
|
|
3617
|
-
geminiDuration: result.duration
|
|
3618
|
-
});
|
|
3619
|
-
if (result.uploadedFileName != null) {
|
|
3620
|
-
uploadedFileNames = [result.uploadedFileName];
|
|
3621
|
-
}
|
|
3622
|
-
const videoId = extractVideoId(resolved.value);
|
|
3623
|
-
videoTitle = videoId != null ? `youtube-${videoId}` : resolved.value;
|
|
3624
|
-
} else {
|
|
3625
|
-
const result = await handleLocalFile(resolved.value, client);
|
|
3626
|
-
fileUri = result.fileUri;
|
|
3627
|
-
mimeType = result.mimeType;
|
|
3628
|
-
duration = await detectDuration({
|
|
3629
|
-
filePath: resolved.value,
|
|
3630
|
-
geminiDuration: result.duration
|
|
3631
|
-
});
|
|
3632
|
-
if (result.uploadedFileName != null) {
|
|
3633
|
-
uploadedFileNames = [result.uploadedFileName];
|
|
3634
|
-
}
|
|
3635
|
-
videoTitle = basename3(resolved.value, extname2(resolved.value));
|
|
3636
|
-
}
|
|
3637
|
-
const mins = Math.floor(duration / 60);
|
|
3638
|
-
const secs = Math.round(duration % 60);
|
|
3639
|
-
log10.info(`Duration: ${pc5.cyan(`${mins}m ${secs}s`)} (${Math.round(duration)}s)`);
|
|
3640
|
-
const model = MODELS.flash;
|
|
3641
|
-
const outputDir = resolve(args.output);
|
|
3642
|
-
const slug = slugify(videoTitle);
|
|
3643
|
-
const finalOutputDir = `${outputDir}/${slug}`;
|
|
3644
|
-
const shutdownHandler = createShutdownHandler({
|
|
3645
|
-
client,
|
|
3646
|
-
uploadedFileNames,
|
|
3647
|
-
outputDir,
|
|
3648
|
-
videoTitle,
|
|
3649
|
-
source: rawInput,
|
|
3650
|
-
duration,
|
|
3651
|
-
model
|
|
3652
|
-
});
|
|
3653
|
-
shutdownHandler.register();
|
|
3654
|
-
const rateLimiter = new RateLimiter();
|
|
3655
|
-
const progress = createProgressDisplay();
|
|
3656
|
-
const startTime = Date.now();
|
|
3657
|
-
const pipelineResult = await runPipeline({
|
|
3658
|
-
client,
|
|
3659
|
-
fileUri,
|
|
3660
|
-
mimeType,
|
|
3661
|
-
duration,
|
|
3662
|
-
model,
|
|
3663
|
-
context,
|
|
3664
|
-
rateLimiter,
|
|
3665
|
-
onProgress: (status) => progress.update(status),
|
|
3666
|
-
onWait: (delayMs) => progress.onWait(delayMs),
|
|
3667
|
-
isShuttingDown: () => shutdownHandler.isShuttingDown()
|
|
3787
|
+
await runDistill({
|
|
3788
|
+
input: args.input,
|
|
3789
|
+
context: args.context,
|
|
3790
|
+
output: args.output,
|
|
3791
|
+
lang: args.lang
|
|
3668
3792
|
});
|
|
3669
|
-
const elapsedMs = Date.now() - startTime;
|
|
3670
|
-
shutdownHandler.deregister();
|
|
3671
|
-
progress.complete(pipelineResult, elapsedMs);
|
|
3672
|
-
const outputResult = await generateOutput({
|
|
3673
|
-
pipelineResult,
|
|
3674
|
-
outputDir,
|
|
3675
|
-
videoTitle,
|
|
3676
|
-
source: rawInput,
|
|
3677
|
-
duration,
|
|
3678
|
-
model,
|
|
3679
|
-
processingTimeMs: elapsedMs
|
|
3680
|
-
});
|
|
3681
|
-
const fileCount = outputResult.filesGenerated.length;
|
|
3682
|
-
log10.success(
|
|
3683
|
-
`Output: ${pc5.cyan(finalOutputDir + "/")} \u2014 ${pc5.cyan(String(fileCount))} files generated ${pc5.dim("(guide.md for overview)")}`
|
|
3684
|
-
);
|
|
3685
|
-
if (outputResult.errors.length > 0) {
|
|
3686
|
-
log10.warn(`Output errors: ${pc5.yellow(String(outputResult.errors.length))}`);
|
|
3687
|
-
for (const err of outputResult.errors) {
|
|
3688
|
-
log10.warn(pc5.dim(` ${err}`));
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
3793
|
} catch (err) {
|
|
3794
|
+
const { log: log9 } = await import("@clack/prompts");
|
|
3795
|
+
const { default: pc5 } = await import("picocolors");
|
|
3692
3796
|
const raw = err instanceof Error ? err.message : String(err);
|
|
3693
3797
|
const message = raw.split("\n")[0].slice(0, 200);
|
|
3694
|
-
|
|
3798
|
+
log9.error(pc5.red(message));
|
|
3695
3799
|
process.exit(1);
|
|
3696
3800
|
}
|
|
3697
3801
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vidistill",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@clack/prompts": "
|
|
32
|
+
"@clack/prompts": "1.0.1",
|
|
33
33
|
"@google/genai": "^1.40.0",
|
|
34
34
|
"citty": "^0.1.6",
|
|
35
35
|
"figlet": "^1.8.0",
|