{"version":3,"file":"fusion.cjs","names":[],"sources":["../../src/vision/fusion.ts"],"sourcesContent":["/** @generated Vendored from @mauriciobenjamin700/ort-vision-sdk-web. Do not hand-edit — regenerate with `npm run vendor:vision`. */\n/**\n * The contract a fused pipeline carries inside its own `.onnx` file.\n *\n * A pipeline built by the Python SDK's `ort_vision_sdk.compose` is a single\n * graph that already contains the detector, the crop-and-resize bridge and the\n * classifier. Everything the runtime needs to drive it — the letterbox\n * resolution, the crop resolution, where the crops come from, how many\n * detections the graph emits, the class names of both stages — was decided at\n * fusion time and written into the model's metadata.\n *\n * This module reads it back. It is the browser half of a contract whose other\n * half lives in `ort_vision_sdk/fusion.py`: the same keys, the same encodings,\n * the same fallbacks. A pipeline fused once therefore runs identically in\n * Python and in a browser tab, off the same file.\n *\n * Building a pipeline stays a Python-side build step — there is no ONNX\n * protobuf writer here, and there is no reason for one: fusing is something you\n * do once next to your export pipeline, not in a page load.\n */\n\nimport { parseNames } from \"./core/metadata\";\n\n/**\n * Which tensor the bridge crops the detected boxes out of.\n *\n * - `\"detector_input\"`: the letterboxed tensor already fed to the detector.\n *   The fused graph then has a **single** image input, but a small object is\n *   cropped out of its downscaled copy.\n * - `\"original\"`: a second, full-resolution image input. The bridge undoes the\n *   letterbox transform in-graph and crops at native resolution. Two tensors to\n *   feed, still one session and one model load.\n */\nexport type CropSource = \"detector_input\" | \"original\";\n\n/** Value of the `ovs.kind` metadata key for a detector→classifier pipeline. */\nexport const FUSION_KIND_DETECT_CLASSIFY = \"detect_classify\";\n\n/**\n * Namespace for every metadata key the fusion writes.\n *\n * Namespaced on purpose: the detector's own Ultralytics metadata (`names`,\n * `task`, `imgsz`) is carried over into the fused model, and an un-prefixed key\n * would either collide with it or be mistaken for it.\n */\nexport const METADATA_PREFIX = \"ovs.\";\n\n/** Name of the fused graph's letterboxed detector input, `[1, 3, H, W]` float32 in `[0, 1]`. */\nexport const INPUT_IMAGE = \"images\";\n\n/** Name of the full-resolution input. Present only when `cropSource === \"original\"`. */\nexport const INPUT_SOURCE = \"source_image\";\n\n/** Name of the `[1]` float32 letterbox scale factor. Only with `cropSource === \"original\"`. */\nexport const INPUT_SCALE = \"letterbox_scale\";\n\n/** Name of the `[2]` float32 `[padLeft, padTop]`. Only with `cropSource === \"original\"`. */\nexport const INPUT_PAD = \"letterbox_pad\";\n\n/**\n * Name of the `[K, 4]` float32 xyxy output, in **letterboxed** input pixels.\n *\n * A file fused by `ort-vision-sdk` 0.9.0 or later reports the box that was\n * actually classified: clamped to the image the crop came from, exactly as\n * RoiAlign received it. Older files report the raw box, so one that ran off the\n * frame draws a rectangle wider than the region the classifier saw.\n */\nexport const OUTPUT_BOXES = \"boxes\";\n\n/** Name of the `[K]` float32 detection-confidence output. */\nexport const OUTPUT_SCORES = \"scores\";\n\n/** Name of the `[K]` int64 detector-class output. */\nexport const OUTPUT_CLASSES = \"classes\";\n\n/** Name of the `[1]` int64 output holding how many of the `K` rows are real. */\nexport const OUTPUT_NUM_DETECTIONS = \"num_detections\";\n\n/** Name of the `[K, numClassifierClasses]` float32 classifier output, one row per box. */\nexport const OUTPUT_PROBS = \"probs\";\n\n/** Everything a fused pipeline declares about how it must be driven. */\nexport interface FusionSpec {\n    /** Pipeline family. Only `\"detect_classify\"` exists today. */\n    readonly kind: string;\n    /** `[width, height]` the detector stage expects — the resolution to letterbox to. */\n    readonly inputSize: readonly [number, number];\n    /** `[width, height]` every crop is resampled to inside the graph. */\n    readonly cropSize: readonly [number, number];\n    /** Which tensor the crops are taken from. */\n    readonly cropSource: CropSource;\n    /**\n     * Fixed number of rows `K` every output carries, surplus zero-padded and\n     * counted by {@link OUTPUT_NUM_DETECTIONS}. `null` means the graph emits\n     * exactly as many rows as survived NMS.\n     */\n    readonly maxDetections: number | null;\n    /** Score threshold baked into the graph's NMS node. */\n    readonly confThreshold: number;\n    /** IoU threshold baked into the graph's NMS node. */\n    readonly iouThreshold: number;\n    /** Whether the classifier stage emits logits that still need a softmax. */\n    readonly applySoftmax: boolean;\n    /** Detector class names in class-id order, or `null` when the fusion recorded none. */\n    readonly detectorNames: readonly string[] | null;\n    /** Classifier class names in class-id order, or `null`. */\n    readonly classifierNames: readonly string[] | null;\n    /** Version of `ort-vision-sdk` that produced the file. */\n    readonly sdkVersion: string;\n    /** Whether driving this pipeline requires feeding the full-resolution input. */\n    readonly needsSourceImage: boolean;\n}\n\nconst KEY_KIND = \"kind\";\nconst KEY_SDK_VERSION = \"sdk_version\";\nconst KEY_INPUT_SIZE = \"input_size\";\nconst KEY_CROP_SIZE = \"crop_size\";\nconst KEY_CROP_SOURCE = \"crop_source\";\nconst KEY_MAX_DETECTIONS = \"max_detections\";\nconst KEY_CONF_THRESHOLD = \"conf_threshold\";\nconst KEY_IOU_THRESHOLD = \"iou_threshold\";\nconst KEY_APPLY_SOFTMAX = \"apply_softmax\";\nconst KEY_DETECTOR_NAMES = \"detector_names\";\nconst KEY_CLASSIFIER_NAMES = \"classifier_names\";\n\nconst DYNAMIC = \"dynamic\";\n\n/**\n * Decode a `\"640,640\"` pair.\n *\n * @param raw The encoded pair.\n * @returns `[width, height]`, or `null` when the value is missing or malformed.\n */\nfunction decodeSize(raw: string | undefined): readonly [number, number] | null {\n    if (!raw) return null;\n    const parts = raw.split(\",\");\n    if (parts.length !== 2) return null;\n    const width = Number(parts[0]);\n    const height = Number(parts[1]);\n    if (!Number.isInteger(width) || !Number.isInteger(height)) return null;\n    if (width < 1 || height < 1) return null;\n    return [width, height];\n}\n\n/**\n * Decode a float, falling back when the value is missing or malformed.\n *\n * @param raw The encoded value.\n * @param fallback Value to use when `raw` cannot be read.\n * @returns The parsed number, or `fallback`.\n */\nfunction decodeFloat(raw: string | undefined, fallback: number): number {\n    if (!raw) return fallback;\n    const value = Number(raw);\n    return Number.isFinite(value) ? value : fallback;\n}\n\n/**\n * Read a pipeline spec out of a model's custom metadata.\n *\n * Individual malformed entries fall back to the value a fusion would have used\n * by default — a single bad float is not a reason to reject an otherwise\n * loadable pipeline. A malformed resolution is fatal, because there is no safe\n * default for one.\n *\n * @param metadata A model's custom metadata map, as read by\n *   {@link readModelMetadata}.\n * @returns The decoded spec, or `null` when the model is not a fused pipeline —\n *   it carries no `ovs.kind` entry, or one naming a pipeline kind this version\n *   does not know how to drive.\n */\nexport function readFusionSpec(\n    metadata: Readonly<Record<string, string>> | undefined,\n): FusionSpec | null {\n    if (!metadata) return null;\n\n    const read: Record<string, string> = {};\n    for (const [key, value] of Object.entries(metadata)) {\n        if (key.startsWith(METADATA_PREFIX)) read[key.slice(METADATA_PREFIX.length)] = value;\n    }\n    if (read[KEY_KIND] !== FUSION_KIND_DETECT_CLASSIFY) return null;\n\n    const inputSize = decodeSize(read[KEY_INPUT_SIZE]);\n    const cropSize = decodeSize(read[KEY_CROP_SIZE]);\n    if (inputSize === null || cropSize === null) return null;\n\n    const rawMax = read[KEY_MAX_DETECTIONS] ?? DYNAMIC;\n    const parsedMax = Number(rawMax);\n    const maxDetections =\n        rawMax === DYNAMIC || !Number.isInteger(parsedMax) || parsedMax < 1 ? null : parsedMax;\n\n    const cropSource: CropSource =\n        read[KEY_CROP_SOURCE] === \"original\" ? \"original\" : \"detector_input\";\n\n    return {\n        kind: FUSION_KIND_DETECT_CLASSIFY,\n        inputSize,\n        cropSize,\n        cropSource,\n        maxDetections,\n        confThreshold: decodeFloat(read[KEY_CONF_THRESHOLD], 0.25),\n        iouThreshold: decodeFloat(read[KEY_IOU_THRESHOLD], 0.45),\n        applySoftmax: (read[KEY_APPLY_SOFTMAX] ?? \"1\") !== \"0\",\n        detectorNames: parseNames(read[KEY_DETECTOR_NAMES]),\n        classifierNames: parseNames(read[KEY_CLASSIFIER_NAMES]),\n        sdkVersion: read[KEY_SDK_VERSION] ?? \"\",\n        needsSourceImage: cropSource === \"original\",\n    };\n}\n"],"mappings":"uCAoCA,IAAa,EAA8B,kBAS9B,EAAkB,OAGlB,EAAc,SAGd,EAAe,eAGf,EAAc,kBAGd,EAAY,gBAUZ,EAAe,QAGf,EAAgB,SAGhB,EAAiB,UAGjB,EAAwB,iBAGxB,EAAe,QAkCtB,EAAW,OACX,EAAkB,cAClB,EAAiB,aACjB,EAAgB,YAChB,EAAkB,cAClB,EAAqB,iBACrB,EAAqB,iBACrB,EAAoB,gBACpB,EAAoB,gBACpB,EAAqB,iBACrB,EAAuB,mBAEvB,EAAU,UAQhB,SAAS,EAAW,EAA2D,CAC3E,GAAI,CAAC,EAAK,OAAO,KACjB,IAAM,EAAQ,EAAI,MAAM,GAAG,EAC3B,GAAI,EAAM,SAAW,EAAG,OAAO,KAC/B,IAAM,EAAQ,OAAO,EAAM,EAAE,EACvB,EAAS,OAAO,EAAM,EAAE,EAG9B,MAFI,CAAC,OAAO,UAAU,CAAK,GAAK,CAAC,OAAO,UAAU,CAAM,GACpD,EAAQ,GAAK,EAAS,EAAU,KAC7B,CAAC,EAAO,CAAM,CACzB,CASA,SAAS,EAAY,EAAyB,EAA0B,CACpE,GAAI,CAAC,EAAK,OAAO,EACjB,IAAM,EAAQ,OAAO,CAAG,EACxB,OAAO,OAAO,SAAS,CAAK,EAAI,EAAQ,CAC5C,CAgBA,SAAgB,EACZ,EACiB,CACjB,GAAI,CAAC,EAAU,OAAO,KAEtB,IAAM,EAA+B,CAAC,EACtC,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAQ,EAC1C,EAAI,WAAA,MAA0B,IAAG,EAAK,EAAI,MAAM,CAAsB,GAAK,GAEnF,GAAI,EAAK,KAAA,kBAA2C,OAAO,KAE3D,IAAM,EAAY,EAAW,EAAK,EAAe,EAC3C,EAAW,EAAW,EAAK,EAAc,EAC/C,GAAI,IAAc,MAAQ,IAAa,KAAM,OAAO,KAEpD,IAAM,EAAS,EAAK,IAAuB,EACrC,EAAY,OAAO,CAAM,EACzB,EACF,IAAW,GAAW,CAAC,OAAO,UAAU,CAAS,GAAK,EAAY,EAAI,KAAO,EAE3E,EACF,EAAK,KAAqB,WAAa,WAAa,iBAExD,MAAO,CACH,KAAM,EACN,YACA,WACA,aACA,gBACA,cAAe,EAAY,EAAK,GAAqB,GAAI,EACzD,aAAc,EAAY,EAAK,GAAoB,GAAI,EACvD,cAAe,EAAK,IAAsB,OAAS,IACnD,cAAe,EAAA,WAAW,EAAK,EAAmB,EAClD,gBAAiB,EAAA,WAAW,EAAK,EAAqB,EACtD,WAAY,EAAK,IAAoB,GACrC,iBAAkB,IAAe,UACrC,CACJ"}