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/__init__.py +82 -0
- vlalab/adapters/__init__.py +10 -0
- vlalab/adapters/converter.py +146 -0
- vlalab/adapters/dp_adapter.py +181 -0
- vlalab/adapters/groot_adapter.py +148 -0
- vlalab/apps/__init__.py +1 -0
- vlalab/apps/streamlit/__init__.py +1 -0
- vlalab/apps/streamlit/app.py +103 -0
- vlalab/apps/streamlit/pages/__init__.py +1 -0
- vlalab/apps/streamlit/pages/dataset_viewer.py +322 -0
- vlalab/apps/streamlit/pages/inference_viewer.py +360 -0
- vlalab/apps/streamlit/pages/latency_viewer.py +256 -0
- vlalab/cli.py +137 -0
- vlalab/core.py +672 -0
- vlalab/logging/__init__.py +10 -0
- vlalab/logging/jsonl_writer.py +114 -0
- vlalab/logging/run_loader.py +216 -0
- vlalab/logging/run_logger.py +343 -0
- vlalab/schema/__init__.py +17 -0
- vlalab/schema/run.py +162 -0
- vlalab/schema/step.py +177 -0
- vlalab/viz/__init__.py +9 -0
- vlalab/viz/mpl_fonts.py +161 -0
- vlalab-0.1.0.dist-info/METADATA +443 -0
- vlalab-0.1.0.dist-info/RECORD +29 -0
- vlalab-0.1.0.dist-info/WHEEL +5 -0
- vlalab-0.1.0.dist-info/entry_points.txt +2 -0
- vlalab-0.1.0.dist-info/licenses/LICENSE +21 -0
- vlalab-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|