Skip to content

Search documentation

Find a section by name or by what it says.

Documentation

Analyzer SDK

Every detector the platform builds is exportable as one file, and every exported file is importable. This page describes what is inside that file. It is a stable, versioned format: a consumer that understands its version can read a bundle without the product that produced it.

The export bundle

There are two export shapes, and which one you get depends on whether the detector has weights.

Trained modelZIP
model-export.json at the root, plus weights.onnx beside it. The JSON names the weights file in its weightsPath.
AlgorithmJSON
The same model-export.json document on its own, with weightsPath set to null. Not a ZIP, because there is nothing to bundle alongside it.

Both shapes are the same document type. A learned export is that specification plus a referenced weights file, so one reader handles both.

There are two specVersion fields in a bundle and they are versioned independently: the one at the top is the export document's format, and the one inside recipe is the recipe format's. They are 1 and 2 respectively. A reader that checks one against the other will reject valid bundles.

json
{
  "specVersion": 1,
  "modelId": "mdl-7f3c1a",
  "name": "Presence (living room)",
  "version": "1.0.0",
  "algorithmId": "algo-presence",
  "kind": "deterministic",
  "architecture": null,
  "filterIds": ["denoising", "bandpass"],
  "filterImplVersions": [
    ["denoising", "1.0.0"],
    ["bandpass", "1.2.0"]
  ],
  "baselineSessionId": null,
  "weightsPath": null,
  "accuracy": null,
  "f1": null,
  "exportedAt": "2026-08-27T09:14:02Z",
  "recipe": {
    "specVersion": 2,
    "algorithmId": "algo-presence",
    "kind": "deterministic",
    "pipeline": [{ "op": "temporal_presence_optimized" }],
    "output": { "format": "boolean", "deriveFrom": "present" }
  }
}
model-export.json for a deterministic detector — the whole file

Filters and reproducibility

filterIds names the preprocessing stages the detector was built against. filterImplVersions gives the implementation version of each one.

Two implementations of the same named filter can produce different outputs from a byte-identical specification: the ids match, the names match, and the answers differ. A learned model is pinned by the content hash over its weights; a closed-form detector has no equivalent, so its stage versions are its version.

An export whose filterImplVersions do not match the versions available where it is imported is still valid and still runs. It is simply not guaranteed to reproduce the numbers it was published with, and the field is what lets you notice.

The recipe document

A recipe is a declarative pipeline: an ordered list of operations over the CSI, plus a statement of how the result is shaped. When a bundle carries one, the SDK executes the recipe — it does not look up a compiled algorithm by name. That is what makes an exported detector portable rather than a reference to something on the other side.

specVersionnumber
Format version of the recipe; the current version is 2. A reader rejects any version above the one it knows. Version 1 documents are accepted and run unchanged.
algorithmIdstring
Which detector this is. Must match the algorithmId on the enclosing export document.
kindstring
deterministic, calibrated, or learned. Checked against the pipeline: a recipe declaring learned must contain an inference step, and one containing an inference step must declare learned. A document that runs weights while calling itself closed-form is not reproducible from the document alone.
pipelinearray
One or more steps, executed in order. An empty pipeline is rejected. Each step names an op and may carry a params object; a parameter the op does not accept is an error rather than something ignored.
outputobject
How the pipeline's result is exposed. See below.

The document is validated as a whole before anything runs, and that includes the shape of any fitted numbers inside it. A classifier whose mean rows do not match its class list is rejected at load rather than scoring against the wrong column on somebody's recording.

Pipeline operations

A step names an op. Anything not on this list is rejected — an unknown op is a document a reader cannot honour, and skipping it would produce an answer from a pipeline that never ran.

temporal_presencedetect
Presence over a window from temporal variation.
temporal_presence_optimizeddetect
Presence with tuned thresholds. The default presence step.
temporal_headcountdetect
How many occupants, given presence. Meaningless on its own — presence has to have been established first.
temporal_stationary_holdstabilise
Holds a decision across intervals where an occupant is still. Without it, someone sitting motionless reads as an empty room.
median_smoothstabilise
Median filter over the decision series, removing single-window flips.
capture_breathing_windowsmeasure
Per-window breathing rate and a signal-quality figure. Produces a time series rather than a decision.
capability_gategate
Suppresses downstream steps when the recording does not have what they need.
diag_mahalanobis_countclassify
Diagonal-Mahalanobis classification over named features. Requires a classifier on the same step; no other op accepts one.
onnx_inferinfer
Runs the bundle's weights over the capture. Takes artifact (the weights file inside the bundle) and emits (count or presence). A recipe containing this step must declare kind: "learned". Added in spec version 2.
require_presencegate
Zeroes the count wherever the presence series says the space is empty. Takes target: "count". Added in spec version 2.
gate_breathinggate
Suppresses breathing measurements for windows that are unoccupied or too active for a breathing rate to mean anything. Takes by: "presence". Added in spec version 2.
json
"pipeline": [
  { "op": "temporal_presence" },
  { "op": "temporal_headcount" },
  { "op": "temporal_stationary_hold" },
  { "op": "median_smooth", "window": 5 }
]
A four-step counting pipeline

