|
|
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() |
|
|
|