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.
@@ -0,0 +1,114 @@
1
+ """
2
+ VLA-Lab JSONL Writer
3
+
4
+ Efficient append-only writer for step records.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Union, Dict, Any
10
+ import threading
11
+
12
+
13
+ class JsonlWriter:
14
+ """Thread-safe JSONL file writer for step records."""
15
+
16
+ def __init__(self, file_path: Union[str, Path]):
17
+ """
18
+ Initialize JSONL writer.
19
+
20
+ Args:
21
+ file_path: Path to the JSONL file
22
+ """
23
+ self.file_path = Path(file_path)
24
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
25
+ self._lock = threading.Lock()
26
+ self._file = None
27
+ self._line_count = 0
28
+
29
+ # Open file in append mode
30
+ self._file = open(self.file_path, "a", encoding="utf-8")
31
+
32
+ def write(self, record: Union[Dict[str, Any], "StepRecord"]) -> int:
33
+ """
34
+ Write a record to the JSONL file.
35
+
36
+ Args:
37
+ record: Dictionary or StepRecord to write
38
+
39
+ Returns:
40
+ Line number of the written record
41
+ """
42
+ if hasattr(record, "to_dict"):
43
+ record = record.to_dict()
44
+
45
+ json_str = json.dumps(record, ensure_ascii=False)
46
+
47
+ with self._lock:
48
+ self._file.write(json_str + "\n")
49
+ self._file.flush()
50
+ self._line_count += 1
51
+ return self._line_count - 1
52
+
53
+ def close(self):
54
+ """Close the file."""
55
+ if self._file is not None:
56
+ self._file.close()
57
+ self._file = None
58
+
59
+ @property
60
+ def line_count(self) -> int:
61
+ """Get the number of lines written."""
62
+ return self._line_count
63
+
64
+ def __enter__(self):
65
+ return self
66
+
67
+ def __exit__(self, exc_type, exc_val, exc_tb):
68
+ self.close()
69
+ return False
70
+
71
+
72
+ class JsonlReader:
73
+ """Reader for JSONL files."""
74
+
75
+ def __init__(self, file_path: Union[str, Path]):
76
+ """
77
+ Initialize JSONL reader.
78
+
79
+ Args:
80
+ file_path: Path to the JSONL file
81
+ """
82
+ self.file_path = Path(file_path)
83
+
84
+ if not self.file_path.exists():
85
+ raise FileNotFoundError(f"JSONL file not found: {self.file_path}")
86
+
87
+ def __iter__(self):
88
+ """Iterate over records in the file."""
89
+ with open(self.file_path, "r", encoding="utf-8") as f:
90
+ for line in f:
91
+ line = line.strip()
92
+ if line:
93
+ yield json.loads(line)
94
+
95
+ def read_all(self):
96
+ """Read all records into a list."""
97
+ return list(self)
98
+
99
+ def count(self) -> int:
100
+ """Count the number of records."""
101
+ count = 0
102
+ with open(self.file_path, "r", encoding="utf-8") as f:
103
+ for line in f:
104
+ if line.strip():
105
+ count += 1
106
+ return count
107
+
108
+ def read_line(self, line_idx: int) -> Dict[str, Any]:
109
+ """Read a specific line by index."""
110
+ with open(self.file_path, "r", encoding="utf-8") as f:
111
+ for i, line in enumerate(f):
112
+ if i == line_idx:
113
+ return json.loads(line.strip())
114
+ raise IndexError(f"Line {line_idx} not found")
@@ -0,0 +1,216 @@
1
+ """
2
+ VLA-Lab Run Loader
3
+
4
+ Load and access run data.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Dict, Any, List, Optional, Iterator
10
+ import numpy as np
11
+
12
+ from vlalab.schema.run import RunMeta
13
+ from vlalab.schema.step import StepRecord
14
+ from vlalab.logging.jsonl_writer import JsonlReader
15
+
16
+
17
+ def load_run_info(run_dir: Path) -> Dict[str, Any]:
18
+ """
19
+ Load basic run information.
20
+
21
+ Args:
22
+ run_dir: Path to run directory
23
+
24
+ Returns:
25
+ Dictionary with run info
26
+ """
27
+ run_dir = Path(run_dir)
28
+ meta_path = run_dir / "meta.json"
29
+ steps_path = run_dir / "steps.jsonl"
30
+
31
+ info = {
32
+ "run_dir": str(run_dir),
33
+ "run_name": run_dir.name,
34
+ }
35
+
36
+ if meta_path.exists():
37
+ with open(meta_path, "r") as f:
38
+ meta = json.load(f)
39
+ info.update({
40
+ "model_name": meta.get("model_name", "unknown"),
41
+ "task_name": meta.get("task_name", "unknown"),
42
+ "robot_name": meta.get("robot_name", "unknown"),
43
+ "start_time": meta.get("start_time", "unknown"),
44
+ "total_steps": meta.get("total_steps", 0),
45
+ })
46
+
47
+ if steps_path.exists():
48
+ reader = JsonlReader(steps_path)
49
+ info["actual_steps"] = reader.count()
50
+
51
+ return info
52
+
53
+
54
+ class RunLoader:
55
+ """
56
+ Load and access run data.
57
+
58
+ Usage:
59
+ loader = RunLoader("runs/my_run")
60
+
61
+ # Access metadata
62
+ print(loader.meta.model_name)
63
+
64
+ # Iterate over steps
65
+ for step in loader.iter_steps():
66
+ print(step.step_idx, step.action.values)
67
+
68
+ # Access specific step
69
+ step = loader.get_step(10)
70
+
71
+ # Load image for a step
72
+ image = loader.load_image(step, camera_name="front")
73
+ """
74
+
75
+ def __init__(self, run_dir: Path):
76
+ """
77
+ Initialize run loader.
78
+
79
+ Args:
80
+ run_dir: Path to run directory
81
+ """
82
+ self.run_dir = Path(run_dir)
83
+
84
+ if not self.run_dir.exists():
85
+ raise FileNotFoundError(f"Run directory not found: {self.run_dir}")
86
+
87
+ # Load metadata
88
+ meta_path = self.run_dir / "meta.json"
89
+ if meta_path.exists():
90
+ self.meta = RunMeta.load(str(meta_path))
91
+ else:
92
+ # Create minimal metadata
93
+ self.meta = RunMeta(
94
+ run_name=self.run_dir.name,
95
+ start_time="unknown",
96
+ )
97
+
98
+ # Initialize step reader
99
+ self._steps_path = self.run_dir / "steps.jsonl"
100
+ self._steps_cache: Optional[List[StepRecord]] = None
101
+
102
+ @property
103
+ def step_count(self) -> int:
104
+ """Get the number of steps."""
105
+ if self._steps_path.exists():
106
+ return JsonlReader(self._steps_path).count()
107
+ return 0
108
+
109
+ def iter_steps(self) -> Iterator[StepRecord]:
110
+ """Iterate over all steps."""
111
+ if not self._steps_path.exists():
112
+ return
113
+
114
+ reader = JsonlReader(self._steps_path)
115
+ for data in reader:
116
+ yield StepRecord.from_dict(data)
117
+
118
+ def get_steps(self) -> List[StepRecord]:
119
+ """Get all steps as a list (cached)."""
120
+ if self._steps_cache is None:
121
+ self._steps_cache = list(self.iter_steps())
122
+ return self._steps_cache
123
+
124
+ def get_step(self, step_idx: int) -> Optional[StepRecord]:
125
+ """
126
+ Get a specific step by index.
127
+
128
+ Note: This loads steps sequentially, so it's O(n).
129
+ For random access, use get_steps() first.
130
+ """
131
+ for step in self.iter_steps():
132
+ if step.step_idx == step_idx:
133
+ return step
134
+ return None
135
+
136
+ def load_image(
137
+ self,
138
+ step: StepRecord,
139
+ camera_name: Optional[str] = None,
140
+ ) -> Optional[np.ndarray]:
141
+ """
142
+ Load image for a step.
143
+
144
+ Args:
145
+ step: StepRecord
146
+ camera_name: Camera name (None = first camera)
147
+
148
+ Returns:
149
+ Image array (RGB) or None if not found
150
+ """
151
+ import cv2
152
+
153
+ if not step.obs.images:
154
+ return None
155
+
156
+ # Find image ref
157
+ image_ref = None
158
+ for ref in step.obs.images:
159
+ if camera_name is None or ref.camera_name == camera_name:
160
+ image_ref = ref
161
+ break
162
+
163
+ if image_ref is None:
164
+ return None
165
+
166
+ # Load image
167
+ image_path = self.run_dir / image_ref.path
168
+ if not image_path.exists():
169
+ return None
170
+
171
+ image = cv2.imread(str(image_path))
172
+ if image is None:
173
+ return None
174
+
175
+ return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
176
+
177
+ def get_all_states(self) -> np.ndarray:
178
+ """Get all states as a numpy array."""
179
+ steps = self.get_steps()
180
+ if not steps:
181
+ return np.array([])
182
+
183
+ states = [step.obs.state for step in steps if step.obs.state]
184
+ if not states:
185
+ return np.array([])
186
+
187
+ return np.array(states)
188
+
189
+ def get_all_actions(self) -> List[np.ndarray]:
190
+ """Get all actions as a list of arrays."""
191
+ steps = self.get_steps()
192
+ actions = []
193
+ for step in steps:
194
+ if step.action.values:
195
+ actions.append(np.array(step.action.values))
196
+ return actions
197
+
198
+ def get_timing_series(self) -> Dict[str, np.ndarray]:
199
+ """Get timing data as arrays."""
200
+ steps = self.get_steps()
201
+
202
+ timing_keys = [
203
+ "transport_latency_ms",
204
+ "inference_latency_ms",
205
+ "total_latency_ms",
206
+ "message_interval_ms",
207
+ ]
208
+
209
+ result = {key: [] for key in timing_keys}
210
+
211
+ for step in steps:
212
+ for key in timing_keys:
213
+ value = getattr(step.timing, key, None)
214
+ result[key].append(value if value is not None else np.nan)
215
+
216
+ return {key: np.array(values) for key, values in result.items()}
@@ -0,0 +1,343 @@
1
+ """
2
+ VLA-Lab Run Logger
3
+
4
+ Unified logging interface for VLA real-world deployment.
5
+ """
6
+
7
+ import os
8
+ import time
9
+ import base64
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ from typing import Optional, List, Dict, Any, Union
13
+ import numpy as np
14
+
15
+ from vlalab.schema.step import StepRecord, ObsData, ActionData, TimingData, ImageRef
16
+ from vlalab.schema.run import RunMeta, CameraConfig
17
+ from vlalab.logging.jsonl_writer import JsonlWriter
18
+
19
+
20
+ class RunLogger:
21
+ """
22
+ Unified logger for VLA deployment runs.
23
+
24
+ Creates a run directory with:
25
+ - meta.json: Run metadata
26
+ - steps.jsonl: Step records (one per line)
27
+ - artifacts/images/: Image files
28
+
29
+ Usage:
30
+ logger = RunLogger(
31
+ run_dir="runs/my_run",
32
+ model_name="diffusion_policy",
33
+ task_name="pick_and_place",
34
+ )
35
+
36
+ # Log a step
37
+ logger.log_step(
38
+ step_idx=0,
39
+ state=[0.5, 0.2, 0.3, 0, 0, 0, 1, 1.0],
40
+ action=[[0.51, 0.21, 0.31, 0, 0, 0, 1, 1.0]],
41
+ images={"front": image_array},
42
+ timing={...},
43
+ )
44
+
45
+ # Close when done
46
+ logger.close()
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ run_dir: Union[str, Path],
52
+ model_name: str = "unknown",
53
+ model_path: Optional[str] = None,
54
+ model_type: Optional[str] = None,
55
+ task_name: str = "unknown",
56
+ task_prompt: Optional[str] = None,
57
+ robot_name: str = "unknown",
58
+ robot_type: Optional[str] = None,
59
+ cameras: Optional[List[Dict[str, Any]]] = None,
60
+ inference_freq: Optional[float] = None,
61
+ action_dim: Optional[int] = None,
62
+ action_horizon: Optional[int] = None,
63
+ server_config: Optional[Dict[str, Any]] = None,
64
+ client_config: Optional[Dict[str, Any]] = None,
65
+ auto_create: bool = True,
66
+ image_quality: int = 85,
67
+ ):
68
+ """
69
+ Initialize the run logger.
70
+
71
+ Args:
72
+ run_dir: Directory to store run data
73
+ model_name: Name of the model
74
+ model_path: Path to the model checkpoint
75
+ model_type: Type of model (diffusion_policy, groot, etc.)
76
+ task_name: Name of the task
77
+ task_prompt: Language prompt for the task
78
+ robot_name: Name of the robot
79
+ robot_type: Type of robot (franka, ur5, etc.)
80
+ cameras: List of camera configurations
81
+ inference_freq: Inference frequency in Hz
82
+ action_dim: Action dimension
83
+ action_horizon: Action horizon (chunk size)
84
+ server_config: Server configuration dict
85
+ client_config: Client configuration dict
86
+ auto_create: Whether to create the run directory
87
+ image_quality: JPEG quality for saved images (1-100)
88
+ """
89
+ self.run_dir = Path(run_dir)
90
+ self.image_quality = image_quality
91
+ self._step_count = 0
92
+ self._closed = False
93
+
94
+ if auto_create:
95
+ self.run_dir.mkdir(parents=True, exist_ok=True)
96
+ (self.run_dir / "artifacts" / "images").mkdir(parents=True, exist_ok=True)
97
+
98
+ # Create run name from directory name
99
+ run_name = self.run_dir.name
100
+ start_time = datetime.now().isoformat()
101
+
102
+ # Build camera configs
103
+ camera_configs = []
104
+ if cameras:
105
+ for cam in cameras:
106
+ if isinstance(cam, dict):
107
+ camera_configs.append(CameraConfig(**cam))
108
+ else:
109
+ camera_configs.append(cam)
110
+
111
+ # Create metadata
112
+ self.meta = RunMeta(
113
+ run_name=run_name,
114
+ start_time=start_time,
115
+ model_name=model_name,
116
+ model_path=model_path,
117
+ model_type=model_type,
118
+ task_name=task_name,
119
+ task_prompt=task_prompt,
120
+ robot_name=robot_name,
121
+ robot_type=robot_type,
122
+ cameras=camera_configs,
123
+ inference_freq=inference_freq,
124
+ action_dim=action_dim,
125
+ action_horizon=action_horizon,
126
+ server_config=server_config or {},
127
+ client_config=client_config or {},
128
+ )
129
+
130
+ # Save initial metadata
131
+ self._save_meta()
132
+
133
+ # Initialize JSONL writer
134
+ self._jsonl_writer = JsonlWriter(self.run_dir / "steps.jsonl")
135
+
136
+ def _save_meta(self):
137
+ """Save metadata to meta.json."""
138
+ self.meta.save(str(self.run_dir / "meta.json"))
139
+
140
+ def _save_image(
141
+ self,
142
+ image: np.ndarray,
143
+ step_idx: int,
144
+ camera_name: str = "default",
145
+ ) -> ImageRef:
146
+ """
147
+ Save an image to the artifacts directory.
148
+
149
+ Args:
150
+ image: Image array (H, W, C) in RGB or BGR format
151
+ step_idx: Step index
152
+ camera_name: Camera name
153
+
154
+ Returns:
155
+ ImageRef with path information
156
+ """
157
+ # Import cv2 here to avoid hard dependency
158
+ import cv2
159
+
160
+ # Create filename
161
+ filename = f"step_{step_idx:06d}_{camera_name}.jpg"
162
+ rel_path = f"artifacts/images/{filename}"
163
+ abs_path = self.run_dir / rel_path
164
+
165
+ # Ensure image is uint8
166
+ if image.dtype != np.uint8:
167
+ image = (image * 255).astype(np.uint8)
168
+
169
+ # Convert RGB to BGR for cv2
170
+ if len(image.shape) == 3 and image.shape[2] == 3:
171
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
172
+ else:
173
+ image_bgr = image
174
+
175
+ # Save image
176
+ cv2.imwrite(str(abs_path), image_bgr, [cv2.IMWRITE_JPEG_QUALITY, self.image_quality])
177
+
178
+ return ImageRef(
179
+ path=rel_path,
180
+ camera_name=camera_name,
181
+ shape=list(image.shape),
182
+ encoding="jpeg",
183
+ )
184
+
185
+ def _decode_base64_image(self, b64_str: str) -> Optional[np.ndarray]:
186
+ """Decode a base64-encoded image."""
187
+ try:
188
+ import cv2
189
+ img_data = base64.b64decode(b64_str)
190
+ img_array = np.frombuffer(img_data, dtype=np.uint8)
191
+ image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
192
+ return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
193
+ except Exception:
194
+ return None
195
+
196
+ def log_step(
197
+ self,
198
+ step_idx: int,
199
+ state: Optional[List[float]] = None,
200
+ pose: Optional[List[float]] = None,
201
+ gripper: Optional[float] = None,
202
+ action: Optional[Union[List[float], List[List[float]]]] = None,
203
+ images: Optional[Dict[str, Union[np.ndarray, str]]] = None,
204
+ timing: Optional[Dict[str, Any]] = None,
205
+ prompt: Optional[str] = None,
206
+ tags: Optional[Dict[str, Any]] = None,
207
+ ):
208
+ """
209
+ Log a single step.
210
+
211
+ Args:
212
+ step_idx: Step index
213
+ state: Low-dim state vector
214
+ pose: Robot pose [x, y, z, qx, qy, qz, qw] or similar
215
+ gripper: Gripper state (0-1)
216
+ action: Action values (single or chunk)
217
+ images: Dict of camera_name -> image_array or base64_string
218
+ timing: Timing information dict
219
+ prompt: Language prompt
220
+ tags: Additional tags/metadata
221
+ """
222
+ if self._closed:
223
+ raise RuntimeError("Logger is closed")
224
+
225
+ # Process images
226
+ image_refs = []
227
+ if images:
228
+ for camera_name, image_data in images.items():
229
+ # Handle base64 string
230
+ if isinstance(image_data, str):
231
+ image_array = self._decode_base64_image(image_data)
232
+ if image_array is None:
233
+ continue
234
+ else:
235
+ image_array = image_data
236
+
237
+ # Save image and get reference
238
+ ref = self._save_image(image_array, step_idx, camera_name)
239
+ image_refs.append(ref)
240
+
241
+ # Build observation
242
+ obs = ObsData(
243
+ state=state or [],
244
+ images=image_refs,
245
+ pose=pose,
246
+ gripper=gripper,
247
+ )
248
+
249
+ # Build action
250
+ action_data = ActionData()
251
+ if action is not None:
252
+ # Normalize to list of lists (chunk format)
253
+ if action and not isinstance(action[0], (list, tuple)):
254
+ action = [action] # Single action -> chunk of 1
255
+ action_data.values = [list(a) for a in action]
256
+ if action:
257
+ action_data.action_dim = len(action[0])
258
+ action_data.chunk_size = len(action)
259
+
260
+ # Build timing
261
+ timing_data = TimingData()
262
+ if timing:
263
+ timing_data = TimingData.from_dict(timing)
264
+ timing_data.compute_latencies()
265
+
266
+ # Create step record
267
+ record = StepRecord(
268
+ step_idx=step_idx,
269
+ obs=obs,
270
+ action=action_data,
271
+ timing=timing_data,
272
+ prompt=prompt,
273
+ tags=tags or {},
274
+ )
275
+
276
+ # Write to JSONL
277
+ self._jsonl_writer.write(record)
278
+ self._step_count += 1
279
+
280
+ def log_step_raw(
281
+ self,
282
+ step_idx: int,
283
+ obs_dict: Dict[str, Any],
284
+ action_dict: Dict[str, Any],
285
+ timing_dict: Dict[str, Any],
286
+ **kwargs,
287
+ ):
288
+ """
289
+ Log a step from raw dictionaries (for adapters).
290
+
291
+ Args:
292
+ step_idx: Step index
293
+ obs_dict: Observation dictionary
294
+ action_dict: Action dictionary
295
+ timing_dict: Timing dictionary
296
+ **kwargs: Additional fields
297
+ """
298
+ record = StepRecord(
299
+ step_idx=step_idx,
300
+ obs=ObsData.from_dict(obs_dict),
301
+ action=ActionData.from_dict(action_dict),
302
+ timing=TimingData.from_dict(timing_dict),
303
+ **kwargs,
304
+ )
305
+ self._jsonl_writer.write(record)
306
+ self._step_count += 1
307
+
308
+ def update_meta(self, **kwargs):
309
+ """Update metadata fields."""
310
+ for key, value in kwargs.items():
311
+ if hasattr(self.meta, key):
312
+ setattr(self.meta, key, value)
313
+ self._save_meta()
314
+
315
+ def close(self):
316
+ """Close the logger and finalize metadata."""
317
+ if self._closed:
318
+ return
319
+
320
+ # Update final metadata
321
+ self.meta.end_time = datetime.now().isoformat()
322
+ self.meta.total_steps = self._step_count
323
+
324
+ # Calculate duration
325
+ start = datetime.fromisoformat(self.meta.start_time)
326
+ end = datetime.fromisoformat(self.meta.end_time)
327
+ self.meta.total_duration_s = (end - start).total_seconds()
328
+
329
+ self._save_meta()
330
+ self._jsonl_writer.close()
331
+ self._closed = True
332
+
333
+ @property
334
+ def step_count(self) -> int:
335
+ """Get the number of logged steps."""
336
+ return self._step_count
337
+
338
+ def __enter__(self):
339
+ return self
340
+
341
+ def __exit__(self, exc_type, exc_val, exc_tb):
342
+ self.close()
343
+ return False
@@ -0,0 +1,17 @@
1
+ """
2
+ VLA-Lab Schema Module
3
+
4
+ Defines standardized data structures for VLA deployment logging.
5
+ """
6
+
7
+ from vlalab.schema.step import StepRecord, ObsData, ActionData, TimingData, ImageRef
8
+ from vlalab.schema.run import RunMeta
9
+
10
+ __all__ = [
11
+ "StepRecord",
12
+ "ObsData",
13
+ "ActionData",
14
+ "TimingData",
15
+ "ImageRef",
16
+ "RunMeta",
17
+ ]