@juspay/neurolink 10.10.11 → 10.11.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/CHANGELOG.md +12 -0
- package/dist/adapters/imageFormatSupport.d.ts +75 -0
- package/dist/adapters/imageFormatSupport.js +283 -0
- package/dist/adapters/video/ffmpegAdapter.d.ts +6 -0
- package/dist/adapters/video/ffmpegAdapter.js +1 -1
- package/dist/browser/neurolink.min.js +399 -398
- package/dist/lib/adapters/imageFormatSupport.d.ts +75 -0
- package/dist/lib/adapters/imageFormatSupport.js +284 -0
- package/dist/lib/adapters/video/ffmpegAdapter.d.ts +6 -0
- package/dist/lib/adapters/video/ffmpegAdapter.js +1 -1
- package/dist/lib/processors/config/fileExtensions.d.ts +32 -15
- package/dist/lib/processors/config/fileExtensions.js +27 -66
- package/dist/lib/processors/config/fileTypeRegistry.d.ts +106 -0
- package/dist/lib/processors/config/fileTypeRegistry.js +702 -0
- package/dist/lib/processors/config/index.d.ts +2 -1
- package/dist/lib/processors/config/index.js +5 -1
- package/dist/lib/processors/config/mimeConstants.d.ts +22 -7
- package/dist/lib/processors/config/mimeConstants.js +45 -66
- package/dist/lib/processors/media/AudioProcessor.js +16 -38
- package/dist/lib/processors/media/VideoProcessor.js +11 -32
- package/dist/lib/providers/googleAiStudio/client.js +12 -1
- package/dist/lib/providers/googleVertex/client.js +123 -66
- package/dist/lib/types/file.d.ts +41 -0
- package/dist/lib/utils/fileDetector.js +367 -251
- package/dist/lib/utils/imageProcessor.js +14 -17
- package/dist/lib/utils/markupSniff.d.ts +37 -0
- package/dist/lib/utils/markupSniff.js +125 -0
- package/dist/lib/utils/messageBuilder.d.ts +14 -0
- package/dist/lib/utils/messageBuilder.js +287 -74
- package/dist/processors/config/fileExtensions.d.ts +32 -15
- package/dist/processors/config/fileExtensions.js +27 -66
- package/dist/processors/config/fileTypeRegistry.d.ts +106 -0
- package/dist/processors/config/fileTypeRegistry.js +701 -0
- package/dist/processors/config/index.d.ts +2 -1
- package/dist/processors/config/index.js +5 -1
- package/dist/processors/config/mimeConstants.d.ts +22 -7
- package/dist/processors/config/mimeConstants.js +45 -66
- package/dist/processors/media/AudioProcessor.js +16 -38
- package/dist/processors/media/VideoProcessor.js +11 -32
- package/dist/providers/googleAiStudio/client.js +12 -1
- package/dist/providers/googleVertex/client.js +123 -66
- package/dist/types/file.d.ts +41 -0
- package/dist/utils/fileDetector.js +367 -251
- package/dist/utils/imageProcessor.js +14 -17
- package/dist/utils/markupSniff.d.ts +37 -0
- package/dist/utils/markupSniff.js +124 -0
- package/dist/utils/messageBuilder.d.ts +14 -0
- package/dist/utils/messageBuilder.js +287 -74
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [10.11.0](https://github.com/juspay/neurolink/compare/v10.10.12...v10.11.0) (2026-08-10)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **(files):** support every multimodal format exhaustively via one registry ([a588cca](https://github.com/juspay/neurolink/commit/a588cca4ac89dbcdc2fb1aa0ec256794dbcee3f6))
|
|
6
|
+
|
|
7
|
+
## [10.10.12](https://github.com/juspay/neurolink/compare/v10.10.11...v10.10.12) (2026-08-10)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **(multimodal):** stop dropping images above the lazy-reference threshold ([85de8f3](https://github.com/juspay/neurolink/commit/85de8f352b8a6f4fa4953042247a1eebaedf1640))
|
|
12
|
+
|
|
1
13
|
## [10.10.11](https://github.com/juspay/neurolink/compare/v10.10.10...v10.10.11) (2026-08-08)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vision-provider image format compatibility.
|
|
3
|
+
*
|
|
4
|
+
* NeuroLink identifies far more image formats than any vision API accepts. BMP,
|
|
5
|
+
* TIFF, AVIF, ICO and JPEG 2000 are accepted by none of them; HEIC and HEIF are
|
|
6
|
+
* accepted only by Google. Until this module existed those formats were
|
|
7
|
+
* detected correctly, labelled correctly, and then forwarded verbatim — the
|
|
8
|
+
* request reached the provider and came back as an opaque HTTP 400, which is
|
|
9
|
+
* the least useful outcome available: the file was clearly an image, NeuroLink
|
|
10
|
+
* knew exactly which kind, and still nothing worked.
|
|
11
|
+
*
|
|
12
|
+
* A phone photo is the common case. iOS writes HEIC by default, so "attach a
|
|
13
|
+
* photo and ask what is in it" failed for every provider except Google.
|
|
14
|
+
*
|
|
15
|
+
* Anything outside the universal set is transcoded to PNG. PNG rather than JPEG
|
|
16
|
+
* because the sources are frequently lossless (TIFF, BMP, ICO) or already
|
|
17
|
+
* carry alpha, and a lossy re-encode of an image the model is about to read
|
|
18
|
+
* closely is the wrong default.
|
|
19
|
+
*
|
|
20
|
+
* @module adapters/imageFormatSupport
|
|
21
|
+
*/
|
|
22
|
+
import type { ImageWithAltText, VisionImageConversion } from "../types/index.js";
|
|
23
|
+
/**
|
|
24
|
+
* MIME types every vision-capable provider accepts as-is.
|
|
25
|
+
*
|
|
26
|
+
* This is the intersection across OpenAI, Anthropic, Google (AI Studio and
|
|
27
|
+
* Vertex), Bedrock, Azure and Mistral — deliberately the intersection and not
|
|
28
|
+
* a per-provider matrix. Google additionally accepts HEIC/HEIF natively, but
|
|
29
|
+
* converting those for Google as well costs one transcode and removes an
|
|
30
|
+
* entire axis of provider-specific branching from the dispatch path.
|
|
31
|
+
*/
|
|
32
|
+
export declare const UNIVERSAL_VISION_IMAGE_MIME_TYPES: ReadonlySet<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Every image MIME type NeuroLink accepts as *input*.
|
|
35
|
+
*
|
|
36
|
+
* The union of what providers take as-is and what this module can convert for
|
|
37
|
+
* them. Intake validation must use this rather than the universal set alone:
|
|
38
|
+
* a format we can transcode is a format we accept, and gating intake on the
|
|
39
|
+
* provider-acceptable list rejects the file before conversion ever runs.
|
|
40
|
+
*/
|
|
41
|
+
export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: ReadonlySet<string>;
|
|
42
|
+
/**
|
|
43
|
+
* True when a MIME type needs transcoding before it can be sent to a vision
|
|
44
|
+
* provider. Cheap enough to call on every image; callers use it to avoid
|
|
45
|
+
* reading a file off disk that would not have been converted anyway.
|
|
46
|
+
*/
|
|
47
|
+
export declare function needsVisionTranscode(mimeType: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Return image bytes every vision provider can read, transcoding to PNG when
|
|
50
|
+
* the source format is one no provider accepts.
|
|
51
|
+
*
|
|
52
|
+
* Never throws for image reasons. When neither backend can decode the input,
|
|
53
|
+
* the original bytes are returned with a warning naming the format — the
|
|
54
|
+
* request then fails at the provider exactly as it did before, rather than this
|
|
55
|
+
* compatibility step becoming a new way for a previously working request to
|
|
56
|
+
* break.
|
|
57
|
+
*
|
|
58
|
+
* @param buffer - Raw image bytes.
|
|
59
|
+
* @param mimeType - Detected MIME type of `buffer`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function toVisionCompatibleImage(buffer: Buffer, mimeType: string): Promise<VisionImageConversion>;
|
|
62
|
+
/**
|
|
63
|
+
* Unwrap an `input.images` entry to its payload.
|
|
64
|
+
*
|
|
65
|
+
* `ImageWithAltText` (`{ data, altText }`) is a documented public input shape,
|
|
66
|
+
* but the provider image loops typed the array as `Buffer | string` and so
|
|
67
|
+
* treated a wrapper as raw bytes — `toString("base64")` on the object yields
|
|
68
|
+
* the literal "[object Object]", which is valid base64 that decodes to seven
|
|
69
|
+
* bytes of garbage. The request therefore reached the API and failed as
|
|
70
|
+
* "invalid image data", with nothing pointing at the stringification.
|
|
71
|
+
*
|
|
72
|
+
* Alt text has no representation in Vertex's `inlineData` part, so it is
|
|
73
|
+
* dropped here deliberately rather than corrupting the payload to carry it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function unwrapImagePayload(entry: Buffer | string | ImageWithAltText): Buffer | string;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vision-provider image format compatibility.
|
|
3
|
+
*
|
|
4
|
+
* NeuroLink identifies far more image formats than any vision API accepts. BMP,
|
|
5
|
+
* TIFF, AVIF, ICO and JPEG 2000 are accepted by none of them; HEIC and HEIF are
|
|
6
|
+
* accepted only by Google. Until this module existed those formats were
|
|
7
|
+
* detected correctly, labelled correctly, and then forwarded verbatim — the
|
|
8
|
+
* request reached the provider and came back as an opaque HTTP 400, which is
|
|
9
|
+
* the least useful outcome available: the file was clearly an image, NeuroLink
|
|
10
|
+
* knew exactly which kind, and still nothing worked.
|
|
11
|
+
*
|
|
12
|
+
* A phone photo is the common case. iOS writes HEIC by default, so "attach a
|
|
13
|
+
* photo and ask what is in it" failed for every provider except Google.
|
|
14
|
+
*
|
|
15
|
+
* Anything outside the universal set is transcoded to PNG. PNG rather than JPEG
|
|
16
|
+
* because the sources are frequently lossless (TIFF, BMP, ICO) or already
|
|
17
|
+
* carry alpha, and a lossy re-encode of an image the model is about to read
|
|
18
|
+
* closely is the wrong default.
|
|
19
|
+
*
|
|
20
|
+
* @module adapters/imageFormatSupport
|
|
21
|
+
*/
|
|
22
|
+
import { extensionForMimeType } from "../processors/config/fileTypeRegistry.js";
|
|
23
|
+
import { withTimeout } from "../utils/errorHandling.js";
|
|
24
|
+
import { logger } from "../utils/logger.js";
|
|
25
|
+
import { tryImport } from "../utils/tryImport.js";
|
|
26
|
+
import { getFfmpegPath, runFfmpeg } from "./video/ffmpegAdapter.js";
|
|
27
|
+
/**
|
|
28
|
+
* Per-backend ceiling for one image conversion.
|
|
29
|
+
*
|
|
30
|
+
* Generous enough for a large TIFF or a HEIC burst frame on a loaded machine,
|
|
31
|
+
* short enough that a hung decoder cannot hold a generation request open.
|
|
32
|
+
*/
|
|
33
|
+
const IMAGE_TRANSCODE_TIMEOUT_MS = 30_000;
|
|
34
|
+
/**
|
|
35
|
+
* MIME types every vision-capable provider accepts as-is.
|
|
36
|
+
*
|
|
37
|
+
* This is the intersection across OpenAI, Anthropic, Google (AI Studio and
|
|
38
|
+
* Vertex), Bedrock, Azure and Mistral — deliberately the intersection and not
|
|
39
|
+
* a per-provider matrix. Google additionally accepts HEIC/HEIF natively, but
|
|
40
|
+
* converting those for Google as well costs one transcode and removes an
|
|
41
|
+
* entire axis of provider-specific branching from the dispatch path.
|
|
42
|
+
*/
|
|
43
|
+
export const UNIVERSAL_VISION_IMAGE_MIME_TYPES = new Set([
|
|
44
|
+
"image/png",
|
|
45
|
+
"image/jpeg",
|
|
46
|
+
"image/gif",
|
|
47
|
+
"image/webp",
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Formats that are transcoded to PNG before dispatch.
|
|
51
|
+
*
|
|
52
|
+
* An explicit allowlist rather than "anything not universal": a MIME type this
|
|
53
|
+
* module does not recognise is more likely a mislabelled file than a format
|
|
54
|
+
* sharp can decode, and passing it through unchanged preserves the provider's
|
|
55
|
+
* own error message instead of replacing it with a decode failure here.
|
|
56
|
+
*/
|
|
57
|
+
const TRANSCODABLE_IMAGE_MIME_TYPES = new Set([
|
|
58
|
+
"image/bmp",
|
|
59
|
+
"image/x-ms-bmp",
|
|
60
|
+
"image/tiff",
|
|
61
|
+
"image/x-tiff",
|
|
62
|
+
"image/avif",
|
|
63
|
+
"image/heic",
|
|
64
|
+
"image/heic-sequence",
|
|
65
|
+
"image/heif",
|
|
66
|
+
"image/heif-sequence",
|
|
67
|
+
"image/x-icon",
|
|
68
|
+
"image/vnd.microsoft.icon",
|
|
69
|
+
"image/jp2",
|
|
70
|
+
"image/jpx",
|
|
71
|
+
"image/apng",
|
|
72
|
+
// SVG reaches this module only when it arrives as raw bytes in `input.images`
|
|
73
|
+
// rather than through detection (which routes .svg to the sanitizer). No
|
|
74
|
+
// vision provider accepts image/svg+xml, and sharp rasterises SVG natively,
|
|
75
|
+
// so converting is strictly better than shipping markup labelled as an image.
|
|
76
|
+
"image/svg+xml",
|
|
77
|
+
]);
|
|
78
|
+
/**
|
|
79
|
+
* Every image MIME type NeuroLink accepts as *input*.
|
|
80
|
+
*
|
|
81
|
+
* The union of what providers take as-is and what this module can convert for
|
|
82
|
+
* them. Intake validation must use this rather than the universal set alone:
|
|
83
|
+
* a format we can transcode is a format we accept, and gating intake on the
|
|
84
|
+
* provider-acceptable list rejects the file before conversion ever runs.
|
|
85
|
+
*/
|
|
86
|
+
export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = new Set([
|
|
87
|
+
...UNIVERSAL_VISION_IMAGE_MIME_TYPES,
|
|
88
|
+
...TRANSCODABLE_IMAGE_MIME_TYPES,
|
|
89
|
+
]);
|
|
90
|
+
/**
|
|
91
|
+
* True when a MIME type needs transcoding before it can be sent to a vision
|
|
92
|
+
* provider. Cheap enough to call on every image; callers use it to avoid
|
|
93
|
+
* reading a file off disk that would not have been converted anyway.
|
|
94
|
+
*/
|
|
95
|
+
export function needsVisionTranscode(mimeType) {
|
|
96
|
+
const normalized = mimeType.split(";")[0].trim().toLowerCase();
|
|
97
|
+
return (!UNIVERSAL_VISION_IMAGE_MIME_TYPES.has(normalized) &&
|
|
98
|
+
TRANSCODABLE_IMAGE_MIME_TYPES.has(normalized));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Decode with sharp. Covers TIFF, AVIF, GIF and SVG in-process with no temp
|
|
102
|
+
* files, which is the fast path.
|
|
103
|
+
*
|
|
104
|
+
* Deliberately does NOT cover every transcodable format: sharp's prebuilt
|
|
105
|
+
* binaries report `heif` as an input format but that is AV1-in-HEIF (AVIF)
|
|
106
|
+
* only — actual HEVC-coded HEIC fails inside libheif, and BMP, ICO and
|
|
107
|
+
* JPEG 2000 are not compiled in at all. Those fall through to ffmpeg below.
|
|
108
|
+
*/
|
|
109
|
+
async function transcodeWithSharp(buffer) {
|
|
110
|
+
const sharpModule = await tryImport("sharp", "Image format conversion for vision providers");
|
|
111
|
+
// A shape guard, not a crash guard: an absent or malformed sharp already
|
|
112
|
+
// fails safely, because `tryImport` throws a named install error and any
|
|
113
|
+
// TypeError from calling a non-function is caught by the backend loop, which
|
|
114
|
+
// then tries ffmpeg. What this adds is a legible reason in that loop's
|
|
115
|
+
// failure list instead of "sharpModule.default is not a function".
|
|
116
|
+
//
|
|
117
|
+
// It deliberately throws rather than returning `buffer`: returning the input
|
|
118
|
+
// would report a successful conversion and relabel the original bytes as PNG,
|
|
119
|
+
// and would also skip the ffmpeg backend — the one that actually handles
|
|
120
|
+
// HEIC, BMP, ICO and JPEG 2000.
|
|
121
|
+
if (typeof sharpModule?.default !== "function") {
|
|
122
|
+
throw new Error("the installed sharp package does not expose a callable default export");
|
|
123
|
+
}
|
|
124
|
+
// `pages: 1` keeps a multi-frame source (animated AVIF, a .heics sequence,
|
|
125
|
+
// a multi-page TIFF) from being flattened into one tall strip — the first
|
|
126
|
+
// frame is what a vision model should receive.
|
|
127
|
+
const pipeline = sharpModule.default(buffer, { pages: 1 });
|
|
128
|
+
// The instance shape is checked as well as the factory: a build that exports
|
|
129
|
+
// a callable but returns something without `.png()` would otherwise fail as
|
|
130
|
+
// an opaque TypeError inside the backend loop.
|
|
131
|
+
if (typeof pipeline?.png !== "function") {
|
|
132
|
+
throw new Error("the installed sharp package returned a pipeline without a png() encoder");
|
|
133
|
+
}
|
|
134
|
+
return pipeline.png().toBuffer();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Decode with ffmpeg, which handles what sharp cannot — most importantly HEIC,
|
|
138
|
+
* the format iPhones write by default and therefore the single most common
|
|
139
|
+
* "why can't the model see my photo" case.
|
|
140
|
+
*
|
|
141
|
+
* ffmpeg is already a soft dependency for video keyframe extraction and is
|
|
142
|
+
* resolved through the same `FFMPEG_PATH` → `ffmpeg-static` → system-PATH
|
|
143
|
+
* chain, so this adds a code path rather than a new requirement.
|
|
144
|
+
*
|
|
145
|
+
* Requires temp files: ffmpeg's image demuxers seek, so piping through stdin is
|
|
146
|
+
* not reliable for these formats. The temp directory is removed in `finally`
|
|
147
|
+
* whether or not the conversion succeeded.
|
|
148
|
+
*
|
|
149
|
+
* The Node builtins are imported dynamically rather than at module scope
|
|
150
|
+
* because the browser bundle stubs `node:fs/promises` and its stub has no
|
|
151
|
+
* `mkdtemp`. This whole path is server-only — nothing in a browser is going to
|
|
152
|
+
* spawn ffmpeg — so the import belongs where it is used.
|
|
153
|
+
*/
|
|
154
|
+
async function transcodeWithFfmpeg(buffer, extension, binaryPath) {
|
|
155
|
+
const [{ randomUUID }, { mkdtemp, readFile, rm, writeFile }, { tmpdir }, { join },] = await Promise.all([
|
|
156
|
+
import("node:crypto"),
|
|
157
|
+
import("node:fs/promises"),
|
|
158
|
+
import("node:os"),
|
|
159
|
+
import("node:path"),
|
|
160
|
+
]);
|
|
161
|
+
const workDir = await mkdtemp(join(tmpdir(), "neurolink-img-"));
|
|
162
|
+
const inputPath = join(workDir, `${randomUUID()}${extension}`);
|
|
163
|
+
const outputPath = join(workDir, `${randomUUID()}.png`);
|
|
164
|
+
try {
|
|
165
|
+
await writeFile(inputPath, buffer);
|
|
166
|
+
await runFfmpeg([
|
|
167
|
+
"-y",
|
|
168
|
+
"-v",
|
|
169
|
+
"error",
|
|
170
|
+
"-i",
|
|
171
|
+
inputPath,
|
|
172
|
+
// Take a single frame so a multi-image container yields one PNG rather
|
|
173
|
+
// than ffmpeg erroring on a missing output-sequence pattern.
|
|
174
|
+
"-frames:v",
|
|
175
|
+
"1",
|
|
176
|
+
"-f",
|
|
177
|
+
"image2",
|
|
178
|
+
"-c:v",
|
|
179
|
+
"png",
|
|
180
|
+
outputPath,
|
|
181
|
+
], binaryPath ? { binaryPath } : {});
|
|
182
|
+
return await readFile(outputPath);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The decode backends to try, in order, cheapest first.
|
|
190
|
+
*
|
|
191
|
+
* The two ffmpeg entries are not redundant. `getFfmpegPath()` prefers the
|
|
192
|
+
* `ffmpeg-static` package, whose LGPL build omits HEVC and therefore cannot
|
|
193
|
+
* read HEIC — the format iPhones write by default. A system ffmpeg usually can,
|
|
194
|
+
* so when the resolved binary is not already the system one it is retried
|
|
195
|
+
* explicitly rather than reporting a photo as unsupported.
|
|
196
|
+
*/
|
|
197
|
+
async function* transcodeBackends(buffer, extension) {
|
|
198
|
+
yield { name: "sharp", run: () => transcodeWithSharp(buffer) };
|
|
199
|
+
const resolved = await getFfmpegPath().catch(() => "ffmpeg");
|
|
200
|
+
yield {
|
|
201
|
+
name: "ffmpeg",
|
|
202
|
+
run: () => transcodeWithFfmpeg(buffer, extension),
|
|
203
|
+
};
|
|
204
|
+
if (resolved !== "ffmpeg") {
|
|
205
|
+
yield {
|
|
206
|
+
name: "system ffmpeg",
|
|
207
|
+
run: () => transcodeWithFfmpeg(buffer, extension, "ffmpeg"),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Return image bytes every vision provider can read, transcoding to PNG when
|
|
213
|
+
* the source format is one no provider accepts.
|
|
214
|
+
*
|
|
215
|
+
* Never throws for image reasons. When neither backend can decode the input,
|
|
216
|
+
* the original bytes are returned with a warning naming the format — the
|
|
217
|
+
* request then fails at the provider exactly as it did before, rather than this
|
|
218
|
+
* compatibility step becoming a new way for a previously working request to
|
|
219
|
+
* break.
|
|
220
|
+
*
|
|
221
|
+
* @param buffer - Raw image bytes.
|
|
222
|
+
* @param mimeType - Detected MIME type of `buffer`.
|
|
223
|
+
*/
|
|
224
|
+
export async function toVisionCompatibleImage(buffer, mimeType) {
|
|
225
|
+
if (!needsVisionTranscode(mimeType)) {
|
|
226
|
+
return { buffer, mimeType, converted: false };
|
|
227
|
+
}
|
|
228
|
+
const normalized = mimeType.split(";")[0].trim().toLowerCase();
|
|
229
|
+
const extension = extensionForMimeType(normalized) ?? ".bin";
|
|
230
|
+
const failures = [];
|
|
231
|
+
for await (const backend of transcodeBackends(buffer, extension)) {
|
|
232
|
+
try {
|
|
233
|
+
// Both backends can stall — sharp on a malformed stream, ffmpeg on a
|
|
234
|
+
// container it half-understands — and this runs inline on the request
|
|
235
|
+
// path. A bounded failure falls through to the next backend and finally
|
|
236
|
+
// to pass-through, which is the same degradation as a decode error.
|
|
237
|
+
const converted = await withTimeout(backend.run(), IMAGE_TRANSCODE_TIMEOUT_MS, new Error(`${backend.name} image transcode exceeded ${IMAGE_TRANSCODE_TIMEOUT_MS}ms`));
|
|
238
|
+
if (converted.length === 0) {
|
|
239
|
+
throw new Error("produced an empty image");
|
|
240
|
+
}
|
|
241
|
+
logger.debug(`[imageFormatSupport] Transcoded ${normalized} → image/png via ${backend.name} ` +
|
|
242
|
+
`(${buffer.length} → ${converted.length} bytes) for vision compatibility`);
|
|
243
|
+
return { buffer: converted, mimeType: "image/png", converted: true };
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
failures.push(`${backend.name}: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
logger.warn(`[imageFormatSupport] Could not transcode ${normalized} to PNG — sending the ` +
|
|
250
|
+
`original bytes, which most vision providers will reject. Install a full ` +
|
|
251
|
+
`ffmpeg build (or set FFMPEG_PATH to one) to enable this format. ` +
|
|
252
|
+
`Tried ${failures.join("; ")}`);
|
|
253
|
+
return { buffer, mimeType, converted: false };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Whether an `input.images` entry is the `{ data, altText }` wrapper.
|
|
257
|
+
*
|
|
258
|
+
* Narrowing on the `data` property rather than on `typeof entry === "object"`:
|
|
259
|
+
* a Buffer is also an object, so the looser test leaves `ImageWithAltText` in
|
|
260
|
+
* the union on the false branch and only compiles behind a cast.
|
|
261
|
+
*/
|
|
262
|
+
function isImageWithAltTextEntry(entry) {
|
|
263
|
+
return (typeof entry === "object" &&
|
|
264
|
+
entry !== null &&
|
|
265
|
+
!Buffer.isBuffer(entry) &&
|
|
266
|
+
"data" in entry);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Unwrap an `input.images` entry to its payload.
|
|
270
|
+
*
|
|
271
|
+
* `ImageWithAltText` (`{ data, altText }`) is a documented public input shape,
|
|
272
|
+
* but the provider image loops typed the array as `Buffer | string` and so
|
|
273
|
+
* treated a wrapper as raw bytes — `toString("base64")` on the object yields
|
|
274
|
+
* the literal "[object Object]", which is valid base64 that decodes to seven
|
|
275
|
+
* bytes of garbage. The request therefore reached the API and failed as
|
|
276
|
+
* "invalid image data", with nothing pointing at the stringification.
|
|
277
|
+
*
|
|
278
|
+
* Alt text has no representation in Vertex's `inlineData` part, so it is
|
|
279
|
+
* dropped here deliberately rather than corrupting the payload to carry it.
|
|
280
|
+
*/
|
|
281
|
+
export function unwrapImagePayload(entry) {
|
|
282
|
+
return isImageWithAltTextEntry(entry) ? entry.data : entry;
|
|
283
|
+
}
|
|
@@ -62,6 +62,12 @@ export declare function getFfmpegPath(): Promise<string>;
|
|
|
62
62
|
export declare function runFfmpeg(args: string[], options?: {
|
|
63
63
|
timeoutMs?: number;
|
|
64
64
|
maxBuffer?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Override the resolved binary. Used to retry against the system `ffmpeg`
|
|
67
|
+
* when the `ffmpeg-static` build lacks a codec — its LGPL build ships
|
|
68
|
+
* without HEVC, so it cannot decode HEIC even though a system build can.
|
|
69
|
+
*/
|
|
70
|
+
binaryPath?: string;
|
|
65
71
|
}): Promise<{
|
|
66
72
|
stdout: string;
|
|
67
73
|
stderr: string;
|
|
@@ -167,7 +167,7 @@ export async function getFfmpegPath() {
|
|
|
167
167
|
*/
|
|
168
168
|
export async function runFfmpeg(args, options = {}) {
|
|
169
169
|
const { execFile } = await import("node:child_process");
|
|
170
|
-
const ffmpegPath = await getFfmpegPath();
|
|
170
|
+
const ffmpegPath = options.binaryPath ?? (await getFfmpegPath());
|
|
171
171
|
const timeoutMs = options.timeoutMs ?? FFMPEG_FRAME_TIMEOUT_MS;
|
|
172
172
|
const maxBuffer = options.maxBuffer ?? FFMPEG_FRAME_MAX_BUFFER;
|
|
173
173
|
return new Promise((resolve, reject) => {
|