@juspay/neurolink 10.10.7 → 10.10.9
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/CHANGELOG.md +14 -0
- package/README.md +37 -8
- package/dist/browser/neurolink.min.js +343 -343
- package/dist/cli/factories/commandFactory.js +8 -4
- package/dist/lib/processors/media/VideoProcessor.d.ts +13 -3
- package/dist/lib/processors/media/VideoProcessor.js +53 -12
- package/dist/lib/skills/skillsManager.js +24 -5
- package/dist/lib/types/file.d.ts +41 -0
- package/dist/lib/types/generate.d.ts +19 -0
- package/dist/lib/types/stream.d.ts +15 -0
- package/dist/lib/utils/errorHandling.d.ts +21 -0
- package/dist/lib/utils/errorHandling.js +53 -0
- package/dist/lib/utils/fileDetector.js +9 -6
- package/dist/lib/utils/messageBuilder.js +151 -39
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/lib/utils/pdfProcessor.d.ts +11 -0
- package/dist/lib/utils/pdfProcessor.js +27 -0
- package/dist/processors/media/VideoProcessor.d.ts +13 -3
- package/dist/processors/media/VideoProcessor.js +53 -12
- package/dist/skills/skillsManager.js +24 -5
- package/dist/types/file.d.ts +41 -0
- package/dist/types/generate.d.ts +19 -0
- package/dist/types/stream.d.ts +15 -0
- package/dist/utils/errorHandling.d.ts +21 -0
- package/dist/utils/errorHandling.js +53 -0
- package/dist/utils/fileDetector.js +9 -6
- package/dist/utils/messageBuilder.js +151 -39
- package/dist/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/utils/pdfProcessor.d.ts +11 -0
- package/dist/utils/pdfProcessor.js +27 -0
- package/package.json +1 -1
|
@@ -166,15 +166,19 @@ export class CLICommandFactory {
|
|
|
166
166
|
type: "string",
|
|
167
167
|
description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)",
|
|
168
168
|
},
|
|
169
|
+
// No yargs `default:` on these two. They used to declare 8 / 85 while the
|
|
170
|
+
// processor actually picks a duration-based frame count (up to 100) and
|
|
171
|
+
// encodes at quality 80 — harmless only because the values were never read
|
|
172
|
+
// (#478). Now that they reach the encoder, a default here would silently
|
|
173
|
+
// re-cap every existing CLI video at 8 frames. Unset means "let the
|
|
174
|
+
// processor choose", which is what callers have always effectively had.
|
|
169
175
|
"video-frames": {
|
|
170
176
|
type: "number",
|
|
171
|
-
default:
|
|
172
|
-
description: "Number of frames to extract (default: 8)",
|
|
177
|
+
description: "Number of frames to extract (default: chosen from video duration, max 100)",
|
|
173
178
|
},
|
|
174
179
|
"video-quality": {
|
|
175
180
|
type: "number",
|
|
176
|
-
default:
|
|
177
|
-
description: "Frame quality 0-100 (default: 85)",
|
|
181
|
+
description: "Frame quality 1-100 (default: 80)",
|
|
178
182
|
},
|
|
179
183
|
"video-format": {
|
|
180
184
|
type: "string",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
47
|
-
import type { FileInfo, ProcessedVideo, ProcessorFileProcessingResult, ProcessOptions } from "../../types/index.js";
|
|
47
|
+
import type { FileInfo, ProcessedVideo, ProcessorFileProcessingResult, ProcessOptions, VideoProcessorOptions } from "../../types/index.js";
|
|
48
48
|
/**
|
|
49
49
|
* Narrow a loaded `fluent-ffmpeg` export to the shape this file actually uses:
|
|
50
50
|
* a callable carrying the `ffprobe` and `setFfmpegPath` statics.
|
|
@@ -116,7 +116,7 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
|
|
|
116
116
|
* @param options - Optional processing options
|
|
117
117
|
* @returns Processing result with extracted video data or error
|
|
118
118
|
*/
|
|
119
|
-
processFile(fileInfo: FileInfo, options?: ProcessOptions): Promise<ProcessorFileProcessingResult<ProcessedVideo>>;
|
|
119
|
+
processFile(fileInfo: FileInfo, options?: ProcessOptions & VideoProcessorOptions): Promise<ProcessorFileProcessingResult<ProcessedVideo>>;
|
|
120
120
|
/**
|
|
121
121
|
* Probe a video file to extract metadata using ffprobe.
|
|
122
122
|
*
|
|
@@ -137,6 +137,11 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
|
|
|
137
137
|
* @returns Structured video metadata
|
|
138
138
|
*/
|
|
139
139
|
private buildMetadata;
|
|
140
|
+
/**
|
|
141
|
+
* Clamp a caller-supplied frame quality into sharp's valid 1-100 range,
|
|
142
|
+
* falling back to the default when absent or non-numeric (#478).
|
|
143
|
+
*/
|
|
144
|
+
private static resolveFrameQuality;
|
|
140
145
|
/**
|
|
141
146
|
* Extract keyframes from a video at calculated intervals.
|
|
142
147
|
*
|
|
@@ -153,10 +158,15 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
|
|
|
153
158
|
* The interval is adaptive: if the tier interval would exceed MAX_FRAMES,
|
|
154
159
|
* the interval widens to duration/MAX_FRAMES for full-video coverage.
|
|
155
160
|
*
|
|
161
|
+
* A caller-supplied `options.frames` overrides the tier schedule entirely:
|
|
162
|
+
* that many frames are spread evenly across the clip, still capped at
|
|
163
|
+
* MAX_FRAMES. `options.quality` and `options.format` reach the encoder (#478).
|
|
164
|
+
*
|
|
156
165
|
* @param videoPath - Path to the video file
|
|
157
166
|
* @param tempDir - Temp directory for frame output
|
|
158
167
|
* @param durationSec - Video duration in seconds
|
|
159
|
-
* @
|
|
168
|
+
* @param options - Caller frame budget / encoder settings
|
|
169
|
+
* @returns Array of encoded frame buffers (JPEG unless png was requested)
|
|
160
170
|
*/
|
|
161
171
|
private extractKeyframes;
|
|
162
172
|
/**
|
|
@@ -313,7 +313,10 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
313
313
|
* @param options - Optional processing options
|
|
314
314
|
* @returns Processing result with extracted video data or error
|
|
315
315
|
*/
|
|
316
|
-
async processFile(fileInfo,
|
|
316
|
+
async processFile(fileInfo,
|
|
317
|
+
// #478: widened with the keyframe knobs so `--video-frames`/`-quality`/
|
|
318
|
+
// `-format` can reach the encoder instead of being silently discarded.
|
|
319
|
+
options) {
|
|
317
320
|
const filename = this.getFilename(fileInfo);
|
|
318
321
|
const sizeBytes = fileInfo.size || fileInfo.buffer?.length || 0;
|
|
319
322
|
return withSpan({
|
|
@@ -438,7 +441,7 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
438
441
|
// Step 5: Extract keyframes
|
|
439
442
|
let keyframes = [];
|
|
440
443
|
try {
|
|
441
|
-
keyframes = await this.extractKeyframes(tempVideoPath, tempDir, metadata.duration);
|
|
444
|
+
keyframes = await this.extractKeyframes(tempVideoPath, tempDir, metadata.duration, options);
|
|
442
445
|
}
|
|
443
446
|
catch {
|
|
444
447
|
// Non-fatal: continue without keyframes if extraction fails
|
|
@@ -642,6 +645,16 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
642
645
|
// ===========================================================================
|
|
643
646
|
// KEYFRAME EXTRACTION
|
|
644
647
|
// ===========================================================================
|
|
648
|
+
/**
|
|
649
|
+
* Clamp a caller-supplied frame quality into sharp's valid 1-100 range,
|
|
650
|
+
* falling back to the default when absent or non-numeric (#478).
|
|
651
|
+
*/
|
|
652
|
+
static resolveFrameQuality(quality) {
|
|
653
|
+
if (typeof quality !== "number" || !Number.isFinite(quality)) {
|
|
654
|
+
return VIDEO_CONFIG.FRAME_JPEG_QUALITY;
|
|
655
|
+
}
|
|
656
|
+
return Math.min(100, Math.max(1, Math.round(quality)));
|
|
657
|
+
}
|
|
645
658
|
/**
|
|
646
659
|
* Extract keyframes from a video at calculated intervals.
|
|
647
660
|
*
|
|
@@ -658,20 +671,45 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
658
671
|
* The interval is adaptive: if the tier interval would exceed MAX_FRAMES,
|
|
659
672
|
* the interval widens to duration/MAX_FRAMES for full-video coverage.
|
|
660
673
|
*
|
|
674
|
+
* A caller-supplied `options.frames` overrides the tier schedule entirely:
|
|
675
|
+
* that many frames are spread evenly across the clip, still capped at
|
|
676
|
+
* MAX_FRAMES. `options.quality` and `options.format` reach the encoder (#478).
|
|
677
|
+
*
|
|
661
678
|
* @param videoPath - Path to the video file
|
|
662
679
|
* @param tempDir - Temp directory for frame output
|
|
663
680
|
* @param durationSec - Video duration in seconds
|
|
664
|
-
* @
|
|
681
|
+
* @param options - Caller frame budget / encoder settings
|
|
682
|
+
* @returns Array of encoded frame buffers (JPEG unless png was requested)
|
|
665
683
|
*/
|
|
666
|
-
async extractKeyframes(videoPath, tempDir, durationSec) {
|
|
684
|
+
async extractKeyframes(videoPath, tempDir, durationSec, options) {
|
|
667
685
|
if (durationSec <= 0) {
|
|
668
686
|
return [];
|
|
669
687
|
}
|
|
670
|
-
//
|
|
671
|
-
|
|
688
|
+
// #478: honor the caller's frame budget, still bounded by MAX_FRAMES so a
|
|
689
|
+
// CLI flag can lower the cost but never raise it past the processor's own
|
|
690
|
+
// ceiling. A non-positive/non-finite request falls back to the default.
|
|
691
|
+
const requestedFrames = options?.frames;
|
|
692
|
+
const hasExplicitBudget = typeof requestedFrames === "number" &&
|
|
693
|
+
Number.isFinite(requestedFrames) &&
|
|
694
|
+
requestedFrames > 0;
|
|
695
|
+
const frameBudget = hasExplicitBudget
|
|
696
|
+
? Math.min(Math.floor(requestedFrames), VIDEO_CONFIG.MAX_FRAMES)
|
|
697
|
+
: VIDEO_CONFIG.MAX_FRAMES;
|
|
698
|
+
// Determine extraction interval based on duration. When the caller asked
|
|
699
|
+
// for a specific frame count, spread that many evenly across the whole
|
|
700
|
+
// video instead of using the duration tier — otherwise a short interval
|
|
701
|
+
// would hit the budget early and only cover the opening seconds.
|
|
702
|
+
//
|
|
703
|
+
// Keyed on whether a budget was REQUESTED, not on whether it happens to be
|
|
704
|
+
// below MAX_FRAMES: asking for exactly MAX_FRAMES is still an explicit
|
|
705
|
+
// request and must produce that many frames, not silently fall back to the
|
|
706
|
+
// tier schedule (which yields far fewer on a short clip).
|
|
707
|
+
const intervalSec = hasExplicitBudget
|
|
708
|
+
? Math.max(durationSec / frameBudget, Number.EPSILON)
|
|
709
|
+
: this.getFrameInterval(durationSec);
|
|
672
710
|
// Calculate timestamps to extract
|
|
673
711
|
const timestamps = [];
|
|
674
|
-
for (let t = 0; t < durationSec && timestamps.length <
|
|
712
|
+
for (let t = 0; t < durationSec && timestamps.length < frameBudget; t += intervalSec) {
|
|
675
713
|
timestamps.push(t);
|
|
676
714
|
}
|
|
677
715
|
if (timestamps.length === 0) {
|
|
@@ -691,13 +729,16 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
691
729
|
const rawFrame = await fs.readFile(framePath);
|
|
692
730
|
// Resize to fit within max dimension while preserving aspect ratio
|
|
693
731
|
const sharp = (await import("sharp")).default;
|
|
694
|
-
const
|
|
695
|
-
.resize(VIDEO_CONFIG.FRAME_MAX_DIMENSION, VIDEO_CONFIG.FRAME_MAX_DIMENSION, {
|
|
732
|
+
const pipeline = sharp(rawFrame).resize(VIDEO_CONFIG.FRAME_MAX_DIMENSION, VIDEO_CONFIG.FRAME_MAX_DIMENSION, {
|
|
696
733
|
fit: "inside",
|
|
697
734
|
withoutEnlargement: true,
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
|
|
735
|
+
});
|
|
736
|
+
// #478: `--video-quality` / `--video-format` were accepted by the CLI
|
|
737
|
+
// and then dropped on the floor; both now reach the encoder.
|
|
738
|
+
const quality = VideoProcessor.resolveFrameQuality(options?.quality);
|
|
739
|
+
const resized = await (options?.format === "png"
|
|
740
|
+
? pipeline.png({ quality })
|
|
741
|
+
: pipeline.jpeg({ quality })).toBuffer();
|
|
701
742
|
keyframes.push(resized);
|
|
702
743
|
}
|
|
703
744
|
catch {
|
|
@@ -87,7 +87,14 @@ export class SkillsManager {
|
|
|
87
87
|
// name-find would resolve non-deterministically to the stale deprecated one
|
|
88
88
|
// across store backends. Fall back to any match only when no active skill
|
|
89
89
|
// carries the name.
|
|
90
|
-
|
|
90
|
+
//
|
|
91
|
+
// Matched case-insensitively to agree with assertNameAvailable, which
|
|
92
|
+
// enforces uniqueness that way: names differing only in case cannot
|
|
93
|
+
// coexist, so a case-sensitive lookup could only fail to find a skill that
|
|
94
|
+
// is definitively there (`get("DEPLOY")` returned null for "deploy").
|
|
95
|
+
const wanted = idOrName.toLowerCase();
|
|
96
|
+
const entry = index.find((item) => item.name.toLowerCase() === wanted &&
|
|
97
|
+
(item.status ?? "active") === "active") ?? index.find((item) => item.name.toLowerCase() === wanted);
|
|
91
98
|
return entry ? this.store.get(entry.id) : null;
|
|
92
99
|
}
|
|
93
100
|
/**
|
|
@@ -237,11 +244,23 @@ export class SkillsManager {
|
|
|
237
244
|
}
|
|
238
245
|
async assertNameAvailable(name, excludeId) {
|
|
239
246
|
const index = await this.getIndex(true);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
// Deliberately NOT filtered to active (#1139). Soft-delete only flips
|
|
248
|
+
// status to "deprecated" — the entry stays in the index, so allowing a new
|
|
249
|
+
// skill to take the name left two entries sharing it. Every name-based
|
|
250
|
+
// lookup (CLI `skills show/delete <name>`, the skill_update/skill_delete
|
|
251
|
+
// tools, the `:id`-or-name REST routes) then had to guess which one the
|
|
252
|
+
// caller meant, and iteration order differs across store backends.
|
|
253
|
+
//
|
|
254
|
+
// A deprecated skill's name therefore stays reserved. Reusing it requires
|
|
255
|
+
// hard-deleting the old skill first, which is the explicit choice the
|
|
256
|
+
// ambiguity demands.
|
|
257
|
+
const clash = index.find((item) => item.id !== excludeId && item.name.toLowerCase() === name.toLowerCase());
|
|
243
258
|
if (clash) {
|
|
244
|
-
|
|
259
|
+
const suffix = (clash.status ?? "active") === "active"
|
|
260
|
+
? ""
|
|
261
|
+
: ` (that name belongs to a deprecated skill, id ${clash.id}; ` +
|
|
262
|
+
`remove it before reusing the name)`;
|
|
263
|
+
throw new Error(`A skill named "${name}" already exists${suffix}`);
|
|
245
264
|
}
|
|
246
265
|
}
|
|
247
266
|
}
|
package/dist/lib/types/file.d.ts
CHANGED
|
@@ -302,6 +302,21 @@ export type AudioProcessorOptions = {
|
|
|
302
302
|
/** Maximum file size in megabytes */
|
|
303
303
|
maxSizeMB?: number;
|
|
304
304
|
};
|
|
305
|
+
/**
|
|
306
|
+
* Keyframe-extraction knobs for an attached video (#478).
|
|
307
|
+
*
|
|
308
|
+
* These back the `--video-frames` / `--video-quality` / `--video-format` CLI
|
|
309
|
+
* flags and `GenerateOptions.videoOptions`. Each is clamped to the processor's
|
|
310
|
+
* own ceiling — a caller cannot raise `frames` above VIDEO_CONFIG.MAX_FRAMES.
|
|
311
|
+
*/
|
|
312
|
+
export type VideoProcessorOptions = {
|
|
313
|
+
/** Max keyframes to extract. Clamped to the processor's MAX_FRAMES ceiling. */
|
|
314
|
+
frames?: number;
|
|
315
|
+
/** Encoder quality 1-100 for the extracted frames. */
|
|
316
|
+
quality?: number;
|
|
317
|
+
/** Frame encoding. Defaults to jpeg. */
|
|
318
|
+
format?: "jpeg" | "png";
|
|
319
|
+
};
|
|
305
320
|
/**
|
|
306
321
|
* Office processor options for Word, PowerPoint, and Excel documents
|
|
307
322
|
*
|
|
@@ -366,6 +381,7 @@ export type FileDetectorOptions = {
|
|
|
366
381
|
audioOptions?: AudioProcessorOptions;
|
|
367
382
|
csvOptions?: CSVProcessorOptions;
|
|
368
383
|
officeOptions?: OfficeProcessorOptions;
|
|
384
|
+
videoOptions?: VideoProcessorOptions;
|
|
369
385
|
confidenceThreshold?: number;
|
|
370
386
|
provider?: string;
|
|
371
387
|
/** Maximum number of retry attempts for network requests (default: 3) */
|
|
@@ -438,6 +454,31 @@ export type PDFImagePage = {
|
|
|
438
454
|
/** Populated when this page failed to render (#294). */
|
|
439
455
|
error?: string;
|
|
440
456
|
};
|
|
457
|
+
/**
|
|
458
|
+
* A single PDF queued for multimodal message building, normalised from either
|
|
459
|
+
* submission surface — `input.pdfFiles` or `input.content` with `type: "pdf"`
|
|
460
|
+
* — so both can share the aggregate page/size guard (#309).
|
|
461
|
+
*/
|
|
462
|
+
export type MultimodalPdfEntry = {
|
|
463
|
+
/** Raw PDF bytes. */
|
|
464
|
+
buffer: Buffer;
|
|
465
|
+
/** Display name; may be a full path, so log only its basename. */
|
|
466
|
+
filename: string;
|
|
467
|
+
/**
|
|
468
|
+
* Page count when known. Null/undefined on the `input.content` path whenever
|
|
469
|
+
* the caller omitted `metadata.pages`; the aggregate guard resolves those
|
|
470
|
+
* from `buffer` rather than treating them as zero.
|
|
471
|
+
*/
|
|
472
|
+
pageCount?: number | null;
|
|
473
|
+
/** Password for an encrypted PDF (#258). */
|
|
474
|
+
password?: string;
|
|
475
|
+
/** Per-page pixel ceiling for the image fallback (#260). */
|
|
476
|
+
maxCanvasPixels?: number;
|
|
477
|
+
/** Render scale for the image fallback (#297). */
|
|
478
|
+
scale?: number;
|
|
479
|
+
/** Max pages converted by the image fallback (#297). */
|
|
480
|
+
maxPages?: number;
|
|
481
|
+
};
|
|
441
482
|
/** Result of PDF to image conversion. */
|
|
442
483
|
export type PDFImageConversionResult = {
|
|
443
484
|
/** Array of base64-encoded PNG images (one per successfully converted page) */
|
|
@@ -133,11 +133,26 @@ export type GenerateOptions = {
|
|
|
133
133
|
password?: string;
|
|
134
134
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
135
135
|
maxCanvasPixels?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Render scale for the image fallback used by providers without native PDF
|
|
138
|
+
* support (#297). Higher is sharper but costs roughly the square in memory
|
|
139
|
+
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
|
|
140
|
+
*/
|
|
141
|
+
scale?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Max pages converted by the image fallback (#297). Pages beyond this are
|
|
144
|
+
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
|
|
145
|
+
*/
|
|
146
|
+
maxPages?: number;
|
|
136
147
|
};
|
|
137
148
|
videoOptions?: {
|
|
149
|
+
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
138
150
|
frames?: number;
|
|
151
|
+
/** Frame encoder quality, clamped to 1-100. Default 80. */
|
|
139
152
|
quality?: number;
|
|
153
|
+
/** Frame encoding. Default jpeg. */
|
|
140
154
|
format?: "jpeg" | "png";
|
|
155
|
+
/** Not implemented yet (#433) — warns rather than silently doing nothing. */
|
|
141
156
|
transcribeAudio?: boolean;
|
|
142
157
|
};
|
|
143
158
|
/**
|
|
@@ -1261,6 +1276,10 @@ export type TextGenerationOptions = {
|
|
|
1261
1276
|
password?: string;
|
|
1262
1277
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
1263
1278
|
maxCanvasPixels?: number;
|
|
1279
|
+
/** Render scale for the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_SCALE. */
|
|
1280
|
+
scale?: number;
|
|
1281
|
+
/** Max pages converted by the image fallback (#297); defaults to PDF_LIMITS.DEFAULT_MAX_PAGES. */
|
|
1282
|
+
maxPages?: number;
|
|
1264
1283
|
};
|
|
1265
1284
|
enableSummarization?: boolean;
|
|
1266
1285
|
/**
|
|
@@ -225,11 +225,26 @@ export type StreamOptions = {
|
|
|
225
225
|
password?: string;
|
|
226
226
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
227
227
|
maxCanvasPixels?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Render scale for the image fallback used by providers without native PDF
|
|
230
|
+
* support (#297). Higher is sharper but costs roughly the square in memory
|
|
231
|
+
* and tokens. Range 0.1-10; defaults to PDF_LIMITS.DEFAULT_SCALE (1.5).
|
|
232
|
+
*/
|
|
233
|
+
scale?: number;
|
|
234
|
+
/**
|
|
235
|
+
* Max pages converted by the image fallback (#297). Pages beyond this are
|
|
236
|
+
* not sent to the model at all. Defaults to PDF_LIMITS.DEFAULT_MAX_PAGES (20).
|
|
237
|
+
*/
|
|
238
|
+
maxPages?: number;
|
|
228
239
|
};
|
|
229
240
|
videoOptions?: {
|
|
241
|
+
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
230
242
|
frames?: number;
|
|
243
|
+
/** Frame encoder quality, clamped to 1-100. Default 80. */
|
|
231
244
|
quality?: number;
|
|
245
|
+
/** Frame encoding. Default jpeg. */
|
|
232
246
|
format?: "jpeg" | "png";
|
|
247
|
+
/** Not implemented yet (#433) — warns rather than silently doing nothing. */
|
|
233
248
|
transcribeAudio?: boolean;
|
|
234
249
|
};
|
|
235
250
|
/**
|
|
@@ -39,6 +39,9 @@ export declare const ERROR_CODES: {
|
|
|
39
39
|
readonly FILE_PROCESSING_FAILED: "FILE_PROCESSING_FAILED";
|
|
40
40
|
readonly CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED";
|
|
41
41
|
readonly PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED";
|
|
42
|
+
readonly PDF_AGGREGATE_PAGE_LIMIT_EXCEEDED: "PDF_AGGREGATE_PAGE_LIMIT_EXCEEDED";
|
|
43
|
+
readonly PDF_AGGREGATE_SIZE_LIMIT_EXCEEDED: "PDF_AGGREGATE_SIZE_LIMIT_EXCEEDED";
|
|
44
|
+
readonly PDF_PAGE_COUNT_UNVERIFIABLE: "PDF_PAGE_COUNT_UNVERIFIABLE";
|
|
42
45
|
readonly PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED";
|
|
43
46
|
readonly PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD";
|
|
44
47
|
readonly RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL";
|
|
@@ -203,6 +206,24 @@ export declare class ErrorFactory {
|
|
|
203
206
|
* Create a PDF page limit exceeded error
|
|
204
207
|
*/
|
|
205
208
|
static pdfPageLimitExceeded(estimatedPages: number, maxPages: number, provider: string): NeuroLinkError;
|
|
209
|
+
/**
|
|
210
|
+
* The combined page count across every PDF in one request exceeds what the
|
|
211
|
+
* provider accepts (#309). Distinct from `pdfPageLimitExceeded`, which is
|
|
212
|
+
* per-file — here each document can be individually legal.
|
|
213
|
+
*/
|
|
214
|
+
static pdfAggregatePageLimitExceeded(fileCount: number, totalPages: number, maxPages: number, provider: string): NeuroLinkError;
|
|
215
|
+
/**
|
|
216
|
+
* The combined byte size across every PDF in one request exceeds what the
|
|
217
|
+
* provider accepts (#309).
|
|
218
|
+
*/
|
|
219
|
+
static pdfAggregateSizeLimitExceeded(fileCount: number, totalMB: number, maxSizeMB: number, provider: string): NeuroLinkError;
|
|
220
|
+
/**
|
|
221
|
+
* A PDF supplied through the untrusted `input.content` surface could not be
|
|
222
|
+
* parsed for a page count (#309). Caller-supplied `metadata.pages` is not
|
|
223
|
+
* authoritative there, so an unreadable document is rejected rather than
|
|
224
|
+
* admitted with an assumed count of zero.
|
|
225
|
+
*/
|
|
226
|
+
static pdfPageCountUnverifiable(filenames: string[], provider: string): NeuroLinkError;
|
|
206
227
|
/**
|
|
207
228
|
* The PDF is encrypted and no password was supplied (#258).
|
|
208
229
|
*/
|
|
@@ -52,6 +52,9 @@ export const ERROR_CODES = {
|
|
|
52
52
|
CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED",
|
|
53
53
|
// PDF validation errors
|
|
54
54
|
PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED",
|
|
55
|
+
PDF_AGGREGATE_PAGE_LIMIT_EXCEEDED: "PDF_AGGREGATE_PAGE_LIMIT_EXCEEDED",
|
|
56
|
+
PDF_AGGREGATE_SIZE_LIMIT_EXCEEDED: "PDF_AGGREGATE_SIZE_LIMIT_EXCEEDED",
|
|
57
|
+
PDF_PAGE_COUNT_UNVERIFIABLE: "PDF_PAGE_COUNT_UNVERIFIABLE",
|
|
55
58
|
PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED",
|
|
56
59
|
PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD",
|
|
57
60
|
// Rate limiter errors
|
|
@@ -557,6 +560,56 @@ export class ErrorFactory {
|
|
|
557
560
|
},
|
|
558
561
|
});
|
|
559
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* The combined page count across every PDF in one request exceeds what the
|
|
565
|
+
* provider accepts (#309). Distinct from `pdfPageLimitExceeded`, which is
|
|
566
|
+
* per-file — here each document can be individually legal.
|
|
567
|
+
*/
|
|
568
|
+
static pdfAggregatePageLimitExceeded(fileCount, totalPages, maxPages, provider) {
|
|
569
|
+
return new NeuroLinkError({
|
|
570
|
+
code: ERROR_CODES.PDF_AGGREGATE_PAGE_LIMIT_EXCEEDED,
|
|
571
|
+
message: `[PDF] Combined page count across ${fileCount} PDF(s) (${totalPages}) exceeds the ` +
|
|
572
|
+
`${maxPages}-page limit for ${provider}. ` +
|
|
573
|
+
`Split the request or reduce the number of PDFs.`,
|
|
574
|
+
category: ErrorCategory.VALIDATION,
|
|
575
|
+
severity: ErrorSeverity.MEDIUM,
|
|
576
|
+
retriable: false,
|
|
577
|
+
context: { fileCount, totalPages, maxPages, provider },
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The combined byte size across every PDF in one request exceeds what the
|
|
582
|
+
* provider accepts (#309).
|
|
583
|
+
*/
|
|
584
|
+
static pdfAggregateSizeLimitExceeded(fileCount, totalMB, maxSizeMB, provider) {
|
|
585
|
+
return new NeuroLinkError({
|
|
586
|
+
code: ERROR_CODES.PDF_AGGREGATE_SIZE_LIMIT_EXCEEDED,
|
|
587
|
+
message: `[PDF] Combined size across ${fileCount} PDF(s) (${totalMB.toFixed(2)}MB) exceeds the ` +
|
|
588
|
+
`${maxSizeMB}MB limit for ${provider}.`,
|
|
589
|
+
category: ErrorCategory.VALIDATION,
|
|
590
|
+
severity: ErrorSeverity.MEDIUM,
|
|
591
|
+
retriable: false,
|
|
592
|
+
context: { fileCount, totalMB, maxSizeMB, provider },
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* A PDF supplied through the untrusted `input.content` surface could not be
|
|
597
|
+
* parsed for a page count (#309). Caller-supplied `metadata.pages` is not
|
|
598
|
+
* authoritative there, so an unreadable document is rejected rather than
|
|
599
|
+
* admitted with an assumed count of zero.
|
|
600
|
+
*/
|
|
601
|
+
static pdfPageCountUnverifiable(filenames, provider) {
|
|
602
|
+
return new NeuroLinkError({
|
|
603
|
+
code: ERROR_CODES.PDF_PAGE_COUNT_UNVERIFIABLE,
|
|
604
|
+
message: `[PDF] Cannot verify the page count for ${filenames.length} PDF(s) supplied via ` +
|
|
605
|
+
`input.content (${filenames.join(", ")}). Provide readable PDFs, or submit them ` +
|
|
606
|
+
`via input.pdfFiles where page counts are derived during detection.`,
|
|
607
|
+
category: ErrorCategory.VALIDATION,
|
|
608
|
+
severity: ErrorSeverity.MEDIUM,
|
|
609
|
+
retriable: false,
|
|
610
|
+
context: { filenames, provider },
|
|
611
|
+
});
|
|
612
|
+
}
|
|
560
613
|
/**
|
|
561
614
|
* The PDF is encrypted and no password was supplied (#258).
|
|
562
615
|
*/
|
|
@@ -344,13 +344,13 @@ export class FileDetector {
|
|
|
344
344
|
logger.warn(`[FileDetector] All fallback parsing failed for type "${detection.type}". ` +
|
|
345
345
|
`Attempted: ${options.allowedTypes.join(", ")}. Falling through to universal handler.`);
|
|
346
346
|
const csvOptions = options?.csvOptions;
|
|
347
|
-
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider);
|
|
347
|
+
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions);
|
|
348
348
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
349
349
|
return result;
|
|
350
350
|
}
|
|
351
351
|
const content = await FileDetector.loadContent(input, detection, options);
|
|
352
352
|
const csvOptions = options?.csvOptions;
|
|
353
|
-
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider);
|
|
353
|
+
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions);
|
|
354
354
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
355
355
|
return result;
|
|
356
356
|
});
|
|
@@ -967,7 +967,7 @@ export class FileDetector {
|
|
|
967
967
|
/**
|
|
968
968
|
* Route to appropriate processor
|
|
969
969
|
*/
|
|
970
|
-
static async processFile(content, detection, options, provider) {
|
|
970
|
+
static async processFile(content, detection, options, provider, videoOptions) {
|
|
971
971
|
switch (detection.type) {
|
|
972
972
|
case "csv":
|
|
973
973
|
// Pass original extension through to CSV processor; if detection has none,
|
|
@@ -985,7 +985,7 @@ export class FileDetector {
|
|
|
985
985
|
// AI providers don't support SVG as image format, so we extract text content
|
|
986
986
|
return await FileDetector.processSvgAsText(content, detection);
|
|
987
987
|
case "video":
|
|
988
|
-
return await FileDetector.processVideoFile(content, detection);
|
|
988
|
+
return await FileDetector.processVideoFile(content, detection, videoOptions);
|
|
989
989
|
case "audio":
|
|
990
990
|
return await FileDetector.processAudioFile(content, detection);
|
|
991
991
|
case "archive":
|
|
@@ -1031,7 +1031,7 @@ export class FileDetector {
|
|
|
1031
1031
|
/**
|
|
1032
1032
|
* Process video file: extract metadata, keyframes, and subtitles via VideoProcessor
|
|
1033
1033
|
*/
|
|
1034
|
-
static async processVideoFile(content, detection) {
|
|
1034
|
+
static async processVideoFile(content, detection, videoOptions) {
|
|
1035
1035
|
const videoFilename = detection.metadata.filename || "video";
|
|
1036
1036
|
try {
|
|
1037
1037
|
const videoResult = await (await getVideoProcessor()).processFile({
|
|
@@ -1040,7 +1040,10 @@ export class FileDetector {
|
|
|
1040
1040
|
mimetype: detection.mimeType || "video/mp4",
|
|
1041
1041
|
size: content.length,
|
|
1042
1042
|
buffer: content,
|
|
1043
|
-
}
|
|
1043
|
+
},
|
|
1044
|
+
// #478: carry the caller's keyframe budget/quality/format through to
|
|
1045
|
+
// the processor; previously these stopped at the CLI layer.
|
|
1046
|
+
videoOptions);
|
|
1044
1047
|
if (videoResult.success && videoResult.data) {
|
|
1045
1048
|
return {
|
|
1046
1049
|
type: "video",
|