cduss commited on
Commit
f0bd6e2
·
1 Parent(s): 0bc8ef2
Files changed (4) hide show
  1. .gitignore +25 -0
  2. README.md +48 -5
  3. app.py +503 -0
  4. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+ venv/
22
+ env/
23
+ ENV/
24
+ .venv
25
+ flagged/
README.md CHANGED
@@ -1,12 +1,55 @@
1
  ---
2
  title: Reachy Mini Simple Control Panel
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.0.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Reachy Mini Simple Control Panel
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: "4.0.0"
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Reachy Mini Simple Control Panel
13
+
14
+ A simple web-based control panel for the Reachy Mini robot.
15
+
16
+ ## Features
17
+
18
+ - **Connect to Robot**: Connect to your Reachy Mini robot at localhost:8000
19
+ - **Real-time Status**: Monitor robot status with 5Hz polling
20
+ - **Motor Control**: Enable/disable motors (torque on/off)
21
+ - **Movement Commands**: Wake up and go to sleep commands
22
+ - **Task Space Control**: Control robot's head pose (X, Y, Z, Roll, Pitch, Yaw)
23
+ - **Body Control**: Control body yaw rotation
24
+ - **Antenna Control**: Control left and right antennas
25
+
26
+ ## How to Use
27
+
28
+ 1. Make sure your Reachy Mini robot daemon is running on localhost:8000
29
+ 2. Click "Connect to Robot" button
30
+ 3. Use the buttons and sliders to control your robot
31
+
32
+ ## Local Testing
33
+
34
+ To test locally:
35
+
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ python app.py
39
+ ```
40
+
41
+ Then open http://localhost:7860 in your browser.
42
+
43
+ ## Architecture
44
+
45
+ This app uses the Reachy Mini HTTP REST API. It communicates with the following endpoints:
46
+
47
+ - `GET /api/state/full` - Get robot state (polled at 5Hz)
48
+ - `POST /api/motors/set_mode/{mode}` - Set motor mode (enabled/disabled)
49
+ - `POST /api/move/play/wake_up` - Wake up robot
50
+ - `POST /api/move/play/goto_sleep` - Put robot to sleep
51
+ - `POST /api/move/set_target` - Set target pose
52
+
53
+ ## HuggingFace Spaces Deployment
54
+
55
+ To deploy on HuggingFace Spaces, you'll need to set up a tunnel or VPN to make your robot accessible from the internet. The app assumes the robot is at localhost:8000.
app.py ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reachy Mini Simple Control Panel - Gradio App
3
+
4
+ A simple web-based control panel for Reachy Mini robot using HTTP API.
5
+ Connects to localhost:8000 by default.
6
+ """
7
+
8
+ import gradio as gr
9
+ import requests
10
+ import json
11
+ import time
12
+ from typing import Optional, Dict, Any
13
+
14
+
15
+ class ReachyMiniClient:
16
+ """HTTP client for Reachy Mini robot."""
17
+
18
+ def __init__(self, base_url: str):
19
+ self.base_url = base_url.rstrip('/')
20
+ self.session = requests.Session()
21
+ self.session.timeout = 5
22
+ self.connected = False
23
+
24
+ def test_connection(self) -> bool:
25
+ """Test if the robot is reachable."""
26
+ try:
27
+ response = self.session.get(f"{self.base_url}/api/state/full", timeout=2)
28
+ return response.status_code == 200
29
+ except Exception:
30
+ return False
31
+
32
+ def get_full_state(self) -> Optional[Dict[str, Any]]:
33
+ """Get the full robot state."""
34
+ try:
35
+ response = self.session.get(
36
+ f"{self.base_url}/api/state/full",
37
+ params={
38
+ "with_control_mode": True,
39
+ "with_head_pose": True,
40
+ "with_body_yaw": True,
41
+ "with_antenna_positions": True,
42
+ }
43
+ )
44
+ if response.status_code == 200:
45
+ return response.json()
46
+ return None
47
+ except Exception as e:
48
+ print(f"Error getting state: {e}")
49
+ return None
50
+
51
+ def set_motor_mode(self, mode: str) -> bool:
52
+ """Set motor control mode (enabled, disabled, gravity_compensation)."""
53
+ try:
54
+ response = self.session.post(f"{self.base_url}/api/motors/set_mode/{mode}")
55
+ return response.status_code == 200
56
+ except Exception as e:
57
+ print(f"Error setting motor mode: {e}")
58
+ return False
59
+
60
+ def wake_up(self) -> tuple[bool, str]:
61
+ """Send wake up command."""
62
+ try:
63
+ # First check motor status
64
+ motor_status = self.session.get(f"{self.base_url}/api/motors/status")
65
+ if motor_status.status_code == 200:
66
+ mode = motor_status.json().get("mode", "unknown")
67
+ if mode == "disabled":
68
+ return False, "Cannot wake up: motors are disabled. Enable motors first!"
69
+
70
+ response = self.session.post(f"{self.base_url}/api/move/play/wake_up")
71
+ if response.status_code == 200:
72
+ data = response.json()
73
+ move_uuid = data.get("uuid", "unknown")
74
+ return True, f"Wake up animation started (ID: {move_uuid[:8]}...)"
75
+ else:
76
+ return False, f"Failed: HTTP {response.status_code} - {response.text}"
77
+ except Exception as e:
78
+ print(f"Error waking up: {e}")
79
+ return False, f"Error: {str(e)}"
80
+
81
+ def goto_sleep(self) -> tuple[bool, str]:
82
+ """Send go to sleep command."""
83
+ try:
84
+ # First check motor status
85
+ motor_status = self.session.get(f"{self.base_url}/api/motors/status")
86
+ if motor_status.status_code == 200:
87
+ mode = motor_status.json().get("mode", "unknown")
88
+ if mode == "disabled":
89
+ return False, "Cannot sleep: motors are disabled. Enable motors first!"
90
+
91
+ response = self.session.post(f"{self.base_url}/api/move/play/goto_sleep")
92
+ if response.status_code == 200:
93
+ data = response.json()
94
+ move_uuid = data.get("uuid", "unknown")
95
+ return True, f"Sleep animation started (ID: {move_uuid[:8]}...)"
96
+ else:
97
+ return False, f"Failed: HTTP {response.status_code} - {response.text}"
98
+ except Exception as e:
99
+ print(f"Error going to sleep: {e}")
100
+ return False, f"Error: {str(e)}"
101
+
102
+ def get_running_moves(self) -> Optional[list]:
103
+ """Get list of running moves."""
104
+ try:
105
+ response = self.session.get(f"{self.base_url}/api/move/running")
106
+ if response.status_code == 200:
107
+ return response.json()
108
+ return None
109
+ except Exception as e:
110
+ print(f"Error getting running moves: {e}")
111
+ return None
112
+
113
+ def get_daemon_status(self) -> Optional[Dict[str, Any]]:
114
+ """Get daemon status."""
115
+ try:
116
+ response = self.session.get(f"{self.base_url}/api/daemon/status")
117
+ if response.status_code == 200:
118
+ return response.json()
119
+ return None
120
+ except Exception as e:
121
+ print(f"Error getting daemon status: {e}")
122
+ return None
123
+
124
+ def set_target(self, head_pose: Optional[Dict[str, float]] = None,
125
+ body_yaw: Optional[float] = None,
126
+ antennas: Optional[tuple] = None) -> bool:
127
+ """Set target pose for the robot."""
128
+ try:
129
+ payload = {}
130
+ if head_pose is not None:
131
+ payload["target_head_pose"] = head_pose
132
+ if body_yaw is not None:
133
+ payload["target_body_yaw"] = body_yaw
134
+ if antennas is not None:
135
+ payload["target_antennas"] = list(antennas)
136
+
137
+ response = self.session.post(
138
+ f"{self.base_url}/api/move/set_target",
139
+ json=payload
140
+ )
141
+ return response.status_code == 200
142
+ except Exception as e:
143
+ print(f"Error setting target: {e}")
144
+ return False
145
+
146
+
147
+ # Global client instance
148
+ client: Optional[ReachyMiniClient] = None
149
+ user_is_controlling = False # Flag to prevent polling from updating sliders during user control
150
+
151
+
152
+ def connect_to_robot():
153
+ """Connect to the robot (localhost:8000 by default)."""
154
+ global client
155
+
156
+ robot_url = "http://localhost:8000"
157
+
158
+ try:
159
+ client = ReachyMiniClient(robot_url)
160
+ if client.test_connection():
161
+ # Check daemon status
162
+ daemon_status = client.get_daemon_status()
163
+ daemon_state = "unknown"
164
+ if daemon_status:
165
+ daemon_state = daemon_status.get("state", "unknown")
166
+
167
+ # Get initial state
168
+ state = client.get_full_state()
169
+ if state:
170
+ client.connected = True
171
+ head_pose = state.get("head_pose", {})
172
+ body_yaw = state.get("body_yaw", 0.0)
173
+ antennas = state.get("antennas_position", [0.0, 0.0])
174
+ control_mode = state.get("control_mode", "unknown")
175
+
176
+ status_msg = f"Connected | Daemon: {daemon_state}"
177
+ if daemon_state != "running":
178
+ status_msg += " ⚠️ DAEMON NOT RUNNING"
179
+
180
+ return (
181
+ status_msg,
182
+ f"Motor Mode: {control_mode}",
183
+ head_pose.get("x", 0.0),
184
+ head_pose.get("y", 0.0),
185
+ head_pose.get("z", 0.0),
186
+ head_pose.get("roll", 0.0),
187
+ head_pose.get("pitch", 0.0),
188
+ head_pose.get("yaw", 0.0),
189
+ body_yaw,
190
+ antennas[0] if len(antennas) > 0 else 0.0,
191
+ antennas[1] if len(antennas) > 1 else 0.0,
192
+ )
193
+
194
+ client = None
195
+ return ("Connection failed - robot not reachable", "Disconnected", 0, 0, 0, 0, 0, 0, 0, 0, 0)
196
+
197
+ except Exception as e:
198
+ client = None
199
+ return (f"Connection error: {str(e)}", "Disconnected", 0, 0, 0, 0, 0, 0, 0, 0, 0)
200
+
201
+
202
+ def poll_robot_state():
203
+ """Poll the robot state at 5Hz (called by Gradio timer)."""
204
+ global client, user_is_controlling
205
+
206
+ if client is None or not client.connected:
207
+ return ("Disconnected", "N/A", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
208
+
209
+ try:
210
+ state = client.get_full_state()
211
+ if state:
212
+ control_mode = state.get("control_mode", "unknown")
213
+
214
+ # If user is actively controlling, don't update sliders (prevents laggy feedback)
215
+ if user_is_controlling:
216
+ return (
217
+ f"Connected to {client.base_url}",
218
+ f"Motor Mode: {control_mode}",
219
+ gr.update(), # Don't update sliders during user control
220
+ gr.update(),
221
+ gr.update(),
222
+ gr.update(),
223
+ gr.update(),
224
+ gr.update(),
225
+ gr.update(),
226
+ gr.update(),
227
+ gr.update(),
228
+ )
229
+
230
+ # Update sliders with robot state when user is not controlling
231
+ head_pose = state.get("head_pose", {})
232
+ body_yaw = state.get("body_yaw", 0.0)
233
+ antennas = state.get("antennas_position", [0.0, 0.0])
234
+
235
+ return (
236
+ f"Connected to {client.base_url}",
237
+ f"Motor Mode: {control_mode}",
238
+ head_pose.get("x", 0.0),
239
+ head_pose.get("y", 0.0),
240
+ head_pose.get("z", 0.0),
241
+ head_pose.get("roll", 0.0),
242
+ head_pose.get("pitch", 0.0),
243
+ head_pose.get("yaw", 0.0),
244
+ body_yaw,
245
+ antennas[0] if len(antennas) > 0 else 0.0,
246
+ antennas[1] if len(antennas) > 1 else 0.0,
247
+ )
248
+ else:
249
+ client.connected = False
250
+ return ("Connection lost", "Disconnected", 0, 0, 0, 0, 0, 0, 0, 0, 0)
251
+
252
+ except Exception as e:
253
+ client.connected = False
254
+ return (f"Polling error: {str(e)}", "Disconnected", 0, 0, 0, 0, 0, 0, 0, 0, 0)
255
+
256
+
257
+ def enable_motors():
258
+ """Enable motors (torque on)."""
259
+ global client
260
+ if client and client.connected:
261
+ success = client.set_motor_mode("enabled")
262
+ return "Motors enabled" if success else "Failed to enable motors"
263
+ return "Not connected to robot"
264
+
265
+
266
+ def disable_motors():
267
+ """Disable motors (torque off)."""
268
+ global client
269
+ if client and client.connected:
270
+ success = client.set_motor_mode("disabled")
271
+ return "Motors disabled" if success else "Failed to disable motors"
272
+ return "Not connected to robot"
273
+
274
+
275
+ def send_wake_up():
276
+ """Send wake up command."""
277
+ global client
278
+ if client and client.connected:
279
+ # First, stop any running moves
280
+ running = client.get_running_moves()
281
+ if running and len(running) > 0:
282
+ return f"Wait: {len(running)} move(s) still running. Try again in a moment."
283
+
284
+ success, message = client.wake_up()
285
+ return message
286
+ return "Not connected to robot"
287
+
288
+
289
+ def send_goto_sleep():
290
+ """Send go to sleep command."""
291
+ global client
292
+ if client and client.connected:
293
+ # First, stop any running moves
294
+ running = client.get_running_moves()
295
+ if running and len(running) > 0:
296
+ return f"Wait: {len(running)} move(s) still running. Try again in a moment."
297
+
298
+ success, message = client.goto_sleep()
299
+ return message
300
+ return "Not connected to robot"
301
+
302
+
303
+ def test_basic_movement():
304
+ """Test if basic movements work at all."""
305
+ global client
306
+ if client and client.connected:
307
+ try:
308
+ # Try a simple goto movement - move head slightly forward
309
+ response = client.session.post(
310
+ f"{client.base_url}/api/move/goto",
311
+ json={
312
+ "head_pose": {
313
+ "x": 0.02, # 2cm forward
314
+ "y": 0.0,
315
+ "z": 0.0,
316
+ "roll": 0.0,
317
+ "pitch": 0.0,
318
+ "yaw": 0.0
319
+ },
320
+ "duration": 1.0,
321
+ "interpolation": "minjerk"
322
+ }
323
+ )
324
+ if response.status_code == 200:
325
+ data = response.json()
326
+ return f"Test movement started: {data.get('uuid', 'unknown')[:8]}..."
327
+ else:
328
+ return f"Test failed: {response.status_code} - {response.text}"
329
+ except Exception as e:
330
+ return f"Test error: {str(e)}"
331
+ return "Not connected to robot"
332
+
333
+
334
+ def update_head_pose(x, y, z, roll, pitch, yaw, body_yaw, ant_left, ant_right):
335
+ """Update robot target pose (called when sliders change)."""
336
+ global client, user_is_controlling
337
+
338
+ # Set flag to prevent polling from overwriting slider values
339
+ user_is_controlling = True
340
+
341
+ if client and client.connected:
342
+ head_pose = {
343
+ "x": x,
344
+ "y": y,
345
+ "z": z,
346
+ "roll": roll,
347
+ "pitch": pitch,
348
+ "yaw": yaw,
349
+ }
350
+ client.set_target(
351
+ head_pose=head_pose,
352
+ body_yaw=body_yaw,
353
+ antennas=(ant_left, ant_right)
354
+ )
355
+
356
+
357
+ def stop_controlling():
358
+ """Reset the controlling flag after user stops interacting."""
359
+ global user_is_controlling
360
+ user_is_controlling = False
361
+ return None
362
+
363
+
364
+ # Create Gradio UI
365
+ def create_app():
366
+ """Create the Gradio application."""
367
+
368
+ with gr.Blocks(title="Reachy Mini Simple Control Panel") as app:
369
+
370
+ gr.Markdown("# Reachy Mini Simple Control Panel")
371
+ gr.Markdown("Control your Reachy Mini robot via HTTP API")
372
+
373
+ # Connection Section
374
+ with gr.Group():
375
+ gr.Markdown("### Connection")
376
+ connect_btn = gr.Button("Connect to Robot", variant="primary")
377
+ status_text = gr.Textbox(label="Connection Status", value="Disconnected", interactive=False)
378
+ motor_status = gr.Textbox(label="Motor Status", value="N/A", interactive=False)
379
+
380
+ # Motor Control Section
381
+ with gr.Group():
382
+ gr.Markdown("### Motor Control")
383
+ with gr.Row():
384
+ enable_btn = gr.Button("Torque On", variant="primary")
385
+ disable_btn = gr.Button("Torque Off", variant="stop")
386
+ log_output = gr.Textbox(label="Log", lines=2, interactive=False)
387
+
388
+ # Movement Commands Section
389
+ with gr.Group():
390
+ gr.Markdown("### Movement Commands")
391
+ with gr.Row():
392
+ wakeup_btn = gr.Button("Wake Up", variant="primary")
393
+ sleep_btn = gr.Button("Go to Sleep")
394
+ test_btn = gr.Button("Test Movement", variant="secondary")
395
+
396
+ # Task Space Control Section
397
+ with gr.Group():
398
+ gr.Markdown("### Task Space Control - Head Pose")
399
+
400
+ head_x = gr.Slider(
401
+ minimum=-0.1, maximum=0.1, value=0, step=0.001,
402
+ label="X (forward/backward) [m]"
403
+ )
404
+ head_y = gr.Slider(
405
+ minimum=-0.1, maximum=0.1, value=0, step=0.001,
406
+ label="Y (left/right) [m]"
407
+ )
408
+ head_z = gr.Slider(
409
+ minimum=-0.1, maximum=0.1, value=0, step=0.001,
410
+ label="Z (up/down) [m]"
411
+ )
412
+ head_roll = gr.Slider(
413
+ minimum=-0.5, maximum=0.5, value=0, step=0.001,
414
+ label="Roll [rad]"
415
+ )
416
+ head_pitch = gr.Slider(
417
+ minimum=-0.8, maximum=0.8, value=0, step=0.001,
418
+ label="Pitch [rad]"
419
+ )
420
+ head_yaw = gr.Slider(
421
+ minimum=-1.0, maximum=1.0, value=0, step=0.001,
422
+ label="Yaw [rad]"
423
+ )
424
+
425
+ # Body & Antennas Section
426
+ with gr.Group():
427
+ gr.Markdown("### Body & Antennas")
428
+
429
+ body_yaw = gr.Slider(
430
+ minimum=-1.5, maximum=1.5, value=0, step=0.001,
431
+ label="Body Yaw [rad]"
432
+ )
433
+ antenna_left = gr.Slider(
434
+ minimum=-1.0, maximum=1.0, value=0, step=0.001,
435
+ label="Left Antenna [rad]"
436
+ )
437
+ antenna_right = gr.Slider(
438
+ minimum=-1.0, maximum=1.0, value=0, step=0.001,
439
+ label="Right Antenna [rad]"
440
+ )
441
+
442
+ # Connect button handler
443
+ connect_btn.click(
444
+ fn=connect_to_robot,
445
+ inputs=[],
446
+ outputs=[
447
+ status_text, motor_status,
448
+ head_x, head_y, head_z, head_roll, head_pitch, head_yaw,
449
+ body_yaw, antenna_left, antenna_right
450
+ ]
451
+ )
452
+
453
+ # Motor control handlers
454
+ enable_btn.click(fn=enable_motors, inputs=[], outputs=[log_output])
455
+ disable_btn.click(fn=disable_motors, inputs=[], outputs=[log_output])
456
+
457
+ # Movement command handlers
458
+ wakeup_btn.click(fn=send_wake_up, inputs=[], outputs=[log_output])
459
+ sleep_btn.click(fn=send_goto_sleep, inputs=[], outputs=[log_output])
460
+ test_btn.click(fn=test_basic_movement, inputs=[], outputs=[log_output])
461
+
462
+ # Slider change handlers - update target when any slider changes
463
+ all_sliders = [head_x, head_y, head_z, head_roll, head_pitch, head_yaw,
464
+ body_yaw, antenna_left, antenna_right]
465
+
466
+ # Use .change for real-time control updates
467
+ for slider in all_sliders:
468
+ slider.change(
469
+ fn=update_head_pose,
470
+ inputs=all_sliders,
471
+ outputs=[]
472
+ )
473
+ # Reset controlling flag when user releases slider
474
+ slider.release(
475
+ fn=stop_controlling,
476
+ inputs=[],
477
+ outputs=[]
478
+ )
479
+
480
+ # Polling timer - updates state at 5Hz (every 200ms)
481
+ timer = gr.Timer(value=0.2)
482
+ timer.tick(
483
+ fn=poll_robot_state,
484
+ inputs=[],
485
+ outputs=[
486
+ status_text, motor_status,
487
+ head_x, head_y, head_z, head_roll, head_pitch, head_yaw,
488
+ body_yaw, antenna_left, antenna_right
489
+ ]
490
+ )
491
+
492
+ return app
493
+
494
+
495
+ # Create and launch the app
496
+ app = create_app()
497
+
498
+ if __name__ == "__main__":
499
+ app.launch(
500
+ server_name="0.0.0.0",
501
+ server_port=7860,
502
+ share=False
503
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0