motion-intelligence 0.2.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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"
motion/batch.py ADDED
@@ -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())
motion/categories.py ADDED
@@ -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