HaptalAI commited on
Commit
7c19df4
Β·
verified Β·
1 Parent(s): 7a8fe87

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +297 -497
app.py CHANGED
@@ -1,14 +1,14 @@
1
  """
2
  RoboGen β€” HaptalAI Synthetic Robotics Dataset Generator
3
- Gradio Space: HaptalAI/robogen
4
-
5
- Step-by-step UI:
6
- 1 β†’ Robot selection (card buttons)
7
- 2 β†’ Task selection (dropdown)
8
- 3 β†’ Parameter configuration (sliders + checkboxes with tooltips)
9
- 4 β†’ Generate (progress bar)
10
- 5 β†’ Results dashboard (quality score, band, failure breakdown)
11
- 6 β†’ Email gate β†’ Download zip (parquet + README)
12
  """
13
 
14
  from __future__ import annotations
@@ -16,18 +16,15 @@ from __future__ import annotations
16
  import os
17
  import sys
18
  import io
19
- import json
20
  import zipfile
21
  import tempfile
22
  import traceback
23
  from typing import Optional, Dict, List
24
 
25
- # Allow running as python space/app.py from repo root
26
- sys.path.insert(0, os.path.dirname(__file__))
27
 
28
  import gradio as gr
29
  import pandas as pd
30
- import numpy as np
31
 
