| import gradio as gr |
| from pathlib import Path |
| import datetime as _dt |
|
|
| |
| 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 = 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")) |
|
|
| |
| def pill_link(text: str, url: str): |
| return f'<a class="pill" href="{url}" target="_blank" rel="noopener">{text}</a>' |
|
|
| def topbar(): |
| links = " · ".join([pill_link(name, url) for name, url in GITHUBS]) |
| return f""" |
| <div class="topbar"> |
| <div class="title">{TITLE}</div> |
| <div class="links"> |
| {pill_link("📄 arXiv", PAPER_URL)} {links} |
| </div> |
| </div> |
| """ |
|
|
| 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""" |
| <div class="footer"> |
| <span>© {year} JuggleRL Team · Hosted on <a href="https://huggingface.co" target="_blank">Hugging Face Spaces</a></span> |
| </div> |
| """ |
|
|
| |
| 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%)", |
| ) |
|
|
| |
| 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: |
| |
| 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""" |
| <div class="info-cards"> |
| <div class="metric"><div class="k">{HITS_BEST}</div><div>Max real-world hits</div></div> |
| <div class="metric"><div class="k">{HITS_MEAN}</div><div>Avg hits (10 trials)</div></div> |
| <div class="metric"><div class="k">0</div><div>Real data for training</div></div> |
| </div> |
| """) |
|
|
| 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"): |
| gr.Markdown("### Full Demo on Bilibili") |
| gr.HTML(''' |
| <div style="position:relative;padding-top:56.25%;"> |
| <iframe src="https://player.bilibili.com/player.html?bvid=BV1hKxDzrEw5&autoplay=0" |
| scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" |
| style="position:absolute;top:0;left:0;width:100%;height:100%;"> |
| </iframe> |
| </div> |
| ''') |
|
|
|
|
| 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() |
|
|