vlalab 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.
vlalab/schema/run.py ADDED
@@ -0,0 +1,162 @@
1
+ """
2
+ VLA-Lab Run Schema
3
+
4
+ Defines the metadata structure for a deployment run.
5
+ """
6
+
7
+ from dataclasses import dataclass, field, asdict
8
+ from typing import List, Optional, Dict, Any
9
+ from datetime import datetime
10
+ import json
11
+
12
+
13
+ @dataclass
14
+ class CameraConfig:
15
+ """Camera configuration."""
16
+ name: str
17
+ resolution: Optional[List[int]] = None # [width, height]
18
+ fps: Optional[float] = None
19
+ camera_type: Optional[str] = None # "realsense", "usb", etc.
20
+ serial_number: Optional[str] = None
21
+
22
+ def to_dict(self) -> Dict[str, Any]:
23
+ d = {"name": self.name}
24
+ if self.resolution is not None:
25
+ d["resolution"] = self.resolution
26
+ if self.fps is not None:
27
+ d["fps"] = self.fps
28
+ if self.camera_type is not None:
29
+ d["camera_type"] = self.camera_type
30
+ if self.serial_number is not None:
31
+ d["serial_number"] = self.serial_number
32
+ return d
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: Dict[str, Any]) -> "CameraConfig":
36
+ return cls(**data)
37
+
38
+
39
+ @dataclass
40
+ class RunMeta:
41
+ """Metadata for a deployment run."""
42
+ # Required fields
43
+ run_name: str
44
+ start_time: str # ISO format timestamp
45
+
46
+ # Model info
47
+ model_name: str = "unknown"
48
+ model_path: Optional[str] = None
49
+ model_type: Optional[str] = None # "diffusion_policy", "groot", etc.
50
+
51
+ # Task info
52
+ task_name: str = "unknown"
53
+ task_prompt: Optional[str] = None
54
+
55
+ # Robot info
56
+ robot_name: str = "unknown"
57
+ robot_type: Optional[str] = None # "franka", "ur5", etc.
58
+
59
+ # Camera info
60
+ cameras: List[CameraConfig] = field(default_factory=list)
61
+
62
+ # Inference config
63
+ inference_freq: Optional[float] = None # Hz
64
+ action_dim: Optional[int] = None
65
+ action_horizon: Optional[int] = None
66
+
67
+ # Deployment info
68
+ server_config: Dict[str, Any] = field(default_factory=dict)
69
+ client_config: Dict[str, Any] = field(default_factory=dict)
70
+
71
+ # Statistics (updated during/after run)
72
+ end_time: Optional[str] = None
73
+ total_steps: int = 0
74
+ total_duration_s: Optional[float] = None
75
+
76
+ # Version info
77
+ vlalab_version: str = "0.1.0"
78
+ framework_version: Optional[str] = None
79
+
80
+ # Extra fields
81
+ extra: Dict[str, Any] = field(default_factory=dict)
82
+
83
+ def to_dict(self) -> Dict[str, Any]:
84
+ d = {
85
+ "run_name": self.run_name,
86
+ "start_time": self.start_time,
87
+ "model_name": self.model_name,
88
+ "task_name": self.task_name,
89
+ "robot_name": self.robot_name,
90
+ "cameras": [cam.to_dict() for cam in self.cameras],
91
+ "total_steps": self.total_steps,
92
+ "vlalab_version": self.vlalab_version,
93
+ }
94
+
95
+ # Add optional fields if set
96
+ optional_fields = [
97
+ "model_path", "model_type", "task_prompt", "robot_type",
98
+ "inference_freq", "action_dim", "action_horizon",
99
+ "end_time", "total_duration_s", "framework_version",
100
+ ]
101
+ for field_name in optional_fields:
102
+ value = getattr(self, field_name)
103
+ if value is not None:
104
+ d[field_name] = value
105
+
106
+ if self.server_config:
107
+ d["server_config"] = self.server_config
108
+ if self.client_config:
109
+ d["client_config"] = self.client_config
110
+ if self.extra:
111
+ d["extra"] = self.extra
112
+
113
+ return d
114
+
115
+ def to_json(self, indent: int = 2) -> str:
116
+ return json.dumps(self.to_dict(), indent=indent)
117
+
118
+ def save(self, path: str):
119
+ """Save metadata to JSON file."""
120
+ with open(path, "w") as f:
121
+ f.write(self.to_json())
122
+
123
+ @classmethod
124
+ def from_dict(cls, data: Dict[str, Any]) -> "RunMeta":
125
+ cameras = [
126
+ CameraConfig.from_dict(cam) if isinstance(cam, dict) else cam
127
+ for cam in data.get("cameras", [])
128
+ ]
129
+
130
+ return cls(
131
+ run_name=data["run_name"],
132
+ start_time=data["start_time"],
133
+ model_name=data.get("model_name", "unknown"),
134
+ model_path=data.get("model_path"),
135
+ model_type=data.get("model_type"),
136
+ task_name=data.get("task_name", "unknown"),
137
+ task_prompt=data.get("task_prompt"),
138
+ robot_name=data.get("robot_name", "unknown"),
139
+ robot_type=data.get("robot_type"),
140
+ cameras=cameras,
141
+ inference_freq=data.get("inference_freq"),
142
+ action_dim=data.get("action_dim"),
143
+ action_horizon=data.get("action_horizon"),
144
+ server_config=data.get("server_config", {}),
145
+ client_config=data.get("client_config", {}),
146
+ end_time=data.get("end_time"),
147
+ total_steps=data.get("total_steps", 0),
148
+ total_duration_s=data.get("total_duration_s"),
149
+ vlalab_version=data.get("vlalab_version", "0.1.0"),
150
+ framework_version=data.get("framework_version"),
151
+ extra=data.get("extra", {}),
152
+ )
153
+
154
+ @classmethod
155
+ def from_json(cls, json_str: str) -> "RunMeta":
156
+ return cls.from_dict(json.loads(json_str))
157
+
158
+ @classmethod
159
+ def load(cls, path: str) -> "RunMeta":
160
+ """Load metadata from JSON file."""
161
+ with open(path, "r") as f:
162
+ return cls.from_json(f.read())
vlalab/schema/step.py ADDED
@@ -0,0 +1,177 @@
1
+ """
2
+ VLA-Lab Step Schema
3
+
4
+ Defines the data structure for a single inference step.
5
+ """
6
+
7
+ from dataclasses import dataclass, field, asdict
8
+ from typing import List, Optional, Dict, Any, Union
9
+ from datetime import datetime
10
+ import json
11
+
12
+
13
+ @dataclass
14
+ class ImageRef:
15
+ """Reference to an image artifact file."""
16
+ path: str # Relative path from run_dir
17
+ camera_name: str = "default"
18
+ shape: Optional[List[int]] = None # [H, W, C]
19
+ encoding: str = "jpeg"
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ return asdict(self)
23
+
24
+ @classmethod
25
+ def from_dict(cls, data: Dict[str, Any]) -> "ImageRef":
26
+ return cls(**data)
27
+
28
+
29
+ @dataclass
30
+ class ObsData:
31
+ """Observation data for a step."""
32
+ state: List[float] = field(default_factory=list) # Low-dim state (pose, gripper, etc.)
33
+ images: List[ImageRef] = field(default_factory=list) # Image references
34
+
35
+ # Optional detailed state breakdown
36
+ pose: Optional[List[float]] = None # [x, y, z, qx, qy, qz, qw] or [x, y, z, qw, qx, qy, qz]
37
+ gripper: Optional[float] = None
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ d = {
41
+ "state": self.state,
42
+ "images": [img.to_dict() for img in self.images],
43
+ }
44
+ if self.pose is not None:
45
+ d["pose"] = self.pose
46
+ if self.gripper is not None:
47
+ d["gripper"] = self.gripper
48
+ return d
49
+
50
+ @classmethod
51
+ def from_dict(cls, data: Dict[str, Any]) -> "ObsData":
52
+ images = [ImageRef.from_dict(img) for img in data.get("images", [])]
53
+ return cls(
54
+ state=data.get("state", []),
55
+ images=images,
56
+ pose=data.get("pose"),
57
+ gripper=data.get("gripper"),
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class ActionData:
63
+ """Action data for a step."""
64
+ values: List[List[float]] = field(default_factory=list) # Action chunk: [[a1], [a2], ...]
65
+
66
+ # Optional metadata
67
+ action_dim: Optional[int] = None
68
+ chunk_size: Optional[int] = None
69
+
70
+ def to_dict(self) -> Dict[str, Any]:
71
+ d = {"values": self.values}
72
+ if self.action_dim is not None:
73
+ d["action_dim"] = self.action_dim
74
+ if self.chunk_size is not None:
75
+ d["chunk_size"] = self.chunk_size
76
+ return d
77
+
78
+ @classmethod
79
+ def from_dict(cls, data: Dict[str, Any]) -> "ActionData":
80
+ return cls(
81
+ values=data.get("values", []),
82
+ action_dim=data.get("action_dim"),
83
+ chunk_size=data.get("chunk_size"),
84
+ )
85
+
86
+
87
+ @dataclass
88
+ class TimingData:
89
+ """Timing data for a step (all times in milliseconds or Unix timestamps)."""
90
+ # Timestamps (Unix time, float)
91
+ client_send: Optional[float] = None
92
+ server_recv: Optional[float] = None
93
+ infer_start: Optional[float] = None
94
+ infer_end: Optional[float] = None
95
+ send_timestamp: Optional[float] = None
96
+
97
+ # Computed latencies (milliseconds)
98
+ transport_latency_ms: Optional[float] = None
99
+ inference_latency_ms: Optional[float] = None
100
+ total_latency_ms: Optional[float] = None
101
+ message_interval_ms: Optional[float] = None
102
+
103
+ def to_dict(self) -> Dict[str, Any]:
104
+ d = {}
105
+ for k, v in asdict(self).items():
106
+ if v is not None:
107
+ d[k] = v
108
+ return d
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: Dict[str, Any]) -> "TimingData":
112
+ return cls(
113
+ client_send=data.get("client_send"),
114
+ server_recv=data.get("server_recv"),
115
+ infer_start=data.get("infer_start"),
116
+ infer_end=data.get("infer_end"),
117
+ send_timestamp=data.get("send_timestamp"),
118
+ transport_latency_ms=data.get("transport_latency_ms"),
119
+ inference_latency_ms=data.get("inference_latency_ms"),
120
+ total_latency_ms=data.get("total_latency_ms"),
121
+ message_interval_ms=data.get("message_interval_ms"),
122
+ )
123
+
124
+ def compute_latencies(self):
125
+ """Compute latency values from timestamps."""
126
+ if self.server_recv is not None and self.client_send is not None:
127
+ self.transport_latency_ms = (self.server_recv - self.client_send) * 1000
128
+
129
+ if self.infer_end is not None and self.infer_start is not None:
130
+ self.inference_latency_ms = (self.infer_end - self.infer_start) * 1000
131
+
132
+ if self.send_timestamp is not None and self.client_send is not None:
133
+ self.total_latency_ms = (self.send_timestamp - self.client_send) * 1000
134
+
135
+
136
+ @dataclass
137
+ class StepRecord:
138
+ """A single step record in the inference log."""
139
+ step_idx: int
140
+ obs: ObsData = field(default_factory=ObsData)
141
+ action: ActionData = field(default_factory=ActionData)
142
+ timing: TimingData = field(default_factory=TimingData)
143
+
144
+ # Optional fields
145
+ tags: Dict[str, Any] = field(default_factory=dict)
146
+ prompt: Optional[str] = None # For language-conditioned models
147
+
148
+ def to_dict(self) -> Dict[str, Any]:
149
+ d = {
150
+ "step_idx": self.step_idx,
151
+ "obs": self.obs.to_dict(),
152
+ "action": self.action.to_dict(),
153
+ "timing": self.timing.to_dict(),
154
+ }
155
+ if self.tags:
156
+ d["tags"] = self.tags
157
+ if self.prompt is not None:
158
+ d["prompt"] = self.prompt
159
+ return d
160
+
161
+ def to_json(self) -> str:
162
+ return json.dumps(self.to_dict())
163
+
164
+ @classmethod
165
+ def from_dict(cls, data: Dict[str, Any]) -> "StepRecord":
166
+ return cls(
167
+ step_idx=data["step_idx"],
168
+ obs=ObsData.from_dict(data.get("obs", {})),
169
+ action=ActionData.from_dict(data.get("action", {})),
170
+ timing=TimingData.from_dict(data.get("timing", {})),
171
+ tags=data.get("tags", {}),
172
+ prompt=data.get("prompt"),
173
+ )
174
+
175
+ @classmethod
176
+ def from_json(cls, json_str: str) -> "StepRecord":
177
+ return cls.from_dict(json.loads(json_str))
vlalab/viz/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ VLA-Lab Visualization Module
3
+
4
+ Provides visualization utilities including matplotlib font setup.
5
+ """
6
+
7
+ from vlalab.viz.mpl_fonts import setup_matplotlib_fonts, FontSetupResult
8
+
9
+ __all__ = ["setup_matplotlib_fonts", "FontSetupResult"]
@@ -0,0 +1,161 @@
1
+ """
2
+ Matplotlib 字体配置工具
3
+
4
+ 目标:
5
+ - 自动选择可用的中文字体(优先 WenQuanYi / Noto / Source Han)
6
+ - 必要时通过字体文件路径 addfont(绕过 matplotlib 缓存导致的"明明装了字体却找不到")
7
+ - 抑制常见的 Glyph missing / findfont 噪音警告
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Iterable, Optional
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class FontSetupResult:
18
+ chosen_font: Optional[str]
19
+ available_chinese_like_fonts: list[str]
20
+
21
+
22
+ def _unique_preserve_order(items: Iterable[str]) -> list[str]:
23
+ seen: set[str] = set()
24
+ out: list[str] = []
25
+ for x in items:
26
+ if x and x not in seen:
27
+ out.append(x)
28
+ seen.add(x)
29
+ return out
30
+
31
+
32
+ def setup_matplotlib_fonts(verbose: bool = True) -> FontSetupResult:
33
+ """
34
+ 配置 matplotlib 中文字体。
35
+
36
+ 返回:
37
+ - chosen_font: 选中的字体名称;若为 None 表示未找到合适中文字体
38
+ - available_chinese_like_fonts: 检测到的"疑似中文字体"列表(用于诊断)
39
+ """
40
+ import os
41
+ import warnings
42
+
43
+ import matplotlib as mpl
44
+ import matplotlib.font_manager as fm
45
+
46
+ # 抑制字体相关噪音 warning(不影响实际渲染)
47
+ warnings.filterwarnings("ignore", category=UserWarning, message=".*Glyph.*missing.*")
48
+ warnings.filterwarnings("ignore", category=UserWarning, message="findfont: Font family.*")
49
+ warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib.font_manager")
50
+
51
+ # 常见中文字体文件路径(存在则 addfont,避免缓存/扫描问题)
52
+ font_paths = [
53
+ # WenQuanYi
54
+ "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
55
+ "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
56
+ # Noto CJK(不同发行版路径/后缀会不同,尽量覆盖)
57
+ "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
58
+ "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttf",
59
+ "/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf",
60
+ "/usr/share/fonts/opentype/noto/NotoSansCJKtc-Regular.otf",
61
+ "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
62
+ "/usr/share/fonts/truetype/noto/NotoSansCJKsc-Regular.otf",
63
+ "/usr/share/fonts/truetype/noto/NotoSansCJKtc-Regular.otf",
64
+ # Source Han
65
+ "/usr/share/fonts/opentype/source-han-sans/SourceHanSansCN-Regular.otf",
66
+ "/usr/share/fonts/opentype/source-han-sans/SourceHanSansSC-Regular.otf",
67
+ "/usr/share/fonts/opentype/source-han-sans/SourceHanSansTC-Regular.otf",
68
+ ]
69
+
70
+ for p in font_paths:
71
+ if os.path.exists(p):
72
+ try:
73
+ fm.fontManager.addfont(p)
74
+ except Exception:
75
+ # addfont 失败不应中断主流程
76
+ pass
77
+
78
+ # 重新获取字体列表(包含 addfont 后的新字体)
79
+ all_fonts = [f.name for f in fm.fontManager.ttflist]
80
+ all_fonts_lower = [n.lower() for n in all_fonts]
81
+
82
+ # 首选字体名称(按优先级)
83
+ preferred_names = [
84
+ "WenQuanYi Micro Hei",
85
+ "WenQuanYi Zen Hei",
86
+ "Noto Sans CJK SC",
87
+ "Noto Sans CJK TC",
88
+ "Source Han Sans CN",
89
+ "Source Han Sans SC",
90
+ "Source Han Sans TC",
91
+ # Windows/macOS 常见
92
+ "SimHei",
93
+ "Microsoft YaHei",
94
+ "PingFang SC",
95
+ "STHeiti",
96
+ ]
97
+
98
+ chosen: Optional[str] = None
99
+
100
+ # 1) 先严格按名称匹配
101
+ available_set = set(all_fonts)
102
+ for name in preferred_names:
103
+ if name in available_set:
104
+ chosen = name
105
+ break
106
+
107
+ # 2) 再做一次"模糊匹配"(不同系统字体命名略有差异)
108
+ if chosen is None:
109
+ preferred_keywords = [
110
+ "wenquanyi",
111
+ "wqy",
112
+ "noto sans cjk",
113
+ "noto cjk",
114
+ "source han sans",
115
+ "simhei",
116
+ "yahei",
117
+ "pingfang",
118
+ "stheiti",
119
+ "cjk",
120
+ ]
121
+ for kw in preferred_keywords:
122
+ for i, name_l in enumerate(all_fonts_lower):
123
+ if kw in name_l:
124
+ chosen = all_fonts[i]
125
+ break
126
+ if chosen is not None:
127
+ break
128
+
129
+ # 诊断信息:列出"疑似中文字体"
130
+ chinese_like = []
131
+ chinese_keywords = [
132
+ "wenquanyi",
133
+ "wqy",
134
+ "noto",
135
+ "cjk",
136
+ "source han",
137
+ "simhei",
138
+ "yahei",
139
+ "pingfang",
140
+ "heiti",
141
+ ]
142
+ for i, name_l in enumerate(all_fonts_lower):
143
+ if any(k in name_l for k in chinese_keywords):
144
+ chinese_like.append(all_fonts[i])
145
+ chinese_like = sorted(set(chinese_like))
146
+
147
+ if chosen is not None:
148
+ # 注意:不要覆盖掉用户可能已有的 font.sans-serif 配置,采用"前置 + 去重"
149
+ current = list(mpl.rcParams.get("font.sans-serif", []))
150
+ mpl.rcParams["font.family"] = "sans-serif"
151
+ mpl.rcParams["font.sans-serif"] = _unique_preserve_order([chosen, *current, "DejaVu Sans"])
152
+ mpl.rcParams["axes.unicode_minus"] = False
153
+ if verbose:
154
+ print(f"[字体] 使用字体: {chosen}")
155
+ else:
156
+ # 没找到中文字体也不报错,只提示(图里中文会是方块)
157
+ mpl.rcParams["axes.unicode_minus"] = False
158
+ if verbose:
159
+ print("[字体] 警告: 未找到可用中文字体,中文可能显示为方块")
160
+
161
+ return FontSetupResult(chosen_font=chosen, available_chinese_like_fonts=chinese_like)