import json import os import shutil import subprocess import uuid from pathlib import Path import gradio as gr from huggingface_hub import snapshot_download LEGUARD_BIN = os.environ.get("LEGUARD_BIN", "/usr/local/bin/leguard") WORKDIR = Path("/tmp/leguard-space") DATASETS_DIR = WORKDIR / "datasets" RUNS_DIR = WORKDIR / "runs" CHECK_ORDER = [ "structure", "schema", "consistency", "episodes", "temporal", "numerical", "video", "annotation", "training", "portability", ] BANNER_URL = "https://raw.githubusercontent.com/0xPraedico/LeGuard/main/assets/leguard-banner.png" REPO_URL = "https://github.com/0xPraedico/LeGuard" ISSUES_URL = f"{REPO_URL}/issues" BOOTSTRAP_JS = """ () => { const root = document.documentElement; const applyTheme = (mode) => { const normalized = String(mode || "light").toLowerCase(); const finalMode = normalized === "light" ? "light" : "dark"; root.setAttribute("data-leguard-theme", finalMode); }; window.__leguardApplyTheme = applyTheme; applyTheme("light"); const localizeFooter = () => { const localizedBuiltWith = [67, 114, 233, 233, 32, 97, 118, 101, 99] .map((cp) => String.fromCodePoint(cp)) .join(""); const localizedSettings = [80, 97, 114, 97, 109, 232, 116, 114, 101, 115] .map((cp) => String.fromCodePoint(cp)) .join(""); const localizedError = [69, 114, 114, 101, 117, 114] .map((cp) => String.fromCodePoint(cp)) .join(""); const localizedWarning = [65, 118, 101, 114, 116, 105, 115, 115, 101, 109, 101, 110, 116] .map((cp) => String.fromCodePoint(cp)) .join(""); const replacer = { [localizedBuiltWith]: "Built with", [localizedSettings]: "Settings", [localizedError]: "Error", [localizedWarning]: "Warning", }; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let current = walker.nextNode(); while (current) { if (current.nodeValue) { Object.entries(replacer).forEach(([from, to]) => { if (current.nodeValue.includes(from)) { current.nodeValue = current.nodeValue.replace(from, to); } }); } current = walker.nextNode(); } }; localizeFooter(); const observer = new MutationObserver(() => localizeFooter()); observer.observe(document.body, { childList: true, subtree: true }); } """ THEME_CHANGE_JS = """ (mode) => { if (window.__leguardApplyTheme) { window.__leguardApplyTheme(mode); } return []; } """ CHECK_LABELS = { "structure": "Structure", "schema": "Schema", "consistency": "Consistency", "episodes": "Episodes", "temporal": "Temporal", "numerical": "Numerical", "video": "Video", "annotation": "Annotation", "training": "Training Readiness", "portability": "Portability", } CSS = """ :root, html[data-leguard-theme="light"] { --lg-page-bg: #f3f6fb; --lg-panel-bg: #ffffff; --lg-surface-bg: #ffffff; --lg-border: #d8e1ef; --lg-text: #111827; --lg-muted: #5b6473; --lg-accent: #4f46e5; --lg-label-color: #5b6473; --lg-info-color: #4f46e5; --lg-accent-soft: #e0e7ff; --lg-input-bg: #ffffff; --lg-input-border: #c7d2e5; --lg-input-text: #111827; } html[data-leguard-theme="dark"] { --lg-page-bg: #060c1b; --lg-panel-bg: #0a1226; --lg-surface-bg: #0d1a34; --lg-border: #19345c; --lg-text: #e7eefc; --lg-muted: #93a9c9; --lg-accent: #22d3ee; --lg-label-color: #5b6473; --lg-info-color: #4f46e5; --lg-accent-soft: #14324a; --lg-input-bg: #09162d; --lg-input-border: #1b3d68; --lg-input-text: #dbeafe; } .gradio-container { background: var(--lg-page-bg) !important; color: var(--lg-text) !important; } .gradio-container, .gradio-container * { border-color: var(--lg-border); } .gradio-container input[type="text"], .gradio-container input[type="number"], .gradio-container input[type="search"], .gradio-container textarea, .gradio-container .wrap.svelte-1ykxmf5, .gradio-container .wrap.svelte-1rjryqp { background: var(--lg-input-bg) !important; color: var(--lg-input-text) !important; border-color: var(--lg-input-border) !important; } .gradio-container .tabs, .gradio-container .gradio-accordion, .gradio-container [role="tablist"], .gradio-container .form, .gradio-container .panel { background: var(--lg-panel-bg) !important; border-color: var(--lg-border) !important; } .gradio-container [role="tab"] { color: var(--lg-muted) !important; } .gradio-container [role="tab"][aria-selected="true"] { color: var(--lg-text) !important; border-bottom-color: var(--lg-accent) !important; } .gradio-container button { border-radius: 10px !important; } .gradio-container label, .gradio-container legend, .gradio-container .gr-form label, .gradio-container .gr-block label, .gradio-container .caption, .gradio-container [data-testid="block-label"], .gradio-container .svelte-1ipelgc, .gradio-container [data-testid="block-label"] *, .gradio-container .svelte-1ipelgc * { color: var(--lg-label-color) !important; opacity: 1 !important; } .gradio-container .prose, .gradio-container .prose *, .gradio-container .markdown, .gradio-container .markdown * { color: var(--lg-text) !important; opacity: 1 !important; } .gradio-container .prose p, .gradio-container .prose li, .gradio-container .prose ul, .gradio-container .prose ol, .gradio-container .prose span { color: var(--lg-text) !important; } .gradio-container .prose h1, .gradio-container .prose h2, .gradio-container .prose h3, .gradio-container .prose strong { color: var(--lg-text) !important; } .gradio-container .prose a, .gradio-container .markdown a { color: var(--lg-accent) !important; } .gradio-container [data-testid="block-info"], .gradio-container .block-info, .gradio-container .gradio-info, .gradio-container [data-testid="block-info"] *, .gradio-container .block-info *, .gradio-container .gradio-info * { color: var(--lg-info-color) !important; opacity: 1 !important; } .field-help { margin-top: 4px; font-size: 0.78rem; line-height: 1.25; color: var(--lg-info-color) !important; opacity: 1 !important; } .app-container { max-width: 1080px; margin: 0 auto; } .hero-card { border: 1px solid var(--lg-border); border-radius: 14px; padding: 14px 16px 10px 16px; background: var(--lg-panel-bg); margin-bottom: 14px; } .hero-card img { width: 100%; border-radius: 10px; } .hero-card h1 { margin: 10px 0 4px 0; font-size: 1.35rem; } .hero-card p { margin: 0; color: var(--lg-muted); } .hero-links { margin-top: 10px; } .hero-links a { color: var(--lg-accent); text-decoration: none; font-weight: 600; } .hero-links a:hover { text-decoration: underline; } .run-btn button { min-height: 48px !important; font-size: 1rem !important; font-weight: 700 !important; background: linear-gradient(90deg, #4f46e5, #4338ca) !important; color: #ffffff !important; border: none !important; } html[data-leguard-theme="dark"] .run-btn button { background: linear-gradient(90deg, #0ea5e9, #06b6d4) !important; } .refresh-btn button { min-height: 40px !important; font-size: 0.88rem !important; font-weight: 600 !important; background: var(--lg-surface-bg) !important; color: var(--lg-text) !important; border: 1px solid var(--lg-border) !important; } .status-card { border-radius: 10px; padding: 12px 14px; margin-bottom: 12px; font-weight: 600; } .gradio-container .status-card, .gradio-container .status-card * { color: inherit !important; opacity: 1 !important; } .status-neutral { background: var(--lg-surface-bg); color: var(--lg-muted); border: 1px solid var(--lg-border); } .status-pass { background: #ecfdf3 !important; color: #166534 !important; border: 1px solid #bbf7d0 !important; } .status-warn { background: #fff7ed !important; color: #9a3412 !important; border: 1px solid #fed7aa !important; } .status-fail { background: #fef2f2 !important; color: #991b1b !important; border: 1px solid #fecaca !important; } html[data-leguard-theme="dark"] .status-pass { background: #052e1f !important; color: #86efac !important; border: 1px solid #166534 !important; } html[data-leguard-theme="dark"] .status-warn { background: #3b2206 !important; color: #fdba74 !important; border: 1px solid #9a5800 !important; } html[data-leguard-theme="dark"] .status-fail { background: #3a0b10 !important; color: #fca5a5 !important; border: 1px solid #b91c1c !important; } .metrics-grid { display: grid; grid-template-columns: repeat(3, minmax(120px, 1fr)); gap: 10px; margin-bottom: 14px; } .metric { border: 1px solid var(--lg-border); border-radius: 10px; padding: 10px 12px; background: var(--lg-surface-bg); } .metric-label { font-size: 0.74rem; color: var(--lg-muted); text-transform: uppercase; letter-spacing: 0.04em; } .metric-value { margin-top: 4px; font-size: 1.1rem; font-weight: 700; color: var(--lg-text) !important; } .section-note { color: var(--lg-muted); font-size: 0.92rem; } .issues-card { border: 1px solid var(--lg-border); border-radius: 10px; background: var(--lg-surface-bg); padding: 12px; margin-top: 4px; color: var(--lg-muted); font-size: 0.92rem; } .issues-card a { color: var(--lg-accent); text-decoration: none; font-weight: 600; } .issues-card a:hover { text-decoration: underline; } """ for directory in (DATASETS_DIR, RUNS_DIR): directory.mkdir(parents=True, exist_ok=True) def run_command(command): completed = subprocess.run(command, capture_output=True, text=True) logs = [f"$ {' '.join(command)}"] if completed.stdout.strip(): logs.append(completed.stdout.strip()) if completed.stderr.strip(): logs.append(completed.stderr.strip()) return completed.returncode, "\n\n".join(logs) def resolve_dataset_path(dataset_repo_id): dataset_repo_id = dataset_repo_id.strip() if not dataset_repo_id: raise ValueError("Enter a Hugging Face dataset id, for example: praedico/SO101_pillbox_vita") target_dir = DATASETS_DIR / dataset_repo_id.replace("/", "__") if target_dir.exists(): shutil.rmtree(target_dir) token = os.environ.get("HF_TOKEN") snapshot_download( repo_id=dataset_repo_id, repo_type="dataset", local_dir=str(target_dir), token=token, ) return target_dir def summarize_report(report, check_exit): counts = report["issue_counts"] errors = counts["errors"] warnings = counts["warnings"] if errors > 0 or check_exit != 0: status = "FAIL" style = "status-fail" status_text = "Blocking issues detected. Review and fix before using this dataset in CI/training." elif warnings > 0: status = "WARN" style = "status-warn" status_text = "Warnings detected. Dataset is likely usable, but review issues before large runs." else: status = "PASS" style = "status-pass" status_text = "No blocking issues detected." return ( f"
[{status}] {status_text}
", style, ) def build_metrics_html(report): dataset = report["dataset"]["name"] counts = report["issue_counts"] return ( "
" f"
Dataset
{dataset}
" f"
Total issues
{counts['total']}
" f"
Errors
{counts['errors']}
" f"
Warnings
{counts['warnings']}
" f"
Infos
{counts['infos']}
" "
" ) def empty_status_html(): return "
No diagnostics run yet.
" def empty_details_markdown(): return "Run diagnostics to display issues and recommendations." def reset_diagnostics_state(): return ( empty_status_html(), "", empty_details_markdown(), "", None, None, None, ) def build_issue_markdown(report): grouped = {name: [] for name in CHECK_ORDER} grouped["other"] = [] for issue in report.get("issues", []): check_id = issue.get("check_id", "unknown") prefix = check_id.split(".", 1)[0] if prefix in grouped: grouped[prefix].append(issue) else: grouped["other"].append(issue) sections = [] for check_name in CHECK_ORDER + ["other"]: issues = grouped.get(check_name) or [] if not issues: continue title = CHECK_LABELS.get(check_name, "Other") sections.append(f"### {title}") for issue in issues[:12]: severity = str(issue.get("severity", "info")).upper() message = issue.get("message", "No message") suggestion = issue.get("suggestion") line = f"- **[{severity}]** {message}" if suggestion: line += f" \n Suggestion: {suggestion}" sections.append(line) if len(issues) > 12: sections.append(f"- ... {len(issues) - 12} additional issue(s)") sections.append("") if not sections: return "No issues found." return "\n".join(sections).strip() def build_runtime_config(selected_checks, max_episodes): if not selected_checks: raise ValueError("Select at least one check to run.") selected = set(selected_checks) lines = ["checks:"] for check_name in CHECK_ORDER: enabled = "true" if check_name in selected else "false" lines.append(f" {check_name}: {enabled}") if max_episodes is not None: parsed_max_episodes = int(max_episodes) if parsed_max_episodes < 0: raise ValueError("Max episodes must be 0 or a positive integer.") if parsed_max_episodes > 0: lines.append(f"max_episodes: {parsed_max_episodes}") return "\n".join(lines) + "\n" def selected_checks_from_flags(*flags): return [name for name, enabled in zip(CHECK_ORDER, flags) if enabled] def set_check_flags(mode): if mode == "all": return (True,) * len(CHECK_ORDER) if mode == "recommended": recommended = { "structure", "schema", "consistency", "episodes", "temporal", "numerical", "training", "portability", } return tuple(check in recommended for check in CHECK_ORDER) return (False,) * len(CHECK_ORDER) def run_leguard( dataset_repo_id, fail_on, max_episodes, check_structure, check_schema, check_consistency, check_episodes, check_temporal, check_numerical, check_video, check_annotation, check_training, check_portability, ): selected_checks = selected_checks_from_flags( check_structure, check_schema, check_consistency, check_episodes, check_temporal, check_numerical, check_video, check_annotation, check_training, check_portability, ) try: dataset_path = resolve_dataset_path(dataset_repo_id) runtime_config = build_runtime_config(selected_checks, max_episodes) except Exception as exc: return ( "
[FAIL] Unable to start diagnostics.
", "", f"### Error\n- {exc}", str(exc), None, None, None, ) run_id = uuid.uuid4().hex[:8] run_dir = RUNS_DIR / run_id run_dir.mkdir(parents=True, exist_ok=True) runtime_config_path = run_dir / "space-config.yml" runtime_config_path.write_text(runtime_config, encoding="utf-8") json_report = run_dir / "report.json" html_report = run_dir / "report.html" check_exit, check_logs = run_command( [ LEGUARD_BIN, "check", str(dataset_path), "--fail-on", fail_on, "--config", str(runtime_config_path), ] ) json_exit, json_logs = run_command( [ LEGUARD_BIN, "report", str(dataset_path), "--format", "json", "--out", str(json_report), "--config", str(runtime_config_path), ] ) html_exit, html_logs = run_command( [ LEGUARD_BIN, "report", str(dataset_path), "--format", "html", "--out", str(html_report), "--config", str(runtime_config_path), ] ) all_logs = "\n\n---\n\n".join([check_logs, json_logs, html_logs]) if json_exit != 0 or html_exit != 0 or not json_report.exists(): status_html = ( "
[FAIL] Report generation failed. " "Check command logs for details.
" ) details_md = ( "### Run failed\n" f"- Dataset path: `{dataset_path}`\n" "- `leguard report` did not produce the expected JSON/HTML outputs." ) return status_html, "", details_md, all_logs, None, None, None report = json.loads(json_report.read_text(encoding="utf-8")) status_html, _ = summarize_report(report, check_exit) metrics_html = build_metrics_html(report) issues_markdown = build_issue_markdown(report) return ( status_html, metrics_html, issues_markdown, all_logs, report, str(json_report), str(html_report), ) with gr.Blocks( title="LeGuard Space", css=CSS, theme=gr.themes.Soft(), js=BOOTSTRAP_JS, ) as demo: with gr.Column(elem_classes=["app-container"]): gr.HTML( f"""
LeGuard banner

LeGuard Space

Run LeGuard diagnostics on Hugging Face LeRobot datasets and export actionable reports.

""" ) with gr.Row(): dataset_repo_id = gr.Textbox( label="Dataset (Hugging Face repo id)", placeholder="praedico/SO101_pillbox_vita", scale=4, ) with gr.Column(scale=1): max_episodes = gr.Number( label="Max episodes (optional)", value=0, precision=0, minimum=0, ) gr.HTML("
Set 0 to validate all episodes.
") theme_mode = gr.Radio( [("Light", "light"), ("Dark", "dark")], value="light", label="Theme mode", scale=1, ) with gr.Accordion("Advanced: checks & policy", open=False): gr.Markdown("**Checks to run**") with gr.Row(): select_all_btn = gr.Button("Select all", variant="secondary", size="sm") select_recommended_btn = gr.Button("Recommended", variant="secondary", size="sm") clear_btn = gr.Button("Clear all", variant="secondary", size="sm") with gr.Row(): check_structure = gr.Checkbox(label="Structure", value=True) check_schema = gr.Checkbox(label="Schema", value=True) check_consistency = gr.Checkbox(label="Consistency", value=True) check_episodes = gr.Checkbox(label="Episodes", value=True) check_temporal = gr.Checkbox(label="Temporal", value=True) with gr.Row(): check_numerical = gr.Checkbox(label="Numerical", value=True) check_video = gr.Checkbox(label="Video", value=True) check_annotation = gr.Checkbox(label="Annotation", value=True) check_training = gr.Checkbox(label="Training readiness", value=True) check_portability = gr.Checkbox(label="Portability", value=True) fail_on = gr.Radio( ["error", "warning"], value="error", label="Fail policy", ) check_outputs = [ check_structure, check_schema, check_consistency, check_episodes, check_temporal, check_numerical, check_video, check_annotation, check_training, check_portability, ] select_all_btn.click(fn=lambda: set_check_flags("all"), outputs=check_outputs) select_recommended_btn.click( fn=lambda: set_check_flags("recommended"), outputs=check_outputs, ) clear_btn.click(fn=lambda: set_check_flags("none"), outputs=check_outputs) with gr.Row(): run_button = gr.Button( "Run LeGuard diagnostics", variant="primary", scale=6, elem_classes=["run-btn"], ) refresh_button = gr.Button( "Refresh", variant="secondary", scale=1, min_width=120, elem_classes=["refresh-btn"], ) with gr.Row(): with gr.Column(scale=4): with gr.Tabs(): with gr.TabItem("Report"): status_output = gr.HTML() metrics_output = gr.HTML() details_output = gr.Markdown() with gr.TabItem("JSON"): json_view = gr.JSON(label="Validation report (JSON)") with gr.Column(scale=2, min_width=260): gr.HTML( f"""
Issues or recommendations? Open an issue on GitHub.
""" ) with gr.Accordion("Command logs", open=False): logs_output = gr.Textbox(lines=16, show_copy_button=True) with gr.Row(): json_file_output = gr.File(label="Download JSON report") html_file_output = gr.File(label="Download HTML report") run_inputs = [ dataset_repo_id, fail_on, max_episodes, check_structure, check_schema, check_consistency, check_episodes, check_temporal, check_numerical, check_video, check_annotation, check_training, check_portability, ] run_outputs = [ status_output, metrics_output, details_output, logs_output, json_view, json_file_output, html_file_output, ] run_button.click( fn=run_leguard, inputs=run_inputs, outputs=run_outputs, ) refresh_button.click( fn=reset_diagnostics_state, inputs=[], outputs=run_outputs, ) theme_mode.change( fn=lambda _mode: None, inputs=[theme_mode], outputs=[], js=THEME_CHANGE_JS, ) if __name__ == "__main__": launch_kwargs = {"show_api": False} if os.environ.get("SPACE_ID"): # Hugging Face Spaces provides the runtime ingress. demo.queue().launch(**launch_kwargs) else: demo.queue().launch( server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")), **launch_kwargs, )