@juspay/neurolink 12.28.1 → 12.28.3
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 -2
- package/README.md +65 -51
- package/dist/adapters/imageFormatSupport.d.ts +115 -5
- package/dist/adapters/imageFormatSupport.js +424 -12
- package/dist/browser/neurolink.min.js +367 -367
- package/dist/types/file.d.ts +39 -0
- package/dist/utils/messageBuilder.js +154 -16
- package/docs-site/static/search-index.json +16 -16
- package/package.json +3 -2
package/dist/types/file.d.ts
CHANGED
|
@@ -32,6 +32,45 @@ export type VisionImageConversion = {
|
|
|
32
32
|
/** True when the bytes were re-encoded; false when they were left alone. */
|
|
33
33
|
readonly converted: boolean;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Knobs for one vision-compatibility pass.
|
|
37
|
+
*
|
|
38
|
+
* `autoOrient` exists so a caller that also wants EXIF orientation applied
|
|
39
|
+
* gets it inside the same decode. Rotating first and transcoding second means
|
|
40
|
+
* two full decode/re-encode cycles, and for the lossy formats in both sets
|
|
41
|
+
* (TIFF, which sharp writes JPEG-compressed by default) the intermediate
|
|
42
|
+
* re-encode costs a generation of image quality that the model then reads.
|
|
43
|
+
*
|
|
44
|
+
* `outputFormat` picks the transcode target; see {@link VisionImageOutputFormat}
|
|
45
|
+
* for the default and why it stays PNG unless a caller opts into JPEG.
|
|
46
|
+
*/
|
|
47
|
+
export type VisionTranscodeOptions = {
|
|
48
|
+
readonly autoOrient?: boolean;
|
|
49
|
+
readonly outputFormat?: VisionImageOutputFormat;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* What a bounded header probe could establish about an image's EXIF
|
|
53
|
+
* orientation tag, without decoding the image or reading all of its bytes.
|
|
54
|
+
*
|
|
55
|
+
* `"inconclusive"` is not a failure — it is the honest answer whenever the
|
|
56
|
+
* prefix ran out before the parse reached a verdict, or the container is one
|
|
57
|
+
* this probe does not parse. Callers must treat it as "find out the expensive
|
|
58
|
+
* way", never as "no tag".
|
|
59
|
+
*/
|
|
60
|
+
export type ExifOrientationProbe = "absent" | "present" | "inconclusive";
|
|
61
|
+
/**
|
|
62
|
+
* Outcome of an EXIF-orientation pass over one image.
|
|
63
|
+
*
|
|
64
|
+
* See `adapters/imageFormatSupport.ts` — `normalized` is false both when the
|
|
65
|
+
* image carried no orientation tag (or an already-upright `1`) and when the
|
|
66
|
+
* pass could not run (sharp unavailable, decode failure), so callers must not
|
|
67
|
+
* treat it as a success flag; either way `buffer` is safe to send as-is.
|
|
68
|
+
*/
|
|
69
|
+
export type ImageOrientationNormalization = {
|
|
70
|
+
readonly buffer: Buffer;
|
|
71
|
+
/** True when the bytes were re-encoded to apply and strip EXIF orientation. */
|
|
72
|
+
readonly normalized: boolean;
|
|
73
|
+
};
|
|
35
74
|
/**
|
|
36
75
|
* Outcome of an audio-compatibility pass over one file.
|
|
37
76
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
-
import { readFile as readFileAsync, stat as statAsync } from "fs/promises";
|
|
2
|
+
import { open as openAsync, readFile as readFileAsync, stat as statAsync, } from "fs/promises";
|
|
3
3
|
import pLimit from "p-limit";
|
|
4
4
|
import { request } from "undici";
|
|
5
5
|
import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
|
|
@@ -10,7 +10,7 @@ import { PDF_LIMITS } from "../core/constants.js";
|
|
|
10
10
|
import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
|
|
11
11
|
import { isCSVContent, isTextContent, SIZE_TIER_THRESHOLDS, } from "../types/index.js";
|
|
12
12
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
13
|
-
import { needsVisionTranscode, toVisionCompatibleImage, } from "../adapters/imageFormatSupport.js";
|
|
13
|
+
import { EXIF_PROBE_PREFIX_BYTES, mayCarryExifOrientation, needsVisionTranscode, normalizeImageOrientation, probeExifOrientation, toVisionCompatibleImage, } from "../adapters/imageFormatSupport.js";
|
|
14
14
|
import { 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";
|
|
@@ -2007,12 +2007,17 @@ async function processImageToBase64(image, index, outputFormat) {
|
|
|
2007
2007
|
ImageProcessor.validateBufferSize(image, `image input at index ${index}`);
|
|
2008
2008
|
imageData = image.toString("base64");
|
|
2009
2009
|
}
|
|
2010
|
-
// Last line of defence for vision-format compatibility.
|
|
2011
|
-
// handles `input.images` eagerly so the
|
|
2012
|
-
// directly (Google AI Studio, Bedrock) see
|
|
2013
|
-
// arrive as URLs are downloaded
|
|
2014
|
-
// here. A no-op for
|
|
2015
|
-
|
|
2010
|
+
// Last line of defence for vision-format compatibility AND orientation.
|
|
2011
|
+
// `normalizeVisionImageFormats` handles `input.images` eagerly so the
|
|
2012
|
+
// providers that read that array directly (Google AI Studio, Bedrock) see
|
|
2013
|
+
// converted/oriented bytes, but images that arrive as URLs are downloaded
|
|
2014
|
+
// further downstream and only become bytes here. A no-op for PNG/GIF (which
|
|
2015
|
+
// cannot carry an orientation tag) already sent as-is, so the common
|
|
2016
|
+
// no-EXIF case is unaffected.
|
|
2017
|
+
const needsOrientationCheck = mayCarryExifOrientation(mimeType);
|
|
2018
|
+
const needsTranscode = needsVisionTranscode(mimeType);
|
|
2019
|
+
const context = `image input at index ${index}`;
|
|
2020
|
+
if (needsTranscode) {
|
|
2016
2021
|
// Guard the decoded bytes, not just the Buffer input. The buffer branch
|
|
2017
2022
|
// above is already checked, but a data: URI reaches here having only been
|
|
2018
2023
|
// regex-matched — so an oversized one was handed straight to sharp/ffmpeg,
|
|
@@ -2020,15 +2025,41 @@ async function processImageToBase64(image, index, outputFormat) {
|
|
|
2020
2025
|
//
|
|
2021
2026
|
// Sized before the decode rather than after: checking the Buffer would mean
|
|
2022
2027
|
// allocating the very thing the limit exists to refuse.
|
|
2023
|
-
const context = `image input at index ${index}`;
|
|
2024
2028
|
ImageProcessor.validateSize(base64DecodedByteLength(imageData), context);
|
|
2025
2029
|
const rawImage = Buffer.from(imageData, "base64");
|
|
2026
|
-
|
|
2030
|
+
// Both passes need the same decode, so a format in both sets (HEIC,
|
|
2031
|
+
// HEIF, TIFF, AVIF) gets one pipeline rather than two — see
|
|
2032
|
+
// `toVisionCompatibleImage`'s `autoOrient`.
|
|
2033
|
+
const compatible = await toVisionCompatibleImage(rawImage, mimeType, {
|
|
2034
|
+
autoOrient: needsOrientationCheck,
|
|
2035
|
+
outputFormat,
|
|
2036
|
+
});
|
|
2027
2037
|
if (compatible.converted) {
|
|
2028
2038
|
imageData = compatible.buffer.toString("base64");
|
|
2029
2039
|
mimeType = compatible.mimeType;
|
|
2030
2040
|
}
|
|
2031
2041
|
}
|
|
2042
|
+
else if (needsOrientationCheck) {
|
|
2043
|
+
// Ask a bounded header prefix whether this image needs touching at all
|
|
2044
|
+
// before paying for the size guard or a full decode. PNG/JPEG/WebP were
|
|
2045
|
+
// previously gated on `needsTranscode` alone — false for all three — so
|
|
2046
|
+
// this MIME type reached the provider with no local size check
|
|
2047
|
+
// whatsoever. A large image with no orientation tag is never going to be
|
|
2048
|
+
// modified by this pass, and must keep reaching the provider exactly as
|
|
2049
|
+
// it did before rather than being rejected locally for a limit that
|
|
2050
|
+
// exists to bound a decode nothing here needs.
|
|
2051
|
+
if (probeBase64ExifOrientation(imageData, mimeType) !== "absent") {
|
|
2052
|
+
ImageProcessor.validateSize(base64DecodedByteLength(imageData), context);
|
|
2053
|
+
const rawImage = Buffer.from(imageData, "base64");
|
|
2054
|
+
const oriented = await normalizeImageOrientation(rawImage, mimeType);
|
|
2055
|
+
// Re-encoding changes byte length. An oversized result falls back to
|
|
2056
|
+
// the already-validated original, matching normalizeVisionImageFormats.
|
|
2057
|
+
if (oriented.normalized &&
|
|
2058
|
+
withinConversionLimit(oriented.buffer, `oriented ${context}`)) {
|
|
2059
|
+
imageData = oriented.buffer.toString("base64");
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2032
2063
|
return { imageData, mimeType };
|
|
2033
2064
|
}
|
|
2034
2065
|
/**
|
|
@@ -2058,14 +2089,46 @@ export async function normalizeVisionImageFormats(input, outputFormat) {
|
|
|
2058
2089
|
? entry.data
|
|
2059
2090
|
: entry;
|
|
2060
2091
|
const source = await readImageSourceForConversion(payload);
|
|
2061
|
-
if (!source ||
|
|
2092
|
+
if (!source ||
|
|
2093
|
+
(!needsVisionTranscode(source.mimeType) &&
|
|
2094
|
+
!mayCarryExifOrientation(source.mimeType))) {
|
|
2062
2095
|
continue;
|
|
2063
2096
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2097
|
+
let buffer = source.buffer;
|
|
2098
|
+
let mimeType = source.mimeType;
|
|
2099
|
+
let changed = false;
|
|
2100
|
+
const orientable = mayCarryExifOrientation(mimeType);
|
|
2101
|
+
if (needsVisionTranscode(mimeType)) {
|
|
2102
|
+
// A format in both sets (HEIC, HEIF, TIFF, AVIF) is oriented inside the
|
|
2103
|
+
// transcode's own decode instead of ahead of it. Two sequential passes
|
|
2104
|
+
// cost two decodes and two encodes, and the intermediate re-encode is
|
|
2105
|
+
// lossy for some of these formats — so the model would read an image a
|
|
2106
|
+
// generation worse than the one it could have been sent.
|
|
2107
|
+
const compatible = await toVisionCompatibleImage(buffer, mimeType, {
|
|
2108
|
+
autoOrient: orientable,
|
|
2109
|
+
outputFormat,
|
|
2110
|
+
});
|
|
2111
|
+
if (compatible.converted) {
|
|
2112
|
+
buffer = compatible.buffer;
|
|
2113
|
+
mimeType = compatible.mimeType;
|
|
2114
|
+
changed = true;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
else if (orientable) {
|
|
2118
|
+
const oriented = await normalizeImageOrientation(buffer, mimeType);
|
|
2119
|
+
// Re-encoding changes byte length, so the size guard that gated the
|
|
2120
|
+
// read above must run again on the result — a re-encode that grew past
|
|
2121
|
+
// the limit must not silently replace an under-limit original.
|
|
2122
|
+
if (oriented.normalized &&
|
|
2123
|
+
withinConversionLimit(oriented.buffer, `oriented image at index ${index}`)) {
|
|
2124
|
+
buffer = oriented.buffer;
|
|
2125
|
+
changed = true;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (!changed) {
|
|
2066
2129
|
continue;
|
|
2067
2130
|
}
|
|
2068
|
-
const dataUri = `data:${
|
|
2131
|
+
const dataUri = `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
2069
2132
|
images[index] = isWrapped
|
|
2070
2133
|
? { ...entry, data: dataUri }
|
|
2071
2134
|
: dataUri;
|
|
@@ -2112,6 +2175,34 @@ function base64DecodedByteLength(base64) {
|
|
|
2112
2175
|
}
|
|
2113
2176
|
return Math.max(0, Math.floor(significant / 4) * 3 - padding);
|
|
2114
2177
|
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Base64 characters sufficient to decode at least
|
|
2180
|
+
* {@link EXIF_PROBE_PREFIX_BYTES} bytes, rounded up to a 4-character group so
|
|
2181
|
+
* the slice below always starts at the beginning of the encoded stream.
|
|
2182
|
+
*/
|
|
2183
|
+
const EXIF_PROBE_BASE64_CHARS = Math.ceil(EXIF_PROBE_PREFIX_BYTES / 3) * 4;
|
|
2184
|
+
/**
|
|
2185
|
+
* `probeExifOrientation`, but for a still-encoded data-URI payload: decodes
|
|
2186
|
+
* only its leading {@link EXIF_PROBE_BASE64_CHARS} characters rather than the
|
|
2187
|
+
* whole string, the same bounded-prefix trade `probeFileExifOrientation`
|
|
2188
|
+
* makes for a local file. This is what lets `processImageToBase64` ask "does
|
|
2189
|
+
* this image need touching at all?" before it pays for the size guard or a
|
|
2190
|
+
* full decode — see the call site for why that ordering is the point.
|
|
2191
|
+
*
|
|
2192
|
+
* Reports `"inconclusive"` on any decode failure rather than propagating it:
|
|
2193
|
+
* the caller's full-size path handles that verdict correctly already.
|
|
2194
|
+
*/
|
|
2195
|
+
function probeBase64ExifOrientation(base64, mimeType) {
|
|
2196
|
+
try {
|
|
2197
|
+
const window = base64.length <= EXIF_PROBE_BASE64_CHARS
|
|
2198
|
+
? base64
|
|
2199
|
+
: base64.slice(0, EXIF_PROBE_BASE64_CHARS);
|
|
2200
|
+
return probeExifOrientation(Buffer.from(window, "base64"), mimeType);
|
|
2201
|
+
}
|
|
2202
|
+
catch {
|
|
2203
|
+
return "inconclusive";
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2115
2206
|
/**
|
|
2116
2207
|
* Whether a payload is small enough to hand to an image decoder, reported
|
|
2117
2208
|
* rather than thrown. See {@link readImageSourceForConversion} for why this
|
|
@@ -2131,6 +2222,42 @@ function withinConversionByteLimit(bytes, context) {
|
|
|
2131
2222
|
function withinConversionLimit(buffer, context) {
|
|
2132
2223
|
return withinConversionByteLimit(buffer.length, context);
|
|
2133
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Ask a file's own header bytes whether it carries an EXIF orientation,
|
|
2227
|
+
* reading at most {@link EXIF_PROBE_PREFIX_BYTES} to find out.
|
|
2228
|
+
*
|
|
2229
|
+
* This is what keeps the orientation pass off the JPEG hot path. Without it,
|
|
2230
|
+
* widening the conversion gate to cover orientation means every local `.jpg`
|
|
2231
|
+
* and `.webp` is stat'd, read into memory in full and handed to a decoder,
|
|
2232
|
+
* purely to discover that the overwhelming majority carry no tag — new I/O
|
|
2233
|
+
* on the single most common image input there is.
|
|
2234
|
+
*
|
|
2235
|
+
* Reports `"inconclusive"` on any read failure rather than propagating it:
|
|
2236
|
+
* the full read that follows will hit the same problem a few lines later,
|
|
2237
|
+
* where the existing handler already turns it into a warning and a skipped
|
|
2238
|
+
* conversion. Failing here instead would only duplicate that.
|
|
2239
|
+
*/
|
|
2240
|
+
async function probeFileExifOrientation(path, mimeType, size) {
|
|
2241
|
+
// The caller has already stat'd, so a small file costs a small buffer
|
|
2242
|
+
// rather than the window's full width.
|
|
2243
|
+
const window = Math.min(Math.max(size, 0), EXIF_PROBE_PREFIX_BYTES);
|
|
2244
|
+
if (window === 0) {
|
|
2245
|
+
return "inconclusive";
|
|
2246
|
+
}
|
|
2247
|
+
let handle;
|
|
2248
|
+
try {
|
|
2249
|
+
handle = await openAsync(path, "r");
|
|
2250
|
+
const prefix = Buffer.alloc(window);
|
|
2251
|
+
const { bytesRead } = await handle.read(prefix, 0, prefix.length, 0);
|
|
2252
|
+
return probeExifOrientation(prefix.subarray(0, bytesRead), mimeType);
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
return "inconclusive";
|
|
2256
|
+
}
|
|
2257
|
+
finally {
|
|
2258
|
+
await handle?.close().catch(() => undefined);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2134
2261
|
async function readImageSourceForConversion(payload) {
|
|
2135
2262
|
// Both in-memory shapes are size-checked before they can reach a decoder,
|
|
2136
2263
|
// the same way the file-path branch below is. `withinConversionLimit`
|
|
@@ -2162,16 +2289,27 @@ async function readImageSourceForConversion(payload) {
|
|
|
2162
2289
|
return undefined;
|
|
2163
2290
|
}
|
|
2164
2291
|
const mimeType = getMimeTypeFromExtension(payload);
|
|
2165
|
-
|
|
2292
|
+
const transcodable = needsVisionTranscode(mimeType);
|
|
2293
|
+
if (!transcodable && !mayCarryExifOrientation(mimeType)) {
|
|
2166
2294
|
return undefined;
|
|
2167
2295
|
}
|
|
2168
2296
|
try {
|
|
2169
2297
|
// Preflight the size before reading. Conversion replaces the entry with a
|
|
2170
2298
|
// data URI, which bypasses processImageToBase64's buffer-size guard — so
|
|
2171
2299
|
// without this a large local HEIC/TIFF/BMP was read fully into memory and
|
|
2172
|
-
// then re-encoded, with no limit applied at either step.
|
|
2300
|
+
// then re-encoded, with no limit applied at either step. The same applies
|
|
2301
|
+
// to a local JPEG read only for its EXIF tag: still bounded before the read.
|
|
2173
2302
|
const { size } = await statAsync(payload);
|
|
2174
2303
|
ImageProcessor.validateSize(size, `image at ${safeBasename(payload)}`);
|
|
2304
|
+
// A file whose only reason to be here is the orientation question gets
|
|
2305
|
+
// that question answered from its header, and is not read at all when the
|
|
2306
|
+
// answer is no. A transcodable format is exempt: its bytes are needed
|
|
2307
|
+
// whatever the orientation turns out to be, so a probe would be pure
|
|
2308
|
+
// overhead.
|
|
2309
|
+
if (!transcodable &&
|
|
2310
|
+
(await probeFileExifOrientation(payload, mimeType, size)) === "absent") {
|
|
2311
|
+
return undefined;
|
|
2312
|
+
}
|
|
2175
2313
|
const buffer = await readFileAsync(payload);
|
|
2176
2314
|
ImageProcessor.validateBufferSize(buffer, `image at ${safeBasename(payload)}`);
|
|
2177
2315
|
return { buffer, mimeType };
|