robot-keyframe-kit 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {robot_keyframe_kit-0.2.0/src/robot_keyframe_kit.egg-info → robot_keyframe_kit-0.3.0}/PKG-INFO +5 -5
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/README.md +2 -2
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/pyproject.toml +3 -3
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/config.py +106 -50
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/editor.py +1138 -171
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/sim_worker.py +65 -1
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0/src/robot_keyframe_kit.egg-info}/PKG-INFO +5 -5
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/requires.txt +2 -2
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/LICENSE +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/MANIFEST.in +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/setup.cfg +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/__init__.py +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/keyframe.py +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/math_utils.py +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/SOURCES.txt +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/dependency_links.txt +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/entry_points.txt +0 -0
- {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/top_level.txt +0 -0
{robot_keyframe_kit-0.2.0/src/robot_keyframe_kit.egg-info → robot_keyframe_kit-0.3.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robot-keyframe-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A generalizable Viser-based keyframe editor for any MuJoCo robot
|
|
5
5
|
Author-email: Stanford TML <yuming29@stanford.edu>
|
|
6
6
|
Maintainer-email: Stanford TML <yuming29@stanford.edu>
|
|
@@ -19,9 +19,9 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: mujoco
|
|
22
|
+
Requires-Dist: mujoco>=3.3.4
|
|
23
23
|
Requires-Dist: mink==0.0.13
|
|
24
|
-
Requires-Dist: viser-keyframe==1.0.
|
|
24
|
+
Requires-Dist: viser-keyframe==1.0.20
|
|
25
25
|
Requires-Dist: numpy
|
|
26
26
|
Requires-Dist: scipy
|
|
27
27
|
Requires-Dist: joblib
|
|
@@ -38,11 +38,11 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
A generalizable MuJoCo keyframe editor for creating and editing robot motion sequences. Works with any MuJoCo-compatible robot model.
|
|
40
40
|
|
|
41
|
-

|
|
41
|
+

|
|
42
42
|
|
|
43
43
|
## 🎬 Video Tutorial
|
|
44
44
|
|
|
45
|
-
[](https://www.youtube.com/watch?v=ZoRK3STKsd0)
|
|
45
|
+
[](https://www.youtube.com/watch?v=ZoRK3STKsd0)
|
|
46
46
|
|
|
47
47
|
**▶️ [Click to watch the full tutorial on YouTube](https://www.youtube.com/watch?v=ZoRK3STKsd0)**
|
|
48
48
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A generalizable MuJoCo keyframe editor for creating and editing robot motion sequences. Works with any MuJoCo-compatible robot model.
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
## 🎬 Video Tutorial
|
|
8
8
|
|
|
9
|
-
[](https://www.youtube.com/watch?v=ZoRK3STKsd0)
|
|
9
|
+
[](https://www.youtube.com/watch?v=ZoRK3STKsd0)
|
|
10
10
|
|
|
11
11
|
**▶️ [Click to watch the full tutorial on YouTube](https://www.youtube.com/watch?v=ZoRK3STKsd0)**
|
|
12
12
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "robot-keyframe-kit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "A generalizable Viser-based keyframe editor for any MuJoCo robot"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -28,9 +28,9 @@ classifiers = [
|
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
dependencies = [
|
|
31
|
-
"mujoco
|
|
31
|
+
"mujoco>=3.3.4",
|
|
32
32
|
"mink==0.0.13",
|
|
33
|
-
"viser-keyframe==1.0.
|
|
33
|
+
"viser-keyframe==1.0.20",
|
|
34
34
|
"numpy",
|
|
35
35
|
"scipy",
|
|
36
36
|
"joblib",
|
|
@@ -15,7 +15,7 @@ except ImportError:
|
|
|
15
15
|
@dataclass
|
|
16
16
|
class EditorConfig:
|
|
17
17
|
"""Configuration options for the ViserKeyframeEditor.
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
Attributes:
|
|
20
20
|
root_body: Name of the root body used for ground alignment (e.g., "torso", "base_link").
|
|
21
21
|
If None, will be auto-detected as the first non-world body that is a direct child of world.
|
|
@@ -29,6 +29,7 @@ class EditorConfig:
|
|
|
29
29
|
save_dir: Directory to save keyframe data files.
|
|
30
30
|
name: Optional name for this robot/project (used in save filenames).
|
|
31
31
|
"""
|
|
32
|
+
|
|
32
33
|
root_body: Optional[str] = None
|
|
33
34
|
end_effector_sites: Optional[List[str]] = None
|
|
34
35
|
mirror_pairs: Optional[Dict[str, str]] = None
|
|
@@ -36,36 +37,49 @@ class EditorConfig:
|
|
|
36
37
|
dt: float = 0.02
|
|
37
38
|
save_dir: str = "keyframes"
|
|
38
39
|
name: str = "robot"
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
# Physics simulation settings
|
|
41
42
|
n_frames: int = 20 # Number of physics substeps per control step
|
|
42
43
|
physics_dt: float = 0.001 # Physics timestep
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
# PD control gains for trajectory playback (for motor/torque actuators)
|
|
45
46
|
# These are used when actuators are motor-type (not position-type).
|
|
46
47
|
# Toddlerbot uses kp_sim = 10-14 (after kp_ratio division from config values 1500-2100)
|
|
47
48
|
# Note: Position-type actuators (like Unitree G1) use MuJoCo's built-in PD.
|
|
48
49
|
kp: float = 12.0 # Position gain (proportional) - matches typical Dynamixel motors
|
|
49
|
-
kd: float = 0.5
|
|
50
|
-
|
|
50
|
+
kd: float = 0.5 # Velocity gain (derivative) - light damping
|
|
51
|
+
motor_tau_limit: float = (
|
|
52
|
+
1.0 # Fallback |tau| clamp for torque actuators without model limits
|
|
53
|
+
)
|
|
54
|
+
|
|
51
55
|
# UI settings
|
|
52
56
|
show_com: bool = True # Show center of mass marker
|
|
53
57
|
show_grid: bool = True # Show ground grid
|
|
54
|
-
|
|
58
|
+
scene_update_hz: float = 60.0 # Scene transform push rate to browser
|
|
59
|
+
root_pose_gizmo_scale: float = 0.35 # Base scale for root pose transform gizmo
|
|
60
|
+
ik_target_gizmo_scale: float = 0.18 # Base scale for IK target transform gizmos
|
|
61
|
+
auto_scale_gizmos: bool = True # Scale gizmos based on robot model size
|
|
62
|
+
gizmo_scale_ratio: float = (
|
|
63
|
+
1.0 # Optional global multiplier applied after auto-scaling
|
|
64
|
+
)
|
|
65
|
+
gizmo_reference_height: float = (
|
|
66
|
+
1.28 # Height (m) where base gizmo scales are unchanged
|
|
67
|
+
)
|
|
68
|
+
|
|
55
69
|
# Scene generation settings
|
|
56
70
|
auto_inject_floor: bool = True # Whether to auto-inject floor for robot-only XMLs
|
|
57
71
|
show_floor: bool = True # Whether the injected floor should be visible
|
|
58
|
-
|
|
72
|
+
|
|
59
73
|
@classmethod
|
|
60
74
|
def from_yaml(cls, path: str) -> "EditorConfig":
|
|
61
75
|
"""Load configuration from a YAML file.
|
|
62
|
-
|
|
76
|
+
|
|
63
77
|
Args:
|
|
64
78
|
path: Path to the YAML configuration file.
|
|
65
|
-
|
|
79
|
+
|
|
66
80
|
Returns:
|
|
67
81
|
EditorConfig instance loaded from the file.
|
|
68
|
-
|
|
82
|
+
|
|
69
83
|
Raises:
|
|
70
84
|
ImportError: If PyYAML is not installed.
|
|
71
85
|
FileNotFoundError: If the config file doesn't exist.
|
|
@@ -75,58 +89,87 @@ class EditorConfig:
|
|
|
75
89
|
"PyYAML is required to load YAML config files. "
|
|
76
90
|
"Install with: pip install pyyaml"
|
|
77
91
|
)
|
|
78
|
-
|
|
92
|
+
|
|
79
93
|
if not os.path.exists(path):
|
|
80
94
|
raise FileNotFoundError(f"Config file not found: {path}")
|
|
81
|
-
|
|
95
|
+
|
|
82
96
|
with open(path, "r") as f:
|
|
83
97
|
data = yaml.safe_load(f)
|
|
84
|
-
|
|
98
|
+
|
|
85
99
|
if data is None:
|
|
86
100
|
data = {}
|
|
87
|
-
|
|
101
|
+
|
|
88
102
|
# Map YAML keys to config fields
|
|
89
103
|
mapped_data = {}
|
|
90
|
-
|
|
104
|
+
|
|
91
105
|
# Direct mappings
|
|
92
|
-
for key in [
|
|
106
|
+
for key in [
|
|
107
|
+
"name",
|
|
108
|
+
"root_body",
|
|
109
|
+
"dt",
|
|
110
|
+
"save_dir",
|
|
111
|
+
"kp",
|
|
112
|
+
"kd",
|
|
113
|
+
"motor_tau_limit",
|
|
114
|
+
"n_frames",
|
|
115
|
+
"physics_dt",
|
|
116
|
+
"show_com",
|
|
117
|
+
"show_grid",
|
|
118
|
+
"scene_update_hz",
|
|
119
|
+
"root_pose_gizmo_scale",
|
|
120
|
+
"ik_target_gizmo_scale",
|
|
121
|
+
"auto_scale_gizmos",
|
|
122
|
+
"gizmo_scale_ratio",
|
|
123
|
+
"gizmo_reference_height",
|
|
124
|
+
"auto_inject_floor",
|
|
125
|
+
"show_floor",
|
|
126
|
+
]:
|
|
93
127
|
if key in data:
|
|
94
128
|
mapped_data[key] = data[key]
|
|
95
|
-
|
|
129
|
+
|
|
96
130
|
# Handle end_effectors -> end_effector_sites
|
|
97
131
|
if "end_effectors" in data:
|
|
98
132
|
mapped_data["end_effector_sites"] = data["end_effectors"]
|
|
99
133
|
elif "end_effector_sites" in data:
|
|
100
134
|
mapped_data["end_effector_sites"] = data["end_effector_sites"]
|
|
101
|
-
|
|
135
|
+
|
|
102
136
|
# Handle mirror_pairs and mirror_signs
|
|
103
137
|
if "mirror_pairs" in data:
|
|
104
138
|
mapped_data["mirror_pairs"] = data["mirror_pairs"]
|
|
105
139
|
if "mirror_signs" in data:
|
|
106
140
|
mapped_data["mirror_signs"] = data["mirror_signs"]
|
|
107
|
-
|
|
141
|
+
|
|
108
142
|
# Extract nested physics settings (override direct settings)
|
|
109
143
|
physics = data.get("physics", {})
|
|
110
144
|
if isinstance(physics, dict):
|
|
111
|
-
for key in ["kp", "kd", "dt"]:
|
|
145
|
+
for key in ["kp", "kd", "dt", "motor_tau_limit"]:
|
|
112
146
|
if key in physics:
|
|
113
147
|
mapped_data[key] = physics[key]
|
|
114
|
-
|
|
148
|
+
|
|
115
149
|
# Extract nested UI settings
|
|
116
150
|
ui = data.get("ui", {})
|
|
117
151
|
if isinstance(ui, dict):
|
|
118
|
-
for key in [
|
|
152
|
+
for key in [
|
|
153
|
+
"show_com",
|
|
154
|
+
"show_grid",
|
|
155
|
+
"scene_update_hz",
|
|
156
|
+
"root_pose_gizmo_scale",
|
|
157
|
+
"ik_target_gizmo_scale",
|
|
158
|
+
"auto_scale_gizmos",
|
|
159
|
+
"gizmo_scale_ratio",
|
|
160
|
+
"gizmo_reference_height",
|
|
161
|
+
]:
|
|
119
162
|
if key in ui:
|
|
120
163
|
mapped_data[key] = ui[key]
|
|
121
|
-
|
|
164
|
+
|
|
122
165
|
return cls(**mapped_data)
|
|
123
|
-
|
|
166
|
+
|
|
124
167
|
def to_yaml(self, path: str) -> None:
|
|
125
168
|
"""Save configuration to a YAML file.
|
|
126
|
-
|
|
169
|
+
|
|
127
170
|
Args:
|
|
128
171
|
path: Path where to save the YAML configuration file.
|
|
129
|
-
|
|
172
|
+
|
|
130
173
|
Raises:
|
|
131
174
|
ImportError: If PyYAML is not installed.
|
|
132
175
|
"""
|
|
@@ -135,7 +178,7 @@ class EditorConfig:
|
|
|
135
178
|
"PyYAML is required to save YAML config files. "
|
|
136
179
|
"Install with: pip install pyyaml"
|
|
137
180
|
)
|
|
138
|
-
|
|
181
|
+
|
|
139
182
|
data = {
|
|
140
183
|
"name": self.name,
|
|
141
184
|
"root_body": self.root_body,
|
|
@@ -147,46 +190,57 @@ class EditorConfig:
|
|
|
147
190
|
"physics": {
|
|
148
191
|
"kp": self.kp,
|
|
149
192
|
"kd": self.kd,
|
|
193
|
+
"motor_tau_limit": self.motor_tau_limit,
|
|
150
194
|
"dt": self.dt,
|
|
151
195
|
},
|
|
152
196
|
"ui": {
|
|
153
197
|
"show_com": self.show_com,
|
|
154
198
|
"show_grid": self.show_grid,
|
|
199
|
+
"scene_update_hz": self.scene_update_hz,
|
|
200
|
+
"root_pose_gizmo_scale": self.root_pose_gizmo_scale,
|
|
201
|
+
"ik_target_gizmo_scale": self.ik_target_gizmo_scale,
|
|
202
|
+
"auto_scale_gizmos": self.auto_scale_gizmos,
|
|
203
|
+
"gizmo_scale_ratio": self.gizmo_scale_ratio,
|
|
204
|
+
"gizmo_reference_height": self.gizmo_reference_height,
|
|
155
205
|
},
|
|
156
206
|
"scene": {
|
|
157
207
|
"auto_inject_floor": self.auto_inject_floor,
|
|
158
208
|
"show_floor": self.show_floor,
|
|
159
209
|
},
|
|
160
210
|
}
|
|
161
|
-
|
|
211
|
+
|
|
162
212
|
# Remove None values
|
|
163
213
|
data = {k: v for k, v in data.items() if v is not None}
|
|
164
|
-
|
|
165
|
-
os.makedirs(
|
|
214
|
+
|
|
215
|
+
os.makedirs(
|
|
216
|
+
os.path.dirname(path) if os.path.dirname(path) else ".", exist_ok=True
|
|
217
|
+
)
|
|
166
218
|
with open(path, "w") as f:
|
|
167
219
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
168
|
-
|
|
220
|
+
|
|
169
221
|
@classmethod
|
|
170
|
-
def generate_from_model(
|
|
222
|
+
def generate_from_model(
|
|
223
|
+
cls, xml_path: str, name: Optional[str] = None
|
|
224
|
+
) -> "EditorConfig":
|
|
171
225
|
"""Generate a configuration file from a MuJoCo model by auto-detecting settings.
|
|
172
|
-
|
|
226
|
+
|
|
173
227
|
This creates a config with auto-detected values that can be manually edited.
|
|
174
|
-
|
|
228
|
+
|
|
175
229
|
Args:
|
|
176
230
|
xml_path: Path to the MuJoCo XML file.
|
|
177
231
|
name: Optional name for the robot. If None, inferred from XML filename.
|
|
178
|
-
|
|
232
|
+
|
|
179
233
|
Returns:
|
|
180
234
|
EditorConfig instance with auto-detected values.
|
|
181
235
|
"""
|
|
182
236
|
import mujoco
|
|
183
|
-
|
|
237
|
+
|
|
184
238
|
model = mujoco.MjModel.from_xml_path(xml_path)
|
|
185
|
-
|
|
239
|
+
|
|
186
240
|
# Infer name from XML path if not provided
|
|
187
241
|
if name is None:
|
|
188
242
|
name = os.path.splitext(os.path.basename(xml_path))[0]
|
|
189
|
-
|
|
243
|
+
|
|
190
244
|
# Auto-detect root body
|
|
191
245
|
root_body = None
|
|
192
246
|
for body_id in range(model.nbody):
|
|
@@ -196,22 +250,24 @@ class EditorConfig:
|
|
|
196
250
|
root_body = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, body_id)
|
|
197
251
|
if root_body and root_body != "world":
|
|
198
252
|
break
|
|
199
|
-
|
|
253
|
+
|
|
200
254
|
# Auto-detect end-effector sites (from leaf bodies)
|
|
201
255
|
parent_ids = set(model.body_parentid)
|
|
202
256
|
leaf_body_ids = [bid for bid in range(model.nbody) if bid not in parent_ids]
|
|
203
|
-
|
|
257
|
+
|
|
204
258
|
end_effectors = []
|
|
205
259
|
ee_keywords = ["foot", "hand", "calf", "leg", "lleg", "ankle", "toe", "gripper"]
|
|
206
|
-
|
|
260
|
+
|
|
207
261
|
# First try sites
|
|
208
262
|
for body_id in leaf_body_ids:
|
|
209
263
|
for site_id in range(model.nsite):
|
|
210
264
|
if model.site_bodyid[site_id] == body_id:
|
|
211
|
-
site_name = mujoco.mj_id2name(
|
|
265
|
+
site_name = mujoco.mj_id2name(
|
|
266
|
+
model, mujoco.mjtObj.mjOBJ_SITE, site_id
|
|
267
|
+
)
|
|
212
268
|
if site_name:
|
|
213
269
|
end_effectors.append(site_name)
|
|
214
|
-
|
|
270
|
+
|
|
215
271
|
# Fallback to leaf bodies
|
|
216
272
|
if not end_effectors:
|
|
217
273
|
for body_id in leaf_body_ids:
|
|
@@ -220,7 +276,7 @@ class EditorConfig:
|
|
|
220
276
|
body_lower = body_name.lower()
|
|
221
277
|
if any(kw in body_lower for kw in ee_keywords):
|
|
222
278
|
end_effectors.append(body_name)
|
|
223
|
-
|
|
279
|
+
|
|
224
280
|
# Auto-detect mirror pairs (left/right joints)
|
|
225
281
|
mirror_pairs = {}
|
|
226
282
|
joint_names = []
|
|
@@ -228,19 +284,21 @@ class EditorConfig:
|
|
|
228
284
|
jnt_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, jnt_id)
|
|
229
285
|
if jnt_name:
|
|
230
286
|
joint_names.append(jnt_name)
|
|
231
|
-
|
|
287
|
+
|
|
232
288
|
# Find left/right pairs
|
|
233
289
|
for joint_name in joint_names:
|
|
234
290
|
if "left" in joint_name.lower() or "_l_" in joint_name.lower():
|
|
235
291
|
# Try to find corresponding right joint
|
|
236
|
-
right_name = joint_name.replace("left", "right").replace(
|
|
292
|
+
right_name = joint_name.replace("left", "right").replace(
|
|
293
|
+
"Left", "Right"
|
|
294
|
+
)
|
|
237
295
|
right_name_alt = joint_name.replace("_l_", "_r_").replace("_L_", "_R_")
|
|
238
|
-
|
|
296
|
+
|
|
239
297
|
if right_name in joint_names:
|
|
240
298
|
mirror_pairs[joint_name] = right_name
|
|
241
299
|
elif right_name_alt in joint_names:
|
|
242
300
|
mirror_pairs[joint_name] = right_name_alt
|
|
243
|
-
|
|
301
|
+
|
|
244
302
|
return cls(
|
|
245
303
|
name=name,
|
|
246
304
|
root_body=root_body,
|
|
@@ -248,5 +306,3 @@ class EditorConfig:
|
|
|
248
306
|
mirror_pairs=mirror_pairs if mirror_pairs else None,
|
|
249
307
|
mirror_signs=None, # Will be auto-computed by editor
|
|
250
308
|
)
|
|
251
|
-
|
|
252
|
-
|