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.
Files changed (18) hide show
  1. {robot_keyframe_kit-0.2.0/src/robot_keyframe_kit.egg-info → robot_keyframe_kit-0.3.0}/PKG-INFO +5 -5
  2. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/README.md +2 -2
  3. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/pyproject.toml +3 -3
  4. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/config.py +106 -50
  5. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/editor.py +1138 -171
  6. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/sim_worker.py +65 -1
  7. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0/src/robot_keyframe_kit.egg-info}/PKG-INFO +5 -5
  8. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/requires.txt +2 -2
  9. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/LICENSE +0 -0
  10. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/MANIFEST.in +0 -0
  11. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/setup.cfg +0 -0
  12. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/__init__.py +0 -0
  13. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/keyframe.py +0 -0
  14. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit/math_utils.py +0 -0
  15. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/SOURCES.txt +0 -0
  16. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/dependency_links.txt +0 -0
  17. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/entry_points.txt +0 -0
  18. {robot_keyframe_kit-0.2.0 → robot_keyframe_kit-0.3.0}/src/robot_keyframe_kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robot-keyframe-kit
3
- Version: 0.2.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==3.3.4
22
+ Requires-Dist: mujoco>=3.3.4
23
23
  Requires-Dist: mink==0.0.13
24
- Requires-Dist: viser-keyframe==1.0.19
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
- ![](demo.gif)
41
+ ![](docs/media/demo.gif)
42
42
 
43
43
  ## 🎬 Video Tutorial
44
44
 
45
- [![Watch the Tutorial](screenshot_unitree.png)](https://www.youtube.com/watch?v=ZoRK3STKsd0)
45
+ [![Watch the Tutorial](docs/media/screenshot_unitree.png)](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
- ![](demo.gif)
5
+ ![](docs/media/demo.gif)
6
6
 
7
7
  ## 🎬 Video Tutorial
8
8
 
9
- [![Watch the Tutorial](screenshot_unitree.png)](https://www.youtube.com/watch?v=ZoRK3STKsd0)
9
+ [![Watch the Tutorial](docs/media/screenshot_unitree.png)](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.2.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==3.3.4",
31
+ "mujoco>=3.3.4",
32
32
  "mink==0.0.13",
33
- "viser-keyframe==1.0.19",
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 # Velocity gain (derivative) - light damping
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 ["name", "root_body", "dt", "save_dir", "kp", "kd", "n_frames", "physics_dt", "show_com", "show_grid", "auto_inject_floor", "show_floor"]:
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 ["show_com", "show_grid"]:
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(os.path.dirname(path) if os.path.dirname(path) else ".", exist_ok=True)
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(cls, xml_path: str, name: Optional[str] = None) -> "EditorConfig":
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(model, mujoco.mjtObj.mjOBJ_SITE, site_id)
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("Left", "Right")
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
-