Skip to content

feat(browser-inference): run MobileSAM and EfficientViT-SAM-L0 through a per-model definition - #906

Merged
JArmandoAnaya merged 6 commits into
mainfrom
feat/browser-model-catalog
Oct 9, 2026
Merged

JArmandoAnaya merged 6 commits into
mainfrom
feat/browser-model-catalog

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

What

Generalises the browser inference host over a PromptableModelDefinition and adds two more promptable segmentation models to @visionset/browser-inference: MobileSAM and EfficientViT-SAM-L0.

  • The host, worker and client no longer assume EfficientSAM-Ti. Decoder tensor shapes, mask-refinement inputs and the orig_im_size dtype are declared per model.
  • mobileSam.ts and efficientVitSam.ts define each model's preprocessing, encoder and decoder contract.
  • EfficientSAM-Ti is moved onto the same definition.

Scope

Runtime code and its vitest coverage only. The ONNX exporters, parity checks and real-browser specs for these models are not part of this PR, so the new definitions are tested against fakes here.

Verification

  • @visionset/browser-inference: lint, typecheck and 166 tests pass.
  • @visionset/app (src/data/browserInference): 109 tests pass; @visionset/ui-core (src/inference): 27 tests pass.
  • pnpm -r build succeeds.

JArmandoAnaya and others added 6 commits October 9, 2026 01:27
…bleModelDefinition

host.ts, client.ts and worker.ts were hardcoded to EfficientSAM-Ti's tensor
names and pure functions directly, even though promptable.ts's own runtime
contract was already model-agnostic by design. Adds definition.ts's
PromptableModelDefinition, bundling a model's tensor names and its
requireUsableImage/requireAnswerablePrompt/encoderInput/decoderPrompt/
bestCandidate/binaryMask functions into one value.

createModelHost and createModelClient default this parameter to
EFFICIENT_SAM_TI_DEFINITION, so the ~40 existing unit-test call sites that
construct them with just a factory/channel are unchanged. The worker learns
which definition to run a model-load's graphs against from a new modelId
field on that protocol message, looked up in a small per-worker registry --
adding a model is one entry in that registry plus a new create<Model>Runtime
factory, not a change to any message-handling code.

Verified behavior-preserving: typecheck, lint, all 129 unit tests, and all
17 real-Chromium browser tests pass, with the browser's mask SHA-256s
still matching the Python parity reference exactly.
…t inputs are per-model

host.ts hardcoded [1, 1, maxPoints, 2]/[1, 1, maxPoints] as if every
decoder padded points into the same fixed, batched shape EfficientSAM-Ti
uses. MobileSAM's official ONNX decoder disagrees on both counts: its
point tensors are dynamic-length ([1, N, 2]/[1, N]), and its graph also
declares mask_input/has_mask_input, the SAM-family "previous low-res
mask" refinement pair that EfficientSAM-Ti's graph does not have.

decoderPrompt() now returns its own coords/labels dims alongside the
data, and DecoderTensorNames gains optional maskInput/hasMaskInput
names plus an emptyMaskInput() definition hook -- host.ts feeds "there
is no previous mask" (hasMaskInput zeroed) whenever a decoder declares
that pair, rather than omitting an input the graph requires.

Verified behavior-preserving: typecheck, lint, all 129 unit tests and
all 17 real-Chromium browser tests pass, mask SHA-256s unchanged.
MobileSAM's official ONNX decoder export (SamOnnxModel, upstream's own
dummy inputs) declares orig_im_size as float32, while EfficientSAM-Ti's
graph declares the same-purpose input int64 -- a real disagreement
between two SAM-family graphs. DecoderTensorNames.sizeDtype makes this
explicit instead of host.ts hardcoding int64.
Adds mobileSam.ts's PromptableModelDefinition -- including a hand-rolled
bilinear resize (align_corners=False, PyTorch's own "half-pixel centers"
sampling grid) for the resize-longest-side + normalize + pad step that
runs outside the encoder graph -- registers it in worker.ts's model
registry, and exposes createMobileSamRuntime from the browser adapter.

requireAnswerablePrompt refuses a negative point, matching the measured
finding from scripts/browser_models/mobilesam/parity.py: the mechanism
is real (a lone label-0 point differs from a lone label-1 point) but
not reliably exclusionary, so it is refused rather than answered with
an exclusion that was not confirmed.

Real-browser proof (mobileSam.spec.ts, 10 tests, all passing): loads
the real exported graphs, encodes the fixture image once, refines
across three suggest calls on the same held embedding, proves the
encoder is not rerun, and reports WebGPU honestly. The masks it
produces are bit-identical to the Python parity reference -- exact
confidence, exact lit-pixel count, matching SHA-256 -- meaning the
TypeScript resize matches torch.nn.functional.interpolate exactly.

CI: browser-models job now exports, runs parity for, and browser-tests
both models in one job (one shared torch install, one cache step per
model keyed on that model's own pin). assert_tests_ran.mjs's
REAL_MODEL_SPEC is now REAL_MODEL_SPECS, a list, so each model's
minimum-tests floor is independently visible in a diff.

Caught and fixed in this change: an unquoted job `name:` containing a
colon inside parentheses is invalid YAML (a bare `key:` is parsed as a
new mapping key) -- validated with a real YAML parse, not just review.
@JArmandoAnaya
JArmandoAnaya merged commit 49177d3 into main Oct 9, 2026
31 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/browser-model-catalog branch October 9, 2026 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant