motion-intelligence 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- motion_intelligence-0.2.0/PKG-INFO +134 -0
- motion_intelligence-0.2.0/README.md +122 -0
- motion_intelligence-0.2.0/motion/__init__.py +10 -0
- motion_intelligence-0.2.0/motion/batch.py +153 -0
- motion_intelligence-0.2.0/motion/categories.py +46 -0
- motion_intelligence-0.2.0/motion/cli.py +503 -0
- motion_intelligence-0.2.0/motion/config.py +148 -0
- motion_intelligence-0.2.0/motion/dockerfile.py +110 -0
- motion_intelligence-0.2.0/motion/predictor.py +62 -0
- motion_intelligence-0.2.0/motion/profile.py +345 -0
- motion_intelligence-0.2.0/motion/server.py +138 -0
- motion_intelligence-0.2.0/motion/weight_upload.py +324 -0
- motion_intelligence-0.2.0/motion_intelligence.egg-info/PKG-INFO +134 -0
- motion_intelligence-0.2.0/motion_intelligence.egg-info/SOURCES.txt +18 -0
- motion_intelligence-0.2.0/motion_intelligence.egg-info/dependency_links.txt +1 -0
- motion_intelligence-0.2.0/motion_intelligence.egg-info/entry_points.txt +2 -0
- motion_intelligence-0.2.0/motion_intelligence.egg-info/requires.txt +3 -0
- motion_intelligence-0.2.0/motion_intelligence.egg-info/top_level.txt +1 -0
- motion_intelligence-0.2.0/pyproject.toml +29 -0
- motion_intelligence-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: motion-intelligence
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Author, build & deploy Python models for the Motion inference platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/devathub9/Motion-packages
|
|
6
|
+
Project-URL: Repository, https://github.com/devathub9/Motion-packages
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: requests>=2.28
|
|
11
|
+
Requires-Dist: boto3>=1.26
|
|
12
|
+
|
|
13
|
+
# motion-cli (Python)
|
|
14
|
+
|
|
15
|
+
Build and deploy Docker images for models on the Motion inference platform.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e /path/to/cli-py # gives the `motion` command
|
|
19
|
+
export MOTION_API_KEY=motion_live_<your_key>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
A model is a folder with `motion.yml` + `predict.py`:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# predict.py
|
|
28
|
+
from motion import BasePredictor, Input, Path
|
|
29
|
+
|
|
30
|
+
class MyPredictor(BasePredictor):
|
|
31
|
+
def setup(self):
|
|
32
|
+
self.model = load_model(...)
|
|
33
|
+
|
|
34
|
+
def predict(self, video: Path = Input(description="Input clip")) -> Path:
|
|
35
|
+
out = "/tmp/out.mp4"
|
|
36
|
+
self.model.run(video, out)
|
|
37
|
+
return Path(out)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# motion.yml
|
|
42
|
+
name: lingbot-map
|
|
43
|
+
predict: predict.py:MyPredictor
|
|
44
|
+
description: Streaming 3D reconstruction
|
|
45
|
+
|
|
46
|
+
# Where the model appears in GET /api/v1/motion/models
|
|
47
|
+
category: reconstruction # required for catalog listing
|
|
48
|
+
inference_model: lingbot-map # optional; defaults to `name`
|
|
49
|
+
input_type: video # video | image | text
|
|
50
|
+
output_type: mp4
|
|
51
|
+
|
|
52
|
+
# Used by `motion deploy` to measure CPU/RAM via docker stats
|
|
53
|
+
sample: demo_files/input.mp4
|
|
54
|
+
|
|
55
|
+
build:
|
|
56
|
+
gpu: true
|
|
57
|
+
cuda: "11.8"
|
|
58
|
+
python_version: "3.11"
|
|
59
|
+
system_packages: [ffmpeg]
|
|
60
|
+
python_packages: ["torch>=2.4.0"]
|
|
61
|
+
|
|
62
|
+
# Optional — overrides auto-profiling
|
|
63
|
+
compute:
|
|
64
|
+
cpu: 4
|
|
65
|
+
memory_mib: 16384
|
|
66
|
+
gpu: true
|
|
67
|
+
|
|
68
|
+
weights:
|
|
69
|
+
- path: weights/model.pt
|
|
70
|
+
url: https://example.com/model.pt
|
|
71
|
+
dest: weights/model.pt
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
### `motion build`
|
|
77
|
+
|
|
78
|
+
Uploads local weights to S3 (via `MOTION_API_KEY`, no AWS creds on your machine),
|
|
79
|
+
generates `.motion/Dockerfile`, and builds `motion-<name>:latest`.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
motion build . # build image
|
|
83
|
+
motion build . --dry-run # print Dockerfile only
|
|
84
|
+
motion build . --tag myimg:dev # custom tag
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Requires:** `MOTION_API_KEY` when `weights:` has local files to upload.
|
|
88
|
+
|
|
89
|
+
Large weights (≥64 MiB) use multipart S3 upload automatically.
|
|
90
|
+
|
|
91
|
+
### `motion deploy`
|
|
92
|
+
|
|
93
|
+
Profiles the local image, pushes to ECR, and registers the model in the platform.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
motion deploy .
|
|
97
|
+
motion deploy . --no-profile # skip docker stats measurement
|
|
98
|
+
motion deploy . --sample tests/clip.mp4 # override profiling input
|
|
99
|
+
motion deploy . --profile-timeout 300 # longer profiling window
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Flow:**
|
|
103
|
+
1. Parse `motion.yml` (including `category`, `sample`, `compute`)
|
|
104
|
+
2. **Profile** — run the container locally, poll `docker stats`, derive `cpu_vcpus` + `memory_mib`
|
|
105
|
+
- Skipped if `compute:` is set in motion.yml (`compute_source=declared`)
|
|
106
|
+
- Skipped with `--no-profile`
|
|
107
|
+
- Skipped if no sample file is found
|
|
108
|
+
3. Push image to ECR (backend issues a short-lived docker login token)
|
|
109
|
+
4. **Register** — `POST /api/v1/motion/models/register` with:
|
|
110
|
+
- `model_code` = `<namespace>/<name>` (e.g. `saurav/lingbot-map`)
|
|
111
|
+
- `category_code`, `inference_model_slug`, compute profile, predict params
|
|
112
|
+
|
|
113
|
+
**Requires:** `MOTION_API_KEY`, local image from `motion build`.
|
|
114
|
+
|
|
115
|
+
**Category codes:** list with `GET /api/v1/motion/models` — e.g. `reconstruction`, `hand-tracking`, `mesh`, `3d-object`.
|
|
116
|
+
|
|
117
|
+
After deploy, submit jobs with:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
POST /api/v1/motion/submit model_name=<namespace>/<name>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Environment
|
|
124
|
+
|
|
125
|
+
| Variable | Purpose |
|
|
126
|
+
|----------|---------|
|
|
127
|
+
| `MOTION_API_KEY` | Auth for build (weight upload) and deploy (ECR + register) |
|
|
128
|
+
| `MOTION_API_URL` | Backend base URL (default: Motion App Runner prod) |
|
|
129
|
+
|
|
130
|
+
## Image contract
|
|
131
|
+
|
|
132
|
+
Every built image exposes: `GET /health`, `GET /schema`, `POST /predict`.
|
|
133
|
+
|
|
134
|
+
Weights declared in `motion.yml` are fetched from S3 at container startup — not baked into the image.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# motion-cli (Python)
|
|
2
|
+
|
|
3
|
+
Build and deploy Docker images for models on the Motion inference platform.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install -e /path/to/cli-py # gives the `motion` command
|
|
7
|
+
export MOTION_API_KEY=motion_live_<your_key>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
A model is a folder with `motion.yml` + `predict.py`:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
# predict.py
|
|
16
|
+
from motion import BasePredictor, Input, Path
|
|
17
|
+
|
|
18
|
+
class MyPredictor(BasePredictor):
|
|
19
|
+
def setup(self):
|
|
20
|
+
self.model = load_model(...)
|
|
21
|
+
|
|
22
|
+
def predict(self, video: Path = Input(description="Input clip")) -> Path:
|
|
23
|
+
out = "/tmp/out.mp4"
|
|
24
|
+
self.model.run(video, out)
|
|
25
|
+
return Path(out)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
# motion.yml
|
|
30
|
+
name: lingbot-map
|
|
31
|
+
predict: predict.py:MyPredictor
|
|
32
|
+
description: Streaming 3D reconstruction
|
|
33
|
+
|
|
34
|
+
# Where the model appears in GET /api/v1/motion/models
|
|
35
|
+
category: reconstruction # required for catalog listing
|
|
36
|
+
inference_model: lingbot-map # optional; defaults to `name`
|
|
37
|
+
input_type: video # video | image | text
|
|
38
|
+
output_type: mp4
|
|
39
|
+
|
|
40
|
+
# Used by `motion deploy` to measure CPU/RAM via docker stats
|
|
41
|
+
sample: demo_files/input.mp4
|
|
42
|
+
|
|
43
|
+
build:
|
|
44
|
+
gpu: true
|
|
45
|
+
cuda: "11.8"
|
|
46
|
+
python_version: "3.11"
|
|
47
|
+
system_packages: [ffmpeg]
|
|
48
|
+
python_packages: ["torch>=2.4.0"]
|
|
49
|
+
|
|
50
|
+
# Optional — overrides auto-profiling
|
|
51
|
+
compute:
|
|
52
|
+
cpu: 4
|
|
53
|
+
memory_mib: 16384
|
|
54
|
+
gpu: true
|
|
55
|
+
|
|
56
|
+
weights:
|
|
57
|
+
- path: weights/model.pt
|
|
58
|
+
url: https://example.com/model.pt
|
|
59
|
+
dest: weights/model.pt
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
### `motion build`
|
|
65
|
+
|
|
66
|
+
Uploads local weights to S3 (via `MOTION_API_KEY`, no AWS creds on your machine),
|
|
67
|
+
generates `.motion/Dockerfile`, and builds `motion-<name>:latest`.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
motion build . # build image
|
|
71
|
+
motion build . --dry-run # print Dockerfile only
|
|
72
|
+
motion build . --tag myimg:dev # custom tag
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Requires:** `MOTION_API_KEY` when `weights:` has local files to upload.
|
|
76
|
+
|
|
77
|
+
Large weights (≥64 MiB) use multipart S3 upload automatically.
|
|
78
|
+
|
|
79
|
+
### `motion deploy`
|
|
80
|
+
|
|
81
|
+
Profiles the local image, pushes to ECR, and registers the model in the platform.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
motion deploy .
|
|
85
|
+
motion deploy . --no-profile # skip docker stats measurement
|
|
86
|
+
motion deploy . --sample tests/clip.mp4 # override profiling input
|
|
87
|
+
motion deploy . --profile-timeout 300 # longer profiling window
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Flow:**
|
|
91
|
+
1. Parse `motion.yml` (including `category`, `sample`, `compute`)
|
|
92
|
+
2. **Profile** — run the container locally, poll `docker stats`, derive `cpu_vcpus` + `memory_mib`
|
|
93
|
+
- Skipped if `compute:` is set in motion.yml (`compute_source=declared`)
|
|
94
|
+
- Skipped with `--no-profile`
|
|
95
|
+
- Skipped if no sample file is found
|
|
96
|
+
3. Push image to ECR (backend issues a short-lived docker login token)
|
|
97
|
+
4. **Register** — `POST /api/v1/motion/models/register` with:
|
|
98
|
+
- `model_code` = `<namespace>/<name>` (e.g. `saurav/lingbot-map`)
|
|
99
|
+
- `category_code`, `inference_model_slug`, compute profile, predict params
|
|
100
|
+
|
|
101
|
+
**Requires:** `MOTION_API_KEY`, local image from `motion build`.
|
|
102
|
+
|
|
103
|
+
**Category codes:** list with `GET /api/v1/motion/models` — e.g. `reconstruction`, `hand-tracking`, `mesh`, `3d-object`.
|
|
104
|
+
|
|
105
|
+
After deploy, submit jobs with:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
POST /api/v1/motion/submit model_name=<namespace>/<name>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Environment
|
|
112
|
+
|
|
113
|
+
| Variable | Purpose |
|
|
114
|
+
|----------|---------|
|
|
115
|
+
| `MOTION_API_KEY` | Auth for build (weight upload) and deploy (ECR + register) |
|
|
116
|
+
| `MOTION_API_URL` | Backend base URL (default: Motion App Runner prod) |
|
|
117
|
+
|
|
118
|
+
## Image contract
|
|
119
|
+
|
|
120
|
+
Every built image exposes: `GET /health`, `GET /schema`, `POST /predict`.
|
|
121
|
+
|
|
122
|
+
Weights declared in `motion.yml` are fetched from S3 at container startup — not baked into the image.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""motion — package & deploy models to the Motion inference platform.
|
|
2
|
+
|
|
3
|
+
Authors import from here in their predict.py:
|
|
4
|
+
|
|
5
|
+
from motion import BasePredictor, Input, Path
|
|
6
|
+
"""
|
|
7
|
+
from .predictor import BasePredictor, Input, Path
|
|
8
|
+
|
|
9
|
+
__all__ = ["BasePredictor", "Input", "Path"]
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Batch job runner — invoked when the container runs inside AWS Batch.
|
|
2
|
+
|
|
3
|
+
Environment variables (set by the Lambda dispatcher):
|
|
4
|
+
JOB_ID — unique job identifier
|
|
5
|
+
S3_BUCKET — bucket for input/output files
|
|
6
|
+
INPUT_KEY — S3 key of the input file (video/image)
|
|
7
|
+
CALLBACK_URL — backend URL to POST result to
|
|
8
|
+
CALLBACK_SECRET — shared secret for the callback
|
|
9
|
+
|
|
10
|
+
The container exits 0 on success, non-zero on failure.
|
|
11
|
+
The backend marks the job completed/failed via the callback.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import urllib.request
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import boto3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _env(name: str, required: bool = True, default: str = "") -> str:
|
|
26
|
+
val = os.environ.get(name, default).strip()
|
|
27
|
+
if required and not val:
|
|
28
|
+
raise RuntimeError(f"Missing required env var: {name}")
|
|
29
|
+
return val
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _download_s3(bucket: str, key: str, dest: Path) -> None:
|
|
33
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
print(f"[batch] download s3://{bucket}/{key} → {dest}")
|
|
35
|
+
boto3.client("s3").download_file(bucket, key, str(dest))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _upload_s3(bucket: str, key: str, path: Path, content_type: str) -> None:
|
|
39
|
+
print(f"[batch] upload {path} → s3://{bucket}/{key}")
|
|
40
|
+
boto3.client("s3").upload_file(
|
|
41
|
+
str(path), bucket, key,
|
|
42
|
+
ExtraArgs={"ContentType": content_type},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _callback(callback_url: str, job_id: str, secret: str, *,
|
|
47
|
+
status: str, output_key: str | None = None,
|
|
48
|
+
error: str | None = None) -> None:
|
|
49
|
+
url = f"{callback_url.rstrip('/')}/api/v1/motion/jobs/{job_id}/callback"
|
|
50
|
+
payload: dict = {"status": status, "callback_secret": secret}
|
|
51
|
+
if output_key:
|
|
52
|
+
payload["output_video_key"] = output_key
|
|
53
|
+
if error:
|
|
54
|
+
payload["error_message"] = error
|
|
55
|
+
req = urllib.request.Request(
|
|
56
|
+
url, data=json.dumps(payload).encode(),
|
|
57
|
+
headers={"Content-Type": "application/json"}, method="POST",
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
with urllib.request.urlopen(req, timeout=120) as r:
|
|
61
|
+
print(f"[batch] callback HTTP {r.status}")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"[batch] callback failed: {e}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ext_for(path_str: str) -> str:
|
|
67
|
+
ext = Path(path_str).suffix
|
|
68
|
+
return ext if ext else ".mp4"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _content_type(ext: str) -> str:
|
|
72
|
+
return {
|
|
73
|
+
".mp4": "video/mp4",
|
|
74
|
+
".glb": "model/gltf-binary",
|
|
75
|
+
".obj": "text/plain",
|
|
76
|
+
".png": "image/png",
|
|
77
|
+
".jpg": "image/jpeg",
|
|
78
|
+
".json": "application/json",
|
|
79
|
+
}.get(ext.lower(), "application/octet-stream")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> int:
|
|
83
|
+
job_id = _env("JOB_ID")
|
|
84
|
+
bucket = _env("S3_BUCKET")
|
|
85
|
+
input_key = _env("INPUT_KEY")
|
|
86
|
+
callback_url = _env("CALLBACK_URL")
|
|
87
|
+
callback_secret = _env("CALLBACK_SECRET")
|
|
88
|
+
|
|
89
|
+
print(f"[batch] job={job_id} model={os.environ.get('MOTION_NAME','?')} input={input_key}")
|
|
90
|
+
|
|
91
|
+
work = Path(tempfile.mkdtemp(prefix=f"motion-{job_id[:8]}-"))
|
|
92
|
+
input_ext = _ext_for(input_key)
|
|
93
|
+
input_path = work / f"input{input_ext}"
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# 1. Pull weights declared in motion.yml (skipped if already present)
|
|
97
|
+
from .server import _fetch_weights, _load_predictor
|
|
98
|
+
_fetch_weights()
|
|
99
|
+
|
|
100
|
+
# 2. Load predictor and call setup() once
|
|
101
|
+
predictor = _load_predictor()
|
|
102
|
+
predictor.setup()
|
|
103
|
+
|
|
104
|
+
# 3. Download input from S3
|
|
105
|
+
_download_s3(bucket, input_key, input_path)
|
|
106
|
+
|
|
107
|
+
# 4. Run predict() — pass input as a Path
|
|
108
|
+
import inspect
|
|
109
|
+
sig = inspect.signature(predictor.predict)
|
|
110
|
+
first_param = next(iter(sig.parameters.values()))
|
|
111
|
+
|
|
112
|
+
# Coerce input to the annotated type (Path or str)
|
|
113
|
+
annotation = first_param.annotation
|
|
114
|
+
if annotation is Path or str(annotation) == "<class 'pathlib.Path'>":
|
|
115
|
+
input_arg = input_path
|
|
116
|
+
else:
|
|
117
|
+
input_arg = str(input_path)
|
|
118
|
+
|
|
119
|
+
print(f"[batch] running predict({first_param.name}={input_path})")
|
|
120
|
+
result = predictor.predict(**{first_param.name: input_arg})
|
|
121
|
+
|
|
122
|
+
# 5. Upload output to S3
|
|
123
|
+
output_path = Path(str(result))
|
|
124
|
+
if not output_path.exists():
|
|
125
|
+
raise FileNotFoundError(f"predict() did not produce output: {output_path}")
|
|
126
|
+
|
|
127
|
+
output_ext = output_path.suffix or ".mp4"
|
|
128
|
+
output_key = f"outputs/{job_id}/output{output_ext}"
|
|
129
|
+
_upload_s3(bucket, output_key, output_path, _content_type(output_ext))
|
|
130
|
+
|
|
131
|
+
# 6. Callback → backend marks job completed
|
|
132
|
+
_callback(callback_url, job_id, callback_secret,
|
|
133
|
+
status="completed", output_key=output_key)
|
|
134
|
+
print(f"[batch] done output=s3://{bucket}/{output_key}")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
print(f"[batch] FAILED: {exc}", file=sys.stderr)
|
|
139
|
+
import traceback; traceback.print_exc()
|
|
140
|
+
_callback(callback_url, job_id, callback_secret,
|
|
141
|
+
status="failed", error=str(exc))
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
finally:
|
|
145
|
+
for p in work.glob("*"):
|
|
146
|
+
try: p.unlink()
|
|
147
|
+
except OSError: pass
|
|
148
|
+
try: work.rmdir()
|
|
149
|
+
except OSError: pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Static category_code → category_id map (prod catalog).
|
|
2
|
+
|
|
3
|
+
Used by `motion deploy` until the platform exposes a category lookup API.
|
|
4
|
+
Refresh when categories are added in meta.model_category.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# From GET /api/v1/motion/models (2026-06)
|
|
11
|
+
CATEGORY_CODE_TO_ID: dict[str, int] = {
|
|
12
|
+
"skeleton": 1,
|
|
13
|
+
"capsule": 2,
|
|
14
|
+
"mesh": 3,
|
|
15
|
+
"hand-tracking": 4,
|
|
16
|
+
"3d-object": 5,
|
|
17
|
+
"vlm": 6,
|
|
18
|
+
"vla": 7,
|
|
19
|
+
"world": 8,
|
|
20
|
+
"annotation": 9,
|
|
21
|
+
"reconstruction": 10,
|
|
22
|
+
"spatial-perception": 11,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_category_id(
|
|
27
|
+
*,
|
|
28
|
+
category_id: Optional[int],
|
|
29
|
+
category_code: str,
|
|
30
|
+
) -> Optional[int]:
|
|
31
|
+
"""Return category_id from motion.yml `category_id` or mapped `category` code."""
|
|
32
|
+
if category_id is not None:
|
|
33
|
+
return int(category_id)
|
|
34
|
+
code = (category_code or "").strip().lower()
|
|
35
|
+
if not code:
|
|
36
|
+
return None
|
|
37
|
+
if code.isdigit():
|
|
38
|
+
return int(code)
|
|
39
|
+
mapped = CATEGORY_CODE_TO_ID.get(code)
|
|
40
|
+
if mapped is None:
|
|
41
|
+
known = ", ".join(sorted(CATEGORY_CODE_TO_ID))
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Unknown category '{category_code}'. "
|
|
44
|
+
f"Use category_id or one of: {known}"
|
|
45
|
+
)
|
|
46
|
+
return mapped
|