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