32
  from generator import (
33
  generate_dataset,
@@ -40,25 +37,13 @@ from generator import (
40
  from readme_gen import generate_readme
41
  from airtable import log_email
42
 
43
- # ── Load CSS ──────────────────────────────────────────────────────────────────
44
 
45
- _CSS_PATH = os.path.join(os.path.dirname(__file__), "style.css")
46
- with open(_CSS_PATH) as _f:
47
- _CSS = _f.read()
48
 
49
- # ── Robot display config ──────────────────────────────────────────────────────
50
-
51
- ROBOT_ICONS = {
52
- "SO-100": "SO-100",
53
- "SO-101": "SO-101",
54
- "Koch": "Koch",
55
- }
56
-
57
- ROBOT_DESCRIPTIONS = {
58
- "SO-100": "Low-cost 6-DOF arm, community favourite",
59
- "SO-101": "Upgraded SO-100 with refined kinematics",
60
- "Koch": "Koch arm β€” drawer & manipulation tasks",
61
- }
62
 
63
  TASK_LABELS = {
64
  "pick_and_place": "Pick and Place",
@@ -74,9 +59,7 @@ FAILURE_LABELS = {
74
  "torque_saturation": "Torque Saturation",
75
  }
76
 
77
- # ── Default parameters per robot Γ— task ──────────────────────────────────────
78
-
79
- DEFAULTS: Dict[str, Dict] = {
80
  "SO-100": {"n_eps": 50, "success": 70, "fmin": 1.0, "fmax": 10.0},
81
  "SO-101": {"n_eps": 50, "success": 70, "fmin": 1.0, "fmax": 10.0},
82
  "Koch": {"n_eps": 30, "success": 75, "fmin": 0.5, "fmax": 8.0},
@@ -84,39 +67,30 @@ DEFAULTS: Dict[str, Dict] = {
84
 
85
  # ── HTML helpers ──────────────────────────────────────────────────────────────
86
 
87
- def _make_results_html(result: Dict, robot: str, task: str) -> str:
88
- score = result["overall_score"]
89
- band = result["band"]
90
- n_pass = result["n_passed"]
91
- n_flag = result["n_flagged"]
92
- n_eps = result["n_episodes"]
93
  mismatch = result["mean_mismatch"]
94
  fb = result["failure_breakdown"]
95
  scorer = result["scorer_used"]
96
-
97
- band_cls = band.lower()
98
  band_desc = {
99
  "Clean": "Trajectories are smooth and anomaly-free. Ready for policy training.",
100
  "Review": "Some anomalies detected. Review flagged episodes before training.",
101
  "Flagged": "High anomaly rate. Best used for failure analysis and augmentation.",
102
  }.get(band, "")
103
-
104
- # Failure bars
105
- total_failures = sum(fb.values()) or 1
106
- bar_html = ""
107
- for key, count in sorted(fb.items(), key=lambda x: -x[1]):
108
- label = FAILURE_LABELS.get(key, key)
109
- pct = count / total_failures * 100
110
- bar_html += f"""
111
- <div class="rg-failure-bar">
112
- <span class="rg-failure-label">{label}</span>
113
- <div class="rg-bar-track"><div class="rg-bar-fill" style="width:{pct:.0f}%"></div></div>
114
- <span class="rg-bar-count">{count}</span>
115
- </div>"""
116
-
117
- task_label = TASK_LABELS.get(task, task)
118
- no_failures = "No failure episodes in dataset." if not fb else ""
119
-
120
  return f"""
121
  <div class="rg-results">
122
  <div class="rg-score-row">
@@ -129,63 +103,27 @@ def _make_results_html(result: Dict, robot: str, task: str) -> str:
129
  <div class="rg-band-desc">{band_desc}</div>
130
  </div>
131
  </div>
132
-
133
  <div class="rg-stat-grid">
134
- <div class="rg-stat">
135
- <div class="rg-stat-value">{n_eps}</div>
136
- <div class="rg-stat-label">Total Episodes</div>
137
- </div>
138
- <div class="rg-stat">
139
- <div class="rg-stat-value" style="color:var(--green)">{n_pass}</div>
140
- <div class="rg-stat-label">Passed</div>
141
- </div>
142
- <div class="rg-stat">
143
- <div class="rg-stat-value" style="color:var(--red)">{n_flag}</div>
144
- <div class="rg-stat-label">Flagged</div>
145
- </div>
146
- <div class="rg-stat">
147
- <div class="rg-stat-value">{mismatch:.3f}</div>
148
- <div class="rg-stat-label">Mean Mismatch</div>
149
- </div>
150
- <div class="rg-stat">
151
- <div class="rg-stat-value">{robot}</div>
152
- <div class="rg-stat-label">Robot</div>
153
- </div>
154
- <div class="rg-stat">
155
- <div class="rg-stat-value" style="font-size:0.9rem">{task_label}</div>
156
- <div class="rg-stat-label">Task</div>
157
- </div>
158
  </div>
159
-
160
  <div class="rg-failure-section">
161
  <div class="rg-failure-title">Failure Type Breakdown</div>
162
- {bar_html or no_failures}
163
  </div>
164
-
165
  <div class="rg-scorer-note">
166
  Scored by HaptalAI misalignment benchmark &middot; scorer: <code>{scorer}</code>
167
  </div>
168
- </div>
169
- """
170
-
171
-
172
- # ── Download bundle builder ───────────────────────────────────────────────────
173
 
174
- def _build_zip(
175
- df: pd.DataFrame,
176
- result: Dict,
177
- robot: str,
178
- task: str,
179
- n_eps: int,
180
- success: float,
181
- fmin: float,
182
- fmax: float,
183
- failures: List[str],
184
- ) -> str:
185
- """Annotate DF, write parquet + README into a temp zip, return zip path."""
186
- df_annotated = annotate_quality_scores(df, result)
187
 
188
- readme_md = generate_readme(
 
 
189
  robot=robot, task=task, n_episodes=n_eps,
190
  success_rate=success / 100, force_min=fmin, force_max=fmax,
191
  failures=failures,
@@ -195,423 +133,285 @@ def _build_zip(
195
  failure_breakdown=result["failure_breakdown"],
196
  scorer_used=result["scorer_used"],
197
  )
198
-
199
- tag = f"{robot.replace('-', '')}_{task}"
200
- zip_fd, zip_path = tempfile.mkstemp(suffix=".zip", prefix=f"robogen_{tag}_")
201
- os.close(zip_fd)
202
-
203
- with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
204
- # Parquet
205
  buf = io.BytesIO()
206
- df_annotated.to_parquet(buf, index=False)
207
  zf.writestr(f"robogen_{tag}.parquet", buf.getvalue())
208
- # README
209
- zf.writestr("README.md", readme_md.encode("utf-8"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- return zip_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
 
214
- # ── Gradio app ────────────────────────────────────────────────────────────────
215
 
216
- def build_app() -> gr.Blocks:
217
 
218
- with gr.Blocks(
219
- css=_CSS,
220
- theme=gr.themes.Base(
221
- primary_hue="purple",
222
- neutral_hue="slate",
223
- ),
224
- title="RoboGen β€” Synthetic Robotics Datasets",
225
- analytics_enabled=False,
226
- ) as demo:
227
 
228
- # ── Persistent state ──────────────────────────────────────────────
229
- robot_state = gr.State("")
230
- df_state = gr.State(None)
231
- result_state = gr.State(None)
 
 
232
 
233
- # ── Header ────────────────────────────────────────────────────────
 
234
  gr.HTML("""
235
- <div class="rg-header">
236
- <div class="rg-logo">RoboGen</div>
237
- <div class="rg-tagline">Synthetic robotics datasets, physics-accurate &amp; quality-scored</div>
238
- <div class="rg-badge">LeRobot-format &nbsp;&middot;&nbsp; SO-100 / SO-101 / Koch &nbsp;&middot;&nbsp; HaptalAI</div>
239
- </div>
240
- """)
241
-
242
- # ────────────────────────────────────────────────────────────────────
243
- # STEP 1 β€” Robot selection
244
- # ────────────────────────────────────────────────────────────────────
245
- with gr.Group(elem_classes=["step-card"]):
246
- gr.HTML("""
247
- <div class="step-header">
248
- <span class="step-num">1</span>
249
- <span class="step-title">Select Robot</span>
250
- </div>""")
251
-
252
- robot_select = gr.Radio(
253
- choices=["SO-100", "Koch", "SO-101"],
254
- value=None,
255
- label="",
256
- elem_classes=["robot-radio"],
257
- )
258
 
259
- # ────────────────────────────────────────────────────────────────────
260
- # STEP 2 β€” Task selection
261
- # ────────────────────────────────────────────────────────────────────
262
- with gr.Group(visible=False, elem_classes=["step-card"]) as step2_grp:
263
- gr.HTML("""
264
- <div class="step-header">
265
- <span class="step-num">2</span>
266
- <span class="step-title">Select Task</span>
267
- </div>""")
268
-
269
- task_select = gr.Dropdown(
270
- choices=[],
271
- value=None,
272
- label="Task",
273
- interactive=True,
 
 
 
 
 
 
274
  )
275
-
276
- # ────────────────────────────────────────────────────────────────────
277
- # STEP 3 β€” Parameters
278
- # ────────────────────────────────────────────────────────────────────
279
- with gr.Group(visible=False, elem_classes=["step-card"]) as step3_grp:
280
- gr.HTML("""
281
- <div class="step-header">
282
- <span class="step-num">3</span>
283
- <span class="step-title">Configure Parameters</span>
284
- </div>""")
285
-
286
- with gr.Row():
287
- n_episodes_slider = gr.Slider(
288
- minimum=10, maximum=500, value=50, step=5,
289
- label="Number of Episodes",
290
- info="Total episodes in the dataset (10–500)",
291
- )
292
- success_slider = gr.Slider(
293
- minimum=0, maximum=100, value=70, step=5,
294
- label="Success Rate (%)",
295
- info="Fraction of episodes with successful trajectories",
296
- )
297
-
298
- with gr.Row():
299
- force_min_slider = gr.Slider(
300
- minimum=0.1, maximum=10.0, value=1.0, step=0.1,
301
- label="Min Contact Force (N)",
302
- info="Lower bound of spring-damper contact force during grasping",
303
- )
304
- force_max_slider = gr.Slider(
305
- minimum=1.0, maximum=20.0, value=10.0, step=0.5,
306
- label="Max Contact Force (N)",
307
- info="Upper bound of contact force β€” higher = firmer grip",
308
- )
309
-
310
- gr.HTML("""
311
- <div style="margin: 4px 0 8px;font-size:0.82rem;color:#8892a4;">
312
- <b>Failure types to include</b> &nbsp;
313
- <span style="font-style:italic;">
314
- Grasp Slip β€” gripper opens mid-episode &nbsp;|&nbsp;
315
- Velocity Spike β€” servo glitch (z&gt;6.5) &nbsp;|&nbsp;
316
- Torque Saturation β€” joint hits angular limit
317
- </span>
318
- </div>""")
319
-
320
- failure_check = gr.CheckboxGroup(
321
- choices=["grasp_slip", "velocity_spike", "torque_saturation"],
322
- value=["grasp_slip", "velocity_spike", "torque_saturation"],
323
- label="",
324
- elem_classes=["checkbox-group"],
325
  )
326
-
327
- # ────────────────────────────────────────────────────────────────────
328
- # STEP 4 β€” Generate
329
- # ────────────────────────────────────────────────────────────────────
330
- with gr.Group(visible=False, elem_classes=["step-card"]) as step4_grp:
331
- gr.HTML("""
332
- <div class="step-header">
333
- <span class="step-num">4</span>
334
- <span class="step-title">Generate Dataset</span>
335
- </div>""")
336
-
337
- generate_btn = gr.Button(
338
- "Generate Dataset",
339
- elem_classes=["btn-generate"],
340
- size="lg",
341
  )
342
- gen_status = gr.Markdown("", elem_classes=["status-msg"])
343
-
344
- # ────────────────────────────────────────────────────────────────────
345
- # STEP 5 β€” Results dashboard
346
- # ────────────────────────────────────────────────────────────────────
347
- with gr.Group(visible=False, elem_classes=["step-card"]) as step5_grp:
348
- gr.HTML("""
349
- <div class="step-header">
350
- <span class="step-num">5</span>
351
- <span class="step-title">Quality Results</span>
352
- </div>""")
353
- results_html = gr.HTML("")
354
-
355
- # ────────────────────────────────────────────────────────────────────
356
- # STEP 6 β€” Email gate + Download
357
- # ────────────────────────────────────────────────────────────────────
358
- with gr.Group(visible=False, elem_classes=["step-card"]) as step6_grp:
359
- gr.HTML("""
360
- <div class="step-header">
361
- <span class="step-num">6</span>
362
- <span class="step-title">Download Dataset</span>
363
- </div>
364
- <div class="email-gate-note">
365
- Enter your email to unlock the download. You'll receive occasional
366
- updates on new robot configs and dataset improvements.
367
- </div>""")
368
-
369
- with gr.Row():
370
- email_input = gr.Textbox(
371
- placeholder="you@example.com",
372
- label="Email",
373
- scale=4,
374
- max_lines=1,
375
- )
376
- email_btn = gr.Button(
377
- "Confirm β†’",
378
- elem_classes=["btn-primary"],
379
- scale=1,
380
- )
381
-
382
- email_status = gr.Markdown("", visible=True)
383
-
384
- download_file = gr.File(
385
- label="Download robogen_dataset.zip",
386
- visible=False,
387
- interactive=False,
388
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
- # ════════════════════════════════════════════════════════════════════
391
- # EVENT HANDLERS
392
- # ════════════════════════════════════════════════════════════════════
393
-
394
- # ── Step 1 β†’ Step 2: Robot selected ──────────────────────────────
395
- def on_robot_select(robot: str):
396
- if not robot:
397
- return (
398
- gr.update(visible=False),
399
- gr.update(choices=[], value=None),
400
- gr.update(visible=False),
401
- gr.update(visible=False),
402
- robot,
403
- )
404
- tasks_raw = TASKS_BY_ROBOT[robot]
405
- tasks_disp = [(TASK_LABELS.get(t, t), t) for t in tasks_raw]
406
- d = DEFAULTS.get(robot, DEFAULTS["SO-100"])
407
- return (
408
- gr.update(visible=True), # step2_grp
409
- gr.update(choices=tasks_disp, value=tasks_raw[0]), # task_select
410
- gr.update(visible=False), # step3_grp
411
- gr.update(visible=False), # step4_grp
412
- robot, # robot_state
 
 
 
 
 
 
 
 
 
 
 
413
  )
 
 
 
414
 
415
- robot_select.change(
416
- on_robot_select,
417
- inputs=[robot_select],
418
- outputs=[step2_grp, task_select, step3_grp, step4_grp, robot_state],
419
- )
420
 
421
- # ── Step 2 β†’ Step 3: Task selected ───────────────────────────────
422
- def on_task_select(task: str, robot: str):
423
- if not task or not robot:
424
- return (
425
- gr.update(visible=False),
426
- gr.update(visible=False),
427
- 50, 70, 1.0, 10.0,
428
- )
429
- d = DEFAULTS.get(robot, DEFAULTS["SO-100"])
430
- cfg_fr = ROBOT_CONFIG[robot]["force_range"]
431
- return (
432
- gr.update(visible=True), # step3_grp
433
- gr.update(visible=True), # step4_grp
434
- d["n_eps"],
435
- d["success"],
436
- cfg_fr[0],
437
- cfg_fr[1],
438
- )
439
 
440
- task_select.change(
441
- on_task_select,
442
- inputs=[task_select, robot_state],
443
- outputs=[
444
- step3_grp, step4_grp,
445
- n_episodes_slider, success_slider,
446
- force_min_slider, force_max_slider,
447
- ],
448
- )
449
 
450
- # ── Step 4: Generate ─────────────────────────────────────────────
451
- def on_generate(
452
- robot, task, n_eps, success_pct, fmin, fmax, failures,
453
- progress=gr.Progress(),
454
- ):
455
- if not robot or not task:
456
- return (
457
- "Please complete steps 1 and 2 first.",
458
- gr.update(visible=False),
459
- gr.update(visible=False),
460
- gr.update(visible=False),
461
- None, None,
462
- )
463
- if not failures:
464
- failures = list(FAILURE_TYPES)
465
-
466
- try:
467
- # ── Generation ──────────────────────────────────────────
468
- def gen_progress(frac, msg):
469
- progress(frac * 0.65, desc=msg)
470
-
471
- progress(0.0, desc="Generating episodes…")
472
- df = generate_dataset(
473
- robot=robot, task=task,
474
- n_episodes=int(n_eps),
475
- success_rate=success_pct / 100,
476
- force_min=float(fmin), force_max=float(fmax),
477
- enabled_failures=list(failures),
478
- seed=None,
479
- progress_callback=gen_progress,
480
- )
481
-
482
- # ── Scoring ─────────────────────────────────────────────
483
- progress(0.70, desc="Running quality checks…")
484
-
485
- def score_progress(frac, msg):
486
- progress(0.70 + frac * 0.20, desc=msg)
487
-
488
- result = score_dataset(df, progress_callback=score_progress)
489
-
490
- progress(0.92, desc="Preparing results…")
491
- results_panel = _make_results_html(result, robot, task)
492
- progress(1.0, desc="Done")
493
-
494
- status = (
495
- f"Generated {len(df):,} rows across {result['n_episodes']} episodes β€” "
496
- f"score **{result['overall_score']:.1f}/100** ({result['band']})"
497
- )
498
-
499
- return (
500
- status,
501
- gr.update(visible=True), # step5_grp
502
- results_panel, # results_html
503
- gr.update(visible=True), # step6_grp
504
- df, # df_state
505
- result, # result_state
506
- )
507
-
508
- except Exception:
509
- err = traceback.format_exc()
510
- return (
511
- f"Generation failed:\n```\n{err}\n```",
512
- gr.update(visible=False),
513
- "",
514
- gr.update(visible=False),
515
- None, None,
516
- )
517
-
518
- generate_btn.click(
519
- on_generate,
520
- inputs=[
521
- robot_state, task_select,
522
- n_episodes_slider, success_slider,
523
- force_min_slider, force_max_slider,
524
- failure_check,
525
- ],
526
- outputs=[
527
- gen_status,
528
- step5_grp, results_html,
529
- step6_grp,
530
- df_state, result_state,
531
- ],
532
- )
533
 
534
- # ── Step 6: Email gate β†’ unlock download ──────────────────────────
535
- def on_email_submit(
536
- email: str,
537
- robot: str,
538
- task: str,
539
- n_eps: float,
540
- success_pct: float,
541
- fmin: float,
542
- fmax: float,
543
- failures: List[str],
544
- df: Optional[pd.DataFrame],
545
- result: Optional[Dict],
546
- ):
547
- if not email or "@" not in email:
548
- return (
549
- "Please enter a valid email address.",
550
- gr.update(visible=False),
551
- )
552
- if df is None or result is None:
553
- return (
554
- "Generate a dataset first (Step 4).",
555
- gr.update(visible=False),
556
- )
557
-
558
- # Fire Airtable (failure is non-blocking)
559
- try:
560
- ok, msg = log_email(
561
- email=email.strip(),
562
- robot=robot, task=task,
563
- n_episodes=int(n_eps),
564
- quality_score=result["overall_score"],
565
- band=result["band"],
566
- )
567
- if not ok:
568
- print(f"[RoboGen] Airtable log failed: {msg}")
569
- except Exception as exc:
570
- print(f"[RoboGen] Airtable exception: {exc}")
571
-
572
- # Build download zip regardless of Airtable outcome
573
- try:
574
- zip_path = _build_zip(
575
- df=df, result=result,
576
- robot=robot, task=task,
577
- n_eps=int(n_eps), success=success_pct,
578
- fmin=float(fmin), fmax=float(fmax),
579
- failures=list(failures),
580
- )
581
- return (
582
- "Email confirmed. Your download is ready below.",
583
- gr.update(visible=True, value=zip_path),
584
- )
585
- except Exception:
586
- err = traceback.format_exc()
587
- return (
588
- f"Download preparation failed:\n```\n{err}\n```",
589
- gr.update(visible=False),
590
- )
591
-
592
- email_btn.click(
593
- on_email_submit,
594
- inputs=[
595
- email_input,
596
- robot_state, task_select,
597
  n_episodes_slider, success_slider,
598
  force_min_slider, force_max_slider,
599
- failure_check,
600
- df_state, result_state,
601
- ],
602
- outputs=[email_status, download_file],
603
- )
604
-
605
- return demo
606
 
 
607
 
608
- # ── Entry point ───────────────────────────────────────────────────────────────
609
 
610
  if __name__ == "__main__":
611
- app = build_app()
612
- app.queue()
613
- app.launch(
614
- server_name="0.0.0.0",
615
- server_port=int(os.environ.get("PORT", 7860)),
616
- show_error=True,
617
- )
 
1
  """
2
  RoboGen β€” HaptalAI Synthetic Robotics Dataset Generator
3
+ Gradio 5.9.1 / Python 3.11
4
+
5
+ Step flow:
6
+ 1 Robot selection (card-style radio)
7
+ 2 Task dropdown
8
+ 3 Parameter sliders + failure checkboxes
9
+ 4 Generate button
10
+ 5 Quality results dashboard
11
+ 6 Email gate + zip download
12
  """
13
 
14
  from __future__ import annotations
 
16
  import os
17
  import sys
18
  import io
 
19
  import zipfile
20
  import tempfile
21
  import traceback
22
  from typing import Optional, Dict, List
23
 
24
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 
25
 
26
  import gradio as gr
27
  import pandas as pd
 
28
 
29
  from generator import (
30
  generate_dataset,
 
37
  from readme_gen import generate_readme
38
  from airtable import log_email
39
 
40
+ # ── CSS ───────────────────────────────────────────────────────────────────────
41
 
42
+ _here = os.path.dirname(os.path.abspath(__file__))
43
+ with open(os.path.join(_here, "style.css")) as _f:
44
+ CSS = _f.read()
45
 
46
+ # ── Constants ─────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  TASK_LABELS = {
49
  "pick_and_place": "Pick and Place",
 
59
  "torque_saturation": "Torque Saturation",
60
  }
61
 
62
+ DEFAULTS = {
 
 
63
  "SO-100": {"n_eps": 50, "success": 70, "fmin": 1.0, "fmax": 10.0},
64
  "SO-101": {"n_eps": 50, "success": 70, "fmin": 1.0, "fmax": 10.0},
65
  "Koch": {"n_eps": 30, "success": 75, "fmin": 0.5, "fmax": 8.0},
 
67
 
68
  # ── HTML helpers ──────────────────────────────────────────────────────────────
69
 
70
+ def _results_html(result: Dict, robot: str, task: str) -> str:
71
+ score = result["overall_score"]
72
+ band = result["band"]
73
+ n_pass = result["n_passed"]
74
+ n_flag = result["n_flagged"]
75
+ n_eps = result["n_episodes"]
76
  mismatch = result["mean_mismatch"]
77
  fb = result["failure_breakdown"]
78
  scorer = result["scorer_used"]
79
+ band_cls = band.lower()
 
80
  band_desc = {
81
  "Clean": "Trajectories are smooth and anomaly-free. Ready for policy training.",
82
  "Review": "Some anomalies detected. Review flagged episodes before training.",
83
  "Flagged": "High anomaly rate. Best used for failure analysis and augmentation.",
84
  }.get(band, "")
85
+ total = sum(fb.values()) or 1
86
+ bars = "".join(
87
+ f'<div class="rg-failure-bar">'
88
+ f'<span class="rg-failure-label">{FAILURE_LABELS.get(k,k)}</span>'
89
+ f'<div class="rg-bar-track"><div class="rg-bar-fill" style="width:{v/total*100:.0f}%"></div></div>'
90
+ f'<span class="rg-bar-count">{v}</span></div>'
91
+ for k, v in sorted(fb.items(), key=lambda x: -x[1])
92
+ )
93
+ task_label = TASK_LABELS.get(task, task)
 
 
 
 
 
 
 
 
94
  return f"""
95
  <div class="rg-results">
96
  <div class="rg-score-row">
 
103
  <div class="rg-band-desc">{band_desc}</div>
104
  </div>
105
  </div>
 
106
  <div class="rg-stat-grid">
107
+ <div class="rg-stat"><div class="rg-stat-value">{n_eps}</div><div class="rg-stat-label">Total Episodes</div></div>
108
+ <div class="rg-stat"><div class="rg-stat-value" style="color:var(--green)">{n_pass}</div><div class="rg-stat-label">Passed</div></div>
109
+ <div class="rg-stat"><div class="rg-stat-value" style="color:var(--red)">{n_flag}</div><div class="rg-stat-label">Flagged</div></div>
110
+ <div class="rg-stat"><div class="rg-stat-value">{mismatch:.3f}</div><div class="rg-stat-label">Mean Mismatch</div></div>
111
+ <div class="rg-stat"><div class="rg-stat-value">{robot}</div><div class="rg-stat-label">Robot</div></div>
112
+ <div class="rg-stat"><div class="rg-stat-value" style="font-size:0.9rem">{task_label}</div><div class="rg-stat-label">Task</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  </div>
 
114
  <div class="rg-failure-section">
115
  <div class="rg-failure-title">Failure Type Breakdown</div>
116
+ {bars or "No failure episodes in dataset."}
117
  </div>
 
118
  <div class="rg-scorer-note">
119
  Scored by HaptalAI misalignment benchmark &middot; scorer: <code>{scorer}</code>
120
  </div>
121
+ </div>"""
 
 
 
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ def _build_zip(df, result, robot, task, n_eps, success, fmin, fmax, failures) -> str:
125
+ df_out = annotate_quality_scores(df, result)
126
+ readme = generate_readme(
127
  robot=robot, task=task, n_episodes=n_eps,
128
  success_rate=success / 100, force_min=fmin, force_max=fmax,
129
  failures=failures,
 
133
  failure_breakdown=result["failure_breakdown"],
134
  scorer_used=result["scorer_used"],
135
  )
136
+ tag = f"{robot.replace('-','')}_{task}"
137
+ fd, path = tempfile.mkstemp(suffix=".zip", prefix=f"robogen_{tag}_")
138
+ os.close(fd)
139
+ with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
 
 
 
140
  buf = io.BytesIO()
141
+ df_out.to_parquet(buf, index=False)
142
  zf.writestr(f"robogen_{tag}.parquet", buf.getvalue())
143
+ zf.writestr("README.md", readme.encode("utf-8"))
144
+ return path
145
+
146
+
147
+ # ── Event handlers (module level β€” Gradio 5 requirement) ─────────────────────
148
+
149
+ def on_robot_select(robot: str):
150
+ if not robot:
151
+ return (
152
+ gr.update(visible=False),
153
+ gr.update(choices=[], value=None),
154
+ gr.update(visible=False),
155
+ gr.update(visible=False),
156
+ "",
157
+ )
158
+ tasks_raw = TASKS_BY_ROBOT[robot]
159
+ tasks_disp = [(TASK_LABELS.get(t, t), t) for t in tasks_raw]
160
+ return (
161
+ gr.update(visible=True),
162
+ gr.update(choices=tasks_disp, value=tasks_raw[0]),
163
+ gr.update(visible=False),
164
+ gr.update(visible=False),
165
+ robot,
166
+ )
167
+
168
+
169
+ def on_task_select(task: str, robot: str):
170
+ if not task or not robot:
171
+ return gr.update(visible=False), gr.update(visible=False), 50, 70, 1.0, 10.0
172
+ d = DEFAULTS.get(robot, DEFAULTS["SO-100"])
173
+ fr = ROBOT_CONFIG[robot]["force_range"]
174
+ return (
175
+ gr.update(visible=True),
176
+ gr.update(visible=True),
177
+ d["n_eps"],
178
+ d["success"],
179
+ fr[0],
180
+ fr[1],
181
+ )
182
+
183
+
184
+ def on_generate(robot, task, n_eps, success_pct, fmin, fmax, failures):
185
+ if not robot or not task:
186
+ return (
187
+ "Please complete steps 1 and 2 first.",
188
+ gr.update(visible=False), "",
189
+ gr.update(visible=False),
190
+ None, None,
191
+ )
192
+ if not failures:
193
+ failures = list(FAILURE_TYPES)
194
+ try:
195
+ df = generate_dataset(
196
+ robot=robot, task=task,
197
+ n_episodes=int(n_eps),
198
+ success_rate=float(success_pct) / 100,
199
+ force_min=float(fmin), force_max=float(fmax),
200
+ enabled_failures=list(failures),
201
+ seed=None,
202
+ )
203
+ result = score_dataset(df)
204
+ panel = _results_html(result, robot, task)
205
+ status = (
206
+ f"Generated {len(df):,} rows across {result['n_episodes']} episodes β€” "
207
+ f"score **{result['overall_score']:.1f}/100** ({result['band']})"
208
+ )
209
+ return (
210
+ status,
211
+ gr.update(visible=True), panel,
212
+ gr.update(visible=True),
213
+ df, result,
214
+ )
215
+ except Exception:
216
+ return (
217
+ f"Generation failed:\n```\n{traceback.format_exc()}\n```",
218
+ gr.update(visible=False), "",
219
+ gr.update(visible=False),
220
+ None, None,
221
+ )
222
 
223
+
224
+ def on_email_submit(email, robot, task, n_eps, success_pct, fmin, fmax, failures, df, result):
225
+ if not email or "@" not in email:
226
+ return "Please enter a valid email address.", gr.update(visible=False)
227
+ if df is None or result is None:
228
+ return "Generate a dataset first (Step 4).", gr.update(visible=False)
229
+ try:
230
+ ok, msg = log_email(
231
+ email=email.strip(), robot=robot, task=task,
232
+ n_episodes=int(n_eps),
233
+ quality_score=result["overall_score"],
234
+ band=result["band"],
235
+ )
236
+ if not ok:
237
+ print(f"[RoboGen] Airtable: {msg}")
238
+ except Exception as exc:
239
+ print(f"[RoboGen] Airtable exception: {exc}")
240
+ try:
241
+ path = _build_zip(
242
+ df=df, result=result, robot=robot, task=task,
243
+ n_eps=int(n_eps), success=float(success_pct),
244
+ fmin=float(fmin), fmax=float(fmax),
245
+ failures=list(failures),
246
+ )
247
+ return "Email confirmed. Your download is ready below.", gr.update(visible=True, value=path)
248
+ except Exception:
249
+ return (
250
+ f"Download preparation failed:\n```\n{traceback.format_exc()}\n```",
251
+ gr.update(visible=False),
252
+ )
253
 
254
 
255
+ # ── Build UI ──────────────────────────────────────────────────────────────────
256
 
257
+ with gr.Blocks(css=CSS, title="RoboGen") as demo:
258
 
259
+ robot_state = gr.State("")
260
+ df_state = gr.State(None)
261
+ result_state = gr.State(None)
 
 
 
 
 
 
262
 
263
+ gr.HTML("""
264
+ <div class="rg-header">
265
+ <div class="rg-logo">RoboGen</div>
266
+ <div class="rg-tagline">Synthetic robotics datasets, physics-accurate &amp; quality-scored</div>
267
+ <div class="rg-badge">LeRobot-format &nbsp;&middot;&nbsp; SO-100 / SO-101 / Koch &nbsp;&middot;&nbsp; HaptalAI</div>
268
+ </div>""")
269
 
270
+ # ── Step 1 ────────────────────────────────────────────────────────────────
271
+ with gr.Group(elem_classes=["step-card"]):
272
  gr.HTML("""
273
+ <div class="step-header">
274
+ <span class="step-num">1</span>
275
+ <span class="step-title">Select Robot</span>
276
+ </div>""")
277
+ robot_select = gr.Radio(
278
+ choices=["SO-100", "Koch", "SO-101"],
279
+ value=None,
280
+ label="",
281
+ elem_classes=["robot-radio"],
282
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
+ # ── Step 2 ────────────────────────────────────────────────────────────────
285
+ with gr.Group(visible=False, elem_classes=["step-card"]) as step2_grp:
286
+ gr.HTML("""
287
+ <div class="step-header">
288
+ <span class="step-num">2</span>
289
+ <span class="step-title">Select Task</span>
290
+ </div>""")
291
+ task_select = gr.Dropdown(choices=[], value=None, label="Task", interactive=True)
292
+
293
+ # ── Step 3 ────────────────────────────────────────────────────────────────
294
+ with gr.Group(visible=False, elem_classes=["step-card"]) as step3_grp:
295
+ gr.HTML("""
296
+ <div class="step-header">
297
+ <span class="step-num">3</span>
298
+ <span class="step-title">Configure Parameters</span>
299
+ </div>""")
300
+ with gr.Row():
301
+ n_episodes_slider = gr.Slider(
302
+ minimum=10, maximum=500, value=50, step=5,
303
+ label="Number of Episodes",
304
+ info="Total episodes in the dataset (10–500)",
305
  )
306
+ success_slider = gr.Slider(
307
+ minimum=0, maximum=100, value=70, step=5,
308
+ label="Success Rate (%)",
309
+ info="Fraction of episodes with successful trajectories",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  )
311
+ with gr.Row():
312
+ force_min_slider = gr.Slider(
313
+ minimum=0.1, maximum=10.0, value=1.0, step=0.1,
314
+ label="Min Contact Force (N)",
315
+ info="Lower bound of spring-damper contact force during grasping",
 
 
 
 
 
 
 
 
 
 
316
  )
317
+ force_max_slider = gr.Slider(
318
+ minimum=1.0, maximum=20.0, value=10.0, step=0.5,
319
+ label="Max Contact Force (N)",
320
+ info="Upper bound of contact force β€” higher = firmer grip",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  )
322
+ gr.HTML("""
323
+ <div style="margin:4px 0 8px;font-size:0.82rem;color:#8892a4;">
324
+ <b>Failure types to include</b> &nbsp;
325
+ <span style="font-style:italic;">
326
+ Grasp Slip β€” gripper opens mid-episode &nbsp;|&nbsp;
327
+ Velocity Spike β€” servo glitch (z&gt;6.5) &nbsp;|&nbsp;
328
+ Torque Saturation β€” joint hits angular limit
329
+ </span>
330
+ </div>""")
331
+ failure_check = gr.CheckboxGroup(
332
+ choices=["grasp_slip", "velocity_spike", "torque_saturation"],
333
+ value=["grasp_slip", "velocity_spike", "torque_saturation"],
334
+ label="",
335
+ elem_classes=["checkbox-group"],
336
+ )
337
 
338
+ # ── Step 4 ────────────────────────────────────────────────────────────────
339
+ with gr.Group(visible=False, elem_classes=["step-card"]) as step4_grp:
340
+ gr.HTML("""
341
+ <div class="step-header">
342
+ <span class="step-num">4</span>
343
+ <span class="step-title">Generate Dataset</span>
344
+ </div>""")
345
+ generate_btn = gr.Button("Generate Dataset", elem_classes=["btn-generate"], size="lg")
346
+ gen_status = gr.Markdown("", elem_classes=["status-msg"])
347
+
348
+ # ── Step 5 ────────────────────────────────────────────────────────────────
349
+ with gr.Group(visible=False, elem_classes=["step-card"]) as step5_grp:
350
+ gr.HTML("""
351
+ <div class="step-header">
352
+ <span class="step-num">5</span>
353
+ <span class="step-title">Quality Results</span>
354
+ </div>""")
355
+ results_html = gr.HTML("")
356
+
357
+ # ── Step 6 ────────────────────────────────────────────────────────────────
358
+ with gr.Group(visible=False, elem_classes=["step-card"]) as step6_grp:
359
+ gr.HTML("""
360
+ <div class="step-header">
361
+ <span class="step-num">6</span>
362
+ <span class="step-title">Download Dataset</span>
363
+ </div>
364
+ <div class="email-gate-note">
365
+ Enter your email to unlock the download. You'll receive occasional
366
+ updates on new robot configs and dataset improvements.
367
+ </div>""")
368
+ with gr.Row():
369
+ email_input = gr.Textbox(
370
+ placeholder="you@example.com", label="Email",
371
+ scale=4, max_lines=1,
372
  )
373
+ email_btn = gr.Button("Confirm", elem_classes=["btn-primary"], scale=1)
374
+ email_status = gr.Markdown("")
375
+ download_file = gr.File(label="Download robogen_dataset.zip", visible=False)
376
 
377
+ # ── Wire events ───────────────────────────────────────────────────────────
 
 
 
 
378
 
379
+ robot_select.change(
380
+ fn=on_robot_select,
381
+ inputs=[robot_select],
382
+ outputs=[step2_grp, task_select, step3_grp, step4_grp, robot_state],
383
+ api_name=False,
384
+ )
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
+ task_select.change(
387
+ fn=on_task_select,
388
+ inputs=[task_select, robot_state],
389
+ outputs=[step3_grp, step4_grp, n_episodes_slider, success_slider,
390
+ force_min_slider, force_max_slider],
391
+ api_name=False,
392
+ )
 
 
393
 
394
+ generate_btn.click(
395
+ fn=on_generate,
396
+ inputs=[robot_state, task_select, n_episodes_slider, success_slider,
397
+ force_min_slider, force_max_slider, failure_check],
398
+ outputs=[gen_status, step5_grp, results_html, step6_grp, df_state, result_state],
399
+ api_name=False,
400
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
+ email_btn.click(
403
+ fn=on_email_submit,
404
+ inputs=[email_input, robot_state, task_select,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  n_episodes_slider, success_slider,
406
  force_min_slider, force_max_slider,
407
+ failure_check, df_state, result_state],
408
+ outputs=[email_status, download_file],
409
+ api_name=False,
410
+ )
 
 
 
411
 
412
+ # ── Launch ────────────────────────────────────────────────────────────────────
413
 
414
+ demo.queue()
415
 
416
  if __name__ == "__main__":
417
+ demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))