XiangpengYang commited on
Commit
93b20f5
·
1 Parent(s): b24bc66

feat: deploy pi05 UR Gradio Space

Browse files
Files changed (4) hide show
  1. .gitignore +5 -0
  2. README.md +54 -1
  3. app.py +154 -0
  4. tests/test_app.py +46 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .coverage
5
+ htmlcov/
README.md CHANGED
@@ -10,4 +10,57 @@ app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  pinned: false
11
  ---
12
 
13
+ # π₀.₅ UR Action Predictor
14
+
15
+ This Hugging Face Space deploys the state-conditioned `pi05_ur_demo_state`
16
+ policy trained on the local UR LeRobot dataset. It predicts an action chunk for
17
+ inspection or download; it never connects to or commands a robot.
18
+
19
+ ## Model repository
20
+
21
+ Configure these Space variables (or enter both values in the UI):
22
+
23
+ - `PI05_MODEL_ID`: Hugging Face repository containing the trained checkpoint.
24
+ - `PI05_CHECKPOINT_PATH`: relative checkpoint directory, for example
25
+ `checkpoints/30000`.
26
+
27
+ The selected directory must contain either `params/` (JAX checkpoint) or
28
+ `model.safetensors` (PyTorch checkpoint), plus the training statistics at:
29
+
30
+ ```text
31
+ assets/ur_demo/norm_stats.json
32
+ ```
33
+
34
+ Use a Space secret named `HF_TOKEN` when the model repository is private.
35
+
36
+ ## Inputs and outputs
37
+
38
+ The two image inputs correspond to training fields `video.image_0` (fixed
39
+ camera) and `video.wrist` (wrist camera). State values must use this exact order:
40
+
41
+ ```text
42
+ x, y, z, roll, pitch, yaw, gripper
43
+ ```
44
+
45
+ The policy returns ten actions with columns:
46
+
47
+ ```text
48
+ dx, dy, dz, droll, dpitch, dyaw, gripper
49
+ ```
50
+
51
+ All state values must be finite. TCP translation uses metres and rotation uses
52
+ radians, matching the collected dataset.
53
+
54
+ ## Deploy
55
+
56
+ Create a Hugging Face Gradio Space with a CUDA GPU and push this repository.
57
+ Model download and initialization happen lazily on the first prediction. Only
58
+ one inference request runs at a time to protect GPU memory.
59
+
60
+ For local use with all dependencies installed:
61
+
62
+ ```bash
63
+ PYTHONPATH=openpi_runtime python app.py
64
+ ```
65
+
66
+ The full OpenPI/JAX stack requires Python 3.11 and a compatible CUDA 12 GPU.
app.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Gradio Space for state-conditioned π₀.₅ UR inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gc
6
+
7
+ try:
8
+ import gradio as gr
9
+ except ImportError: # Core inference tests can run without the UI dependency.
10
+ gr = None
11
+
12
+ try:
13
+ import spaces
14
+ except ImportError: # Local and dedicated-GPU environments omit this helper.
15
+ class _SpacesFallback:
16
+ @staticmethod
17
+ def GPU(*args, **kwargs):
18
+ return lambda function: function
19
+
20
+ spaces = _SpacesFallback()
21
+
22
+ from artifacts import resolve_checkpoint_path, resolve_model_id
23
+ from inference import ACTION_LABELS, run_prediction
24
+ from model_loader import MODEL_MANAGER
25
+
26
+
27
+ def _gradio_integer(value, name: str) -> int:
28
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
29
+ raise ValueError(f"{name} must be an integer")
30
+ if not float(value).is_integer():
31
+ raise ValueError(f"{name} must be an integer")
32
+ result = int(value)
33
+ if result < 0:
34
+ raise ValueError(f"{name} must be non-negative")
35
+ return result
36
+
37
+
38
+ @spaces.GPU(duration=120)
39
+ def predict_ui(
40
+ model_id,
41
+ checkpoint_path,
42
+ fixed_image,
43
+ wrist_image,
44
+ instruction,
45
+ tcp_x,
46
+ tcp_y,
47
+ tcp_z,
48
+ tcp_roll,
49
+ tcp_pitch,
50
+ tcp_yaw,
51
+ gripper,
52
+ trial_index,
53
+ ):
54
+ try:
55
+ trial = _gradio_integer(trial_index, "trial index")
56
+ policy = MODEL_MANAGER.get(model_id, checkpoint_path)
57
+ result = run_prediction(
58
+ policy,
59
+ fixed_image,
60
+ wrist_image,
61
+ instruction,
62
+ [tcp_x, tcp_y, tcp_z, tcp_roll, tcp_pitch, tcp_yaw, gripper],
63
+ trial,
64
+ model_id,
65
+ checkpoint_path,
66
+ )
67
+ return result.actions, result.json_path, result.status
68
+ except Exception as exc:
69
+ gc.collect()
70
+ try:
71
+ import torch
72
+
73
+ if torch.cuda.is_available():
74
+ torch.cuda.empty_cache()
75
+ except ImportError:
76
+ pass
77
+ return None, None, f"Error: {exc}"
78
+
79
+
80
+ def build_demo():
81
+ if gr is None:
82
+ return None
83
+ with gr.Blocks(title="π₀.₅ UR Action Predictor") as demo:
84
+ gr.Markdown(
85
+ "# π₀.₅ UR Action Predictor\n"
86
+ "Upload the fixed and wrist camera views, enter the current TCP/gripper "
87
+ "state and a task instruction. This demo predicts actions only and does "
88
+ "not directly control a robot."
89
+ )
90
+ with gr.Row():
91
+ model_id = gr.Textbox(
92
+ value=resolve_model_id(),
93
+ label="Hugging Face model ID",
94
+ placeholder="owner/pi05-ur-checkpoint",
95
+ )
96
+ checkpoint_path = gr.Textbox(
97
+ value=resolve_checkpoint_path(),
98
+ label="Checkpoint path",
99
+ placeholder="checkpoints/30000",
100
+ )
101
+ with gr.Row():
102
+ fixed_image = gr.Image(type="pil", label="Fixed camera")
103
+ wrist_image = gr.Image(type="pil", label="Wrist camera")
104
+ instruction = gr.Textbox(
105
+ label="Task instruction",
106
+ placeholder="e.g. pick up the object and place it in the tray",
107
+ lines=2,
108
+ )
109
+ gr.Markdown("### Current state — metres/radians, followed by gripper state")
110
+ with gr.Row():
111
+ tcp_x = gr.Number(value=0.0, label="TCP x")
112
+ tcp_y = gr.Number(value=0.0, label="TCP y")
113
+ tcp_z = gr.Number(value=0.0, label="TCP z")
114
+ tcp_roll = gr.Number(value=0.0, label="TCP roll")
115
+ with gr.Row():
116
+ tcp_pitch = gr.Number(value=0.0, label="TCP pitch")
117
+ tcp_yaw = gr.Number(value=0.0, label="TCP yaw")
118
+ gripper = gr.Number(value=0.0, label="Gripper")
119
+ trial_index = gr.Number(value=0, precision=0, minimum=0, label="Trial index")
120
+ predict_button = gr.Button("Predict actions", variant="primary")
121
+ status = gr.Markdown(
122
+ "The model loads on the first prediction; download and initialization may take several minutes."
123
+ )
124
+ actions = gr.Dataframe(headers=list(ACTION_LABELS), interactive=False, label="Predicted actions")
125
+ json_output = gr.File(label="Download JSON result")
126
+ predict_button.click(
127
+ fn=predict_ui,
128
+ inputs=[
129
+ model_id,
130
+ checkpoint_path,
131
+ fixed_image,
132
+ wrist_image,
133
+ instruction,
134
+ tcp_x,
135
+ tcp_y,
136
+ tcp_z,
137
+ tcp_roll,
138
+ tcp_pitch,
139
+ tcp_yaw,
140
+ gripper,
141
+ trial_index,
142
+ ],
143
+ outputs=[actions, json_output, status],
144
+ )
145
+ return demo
146
+
147
+
148
+ demo = build_demo()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ if demo is None:
153
+ raise RuntimeError("Gradio is not installed; install requirements.txt first")
154
+ demo.queue(default_concurrency_limit=1).launch()
tests/test_app.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import unittest
3
+ from unittest import mock
4
+
5
+
6
+ class AppTests(unittest.TestCase):
7
+ def test_predict_ui_returns_table_file_and_status(self):
8
+ import app
9
+
10
+ result = type(
11
+ "Result", (),
12
+ {"actions": "table", "json_path": "/tmp/result.json", "status": "done"},
13
+ )()
14
+ with mock.patch.object(app.MODEL_MANAGER, "get", return_value=object()), mock.patch.object(
15
+ app, "run_prediction", return_value=result
16
+ ):
17
+ actual = app.predict_ui(
18
+ "owner/model", "checkpoint", object(), object(), "task",
19
+ 1, 2, 3, 4, 5, 6, 0, 0,
20
+ )
21
+ self.assertEqual(actual, ("table", "/tmp/result.json", "done"))
22
+
23
+ def test_predict_ui_turns_exceptions_into_status(self):
24
+ import app
25
+
26
+ with mock.patch.object(app.MODEL_MANAGER, "get", side_effect=RuntimeError("load failed")):
27
+ table, output_file, status = app.predict_ui(
28
+ "model", "checkpoint", object(), object(), "task",
29
+ 0, 0, 0, 0, 0, 0, 0, 0,
30
+ )
31
+ self.assertIsNone(table)
32
+ self.assertIsNone(output_file)
33
+ self.assertEqual(status, "Error: load failed")
34
+
35
+ def test_source_exposes_required_prediction_controls(self):
36
+ source = Path("app.py").read_text()
37
+ for label in (
38
+ "Fixed camera", "Wrist camera", "Task instruction", "Predict actions",
39
+ "TCP x", "TCP y", "TCP z", "TCP roll", "TCP pitch", "TCP yaw", "Gripper",
40
+ ):
41
+ self.assertIn(label, source)
42
+ self.assertIn("default_concurrency_limit=1", source)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ unittest.main()