JuggleRL / app.py
JJJJJllll
fix markdown dark
9c17f94
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'<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)} &nbsp; {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 ====
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"""
<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"):
# 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('''
<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()