task-summary-extractor 9.7.0 → 9.8.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/README.md +2 -1
- package/package.json +1 -1
- package/src/modes/deep-summary.js +37 -0
- package/src/modes/focused-reanalysis.js +16 -1
- package/src/phases/process-media.js +36 -11
- package/src/utils/cli.js +5 -2
- package/src/utils/schema-validator.js +33 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Task Summary Extractor
|
|
2
2
|
|
|
3
|
-
> **v9.
|
|
3
|
+
> **v9.8.0** — AI-powered content analysis CLI — meetings, recordings, documents, or any mix. Install globally, run anywhere.
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-green" alt="Node.js" />
|
|
@@ -598,6 +598,7 @@ task-summary-extractor/
|
|
|
598
598
|
|
|
599
599
|
| Version | Highlights |
|
|
600
600
|
|---------|-----------|
|
|
601
|
+
| **v9.8.0** | **Schema hardening & transcript handling** — VTT/SRT auto-excluded from deep-summary (transcripts routed to workflow, not summarizer), `normalizeAnalysis()` fills missing `summary`/`confidence`/`discussed_state` defaults before validation, batch Storage URL→File API auto-retry on `INVALID_ARGUMENT`, focused re-analysis skips sparse segments (≤2 items + low density), 367 tests |
|
|
601
602
|
| **v9.7.0** | **Multi-segment batching** — groups consecutive video segments into single Gemini API calls when context window has headroom, greedy bin-packing by token budget (`planSegmentBatches`), `processSegmentBatch()` multi-video API calls, automatic fallback to single-segment on failure, `--no-batch` to disable, codebase audit fixes (unused imports, variable shadowing) |
|
|
602
603
|
| **v9.6.0** | **Interactive CLI UX** — arrow-key navigation for all selectors (folder, model, run mode, formats, confidence, doc exclusion), zero-dependency prompt engine (`interactive.js`), `selectOne()` with ↑↓+Enter, `selectMany()` with Space toggle + A all/none, non-TTY fallback to number input |
|
|
603
604
|
| **v9.5.0** | **Video processing flags** — `--no-compress`, `--speed`, `--segment-time` CLI flags, hardcoded 1200s for raw mode, deprecated `--skip-compression` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-summary-extractor",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.8.0",
|
|
4
4
|
"description": "AI-powered meeting analysis & document generation CLI — video + document processing, deep dive docs, dynamic mode, interactive CLI with model selection, confidence scoring, learning loop, git progress tracking",
|
|
5
5
|
"main": "process_and_upload.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,20 @@ const config = require('../config');
|
|
|
27
27
|
|
|
28
28
|
// ======================== CONSTANTS ========================
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Transcript file extensions that should NEVER be summarized.
|
|
32
|
+
* VTT/SRT files are time-sliced per segment during analysis — summarising
|
|
33
|
+
* them would destroy the timestamp-indexed structure that `sliceVttForSegment`
|
|
34
|
+
* relies on. They are automatically kept at full fidelity.
|
|
35
|
+
*/
|
|
36
|
+
const TRANSCRIPT_EXTENSIONS = ['.vtt', '.srt'];
|
|
37
|
+
|
|
38
|
+
/** Check whether a filename is a transcript file (VTT/SRT). */
|
|
39
|
+
function isTranscriptFile(fileName) {
|
|
40
|
+
const lower = (fileName || '').toLowerCase();
|
|
41
|
+
return TRANSCRIPT_EXTENSIONS.some(ext => lower.endsWith(ext));
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
/** Max tokens for a single summarization call output */
|
|
31
45
|
const SUMMARY_MAX_OUTPUT = 16384;
|
|
32
46
|
|
|
@@ -262,6 +276,13 @@ async function deepSummarize(ai, contextDocs, opts = {}) {
|
|
|
262
276
|
continue;
|
|
263
277
|
}
|
|
264
278
|
|
|
279
|
+
// Auto-exclude transcript files (VTT/SRT) — they are time-sliced per
|
|
280
|
+
// segment during analysis and must retain their timestamp structure.
|
|
281
|
+
if (isTranscriptFile(doc.fileName)) {
|
|
282
|
+
keepFull.push(doc);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
265
286
|
// Keep excluded docs at full fidelity
|
|
266
287
|
if (excludeSet.has(doc.fileName.toLowerCase())) {
|
|
267
288
|
keepFull.push(doc);
|
|
@@ -294,14 +315,22 @@ async function deepSummarize(ai, contextDocs, opts = {}) {
|
|
|
294
315
|
}
|
|
295
316
|
|
|
296
317
|
// Build focus topics from excluded docs (tell summarizer what to prioritize)
|
|
318
|
+
// NOTE: transcript files (VTT/SRT) are auto-excluded but NOT used as focus
|
|
319
|
+
// topics — they are time-sliced per segment and don't represent "topics".
|
|
297
320
|
const focusTopics = keepFull
|
|
298
321
|
.filter(d => d.type === 'inlineText' && excludeSet.has(d.fileName.toLowerCase()))
|
|
299
322
|
.map(d => d.fileName);
|
|
300
323
|
|
|
324
|
+
// Count auto-excluded transcript files for logging
|
|
325
|
+
const autoExcludedTranscripts = keepFull.filter(d => isTranscriptFile(d.fileName));
|
|
326
|
+
|
|
301
327
|
// Batch documents
|
|
302
328
|
const batches = buildBatches(toSummarize);
|
|
303
329
|
|
|
304
330
|
console.log(` Batched ${c.highlight(toSummarize.length)} doc(s) into ${c.highlight(batches.length)} summarization batch(es)`);
|
|
331
|
+
if (autoExcludedTranscripts.length > 0) {
|
|
332
|
+
console.log(` Auto-excluded ${c.highlight(autoExcludedTranscripts.length)} transcript file(s) (VTT/SRT — time-sliced per segment)`);
|
|
333
|
+
}
|
|
305
334
|
if (focusTopics.length > 0) {
|
|
306
335
|
console.log(` Focus topics from ${c.highlight(focusTopics.length)} excluded doc(s):`);
|
|
307
336
|
focusTopics.forEach(t => console.log(` ${c.dim('•')} ${c.cyan(t)}`));
|
|
@@ -350,6 +379,12 @@ async function deepSummarize(ai, contextDocs, opts = {}) {
|
|
|
350
379
|
continue;
|
|
351
380
|
}
|
|
352
381
|
|
|
382
|
+
// Auto-exclude transcript files (VTT/SRT)
|
|
383
|
+
if (isTranscriptFile(doc.fileName)) {
|
|
384
|
+
resultDocs.push(doc);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
353
388
|
// Check if we have a summary for this doc
|
|
354
389
|
const summaryKey = doc.fileName.toLowerCase();
|
|
355
390
|
const summary = allSummaries.get(summaryKey);
|
|
@@ -399,6 +434,8 @@ module.exports = {
|
|
|
399
434
|
deepSummarize,
|
|
400
435
|
summarizeBatch,
|
|
401
436
|
buildBatches,
|
|
437
|
+
isTranscriptFile,
|
|
438
|
+
TRANSCRIPT_EXTENSIONS,
|
|
402
439
|
SUMMARY_MAX_OUTPUT,
|
|
403
440
|
BATCH_MAX_CHARS,
|
|
404
441
|
MIN_SUMMARIZE_LENGTH,
|
|
@@ -136,9 +136,24 @@ function identifyWeaknesses(qualityReport, analysis) {
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// ── Skip focused pass for simple / sparse segments ──────────────────────
|
|
140
|
+
// When the analysis has very few extracted items AND the density dimension
|
|
141
|
+
// is low, the segment is likely simple (chit-chat, small-talk, intro) or
|
|
142
|
+
// the AI legitimately had nothing to extract. A focused pass won't help.
|
|
143
|
+
const totalItems = [
|
|
144
|
+
...(analysis.tickets || []),
|
|
145
|
+
...(analysis.action_items || []),
|
|
146
|
+
...(analysis.change_requests || []),
|
|
147
|
+
...(analysis.blockers || []),
|
|
148
|
+
...(analysis.scope_changes || []),
|
|
149
|
+
].length;
|
|
150
|
+
|
|
151
|
+
const isSparseSegment = totalItems <= 2 && dims.density && dims.density.score < 30;
|
|
152
|
+
|
|
139
153
|
const shouldReanalyze = focusInstructions.length > 0 &&
|
|
140
154
|
qualityReport.score < 60 && // Only re-analyze if quality is truly lacking
|
|
141
|
-
weakAreas.length >= 2
|
|
155
|
+
weakAreas.length >= 2 && // At least 2 weak areas to justify the cost
|
|
156
|
+
!isSparseSegment; // Don't waste tokens on sparse / simple segments
|
|
142
157
|
|
|
143
158
|
const focusPrompt = focusInstructions.length > 0
|
|
144
159
|
? focusInstructions.join('\n\n')
|
|
@@ -352,18 +352,43 @@ async function phaseProcessVideo(ctx, videoPath, videoIndex) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
355
|
+
let batchRun;
|
|
356
|
+
try {
|
|
357
|
+
batchRun = await processSegmentBatch(
|
|
358
|
+
ai, batchSegs,
|
|
359
|
+
`${callName}_${baseName}_batch${bIdx}`,
|
|
360
|
+
contextDocs, previousAnalyses, userName, PKG_ROOT,
|
|
361
|
+
{
|
|
362
|
+
segmentIndices: batchIndices,
|
|
363
|
+
totalSegments: segments.length,
|
|
364
|
+
segmentTimes: batchTimes,
|
|
365
|
+
thinkingBudget: opts.thinkingBudget || 24576,
|
|
366
|
+
noStorageUrl: !!opts.noStorageUrl,
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
} catch (batchErr) {
|
|
370
|
+
const msg = batchErr.message || '';
|
|
371
|
+
// If Storage URL was rejected, retry batch with forced File API uploads
|
|
372
|
+
if (!opts.noStorageUrl && msg.includes('INVALID_ARGUMENT') && batchSegs.some(s => s.storageUrl)) {
|
|
373
|
+
console.log(` ${c.warn('Storage URL rejected — retrying batch with File API uploads...')}`);
|
|
374
|
+
log.warn(`Batch ${bIdx} Storage URL rejected — retrying with noStorageUrl=true`);
|
|
375
|
+
batchRun = await processSegmentBatch(
|
|
376
|
+
ai, batchSegs,
|
|
377
|
+
`${callName}_${baseName}_batch${bIdx}`,
|
|
378
|
+
contextDocs, previousAnalyses, userName, PKG_ROOT,
|
|
379
|
+
{
|
|
380
|
+
segmentIndices: batchIndices,
|
|
381
|
+
totalSegments: segments.length,
|
|
382
|
+
segmentTimes: batchTimes,
|
|
383
|
+
thinkingBudget: opts.thinkingBudget || 24576,
|
|
384
|
+
noStorageUrl: true,
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
console.log(` ${c.success('File API batch retry succeeded')}`);
|
|
388
|
+
} else {
|
|
389
|
+
throw batchErr;
|
|
365
390
|
}
|
|
366
|
-
|
|
391
|
+
}
|
|
367
392
|
|
|
368
393
|
// Save batch run file
|
|
369
394
|
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
package/src/utils/cli.js
CHANGED
|
@@ -587,9 +587,12 @@ async function selectConfidence() {
|
|
|
587
587
|
* @returns {Promise<string[]>} Array of excluded fileName strings
|
|
588
588
|
*/
|
|
589
589
|
async function selectDocsToExclude(contextDocs) {
|
|
590
|
-
|
|
590
|
+
const { isTranscriptFile } = require('../modes/deep-summary');
|
|
591
|
+
|
|
592
|
+
// Only show inlineText docs with actual content, excluding transcript files
|
|
593
|
+
// (VTT/SRT are auto-excluded from summarization — no need to show them)
|
|
591
594
|
const eligible = contextDocs
|
|
592
|
-
.filter(d => d.type === 'inlineText' && d.content && d.content.length > 0)
|
|
595
|
+
.filter(d => d.type === 'inlineText' && d.content && d.content.length > 0 && !isTranscriptFile(d.fileName))
|
|
593
596
|
.map(d => ({
|
|
594
597
|
fileName: d.fileName,
|
|
595
598
|
chars: d.content.length,
|
|
@@ -316,8 +316,12 @@ const ARRAY_DEFAULTS = [
|
|
|
316
316
|
|
|
317
317
|
/**
|
|
318
318
|
* Normalize a parsed analysis object by filling in missing array fields
|
|
319
|
-
* with empty arrays
|
|
320
|
-
*
|
|
319
|
+
* with empty arrays, ensuring required string fields exist, and patching
|
|
320
|
+
* item-level required fields (e.g. confidence) with sensible defaults.
|
|
321
|
+
*
|
|
322
|
+
* This prevents downstream code from crashing when a segment legitimately
|
|
323
|
+
* has no tickets/action_items/etc., and avoids schema-validation failures
|
|
324
|
+
* on truncated AI outputs.
|
|
321
325
|
*
|
|
322
326
|
* Mutates `data` in-place and returns it for convenience.
|
|
323
327
|
*
|
|
@@ -326,11 +330,38 @@ const ARRAY_DEFAULTS = [
|
|
|
326
330
|
*/
|
|
327
331
|
function normalizeAnalysis(data) {
|
|
328
332
|
if (!data || typeof data !== 'object') return data;
|
|
333
|
+
|
|
334
|
+
// Fill missing array fields
|
|
329
335
|
for (const field of ARRAY_DEFAULTS) {
|
|
330
336
|
if (data[field] === undefined || data[field] === null) {
|
|
331
337
|
data[field] = [];
|
|
332
338
|
}
|
|
333
339
|
}
|
|
340
|
+
|
|
341
|
+
// Ensure top-level required string fields
|
|
342
|
+
if (!data.summary && data.summary !== '') {
|
|
343
|
+
data.summary = data.segment_summary || data.overview || '';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Patch ticket items — fill missing required fields with defaults
|
|
347
|
+
if (Array.isArray(data.tickets)) {
|
|
348
|
+
for (const ticket of data.tickets) {
|
|
349
|
+
if (!ticket || typeof ticket !== 'object') continue;
|
|
350
|
+
if (!ticket.confidence) ticket.confidence = 'MEDIUM';
|
|
351
|
+
if (!ticket.discussed_state && ticket.discussed_state !== null) {
|
|
352
|
+
ticket.discussed_state = { summary: '' };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Patch action_items — fill missing confidence
|
|
358
|
+
if (Array.isArray(data.action_items)) {
|
|
359
|
+
for (const item of data.action_items) {
|
|
360
|
+
if (!item || typeof item !== 'object') continue;
|
|
361
|
+
if (!item.confidence) item.confidence = 'MEDIUM';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
334
365
|
return data;
|
|
335
366
|
}
|
|
336
367
|
|