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.
@@ -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