@juspay/neurolink 12.31.1 → 12.32.1
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 +2 -3
- package/dist/browser/neurolink.min.js +393 -390
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/core/modules/MessageBuilder.js +8 -0
- package/dist/neurolink.js +4 -0
- package/dist/processors/media/AudioProcessor.d.ts +87 -10
- package/dist/processors/media/AudioProcessor.js +341 -47
- package/dist/types/file.d.ts +69 -1
- package/dist/types/generate.d.ts +51 -0
- package/dist/types/processor.d.ts +19 -0
- package/dist/types/stream.d.ts +17 -0
- package/dist/utils/fileDetector.js +40 -18
- package/dist/utils/messageBuilder.js +66 -5
- package/docs-site/static/search-index.json +4323 -4311
- package/package.json +3 -1
|
@@ -714,6 +714,25 @@ export type ProcessedAudio = ProcessedFileBase & {
|
|
|
714
714
|
transcript?: string;
|
|
715
715
|
hasTranscript: boolean;
|
|
716
716
|
transcriptionProvider?: string;
|
|
717
|
+
/**
|
|
718
|
+
* Language the transcription backend reported for the speech (#409), when
|
|
719
|
+
* it reported one.
|
|
720
|
+
*
|
|
721
|
+
* Not always a *detected* language: when the backend's response carries no
|
|
722
|
+
* language (Whisper's `verbose_json` usually does; Google/Azure responses
|
|
723
|
+
* vary), this falls back to the caller-requested `options.language` instead
|
|
724
|
+
* of going unset. A caller that must distinguish "the backend detected X"
|
|
725
|
+
* from "X is just what was asked for" cannot do so from this field alone.
|
|
726
|
+
*/
|
|
727
|
+
transcriptionLanguage?: string;
|
|
728
|
+
/**
|
|
729
|
+
* Audio duration in seconds as measured by the transcription backend (#409).
|
|
730
|
+
*
|
|
731
|
+
* Kept apart from `metadata.duration`, which comes from the container
|
|
732
|
+
* header: the two disagree on a file with a broken or absent header, and the
|
|
733
|
+
* header is the one that is available without a transcription call.
|
|
734
|
+
*/
|
|
735
|
+
transcriptionDuration?: number;
|
|
717
736
|
/**
|
|
718
737
|
* Why transcription produced nothing, when it did (#416). Absent on success.
|
|
719
738
|
* Lets a caller distinguish "this audio has no speech" from "the transcription
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -401,6 +401,23 @@ export type StreamOptions = {
|
|
|
401
401
|
*/
|
|
402
402
|
outputFormat?: VisionImageOutputFormat;
|
|
403
403
|
};
|
|
404
|
+
/**
|
|
405
|
+
* Audio transcription options for attached audio files (#413/#440).
|
|
406
|
+
*
|
|
407
|
+
* Mirrors `GenerateOptions.audioOptions`. `stream()` spreads its options
|
|
408
|
+
* rather than rebuilding them field by field, so declaring it here is what
|
|
409
|
+
* lets it reach the message builder.
|
|
410
|
+
*/
|
|
411
|
+
audioOptions?: {
|
|
412
|
+
/** Backend: "openai" (aliases "whisper"), "google" or "azure". */
|
|
413
|
+
provider?: string;
|
|
414
|
+
/** Transcription model, e.g. "whisper-1". Backend-specific. */
|
|
415
|
+
transcriptionModel?: string;
|
|
416
|
+
/** Language hint, e.g. "en". */
|
|
417
|
+
language?: string;
|
|
418
|
+
/** OpenAI/Whisper-only context prompt to bias transcription; ignored by Google and Azure. */
|
|
419
|
+
prompt?: string;
|
|
420
|
+
};
|
|
404
421
|
/**
|
|
405
422
|
* Text-to-Speech (TTS) configuration for streaming
|
|
406
423
|
*
|
|
@@ -340,15 +340,13 @@ export class FileDetector {
|
|
|
340
340
|
}
|
|
341
341
|
logger.warn(`[FileDetector] All fallback parsing failed for type "${detection.type}". ` +
|
|
342
342
|
`Attempted: ${options.allowedTypes.join(", ")}. Falling through to universal handler.`);
|
|
343
|
-
const
|
|
344
|
-
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions, options?.officeOptions);
|
|
343
|
+
const result = await FileDetector.processFile(content, detection, options);
|
|
345
344
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
346
345
|
return result;
|
|
347
346
|
}
|
|
348
347
|
const content = loadedUrl?.content ??
|
|
349
348
|
(await FileDetector.loadContent(input, detection, options));
|
|
350
|
-
const
|
|
351
|
-
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions, options?.officeOptions);
|
|
349
|
+
const result = await FileDetector.processFile(content, detection, options);
|
|
352
350
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
353
351
|
return result;
|
|
354
352
|
});
|
|
@@ -1063,31 +1061,33 @@ export class FileDetector {
|
|
|
1063
1061
|
/**
|
|
1064
1062
|
* Route to appropriate processor
|
|
1065
1063
|
*/
|
|
1066
|
-
static async processFile(content, detection, options
|
|
1064
|
+
static async processFile(content, detection, options) {
|
|
1067
1065
|
switch (detection.type) {
|
|
1068
1066
|
case "csv":
|
|
1069
1067
|
// Pass original extension through to CSV processor; if detection has none,
|
|
1070
1068
|
// fall back to any extension provided in csvOptions.
|
|
1071
1069
|
return await CSVProcessor.process(content, {
|
|
1072
|
-
...options,
|
|
1073
|
-
extension: detection.extension ?? options?.extension,
|
|
1070
|
+
...options?.csvOptions,
|
|
1071
|
+
extension: detection.extension ?? options?.csvOptions?.extension,
|
|
1074
1072
|
});
|
|
1075
1073
|
case "image":
|
|
1076
1074
|
return await ImageProcessor.process(content);
|
|
1077
1075
|
case "pdf":
|
|
1078
|
-
return await PDFProcessor.process(content, {
|
|
1076
|
+
return await PDFProcessor.process(content, {
|
|
1077
|
+
provider: options?.provider,
|
|
1078
|
+
});
|
|
1079
1079
|
case "svg":
|
|
1080
1080
|
// SVG is processed as text content (sanitized XML markup)
|
|
1081
1081
|
// AI providers don't support SVG as image format, so we extract text content
|
|
1082
1082
|
return await FileDetector.processSvgAsText(content, detection);
|
|
1083
1083
|
case "video":
|
|
1084
|
-
return await FileDetector.processVideoFile(content, detection, videoOptions);
|
|
1084
|
+
return await FileDetector.processVideoFile(content, detection, options?.videoOptions);
|
|
1085
1085
|
case "audio":
|
|
1086
|
-
return await FileDetector.processAudioFile(content, detection);
|
|
1086
|
+
return await FileDetector.processAudioFile(content, detection, options?.audioOptions);
|
|
1087
1087
|
case "archive":
|
|
1088
1088
|
return await FileDetector.processArchiveFile(content, detection);
|
|
1089
1089
|
case "xlsx":
|
|
1090
|
-
return await FileDetector.processXlsxFile(content, detection, officeOptions);
|
|
1090
|
+
return await FileDetector.processXlsxFile(content, detection, options?.officeOptions);
|
|
1091
1091
|
case "docx":
|
|
1092
1092
|
return await FileDetector.processDocxFile(content, detection);
|
|
1093
1093
|
case "pptx":
|
|
@@ -1197,7 +1197,7 @@ export class FileDetector {
|
|
|
1197
1197
|
/**
|
|
1198
1198
|
* Process audio file: extract metadata, tags, and cover art via AudioProcessor
|
|
1199
1199
|
*/
|
|
1200
|
-
static async processAudioFile(content, detection) {
|
|
1200
|
+
static async processAudioFile(content, detection, audioOptions) {
|
|
1201
1201
|
const audioFilename = detection.metadata.filename || "audio";
|
|
1202
1202
|
try {
|
|
1203
1203
|
const audioResult = await (await getAudioProcessor()).processFile({
|
|
@@ -1206,18 +1206,40 @@ export class FileDetector {
|
|
|
1206
1206
|
mimetype: detection.mimeType || "audio/mpeg",
|
|
1207
1207
|
size: content.length,
|
|
1208
1208
|
buffer: content,
|
|
1209
|
-
}
|
|
1209
|
+
},
|
|
1210
|
+
// #440: carry the caller's transcription provider/language/prompt
|
|
1211
|
+
// through to the processor; previously these stopped here.
|
|
1212
|
+
audioOptions);
|
|
1210
1213
|
if (audioResult.success && audioResult.data) {
|
|
1214
|
+
const audio = audioResult.data;
|
|
1211
1215
|
return {
|
|
1212
1216
|
type: "audio",
|
|
1213
|
-
content:
|
|
1217
|
+
content: audio.textContent ||
|
|
1214
1218
|
FileDetector.formatInformativePlaceholder("Audio", audioFilename, content, detection),
|
|
1215
1219
|
mimeType: detection.mimeType,
|
|
1216
1220
|
// Surface embedded cover art as an image content block
|
|
1217
|
-
images:
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
+
images: audio.coverArt ? [audio.coverArt] : undefined,
|
|
1222
|
+
metadata: {
|
|
1223
|
+
...detection.metadata,
|
|
1224
|
+
// #409: the processor's audio metadata used to stop here —
|
|
1225
|
+
// `detection.metadata` alone carries only size/filename/confidence,
|
|
1226
|
+
// so duration and everything about the transcript were dropped by
|
|
1227
|
+
// the very call that produced them.
|
|
1228
|
+
...(audio.metadata.duration > 0
|
|
1229
|
+
? { duration: audio.metadata.duration }
|
|
1230
|
+
: {}),
|
|
1231
|
+
...(audio.transcriptionLanguage
|
|
1232
|
+
? { language: audio.transcriptionLanguage }
|
|
1233
|
+
: {}),
|
|
1234
|
+
// Written whenever a backend actually ran, so 0 (a real empty
|
|
1235
|
+
// transcript) stays distinguishable from "never attempted".
|
|
1236
|
+
...(audio.transcriptionProvider
|
|
1237
|
+
? {
|
|
1238
|
+
transcriptionLength: audio.transcript?.length ?? 0,
|
|
1239
|
+
transcriptionProvider: audio.transcriptionProvider,
|
|
1240
|
+
}
|
|
1241
|
+
: {}),
|
|
1242
|
+
},
|
|
1221
1243
|
};
|
|
1222
1244
|
}
|
|
1223
1245
|
}
|
|
@@ -11,7 +11,7 @@ import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../contex
|
|
|
11
11
|
import { isCSVContent, isTextContent, SIZE_TIER_THRESHOLDS, } from "../types/index.js";
|
|
12
12
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
13
13
|
import { EXIF_PROBE_PREFIX_BYTES, mayCarryExifOrientation, needsVisionTranscode, normalizeImageOrientation, probeExifOrientation, toVisionCompatibleImage, } from "../adapters/imageFormatSupport.js";
|
|
14
|
-
import { FILE_TYPE_REGISTRY, lookupByExtension, } from "../processors/config/fileTypeRegistry.js";
|
|
14
|
+
import { extensionsForModality, FILE_TYPE_REGISTRY, lookupByExtension, } from "../processors/config/fileTypeRegistry.js";
|
|
15
15
|
import { ErrorFactory, NeuroLinkError, withTimeout } from "./errorHandling.js";
|
|
16
16
|
import { FileDetector } from "./fileDetector.js";
|
|
17
17
|
import { detectIsoBmffImageMimeType } from "./isoBmff.js";
|
|
@@ -911,11 +911,41 @@ async function detectFileForUnifiedArray(file, options, provider, genericFileMax
|
|
|
911
911
|
transcribeAudio: options.videoOptions.transcribeAudio,
|
|
912
912
|
}
|
|
913
913
|
: undefined,
|
|
914
|
+
// #413/#440: same story for audio — the transcription backend, language
|
|
915
|
+
// and prompt a caller chose have to reach AudioProcessor, which is
|
|
916
|
+
// several hops down from here.
|
|
917
|
+
audioOptions: options.audioOptions
|
|
918
|
+
? {
|
|
919
|
+
provider: options.audioOptions.provider,
|
|
920
|
+
transcriptionModel: options.audioOptions.transcriptionModel,
|
|
921
|
+
language: options.audioOptions.language,
|
|
922
|
+
prompt: options.audioOptions.prompt,
|
|
923
|
+
}
|
|
924
|
+
: undefined,
|
|
914
925
|
provider: provider,
|
|
915
926
|
mimetypeHint: fileMimetypeHint,
|
|
916
927
|
filenameHint: fileFilenameHint,
|
|
917
928
|
});
|
|
918
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Guidance appended to the system prompt when an audio file is attached (#471).
|
|
932
|
+
*
|
|
933
|
+
* An audio attachment does not reach the model as audio on most providers: the
|
|
934
|
+
* processor transcribes it and inlines the transcript as text. Without saying
|
|
935
|
+
* so, models reliably answer "I cannot listen to audio" while the transcript
|
|
936
|
+
* of that very recording sits in the prompt above — the audio equivalent of
|
|
937
|
+
* the "no files attached" failure the CSV/PDF guidance exists to prevent.
|
|
938
|
+
*
|
|
939
|
+
* It also states the negative case, because a file whose transcription was
|
|
940
|
+
* skipped (no backend configured, unsupported format) still arrives, carrying
|
|
941
|
+
* metadata and a stated reason. A model told only "audio is transcribed" will
|
|
942
|
+
* otherwise invent speech content for a file that has none.
|
|
943
|
+
*/
|
|
944
|
+
const AUDIO_PROMPT_EXTENSIONS = extensionsForModality("audio");
|
|
945
|
+
const AUDIO_TRANSCRIPTION_INSTRUCTIONS = `
|
|
946
|
+
- Any audio file has ALREADY BEEN PROCESSED for you: its metadata, and its transcript when one could be produced, are inlined above under "## Audio File: ..." — you do not need to, and cannot, listen to the recording yourself.
|
|
947
|
+
- Treat the text under "--- Transcript ---" as the verbatim spoken content of that recording, and answer from it directly. Do NOT reply that you are unable to process audio.
|
|
948
|
+
- If an audio file shows no transcript, transcription was skipped and the stated reason is inlined with it. Say so rather than inventing what the audio might contain.`;
|
|
919
949
|
export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
920
950
|
options.input ??= {};
|
|
921
951
|
if (!options.input.files || options.input.files.length === 0) {
|
|
@@ -962,6 +992,12 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
|
962
992
|
inp2.text = inp2.text || "";
|
|
963
993
|
let includedCount = 0;
|
|
964
994
|
const fileRegistry = options.fileRegistry;
|
|
995
|
+
// #471: the system-prompt augmentation below has to know whether any
|
|
996
|
+
// audio actually made it in. Counted from the detection result rather
|
|
997
|
+
// than from `inp2.nativeAudioFiles`, which is only populated when the
|
|
998
|
+
// raw bytes could also be re-read — an audio file whose transcript was
|
|
999
|
+
// inlined but whose bytes were not would otherwise go unmentioned.
|
|
1000
|
+
let audioIncludedCount = 0;
|
|
965
1001
|
// Only detection runs concurrently, and only for files that will not
|
|
966
1002
|
// take the lazy-registration branch.
|
|
967
1003
|
//
|
|
@@ -1022,6 +1058,9 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
|
1022
1058
|
(await detectFileForUnifiedArray(file, options, provider, genericFileMaxSize));
|
|
1023
1059
|
await appendDetectedFileResult(result, file, options);
|
|
1024
1060
|
includedCount++;
|
|
1061
|
+
if (result.type === "audio") {
|
|
1062
|
+
audioIncludedCount++;
|
|
1063
|
+
}
|
|
1025
1064
|
// Log what content type was added to the message
|
|
1026
1065
|
const contentType = result.type === "image" ? "image" : "text";
|
|
1027
1066
|
logger.info(`[NEUROLINK] File added to message: ${filename} as ${contentType} (type: ${result.type})`);
|
|
@@ -1065,7 +1104,7 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
|
1065
1104
|
- The full content of the user's local file(s) is INLINED in this message under "## CSV Data from ..." / "## PDF Data from ..." / "## File: ..." headings — it is the actual file the user is asking about.
|
|
1066
1105
|
- TREAT THE INLINED CONTENT AS IF IT WERE AN ATTACHMENT. Do NOT respond with "no files attached" or ask the user to re-upload — the data is already here.
|
|
1067
1106
|
- DO NOT use GitHub tools (get_file_contents, search_code, etc.) for local files - they only work for remote repository files.
|
|
1068
|
-
- Analyze the inlined file content directly without attempting to fetch or read files using tools
|
|
1107
|
+
- Analyze the inlined file content directly without attempting to fetch or read files using tools.${audioIncludedCount > 0 ? AUDIO_TRANSCRIPTION_INSTRUCTIONS : ""}`;
|
|
1069
1108
|
const existingSystem = (options.systemPrompt || "").trim();
|
|
1070
1109
|
options.systemPrompt = existingSystem
|
|
1071
1110
|
? `${existingSystem}${filePromptAugmentation}`
|
|
@@ -1320,7 +1359,20 @@ function buildMultimodalSystemPrompt(options, hasPDFFiles) {
|
|
|
1320
1359
|
const hasCSVFiles = (inp.csvFiles && inp.csvFiles.length > 0) ||
|
|
1321
1360
|
(inp.files &&
|
|
1322
1361
|
inp.files.some((f) => typeof f === "string" ? f.toLowerCase().endsWith(".csv") : false));
|
|
1323
|
-
|
|
1362
|
+
// #471: audio attachments. `processUnifiedFilesArray` has already run by the
|
|
1363
|
+
// time this is called, so `nativeAudioFiles` is the authoritative signal —
|
|
1364
|
+
// it is populated from the detector's verdict, which reads magic bytes and
|
|
1365
|
+
// therefore recognises an audio file whose name says nothing. The remaining
|
|
1366
|
+
// checks cover the paths that never reach the detector: an explicit
|
|
1367
|
+
// `audioFiles` alias (folded away by `foldMediaAliasesIntoFiles`, but only
|
|
1368
|
+
// once that has run) and a plain path attached by name.
|
|
1369
|
+
const hasAudioFiles = (inp.nativeAudioFiles && inp.nativeAudioFiles.length > 0) ||
|
|
1370
|
+
(inp.audioFiles && inp.audioFiles.length > 0) ||
|
|
1371
|
+
(inp.files &&
|
|
1372
|
+
inp.files.some((f) => typeof f === "string"
|
|
1373
|
+
? AUDIO_PROMPT_EXTENSIONS.some((ext) => f.toLowerCase().endsWith(ext))
|
|
1374
|
+
: false));
|
|
1375
|
+
if (hasCSVFiles || hasPDFFiles || hasAudioFiles) {
|
|
1324
1376
|
const fileTypes = [];
|
|
1325
1377
|
if (hasPDFFiles) {
|
|
1326
1378
|
fileTypes.push("PDFs");
|
|
@@ -1328,13 +1380,22 @@ function buildMultimodalSystemPrompt(options, hasPDFFiles) {
|
|
|
1328
1380
|
if (hasCSVFiles) {
|
|
1329
1381
|
fileTypes.push("CSVs");
|
|
1330
1382
|
}
|
|
1383
|
+
if (hasAudioFiles) {
|
|
1384
|
+
// Not "(transcribed)": `hasAudioFiles` only means an audio file was
|
|
1385
|
+
// attached, not that transcription actually produced anything — a file
|
|
1386
|
+
// with no backend configured, an unavailable pinned backend, or a
|
|
1387
|
+
// format/size the backend rejects still arrives here with metadata
|
|
1388
|
+
// only. AUDIO_TRANSCRIPTION_INSTRUCTIONS covers the per-file case; this
|
|
1389
|
+
// list must not assert the opposite of what it says.
|
|
1390
|
+
fileTypes.push("audio files (transcript may be unavailable)");
|
|
1391
|
+
}
|
|
1331
1392
|
systemPrompt += `\n\nIMPORTANT FILE HANDLING INSTRUCTIONS:
|
|
1332
|
-
- The full content of the user's local ${fileTypes.join(", ")} (and any images) is INLINED in this message under the "## CSV Data from ..." / "## PDF Data from ..." headings — it is the actual file the user is asking about.
|
|
1393
|
+
- The full content of the user's local ${fileTypes.join(", ")} (and any images) is INLINED in this message under the "## CSV Data from ..." / "## PDF Data from ..." / "## Audio File: ..." headings — it is the actual file the user is asking about.
|
|
1333
1394
|
- TREAT THE INLINED CONTENT AS IF IT WERE AN ATTACHMENT. Do NOT respond with "no files attached" or ask the user to re-upload — the data is already here.
|
|
1334
1395
|
- DO NOT use GitHub tools (get_file_contents, search_code, etc.) for local files - they only work for remote repository files
|
|
1335
1396
|
- Analyze the provided file content directly without attempting to fetch or read files using tools
|
|
1336
1397
|
- GitHub MCP tools are ONLY for remote repository operations, not local filesystem access
|
|
1337
|
-
- Use the file content shown in this message for your analysis`;
|
|
1398
|
+
- Use the file content shown in this message for your analysis${hasAudioFiles ? AUDIO_TRANSCRIPTION_INSTRUCTIONS : ""}`;
|
|
1338
1399
|
}
|
|
1339
1400
|
return systemPrompt;
|
|
1340
1401
|
}
|