task-summary-extractor 9.2.2 → 9.4.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 +6 -2
- package/ARCHITECTURE.md +37 -37
- package/QUICK_START.md +1 -1
- package/README.md +32 -13
- package/package.json +2 -3
- package/src/config.js +1 -1
- package/src/modes/deep-summary.js +406 -0
- package/src/phases/discover.js +1 -0
- package/src/phases/init.js +9 -30
- package/src/phases/services.js +61 -1
- package/src/pipeline.js +33 -3
- package/src/services/gemini.js +142 -17
- package/src/utils/cli.js +89 -1
- package/src/utils/context-manager.js +31 -4
- package/EXPLORATION.md +0 -514
package/src/services/gemini.js
CHANGED
|
@@ -90,7 +90,7 @@ async function prepareDocsForGemini(ai, docFileList) {
|
|
|
90
90
|
const pollStart = Date.now();
|
|
91
91
|
while (file.state === 'PROCESSING') {
|
|
92
92
|
if (Date.now() - pollStart > GEMINI_POLL_TIMEOUT_MS) {
|
|
93
|
-
console.warn(` ${c.warn(`${name} —
|
|
93
|
+
console.warn(` ${c.warn(`${name} — file is still processing after ${(GEMINI_POLL_TIMEOUT_MS / 1000).toFixed(0)}s, skipping (you can increase the wait time with GEMINI_POLL_TIMEOUT_MS in .env)`)}`);
|
|
94
94
|
file = null;
|
|
95
95
|
break;
|
|
96
96
|
}
|
|
@@ -287,7 +287,7 @@ async function processWithGemini(ai, filePath, displayName, contextDocs = [], pr
|
|
|
287
287
|
const pollStart = Date.now();
|
|
288
288
|
while (uploaded.state === 'PROCESSING') {
|
|
289
289
|
if (Date.now() - pollStart > GEMINI_POLL_TIMEOUT_MS) {
|
|
290
|
-
throw new Error(`
|
|
290
|
+
throw new Error(`File "${displayName}" is still processing after ${(GEMINI_POLL_TIMEOUT_MS / 1000).toFixed(0)}s. Try again or increase the wait time by setting GEMINI_POLL_TIMEOUT_MS in your .env file.`);
|
|
291
291
|
}
|
|
292
292
|
process.stdout.write(` Processing${'.'.repeat((waited % 3) + 1)} \r`);
|
|
293
293
|
await new Promise(r => setTimeout(r, 5000));
|
|
@@ -343,7 +343,7 @@ async function processWithGemini(ai, filePath, displayName, contextDocs = [], pr
|
|
|
343
343
|
buildProgressiveContext(previousAnalyses, userName) || ''
|
|
344
344
|
);
|
|
345
345
|
const docBudget = Math.max(100000, config.GEMINI_CONTEXT_WINDOW - 350000 - prevContextEstimate);
|
|
346
|
-
console.log(`
|
|
346
|
+
console.log(` Reference docs budget: ${(docBudget / 1000).toFixed(0)}K (${contextDocs.length} doc${contextDocs.length !== 1 ? 's' : ''} available)`);
|
|
347
347
|
|
|
348
348
|
const { selected: selectedDocs, excluded, stats } = selectDocsByBudget(
|
|
349
349
|
contextDocs, docBudget, { segmentIndex }
|
|
@@ -459,16 +459,53 @@ async function processWithGemini(ai, filePath, displayName, contextDocs = [], pr
|
|
|
459
459
|
throw reuploadErr;
|
|
460
460
|
}
|
|
461
461
|
} else {
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
462
|
+
// Handle RESOURCE_EXHAUSTED specifically — shed lower-priority docs and retry
|
|
463
|
+
if (errMsg.includes('RESOURCE_EXHAUSTED') || errMsg.includes('429') || errMsg.includes('quota')) {
|
|
464
|
+
console.warn(` ${c.warn('Context window or quota exceeded — shedding docs and retrying after 30s...')}`);
|
|
465
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
466
|
+
// Rebuild with half the doc budget
|
|
467
|
+
const reducedBudget = Math.floor(docBudget * 0.5);
|
|
468
|
+
const { selected: reducedDocs } = selectDocsByBudget(contextDocs, reducedBudget, { segmentIndex });
|
|
469
|
+
const reducedParts = [contentParts[0]]; // keep video
|
|
470
|
+
for (const doc of reducedDocs) {
|
|
471
|
+
if (doc.type === 'inlineText') {
|
|
472
|
+
let content = doc.content;
|
|
473
|
+
const isVtt = doc.fileName.toLowerCase().endsWith('.vtt') || doc.fileName.toLowerCase().endsWith('.srt');
|
|
474
|
+
if (isVtt && segmentStartSec != null && segmentEndSec != null) {
|
|
475
|
+
content = sliceVttForSegment(content, segmentStartSec, segmentEndSec);
|
|
476
|
+
}
|
|
477
|
+
reducedParts.push({ text: `=== Document: ${doc.fileName} ===\n${content}` });
|
|
478
|
+
} else if (doc.type === 'fileData') {
|
|
479
|
+
reducedParts.push({ fileData: { mimeType: doc.mimeType, fileUri: doc.fileUri } });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Re-add prompt/context parts (last 3-5 parts are prompt, focus, etc.)
|
|
483
|
+
const nonDocParts = contentParts.slice(1 + selectedDocs.length);
|
|
484
|
+
reducedParts.push(...nonDocParts);
|
|
485
|
+
requestPayload.contents[0].parts = reducedParts;
|
|
486
|
+
console.log(` Reduced to ${reducedDocs.length} docs (budget: ${(reducedBudget / 1000).toFixed(0)}K tokens)`);
|
|
487
|
+
try {
|
|
488
|
+
response = await withRetry(
|
|
489
|
+
() => ai.models.generateContent(requestPayload),
|
|
490
|
+
{ label: `Gemini segment analysis — reduced docs (${displayName})`, maxRetries: 1, baseDelay: 5000 }
|
|
491
|
+
);
|
|
492
|
+
console.log(` ${c.success('Reduced-context retry succeeded')}`);
|
|
493
|
+
} catch (reduceErr) {
|
|
494
|
+
console.error(` ${c.error(`Reduced-context retry also failed: ${reduceErr.message}`)}`);
|
|
495
|
+
throw reduceErr;
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
// Log request diagnostics for other errors to aid debugging
|
|
499
|
+
const partSummary = contentParts.map((p, i) => {
|
|
500
|
+
if (p.fileData) return ` [${i}] fileData: ${p.fileData.mimeType} → ${(p.fileData.fileUri || '').substring(0, 120)}`;
|
|
501
|
+
if (p.text) return ` [${i}] text: ${p.text.length} chars → ${p.text.substring(0, 80).replace(/\n/g, ' ')}...`;
|
|
502
|
+
return ` [${i}] unknown part`;
|
|
503
|
+
});
|
|
504
|
+
console.error(` ${c.error('Request diagnostics:')}`);
|
|
505
|
+
console.error(` Model: ${config.GEMINI_MODEL} | Parts: ${contentParts.length} | maxOutput: 65536`);
|
|
506
|
+
partSummary.forEach(s => console.error(` ${s}`));
|
|
507
|
+
throw apiErr;
|
|
508
|
+
}
|
|
472
509
|
}
|
|
473
510
|
}
|
|
474
511
|
const durationMs = Date.now() - t0;
|
|
@@ -628,6 +665,60 @@ ${segmentDumps}`;
|
|
|
628
665
|
|
|
629
666
|
const contentParts = [{ text: compilationPrompt }];
|
|
630
667
|
|
|
668
|
+
// ------- Pre-flight context window check -------
|
|
669
|
+
const estimatedInputTokens = estimateTokens(compilationPrompt);
|
|
670
|
+
const safeLimit = Math.floor(config.GEMINI_CONTEXT_WINDOW * 0.80); // 80% of context window
|
|
671
|
+
if (estimatedInputTokens > safeLimit) {
|
|
672
|
+
console.warn(` ${c.warn(`Compilation input (~${(estimatedInputTokens / 1000).toFixed(0)}K tokens) exceeds 80% of context window (${(safeLimit / 1000).toFixed(0)}K). Trimming older segment detail...`)}`);
|
|
673
|
+
// Re-build segment dumps with aggressive compression: keep only first & last 2 segments
|
|
674
|
+
// at full detail, compress the middle ones to IDs + statuses only.
|
|
675
|
+
const trimmedDumps = allSegmentAnalyses.map((analysis, idx) => {
|
|
676
|
+
const clean = { ...analysis };
|
|
677
|
+
delete clean._geminiMeta;
|
|
678
|
+
delete clean.seg;
|
|
679
|
+
delete clean.conversation_transcript;
|
|
680
|
+
const isEdge = idx < 2 || idx >= allSegmentAnalyses.length - 2;
|
|
681
|
+
if (!isEdge) {
|
|
682
|
+
// Aggressive compression for middle segments
|
|
683
|
+
if (clean.tickets) {
|
|
684
|
+
clean.tickets = clean.tickets.map(t => ({
|
|
685
|
+
ticket_id: t.ticket_id, status: t.status, title: t.title,
|
|
686
|
+
assignee: t.assignee, source_segment: t.source_segment,
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
if (clean.change_requests) {
|
|
690
|
+
clean.change_requests = clean.change_requests.map(cr => ({
|
|
691
|
+
id: cr.id, status: cr.status, title: cr.title,
|
|
692
|
+
assigned_to: cr.assigned_to, source_segment: cr.source_segment,
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
if (clean.action_items) {
|
|
696
|
+
clean.action_items = clean.action_items.map(ai => ({
|
|
697
|
+
id: ai.id, description: ai.description, assigned_to: ai.assigned_to,
|
|
698
|
+
status: ai.status, source_segment: ai.source_segment,
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
delete clean.file_references;
|
|
702
|
+
clean.summary = (clean.summary || '').substring(0, 200);
|
|
703
|
+
} else {
|
|
704
|
+
if (clean.tickets) {
|
|
705
|
+
clean.tickets = clean.tickets.map(t => {
|
|
706
|
+
const tc = { ...t };
|
|
707
|
+
if (tc.comments && tc.comments.length > 5) {
|
|
708
|
+
tc.comments = tc.comments.slice(0, 5);
|
|
709
|
+
tc.comments.push({ note: `...${t.comments.length - 5} more comments omitted` });
|
|
710
|
+
}
|
|
711
|
+
return tc;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return `=== SEGMENT ${idx + 1} OF ${allSegmentAnalyses.length} ===\n${JSON.stringify(clean, null, 2)}`;
|
|
716
|
+
}).join('\n\n');
|
|
717
|
+
contentParts[0] = { text: compilationPrompt.replace(segmentDumps, trimmedDumps) };
|
|
718
|
+
const newEstimate = estimateTokens(contentParts[0].text);
|
|
719
|
+
console.log(` Trimmed compilation input to ~${(newEstimate / 1000).toFixed(0)}K tokens`);
|
|
720
|
+
}
|
|
721
|
+
|
|
631
722
|
const requestPayload = {
|
|
632
723
|
model: config.GEMINI_MODEL,
|
|
633
724
|
contents: [{ role: 'user', parts: contentParts }],
|
|
@@ -640,10 +731,44 @@ ${segmentDumps}`;
|
|
|
640
731
|
|
|
641
732
|
const t0 = Date.now();
|
|
642
733
|
console.log(` Compiling with ${config.GEMINI_MODEL}...`);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
734
|
+
let response;
|
|
735
|
+
try {
|
|
736
|
+
response = await withRetry(
|
|
737
|
+
() => ai.models.generateContent(requestPayload),
|
|
738
|
+
{ label: 'Gemini final compilation', maxRetries: 2, baseDelay: 5000 }
|
|
739
|
+
);
|
|
740
|
+
} catch (compileErr) {
|
|
741
|
+
const errMsg = compileErr.message || '';
|
|
742
|
+
if (errMsg.includes('RESOURCE_EXHAUSTED') || errMsg.includes('429') || errMsg.includes('quota')) {
|
|
743
|
+
console.warn(` ${c.warn('Context window or quota exceeded during compilation — waiting 30s and retrying with reduced input...')}`);
|
|
744
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
745
|
+
// Halve the compilation prompt by keeping only edge segments
|
|
746
|
+
const miniDumps = allSegmentAnalyses.map((analysis, idx) => {
|
|
747
|
+
const clean = { tickets: (analysis.tickets || []).map(t => ({ ticket_id: t.ticket_id, status: t.status, title: t.title, assignee: t.assignee })),
|
|
748
|
+
change_requests: (analysis.change_requests || []).map(cr => ({ id: cr.id, status: cr.status, title: cr.title })),
|
|
749
|
+
action_items: (analysis.action_items || []).map(ai => ({ id: ai.id, description: ai.description, assigned_to: ai.assigned_to, status: ai.status })),
|
|
750
|
+
blockers: (analysis.blockers || []).map(b => ({ id: b.id, description: b.description, status: b.status })),
|
|
751
|
+
scope_changes: analysis.scope_changes || [],
|
|
752
|
+
your_tasks: analysis.your_tasks || {},
|
|
753
|
+
summary: (analysis.summary || '').substring(0, 300),
|
|
754
|
+
};
|
|
755
|
+
return `=== SEGMENT ${idx + 1} OF ${allSegmentAnalyses.length} ===\n${JSON.stringify(clean, null, 2)}`;
|
|
756
|
+
}).join('\n\n');
|
|
757
|
+
requestPayload.contents[0].parts = [{ text: compilationPrompt.replace(/SEGMENT ANALYSES:\n[\s\S]*$/, `SEGMENT ANALYSES:\n${miniDumps}`) }];
|
|
758
|
+
try {
|
|
759
|
+
response = await withRetry(
|
|
760
|
+
() => ai.models.generateContent(requestPayload),
|
|
761
|
+
{ label: 'Gemini compilation (reduced)', maxRetries: 1, baseDelay: 5000 }
|
|
762
|
+
);
|
|
763
|
+
console.log(` ${c.success('Reduced compilation succeeded')}`);
|
|
764
|
+
} catch (reduceErr) {
|
|
765
|
+
console.error(` ${c.error(`Reduced compilation also failed: ${reduceErr.message}`)}`);
|
|
766
|
+
throw reduceErr;
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
throw compileErr;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
647
772
|
const durationMs = Date.now() - t0;
|
|
648
773
|
const rawText = response.text;
|
|
649
774
|
|
package/src/utils/cli.js
CHANGED
|
@@ -35,7 +35,7 @@ function parseArgs(argv) {
|
|
|
35
35
|
'skip-upload', 'force-upload', 'no-storage-url',
|
|
36
36
|
'skip-compression', 'skip-gemini',
|
|
37
37
|
'resume', 'reanalyze', 'dry-run',
|
|
38
|
-
'dynamic', 'deep-dive', 'update-progress',
|
|
38
|
+
'dynamic', 'deep-dive', 'deep-summary', 'update-progress',
|
|
39
39
|
'no-focused-pass', 'no-learning', 'no-diff',
|
|
40
40
|
'no-html',
|
|
41
41
|
]);
|
|
@@ -371,6 +371,8 @@ ${f('(default)', 'Video/audio analysis — compress, analyze, compile')}
|
|
|
371
371
|
${f('--dynamic', 'Document generation — no media required')}
|
|
372
372
|
${f('--update-progress', 'Track item completion via git changes')}
|
|
373
373
|
${f('--deep-dive', 'Generate explanatory docs per topic')}
|
|
374
|
+
${f('--deep-summary', 'Pre-summarize context docs (saves ~60-80% input tokens)')}
|
|
375
|
+
${f('--exclude-docs <list>', 'Comma-separated doc names to keep full (use with --deep-summary)')}
|
|
374
376
|
|
|
375
377
|
${h('CORE OPTIONS')}
|
|
376
378
|
${f('--name <name>', 'Your name (skip interactive prompt)')}
|
|
@@ -435,6 +437,8 @@ ${f('--version, -v', 'Show version')}
|
|
|
435
437
|
${c.dim('$')} taskex --format pdf "call 1" ${c.dim('# PDF report')}
|
|
436
438
|
${c.dim('$')} taskex --format docx "call 1" ${c.dim('# Word document')}
|
|
437
439
|
${c.dim('$')} taskex --resume "call 1" ${c.dim('# Resume interrupted run')}
|
|
440
|
+
${c.dim('$')} taskex --deep-summary "call 1" ${c.dim('# Pre-summarize docs, save tokens')}
|
|
441
|
+
${c.dim('$')} taskex --deep-summary --exclude-docs "board.md,spec.md" "call 1" ${c.dim('# Keep specific docs full')}
|
|
438
442
|
${c.dim('$')} taskex --update-progress --repo ./my-project "call 1"
|
|
439
443
|
`);
|
|
440
444
|
// Signal early exit — pipeline checks for help flag before calling this
|
|
@@ -444,6 +448,7 @@ ${f('--version, -v', 'Show version')}
|
|
|
444
448
|
module.exports = {
|
|
445
449
|
parseArgs, showHelp, discoverFolders, selectFolder, selectModel,
|
|
446
450
|
promptUser, promptUserText, selectRunMode, selectFormats, selectConfidence,
|
|
451
|
+
selectDocsToExclude,
|
|
447
452
|
};
|
|
448
453
|
|
|
449
454
|
// ======================== INTERACTIVE PROMPTS ========================
|
|
@@ -527,6 +532,9 @@ const RUN_PRESETS = {
|
|
|
527
532
|
},
|
|
528
533
|
};
|
|
529
534
|
|
|
535
|
+
// Attach RUN_PRESETS to exports (defined after module.exports due to const ordering)
|
|
536
|
+
module.exports.RUN_PRESETS = RUN_PRESETS;
|
|
537
|
+
|
|
530
538
|
/**
|
|
531
539
|
* Interactive run-mode selector. Shows preset options and returns the chosen
|
|
532
540
|
* preset key. The caller applies overrides to opts.
|
|
@@ -721,3 +729,83 @@ async function selectConfidence() {
|
|
|
721
729
|
});
|
|
722
730
|
});
|
|
723
731
|
}
|
|
732
|
+
|
|
733
|
+
// ======================== DEEP SUMMARY DOC EXCLUSION PICKER ========================
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Interactive picker: let user select documents to EXCLUDE from deep summary.
|
|
737
|
+
* Excluded docs stay at full fidelity; the summary pass focuses on their topics.
|
|
738
|
+
*
|
|
739
|
+
* @param {Array<{fileName: string, type: string, content?: string}>} contextDocs - Prepared docs
|
|
740
|
+
* @returns {Promise<string[]>} Array of excluded fileName strings
|
|
741
|
+
*/
|
|
742
|
+
async function selectDocsToExclude(contextDocs) {
|
|
743
|
+
const readline = require('readline');
|
|
744
|
+
|
|
745
|
+
// Only show inlineText docs with actual content
|
|
746
|
+
const eligible = contextDocs
|
|
747
|
+
.filter(d => d.type === 'inlineText' && d.content && d.content.length > 0)
|
|
748
|
+
.map(d => ({
|
|
749
|
+
fileName: d.fileName,
|
|
750
|
+
chars: d.content.length,
|
|
751
|
+
tokensEst: Math.ceil(d.content.length * 0.3),
|
|
752
|
+
}));
|
|
753
|
+
|
|
754
|
+
if (eligible.length === 0) return [];
|
|
755
|
+
|
|
756
|
+
console.log('');
|
|
757
|
+
console.log(` ${c.bold('📋 Deep Summary — Choose What to Keep in Full')}`);
|
|
758
|
+
console.log(c.dim(' ' + '─'.repeat(60)));
|
|
759
|
+
console.log('');
|
|
760
|
+
console.log(` ${c.dim('To save processing time, we can create short summaries of your')}`);
|
|
761
|
+
console.log(` ${c.dim('reference documents. The AI will still read them — just faster.')}`);
|
|
762
|
+
console.log('');
|
|
763
|
+
console.log(` ${c.bold('If a document is especially important to you, select it below')}`);
|
|
764
|
+
console.log(` ${c.bold('to keep it in full.')} The rest will be smartly condensed.`);
|
|
765
|
+
console.log('');
|
|
766
|
+
|
|
767
|
+
eligible.forEach((d, i) => {
|
|
768
|
+
const num = c.cyan(`[${i + 1}]`);
|
|
769
|
+
const size = d.tokensEst >= 1000
|
|
770
|
+
? c.dim(`~${(d.tokensEst / 1000).toFixed(0)}K words`)
|
|
771
|
+
: c.dim(`~${d.tokensEst} words`);
|
|
772
|
+
console.log(` ${num} ${c.bold(d.fileName)} ${size}`);
|
|
773
|
+
});
|
|
774
|
+
console.log('');
|
|
775
|
+
console.log(c.dim(' Tip: Enter = condense all · Type numbers to keep full (e.g. 1,3)'));
|
|
776
|
+
console.log('');
|
|
777
|
+
|
|
778
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
779
|
+
return new Promise(resolve => {
|
|
780
|
+
rl.question(' Keep full (e.g. 1,3 or Enter = condense all): ', answer => {
|
|
781
|
+
rl.close();
|
|
782
|
+
const trimmed = (answer || '').trim();
|
|
783
|
+
|
|
784
|
+
if (!trimmed) {
|
|
785
|
+
console.log(c.success('Got it — all documents will be condensed for faster processing'));
|
|
786
|
+
resolve([]);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const parts = trimmed.split(/[\s,]+/).filter(Boolean);
|
|
791
|
+
const excluded = [];
|
|
792
|
+
|
|
793
|
+
for (const p of parts) {
|
|
794
|
+
const num = parseInt(p, 10);
|
|
795
|
+
if (num >= 1 && num <= eligible.length) {
|
|
796
|
+
excluded.push(eligible[num - 1].fileName);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (excluded.length === 0) {
|
|
801
|
+
console.log(c.warn('No valid selections — condensing all documents'));
|
|
802
|
+
resolve([]);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
console.log(c.success(`Keeping ${excluded.length} doc(s) in full — the rest will be condensed:`));
|
|
807
|
+
excluded.forEach(f => console.log(` ${c.dim('•')} ${c.cyan(f)}`));
|
|
808
|
+
resolve(excluded);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
}
|
|
@@ -29,6 +29,14 @@ function estimateDocTokens(doc) {
|
|
|
29
29
|
return 500;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Hard character limit for VTT fallback.
|
|
34
|
+
* When VTT parsing fails (0 cues), the full VTT is returned.
|
|
35
|
+
* Cap it so a huge transcript can't blow the context window.
|
|
36
|
+
* 500K chars ≈ 150K tokens — leaves plenty of room for docs + prompt.
|
|
37
|
+
*/
|
|
38
|
+
const VTT_FALLBACK_MAX_CHARS = 500000;
|
|
39
|
+
|
|
32
40
|
// ════════════════════════════════════════════════════════════
|
|
33
41
|
// Priority Classification
|
|
34
42
|
// ════════════════════════════════════════════════════════════
|
|
@@ -100,12 +108,16 @@ function selectDocsByBudget(allDocs, tokenBudget, opts = {}) {
|
|
|
100
108
|
const excluded = [];
|
|
101
109
|
let usedTokens = 0;
|
|
102
110
|
|
|
111
|
+
// Hard cap: even P0/P1 docs may not exceed 2× the budget.
|
|
112
|
+
// This prevents a handful of huge critical docs from blowing the context window.
|
|
113
|
+
const hardCap = tokenBudget * 2;
|
|
114
|
+
|
|
103
115
|
for (const item of classified) {
|
|
104
116
|
if (usedTokens + item.tokens <= tokenBudget) {
|
|
105
117
|
selected.push(item.doc);
|
|
106
118
|
usedTokens += item.tokens;
|
|
107
|
-
} else if (item.priority <= PRIORITY.HIGH) {
|
|
108
|
-
// P0 and P1 are always included even if over budget
|
|
119
|
+
} else if (item.priority <= PRIORITY.HIGH && usedTokens + item.tokens <= hardCap) {
|
|
120
|
+
// P0 and P1 are always included even if over budget, up to the hard cap
|
|
109
121
|
selected.push(item.doc);
|
|
110
122
|
usedTokens += item.tokens;
|
|
111
123
|
} else {
|
|
@@ -171,14 +183,28 @@ function parseVttCues(vttContent) {
|
|
|
171
183
|
*/
|
|
172
184
|
function sliceVttForSegment(vttContent, segStartSec, segEndSec, overlapSec = 30) {
|
|
173
185
|
const cues = parseVttCues(vttContent);
|
|
174
|
-
if (cues.length === 0)
|
|
186
|
+
if (cues.length === 0) {
|
|
187
|
+
// Fallback: return full VTT but cap size to avoid context window overflow
|
|
188
|
+
if (vttContent.length > VTT_FALLBACK_MAX_CHARS) {
|
|
189
|
+
return vttContent.substring(0, VTT_FALLBACK_MAX_CHARS) +
|
|
190
|
+
`\n\n[TRUNCATED — original VTT was ${(vttContent.length / 1024).toFixed(0)} KB; capped at ${(VTT_FALLBACK_MAX_CHARS / 1024).toFixed(0)} KB]`;
|
|
191
|
+
}
|
|
192
|
+
return vttContent;
|
|
193
|
+
}
|
|
175
194
|
|
|
176
195
|
const rangeStart = Math.max(0, segStartSec - overlapSec);
|
|
177
196
|
const rangeEnd = segEndSec + overlapSec;
|
|
178
197
|
|
|
179
198
|
const filtered = cues.filter(c => c.endSec >= rangeStart && c.startSec <= rangeEnd);
|
|
180
199
|
|
|
181
|
-
if (filtered.length === 0)
|
|
200
|
+
if (filtered.length === 0) {
|
|
201
|
+
// Fallback with cap
|
|
202
|
+
if (vttContent.length > VTT_FALLBACK_MAX_CHARS) {
|
|
203
|
+
return vttContent.substring(0, VTT_FALLBACK_MAX_CHARS) +
|
|
204
|
+
`\n\n[TRUNCATED — original VTT was ${(vttContent.length / 1024).toFixed(0)} KB; capped at ${(VTT_FALLBACK_MAX_CHARS / 1024).toFixed(0)} KB]`;
|
|
205
|
+
}
|
|
206
|
+
return vttContent;
|
|
207
|
+
}
|
|
182
208
|
|
|
183
209
|
const header = `WEBVTT\n\n[Segment transcript: ${formatHMS(segStartSec)} — ${formatHMS(segEndSec)}]\n[Showing cues from ${formatHMS(rangeStart)} to ${formatHMS(rangeEnd)} with ${overlapSec}s overlap]\n`;
|
|
184
210
|
|
|
@@ -492,4 +518,5 @@ module.exports = {
|
|
|
492
518
|
buildProgressiveContext,
|
|
493
519
|
buildSegmentFocus,
|
|
494
520
|
detectBoundaryContext,
|
|
521
|
+
VTT_FALLBACK_MAX_CHARS,
|
|
495
522
|
};
|