UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

189 lines (188 loc) • 10.5 kB
/** * Vision-provider image format compatibility. * * NeuroLink identifies far more image formats than any vision API accepts. BMP, * TIFF, AVIF, ICO and JPEG 2000 are accepted by none of them; HEIC and HEIF are * accepted only by Google. Until this module existed those formats were * detected correctly, labelled correctly, and then forwarded verbatim — the * request reached the provider and came back as an opaque HTTP 400, which is * the least useful outcome available: the file was clearly an image, NeuroLink * knew exactly which kind, and still nothing worked. * * A phone photo is the common case. iOS writes HEIC by default, so "attach a * photo and ask what is in it" failed for every provider except Google. * * Anything outside the universal set is transcoded to PNG. PNG rather than JPEG * because the sources are frequently lossless (TIFF, BMP, ICO) or already * carry alpha, and a lossy re-encode of an image the model is about to read * closely is the wrong default. * * @module adapters/imageFormatSupport */ import type { ExifOrientationProbe, ImageOrientationNormalization, ImageWithAltText, VisionImageConversion, VisionTranscodeOptions } from "../types/index.js"; /** * MIME types every vision-capable provider accepts as-is. * * This is the intersection across OpenAI, Anthropic, Google (AI Studio and * Vertex), Bedrock, Azure and Mistral — deliberately the intersection and not * a per-provider matrix. Google additionally accepts HEIC/HEIF natively, but * converting those for Google as well costs one transcode and removes an * entire axis of provider-specific branching from the dispatch path. */ export declare const UNIVERSAL_VISION_IMAGE_MIME_TYPES: ReadonlySet<string>; /** * Every image MIME type NeuroLink accepts as *input*. * * The union of what providers take as-is and what this module can convert for * them. Intake validation must use this rather than the universal set alone: * a format we can transcode is a format we accept, and gating intake on the * provider-acceptable list rejects the file before conversion ever runs. */ export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: ReadonlySet<string>; /** * True when a MIME type needs transcoding before it can be sent to a vision * provider. Cheap enough to call on every image; callers use it to avoid * reading a file off disk that would not have been converted anyway. */ export declare function needsVisionTranscode(mimeType: string): boolean; /** * True when a MIME type's format can carry an orientation tag at all. Cheap * enough to call on every image; callers use it to skip reading a file off * disk (or invoking sharp) for a format that structurally cannot need it. */ export declare function mayCarryExifOrientation(mimeType: string): boolean; /** * Leading bytes {@link probeExifOrientation} needs to reach a verdict on the * containers it parses. * * A JPEG's EXIF block lives in an APP1 segment whose length field is 16 bits * wide, so the segment cannot exceed 64 KiB — and IFD0, which holds the * orientation tag, sits at the very front of it. One window this size * therefore spans the entire tag block of any file written EXIF-first, which * is every file a camera or phone produces. WebP settles in 21 bytes; PNG * walks complete chunks through the first IDAT and falls back if this window * ends first. * * Sized deliberately in terms of what the format guarantees rather than what * a sample of files happens to need: a window chosen by measurement would * turn an unusual-but-legal layout into a silent wrong answer, whereas * running out of this one returns `"inconclusive"` and costs only the read it * was trying to avoid. */ export declare const EXIF_PROBE_PREFIX_BYTES: number; /** * Decide from a bounded header prefix whether an image carries an EXIF * orientation worth acting on — without decoding it, and without needing all * of its bytes. * * This exists because the orientation question is asked of every JPEG, PNG * and WebP a caller supplies, while the answer is "no" for almost all of them. * Answering it with a decoder means reading the whole file into memory and * loading sharp to look at a header; answering it here costs * {@link EXIF_PROBE_PREFIX_BYTES} at most, and usually far less. * * The verdict is deliberately three-valued. `"absent"` is a promise — the * container was parsed far enough to rule an actionable tag out — and callers * may skip the expensive path on it. Everything else, including every * container this probe does not parse (HEIC, HEIF, AVIF: ISOBMFF, where * orientation is an item property rather than a header field), comes back * `"inconclusive"` and must fall through to the decoder. * * @param prefix - Leading bytes of the image; the whole image is also fine. * @param mimeType - Detected MIME type of the image `prefix` came from. */ export declare function probeExifOrientation(prefix: Buffer, mimeType: string): ExifOrientationProbe; /** * Return image bytes every vision provider can read, transcoding to PNG when * the source format is one no provider accepts. * * Never throws for image reasons. When neither backend can decode the input, * the original bytes are returned with a warning naming the format — the * request then fails at the provider exactly as it did before, rather than this * compatibility step becoming a new way for a previously working request to * break. * * `options.autoOrient` folds an EXIF orientation correction into the same * decode, for a format that needs both (HEIC, HEIF, TIFF, AVIF are in both * sets). The sharp backend honours it explicitly, via `.rotate()`. * * The ffmpeg backends take no flag for it, and `-autorotate` would only * restate a default rather than fix anything — but that is established for * one orientation mechanism only, so read the scope before relying on it. * * **Measured — EXIF-IFD orientation.** A 300x150 JPEG tagged `Orientation=6` * came back 150x300 with the tag consumed on system ffmpeg 8.1.1 and on the * bundled `ffmpeg-static` 6.0, and stayed 300x150 on both only when * `-noautorotate` was forced, which is the control showing the default did * the work. TIFF stores orientation the same way. * * ⚠️ **Not measured — ISOBMFF `irot`/`imir`.** HEIC, HEIF and AVIF carry * orientation as item properties, a structurally different mechanism the * EXIF result says nothing about. That gap matters more than its size * suggests: HEVC-coded HEIC is the main reason this fallback exists at all, * since sharp cannot decode it, whereas the JPEG used above never reaches * ffmpeg in production — JPEG is universal and is never transcoded, so the * fixture demonstrates the binaries' behaviour rather than a path a request * takes. Whether an `irot`-bearing HEIC is corrected here is untested in * both directions; treat it as unknown rather than working. * * `autoOrient` is in any case a request rather than a guarantee, exactly like * `converted`: a source no backend can decode is returned untouched. * * ⚠️ Two further things would quietly break the measured case, and neither * fails a test, because the fused path reports `converted: true` either way. * Passing `-noautorotate`, or an ffmpeg build defaulting it off, drops the * rotation. And the sharp-first ordering in {@link transcodeBackends} is what * keeps the two backends' orientation behaviour equivalent rather than merely * similar — sharp strips the tag while ffmpeg's PNG output carries none to * begin with. * * @param buffer - Raw image bytes. * @param mimeType - Detected MIME type of `buffer`. * @param options - See {@link VisionTranscodeOptions}. `outputFormat` * defaults to `"png"` — see the module header for why that stays the * default for every caller who doesn't opt into `"jpeg"` explicitly. */ export declare function toVisionCompatibleImage(buffer: Buffer, mimeType: string, options?: VisionTranscodeOptions): Promise<VisionImageConversion>; /** * Auto-orient an image from its EXIF/XMP tag and strip the tag, so a photo * that was stored rotated (the common case for phone cameras, since the * sensor is read in one fixed orientation and rotation is recorded as a flag * rather than baked into the pixels) is sent upright instead of sideways. * * Three costs are guarded deliberately, matching how `toVisionCompatibleImage` * guards its own: * - **Cost**: `mayCarryExifOrientation` skips formats that cannot carry the * tag; `probeExifOrientation` then settles the formats that can from their * own header bytes, so the overwhelmingly common orientation-less JPEG or * WebP never loads sharp at all. Only a container the probe cannot rule * out reaches a `metadata()` call, and only a tag that is present and not * the already-upright value `1` reaches the decode-and-re-encode. * - **Availability**: sharp is an optional dependency (the ffmpeg fallback * path above exists precisely because it can be absent). Any failure — * missing package, malformed pixels, a timeout — is caught here and * degrades to returning the original bytes unchanged, never throwing. * - **Size**: re-encoding changes byte length. This function only returns the * new bytes; it does not itself re-validate size limits. Callers that * re-encode must re-run their size guard on the returned buffer. * * Never throws for image reasons — mirrors `toVisionCompatibleImage`. * * @param buffer - Raw image bytes. * @param mimeType - Detected MIME type of `buffer`. */ export declare function normalizeImageOrientation(buffer: Buffer, mimeType: string): Promise<ImageOrientationNormalization>; /** * Unwrap an `input.images` entry to its payload. * * `ImageWithAltText` (`{ data, altText }`) is a documented public input shape, * but the provider image loops typed the array as `Buffer | string` and so * treated a wrapper as raw bytes — `toString("base64")` on the object yields * the literal "[object Object]", which is valid base64 that decodes to seven * bytes of garbage. The request therefore reached the API and failed as * "invalid image data", with nothing pointing at the stringification. * * Alt text has no representation in Vertex's `inlineData` part, so it is * dropped here deliberately rather than corrupting the payload to carry it. */ export declare function unwrapImagePayload(entry: Buffer | string | ImageWithAltText): Buffer | string;