import gradio as gr from pathlib import Path import datetime as _dt # ==== Editable Meta ==== TITLE = "JuggleRL: Mastering Ball Juggling with a Quadrotor via Deep Reinforcement Learning" PAPER_URL = "https://arxiv.org/abs/2509.24892" GITHUBS = [ ("Training", "https://github.com/thu-uav/JuggleRL_train"), ("ROS Pack", "https://github.com/thu-uav/JuggleRL_rospack"), ("NatNet SDK", "https://github.com/thu-uav/JuggleRL_NatNetSDK"), ] HITS_BEST = 462 HITS_MEAN = 311 # ==== Assets ==== ASSETS = Path("assets") ASSETS.mkdir(exist_ok=True) videos = list(ASSETS.glob("*.mp4")) + list(ASSETS.glob("*.mov")) images = list(ASSETS.glob("*.png")) + list(ASSETS.glob("*.jpg")) + list(ASSETS.glob("*.jpeg")) # ==== Helper ==== def pill_link(text: str, url: str): return f'{text}' def topbar(): links = " · ".join([pill_link(name, url) for name, url in GITHUBS]) return f"""
{TITLE}
""" def highlights_md(): return f""" ### Highlights - **Zero-shot sim-to-real** deployment, no real data for training. - **Calibrated dynamics + domain randomization** to reduce sim-to-real gap. - **Lightweight Communication Protocol (LCP)** for low-latency state streaming. - **Real-world performance**: up to **{HITS_BEST}** hits (avg **{HITS_MEAN}** across 10 trials). > This page hosts figures, demo videos, and links to paper & code. """ def project_footer(): year = _dt.datetime.now().year return f""" """ # ==== Theme ==== theme = gr.themes.Soft( primary_hue="blue", secondary_hue="slate", ).set( body_background_fill="#0b1020", body_text_color="#e7eefc", block_background_fill="#0f1630", block_border_width="1px", block_shadow="0 4px 24px rgba(0,0,0,0.35)", input_background_fill="#0f1630", link_text_color="hsl(211, 100%, 70%)", ) # ==== App ==== with gr.Blocks( title="JuggleRL · Quadrotor Ball Juggling", theme=theme, css=""" :root, :host, html, body { color-scheme: dark !important; background: #0b1020 !important; color: #e7eefc !important; } /* 让 Gradio Markdown 永远使用深色主题 */ gr-markdown, gr-markdown * { background: #0f1630 !important; color: #e7eefc !important; } /* 修复 Markdown 区块边框与阴影 */ gr-markdown .prose, .prose { background: #0f1630 !important; color: #e7eefc !important; border: 1px solid rgba(255,255,255,0.08) !important; border-radius: 12px !important; box-shadow: 0 4px 24px rgba(0,0,0,0.35); }`` .gradio-container { background: #0b1020 !important; } .topbar{ display:flex;justify-content:space-between;align-items:center; gap:12px; padding:18px 20px; border-bottom:1px solid rgba(255,255,255,.08); position:sticky; top:0; background:#0b1020; z-index:10; } .topbar .title{ font-weight:700; font-size:18px; letter-spacing:.2px; } .pill{ padding:6px 10px; border:1px solid rgba(255,255,255,.15); border-radius:999px; text-decoration:none; transition:all .15s ease; color:#cfe3ff !important; } .pill:hover{ border-color:rgba(255,255,255,.35); background:rgba(255,255,255,.06); } .hero{ display:grid; grid-template-columns:1.1fr .9fr; gap:18px; align-items:center; } @media (max-width: 900px){ .hero{ grid-template-columns:1fr; } } .info-cards{ display:grid; grid-template-columns:repeat(3,1fr); gap:12px; } @media (max-width: 900px){ .info-cards{ grid-template-columns:1fr; } } .metric{ background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02)); border:1px solid rgba(255,255,255,.08); border-radius:14px; padding:14px; text-align:center; } .metric .k{ font-size:28px; font-weight:800; } .footer{ padding:26px 10px; text-align:center; color:#9eb2d8; } """ ) as demo: gr.HTML(topbar()) with gr.Row(): with gr.Column(): with gr.Group(): gr.Markdown(f"## Overview") gr.Markdown(highlights_md()) with gr.Column(): if images: # Show first image as hero gr.Image(value=str(images[0]), interactive=False, show_download_button=False, label="System Diagram / Teaser", height=380) else: gr.Markdown("> Upload a system diagram to `assets/` (PNG/JPG).") with gr.Row(): with gr.Column(): gr.Markdown("### Project Links") links_md = f"- **Paper**: [{PAPER_URL}]({PAPER_URL})\n" + "\n".join([f"- **{name}**: {url}" for name, url in GITHUBS]) gr.Markdown(links_md) with gr.Column(): gr.Markdown("### Key Metrics") gr.HTML(f"""
{HITS_BEST}
Max real-world hits
{HITS_MEAN}
Avg hits (10 trials)
0
Real data for training
""") with gr.Tab("Figures"): if images: gallery = gr.Gallery( value=[str(p) for p in images], label="Figures", columns=3, height=460, preview=True, allow_preview=True, ) else: gr.Markdown("> No figures yet. Put PNG/JPG into `assets/` to show here.") # with gr.Tab("Real-world Videos"): # if videos: # for v in videos: # gr.Video(str(v)) # else: # gr.Markdown("> No videos yet. Put MP4/MOV files into `assets/`.") with gr.Tab("Real-world Videos"): gr.Markdown("### Full Demo on Bilibili") gr.HTML('''
''') with gr.Accordion("BibTeX", open=False): gr.Code( language="markdown", value=f"""@article{{JuggleRL2025, title={{JuggleRL: Mastering Ball Juggling with a Quadrotor via Deep Reinforcement Learning}}, author={{Your Name and Coauthors}}, journal={{arXiv preprint arXiv:2509.24892}}, year={{2025}} }}""", lines=10, ) gr.HTML(project_footer()) if __name__ == "__main__": demo.launch()