task-summary-extractor 8.3.0 → 9.0.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/.env.example +38 -0
- package/ARCHITECTURE.md +99 -3
- package/EXPLORATION.md +148 -89
- package/QUICK_START.md +5 -2
- package/README.md +51 -7
- package/bin/taskex.js +11 -4
- package/package.json +38 -5
- package/src/config.js +52 -3
- package/src/modes/focused-reanalysis.js +2 -1
- package/src/modes/progress-updater.js +1 -1
- package/src/phases/_shared.js +43 -0
- package/src/phases/compile.js +101 -0
- package/src/phases/deep-dive.js +118 -0
- package/src/phases/discover.js +178 -0
- package/src/phases/init.js +192 -0
- package/src/phases/output.js +238 -0
- package/src/phases/process-media.js +633 -0
- package/src/phases/services.js +104 -0
- package/src/phases/summary.js +86 -0
- package/src/pipeline.js +431 -1463
- package/src/renderers/docx.js +531 -0
- package/src/renderers/html.js +672 -0
- package/src/renderers/markdown.js +15 -183
- package/src/renderers/pdf.js +90 -0
- package/src/renderers/shared.js +211 -0
- package/src/schemas/analysis-compiled.schema.json +381 -0
- package/src/schemas/analysis-segment.schema.json +380 -0
- package/src/services/doc-parser.js +346 -0
- package/src/services/gemini.js +101 -44
- package/src/services/video.js +123 -8
- package/src/utils/adaptive-budget.js +6 -4
- package/src/utils/checkpoint.js +2 -1
- package/src/utils/cli.js +131 -110
- package/src/utils/colors.js +83 -0
- package/src/utils/confidence-filter.js +138 -0
- package/src/utils/diff-engine.js +2 -1
- package/src/utils/global-config.js +6 -5
- package/src/utils/health-dashboard.js +11 -9
- package/src/utils/json-parser.js +4 -2
- package/src/utils/learning-loop.js +3 -2
- package/src/utils/progress-bar.js +286 -0
- package/src/utils/quality-gate.js +4 -2
- package/src/utils/retry.js +3 -1
- package/src/utils/schema-validator.js +314 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// --- Config ---
|
|
7
|
+
const config = require('../config');
|
|
8
|
+
|
|
9
|
+
// --- Services ---
|
|
10
|
+
const { uploadToStorage } = require('../services/firebase');
|
|
11
|
+
|
|
12
|
+
// --- Renderers ---
|
|
13
|
+
const { renderResultsMarkdown } = require('../renderers/markdown');
|
|
14
|
+
const { renderResultsHtml } = require('../renderers/html');
|
|
15
|
+
const { renderResultsPdf } = require('../renderers/pdf');
|
|
16
|
+
const { renderResultsDocx } = require('../renderers/docx');
|
|
17
|
+
|
|
18
|
+
// --- Utils ---
|
|
19
|
+
const { loadPreviousCompilation, generateDiff, renderDiffMarkdown } = require('../utils/diff-engine');
|
|
20
|
+
const { filterByConfidence } = require('../utils/confidence-filter');
|
|
21
|
+
const { c } = require('../utils/colors');
|
|
22
|
+
|
|
23
|
+
// --- Shared state ---
|
|
24
|
+
const { getLog, phaseTimer, PROJECT_ROOT } = require('./_shared');
|
|
25
|
+
|
|
26
|
+
/** Check whether a given output type should be rendered. */
|
|
27
|
+
function shouldRender(opts, type) {
|
|
28
|
+
if (opts.format === 'all') return true;
|
|
29
|
+
return opts.format === type;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ======================== PHASE: OUTPUT ========================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write results JSON, generate Markdown, upload final artifacts.
|
|
36
|
+
* Returns { runDir, jsonPath, mdPath }.
|
|
37
|
+
*/
|
|
38
|
+
async function phaseOutput(ctx, results, compiledAnalysis, compilationRun, compilationPayload) {
|
|
39
|
+
const log = getLog();
|
|
40
|
+
const timer = phaseTimer('output');
|
|
41
|
+
const { opts, targetDir, storage, firebaseReady, callName, progress, costTracker, userName } = ctx;
|
|
42
|
+
|
|
43
|
+
progress.setPhase('output');
|
|
44
|
+
|
|
45
|
+
// Determine output directory
|
|
46
|
+
const runTs = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
47
|
+
const runDir = opts.outputDir
|
|
48
|
+
? path.resolve(opts.outputDir)
|
|
49
|
+
: path.join(targetDir, 'runs', runTs);
|
|
50
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
51
|
+
log.step(`Run folder created → ${runDir}`);
|
|
52
|
+
|
|
53
|
+
// Copy compilation JSON into run folder
|
|
54
|
+
if (compilationPayload) {
|
|
55
|
+
const runCompFile = path.join(runDir, 'compilation.json');
|
|
56
|
+
fs.writeFileSync(runCompFile, JSON.stringify(compilationPayload, null, 2), 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Attach cost summary to results
|
|
60
|
+
results.costSummary = costTracker.getSummary();
|
|
61
|
+
|
|
62
|
+
// Write results JSON (always written unless --format excludes it)
|
|
63
|
+
const jsonPath = path.join(runDir, 'results.json');
|
|
64
|
+
if (shouldRender(opts, 'json') || opts.format === 'all') {
|
|
65
|
+
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
|
|
66
|
+
log.step(`Results JSON saved → ${jsonPath}`);
|
|
67
|
+
} else {
|
|
68
|
+
// Still write JSON internally (needed for uploads/diffs) but don't advertise
|
|
69
|
+
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Generate Markdown
|
|
73
|
+
const mdPath = path.join(runDir, 'results.md');
|
|
74
|
+
const totalSegs = results.files.reduce((s, f) => s + f.segmentCount, 0);
|
|
75
|
+
|
|
76
|
+
// Apply confidence filter for rendered output (MD/HTML) — results.json keeps full data
|
|
77
|
+
let renderData = compiledAnalysis;
|
|
78
|
+
if (compiledAnalysis && opts.minConfidence && opts.minConfidence !== 'low') {
|
|
79
|
+
renderData = filterByConfidence(compiledAnalysis, opts.minConfidence);
|
|
80
|
+
const meta = renderData._filterMeta;
|
|
81
|
+
if (meta && meta.removed > 0) {
|
|
82
|
+
console.log(` Confidence filter: ${meta.minConfidence} → kept ${meta.filteredCounts.total}/${meta.originalCounts.total} items (${meta.removed} removed)`);
|
|
83
|
+
log.step(`Confidence filter applied: ${meta.minConfidence}, removed ${meta.removed} items`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build shared meta for renderers
|
|
88
|
+
const renderMeta = {
|
|
89
|
+
callName: results.callName,
|
|
90
|
+
processedAt: results.processedAt,
|
|
91
|
+
geminiModel: config.GEMINI_MODEL,
|
|
92
|
+
userName,
|
|
93
|
+
segmentCount: totalSegs,
|
|
94
|
+
compilation: compilationRun || null,
|
|
95
|
+
costSummary: results.costSummary,
|
|
96
|
+
segments: results.files.flatMap(f => {
|
|
97
|
+
const speed = results.settings?.speed || 1;
|
|
98
|
+
let cum = 0;
|
|
99
|
+
return (f.segments || []).map(s => {
|
|
100
|
+
const startSec = cum;
|
|
101
|
+
cum += (s.durationSeconds || 0) * speed;
|
|
102
|
+
return {
|
|
103
|
+
file: s.segmentFile,
|
|
104
|
+
duration: s.duration,
|
|
105
|
+
durationSeconds: s.durationSeconds,
|
|
106
|
+
sizeMB: s.fileSizeMB,
|
|
107
|
+
video: f.originalFile,
|
|
108
|
+
startTimeSec: startSec,
|
|
109
|
+
endTimeSec: cum,
|
|
110
|
+
segmentNumber: (s.segmentIndex || 0) + 1,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}),
|
|
114
|
+
settings: results.settings,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (renderData && !renderData._incomplete) {
|
|
118
|
+
// --- Markdown report ---
|
|
119
|
+
if (shouldRender(opts, 'md')) {
|
|
120
|
+
const mdContent = renderResultsMarkdown({ compiled: renderData, meta: renderMeta });
|
|
121
|
+
fs.writeFileSync(mdPath, mdContent, 'utf8');
|
|
122
|
+
log.step(`Results MD saved (compiled) → ${mdPath}`);
|
|
123
|
+
console.log(` ${c.success('Markdown report (AI-compiled)')} → ${c.cyan(path.basename(mdPath))}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- HTML report ---
|
|
127
|
+
if (shouldRender(opts, 'html') && !opts.noHtml) {
|
|
128
|
+
const htmlPath = path.join(runDir, 'results.html');
|
|
129
|
+
const htmlContent = renderResultsHtml({ compiled: renderData, meta: renderMeta });
|
|
130
|
+
fs.writeFileSync(htmlPath, htmlContent, 'utf8');
|
|
131
|
+
log.step(`Results HTML saved → ${htmlPath}`);
|
|
132
|
+
console.log(` ${c.success('HTML report')} → ${c.cyan(path.basename(htmlPath))}`);
|
|
133
|
+
|
|
134
|
+
// --- PDF report (requires HTML first) ---
|
|
135
|
+
if (shouldRender(opts, 'pdf')) {
|
|
136
|
+
try {
|
|
137
|
+
const pdfPath = path.join(runDir, 'results.pdf');
|
|
138
|
+
const pdfInfo = await renderResultsPdf(htmlContent, pdfPath);
|
|
139
|
+
log.step(`Results PDF saved → ${pdfPath}`);
|
|
140
|
+
console.log(` ${c.success('PDF report')} → ${c.cyan(path.basename(pdfPath))} ${c.dim(`(${(pdfInfo.bytes / 1024).toFixed(0)} KB)`)}`);
|
|
141
|
+
} catch (pdfErr) {
|
|
142
|
+
console.warn(` ${c.warn('PDF generation failed:')} ${pdfErr.message}`);
|
|
143
|
+
log.warn(`PDF generation error: ${pdfErr.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else if (shouldRender(opts, 'pdf')) {
|
|
147
|
+
// PDF requested without HTML — generate HTML in memory
|
|
148
|
+
try {
|
|
149
|
+
const htmlContent = renderResultsHtml({ compiled: renderData, meta: renderMeta });
|
|
150
|
+
const pdfPath = path.join(runDir, 'results.pdf');
|
|
151
|
+
const pdfInfo = await renderResultsPdf(htmlContent, pdfPath);
|
|
152
|
+
log.step(`Results PDF saved → ${pdfPath}`);
|
|
153
|
+
console.log(` ${c.success('PDF report')} → ${c.cyan(path.basename(pdfPath))} ${c.dim(`(${(pdfInfo.bytes / 1024).toFixed(0)} KB)`)}`);
|
|
154
|
+
} catch (pdfErr) {
|
|
155
|
+
console.warn(` ${c.warn('PDF generation failed:')} ${pdfErr.message}`);
|
|
156
|
+
log.warn(`PDF generation error: ${pdfErr.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- DOCX report ---
|
|
161
|
+
if (shouldRender(opts, 'docx')) {
|
|
162
|
+
try {
|
|
163
|
+
const docxPath = path.join(runDir, 'results.docx');
|
|
164
|
+
const docxBuffer = await renderResultsDocx({ compiled: renderData, meta: renderMeta });
|
|
165
|
+
fs.writeFileSync(docxPath, docxBuffer);
|
|
166
|
+
log.step(`Results DOCX saved → ${docxPath}`);
|
|
167
|
+
console.log(` ${c.success('DOCX report')} → ${c.cyan(path.basename(docxPath))} ${c.dim(`(${(docxBuffer.length / 1024).toFixed(0)} KB)`)}`);
|
|
168
|
+
} catch (docxErr) {
|
|
169
|
+
console.warn(` ${c.warn('DOCX generation failed:')} ${docxErr.message}`);
|
|
170
|
+
log.warn(`DOCX generation error: ${docxErr.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else if (shouldRender(opts, 'md')) {
|
|
174
|
+
const { renderResultsMarkdownLegacy } = require('../renderers/markdown');
|
|
175
|
+
const mdContent = renderResultsMarkdownLegacy(results);
|
|
176
|
+
fs.writeFileSync(mdPath, mdContent, 'utf8');
|
|
177
|
+
log.step(`Results MD saved (legacy merge) → ${mdPath}`);
|
|
178
|
+
console.log(` ${c.success('Markdown report (legacy merge)')} → ${c.cyan(path.basename(mdPath))}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// === DIFF ENGINE (v6) ===
|
|
182
|
+
let diffResult = null;
|
|
183
|
+
if (!opts.disableDiff && compiledAnalysis) {
|
|
184
|
+
try {
|
|
185
|
+
const prevComp = loadPreviousCompilation(targetDir, runTs);
|
|
186
|
+
if (prevComp && prevComp.compiled) {
|
|
187
|
+
diffResult = generateDiff(compiledAnalysis, prevComp.compiled);
|
|
188
|
+
// Inject the previous run timestamp into the diff
|
|
189
|
+
if (diffResult.hasDiff) {
|
|
190
|
+
diffResult.previousTimestamp = prevComp.timestamp;
|
|
191
|
+
const diffMd = renderDiffMarkdown(diffResult);
|
|
192
|
+
if (shouldRender(opts, 'md')) {
|
|
193
|
+
fs.appendFileSync(mdPath, '\n\n' + diffMd, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
fs.writeFileSync(path.join(runDir, 'diff.json'), JSON.stringify(diffResult, null, 2), 'utf8');
|
|
196
|
+
log.step(`Diff report: ${diffResult.totals.newItems} new, ${diffResult.totals.removedItems} removed, ${diffResult.totals.changedItems} changed`);
|
|
197
|
+
console.log(` ${c.success('Diff report appended')} (vs ${c.dim(prevComp.timestamp)})`);
|
|
198
|
+
} else {
|
|
199
|
+
console.log(` ${c.info('No differences vs previous run')} (${c.dim(prevComp.timestamp)})`);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.log(` ${c.info('No previous compilation found for diff comparison')}`);
|
|
203
|
+
}
|
|
204
|
+
} catch (diffErr) {
|
|
205
|
+
console.warn(` ${c.warn('Diff generation failed:')} ${diffErr.message}`);
|
|
206
|
+
log.warn(`Diff generation error: ${diffErr.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Upload results to Firebase
|
|
211
|
+
if (firebaseReady && !opts.skipUpload && !opts.dryRun) {
|
|
212
|
+
try {
|
|
213
|
+
const resultsStoragePath = `calls/${callName}/runs/${runTs}/results.json`;
|
|
214
|
+
// Results always upload fresh (never skip-existing) — they change every run
|
|
215
|
+
const url = await uploadToStorage(storage, jsonPath, resultsStoragePath);
|
|
216
|
+
results.storageUrl = url;
|
|
217
|
+
fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
|
|
218
|
+
console.log(` ${c.success('Results JSON uploaded')} → ${c.dim(resultsStoragePath)}`);
|
|
219
|
+
|
|
220
|
+
if (shouldRender(opts, 'md') && fs.existsSync(mdPath)) {
|
|
221
|
+
const mdStoragePath = `calls/${callName}/runs/${runTs}/results.md`;
|
|
222
|
+
await uploadToStorage(storage, mdPath, mdStoragePath);
|
|
223
|
+
console.log(` ${c.success('Results MD uploaded')} → ${c.dim(mdStoragePath)}`);
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.warn(` ${c.warn('Results upload failed:')} ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
} else if (opts.skipUpload) {
|
|
229
|
+
console.log(` ${c.warn('Skipping results upload')} ${c.dim('(--skip-upload)')}`);
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` ${c.warn('Skipping results upload')} ${c.dim('(Firebase auth not configured)')}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
timer.end();
|
|
235
|
+
return { runDir, jsonPath, mdPath, runTs };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = phaseOutput;
|