@juspay/neurolink 10.10.8 → 10.10.10
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/dist/browser/neurolink.min.js +398 -398
- package/dist/cli/factories/commandFactory.js +6 -0
- package/dist/lib/processors/media/AudioProcessor.js +33 -15
- package/dist/lib/skills/skillsManager.js +24 -5
- package/dist/lib/types/file.d.ts +10 -0
- package/dist/lib/types/generate.d.ts +15 -0
- package/dist/lib/types/processor.d.ts +6 -0
- package/dist/lib/types/stream.d.ts +11 -0
- package/dist/lib/utils/csvProcessor.d.ts +20 -0
- package/dist/lib/utils/csvProcessor.js +162 -47
- package/dist/lib/utils/messageBuilder.js +40 -3
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/lib/utils/pdfProcessor.js +10 -0
- package/dist/processors/media/AudioProcessor.js +33 -15
- package/dist/skills/skillsManager.js +24 -5
- package/dist/types/file.d.ts +10 -0
- package/dist/types/generate.d.ts +15 -0
- package/dist/types/processor.d.ts +6 -0
- package/dist/types/stream.d.ts +11 -0
- package/dist/utils/csvProcessor.d.ts +20 -0
- package/dist/utils/csvProcessor.js +162 -47
- package/dist/utils/messageBuilder.js +40 -3
- package/dist/utils/multimodalOptionsBuilder.d.ts +2 -0
- package/dist/utils/pdfProcessor.js +10 -0
- package/package.json +1 -1
|
@@ -228,6 +228,11 @@ export class CLICommandFactory {
|
|
|
228
228
|
type: "number",
|
|
229
229
|
description: "Wall-clock cap (ms) for CSV parsing; returns partial rows on timeout (#379).",
|
|
230
230
|
},
|
|
231
|
+
"csv-skip-empty-lines": {
|
|
232
|
+
type: "boolean",
|
|
233
|
+
default: true,
|
|
234
|
+
description: "Skip blank/whitespace-only CSV data rows in content and rowCount (default true). Use --no-csv-skip-empty-lines to preserve them (#373).",
|
|
235
|
+
},
|
|
231
236
|
model: {
|
|
232
237
|
type: "string",
|
|
233
238
|
description: "Specific model to use (e.g. gemini-2.5-pro, gemini-2.5-flash)",
|
|
@@ -2441,6 +2446,7 @@ export class CLICommandFactory {
|
|
|
2441
2446
|
sanitizeColumnNames: argv.csvSanitizeNames,
|
|
2442
2447
|
columnNameCase: argv.csvNameCase,
|
|
2443
2448
|
parseTimeoutMs: argv.csvParseTimeoutMs,
|
|
2449
|
+
skipEmptyLines: argv.csvSkipEmptyLines,
|
|
2444
2450
|
};
|
|
2445
2451
|
}
|
|
2446
2452
|
/**
|
|
@@ -41,6 +41,7 @@ import { SIZE_LIMITS_MB } from "../config/index.js";
|
|
|
41
41
|
import { FileErrorCode } from "../errors/index.js";
|
|
42
42
|
import { withTimeout } from "../../utils/timeout.js";
|
|
43
43
|
import { formatMediaDuration } from "../../utils/mediaDuration.js";
|
|
44
|
+
import { logger } from "../../utils/logger.js";
|
|
44
45
|
import { tryImport } from "../../utils/tryImport.js";
|
|
45
46
|
let _musicMetadata = null;
|
|
46
47
|
async function loadMusicMetadata() {
|
|
@@ -264,6 +265,11 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
264
265
|
transcript: transcriptionResult.transcript,
|
|
265
266
|
hasTranscript: transcriptionResult.hasTranscript,
|
|
266
267
|
transcriptionProvider: transcriptionResult.transcriptionProvider,
|
|
268
|
+
...(transcriptionResult.transcriptionSkippedReason
|
|
269
|
+
? {
|
|
270
|
+
transcriptionSkippedReason: transcriptionResult.transcriptionSkippedReason,
|
|
271
|
+
}
|
|
272
|
+
: {}),
|
|
267
273
|
coverArt: coverArt ?? undefined,
|
|
268
274
|
buffer,
|
|
269
275
|
mimetype: fileInfo.mimetype || "audio/mpeg",
|
|
@@ -316,20 +322,25 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
316
322
|
* @returns Transcription result with transcript text, or empty result
|
|
317
323
|
*/
|
|
318
324
|
async attemptTranscription(buffer, filename, mimetype) {
|
|
319
|
-
const
|
|
320
|
-
transcript:
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
const skipped = (reason) => {
|
|
326
|
+
logger.warn(`[AudioProcessor] No transcript for ${filename}: ${reason}. ` +
|
|
327
|
+
`The model will receive metadata only.`);
|
|
328
|
+
return {
|
|
329
|
+
transcript: undefined,
|
|
330
|
+
hasTranscript: false,
|
|
331
|
+
transcriptionProvider: undefined,
|
|
332
|
+
transcriptionSkippedReason: reason,
|
|
333
|
+
};
|
|
323
334
|
};
|
|
324
335
|
// Check if OPENAI_API_KEY is available
|
|
325
336
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
326
337
|
if (!apiKey) {
|
|
327
|
-
return
|
|
338
|
+
return skipped("OPENAI_API_KEY is not set, and Whisper is the only transcription backend wired up");
|
|
328
339
|
}
|
|
329
340
|
// Check file size (Whisper limit is 25MB)
|
|
330
341
|
const fileSizeMB = buffer.length / (1024 * 1024);
|
|
331
342
|
if (fileSizeMB > AUDIO_CONFIG.WHISPER_MAX_SIZE_MB) {
|
|
332
|
-
return
|
|
343
|
+
return skipped(`file is ${fileSizeMB.toFixed(1)}MB, over Whisper's ${AUDIO_CONFIG.WHISPER_MAX_SIZE_MB}MB limit — split or compress it`);
|
|
333
344
|
}
|
|
334
345
|
// Check if file format is supported by Whisper
|
|
335
346
|
const ext = filename.split(".").pop()?.toLowerCase();
|
|
@@ -343,7 +354,7 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
343
354
|
mimetype.startsWith("audio/ogg") ||
|
|
344
355
|
mimetype.startsWith("audio/x-m4a"));
|
|
345
356
|
if (!isFormatSupported && !isMimeSupported) {
|
|
346
|
-
return
|
|
357
|
+
return skipped(`format is not one Whisper accepts (extension "${ext ?? "none"}", mimetype "${mimetype ?? "none"}"); supported: ${AUDIO_CONFIG.WHISPER_SUPPORTED_FORMATS.join(", ")}`);
|
|
347
358
|
}
|
|
348
359
|
try {
|
|
349
360
|
// Dynamic imports to avoid loading these modules when transcription is not needed
|
|
@@ -351,26 +362,33 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
351
362
|
const openai = createOpenAI({ apiKey });
|
|
352
363
|
const model = openai.transcription("whisper-1");
|
|
353
364
|
// Wrap in withTimeout — large audio files can take a while, but a
|
|
354
|
-
// stalled request shouldn't block the processor forever.
|
|
355
|
-
//
|
|
356
|
-
//
|
|
365
|
+
// stalled request shouldn't block the processor forever. A TimeoutError
|
|
366
|
+
// lands in the same handler as other failures below, which reports it as
|
|
367
|
+
// the reason rather than discarding it.
|
|
357
368
|
const result = await withTimeout(experimental_transcribe({
|
|
358
369
|
model,
|
|
359
370
|
audio: buffer,
|
|
360
371
|
}), AUDIO_CONFIG.TRANSCRIPTION_TIMEOUT_MS, "openai-whisper", "generate");
|
|
361
372
|
if (result.text && result.text.trim().length > 0) {
|
|
373
|
+
logger.debug(`[AudioProcessor] Transcribed ${filename} via openai-whisper (${result.text.trim().length} chars)`);
|
|
362
374
|
return {
|
|
363
375
|
transcript: result.text.trim(),
|
|
364
376
|
hasTranscript: true,
|
|
365
377
|
transcriptionProvider: "openai-whisper",
|
|
378
|
+
transcriptionSkippedReason: undefined,
|
|
366
379
|
};
|
|
367
380
|
}
|
|
368
|
-
|
|
381
|
+
// A successful call that returned nothing is a legitimate outcome
|
|
382
|
+
// (silence, music, no speech) — distinct from a failure.
|
|
383
|
+
return skipped("Whisper returned an empty transcript for this audio");
|
|
369
384
|
}
|
|
370
|
-
catch {
|
|
371
|
-
// Transcription
|
|
372
|
-
//
|
|
373
|
-
|
|
385
|
+
catch (error) {
|
|
386
|
+
// Transcription stays best-effort — a failure must never kill the whole
|
|
387
|
+
// processing pipeline. But discarding the error outright, as this block
|
|
388
|
+
// used to, made a bad API key, a rate limit and a network blip all look
|
|
389
|
+
// identical to "this file has no speech in it".
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
391
|
+
return skipped(`transcription request failed — ${message}`);
|
|
374
392
|
}
|
|
375
393
|
}
|
|
376
394
|
// ===========================================================================
|
|
@@ -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
|
@@ -200,6 +200,12 @@ export type CSVProcessorOptions = {
|
|
|
200
200
|
* rather than hanging forever. Defaults: 30s for strings, 5min for files.
|
|
201
201
|
*/
|
|
202
202
|
parseTimeoutMs?: number;
|
|
203
|
+
/**
|
|
204
|
+
* Skip blank / whitespace-only data rows (#373). Default `true`: blank lines
|
|
205
|
+
* are excluded from the returned content (including raw CSV text) and from
|
|
206
|
+
* `metadata.rowCount`. Set to `false` to preserve empty lines literally.
|
|
207
|
+
*/
|
|
208
|
+
skipEmptyLines?: boolean;
|
|
203
209
|
};
|
|
204
210
|
/**
|
|
205
211
|
* PDF API types for different providers
|
|
@@ -474,6 +480,10 @@ export type MultimodalPdfEntry = {
|
|
|
474
480
|
password?: string;
|
|
475
481
|
/** Per-page pixel ceiling for the image fallback (#260). */
|
|
476
482
|
maxCanvasPixels?: number;
|
|
483
|
+
/** Render scale for the image fallback (#297). */
|
|
484
|
+
scale?: number;
|
|
485
|
+
/** Max pages converted by the image fallback (#297). */
|
|
486
|
+
maxPages?: number;
|
|
477
487
|
};
|
|
478
488
|
/** Result of PDF to image conversion. */
|
|
479
489
|
export type PDFImageConversionResult = {
|
|
@@ -133,6 +133,17 @@ 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?: {
|
|
138
149
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
@@ -1265,6 +1276,10 @@ export type TextGenerationOptions = {
|
|
|
1265
1276
|
password?: string;
|
|
1266
1277
|
/** Max rendered-canvas pixels per page (#260 memory guard); oversized pages auto-downscale. */
|
|
1267
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;
|
|
1268
1283
|
};
|
|
1269
1284
|
enableSummarization?: boolean;
|
|
1270
1285
|
/**
|
|
@@ -703,6 +703,12 @@ export type ProcessedAudio = ProcessedFileBase & {
|
|
|
703
703
|
transcript?: string;
|
|
704
704
|
hasTranscript: boolean;
|
|
705
705
|
transcriptionProvider?: string;
|
|
706
|
+
/**
|
|
707
|
+
* Why transcription produced nothing, when it did (#416). Absent on success.
|
|
708
|
+
* Lets a caller distinguish "this audio has no speech" from "the transcription
|
|
709
|
+
* backend was never reachable", which previously looked identical.
|
|
710
|
+
*/
|
|
711
|
+
transcriptionSkippedReason?: string;
|
|
706
712
|
coverArt?: Buffer;
|
|
707
713
|
};
|
|
708
714
|
/**
|
|
@@ -225,6 +225,17 @@ 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?: {
|
|
230
241
|
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
@@ -30,6 +30,18 @@ export declare function isValidCsvRow(row: unknown): row is CSVRow;
|
|
|
30
30
|
* match) when a row violates the invariant.
|
|
31
31
|
*/
|
|
32
32
|
export declare function assertValidCsvRow(row: unknown, rowNumber: number): asserts row is CSVRow;
|
|
33
|
+
/**
|
|
34
|
+
* True when a parsed CSV row is blank: no keys, or every value is empty /
|
|
35
|
+
* whitespace-only (#373). Shared by the structured post-filter and by
|
|
36
|
+
* `streamParse` so blank rows do not consume `maxRows`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isBlankCsvDataRow(row: CSVRow): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Raw-line counterpart of {@link isBlankCsvDataRow} (#373): a fully blank /
|
|
41
|
+
* whitespace line, or a delimiter-only line such as `,,` / ` , ` whose
|
|
42
|
+
* fields are all empty after a CSV-aware split.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isBlankCsvRawLine(line: string, delimiter: string): boolean;
|
|
33
45
|
/**
|
|
34
46
|
* Detect if first line is CSV metadata (not actual data/headers)
|
|
35
47
|
* Common patterns:
|
|
@@ -174,11 +186,17 @@ export declare class CSVProcessor {
|
|
|
174
186
|
/**
|
|
175
187
|
* Format parsed CSV data for LLM consumption
|
|
176
188
|
* Only used for JSON and Markdown formats (raw format handled separately)
|
|
189
|
+
*
|
|
190
|
+
* @param columnNames - Optional schema headers (#373). When preserved blank
|
|
191
|
+
* rows lead `rows`, Markdown must still use the real column names.
|
|
177
192
|
*/
|
|
178
193
|
private static formatForLLM;
|
|
179
194
|
/**
|
|
180
195
|
* Format as markdown table
|
|
181
196
|
* Best for small datasets (<100 rows)
|
|
197
|
+
*
|
|
198
|
+
* @param columnNames - Optional explicit headers (#373). Falls back to
|
|
199
|
+
* `Object.keys(rows[0])` when omitted.
|
|
182
200
|
*/
|
|
183
201
|
private static toMarkdownTable;
|
|
184
202
|
/**
|
|
@@ -187,6 +205,7 @@ export declare class CSVProcessor {
|
|
|
187
205
|
* @param sampleRows - Array of sample row objects
|
|
188
206
|
* @param format - Output format for sample data
|
|
189
207
|
* @param includeHeaders - Whether to include headers in CSV/markdown formats
|
|
208
|
+
* @param columnNames - Optional schema headers for csv/markdown (#373)
|
|
190
209
|
* @returns Formatted sample data as string or array
|
|
191
210
|
*/
|
|
192
211
|
private static formatSampleData;
|
|
@@ -195,6 +214,7 @@ export declare class CSVProcessor {
|
|
|
195
214
|
*
|
|
196
215
|
* @param rows - Array of row objects
|
|
197
216
|
* @param includeHeaders - Whether to include header row
|
|
217
|
+
* @param columnNames - Optional explicit headers (#373)
|
|
198
218
|
* @returns CSV formatted string
|
|
199
219
|
*/
|
|
200
220
|
private static toCSVString;
|