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.jsonat the root, plusweights.onnxbeside it. The JSON names the weights file in itsweightsPath.- AlgorithmJSON
- The same
model-export.jsondocument on its own, withweightsPathset tonull. 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.
{
"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" }
}
}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
algorithmIdon the enclosing export document. - kindstring
deterministic,calibrated, orlearned. Checked against the pipeline: a recipe declaringlearnedmust contain an inference step, and one containing an inference step must declarelearned. 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
opand may carry aparamsobject; 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
classifieron 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) andemits(countorpresence). A recipe containing this step must declarekind: "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.
"pipeline": [
{ "op": "temporal_presence" },
{ "op": "temporal_headcount" },
{ "op": "temporal_stationary_hold" },
{ "op": "median_smooth", "window": 5 }
]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.
"pipeline": [
{ "op": "temporal_presence" },
{ "op": "temporal_headcount" },
{ "op": "require_presence", "params": { "target": "count" } },
{ "op": "median_smooth", "params": { "window": 9 } }
]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.
{
"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]
}
}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.
{
"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" }
}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, ortimeseries.- deriveFromstring
- For
booleanandcount:presentorcount_gt_zero. Fortimeseries:breathing_windows. - channelsarray
- Required for
timeseries, and rejected for the other two. Each channel is alabeland afield; the accepted fields arebpmandsnr.
"output": {
"format": "timeseries",
"deriveFrom": "breathing_windows",
"channels": [
{ "label": "breaths/min", "field": "bpm" },
{ "label": "signal quality", "field": "snr" }
]
}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
modelIdis 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.