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.
- package/dist/index.js +479 -169
- 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
|
|
5
|
+
import { log as log10 } from "@clack/prompts";
|
|
6
6
|
import pc5 from "picocolors";
|
|
7
|
-
import { basename as
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
764
|
-
9. When a file appears multiple times
|
|
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.
|
|
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
|
-
-
|
|
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:
|
|
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:
|
|
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.
|
|
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,
|
|
1542
|
-
const
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
]
|
|
1869
|
+
];
|
|
1870
|
+
return sections.join("\n\n");
|
|
1818
1871
|
}
|
|
1819
1872
|
async function runSynthesis(params) {
|
|
1820
1873
|
const { client, model } = params;
|
|
1821
|
-
const compiledContext =
|
|
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
|
-
|
|
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
|
-
|
|
2000
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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\
|
|
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
|
|
2815
|
+
for (const file of allFiles) {
|
|
2488
2816
|
for (const change of file.changes) {
|
|
2489
|
-
annotated.push({ timestamp: change.timestamp, file, change
|
|
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
|
|
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 {
|
|
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
|
-
|
|
2537
|
-
|
|
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
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3331
|
-
const model = MODELS
|
|
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
|
-
|
|
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
|
-
|
|
3686
|
+
log10.warn(`Output errors: ${pc5.yellow(String(outputResult.errors.length))}`);
|
|
3378
3687
|
for (const err of outputResult.errors) {
|
|
3379
|
-
|
|
3688
|
+
log10.warn(pc5.dim(` ${err}`));
|
|
3380
3689
|
}
|
|
3381
3690
|
}
|
|
3382
3691
|
} catch (err) {
|
|
3383
|
-
const
|
|
3384
|
-
|
|
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