Ghostgim commited on
Commit
4929a5b
·
verified ·
1 Parent(s): d5cc565
Files changed (2) hide show
  1. README.md +1 -1
  2. app.py +43 -43
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 — that's the same shape the
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 ghostloop control panel.
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 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,15 +48,15 @@ from ghostloop.profiles import (
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,7 +133,7 @@ DEFAULT_ARGS: dict[str, dict[str, Any]] = {
133
 
134
 
135
  # ---------------------------------------------------------------------------
136
- # Render helpers keep all formatting in one place.
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}` `{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,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}`** {runtime.registry.get(name).description}"
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 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_icon = {"allow": "", "deny": "🚫", "escalate": "⚠️"}.get(
180
  ev.decision.action.value, "?"
181
  )
182
- result_icon = {"ok": "", "blocked": "🚫", "error": ""}.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_icon} | `{ev.decision.gate_name or ''}` | "
192
- f"{result_icon} | {reason} |"
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
- icon = {
203
- InterventionState.RUNNING: "🟢",
204
- InterventionState.PAUSED: "⏸️",
205
- InterventionState.SWAPPING: "🔄",
206
- InterventionState.EMERGENCY_STOP: "🛑",
207
- }.get(state, "?")
208
- return f"### {icon} Intervention: `{state.value}`"
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 emits drive(linear_x, angular_z) for mobile / quad."""
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 control panel",
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` 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,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 uses sensible defaults)")
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 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,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("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,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("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 . 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 ----------
 
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 ----------