x4d-devkit 0.1.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.
- x4d_devkit/__init__.py +42 -0
- x4d_devkit/cli.py +131 -0
- x4d_devkit/client/__init__.py +3 -0
- x4d_devkit/client/client.py +56 -0
- x4d_devkit/client/clips.py +135 -0
- x4d_devkit/client/projects.py +25 -0
- x4d_devkit/converters/__init__.py +1 -0
- x4d_devkit/converters/nuscenes.py +715 -0
- x4d_devkit/core/__init__.py +28 -0
- x4d_devkit/core/loader.py +259 -0
- x4d_devkit/core/models.py +106 -0
- x4d_devkit/core/token.py +26 -0
- x4d_devkit/core/transform.py +159 -0
- x4d_devkit/eval/__init__.py +17 -0
- x4d_devkit/eval/detection.py +350 -0
- x4d_devkit/eval/matching.py +184 -0
- x4d_devkit/eval/metrics.py +83 -0
- x4d_devkit/validation/__init__.py +4 -0
- x4d_devkit/validation/report.py +46 -0
- x4d_devkit/validation/validator.py +236 -0
- x4d_devkit-0.1.0.dist-info/METADATA +117 -0
- x4d_devkit-0.1.0.dist-info/RECORD +26 -0
- x4d_devkit-0.1.0.dist-info/WHEEL +5 -0
- x4d_devkit-0.1.0.dist-info/entry_points.txt +2 -0
- x4d_devkit-0.1.0.dist-info/licenses/LICENSE +190 -0
- x4d_devkit-0.1.0.dist-info/top_level.txt +1 -0
x4d_devkit/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""X-4D dataset format SDK."""
|
|
2
|
+
|
|
3
|
+
from x4d_devkit.core import (
|
|
4
|
+
Annotation,
|
|
5
|
+
CalibratedSensor,
|
|
6
|
+
ClipLoader,
|
|
7
|
+
ClipMeta,
|
|
8
|
+
EgoPose,
|
|
9
|
+
Instance,
|
|
10
|
+
Sample,
|
|
11
|
+
SampleData,
|
|
12
|
+
Transform,
|
|
13
|
+
TransformChain,
|
|
14
|
+
generate_token,
|
|
15
|
+
)
|
|
16
|
+
from x4d_devkit.eval import (
|
|
17
|
+
DetectionConfig,
|
|
18
|
+
DetectionEval,
|
|
19
|
+
DetectionResult,
|
|
20
|
+
nusc_detection_config,
|
|
21
|
+
)
|
|
22
|
+
from x4d_devkit.validation import validate_clip, ValidationReport
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Annotation",
|
|
26
|
+
"CalibratedSensor",
|
|
27
|
+
"ClipLoader",
|
|
28
|
+
"ClipMeta",
|
|
29
|
+
"DetectionConfig",
|
|
30
|
+
"DetectionEval",
|
|
31
|
+
"DetectionResult",
|
|
32
|
+
"EgoPose",
|
|
33
|
+
"Instance",
|
|
34
|
+
"Sample",
|
|
35
|
+
"SampleData",
|
|
36
|
+
"Transform",
|
|
37
|
+
"TransformChain",
|
|
38
|
+
"ValidationReport",
|
|
39
|
+
"generate_token",
|
|
40
|
+
"nusc_detection_config",
|
|
41
|
+
"validate_clip",
|
|
42
|
+
]
|
x4d_devkit/cli.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""X-4D DevKit CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_projects_list(args):
|
|
10
|
+
from x4d_devkit.client import X4DClient
|
|
11
|
+
|
|
12
|
+
with X4DClient(base_url=args.base_url) as client:
|
|
13
|
+
resp = client.projects.list()
|
|
14
|
+
for p in resp["items"]:
|
|
15
|
+
print(f" [{p['id']}] {p['name']} — {p.get('clip_count', '?')} clips")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cmd_clips_list(args):
|
|
19
|
+
from x4d_devkit.client import X4DClient
|
|
20
|
+
|
|
21
|
+
with X4DClient(base_url=args.base_url) as client:
|
|
22
|
+
resp = client.clips.list(
|
|
23
|
+
project_id=args.project_id,
|
|
24
|
+
page_size=args.page_size,
|
|
25
|
+
lifecycle=args.lifecycle,
|
|
26
|
+
has_archive=args.has_archive,
|
|
27
|
+
)
|
|
28
|
+
print(f"Total: {resp['total']} clips (showing page {resp['page']})")
|
|
29
|
+
for c in resp["items"]:
|
|
30
|
+
status = c.get("lifecycle", "?")
|
|
31
|
+
kf = (
|
|
32
|
+
c.get("statistics", {}).get("keyframe_count", "?")
|
|
33
|
+
if c.get("statistics")
|
|
34
|
+
else "?"
|
|
35
|
+
)
|
|
36
|
+
print(f" {c['clip_id']} [{status}] {kf} keyframes")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_clips_download(args):
|
|
40
|
+
from x4d_devkit.client import X4DClient
|
|
41
|
+
|
|
42
|
+
with X4DClient(base_url=args.base_url, timeout=600.0) as client:
|
|
43
|
+
# Read clip IDs from file or comma-separated
|
|
44
|
+
if Path(args.clip_ids).exists():
|
|
45
|
+
clip_ids = Path(args.clip_ids).read_text().strip().splitlines()
|
|
46
|
+
else:
|
|
47
|
+
clip_ids = args.clip_ids.split(",")
|
|
48
|
+
clip_ids = [cid.strip() for cid in clip_ids if cid.strip()]
|
|
49
|
+
|
|
50
|
+
print(f"Downloading {len(clip_ids)} clips to {args.output}...")
|
|
51
|
+
|
|
52
|
+
def on_progress(clip_id, i, total):
|
|
53
|
+
print(f" [{i}/{total}] {clip_id} done")
|
|
54
|
+
|
|
55
|
+
paths = client.clips.download_tars(
|
|
56
|
+
clip_ids=clip_ids,
|
|
57
|
+
output_dir=args.output,
|
|
58
|
+
workers=args.workers,
|
|
59
|
+
on_progress=on_progress,
|
|
60
|
+
)
|
|
61
|
+
print(f"Downloaded {len(paths)} clips successfully")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_validate(args):
|
|
65
|
+
from x4d_devkit.validation import validate_clip
|
|
66
|
+
|
|
67
|
+
target = Path(args.path)
|
|
68
|
+
|
|
69
|
+
if (target / "meta.json").exists():
|
|
70
|
+
# Single clip directory
|
|
71
|
+
report = validate_clip(target)
|
|
72
|
+
print(report)
|
|
73
|
+
sys.exit(0 if report.ok else 1)
|
|
74
|
+
else:
|
|
75
|
+
# Directory of clips
|
|
76
|
+
all_ok = True
|
|
77
|
+
for clip_dir in sorted(target.iterdir()):
|
|
78
|
+
if clip_dir.is_dir() and (clip_dir / "meta.json").exists():
|
|
79
|
+
report = validate_clip(clip_dir)
|
|
80
|
+
print(report)
|
|
81
|
+
if not report.ok:
|
|
82
|
+
all_ok = False
|
|
83
|
+
sys.exit(0 if all_ok else 1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main():
|
|
87
|
+
parser = argparse.ArgumentParser(prog="x4d", description="X-4D DevKit CLI")
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--base-url", default="http://localhost:8000", help="Platform API URL"
|
|
90
|
+
)
|
|
91
|
+
sub = parser.add_subparsers(dest="command")
|
|
92
|
+
|
|
93
|
+
# projects list
|
|
94
|
+
p_proj = sub.add_parser("projects", help="Project operations")
|
|
95
|
+
p_proj_sub = p_proj.add_subparsers(dest="subcommand")
|
|
96
|
+
p_proj_list = p_proj_sub.add_parser("list", help="List projects")
|
|
97
|
+
p_proj_list.set_defaults(func=cmd_projects_list)
|
|
98
|
+
|
|
99
|
+
# clips list
|
|
100
|
+
p_clips = sub.add_parser("clips", help="Clip operations")
|
|
101
|
+
p_clips_sub = p_clips.add_subparsers(dest="subcommand")
|
|
102
|
+
|
|
103
|
+
p_clips_list = p_clips_sub.add_parser("list", help="List clips")
|
|
104
|
+
p_clips_list.add_argument("--project-id", type=int, required=True)
|
|
105
|
+
p_clips_list.add_argument("--page-size", type=int, default=20)
|
|
106
|
+
p_clips_list.add_argument("--lifecycle", type=str, default=None)
|
|
107
|
+
p_clips_list.add_argument("--has-archive", type=bool, default=None)
|
|
108
|
+
p_clips_list.set_defaults(func=cmd_clips_list)
|
|
109
|
+
|
|
110
|
+
p_clips_dl = p_clips_sub.add_parser("download", help="Download clip tar archives")
|
|
111
|
+
p_clips_dl.add_argument(
|
|
112
|
+
"--clip-ids", required=True, help="Comma-separated IDs or path to file"
|
|
113
|
+
)
|
|
114
|
+
p_clips_dl.add_argument("--output", required=True, help="Output directory")
|
|
115
|
+
p_clips_dl.add_argument("--workers", type=int, default=4)
|
|
116
|
+
p_clips_dl.set_defaults(func=cmd_clips_download)
|
|
117
|
+
|
|
118
|
+
# validate
|
|
119
|
+
p_val = sub.add_parser("validate", help="Validate clip directory")
|
|
120
|
+
p_val.add_argument("path", help="Clip directory or parent directory")
|
|
121
|
+
p_val.set_defaults(func=cmd_validate)
|
|
122
|
+
|
|
123
|
+
args = parser.parse_args()
|
|
124
|
+
if not hasattr(args, "func"):
|
|
125
|
+
parser.print_help()
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
args.func(args)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
main()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""X-4D platform API client."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from x4d_devkit.client.projects import ProjectsAPI
|
|
7
|
+
from x4d_devkit.client.clips import ClipsAPI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class X4DClient:
|
|
11
|
+
"""Client for the X-4D platform REST API.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
base_url: Platform API base URL (e.g. "http://localhost:8000")
|
|
15
|
+
api_key: Optional API key for authentication (future use)
|
|
16
|
+
timeout: Request timeout in seconds
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base_url: str = "http://localhost:8000",
|
|
22
|
+
api_key: str | None = None,
|
|
23
|
+
timeout: float = 30.0,
|
|
24
|
+
):
|
|
25
|
+
self._base_url = base_url.rstrip("/")
|
|
26
|
+
self._api_prefix = "/api/v1"
|
|
27
|
+
headers = {}
|
|
28
|
+
if api_key:
|
|
29
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
30
|
+
self._http = httpx.Client(
|
|
31
|
+
base_url=self._base_url, headers=headers, timeout=timeout
|
|
32
|
+
)
|
|
33
|
+
self.projects = ProjectsAPI(self)
|
|
34
|
+
self.clips = ClipsAPI(self)
|
|
35
|
+
|
|
36
|
+
def _url(self, path: str) -> str:
|
|
37
|
+
return f"{self._api_prefix}{path}"
|
|
38
|
+
|
|
39
|
+
def get(self, path: str, **kwargs) -> dict:
|
|
40
|
+
resp = self._http.get(self._url(path), **kwargs)
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
return resp.json()
|
|
43
|
+
|
|
44
|
+
def post(self, path: str, **kwargs) -> dict:
|
|
45
|
+
resp = self._http.post(self._url(path), **kwargs)
|
|
46
|
+
resp.raise_for_status()
|
|
47
|
+
return resp.json()
|
|
48
|
+
|
|
49
|
+
def close(self):
|
|
50
|
+
self._http.close()
|
|
51
|
+
|
|
52
|
+
def __enter__(self):
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, *args):
|
|
56
|
+
self.close()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Clips API wrapper."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tarfile
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from x4d_devkit.client.client import X4DClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClipsAPI:
|
|
14
|
+
def __init__(self, client: X4DClient):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list(
|
|
18
|
+
self,
|
|
19
|
+
project_id: int,
|
|
20
|
+
page: int = 1,
|
|
21
|
+
page_size: int = 100,
|
|
22
|
+
lifecycle: str | None = None,
|
|
23
|
+
has_archive: bool | None = None,
|
|
24
|
+
) -> dict:
|
|
25
|
+
"""List clips in a project with optional filters."""
|
|
26
|
+
params: dict = {
|
|
27
|
+
"project_id": project_id,
|
|
28
|
+
"page": page,
|
|
29
|
+
"page_size": page_size,
|
|
30
|
+
}
|
|
31
|
+
if lifecycle:
|
|
32
|
+
params["lifecycle"] = lifecycle
|
|
33
|
+
if has_archive is not None:
|
|
34
|
+
params["has_archive"] = str(has_archive).lower()
|
|
35
|
+
return self._client.get("/clips", params=params)
|
|
36
|
+
|
|
37
|
+
def list_all(self, project_id: int, **kwargs) -> list[dict]:
|
|
38
|
+
"""List ALL clips in a project (handles pagination automatically)."""
|
|
39
|
+
all_items = []
|
|
40
|
+
page = 1
|
|
41
|
+
while True:
|
|
42
|
+
resp = self.list(project_id=project_id, page=page, page_size=100, **kwargs)
|
|
43
|
+
all_items.extend(resp["items"])
|
|
44
|
+
if len(all_items) >= resp["total"]:
|
|
45
|
+
break
|
|
46
|
+
page += 1
|
|
47
|
+
return all_items
|
|
48
|
+
|
|
49
|
+
def get(self, clip_id: str) -> dict:
|
|
50
|
+
"""Get a single clip by ID."""
|
|
51
|
+
return self._client.get(f"/clips/{clip_id}")
|
|
52
|
+
|
|
53
|
+
def get_archive_status(self, clip_id: str) -> dict:
|
|
54
|
+
"""Get archive/tar status for a clip."""
|
|
55
|
+
return self._client.get(f"/clips/{clip_id}/archive")
|
|
56
|
+
|
|
57
|
+
def get_archive_download_url(self, clip_id: str) -> str:
|
|
58
|
+
"""Get presigned download URL for the clip's tar archive."""
|
|
59
|
+
resp = self._client.get(f"/clips/{clip_id}/archive/download")
|
|
60
|
+
return resp["url"]
|
|
61
|
+
|
|
62
|
+
def download_tar(
|
|
63
|
+
self, clip_id: str, output_dir: str | Path, extract: bool = True
|
|
64
|
+
) -> Path:
|
|
65
|
+
"""Download and optionally extract a clip's tar archive.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
clip_id: Clip ID to download
|
|
69
|
+
output_dir: Directory to save/extract to
|
|
70
|
+
extract: If True, extract tar and remove the tar file
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Path to extracted clip directory (if extract=True) or tar file
|
|
74
|
+
"""
|
|
75
|
+
output_dir = Path(output_dir)
|
|
76
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
url = self.get_archive_download_url(clip_id)
|
|
79
|
+
tar_path = output_dir / f"{clip_id}.tar"
|
|
80
|
+
|
|
81
|
+
# Stream download
|
|
82
|
+
with self._client._http.stream("GET", url) as resp:
|
|
83
|
+
resp.raise_for_status()
|
|
84
|
+
with open(tar_path, "wb") as f:
|
|
85
|
+
for chunk in resp.iter_bytes(chunk_size=8 * 1024 * 1024):
|
|
86
|
+
f.write(chunk)
|
|
87
|
+
|
|
88
|
+
if extract:
|
|
89
|
+
clip_dir = output_dir / clip_id
|
|
90
|
+
clip_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
with tarfile.open(tar_path, "r") as tar:
|
|
92
|
+
tar.extractall(path=clip_dir)
|
|
93
|
+
tar_path.unlink()
|
|
94
|
+
return clip_dir
|
|
95
|
+
|
|
96
|
+
return tar_path
|
|
97
|
+
|
|
98
|
+
def download_tars(
|
|
99
|
+
self,
|
|
100
|
+
clip_ids: list[str],
|
|
101
|
+
output_dir: str | Path,
|
|
102
|
+
workers: int = 4,
|
|
103
|
+
extract: bool = True,
|
|
104
|
+
on_progress: callable | None = None,
|
|
105
|
+
) -> list[Path]:
|
|
106
|
+
"""Batch download multiple clip tar archives in parallel.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
clip_ids: List of clip IDs to download
|
|
110
|
+
output_dir: Directory to save/extract to
|
|
111
|
+
workers: Number of parallel download threads
|
|
112
|
+
extract: If True, extract tars and remove tar files
|
|
113
|
+
on_progress: Optional callback(clip_id, index, total) called after each download
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of paths to extracted clip directories or tar files
|
|
117
|
+
"""
|
|
118
|
+
results = []
|
|
119
|
+
|
|
120
|
+
def _download_one(clip_id: str) -> Path:
|
|
121
|
+
return self.download_tar(clip_id, output_dir, extract=extract)
|
|
122
|
+
|
|
123
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
124
|
+
futures = {pool.submit(_download_one, cid): cid for cid in clip_ids}
|
|
125
|
+
for i, future in enumerate(as_completed(futures)):
|
|
126
|
+
clip_id = futures[future]
|
|
127
|
+
try:
|
|
128
|
+
path = future.result()
|
|
129
|
+
results.append(path)
|
|
130
|
+
if on_progress:
|
|
131
|
+
on_progress(clip_id, i + 1, len(clip_ids))
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Failed to download {clip_id}: {e}")
|
|
134
|
+
|
|
135
|
+
return results
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Projects API wrapper."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from x4d_devkit.client.client import X4DClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectsAPI:
|
|
11
|
+
def __init__(self, client: X4DClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list(
|
|
15
|
+
self, page: int = 1, page_size: int = 50, search: str | None = None
|
|
16
|
+
) -> dict:
|
|
17
|
+
"""List all projects."""
|
|
18
|
+
params = {"page": page, "page_size": page_size}
|
|
19
|
+
if search:
|
|
20
|
+
params["search"] = search
|
|
21
|
+
return self._client.get("/projects", params=params)
|
|
22
|
+
|
|
23
|
+
def get(self, project_id: int) -> dict:
|
|
24
|
+
"""Get a project by ID."""
|
|
25
|
+
return self._client.get(f"/projects/{project_id}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dataset format converters."""
|