vidistill 0.5.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +1 -3
  2. package/dist/index.js +208 -247
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -69,16 +69,14 @@ vidistill-output/my-video/
69
69
  ├── guide.md # overview and navigation
70
70
  ├── transcript.md # full timestamped transcript
71
71
  ├── combined.md # transcript + visual notes merged
72
- ├── notes.md # meeting/lecture notes
72
+ ├── notes.md # notes, implicit questions/decisions, recurring themes
73
73
  ├── code/ # extracted and reconstructed source files
74
74
  │ ├── *.ext # individual source files
75
75
  │ └── code-timeline.md # code evolution timeline
76
76
  ├── people.md # speakers and participants
77
77
  ├── chat.md # chat messages and links
78
78
  ├── action-items.md # tasks and follow-ups
79
- ├── insights.md # implicit signals and analysis
80
79
  ├── links.md # all URLs mentioned
81
- ├── prereqs.md # prerequisite knowledge (when detected)
82
80
  ├── timeline.html # interactive visual timeline
83
81
  ├── metadata.json # processing metadata
84
82
  └── raw/ # raw pass outputs
package/dist/index.js CHANGED
@@ -263,6 +263,7 @@ function showConfigBox(config) {
263
263
  const lines = [
264
264
  `Video: ${config.input}`,
265
265
  `Context: ${config.context ?? "(none)"}`,
266
+ `Name: ${config.outputName ?? "(auto-detect)"}`,
266
267
  `Output: ${config.output}`
267
268
  ];
268
269
  if (config.videoType === "audio") {
@@ -276,7 +277,7 @@ function showConfigBox(config) {
276
277
  }
277
278
 
278
279
  // src/commands/distill.ts
279
- import { log as log8, cancel as cancel2 } from "@clack/prompts";
280
+ import { log as log7, cancel as cancel2 } from "@clack/prompts";
280
281
  import pc3 from "picocolors";
281
282
  import { basename as basename3, extname as extname2, resolve, join as join4 } from "path";
282
283
  import { existsSync as existsSync3, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
@@ -312,6 +313,15 @@ async function promptContext() {
312
313
  const result = value.trim();
313
314
  return result.length > 0 ? result : void 0;
314
315
  }
316
+ async function promptOutputName() {
317
+ const value = await text({
318
+ message: "Output folder name",
319
+ placeholder: "(press Enter to auto-detect from source)"
320
+ });
321
+ handleCancel(value);
322
+ const result = value.trim();
323
+ return result.length > 0 ? result : void 0;
324
+ }
315
325
  async function promptApiKey() {
316
326
  const value = await password({
317
327
  message: "Gemini API key",
@@ -339,6 +349,7 @@ async function promptConfirmation() {
339
349
  { value: "start", label: "Start processing" },
340
350
  { value: "edit-video", label: "Edit video source" },
341
351
  { value: "edit-context", label: "Edit context" },
352
+ { value: "edit-name", label: "Edit output name" },
342
353
  { value: "cancel", label: "Cancel" }
343
354
  ]
344
355
  });
@@ -657,8 +668,27 @@ function fetchYtDlpDuration(url) {
657
668
  });
658
669
  });
659
670
  }
671
+ async function fetchYouTubePageDuration(url) {
672
+ try {
673
+ const normalized = normalizeYouTubeUrl(url);
674
+ if (!normalized) return void 0;
675
+ const res = await fetch(normalized, {
676
+ headers: { "Accept-Language": "en-US,en;q=0.9" }
677
+ });
678
+ if (!res.ok) return void 0;
679
+ const html = await res.text();
680
+ const match = html.match(/"lengthSeconds"\s*:\s*"(\d+)"/);
681
+ if (match?.[1]) {
682
+ const seconds = parseInt(match[1], 10);
683
+ if (seconds > 0) return seconds;
684
+ }
685
+ return void 0;
686
+ } catch {
687
+ return void 0;
688
+ }
689
+ }
660
690
  async function handleYouTube(url, _client) {
661
- const duration = await fetchYtDlpDuration(url);
691
+ const duration = await fetchYtDlpDuration(url) ?? await fetchYouTubePageDuration(url);
662
692
  return { fileUri: url, mimeType: "video/mp4", source: "direct", duration };
663
693
  }
664
694
 
@@ -933,7 +963,7 @@ async function detectDuration(source) {
933
963
  }
934
964
 
935
965
  // src/core/pipeline.ts
936
- import { log as log6 } from "@clack/prompts";
966
+ import { log as log5 } from "@clack/prompts";
937
967
 
938
968
  // src/gemini/schemas.ts
939
969
  import { Type } from "@google/genai";
@@ -1759,9 +1789,6 @@ function mergeTranscriptResults(pass1a, pass1b) {
1759
1789
  };
1760
1790
  }
1761
1791
 
1762
- // src/core/transcript-consensus.ts
1763
- import { log as log5 } from "@clack/prompts";
1764
-
1765
1792
  // src/core/consensus.ts
1766
1793
  function tokenize(content) {
1767
1794
  const tokens = content.match(/[\p{L}\p{N}_]+/gu) ?? [];
@@ -2084,9 +2111,7 @@ async function runDiarizationConsensus(params) {
2084
2111
  try {
2085
2112
  const result = await runFn();
2086
2113
  successfulRuns.push(result);
2087
- } catch (e) {
2088
- const msg = e instanceof Error ? e.message : String(e);
2089
- log5.warn(`diarization consensus run ${i + 1}/${runs} failed: ${msg}`);
2114
+ } catch {
2090
2115
  }
2091
2116
  onProgress?.(i + 1, runs);
2092
2117
  }
@@ -2137,9 +2162,7 @@ async function runTranscriptionConsensus(params) {
2137
2162
  try {
2138
2163
  const result = await runFn();
2139
2164
  successfulRuns.push(result);
2140
- } catch (e) {
2141
- const msg = e instanceof Error ? e.message : String(e);
2142
- log5.warn(`transcription consensus run ${i + 1}/${runs} failed: ${msg}`);
2165
+ } catch {
2143
2166
  }
2144
2167
  onProgress?.(i + 1, runs);
2145
2168
  }
@@ -2984,7 +3007,7 @@ async function runPipeline(config) {
2984
3007
  "pass0"
2985
3008
  );
2986
3009
  if (pass0Attempt.error !== null) {
2987
- log6.warn(pass0Attempt.error);
3010
+ log5.warn(pass0Attempt.error);
2988
3011
  errors.push(pass0Attempt.error);
2989
3012
  videoProfile = DEFAULT_PROFILE;
2990
3013
  } else {
@@ -3031,7 +3054,7 @@ async function runPipeline(config) {
3031
3054
  const pass1aResult = transcriptConsensusResult.result;
3032
3055
  if (pass1aResult === null) {
3033
3056
  const errMsg = `segment ${i} pass1a: all transcription consensus runs failed`;
3034
- log6.warn(errMsg);
3057
+ log5.warn(errMsg);
3035
3058
  errors.push(errMsg);
3036
3059
  }
3037
3060
  let pass1 = null;
@@ -3049,7 +3072,7 @@ async function runPipeline(config) {
3049
3072
  });
3050
3073
  if (pass1bResult === null) {
3051
3074
  const errMsg = `segment ${i} pass1b: all diarization consensus runs failed`;
3052
- log6.warn(errMsg);
3075
+ log5.warn(errMsg);
3053
3076
  errors.push(errMsg);
3054
3077
  pass1 = mergeTranscriptResults(pass1aResult, { speaker_assignments: [], speaker_summary: [] });
3055
3078
  } else {
@@ -3081,7 +3104,7 @@ async function runPipeline(config) {
3081
3104
  `segment ${i} pass2`
3082
3105
  );
3083
3106
  if (pass2Attempt.error !== null) {
3084
- log6.warn(pass2Attempt.error);
3107
+ log5.warn(pass2Attempt.error);
3085
3108
  errors.push(pass2Attempt.error);
3086
3109
  } else {
3087
3110
  pass2 = pass2Attempt.result;
@@ -3114,7 +3137,7 @@ async function runPipeline(config) {
3114
3137
  });
3115
3138
  if (linkConsensusResult.runsCompleted === 0) {
3116
3139
  const errMsg = `segment ${i} pass3c: all link consensus runs failed`;
3117
- log6.warn(errMsg);
3140
+ log5.warn(errMsg);
3118
3141
  errors.push(errMsg);
3119
3142
  pass3c = null;
3120
3143
  } else {
@@ -3143,7 +3166,7 @@ async function runPipeline(config) {
3143
3166
  `segment ${i} pass3d`
3144
3167
  );
3145
3168
  if (pass3dAttempt.error !== null) {
3146
- log6.warn(pass3dAttempt.error);
3169
+ log5.warn(pass3dAttempt.error);
3147
3170
  errors.push(pass3dAttempt.error);
3148
3171
  pass3d = null;
3149
3172
  } else {
@@ -3201,7 +3224,7 @@ async function runPipeline(config) {
3201
3224
  }
3202
3225
  } catch (e) {
3203
3226
  const msg = e instanceof Error ? e.message : String(e);
3204
- log6.warn(`speaker reconciliation failed, continuing with original labels: ${msg}`);
3227
+ log5.warn(`speaker reconciliation failed, continuing with original labels: ${msg}`);
3205
3228
  }
3206
3229
  let peopleExtraction = null;
3207
3230
  if (strategy.passes.includes("people")) {
@@ -3222,7 +3245,7 @@ async function runPipeline(config) {
3222
3245
  "pass3b"
3223
3246
  );
3224
3247
  if (pass3bAttempt.error !== null) {
3225
- log6.warn(pass3bAttempt.error);
3248
+ log5.warn(pass3bAttempt.error);
3226
3249
  errors.push(pass3bAttempt.error);
3227
3250
  } else {
3228
3251
  peopleExtraction = pass3bAttempt.result;
@@ -3260,7 +3283,7 @@ async function runPipeline(config) {
3260
3283
  });
3261
3284
  if (consensusResult.runsCompleted === 0) {
3262
3285
  const errMsg = "pass3a: all consensus runs failed";
3263
- log6.warn(errMsg);
3286
+ log5.warn(errMsg);
3264
3287
  errors.push(errMsg);
3265
3288
  } else {
3266
3289
  const validationResult = validateCodeReconstruction({
@@ -3299,7 +3322,7 @@ async function runPipeline(config) {
3299
3322
  "synthesis"
3300
3323
  );
3301
3324
  if (synthAttempt.error !== null) {
3302
- log6.warn(synthAttempt.error);
3325
+ log5.warn(synthAttempt.error);
3303
3326
  errors.push(synthAttempt.error);
3304
3327
  } else {
3305
3328
  synthesisResult = synthAttempt.result ?? void 0;
@@ -3370,6 +3393,40 @@ function renderProcessingDetails(pipelineResult, speakerMapping) {
3370
3393
  lines.push(`- **Segments processed:** ${pipelineResult.segments.length}`);
3371
3394
  return lines.join("\n");
3372
3395
  }
3396
+ var PREREQ_LEVEL_ORDER = ["advanced", "intermediate", "basic"];
3397
+ var PREREQ_LEVEL_LABELS = {
3398
+ advanced: "Advanced",
3399
+ intermediate: "Intermediate",
3400
+ basic: "Basic"
3401
+ };
3402
+ function renderPrerequisites(prerequisites) {
3403
+ if (prerequisites == null || prerequisites.length === 0) return "";
3404
+ const grouped = /* @__PURE__ */ new Map();
3405
+ for (const level of PREREQ_LEVEL_ORDER) {
3406
+ grouped.set(level, []);
3407
+ }
3408
+ for (const c of prerequisites) {
3409
+ const bucket = grouped.get(c.assumed_knowledge_level);
3410
+ if (bucket != null) {
3411
+ bucket.push(c);
3412
+ }
3413
+ }
3414
+ const lines = ["", "## Prerequisites", ""];
3415
+ for (const level of PREREQ_LEVEL_ORDER) {
3416
+ const concepts = grouped.get(level) ?? [];
3417
+ if (concepts.length === 0) continue;
3418
+ lines.push(`### ${PREREQ_LEVEL_LABELS[level]} Knowledge`, "");
3419
+ for (const c of concepts) {
3420
+ lines.push(`**${c.concept}**`);
3421
+ lines.push("");
3422
+ lines.push(c.brief_explanation);
3423
+ lines.push("");
3424
+ lines.push(`_First assumed at: ${c.timestamp_first_assumed}_`);
3425
+ lines.push("");
3426
+ }
3427
+ }
3428
+ return lines.join("\n");
3429
+ }
3373
3430
  function renderIncompletePasses(pipelineResult) {
3374
3431
  const { errors, interrupted } = pipelineResult;
3375
3432
  const hasErrors = errors.length > 0;
@@ -3415,7 +3472,7 @@ function writeGuide(params) {
3415
3472
  "## Suggestions",
3416
3473
  "",
3417
3474
  renderSuggestions(synthesisResult, speakerMapping),
3418
- "",
3475
+ renderPrerequisites(synthesisResult?.prerequisites),
3419
3476
  "## Processing Details",
3420
3477
  "",
3421
3478
  renderProcessingDetails(pipelineResult, speakerMapping),
@@ -3706,13 +3763,79 @@ function renderActionItems(items, speakerMapping) {
3706
3763
  lines.push("");
3707
3764
  return lines;
3708
3765
  }
3766
+ function collectImplicitQuestions(segments) {
3767
+ const questions = [];
3768
+ for (const seg of segments) {
3769
+ if (seg.pass3d != null) {
3770
+ questions.push(...seg.pass3d.questions_implicit);
3771
+ }
3772
+ }
3773
+ return questions;
3774
+ }
3775
+ function collectImplicitDecisions(segments) {
3776
+ const decisions = [];
3777
+ for (const seg of segments) {
3778
+ if (seg.pass3d != null) {
3779
+ decisions.push(...seg.pass3d.decisions_implicit);
3780
+ }
3781
+ }
3782
+ return decisions;
3783
+ }
3784
+ function collectEmphasisPatterns(segments) {
3785
+ const patterns = [];
3786
+ for (const seg of segments) {
3787
+ if (seg.pass3d != null) {
3788
+ patterns.push(...seg.pass3d.emphasis_patterns);
3789
+ }
3790
+ }
3791
+ return patterns;
3792
+ }
3793
+ function renderImplicitQuestions(questions, speakerMapping) {
3794
+ if (questions.length === 0) return [];
3795
+ const lines = ["## Implicit Questions", ""];
3796
+ for (const q of questions) {
3797
+ lines.push(`- ${replaceNamesInText(q, speakerMapping)}`);
3798
+ }
3799
+ lines.push("");
3800
+ return lines;
3801
+ }
3802
+ function renderImplicitDecisions(decisions, speakerMapping) {
3803
+ if (decisions.length === 0) return [];
3804
+ const lines = ["## Implicit Decisions", ""];
3805
+ for (const d of decisions) {
3806
+ lines.push(`- ${replaceNamesInText(d, speakerMapping)}`);
3807
+ }
3808
+ lines.push("");
3809
+ return lines;
3810
+ }
3811
+ function renderRecurringThemes(patterns, speakerMapping) {
3812
+ if (patterns.length === 0) return [];
3813
+ const sorted = [...patterns].sort((a, b) => b.times_mentioned - a.times_mentioned);
3814
+ const lines = ["## Recurring Themes", ""];
3815
+ for (const p of sorted) {
3816
+ const ts = p.timestamps.length > 0 ? ` _(${p.timestamps.join(", ")})_` : "";
3817
+ lines.push(`### ${p.concept} (\xD7${p.times_mentioned})${ts}`);
3818
+ lines.push("");
3819
+ if (p.significance.length > 0) {
3820
+ lines.push(replaceNamesInText(p.significance, speakerMapping));
3821
+ lines.push("");
3822
+ }
3823
+ }
3824
+ return lines;
3825
+ }
3709
3826
  function hasMeaningfulContent(s) {
3710
3827
  return s.key_concepts.length > 0 || s.key_decisions.length > 0 || s.topics.length > 0 || s.questions_raised.length > 0 || s.action_items.length > 0;
3711
3828
  }
3829
+ function hasPass3dContent(segments) {
3830
+ return segments.some(
3831
+ (s) => s.pass3d != null && (s.pass3d.questions_implicit.length > 0 || s.pass3d.decisions_implicit.length > 0 || s.pass3d.emphasis_patterns.length > 0)
3832
+ );
3833
+ }
3712
3834
  function writeNotes(params) {
3713
- const { synthesisResult, speakerMapping } = params;
3835
+ const { synthesisResult, segments, speakerMapping } = params;
3714
3836
  if (synthesisResult == null) return null;
3715
- if (!hasMeaningfulContent(synthesisResult)) return null;
3837
+ const hasPass3d = segments != null && hasPass3dContent(segments);
3838
+ if (!hasMeaningfulContent(synthesisResult) && !hasPass3d) return null;
3716
3839
  const sections = ["# Notes", ""];
3717
3840
  if (synthesisResult.overview.length > 0) {
3718
3841
  sections.push(replaceNamesInText(synthesisResult.overview, speakerMapping));
@@ -3723,6 +3846,11 @@ function writeNotes(params) {
3723
3846
  sections.push(...renderTopics(synthesisResult.topics, speakerMapping));
3724
3847
  sections.push(...renderQuestions(synthesisResult.questions_raised, speakerMapping));
3725
3848
  sections.push(...renderActionItems(synthesisResult.action_items, speakerMapping));
3849
+ if (segments != null) {
3850
+ sections.push(...renderImplicitQuestions(collectImplicitQuestions(segments), speakerMapping));
3851
+ sections.push(...renderImplicitDecisions(collectImplicitDecisions(segments), speakerMapping));
3852
+ sections.push(...renderRecurringThemes(collectEmphasisPatterns(segments), speakerMapping));
3853
+ }
3726
3854
  while (sections[sections.length - 1] === "") sections.pop();
3727
3855
  return sections.join("\n");
3728
3856
  }
@@ -4049,150 +4177,6 @@ function writeActionItems(params) {
4049
4177
  return sections.join("\n");
4050
4178
  }
4051
4179
 
4052
- // src/output/insights.ts
4053
- function collectEmotionalShifts(segments) {
4054
- const shifts = [];
4055
- for (const seg of segments) {
4056
- if (seg.pass3d != null) {
4057
- shifts.push(...seg.pass3d.emotional_shifts);
4058
- }
4059
- }
4060
- return shifts;
4061
- }
4062
- function collectEmphasisPatterns(segments) {
4063
- const patterns = [];
4064
- for (const seg of segments) {
4065
- if (seg.pass3d != null) {
4066
- patterns.push(...seg.pass3d.emphasis_patterns);
4067
- }
4068
- }
4069
- return patterns;
4070
- }
4071
- function collectImplicitQuestions(segments) {
4072
- const questions = [];
4073
- for (const seg of segments) {
4074
- if (seg.pass3d != null) {
4075
- questions.push(...seg.pass3d.questions_implicit);
4076
- }
4077
- }
4078
- return questions;
4079
- }
4080
- function collectImplicitDecisions(segments) {
4081
- const decisions = [];
4082
- for (const seg of segments) {
4083
- if (seg.pass3d != null) {
4084
- decisions.push(...seg.pass3d.decisions_implicit);
4085
- }
4086
- }
4087
- return decisions;
4088
- }
4089
- function renderEmotionalShifts(shifts, speakerMapping) {
4090
- if (shifts.length === 0) return [];
4091
- const lines = ["## Emotional Shifts", ""];
4092
- for (const s of shifts) {
4093
- lines.push(`- **[${s.timestamp}]** ${s.from_state} \u2192 ${s.to_state}`);
4094
- if (s.trigger.length > 0) {
4095
- lines.push(` _Trigger: ${replaceNamesInText(s.trigger, speakerMapping)}_`);
4096
- }
4097
- }
4098
- lines.push("");
4099
- return lines;
4100
- }
4101
- function renderEmphasisPatterns(patterns, speakerMapping) {
4102
- if (patterns.length === 0) return [];
4103
- const sorted = [...patterns].sort((a, b) => b.times_mentioned - a.times_mentioned);
4104
- const lines = ["## Emphasis Patterns", ""];
4105
- for (const p of sorted) {
4106
- const ts = p.timestamps.length > 0 ? ` _(${p.timestamps.join(", ")})_` : "";
4107
- lines.push(`### ${p.concept} (\xD7${p.times_mentioned})${ts}`);
4108
- lines.push("");
4109
- if (p.significance.length > 0) {
4110
- lines.push(replaceNamesInText(p.significance, speakerMapping));
4111
- lines.push("");
4112
- }
4113
- }
4114
- return lines;
4115
- }
4116
- function renderImplicitQuestions(questions, speakerMapping) {
4117
- if (questions.length === 0) return [];
4118
- const lines = ["## Implicit Questions", ""];
4119
- for (const q of questions) {
4120
- lines.push(`- ${replaceNamesInText(q, speakerMapping)}`);
4121
- }
4122
- lines.push("");
4123
- return lines;
4124
- }
4125
- function renderImplicitDecisions(decisions, speakerMapping) {
4126
- if (decisions.length === 0) return [];
4127
- const lines = ["## Implicit Decisions", ""];
4128
- for (const d of decisions) {
4129
- lines.push(`- ${replaceNamesInText(d, speakerMapping)}`);
4130
- }
4131
- lines.push("");
4132
- return lines;
4133
- }
4134
- function writeInsights(params) {
4135
- const { segments, speakerMapping } = params;
4136
- const hasPass3d = segments.some((s) => s.pass3d != null);
4137
- if (!hasPass3d) return null;
4138
- const emotionalShifts = collectEmotionalShifts(segments);
4139
- const emphasisPatterns = collectEmphasisPatterns(segments);
4140
- const implicitQuestions = collectImplicitQuestions(segments);
4141
- const implicitDecisions = collectImplicitDecisions(segments);
4142
- if (emotionalShifts.length === 0 && emphasisPatterns.length === 0 && implicitQuestions.length === 0 && implicitDecisions.length === 0) {
4143
- return null;
4144
- }
4145
- const sections = ["# Insights", ""];
4146
- sections.push(...renderEmotionalShifts(emotionalShifts, speakerMapping));
4147
- sections.push(...renderEmphasisPatterns(emphasisPatterns, speakerMapping));
4148
- sections.push(...renderImplicitQuestions(implicitQuestions, speakerMapping));
4149
- sections.push(...renderImplicitDecisions(implicitDecisions, speakerMapping));
4150
- while (sections[sections.length - 1] === "") sections.pop();
4151
- return sections.join("\n");
4152
- }
4153
-
4154
- // src/output/prereqs.ts
4155
- var LEVEL_ORDER = ["advanced", "intermediate", "basic"];
4156
- var LEVEL_LABELS = {
4157
- advanced: "Advanced",
4158
- intermediate: "Intermediate",
4159
- basic: "Basic"
4160
- };
4161
- function renderLevelSection(level, concepts) {
4162
- if (concepts.length === 0) return [];
4163
- const lines = [`## ${LEVEL_LABELS[level]} Knowledge`, ""];
4164
- for (const c of concepts) {
4165
- lines.push(`### ${c.concept}`);
4166
- lines.push("");
4167
- lines.push(c.brief_explanation);
4168
- lines.push("");
4169
- lines.push(`_First assumed at: ${c.timestamp_first_assumed}_`);
4170
- lines.push("");
4171
- }
4172
- return lines;
4173
- }
4174
- function writePrereqs(params) {
4175
- const { prerequisites } = params;
4176
- if (prerequisites == null || prerequisites.length === 0) return null;
4177
- const grouped = /* @__PURE__ */ new Map();
4178
- for (const level of LEVEL_ORDER) {
4179
- grouped.set(level, []);
4180
- }
4181
- for (const c of prerequisites) {
4182
- const bucket = grouped.get(c.assumed_knowledge_level);
4183
- if (bucket != null) {
4184
- bucket.push(c);
4185
- }
4186
- }
4187
- const sections = ["# Prerequisites", ""];
4188
- for (const level of LEVEL_ORDER) {
4189
- const concepts = grouped.get(level) ?? [];
4190
- sections.push(...renderLevelSection(level, concepts));
4191
- }
4192
- while (sections[sections.length - 1] === "") sections.pop();
4193
- return sections.join("\n");
4194
- }
4195
-
4196
4180
  // src/output/timeline.ts
4197
4181
  function toPercent(seconds, duration) {
4198
4182
  if (duration <= 0) return "0";
@@ -4682,13 +4666,9 @@ function resolveFilesToGenerate(params) {
4682
4666
  }
4683
4667
  if (hasPass3d) {
4684
4668
  optional.add("action-items.md");
4685
- optional.add("insights.md");
4686
4669
  }
4687
- if (synthesisResult != null) optional.add("notes.md");
4670
+ if (synthesisResult != null || hasPass3d) optional.add("notes.md");
4688
4671
  if (peopleExtraction != null) optional.add("people.md");
4689
- if (synthesisResult?.prerequisites != null && Array.isArray(synthesisResult.prerequisites) && synthesisResult.prerequisites.length > 0) {
4690
- optional.add("prereqs.md");
4691
- }
4692
4672
  const hasPass1 = segments.some((s) => s.pass1 != null);
4693
4673
  if (hasPass1 || hasPass2) optional.add("timeline.html");
4694
4674
  return optional;
@@ -4747,7 +4727,7 @@ async function generateOutput(params) {
4747
4727
  }
4748
4728
  if (filesToGenerate.has("notes.md")) {
4749
4729
  try {
4750
- const content = writeNotes({ synthesisResult: pipelineResult.synthesisResult, speakerMapping: expandedMapping });
4730
+ const content = writeNotes({ synthesisResult: pipelineResult.synthesisResult, segments: pipelineResult.segments, speakerMapping: expandedMapping });
4751
4731
  if (content != null) {
4752
4732
  await writeOutputFile("notes.md", content);
4753
4733
  }
@@ -4799,26 +4779,6 @@ async function generateOutput(params) {
4799
4779
  errors.push(`action-items.md: ${String(err)}`);
4800
4780
  }
4801
4781
  }
4802
- if (filesToGenerate.has("insights.md")) {
4803
- try {
4804
- const content = writeInsights({ segments: pipelineResult.segments, speakerMapping: expandedMapping });
4805
- if (content != null) {
4806
- await writeOutputFile("insights.md", content);
4807
- }
4808
- } catch (err) {
4809
- errors.push(`insights.md: ${String(err)}`);
4810
- }
4811
- }
4812
- if (filesToGenerate.has("prereqs.md")) {
4813
- try {
4814
- const content = writePrereqs({ prerequisites: pipelineResult.synthesisResult?.prerequisites });
4815
- if (content != null) {
4816
- await writeOutputFile("prereqs.md", content);
4817
- }
4818
- } catch (err) {
4819
- errors.push(`prereqs.md: ${String(err)}`);
4820
- }
4821
- }
4822
4782
  if (filesToGenerate.has("timeline.html")) {
4823
4783
  try {
4824
4784
  const content = generateTimeline({ pipelineResult, duration, speakerMapping: expandedMapping });
@@ -4935,7 +4895,7 @@ async function reRenderWithSpeakerMapping(params) {
4935
4895
  }
4936
4896
  if (filesToReRender.has("notes.md")) {
4937
4897
  try {
4938
- const content = writeNotes({ synthesisResult: pipelineResult.synthesisResult, speakerMapping: expandedMapping });
4898
+ const content = writeNotes({ synthesisResult: pipelineResult.synthesisResult, segments: pipelineResult.segments, speakerMapping: expandedMapping });
4939
4899
  if (content != null) await writeOutputFile("notes.md", content);
4940
4900
  } catch (err) {
4941
4901
  errors.push(`notes.md: ${String(err)}`);
@@ -4969,14 +4929,6 @@ async function reRenderWithSpeakerMapping(params) {
4969
4929
  errors.push(`action-items.md: ${String(err)}`);
4970
4930
  }
4971
4931
  }
4972
- if (filesToReRender.has("insights.md")) {
4973
- try {
4974
- const content = writeInsights({ segments: pipelineResult.segments, speakerMapping: expandedMapping });
4975
- if (content != null) await writeOutputFile("insights.md", content);
4976
- } catch (err) {
4977
- errors.push(`insights.md: ${String(err)}`);
4978
- }
4979
- }
4980
4932
  if (filesToReRender.has("timeline.html")) {
4981
4933
  try {
4982
4934
  const content = generateTimeline({ pipelineResult, duration, speakerMapping: expandedMapping });
@@ -5017,7 +4969,7 @@ async function reRenderWithSpeakerMapping(params) {
5017
4969
  }
5018
4970
 
5019
4971
  // src/core/shutdown.ts
5020
- import { log as log7 } from "@clack/prompts";
4972
+ import { log as log6 } from "@clack/prompts";
5021
4973
  function createShutdownHandler(params) {
5022
4974
  const { client, uploadedFileNames } = params;
5023
4975
  let shuttingDown = false;
@@ -5033,10 +4985,10 @@ function createShutdownHandler(params) {
5033
4985
  }
5034
4986
  shuttingDown = true;
5035
4987
  if (hasProgress) {
5036
- log7.warn(`Interrupted \u2014 progress saved (${progressCurrentStep}/${progressTotalSteps} steps)`);
5037
- log7.info(`Resume: vidistill ${params.source} -o ${params.outputDir}/`);
4988
+ log6.warn(`Interrupted \u2014 progress saved (${progressCurrentStep}/${progressTotalSteps} steps)`);
4989
+ log6.info(`Resume: vidistill ${params.source} -o ${params.outputDir}/`);
5038
4990
  } else {
5039
- log7.warn("Interrupted");
4991
+ log6.warn("Interrupted");
5040
4992
  }
5041
4993
  const forceExitHandler = () => {
5042
4994
  process.exit(1);
@@ -5121,6 +5073,7 @@ async function runDistill(args) {
5121
5073
  const apiKey = await resolveApiKey();
5122
5074
  let rawInput = args.input ?? await promptVideoSource();
5123
5075
  let context = args.context ?? await promptContext();
5076
+ let outputName = await promptOutputName();
5124
5077
  const allFlagsProvided = args.input != null && args.context != null;
5125
5078
  if (!allFlagsProvided) {
5126
5079
  let confirmed = false;
@@ -5131,6 +5084,7 @@ async function runDistill(args) {
5131
5084
  input: rawInput,
5132
5085
  context,
5133
5086
  output: args.output,
5087
+ outputName,
5134
5088
  videoType: inputIsAudio ? "audio" : void 0,
5135
5089
  lang: args.lang
5136
5090
  });
@@ -5145,6 +5099,9 @@ async function runDistill(args) {
5145
5099
  case "edit-context":
5146
5100
  context = await promptContext();
5147
5101
  break;
5102
+ case "edit-name":
5103
+ outputName = await promptOutputName();
5104
+ break;
5148
5105
  case "cancel":
5149
5106
  cancel2("Cancelled.");
5150
5107
  process.exit(0);
@@ -5169,6 +5126,7 @@ async function runDistill(args) {
5169
5126
  });
5170
5127
  } catch {
5171
5128
  duration = 600;
5129
+ log7.warn("Could not detect video duration \u2014 defaulting to 10 minutes. Install yt-dlp for full-length processing: brew install yt-dlp");
5172
5130
  }
5173
5131
  if (result.uploadedFileName != null) {
5174
5132
  uploadedFileNames = [result.uploadedFileName];
@@ -5188,6 +5146,9 @@ async function runDistill(args) {
5188
5146
  }
5189
5147
  videoTitle = basename3(resolved.value, extname2(resolved.value));
5190
5148
  }
5149
+ if (outputName != null) {
5150
+ videoTitle = outputName;
5151
+ }
5191
5152
  const model = MODELS.flash;
5192
5153
  const outputDir = resolve(args.output);
5193
5154
  const slug = slugify(videoTitle);
@@ -5245,32 +5206,32 @@ async function runDistill(args) {
5245
5206
  const elapsedMins = Math.floor(elapsedSecs / 60);
5246
5207
  const remainSecs = elapsedSecs % 60;
5247
5208
  const elapsed = elapsedMins > 0 ? `${elapsedMins}m ${remainSecs}s` : `${remainSecs}s`;
5248
- log8.success(`Done in ${elapsed}`);
5249
- log8.info(`Output: ${finalOutputDir}/`);
5250
- log8.info(pc3.dim("Open guide.md for an overview"));
5209
+ log7.success(`Done in ${elapsed}`);
5210
+ log7.info(`Output: ${finalOutputDir}/`);
5211
+ log7.info(pc3.dim("Open guide.md for an overview"));
5251
5212
  if (pipelineResult.peopleExtraction?.participants != null && pipelineResult.peopleExtraction.participants.length > 1) {
5252
- log8.info(pc3.dim("Tip: vidistill rename-speakers <dir> to assign real names"));
5213
+ log7.info(pc3.dim("Tip: vidistill rename-speakers <dir> to assign real names"));
5253
5214
  }
5254
5215
  if (outputResult.errors.length > 0) {
5255
- log8.warn(`Output errors: ${pc3.yellow(String(outputResult.errors.length))}`);
5216
+ log7.warn(`Output errors: ${pc3.yellow(String(outputResult.errors.length))}`);
5256
5217
  for (const err of outputResult.errors) {
5257
- log8.warn(pc3.dim(` ${err}`));
5218
+ log7.warn(pc3.dim(` ${err}`));
5258
5219
  }
5259
5220
  }
5260
5221
  }
5261
5222
 
5262
5223
  // src/commands/mcp.ts
5263
- import { log as log9 } from "@clack/prompts";
5224
+ import { log as log8 } from "@clack/prompts";
5264
5225
  async function run(_args) {
5265
- log9.info("Not implemented yet.");
5226
+ log8.info("Not implemented yet.");
5266
5227
  }
5267
5228
 
5268
5229
  // src/commands/rename-speakers.ts
5269
5230
  import { join as join5 } from "path";
5270
- import { log as log11, text as text3, isCancel as isCancel3, cancel as cancel4 } from "@clack/prompts";
5231
+ import { log as log10, text as text3, isCancel as isCancel3, cancel as cancel4 } from "@clack/prompts";
5271
5232
 
5272
5233
  // src/cli/speaker-naming.ts
5273
- import { log as log10, text as text2, confirm as confirm2, isCancel as isCancel2, cancel as cancel3 } from "@clack/prompts";
5234
+ import { log as log9, text as text2, confirm as confirm2, isCancel as isCancel2, cancel as cancel3 } from "@clack/prompts";
5274
5235
  async function detectAndPromptMerges(mapping) {
5275
5236
  const byName = /* @__PURE__ */ new Map();
5276
5237
  for (const [label, name] of Object.entries(mapping)) {
@@ -5429,19 +5390,19 @@ async function runList(outputDir) {
5429
5390
  const metadataPath = join5(outputDir, "metadata.json");
5430
5391
  const metadata = await readJsonFile(metadataPath);
5431
5392
  if (metadata == null) {
5432
- log11.error("Not a vidistill output directory");
5393
+ log10.error("Not a vidistill output directory");
5433
5394
  return;
5434
5395
  }
5435
5396
  const rawDir = join5(outputDir, "raw");
5436
5397
  const speakers = await collectSpeakersFromRaw(rawDir);
5437
5398
  const speakerMapping = metadata.speakerMapping ?? {};
5438
5399
  if (speakers.length === 0 && Object.keys(speakerMapping).length === 0) {
5439
- log11.info("No speakers found.");
5400
+ log10.info("No speakers found.");
5440
5401
  return;
5441
5402
  }
5442
5403
  const groups = groupSpeakersByExistingMapping(speakers, speakerMapping);
5443
5404
  if (groups.length === 0) {
5444
- log11.info("No speakers found.");
5405
+ log10.info("No speakers found.");
5445
5406
  return;
5446
5407
  }
5447
5408
  const lines = groups.map((group, idx) => {
@@ -5450,17 +5411,17 @@ async function runList(outputDir) {
5450
5411
  const labelsStr = group.labels.join(", ");
5451
5412
  return `${String(num)}. ${displayName} (${labelsStr}, ${String(group.totalEntries)} entries)`;
5452
5413
  });
5453
- log11.info(lines.join("\n"));
5414
+ log10.info(lines.join("\n"));
5454
5415
  }
5455
5416
  async function runRename(outputDir, oldName, newName) {
5456
5417
  if (newName.trim().length === 0) {
5457
- log11.error("New name cannot be empty. Use the interactive prompt to clear a mapping.");
5418
+ log10.error("New name cannot be empty. Use the interactive prompt to clear a mapping.");
5458
5419
  return;
5459
5420
  }
5460
5421
  const metadataPath = join5(outputDir, "metadata.json");
5461
5422
  const metadata = await readJsonFile(metadataPath);
5462
5423
  if (metadata == null) {
5463
- log11.error("Not a vidistill output directory");
5424
+ log10.error("Not a vidistill output directory");
5464
5425
  return;
5465
5426
  }
5466
5427
  const speakerMapping = { ...metadata.speakerMapping ?? {} };
@@ -5481,18 +5442,18 @@ async function runRename(outputDir, oldName, newName) {
5481
5442
  const currentNames = Object.values(speakerMapping);
5482
5443
  const unmappedLabels = speakers.filter((s) => speakerMapping[s.label] == null).map((s) => s.label);
5483
5444
  const allNames = [.../* @__PURE__ */ new Set([...currentNames, ...unmappedLabels])];
5484
- log11.error(`No speaker named "${oldName}" found. Current speakers: ${formatNameList(allNames)}`);
5445
+ log10.error(`No speaker named "${oldName}" found. Current speakers: ${formatNameList(allNames)}`);
5485
5446
  return;
5486
5447
  }
5487
5448
  if (matchingKeys.length > 1) {
5488
- log11.error(
5449
+ log10.error(
5489
5450
  `Multiple speakers named "${oldName}" (${matchingKeys.join(", ")}). Use SPEAKER_XX label to specify which one.`
5490
5451
  );
5491
5452
  return;
5492
5453
  }
5493
5454
  const key = matchingKeys[0];
5494
5455
  speakerMapping[key] = newName;
5495
- log11.info("Re-rendering output files with updated speaker names...");
5456
+ log10.info("Re-rendering output files with updated speaker names...");
5496
5457
  const result = await reRenderWithSpeakerMapping({
5497
5458
  outputDir,
5498
5459
  speakerMapping,
@@ -5500,16 +5461,16 @@ async function runRename(outputDir, oldName, newName) {
5500
5461
  });
5501
5462
  if (result.errors.length > 0) {
5502
5463
  for (const err of result.errors) {
5503
- log11.error(err);
5464
+ log10.error(err);
5504
5465
  }
5505
5466
  }
5506
- log11.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
5467
+ log10.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
5507
5468
  }
5508
5469
  async function runMerge(outputDir, sourceName, targetName) {
5509
5470
  const metadataPath = join5(outputDir, "metadata.json");
5510
5471
  const metadata = await readJsonFile(metadataPath);
5511
5472
  if (metadata == null) {
5512
- log11.error("Not a vidistill output directory");
5473
+ log10.error("Not a vidistill output directory");
5513
5474
  return;
5514
5475
  }
5515
5476
  const speakerMapping = { ...metadata.speakerMapping ?? {} };
@@ -5538,19 +5499,19 @@ async function runMerge(outputDir, sourceName, targetName) {
5538
5499
  const targetKeys = findKeys(targetName);
5539
5500
  if (sourceKeys.length === 0) {
5540
5501
  const currentNames = buildCurrentNames(speakers, speakerMapping);
5541
- log11.error(`No speaker named "${sourceName}" found. Current speakers: ${formatNameList(currentNames)}`);
5502
+ log10.error(`No speaker named "${sourceName}" found. Current speakers: ${formatNameList(currentNames)}`);
5542
5503
  return;
5543
5504
  }
5544
5505
  if (targetKeys.length === 0) {
5545
5506
  const currentNames = buildCurrentNames(speakers, speakerMapping);
5546
- log11.error(`No speaker named "${targetName}" found. Current speakers: ${formatNameList(currentNames)}`);
5507
+ log10.error(`No speaker named "${targetName}" found. Current speakers: ${formatNameList(currentNames)}`);
5547
5508
  return;
5548
5509
  }
5549
5510
  const resolvedTargetName = speakerMapping[targetKeys[0]] ?? targetName;
5550
5511
  for (const key of sourceKeys) {
5551
5512
  speakerMapping[key] = resolvedTargetName;
5552
5513
  }
5553
- log11.info("Re-rendering output files with updated speaker names...");
5514
+ log10.info("Re-rendering output files with updated speaker names...");
5554
5515
  const result = await reRenderWithSpeakerMapping({
5555
5516
  outputDir,
5556
5517
  speakerMapping,
@@ -5558,10 +5519,10 @@ async function runMerge(outputDir, sourceName, targetName) {
5558
5519
  });
5559
5520
  if (result.errors.length > 0) {
5560
5521
  for (const err of result.errors) {
5561
- log11.error(err);
5522
+ log10.error(err);
5562
5523
  }
5563
5524
  }
5564
- log11.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
5525
+ log10.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
5565
5526
  }
5566
5527
  function buildCurrentNames(speakers, speakerMapping) {
5567
5528
  const names = /* @__PURE__ */ new Set();
@@ -5578,11 +5539,11 @@ function buildCurrentNames(speakers, speakerMapping) {
5578
5539
  async function run2(args) {
5579
5540
  const { outputDir, list, rename, merge, error } = parseArgs(args);
5580
5541
  if (error != null) {
5581
- log11.error(error);
5542
+ log10.error(error);
5582
5543
  return;
5583
5544
  }
5584
5545
  if (outputDir == null || outputDir.trim() === "") {
5585
- log11.error('Usage: vidistill rename-speakers <output-dir> [--list] [--rename "old" "new"] [--merge "source" "target"]');
5546
+ log10.error('Usage: vidistill rename-speakers <output-dir> [--list] [--rename "old" "new"] [--merge "source" "target"]');
5586
5547
  return;
5587
5548
  }
5588
5549
  if (list) {
@@ -5600,22 +5561,22 @@ async function run2(args) {
5600
5561
  const metadataPath = join5(outputDir, "metadata.json");
5601
5562
  const metadata = await readJsonFile(metadataPath);
5602
5563
  if (metadata == null) {
5603
- log11.error("Not a vidistill output directory");
5564
+ log10.error("Not a vidistill output directory");
5604
5565
  return;
5605
5566
  }
5606
5567
  const rawDir = join5(outputDir, "raw");
5607
5568
  const peopleExtraction = await readJsonFile(join5(rawDir, "pass3b-people.json"));
5608
5569
  if (peopleExtraction == null) {
5609
- log11.info("No speakers detected in this video");
5570
+ log10.info("No speakers detected in this video");
5610
5571
  return;
5611
5572
  }
5612
5573
  const speakers = await collectSpeakersFromRaw(rawDir);
5613
5574
  if (speakers.length === 0) {
5614
- log11.info("No speakers detected in this video");
5575
+ log10.info("No speakers detected in this video");
5615
5576
  return;
5616
5577
  }
5617
5578
  const existingMapping = metadata.speakerMapping ?? {};
5618
- log11.info(
5579
+ log10.info(
5619
5580
  `${String(speakers.length)} speaker${speakers.length === 1 ? "" : "s"} found. Enter names (or press Enter to keep current).`
5620
5581
  );
5621
5582
  const groups = groupSpeakersByExistingMapping(speakers, existingMapping);
@@ -5662,7 +5623,7 @@ async function run2(args) {
5662
5623
  return;
5663
5624
  }
5664
5625
  const { mapping: finalMapping, declinedMerges } = mergeResult;
5665
- log11.info("Re-rendering output files with updated speaker names...");
5626
+ log10.info("Re-rendering output files with updated speaker names...");
5666
5627
  const result = await reRenderWithSpeakerMapping({
5667
5628
  outputDir,
5668
5629
  speakerMapping: finalMapping,
@@ -5670,14 +5631,14 @@ async function run2(args) {
5670
5631
  });
5671
5632
  if (result.errors.length > 0) {
5672
5633
  for (const err of result.errors) {
5673
- log11.error(err);
5634
+ log10.error(err);
5674
5635
  }
5675
5636
  }
5676
- log11.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
5637
+ log10.info(`Done. ${String(result.filesGenerated.length)} file${result.filesGenerated.length === 1 ? "" : "s"} updated.`);
5677
5638
  }
5678
5639
 
5679
5640
  // src/cli/index.ts
5680
- var version = "0.5.0";
5641
+ var version = "0.5.2";
5681
5642
  var DEFAULT_OUTPUT = "./vidistill-output/";
5682
5643
  var SUBCOMMANDS = {
5683
5644
  mcp: run,
@@ -5730,11 +5691,11 @@ Commands: ${Object.keys(SUBCOMMANDS).join(", ")}`
5730
5691
  lang: args.lang
5731
5692
  });
5732
5693
  } catch (err) {
5733
- const { log: log12 } = await import("@clack/prompts");
5694
+ const { log: log11 } = await import("@clack/prompts");
5734
5695
  const { default: pc4 } = await import("picocolors");
5735
5696
  const raw = err instanceof Error ? err.message : String(err);
5736
5697
  const message = raw.split("\n")[0].slice(0, 200);
5737
- log12.error(pc4.red(message));
5698
+ log11.error(pc4.red(message));
5738
5699
  process.exit(1);
5739
5700
  }
5740
5701
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidistill",
3
- "version": "0.5.0",
3
+ "version": "0.5.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",