Spaces:
Sleeping
Sleeping
b5027f74
Browse files
README.md
CHANGED
|
@@ -43,7 +43,7 @@ Intents through the safety pipeline that ships in the library.
|
|
| 43 |
watch the GeofenceGate reject it with a structured reason.
|
| 44 |
3. Send `takeoff {"altitude": 1}` on the Tello profile to see the HITL
|
| 45 |
gate escalate.
|
| 46 |
-
4. Read the trace event JSON for any call
|
| 47 |
library emits for replay, diff, query, energy ledger, and judge
|
| 48 |
scoring.
|
| 49 |
|
|
|
|
| 43 |
watch the GeofenceGate reject it with a structured reason.
|
| 44 |
3. Send `takeoff {"altitude": 1}` on the Tello profile to see the HITL
|
| 45 |
gate escalate.
|
| 46 |
+
4. Read the trace event JSON for any call. That's the same shape the
|
| 47 |
library emits for replay, diff, query, energy ledger, and judge
|
| 48 |
scoring.
|
| 49 |
|
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""HuggingFace Space
|
| 2 |
|
| 3 |
Live URL: https://huggingface.co/spaces/Ghostgim/ghostloop-demo
|
| 4 |
|
|
@@ -6,10 +6,10 @@ A no-code interface to ghostloop. Visitors pick a robot profile, then
|
|
| 6 |
drive it via:
|
| 7 |
|
| 8 |
- Per-primitive dispatch buttons (auto-generated from the profile's
|
| 9 |
-
registry
|
| 10 |
- Virtual joystick (D-pad style) for mobile bases / quadrupeds /
|
| 11 |
drones.
|
| 12 |
-
- Free-form Intent dispatch (advanced
|
| 13 |
- Live intervention controls: Pause, Resume, Emergency Stop.
|
| 14 |
- Live trace pane that updates after every dispatch.
|
| 15 |
- Live state pane showing the backend snapshot.
|
|
@@ -48,15 +48,15 @@ from ghostloop.profiles import (
|
|
| 48 |
|
| 49 |
|
| 50 |
PRESETS = {
|
| 51 |
-
"franka_arm
|
| 52 |
-
"spot
|
| 53 |
-
"tello
|
| 54 |
-
"stretch
|
| 55 |
-
"humanoid_demo
|
| 56 |
-
"turtlebot
|
| 57 |
}
|
| 58 |
|
| 59 |
-
DEFAULT_PROFILE = "franka_arm
|
| 60 |
|
| 61 |
|
| 62 |
# ---------------------------------------------------------------------------
|
|
@@ -133,7 +133,7 @@ DEFAULT_ARGS: dict[str, dict[str, Any]] = {
|
|
| 133 |
|
| 134 |
|
| 135 |
# ---------------------------------------------------------------------------
|
| 136 |
-
# Render helpers
|
| 137 |
# ---------------------------------------------------------------------------
|
| 138 |
|
| 139 |
|
|
@@ -141,7 +141,7 @@ def _render_profile_summary(session: dict[str, Any]) -> str:
|
|
| 141 |
profile = session["profile"]
|
| 142 |
runtime = session["runtime"]
|
| 143 |
return (
|
| 144 |
-
f"### `{profile.name}`
|
| 145 |
f"**Backend:** `{runtime.backend.name}` (mock) · "
|
| 146 |
f"**Workspace:** `{profile.workspace_bounds}` · "
|
| 147 |
f"**Max velocity:** `{profile.max_velocity}` m/s · "
|
|
@@ -152,7 +152,7 @@ def _render_profile_summary(session: dict[str, Any]) -> str:
|
|
| 152 |
def _render_primitives_list(session: dict[str, Any]) -> str:
|
| 153 |
runtime = session["runtime"]
|
| 154 |
return "\n".join(
|
| 155 |
-
f"- **`{name}`**
|
| 156 |
for name in runtime.registry.names()
|
| 157 |
)
|
| 158 |
|
|
@@ -172,24 +172,24 @@ def _render_state(session: dict[str, Any]) -> str:
|
|
| 172 |
def _render_trace(session: dict[str, Any]) -> str:
|
| 173 |
runtime = session["runtime"]
|
| 174 |
if not runtime.trace.events:
|
| 175 |
-
return "_(no events yet
|
| 176 |
rows = ["| step | intent | decision | gate | result | reason |",
|
| 177 |
"|---:|---|:---:|---|:---:|---|"]
|
| 178 |
for ev in runtime.trace.events[-12:]:
|
| 179 |
-
|
| 180 |
ev.decision.action.value, "?"
|
| 181 |
)
|
| 182 |
-
|
| 183 |
ev.result.status.value, "?"
|
| 184 |
)
|
| 185 |
args_compact = json.dumps(ev.intent.args, separators=(",", ":"))
|
| 186 |
if len(args_compact) > 40:
|
| 187 |
-
args_compact = args_compact[:37] + "
|
| 188 |
reason = (ev.decision.reason or ev.result.message or "")[:80]
|
| 189 |
rows.append(
|
| 190 |
f"| {ev.step} | `{ev.intent.name}` {args_compact} | "
|
| 191 |
-
f"{
|
| 192 |
-
f"{
|
| 193 |
)
|
| 194 |
n = len(runtime.trace.events)
|
| 195 |
if n > 12:
|
|
@@ -199,13 +199,13 @@ def _render_trace(session: dict[str, Any]) -> str:
|
|
| 199 |
|
| 200 |
def _render_intervention_state(session: dict[str, Any]) -> str:
|
| 201 |
state = session["controller"].state
|
| 202 |
-
|
| 203 |
-
InterventionState.RUNNING: "
|
| 204 |
-
InterventionState.PAUSED: "
|
| 205 |
-
InterventionState.SWAPPING: "
|
| 206 |
-
InterventionState.EMERGENCY_STOP: "
|
| 207 |
-
}.get(state, "
|
| 208 |
-
return f"###
|
| 209 |
|
| 210 |
|
| 211 |
def _render_instructions(session: dict[str, Any]) -> str:
|
|
@@ -266,7 +266,7 @@ def dispatch_custom(session: dict[str, Any], primitive_name: str, args_json: str
|
|
| 266 |
|
| 267 |
|
| 268 |
def dispatch_drive(session: dict[str, Any], linear_x: float, angular_z: float):
|
| 269 |
-
"""Joystick handler
|
| 270 |
runtime = session["runtime"]
|
| 271 |
name = None
|
| 272 |
if "drive" in runtime.registry.names():
|
|
@@ -325,7 +325,7 @@ def clear_trace(session: dict[str, Any]):
|
|
| 325 |
|
| 326 |
|
| 327 |
with gr.Blocks(
|
| 328 |
-
title="ghostloop
|
| 329 |
theme=gr.themes.Soft(primary_hue="teal"),
|
| 330 |
css="""
|
| 331 |
.gl-pad-button { min-height: 56px; font-weight: 600; }
|
|
@@ -339,7 +339,7 @@ with gr.Blocks(
|
|
| 339 |
|
| 340 |
Pick a robot profile, then drive it through the safety pipeline. Every
|
| 341 |
dispatch goes through `Geofence + ForceCap + ActionSmoothing + RateLimit
|
| 342 |
-
+ HITL`
|
| 343 |
geofence reject it.
|
| 344 |
|
| 345 |
Sister to **[GhostLM](https://github.com/joemunene-by/GhostLM)** · `pip install ghostloop` · [GitHub](https://github.com/joemunene-by/ghostloop) · [PyPI](https://pypi.org/project/ghostloop/)
|
|
@@ -370,7 +370,7 @@ Sister to **[GhostLM](https://github.com/joemunene-by/GhostLM)** · `pip install
|
|
| 370 |
instructions_md = gr.Markdown()
|
| 371 |
|
| 372 |
# ---------- Quick-dispatch buttons ----------
|
| 373 |
-
gr.Markdown("## Dispatch a primitive (one click
|
| 374 |
primitive_buttons: list[gr.Button] = []
|
| 375 |
with gr.Row():
|
| 376 |
for _ in range(6):
|
|
@@ -380,23 +380,23 @@ Sister to **[GhostLM](https://github.com/joemunene-by/GhostLM)** · `pip install
|
|
| 380 |
primitive_buttons.append(gr.Button("", variant="primary", elem_classes="gl-pad-button"))
|
| 381 |
|
| 382 |
# ---------- Joystick (D-pad) ----------
|
| 383 |
-
gr.Markdown("## Virtual joystick
|
| 384 |
with gr.Row():
|
| 385 |
with gr.Column(scale=1):
|
| 386 |
pass
|
| 387 |
with gr.Column(scale=1):
|
| 388 |
-
joy_forward = gr.Button("
|
| 389 |
with gr.Column(scale=1):
|
| 390 |
pass
|
| 391 |
with gr.Row():
|
| 392 |
-
joy_left = gr.Button("
|
| 393 |
-
joy_stop = gr.Button("
|
| 394 |
-
joy_right = gr.Button("RIGHT
|
| 395 |
with gr.Row():
|
| 396 |
with gr.Column(scale=1):
|
| 397 |
pass
|
| 398 |
with gr.Column(scale=1):
|
| 399 |
-
joy_back = gr.Button("
|
| 400 |
with gr.Column(scale=1):
|
| 401 |
pass
|
| 402 |
|
|
@@ -404,9 +404,9 @@ Sister to **[GhostLM](https://github.com/joemunene-by/GhostLM)** · `pip install
|
|
| 404 |
gr.Markdown("## Live intervention")
|
| 405 |
intervention_md = gr.Markdown()
|
| 406 |
with gr.Row():
|
| 407 |
-
pause_btn = gr.Button("
|
| 408 |
-
resume_btn = gr.Button("
|
| 409 |
-
estop_btn = gr.Button("
|
| 410 |
|
| 411 |
# ---------- Live trace + state ----------
|
| 412 |
with gr.Row():
|
|
@@ -427,17 +427,17 @@ Sister to **[GhostLM](https://github.com/joemunene-by/GhostLM)** · `pip install
|
|
| 427 |
value='{"x": 0.4, "y": 0.0, "z": 0.5}',
|
| 428 |
lines=2, scale=3,
|
| 429 |
)
|
| 430 |
-
adv_btn = gr.Button("
|
| 431 |
|
| 432 |
# ---------- Try-this hints ----------
|
| 433 |
gr.Markdown("""
|
| 434 |
### Try this:
|
| 435 |
|
| 436 |
-
1. Pick the **`franka_arm`** profile, then click **`move_to`**
|
| 437 |
2. Switch to **`spot`**, then drive with the joystick. Each press emits `walk_to(linear_x, 0, angular_z)` through the safety pipeline.
|
| 438 |
-
3. Hit **
|
| 439 |
|
| 440 |
-
Every dispatch is recorded in the trace pane below
|
| 441 |
""")
|
| 442 |
|
| 443 |
# ---------- Wiring ----------
|
|
|
|
| 1 |
+
"""HuggingFace Space: ghostloop control panel.
|
| 2 |
|
| 3 |
Live URL: https://huggingface.co/spaces/Ghostgim/ghostloop-demo
|
| 4 |
|
|
|
|
| 6 |
drive it via:
|
| 7 |
|
| 8 |
- Per-primitive dispatch buttons (auto-generated from the profile's
|
| 9 |
+
registry, no JSON typing required).
|
| 10 |
- Virtual joystick (D-pad style) for mobile bases / quadrupeds /
|
| 11 |
drones.
|
| 12 |
+
- Free-form Intent dispatch (advanced: JSON args).
|
| 13 |
- Live intervention controls: Pause, Resume, Emergency Stop.
|
| 14 |
- Live trace pane that updates after every dispatch.
|
| 15 |
- Live state pane showing the backend snapshot.
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
PRESETS = {
|
| 51 |
+
"franka_arm (7-DOF arm)": franka_arm,
|
| 52 |
+
"spot (Boston Dynamics quadruped)": spot_quadruped,
|
| 53 |
+
"tello (quadcopter drone)": tello_drone,
|
| 54 |
+
"stretch (mobile arm)": stretch_mobile_arm,
|
| 55 |
+
"humanoid_demo (stationary humanoid)": humanoid_demo,
|
| 56 |
+
"turtlebot (wheeled mobile base)": turtlebot_base,
|
| 57 |
}
|
| 58 |
|
| 59 |
+
DEFAULT_PROFILE = "franka_arm (7-DOF arm)"
|
| 60 |
|
| 61 |
|
| 62 |
# ---------------------------------------------------------------------------
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
# ---------------------------------------------------------------------------
|
| 136 |
+
# Render helpers. Keep all formatting in one place.
|
| 137 |
# ---------------------------------------------------------------------------
|
| 138 |
|
| 139 |
|
|
|
|
| 141 |
profile = session["profile"]
|
| 142 |
runtime = session["runtime"]
|
| 143 |
return (
|
| 144 |
+
f"### `{profile.name}` (`{profile.morphology}`)\n\n"
|
| 145 |
f"**Backend:** `{runtime.backend.name}` (mock) · "
|
| 146 |
f"**Workspace:** `{profile.workspace_bounds}` · "
|
| 147 |
f"**Max velocity:** `{profile.max_velocity}` m/s · "
|
|
|
|
| 152 |
def _render_primitives_list(session: dict[str, Any]) -> str:
|
| 153 |
runtime = session["runtime"]
|
| 154 |
return "\n".join(
|
| 155 |
+
f"- **`{name}`**: {runtime.registry.get(name).description}"
|
| 156 |
for name in runtime.registry.names()
|
| 157 |
)
|
| 158 |
|
|
|
|
| 172 |
def _render_trace(session: dict[str, Any]) -> str:
|
| 173 |
runtime = session["runtime"]
|
| 174 |
if not runtime.trace.events:
|
| 175 |
+
return "_(no events yet. Dispatch a primitive to see the trace.)_"
|
| 176 |
rows = ["| step | intent | decision | gate | result | reason |",
|
| 177 |
"|---:|---|:---:|---|:---:|---|"]
|
| 178 |
for ev in runtime.trace.events[-12:]:
|
| 179 |
+
decision_label = {"allow": "OK", "deny": "BLOCKED", "escalate": "WARN"}.get(
|
| 180 |
ev.decision.action.value, "?"
|
| 181 |
)
|
| 182 |
+
result_label = {"ok": "OK", "blocked": "BLOCKED", "error": "ERR"}.get(
|
| 183 |
ev.result.status.value, "?"
|
| 184 |
)
|
| 185 |
args_compact = json.dumps(ev.intent.args, separators=(",", ":"))
|
| 186 |
if len(args_compact) > 40:
|
| 187 |
+
args_compact = args_compact[:37] + "..."
|
| 188 |
reason = (ev.decision.reason or ev.result.message or "")[:80]
|
| 189 |
rows.append(
|
| 190 |
f"| {ev.step} | `{ev.intent.name}` {args_compact} | "
|
| 191 |
+
f"`{decision_label}` | `{ev.decision.gate_name or ''}` | "
|
| 192 |
+
f"`{result_label}` | {reason} |"
|
| 193 |
)
|
| 194 |
n = len(runtime.trace.events)
|
| 195 |
if n > 12:
|
|
|
|
| 199 |
|
| 200 |
def _render_intervention_state(session: dict[str, Any]) -> str:
|
| 201 |
state = session["controller"].state
|
| 202 |
+
label = {
|
| 203 |
+
InterventionState.RUNNING: "RUNNING",
|
| 204 |
+
InterventionState.PAUSED: "PAUSED",
|
| 205 |
+
InterventionState.SWAPPING: "SWAPPING",
|
| 206 |
+
InterventionState.EMERGENCY_STOP: "STOPPED",
|
| 207 |
+
}.get(state, "UNKNOWN")
|
| 208 |
+
return f"### Intervention: `{state.value}` [{label}]"
|
| 209 |
|
| 210 |
|
| 211 |
def _render_instructions(session: dict[str, Any]) -> str:
|
|
|
|
| 266 |
|
| 267 |
|
| 268 |
def dispatch_drive(session: dict[str, Any], linear_x: float, angular_z: float):
|
| 269 |
+
"""Joystick handler. Emits drive(linear_x, angular_z) for mobile / quad."""
|
| 270 |
runtime = session["runtime"]
|
| 271 |
name = None
|
| 272 |
if "drive" in runtime.registry.names():
|
|
|
|
| 325 |
|
| 326 |
|
| 327 |
with gr.Blocks(
|
| 328 |
+
title="ghostloop control panel",
|
| 329 |
theme=gr.themes.Soft(primary_hue="teal"),
|
| 330 |
css="""
|
| 331 |
.gl-pad-button { min-height: 56px; font-weight: 600; }
|
|
|
|
| 339 |
|
| 340 |
Pick a robot profile, then drive it through the safety pipeline. Every
|
| 341 |
dispatch goes through `Geofence + ForceCap + ActionSmoothing + RateLimit
|
| 342 |
+
+ HITL`. Try sending a `move_to` outside the workspace and watch the
|
| 343 |
geofence reject it.
|
| 344 |
|
| 345 |
Sister to **[GhostLM](https://github.com/joemunene-by/GhostLM)** · `pip install ghostloop` · [GitHub](https://github.com/joemunene-by/ghostloop) · [PyPI](https://pypi.org/project/ghostloop/)
|
|
|
|
| 370 |
instructions_md = gr.Markdown()
|
| 371 |
|
| 372 |
# ---------- Quick-dispatch buttons ----------
|
| 373 |
+
gr.Markdown("## Dispatch a primitive (one click, uses sensible defaults)")
|
| 374 |
primitive_buttons: list[gr.Button] = []
|
| 375 |
with gr.Row():
|
| 376 |
for _ in range(6):
|
|
|
|
| 380 |
primitive_buttons.append(gr.Button("", variant="primary", elem_classes="gl-pad-button"))
|
| 381 |
|
| 382 |
# ---------- Joystick (D-pad) ----------
|
| 383 |
+
gr.Markdown("## Virtual joystick: drive / walk / fly / move")
|
| 384 |
with gr.Row():
|
| 385 |
with gr.Column(scale=1):
|
| 386 |
pass
|
| 387 |
with gr.Column(scale=1):
|
| 388 |
+
joy_forward = gr.Button("FORWARD", elem_classes="gl-pad-button")
|
| 389 |
with gr.Column(scale=1):
|
| 390 |
pass
|
| 391 |
with gr.Row():
|
| 392 |
+
joy_left = gr.Button("LEFT", elem_classes="gl-pad-button")
|
| 393 |
+
joy_stop = gr.Button("STOP", elem_classes="gl-pad-button")
|
| 394 |
+
joy_right = gr.Button("RIGHT", elem_classes="gl-pad-button")
|
| 395 |
with gr.Row():
|
| 396 |
with gr.Column(scale=1):
|
| 397 |
pass
|
| 398 |
with gr.Column(scale=1):
|
| 399 |
+
joy_back = gr.Button("BACK", elem_classes="gl-pad-button")
|
| 400 |
with gr.Column(scale=1):
|
| 401 |
pass
|
| 402 |
|
|
|
|
| 404 |
gr.Markdown("## Live intervention")
|
| 405 |
intervention_md = gr.Markdown()
|
| 406 |
with gr.Row():
|
| 407 |
+
pause_btn = gr.Button("Pause", elem_classes="gl-pause")
|
| 408 |
+
resume_btn = gr.Button("Resume", elem_classes="gl-resume")
|
| 409 |
+
estop_btn = gr.Button("EMERGENCY STOP", elem_classes="gl-estop")
|
| 410 |
|
| 411 |
# ---------- Live trace + state ----------
|
| 412 |
with gr.Row():
|
|
|
|
| 427 |
value='{"x": 0.4, "y": 0.0, "z": 0.5}',
|
| 428 |
lines=2, scale=3,
|
| 429 |
)
|
| 430 |
+
adv_btn = gr.Button("runtime.step(intent)", variant="secondary")
|
| 431 |
|
| 432 |
# ---------- Try-this hints ----------
|
| 433 |
gr.Markdown("""
|
| 434 |
### Try this:
|
| 435 |
|
| 436 |
+
1. Pick the **`franka_arm`** profile, then click **`move_to`**. It dispatches with `(0.4, 0, 0.5)` and lands inside the workspace (allowed). Now expand the **Advanced** accordion and dispatch `move_to` with `{"x": 5.0, "y": 0, "z": 0}`. The GeofenceGate rejects it.
|
| 437 |
2. Switch to **`spot`**, then drive with the joystick. Each press emits `walk_to(linear_x, 0, angular_z)` through the safety pipeline.
|
| 438 |
+
3. Hit **EMERGENCY STOP**. Try clicking another primitive: it gets denied. Hit **Resume** to recover.
|
| 439 |
|
| 440 |
+
Every dispatch is recorded in the trace pane below. That's the same `TraceEvent` shape the library exports for replay, diff, query, energy ledger, and LLM-judge scoring.
|
| 441 |
""")
|
| 442 |
|
| 443 |
# ---------- Wiring ----------
|