vidistill 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +479 -169
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { defineCommand, runMain } from "citty";
5
- import { log as log9 } from "@clack/prompts";
5
+ import { log as log10 } from "@clack/prompts";
6
6
  import pc5 from "picocolors";
7
- import { basename as basename2, extname as extname2, resolve } from "path";
7
+ import { basename as basename3, extname as extname2, resolve } from "path";
8
8
 
9
9
  // src/cli/ui.ts
10
10
  import figlet from "figlet";
@@ -233,7 +233,11 @@ function createProgressDisplay() {
233
233
  } else if (status.phase === "pass2") {
234
234
  s.message(`Pass 2: Visual extraction (${segNum}/${total} segments)`);
235
235
  } else if (status.phase === "pass3a") {
236
- s.message(`Code reconstruction (${segNum}/${total} segments)`);
236
+ if (total > 1) {
237
+ s.message(`Reconstructing code (run ${segNum}/${total})...`);
238
+ } else {
239
+ s.message("Reconstructing code...");
240
+ }
237
241
  } else if (status.phase === "pass3b") {
238
242
  s.message("People extraction...");
239
243
  } else if (status.phase === "pass3c") {
@@ -332,10 +336,10 @@ import { log as log4 } from "@clack/prompts";
332
336
  import pc4 from "picocolors";
333
337
 
334
338
  // src/gemini/models.ts
335
- var MODELS = [
336
- { id: "gemini-3-flash-preview", name: "Gemini 3 Flash (Best OCR)", tpmLimit: 1e6 },
337
- { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash (Fallback)", tpmLimit: 1e6 }
338
- ];
339
+ var MODELS = {
340
+ flash: "gemini-3-flash-preview",
341
+ pro: "gemini-2.5-pro"
342
+ };
339
343
 
340
344
  // src/input/youtube.ts
341
345
  var YOUTUBE_PATTERNS = [
@@ -359,7 +363,7 @@ function normalizeYouTubeUrl(url) {
359
363
  async function handleYouTube(url, client) {
360
364
  try {
361
365
  await client.generate({
362
- model: MODELS[1].id,
366
+ model: MODELS.flash,
363
367
  contents: [
364
368
  {
365
369
  role: "user",
@@ -373,7 +377,7 @@ async function handleYouTube(url, client) {
373
377
  });
374
378
  return { fileUri: url, mimeType: "video/mp4", source: "direct" };
375
379
  } catch (err) {
376
- log4.warn(pc4.dim(`Direct Gemini probe failed: ${err instanceof Error ? err.message : String(err)}. Falling back to yt-dlp.`));
380
+ log4.warn(pc4.dim("Direct Gemini probe failed. Falling back to yt-dlp."));
377
381
  }
378
382
  const tempPath2 = await downloadWithYtDlp(url);
379
383
  try {
@@ -654,7 +658,7 @@ async function detectDuration(source) {
654
658
  }
655
659
 
656
660
  // src/core/pipeline.ts
657
- import { log as log7 } from "@clack/prompts";
661
+ import { log as log8 } from "@clack/prompts";
658
662
 
659
663
  // src/constants/prompts.ts
660
664
  var SYSTEM_INSTRUCTION_PASS_1 = `
@@ -750,7 +754,7 @@ PASS RECOMMENDATIONS BY TYPE:
750
754
  var SYSTEM_INSTRUCTION_PASS_3A = `
751
755
  You are an expert code reconstruction analyst. Your task is to reconstruct the complete, final state of every code file shown across this entire video, synthesizing all edits into a coherent codebase snapshot.
752
756
 
753
- You will receive the transcript and visual extraction data from all segments. Use them together to understand what code was written, modified, and deleted.
757
+ You will receive the complete video and all extracted transcript and code block data. Use them together to understand what code was written, modified, and deleted.
754
758
 
755
759
  CRITICAL RULES:
756
760
  1. RECONSTRUCT each file to its final state \u2014 apply all changes in chronological order so the output reflects the code as it was at the end of the video.
@@ -760,9 +764,11 @@ CRITICAL RULES:
760
764
  5. EXTRACT dependencies: every library import, require(), package name, or external module reference mentioned or shown counts as a dependency.
761
765
  6. CAPTURE build commands: any terminal command shown or spoken for installing, building, running, or testing the project (e.g., "npm install", "go build", "python -m pytest").
762
766
  7. NEVER invent code that was not shown or described. If a section was unclear, note it with a comment like "// content not fully visible".
763
- 8. NEVER skip a file because it appears in only one segment \u2014 if code was shown, reconstruct it.
764
- 9. When a file appears multiple times across segments, merge all its appearances into a single entry with the complete change history.
767
+ 8. NEVER skip a file because it appears in only one part of the video \u2014 if code was shown, reconstruct it.
768
+ 9. When a file appears multiple times, record its complete change history in a single entry with all edits in chronological order.
765
769
  10. INCLUDE empty files if created but not yet written \u2014 use empty string for final_content and note the creation in changes.
770
+ 11. Cross-reference your visual analysis of the video against the extracted code blocks provided in the text context. Prioritize what you can visually verify on screen. If code is partially visible, include what you can see and mark unclear sections with \`// [content not fully visible]\`.
771
+ 12. Do NOT invent code files that are not clearly visible on screen. If you are uncertain whether a file exists, do not include it.
766
772
 
767
773
  COMPLETENESS TARGET:
768
774
  - Every distinct filename that appeared on screen must produce a files entry
@@ -849,13 +855,13 @@ CRITICAL RULES:
849
855
  6. CAPTURE every question: include questions asked explicitly and questions raised implicitly (from the implicit signals pass). Note whether each was answered.
850
856
  7. PRODUCE meaningful suggestions: AI-generated suggestions must follow logically from the video content. Suggest next steps, deeper resources, or practice exercises that are directly relevant.
851
857
  8. USE precise timestamps: every entry with a timestamp field must contain a valid HH:MM:SS value referencing when the content appeared.
852
- 9. DETERMINE files_to_generate: based on the video content and what was produced, list the output files that should be generated (e.g., "transcript.md", "notes.md", "code/main.py", "people.json").
858
+ 9. LIST files_to_generate for reference purposes \u2014 this list is informational and does not control which output files are generated. Output files are determined automatically based on available extraction data.
853
859
  10. NEVER add information not present in the source data. Suggestions are the only place for AI-generated content beyond the video.
854
860
 
855
861
  COMPLETENESS TARGET:
856
862
  - Aim for at least 5 topics for any video over 15 minutes
857
863
  - Every explicit and implicit decision must appear in key_decisions
858
- - Every code file identified in the code reconstruction pass must be listed in files_to_generate
864
+ - The files_to_generate list should reflect what content was found, but output routing is handled automatically
859
865
  - The overview should be dense with specifics, not vague summary language
860
866
  `;
861
867
 
@@ -1398,6 +1404,9 @@ function parseTimestamp(ts) {
1398
1404
  }
1399
1405
  return parts[0] ?? 0;
1400
1406
  }
1407
+ function normalizeFilename(name) {
1408
+ return name.toLowerCase().replace(/^\.\//, "").replace(/\\/g, "/");
1409
+ }
1401
1410
  function changeTypeBadge(changeType) {
1402
1411
  const badges = {
1403
1412
  new_file: "[NEW]",
@@ -1439,7 +1448,7 @@ async function runTranscript(params) {
1439
1448
  responseMimeType: "application/json",
1440
1449
  ...resolution !== void 0 ? { mediaResolution: resolution } : {},
1441
1450
  maxOutputTokens: 65536,
1442
- temperature: 1
1451
+ temperature: 0
1443
1452
  }
1444
1453
  });
1445
1454
  if (result === null || typeof result !== "object" || !Array.isArray(result["transcript_entries"])) {
@@ -1482,7 +1491,7 @@ async function runVisual(params) {
1482
1491
  responseMimeType: "application/json",
1483
1492
  ...resolution !== void 0 ? { mediaResolution: resolution } : {},
1484
1493
  maxOutputTokens: 65536,
1485
- temperature: 1
1494
+ temperature: 0
1486
1495
  }
1487
1496
  });
1488
1497
  if (result === null || typeof result !== "object" || !Array.isArray(result["code_blocks"])) {
@@ -1523,7 +1532,7 @@ async function runSceneAnalysis(params) {
1523
1532
  responseMimeType: "application/json",
1524
1533
  ...resolution !== void 0 ? { mediaResolution: resolution } : { mediaResolution: MediaResolution.MEDIA_RESOLUTION_LOW },
1525
1534
  maxOutputTokens: 8192,
1526
- temperature: 0.5
1535
+ temperature: 0.2
1527
1536
  }
1528
1537
  });
1529
1538
  if (result === null || typeof result !== "object" || typeof result["type"] !== "string") {
@@ -1537,31 +1546,73 @@ async function runSceneAnalysis(params) {
1537
1546
  }
1538
1547
 
1539
1548
  // src/passes/code.ts
1549
+ var MAX_CONTEXT_CHARS = 2e5;
1550
+ var LONG_VIDEO_THRESHOLD_SECONDS = 3600;
1551
+ function compileContext(duration, pass1Results, pass2Results) {
1552
+ const isLongVideo = duration > LONG_VIDEO_THRESHOLD_SECONDS;
1553
+ const segmentIndicesToInclude = isLongVideo ? new Set(
1554
+ pass2Results.map((r, i) => r != null && r.code_blocks.length > 0 ? i : -1).filter((i) => i !== -1)
1555
+ ) : null;
1556
+ const transcriptLines = ["TRANSCRIPT (all segments):"];
1557
+ for (let i = 0; i < pass1Results.length; i++) {
1558
+ const p1 = pass1Results[i] ?? null;
1559
+ const p2 = pass2Results[i] ?? null;
1560
+ if (isLongVideo && segmentIndicesToInclude !== null && !segmentIndicesToInclude.has(i)) {
1561
+ continue;
1562
+ }
1563
+ let header;
1564
+ if (p1 != null && p1.time_range) {
1565
+ header = `=== Segment ${i + 1} (${p1.time_range}) ===`;
1566
+ } else if (p2 != null && p2.time_range) {
1567
+ header = `=== Segment ${i + 1} (${p2.time_range}) ===`;
1568
+ } else {
1569
+ header = `=== Segment ${i + 1} ===`;
1570
+ }
1571
+ transcriptLines.push(header);
1572
+ if (p1 != null) {
1573
+ for (const entry of p1.transcript_entries) {
1574
+ transcriptLines.push(`[${entry.timestamp}] ${entry.speaker}: ${entry.text}`);
1575
+ }
1576
+ } else {
1577
+ transcriptLines.push("[No transcript available]");
1578
+ }
1579
+ }
1580
+ const codeLines = ["CODE BLOCKS EXTRACTED (all segments):"];
1581
+ for (let i = 0; i < pass2Results.length; i++) {
1582
+ const p2 = pass2Results[i] ?? null;
1583
+ if (p2 == null || p2.code_blocks.length === 0) continue;
1584
+ const p1 = pass1Results[i] ?? null;
1585
+ let header;
1586
+ if (p2.time_range) {
1587
+ header = `=== Segment ${i + 1} (${p2.time_range}) ===`;
1588
+ } else if (p1 != null && p1.time_range) {
1589
+ header = `=== Segment ${i + 1} (${p1.time_range}) ===`;
1590
+ } else {
1591
+ header = `=== Segment ${i + 1} ===`;
1592
+ }
1593
+ codeLines.push(header);
1594
+ for (const block of p2.code_blocks) {
1595
+ codeLines.push(`[${block.timestamp}] ${block.language}:
1596
+ ${block.content}`);
1597
+ }
1598
+ }
1599
+ let contextText = [transcriptLines.join("\n"), "", codeLines.join("\n")].join("\n");
1600
+ if (isLongVideo && contextText.length > MAX_CONTEXT_CHARS) {
1601
+ const lastNewline = contextText.lastIndexOf("\n", MAX_CONTEXT_CHARS);
1602
+ contextText = contextText.slice(0, lastNewline > 0 ? lastNewline : MAX_CONTEXT_CHARS);
1603
+ }
1604
+ return contextText;
1605
+ }
1540
1606
  async function runCodeReconstruction(params) {
1541
- const { client, fileUri, mimeType, segment, model, resolution, pass1Result, pass2Result } = params;
1542
- const transcriptText = pass1Result != null ? pass1Result.transcript_entries.map((t) => `[${t.timestamp}] ${t.speaker}: ${t.text}`).join("\n") : "[No transcript available for this segment]";
1543
- const codeBlocksText = pass2Result != null && pass2Result.code_blocks.length > 0 ? pass2Result.code_blocks.map((b) => `[${b.timestamp}] ${b.filename} (${b.language}):
1544
- ${b.content}`).join("\n\n") : "[No code blocks available for this segment]";
1545
- const contextText = [
1546
- "TRANSCRIPT FROM THIS SEGMENT:",
1547
- transcriptText,
1548
- "",
1549
- "CODE BLOCKS FROM THIS SEGMENT:",
1550
- codeBlocksText
1551
- ].join("\n");
1607
+ const { client, fileUri, mimeType, duration, model, resolution, pass1Results, pass2Results } = params;
1608
+ const contextText = compileContext(duration, pass1Results, pass2Results);
1552
1609
  const contents = [
1553
1610
  {
1554
1611
  role: "user",
1555
1612
  parts: [
1613
+ { fileData: { fileUri, mimeType } },
1556
1614
  {
1557
- fileData: { fileUri, mimeType },
1558
- videoMetadata: {
1559
- startOffset: `${segment.startTime}s`,
1560
- endOffset: `${segment.endTime}s`
1561
- }
1562
- },
1563
- {
1564
- text: `Process segment #${segment.index + 1}. Analyze from ${formatTime(segment.startTime)} to ${formatTime(segment.endTime)}.
1615
+ text: `Analyze the entire video (${formatTime(duration)} total).
1565
1616
 
1566
1617
  ${contextText}`
1567
1618
  }
@@ -1577,7 +1628,7 @@ ${contextText}`
1577
1628
  responseMimeType: "application/json",
1578
1629
  ...resolution !== void 0 ? { mediaResolution: resolution } : {},
1579
1630
  maxOutputTokens: 65536,
1580
- temperature: 1
1631
+ temperature: 0
1581
1632
  }
1582
1633
  });
1583
1634
  if (result === null || typeof result !== "object" || !Array.isArray(result["files"]) || !Array.isArray(result["dependencies_mentioned"]) || !Array.isArray(result["build_commands"])) {
@@ -1612,7 +1663,7 @@ ${transcriptText}`;
1612
1663
  responseSchema: SCHEMA_PASS_3B,
1613
1664
  responseMimeType: "application/json",
1614
1665
  maxOutputTokens: 65536,
1615
- temperature: 1
1666
+ temperature: 0
1616
1667
  }
1617
1668
  });
1618
1669
  if (result === null || typeof result !== "object" || !Array.isArray(result["participants"]) || !Array.isArray(result["relationships"])) {
@@ -1662,7 +1713,7 @@ ${contextText}`
1662
1713
  responseMimeType: "application/json",
1663
1714
  ...resolution !== void 0 ? { mediaResolution: resolution } : {},
1664
1715
  maxOutputTokens: 65536,
1665
- temperature: 1
1716
+ temperature: 0
1666
1717
  }
1667
1718
  });
1668
1719
  if (result === null || typeof result !== "object" || !Array.isArray(result["messages"]) || !Array.isArray(result["links"])) {
@@ -1711,7 +1762,7 @@ ${contextText}`
1711
1762
  responseMimeType: "application/json",
1712
1763
  ...resolution !== void 0 ? { mediaResolution: resolution } : {},
1713
1764
  maxOutputTokens: 65536,
1714
- temperature: 1
1765
+ temperature: 0.1
1715
1766
  }
1716
1767
  });
1717
1768
  if (result === null || typeof result !== "object" || !Array.isArray(result["emotional_shifts"]) || !Array.isArray(result["questions_implicit"]) || !Array.isArray(result["decisions_implicit"]) || !Array.isArray(result["tasks_assigned"]) || !Array.isArray(result["emphasis_patterns"])) {
@@ -1721,13 +1772,12 @@ ${contextText}`
1721
1772
  }
1722
1773
 
1723
1774
  // src/passes/synthesis.ts
1724
- function compileContext(params) {
1725
- const { segmentResults, videoProfile, peopleExtraction, context } = params;
1775
+ function compileContext2(params) {
1776
+ const { segmentResults, videoProfile, peopleExtraction, codeReconstruction, context } = params;
1726
1777
  const segmentSections = segmentResults.map((seg, idx) => {
1727
1778
  const segNum = idx + 1;
1728
1779
  const pass1 = seg.pass1;
1729
1780
  const pass2 = seg.pass2;
1730
- const pass3a = seg.pass3a;
1731
1781
  const pass3c = seg.pass3c;
1732
1782
  const pass3d = seg.pass3d;
1733
1783
  const timeRange = pass1?.time_range ?? pass2?.time_range ?? `segment ${segNum}`;
@@ -1760,14 +1810,6 @@ function compileContext(params) {
1760
1810
  lines.push("[No visual notes]");
1761
1811
  }
1762
1812
  lines.push("");
1763
- if (pass3a != null) {
1764
- lines.push("--- Code Reconstruction ---");
1765
- for (const f of pass3a.files) {
1766
- lines.push(`File: ${f.filename} (${f.language})`);
1767
- lines.push(`Final content: ${f.final_content}`);
1768
- }
1769
- lines.push("");
1770
- }
1771
1813
  if (pass3c != null) {
1772
1814
  lines.push("--- Chat Messages ---");
1773
1815
  if (pass3c.messages.length > 0) {
@@ -1799,6 +1841,15 @@ function compileContext(params) {
1799
1841
  "=== VIDEO PROFILE ===",
1800
1842
  `Type: ${videoProfile.type} | Complexity: ${videoProfile.complexity} | Speakers: ${videoProfile.speakers.count}`
1801
1843
  ];
1844
+ const codeLines = [];
1845
+ if (codeReconstruction != null) {
1846
+ codeLines.push("=== CODE RECONSTRUCTION ===");
1847
+ codeLines.push("--- Code Reconstruction ---");
1848
+ for (const f of codeReconstruction.files) {
1849
+ codeLines.push(`File: ${f.filename} (${f.language})`);
1850
+ codeLines.push(`Final content: ${f.final_content}`);
1851
+ }
1852
+ }
1802
1853
  const peopleLines = ["=== PEOPLE ==="];
1803
1854
  if (peopleExtraction != null && peopleExtraction.participants.length > 0) {
1804
1855
  for (const p of peopleExtraction.participants) {
@@ -1809,16 +1860,18 @@ function compileContext(params) {
1809
1860
  peopleLines.push("[No people data]");
1810
1861
  }
1811
1862
  const contextLines = ["=== USER CONTEXT ===", context != null && context.length > 0 ? context : "[No user context provided]"];
1812
- return [
1863
+ const sections = [
1813
1864
  ...segmentSections,
1814
1865
  profileLines.join("\n"),
1866
+ ...codeLines.length > 0 ? [codeLines.join("\n")] : [],
1815
1867
  peopleLines.join("\n"),
1816
1868
  contextLines.join("\n")
1817
- ].join("\n\n");
1869
+ ];
1870
+ return sections.join("\n\n");
1818
1871
  }
1819
1872
  async function runSynthesis(params) {
1820
1873
  const { client, model } = params;
1821
- const compiledContext = compileContext(params);
1874
+ const compiledContext = compileContext2(params);
1822
1875
  const contents = [
1823
1876
  {
1824
1877
  role: "user",
@@ -1833,7 +1886,7 @@ async function runSynthesis(params) {
1833
1886
  responseSchema: SCHEMA_SYNTHESIS,
1834
1887
  responseMimeType: "application/json",
1835
1888
  maxOutputTokens: 65536,
1836
- temperature: 1
1889
+ temperature: 0.1
1837
1890
  }
1838
1891
  });
1839
1892
  if (result === null || typeof result !== "object" || typeof result["overview"] !== "string" || !Array.isArray(result["files_to_generate"]) || !Array.isArray(result["key_decisions"]) || !Array.isArray(result["key_concepts"]) || !Array.isArray(result["action_items"]) || !Array.isArray(result["questions_raised"]) || !Array.isArray(result["suggestions"]) || !Array.isArray(result["topics"])) {
@@ -1937,6 +1990,258 @@ function createSegmentPlan(durationSeconds, options) {
1937
1990
  return { segments, resolution: resolutionOverride ?? computedResolution };
1938
1991
  }
1939
1992
 
1993
+ // src/core/consensus.ts
1994
+ import { log as log7 } from "@clack/prompts";
1995
+ function tokenize(content) {
1996
+ const tokens = content.match(/[\p{L}\p{N}_]+/gu) ?? [];
1997
+ return new Set(tokens);
1998
+ }
1999
+ function tokenOverlap(a, b) {
2000
+ const setA = tokenize(a);
2001
+ const setB = tokenize(b);
2002
+ let count = 0;
2003
+ for (const t of setA) {
2004
+ if (setB.has(t)) count++;
2005
+ }
2006
+ return count;
2007
+ }
2008
+ function selectBestContent(normalizedName, candidates, pass2Results) {
2009
+ const referenceContent = [];
2010
+ for (const p2 of pass2Results) {
2011
+ if (p2 == null) continue;
2012
+ for (const block of p2.code_blocks) {
2013
+ if (normalizeFilename(block.filename) === normalizedName) {
2014
+ referenceContent.push(block.content);
2015
+ }
2016
+ }
2017
+ }
2018
+ const referenceText = referenceContent.join("\n");
2019
+ let bestFile = candidates[0];
2020
+ let bestScore = -1;
2021
+ for (const candidate of candidates) {
2022
+ let score;
2023
+ if (referenceText.length === 0) {
2024
+ score = candidate.final_content.length;
2025
+ } else {
2026
+ score = tokenOverlap(candidate.final_content, referenceText);
2027
+ }
2028
+ if (score > bestScore || score === bestScore && candidate.final_content.length > bestFile.final_content.length) {
2029
+ bestScore = score;
2030
+ bestFile = candidate;
2031
+ }
2032
+ }
2033
+ return bestFile;
2034
+ }
2035
+ function mergeChanges(allChanges) {
2036
+ const seen = /* @__PURE__ */ new Set();
2037
+ const merged = [];
2038
+ for (const changes of allChanges) {
2039
+ for (const change of changes) {
2040
+ const key = `${change.timestamp}|${change.change_type}`;
2041
+ if (!seen.has(key)) {
2042
+ seen.add(key);
2043
+ merged.push(change);
2044
+ }
2045
+ }
2046
+ }
2047
+ return merged;
2048
+ }
2049
+ function unionDedup(arrays) {
2050
+ const seen = /* @__PURE__ */ new Set();
2051
+ const result = [];
2052
+ for (const arr of arrays) {
2053
+ for (const item of arr) {
2054
+ if (!seen.has(item)) {
2055
+ seen.add(item);
2056
+ result.push(item);
2057
+ }
2058
+ }
2059
+ }
2060
+ return result;
2061
+ }
2062
+ async function runCodeConsensus(params) {
2063
+ const { config, runFn, pass2Results, onProgress } = params;
2064
+ const { runs, minAgreement } = config;
2065
+ const successfulRuns = [];
2066
+ for (let i = 0; i < runs; i++) {
2067
+ try {
2068
+ const result = await runFn();
2069
+ successfulRuns.push(result);
2070
+ } catch (e) {
2071
+ const msg = e instanceof Error ? e.message : String(e);
2072
+ log7.warn(`consensus run ${i + 1}/${runs} failed: ${msg}`);
2073
+ }
2074
+ onProgress?.(i + 1, runs);
2075
+ }
2076
+ const runsCompleted = successfulRuns.length;
2077
+ if (runsCompleted === 0) {
2078
+ return {
2079
+ confirmed: [],
2080
+ rejected: [],
2081
+ runsCompleted: 0,
2082
+ runsAttempted: runs,
2083
+ mergedDependencies: [],
2084
+ mergedBuildCommands: []
2085
+ };
2086
+ }
2087
+ if (runs === 1 && runsCompleted === 1) {
2088
+ const only = successfulRuns[0];
2089
+ return {
2090
+ confirmed: only.files,
2091
+ rejected: [],
2092
+ runsCompleted: 1,
2093
+ runsAttempted: 1,
2094
+ mergedDependencies: [...new Set(only.dependencies_mentioned)],
2095
+ mergedBuildCommands: [...new Set(only.build_commands)]
2096
+ };
2097
+ }
2098
+ const voteMap = /* @__PURE__ */ new Map();
2099
+ for (const run of successfulRuns) {
2100
+ if (run.files.length === 0) continue;
2101
+ const seenInRun = /* @__PURE__ */ new Set();
2102
+ for (const file of run.files) {
2103
+ const normalized = normalizeFilename(file.filename);
2104
+ if (seenInRun.has(normalized)) continue;
2105
+ seenInRun.add(normalized);
2106
+ const entry = voteMap.get(normalized);
2107
+ if (entry == null) {
2108
+ voteMap.set(normalized, { count: 1, originals: [file] });
2109
+ } else {
2110
+ entry.count++;
2111
+ entry.originals.push(file);
2112
+ }
2113
+ }
2114
+ }
2115
+ const confirmed = [];
2116
+ const rejected = [];
2117
+ for (const [normalizedName, { count, originals }] of voteMap.entries()) {
2118
+ if (count >= minAgreement) {
2119
+ const bestBase = selectBestContent(normalizedName, originals, pass2Results);
2120
+ const mergedChanges = mergeChanges(originals.map((f) => f.changes));
2121
+ confirmed.push({
2122
+ filename: bestBase.filename,
2123
+ language: bestBase.language,
2124
+ final_content: bestBase.final_content,
2125
+ changes: mergedChanges
2126
+ });
2127
+ } else {
2128
+ rejected.push(originals[0].filename);
2129
+ }
2130
+ }
2131
+ const mergedDependencies = unionDedup(successfulRuns.map((r) => r.dependencies_mentioned));
2132
+ const mergedBuildCommands = unionDedup(successfulRuns.map((r) => r.build_commands));
2133
+ return {
2134
+ confirmed,
2135
+ rejected,
2136
+ runsCompleted,
2137
+ runsAttempted: runs,
2138
+ mergedDependencies,
2139
+ mergedBuildCommands
2140
+ };
2141
+ }
2142
+
2143
+ // src/core/validator.ts
2144
+ var VALID_FILENAME_CHARS = /^[a-zA-Z0-9._\-\/]+$/;
2145
+ var PLACEHOLDER_PATTERNS = /* @__PURE__ */ new Set(["// TODO", "// empty file", "# TODO", "/* TODO */"]);
2146
+ function basename2(filename) {
2147
+ const normalized = normalizeFilename(filename);
2148
+ const parts = normalized.split("/");
2149
+ return parts[parts.length - 1] ?? normalized;
2150
+ }
2151
+ function collectPass2Filenames(pass2Results) {
2152
+ const normalized = /* @__PURE__ */ new Set();
2153
+ const basenames = /* @__PURE__ */ new Set();
2154
+ for (const p2 of pass2Results) {
2155
+ if (p2 == null) continue;
2156
+ for (const block of p2.code_blocks) {
2157
+ const norm = normalizeFilename(block.filename);
2158
+ normalized.add(norm);
2159
+ basenames.add(basename2(block.filename));
2160
+ }
2161
+ }
2162
+ return { normalized, basenames };
2163
+ }
2164
+ function checkGate1(file) {
2165
+ if (!file.filename || file.filename.trim() === "") {
2166
+ return "empty filename";
2167
+ }
2168
+ if (!file.final_content || file.final_content.trim() === "") {
2169
+ return "empty content";
2170
+ }
2171
+ if (!file.language || file.language.trim() === "") {
2172
+ return "empty language";
2173
+ }
2174
+ if (file.changes.length === 0) {
2175
+ return "no changes recorded";
2176
+ }
2177
+ return null;
2178
+ }
2179
+ function checkGate2(filename) {
2180
+ if (filename.includes("../")) {
2181
+ return "path traversal";
2182
+ }
2183
+ if (filename.startsWith("/")) {
2184
+ return "absolute path";
2185
+ }
2186
+ if (!VALID_FILENAME_CHARS.test(filename)) {
2187
+ return "invalid characters";
2188
+ }
2189
+ return null;
2190
+ }
2191
+ function checkGate3Ungrounded(filename, pass2Normalized, pass2Basenames) {
2192
+ const norm = normalizeFilename(filename);
2193
+ const base = basename2(filename);
2194
+ if (pass2Normalized.has(norm) || pass2Basenames.has(base)) {
2195
+ return false;
2196
+ }
2197
+ return true;
2198
+ }
2199
+ function checkGate5(file) {
2200
+ if (PLACEHOLDER_PATTERNS.has(file.final_content.trim())) {
2201
+ return "placeholder content";
2202
+ }
2203
+ if (file.final_content.length <= 20) {
2204
+ return "trivially short content";
2205
+ }
2206
+ return null;
2207
+ }
2208
+ function validateCodeReconstruction(params) {
2209
+ const { consensusResult, pass2Results } = params;
2210
+ const confirmed = [];
2211
+ const uncertain = [];
2212
+ const rejected = [];
2213
+ const warnings = [];
2214
+ const { normalized: pass2Normalized, basenames: pass2Basenames } = collectPass2Filenames(pass2Results);
2215
+ for (const file of consensusResult.confirmed) {
2216
+ const gate1Failure = checkGate1(file);
2217
+ if (gate1Failure != null) {
2218
+ warnings.push({ gate: 1, filename: file.filename, message: gate1Failure });
2219
+ rejected.push(file);
2220
+ continue;
2221
+ }
2222
+ const gate2Failure = checkGate2(file.filename);
2223
+ if (gate2Failure != null) {
2224
+ warnings.push({ gate: 2, filename: file.filename, message: gate2Failure });
2225
+ rejected.push(file);
2226
+ continue;
2227
+ }
2228
+ const gate5Failure = checkGate5(file);
2229
+ if (gate5Failure != null) {
2230
+ warnings.push({ gate: 5, filename: file.filename, message: gate5Failure });
2231
+ rejected.push(file);
2232
+ continue;
2233
+ }
2234
+ const isUngrounded = checkGate3Ungrounded(file.filename, pass2Normalized, pass2Basenames);
2235
+ if (isUngrounded) {
2236
+ warnings.push({ gate: 3, filename: file.filename, message: "ungrounded" });
2237
+ uncertain.push(file);
2238
+ continue;
2239
+ }
2240
+ confirmed.push(file);
2241
+ }
2242
+ return { confirmed, uncertain, rejected, warnings };
2243
+ }
2244
+
1940
2245
  // src/core/pipeline.ts
1941
2246
  var RETRY_DELAYS_MS = [2e3, 4e3, 8e3];
1942
2247
  async function withRetry(fn, label) {
@@ -1988,7 +2293,7 @@ async function runPipeline(config) {
1988
2293
  "pass0"
1989
2294
  );
1990
2295
  if (pass0Attempt.error !== null) {
1991
- log7.warn(pass0Attempt.error);
2296
+ log8.warn(pass0Attempt.error);
1992
2297
  errors.push(pass0Attempt.error);
1993
2298
  videoProfile = DEFAULT_PROFILE;
1994
2299
  } else {
@@ -1996,8 +2301,8 @@ async function runPipeline(config) {
1996
2301
  }
1997
2302
  strategy = determineStrategy(videoProfile);
1998
2303
  onProgress?.({ phase: "pass0", segment: 0, totalSegments: 1, status: "done" });
1999
- log7.info(`Video type: ${videoProfile.type}`);
2000
- log7.info(`Strategy: ${strategy.passes.join(" \u2192 ")}`);
2304
+ log8.info(`Video type: ${videoProfile.type}`);
2305
+ log8.info(`Strategy: ${strategy.passes.join(" \u2192 ")}`);
2001
2306
  const plan = createSegmentPlan(duration, {
2002
2307
  segmentMinutes: strategy.segmentMinutes,
2003
2308
  resolution: strategy.resolution
@@ -2008,7 +2313,6 @@ async function runPipeline(config) {
2008
2313
  const n = segments.length;
2009
2314
  let pass1RanOnce = false;
2010
2315
  let pass2RanOnce = false;
2011
- let pass3aRanOnce = false;
2012
2316
  let pass3cRanOnce = false;
2013
2317
  let pass3dRanOnce = false;
2014
2318
  let wasInterrupted = false;
@@ -2027,7 +2331,7 @@ async function runPipeline(config) {
2027
2331
  `segment ${i} pass1`
2028
2332
  );
2029
2333
  if (pass1Attempt.error !== null) {
2030
- log7.warn(pass1Attempt.error);
2334
+ log8.warn(pass1Attempt.error);
2031
2335
  errors.push(pass1Attempt.error);
2032
2336
  } else {
2033
2337
  pass1 = pass1Attempt.result;
@@ -2052,42 +2356,13 @@ async function runPipeline(config) {
2052
2356
  `segment ${i} pass2`
2053
2357
  );
2054
2358
  if (pass2Attempt.error !== null) {
2055
- log7.warn(pass2Attempt.error);
2359
+ log8.warn(pass2Attempt.error);
2056
2360
  errors.push(pass2Attempt.error);
2057
2361
  } else {
2058
2362
  pass2 = pass2Attempt.result;
2059
2363
  pass2RanOnce = true;
2060
2364
  }
2061
2365
  onProgress?.({ phase: "pass2", segment: i, totalSegments: n, status: "done" });
2062
- let pass3a;
2063
- if (strategy.passes.includes("code")) {
2064
- onProgress?.({ phase: "pass3a", segment: i, totalSegments: n, status: "running" });
2065
- const pass3aAttempt = await withRetry(
2066
- () => rateLimiter.execute(
2067
- () => runCodeReconstruction({
2068
- client,
2069
- fileUri,
2070
- mimeType,
2071
- segment,
2072
- model: MODELS[0].id,
2073
- resolution,
2074
- pass1Result: pass1 ?? void 0,
2075
- pass2Result: pass2 ?? void 0
2076
- }),
2077
- { onWait }
2078
- ),
2079
- `segment ${i} pass3a`
2080
- );
2081
- if (pass3aAttempt.error !== null) {
2082
- log7.warn(pass3aAttempt.error);
2083
- errors.push(pass3aAttempt.error);
2084
- pass3a = null;
2085
- } else {
2086
- pass3a = pass3aAttempt.result;
2087
- pass3aRanOnce = true;
2088
- }
2089
- onProgress?.({ phase: "pass3a", segment: i, totalSegments: n, status: "done" });
2090
- }
2091
2366
  let pass3c;
2092
2367
  if (strategy.passes.includes("chat")) {
2093
2368
  onProgress?.({ phase: "pass3c", segment: i, totalSegments: n, status: "running" });
@@ -2098,7 +2373,7 @@ async function runPipeline(config) {
2098
2373
  fileUri,
2099
2374
  mimeType,
2100
2375
  segment,
2101
- model: MODELS[1].id,
2376
+ model: MODELS.flash,
2102
2377
  resolution,
2103
2378
  pass2Result: pass2 ?? void 0
2104
2379
  }),
@@ -2107,7 +2382,7 @@ async function runPipeline(config) {
2107
2382
  `segment ${i} pass3c`
2108
2383
  );
2109
2384
  if (pass3cAttempt.error !== null) {
2110
- log7.warn(pass3cAttempt.error);
2385
+ log8.warn(pass3cAttempt.error);
2111
2386
  errors.push(pass3cAttempt.error);
2112
2387
  pass3c = null;
2113
2388
  } else {
@@ -2126,7 +2401,7 @@ async function runPipeline(config) {
2126
2401
  fileUri,
2127
2402
  mimeType,
2128
2403
  segment,
2129
- model: MODELS[0].id,
2404
+ model: MODELS.flash,
2130
2405
  resolution,
2131
2406
  pass1Result: pass1 ?? void 0,
2132
2407
  pass2Result: pass2 ?? void 0
@@ -2136,7 +2411,7 @@ async function runPipeline(config) {
2136
2411
  `segment ${i} pass3d`
2137
2412
  );
2138
2413
  if (pass3dAttempt.error !== null) {
2139
- log7.warn(pass3dAttempt.error);
2414
+ log8.warn(pass3dAttempt.error);
2140
2415
  errors.push(pass3dAttempt.error);
2141
2416
  pass3d = null;
2142
2417
  } else {
@@ -2145,14 +2420,14 @@ async function runPipeline(config) {
2145
2420
  }
2146
2421
  onProgress?.({ phase: "pass3d", segment: i, totalSegments: n, status: "done" });
2147
2422
  }
2148
- results.push({ index: segment.index, pass1, pass2, pass3a, pass3c, pass3d });
2423
+ results.push({ index: segment.index, pass1, pass2, pass3c, pass3d });
2149
2424
  }
2150
2425
  if (pass1RanOnce) passesRun.push("pass1");
2151
2426
  if (pass2RanOnce) passesRun.push("pass2");
2152
- if (pass3aRanOnce) passesRun.push("pass3a");
2153
2427
  if (pass3cRanOnce) passesRun.push("pass3c");
2154
2428
  if (pass3dRanOnce) passesRun.push("pass3d");
2155
2429
  if (wasInterrupted) {
2430
+ if (strategy.passes.includes("code")) interruptedPasses.push("pass3a");
2156
2431
  if (strategy.passes.includes("people")) interruptedPasses.push("pass3b");
2157
2432
  if (strategy.passes.includes("synthesis")) interruptedPasses.push("synthesis");
2158
2433
  return {
@@ -2163,20 +2438,23 @@ async function runPipeline(config) {
2163
2438
  strategy,
2164
2439
  synthesisResult: void 0,
2165
2440
  peopleExtraction: null,
2441
+ codeReconstruction: null,
2442
+ uncertainCodeFiles: void 0,
2166
2443
  interrupted: interruptedPasses
2167
2444
  };
2168
2445
  }
2446
+ const pass1Results = results.map((r) => r.pass1);
2447
+ const pass2Results = results.map((r) => r.pass2);
2169
2448
  let peopleExtraction = null;
2170
2449
  if (strategy.passes.includes("people")) {
2171
2450
  onProgress?.({ phase: "pass3b", segment: 0, totalSegments: 1, status: "running" });
2172
- const pass1Results = results.map((r) => r.pass1);
2173
2451
  const pass3bAttempt = await withRetry(
2174
2452
  () => rateLimiter.execute(
2175
2453
  () => runPeopleExtraction({
2176
2454
  client,
2177
2455
  fileUri,
2178
2456
  mimeType,
2179
- model: MODELS[0].id,
2457
+ model: MODELS.flash,
2180
2458
  pass1Results
2181
2459
  }),
2182
2460
  { onWait }
@@ -2184,7 +2462,7 @@ async function runPipeline(config) {
2184
2462
  "pass3b"
2185
2463
  );
2186
2464
  if (pass3bAttempt.error !== null) {
2187
- log7.warn(pass3bAttempt.error);
2465
+ log8.warn(pass3bAttempt.error);
2188
2466
  errors.push(pass3bAttempt.error);
2189
2467
  } else {
2190
2468
  peopleExtraction = pass3bAttempt.result;
@@ -2192,6 +2470,53 @@ async function runPipeline(config) {
2192
2470
  onProgress?.({ phase: "pass3b", segment: 0, totalSegments: 1, status: "done" });
2193
2471
  if (peopleExtraction !== null) passesRun.push("pass3b");
2194
2472
  }
2473
+ let codeReconstruction = null;
2474
+ let uncertainCodeFiles;
2475
+ if (strategy.passes.includes("code")) {
2476
+ const consensusConfig = { runs: 3, minAgreement: 2 };
2477
+ const consensusResult = await runCodeConsensus({
2478
+ config: consensusConfig,
2479
+ runFn: () => rateLimiter.execute(
2480
+ () => runCodeReconstruction({
2481
+ client,
2482
+ fileUri,
2483
+ mimeType,
2484
+ duration,
2485
+ model: MODELS.pro,
2486
+ resolution,
2487
+ pass1Results,
2488
+ pass2Results
2489
+ }),
2490
+ { onWait }
2491
+ ),
2492
+ pass2Results,
2493
+ onProgress: (run, total) => {
2494
+ onProgress?.({ phase: "pass3a", segment: run - 1, totalSegments: total, status: "running" });
2495
+ }
2496
+ });
2497
+ onProgress?.({ phase: "pass3a", segment: consensusConfig.runs - 1, totalSegments: consensusConfig.runs, status: "done" });
2498
+ if (consensusResult.runsCompleted === 0) {
2499
+ const errMsg = "pass3a: all consensus runs failed";
2500
+ log8.warn(errMsg);
2501
+ errors.push(errMsg);
2502
+ } else {
2503
+ const validationResult = validateCodeReconstruction({
2504
+ consensusResult,
2505
+ pass2Results
2506
+ });
2507
+ const allFiles = [...validationResult.confirmed, ...validationResult.uncertain];
2508
+ if (allFiles.length > 0) {
2509
+ codeReconstruction = {
2510
+ files: allFiles,
2511
+ dependencies_mentioned: consensusResult.mergedDependencies,
2512
+ build_commands: consensusResult.mergedBuildCommands
2513
+ };
2514
+ uncertainCodeFiles = validationResult.uncertain.map((f) => f.filename);
2515
+ }
2516
+ log8.info(`Code: ${validationResult.confirmed.length} confirmed, ${validationResult.uncertain.length} uncertain, ${validationResult.rejected.length} rejected`);
2517
+ }
2518
+ if (codeReconstruction !== null) passesRun.push("pass3a");
2519
+ }
2195
2520
  let synthesisResult;
2196
2521
  if (strategy.passes.includes("synthesis")) {
2197
2522
  onProgress?.({ phase: "synthesis", segment: 0, totalSegments: 1, status: "running" });
@@ -2199,10 +2524,11 @@ async function runPipeline(config) {
2199
2524
  () => rateLimiter.execute(
2200
2525
  () => runSynthesis({
2201
2526
  client,
2202
- model: MODELS[1].id,
2527
+ model: MODELS.pro,
2203
2528
  segmentResults: results,
2204
2529
  videoProfile,
2205
2530
  peopleExtraction,
2531
+ codeReconstruction,
2206
2532
  context: config.context
2207
2533
  }),
2208
2534
  { onWait }
@@ -2210,7 +2536,7 @@ async function runPipeline(config) {
2210
2536
  "synthesis"
2211
2537
  );
2212
2538
  if (synthAttempt.error !== null) {
2213
- log7.warn(synthAttempt.error);
2539
+ log8.warn(synthAttempt.error);
2214
2540
  errors.push(synthAttempt.error);
2215
2541
  } else {
2216
2542
  synthesisResult = synthAttempt.result ?? void 0;
@@ -2226,6 +2552,8 @@ async function runPipeline(config) {
2226
2552
  strategy,
2227
2553
  synthesisResult,
2228
2554
  peopleExtraction,
2555
+ codeReconstruction,
2556
+ uncertainCodeFiles,
2229
2557
  interrupted: void 0
2230
2558
  };
2231
2559
  }
@@ -2480,13 +2808,13 @@ function renderChangeRow(change) {
2480
2808
  }
2481
2809
  function buildTimeline(allFiles) {
2482
2810
  if (allFiles.length === 0) {
2483
- return "# Code Timeline\n\n_No code files reconstructed._";
2811
+ return "# Code Timeline\n\nNo code files could be reliably reconstructed.";
2484
2812
  }
2485
2813
  const lines = ["# Code Timeline", ""];
2486
2814
  const annotated = [];
2487
- for (const { file, segmentIndex } of allFiles) {
2815
+ for (const file of allFiles) {
2488
2816
  for (const change of file.changes) {
2489
- annotated.push({ timestamp: change.timestamp, file, change, segmentIndex });
2817
+ annotated.push({ timestamp: change.timestamp, file, change });
2490
2818
  }
2491
2819
  }
2492
2820
  annotated.sort((a, b) => parseTimestamp(a.timestamp) - parseTimestamp(b.timestamp));
@@ -2501,7 +2829,7 @@ function buildTimeline(allFiles) {
2501
2829
  lines.push("");
2502
2830
  }
2503
2831
  const seenFiles = /* @__PURE__ */ new Set();
2504
- for (const { file } of allFiles) {
2832
+ for (const file of allFiles) {
2505
2833
  if (seenFiles.has(file.filename)) continue;
2506
2834
  seenFiles.add(file.filename);
2507
2835
  lines.push(`## ${file.filename}`);
@@ -2521,20 +2849,20 @@ function buildTimeline(allFiles) {
2521
2849
  return lines.join("\n");
2522
2850
  }
2523
2851
  function writeCodeFiles(params) {
2524
- const { pipelineResult } = params;
2525
- const { segments } = pipelineResult;
2852
+ const { pipelineResult, uncertainFiles } = params;
2853
+ const { codeReconstruction } = pipelineResult;
2526
2854
  const allFiles = [];
2527
- const latestByFilename = /* @__PURE__ */ new Map();
2528
- for (const seg of segments) {
2529
- if (seg.pass3a == null) continue;
2530
- for (const file of seg.pass3a.files) {
2531
- allFiles.push({ file, segmentIndex: seg.index });
2532
- latestByFilename.set(file.filename, { file, segmentIndex: seg.index });
2533
- }
2534
- }
2535
2855
  const files = /* @__PURE__ */ new Map();
2536
- for (const [filename, { file }] of latestByFilename) {
2537
- files.set(filename, file.final_content);
2856
+ if (codeReconstruction != null) {
2857
+ for (const file of codeReconstruction.files) {
2858
+ allFiles.push(file);
2859
+ let content = file.final_content;
2860
+ if (uncertainFiles?.has(file.filename)) {
2861
+ content = `// [note: this file passed consensus but could not be cross-referenced against visual observations \u2014 content may be approximate]
2862
+ ${content}`;
2863
+ }
2864
+ files.set(file.filename, content);
2865
+ }
2538
2866
  }
2539
2867
  const timeline = buildTimeline(allFiles);
2540
2868
  return { files, timeline };
@@ -2981,7 +3309,7 @@ function writeMetadata(params) {
2981
3309
  }
2982
3310
  function writeRawOutput(pipelineResult) {
2983
3311
  const files = /* @__PURE__ */ new Map();
2984
- const { segments, videoProfile, peopleExtraction, synthesisResult } = pipelineResult;
3312
+ const { segments, videoProfile, peopleExtraction, synthesisResult, codeReconstruction } = pipelineResult;
2985
3313
  if (videoProfile != null) {
2986
3314
  files.set("pass0-scene.json", JSON.stringify(videoProfile, null, 2));
2987
3315
  }
@@ -2993,9 +3321,6 @@ function writeRawOutput(pipelineResult) {
2993
3321
  if (seg.pass2 != null) {
2994
3322
  files.set(`pass2-seg${n}.json`, JSON.stringify(seg.pass2, null, 2));
2995
3323
  }
2996
- if (seg.pass3a != null) {
2997
- files.set(`pass3a-seg${n}.json`, JSON.stringify(seg.pass3a, null, 2));
2998
- }
2999
3324
  if (seg.pass3c != null) {
3000
3325
  files.set(`pass3c-seg${n}.json`, JSON.stringify(seg.pass3c, null, 2));
3001
3326
  }
@@ -3003,6 +3328,9 @@ function writeRawOutput(pipelineResult) {
3003
3328
  files.set(`pass3d-seg${n}.json`, JSON.stringify(seg.pass3d, null, 2));
3004
3329
  }
3005
3330
  }
3331
+ if (codeReconstruction != null) {
3332
+ files.set("pass3a.json", JSON.stringify(codeReconstruction, null, 2));
3333
+ }
3006
3334
  if (peopleExtraction != null) {
3007
3335
  files.set("pass3b-people.json", JSON.stringify(peopleExtraction, null, 2));
3008
3336
  }
@@ -3020,42 +3348,22 @@ function resolveFilesToGenerate(params) {
3020
3348
  const { pipelineResult } = params;
3021
3349
  const { synthesisResult, segments, peopleExtraction } = pipelineResult;
3022
3350
  const optional = /* @__PURE__ */ new Set();
3023
- if (synthesisResult != null) {
3024
- const knownOutputFiles = /* @__PURE__ */ new Set([
3025
- "transcript.md",
3026
- "combined.md",
3027
- "notes.md",
3028
- "people.md",
3029
- "chat.md",
3030
- "links.md",
3031
- "action-items.md",
3032
- "insights.md",
3033
- "code/"
3034
- ]);
3035
- for (const f of synthesisResult.files_to_generate) {
3036
- if (knownOutputFiles.has(f)) {
3037
- optional.add(f);
3038
- } else {
3039
- optional.add("code/");
3040
- }
3041
- }
3042
- } else {
3043
- const hasPass2 = segments.some((s) => s.pass2 != null);
3044
- const hasPass3a = segments.some((s) => s.pass3a != null);
3045
- const hasPass3c = segments.some((s) => s.pass3c != null);
3046
- const hasPass3d = segments.some((s) => s.pass3d != null);
3047
- if (hasPass2) optional.add("combined.md");
3048
- if (hasPass3a) optional.add("code/");
3049
- if (hasPass3c) {
3050
- optional.add("chat.md");
3051
- optional.add("links.md");
3052
- }
3053
- if (hasPass3d) {
3054
- optional.add("action-items.md");
3055
- optional.add("insights.md");
3056
- }
3057
- if (peopleExtraction != null) optional.add("people.md");
3058
- }
3351
+ const hasPass2 = segments.some((s) => s.pass2 != null);
3352
+ const hasPass3a = pipelineResult.codeReconstruction != null;
3353
+ const hasPass3c = segments.some((s) => s.pass3c != null);
3354
+ const hasPass3d = segments.some((s) => s.pass3d != null);
3355
+ if (hasPass2) optional.add("combined.md");
3356
+ if (hasPass3a) optional.add("code/");
3357
+ if (hasPass3c) {
3358
+ optional.add("chat.md");
3359
+ optional.add("links.md");
3360
+ }
3361
+ if (hasPass3d) {
3362
+ optional.add("action-items.md");
3363
+ optional.add("insights.md");
3364
+ }
3365
+ if (synthesisResult != null) optional.add("notes.md");
3366
+ if (peopleExtraction != null) optional.add("people.md");
3059
3367
  return optional;
3060
3368
  }
3061
3369
  async function generateOutput(params) {
@@ -3091,7 +3399,8 @@ async function generateOutput(params) {
3091
3399
  }
3092
3400
  if (filesToGenerate.has("code/")) {
3093
3401
  try {
3094
- const { files, timeline } = writeCodeFiles({ pipelineResult });
3402
+ const uncertainSet = new Set(pipelineResult.uncertainCodeFiles ?? []);
3403
+ const { files, timeline } = writeCodeFiles({ pipelineResult, uncertainFiles: uncertainSet });
3095
3404
  for (const [filename, content] of files) {
3096
3405
  try {
3097
3406
  await writeOutputFile(`code/${filename}`, content);
@@ -3212,7 +3521,7 @@ async function generateOutput(params) {
3212
3521
  }
3213
3522
 
3214
3523
  // src/core/shutdown.ts
3215
- import { log as log8 } from "@clack/prompts";
3524
+ import { log as log9 } from "@clack/prompts";
3216
3525
  function createShutdownHandler(params) {
3217
3526
  const { client, uploadedFileNames } = params;
3218
3527
  let shuttingDown = false;
@@ -3223,7 +3532,7 @@ function createShutdownHandler(params) {
3223
3532
  return;
3224
3533
  }
3225
3534
  shuttingDown = true;
3226
- log8.warn("Interrupted. Saving partial results...");
3535
+ log9.warn("Interrupted. Saving partial results...");
3227
3536
  const forceExitHandler = () => {
3228
3537
  process.exit(1);
3229
3538
  };
@@ -3323,12 +3632,12 @@ var main = defineCommand({
3323
3632
  if (result.uploadedFileName != null) {
3324
3633
  uploadedFileNames = [result.uploadedFileName];
3325
3634
  }
3326
- videoTitle = basename2(resolved.value, extname2(resolved.value));
3635
+ videoTitle = basename3(resolved.value, extname2(resolved.value));
3327
3636
  }
3328
3637
  const mins = Math.floor(duration / 60);
3329
3638
  const secs = Math.round(duration % 60);
3330
- log9.info(`Duration: ${pc5.cyan(`${mins}m ${secs}s`)} (${Math.round(duration)}s)`);
3331
- const model = MODELS[0].id;
3639
+ log10.info(`Duration: ${pc5.cyan(`${mins}m ${secs}s`)} (${Math.round(duration)}s)`);
3640
+ const model = MODELS.flash;
3332
3641
  const outputDir = resolve(args.output);
3333
3642
  const slug = slugify(videoTitle);
3334
3643
  const finalOutputDir = `${outputDir}/${slug}`;
@@ -3370,18 +3679,19 @@ var main = defineCommand({
3370
3679
  processingTimeMs: elapsedMs
3371
3680
  });
3372
3681
  const fileCount = outputResult.filesGenerated.length;
3373
- log9.success(
3682
+ log10.success(
3374
3683
  `Output: ${pc5.cyan(finalOutputDir + "/")} \u2014 ${pc5.cyan(String(fileCount))} files generated ${pc5.dim("(guide.md for overview)")}`
3375
3684
  );
3376
3685
  if (outputResult.errors.length > 0) {
3377
- log9.warn(`Output errors: ${pc5.yellow(String(outputResult.errors.length))}`);
3686
+ log10.warn(`Output errors: ${pc5.yellow(String(outputResult.errors.length))}`);
3378
3687
  for (const err of outputResult.errors) {
3379
- log9.warn(pc5.dim(` ${err}`));
3688
+ log10.warn(pc5.dim(` ${err}`));
3380
3689
  }
3381
3690
  }
3382
3691
  } catch (err) {
3383
- const message = err instanceof Error ? err.message : String(err);
3384
- log9.error(pc5.red(message));
3692
+ const raw = err instanceof Error ? err.message : String(err);
3693
+ const message = raw.split("\n")[0].slice(0, 200);
3694
+ log10.error(pc5.red(message));
3385
3695
  process.exit(1);
3386
3696
  }
3387
3697
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidistill",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Video intelligence distiller — extract structured notes, transcripts, and insights from any video using Gemini",
5
5
  "type": "module",
6
6
  "license": "MIT",