window on median_smooth is read as of spec version 2: a document asking for 9 is smoothed over 9. It must be odd, so the median has a single middle element, and at most 99.

Step parameters

A step's settings go in a params object keyed by name. Each op declares which parameters it accepts, and a key the op does not take is rejected when the document is loaded — not ignored.

json
"pipeline": [
  { "op": "temporal_presence" },
  { "op": "temporal_headcount" },
  { "op": "require_presence", "params": { "target": "count" } },
  { "op": "median_smooth", "params": { "window": 9 } }
]
A gated pipeline that smooths over a window it chooses

Values are plain JSON scalars — 9, "count", true — not tagged objects. A recipe should read like configuration.

Fitted numbers as data

A calibrated detector's parameters live in the document, not in code. diag_mahalanobis_count carries per-class feature means in raw units and one pooled within-class variance per feature.

json
{
  "op": "diag_mahalanobis_count",
  "classifier": {
    "classes": [0, 1, 2],
    "features": ["wb_tap_power_max", "wb_tap_power_sum"],
    "means": [
      [141.0, 402.0],
      [1232.0, 3110.0],
      [2084.0, 5233.0]
    ],
    "variances": [88.5, 240.1]
  }
}
A three-class classifier over two features

The numbers are in raw feature units and there is no scaler to apply. Diagonal Mahalanobis over raw features with pooled variances is the same rule as over z-scored features, so a reader scores a capture directly against the means and variances in the document.

Composing two detectors

A pipeline can mix a closed-form detector with a learned one. The composition happens at the decision level: one detector's answer becomes an input to the other's, with no shared feature vector anywhere.

json
{
  "specVersion": 2,
  "algorithmId": "algo-presence-gated-count",
  "kind": "learned",
  "pipeline": [
    { "op": "temporal_presence" },
    { "op": "onnx_infer", "params": { "artifact": "weights.onnx", "emits": "count" } },
    { "op": "require_presence", "params": { "target": "count" } },
    { "op": "median_smooth", "params": { "window": 5 } }
  ],
  "output": { "format": "count", "deriveFrom": "present" }
}
An algorithmic presence detector gating a learned occupant counter

Presence gates the count, and not the reverse. A headcount read from an interval whose presence detector says the room is empty is reading noise.

Output

output states the shape of the answer, which is what lets one consumer render every detector without knowing which one it is holding.

formatstring
boolean, count, or timeseries.
deriveFromstring
For boolean and count: present or count_gt_zero. For timeseries: breathing_windows.
channelsarray
Required for timeseries, and rejected for the other two. Each channel is a label and a field; the accepted fields are bpm and snr.
json
"output": {
  "format": "timeseries",
  "deriveFrom": "breathing_windows",
  "channels": [
    { "label": "breaths/min", "field": "bpm" },
    { "label": "signal quality", "field": "snr" }
  ]
}
A two-channel time series

Exporting and importing

Export is a download from the detector's own page: a trained model from the model detail panel, a catalog algorithm from its card. Import is a single upload that accepts either shape and figures out which it received.

An import is not a restore. Several fields on the bundle name rows in the organization that produced it, and carrying them across would be meaningless at best:

  • A fresh id is minted. The bundle's modelId is recorded as the source and never reused, so an import can never overwrite an existing detector — including one in an organization the importer does not belong to.
  • Visibility is forced private. A bundle that was public where it was made does not become public where it lands; sharing it is a separate, deliberate act.
  • Session and training-pack references are dropped. They name recordings in another organization, which the importer cannot see and must not be told about.
  • The recipe is kept. It is the executable part — an import that dropped it would produce a row that looks like a detector and cannot answer anything.

A round trip is exact for the parts that define behaviour. Export a catalog algorithm, import it under a name of your own, and it answers identically to the built-in — the recipe is what runs, and the imported one is no longer recognised by id.