Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app.py +753 -0
- requirements.txt +11 -0
app.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py โ Car Racing Agent ยท Gradio Demo
|
| 3 |
+
OpenEnv Student Challenge 2026 ยท NirmalPratheep
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os, sys, tempfile, uuid, traceback
|
| 7 |
+
|
| 8 |
+
_APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
# Local layout: app.py lives in app/, repo root is parent.
|
| 10 |
+
# HF Space layout: app.py sits at root alongside game/, env/, training/.
|
| 11 |
+
_ROOT = _APP_DIR if os.path.isdir(os.path.join(_APP_DIR, "game")) else os.path.dirname(_APP_DIR)
|
| 12 |
+
sys.path.insert(0, _ROOT)
|
| 13 |
+
sys.path.insert(1, os.path.join(_ROOT, "training"))
|
| 14 |
+
|
| 15 |
+
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
|
| 16 |
+
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
|
| 17 |
+
|
| 18 |
+
import gradio as gr
|
| 19 |
+
import numpy as np
|
| 20 |
+
import torch
|
| 21 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 22 |
+
|
| 23 |
+
from game.rl_splits import TRAIN, _ensure_pygame
|
| 24 |
+
from env.environment import RaceEnvironment
|
| 25 |
+
from env.models import DriveAction
|
| 26 |
+
from train_torchrl import build_policy_and_value
|
| 27 |
+
|
| 28 |
+
_ensure_pygame()
|
| 29 |
+
import pygame
|
| 30 |
+
from game.oval_racer import draw_car, draw_headlights
|
| 31 |
+
|
| 32 |
+
# โโ Constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 33 |
+
STEP_CHUNK = 20
|
| 34 |
+
MAX_STEPS = 3000
|
| 35 |
+
LAPS_TARGET = 1
|
| 36 |
+
FRAME_W, FRAME_H = 540, 360
|
| 37 |
+
HEADLIGHT_PX = 192
|
| 38 |
+
DEVICE = torch.device("cpu")
|
| 39 |
+
|
| 40 |
+
_CKPT_CANDIDATES = [
|
| 41 |
+
os.path.join(_APP_DIR, "ppo_torchrl_final.pt"),
|
| 42 |
+
os.path.join(_ROOT, "ppo_torchrl_final.pt"),
|
| 43 |
+
os.path.join(_ROOT, "checkpoints", "ppo_torchrl_final.pt"),
|
| 44 |
+
]
|
| 45 |
+
CKPT_PATH = next((p for p in _CKPT_CANDIDATES if os.path.isfile(p)), None)
|
| 46 |
+
POLICY = None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _load_policy():
|
| 50 |
+
global POLICY
|
| 51 |
+
if POLICY is not None:
|
| 52 |
+
return POLICY
|
| 53 |
+
if CKPT_PATH is None:
|
| 54 |
+
raise RuntimeError("Checkpoint not found. Put ppo_torchrl_final.pt in app/ or checkpoints/")
|
| 55 |
+
policy, _, _ = build_policy_and_value(DEVICE)
|
| 56 |
+
ckpt = torch.load(CKPT_PATH, map_location="cpu", weights_only=False)
|
| 57 |
+
sd = ckpt.get("policy", ckpt.get("model", {}))
|
| 58 |
+
if any(k.startswith("_orig_mod.") for k in sd):
|
| 59 |
+
sd = {k.replace("_orig_mod.", "", 1): v for k, v in sd.items()}
|
| 60 |
+
policy.load_state_dict(sd)
|
| 61 |
+
policy.eval()
|
| 62 |
+
POLICY = policy
|
| 63 |
+
print(f"โ Policy loaded: {CKPT_PATH}")
|
| 64 |
+
return POLICY
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# โโ Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 68 |
+
|
| 69 |
+
def _surf_to_pil(surf, size) -> Image.Image:
|
| 70 |
+
small = pygame.transform.scale(surf, size)
|
| 71 |
+
arr = pygame.surfarray.array3d(small).transpose(1, 0, 2)
|
| 72 |
+
return Image.fromarray(arr.astype(np.uint8))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _game_frame(race_env, trail=None) -> Image.Image:
|
| 76 |
+
ce = race_env._env
|
| 77 |
+
surf = ce.track.surface.copy()
|
| 78 |
+
# Draw path trail before car so car renders on top
|
| 79 |
+
if trail and len(trail) > 1:
|
| 80 |
+
scale_x = FRAME_W / 900
|
| 81 |
+
scale_y = FRAME_H / 600
|
| 82 |
+
for i, (px, py) in enumerate(trail):
|
| 83 |
+
alpha = max(60, int(255 * i / len(trail)))
|
| 84 |
+
r = max(2, int(4 * i / len(trail)))
|
| 85 |
+
color = (255, int(140 * i / len(trail)), 0) # orange fade-in
|
| 86 |
+
pygame.draw.circle(surf, color, (int(px), int(py)), r)
|
| 87 |
+
draw_headlights(surf, ce._x, ce._y, ce._angle)
|
| 88 |
+
draw_car(surf, ce._x, ce._y, ce._angle)
|
| 89 |
+
return _surf_to_pil(surf, (FRAME_W, FRAME_H))
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _headlight_frame(race_env) -> Image.Image:
|
| 93 |
+
img64 = race_env._render_headlight_image()
|
| 94 |
+
return Image.fromarray(img64).resize((HEADLIGHT_PX, HEADLIGHT_PX), Image.NEAREST)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _placeholder(w, h, text, bg=(15, 17, 26), fg=(80, 90, 120)) -> Image.Image:
|
| 98 |
+
img = Image.new("RGB", (w, h), bg)
|
| 99 |
+
draw = ImageDraw.Draw(img)
|
| 100 |
+
try:
|
| 101 |
+
font = ImageFont.truetype("arial.ttf", 18)
|
| 102 |
+
except Exception:
|
| 103 |
+
font = ImageFont.load_default()
|
| 104 |
+
bb = draw.textbbox((0, 0), text, font=font)
|
| 105 |
+
tw, th = bb[2] - bb[0], bb[3] - bb[1]
|
| 106 |
+
draw.text(((w - tw) // 2, (h - th) // 2), text, fill=fg, font=font)
|
| 107 |
+
return img
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _placeholder_main():
|
| 111 |
+
return _placeholder(FRAME_W, FRAME_H, "โ Click Reset to load the track")
|
| 112 |
+
|
| 113 |
+
def _placeholder_pov():
|
| 114 |
+
return _placeholder(HEADLIGHT_PX, HEADLIGHT_PX, "POV", fg=(100, 110, 140))
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _agent_action(obs):
|
| 118 |
+
from tensordict import TensorDict
|
| 119 |
+
from torchrl.envs.utils import ExplorationType, set_exploration_type
|
| 120 |
+
policy = _load_policy()
|
| 121 |
+
img = (torch.from_numpy(obs.image.copy())
|
| 122 |
+
.float().div(255.0).permute(2, 0, 1).unsqueeze(0).to(DEVICE))
|
| 123 |
+
scalars = torch.tensor(obs.scalars, dtype=torch.float32, device=DEVICE).unsqueeze(0)
|
| 124 |
+
td = TensorDict({"image": img, "scalars": scalars}, batch_size=[1])
|
| 125 |
+
with set_exploration_type(ExplorationType.MEAN):
|
| 126 |
+
td = policy(td)
|
| 127 |
+
a = td["action"][0].detach().clamp(-1.0, 1.0).cpu().numpy()
|
| 128 |
+
return float(a[0]), float(a[1])
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# โโ Status HTML โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 132 |
+
TRACK_CHOICES = [f"Track {t.level:02d} โ {t.name}" for t in TRAIN]
|
| 133 |
+
|
| 134 |
+
def _get_track(label: str):
|
| 135 |
+
lvl = int(label.split(" ")[1])
|
| 136 |
+
return next((t for t in TRAIN if t.level == lvl), TRAIN[0])
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _stat_card(label, value, cls=""):
|
| 140 |
+
return (f"<div class='sc'><div class='sl'>{label}</div>"
|
| 141 |
+
f"<div class='sv {cls}'>{value}</div></div>")
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _status_html(state, error=None):
|
| 145 |
+
if error:
|
| 146 |
+
return (f"<div class='stat-row'>"
|
| 147 |
+
f"<div class='sc err'><div class='sl'>ERROR</div>"
|
| 148 |
+
f"<div class='sv' style='font-size:.7rem;color:#ff6b6b;word-break:break-all'>{error}</div></div>"
|
| 149 |
+
f"</div>")
|
| 150 |
+
if state is None:
|
| 151 |
+
return (f"<div class='stat-row'>"
|
| 152 |
+
+ _stat_card("STATUS", "IDLE", "idle")
|
| 153 |
+
+ _stat_card("SPEED", "โ")
|
| 154 |
+
+ _stat_card("LAPS", "โ")
|
| 155 |
+
+ _stat_card("STEPS", "โ")
|
| 156 |
+
+ "</div>")
|
| 157 |
+
ce = state["env"]._env
|
| 158 |
+
laps = ce._laps
|
| 159 |
+
speed = ce._speed
|
| 160 |
+
step = state["step"]
|
| 161 |
+
done = state["done"]
|
| 162 |
+
spd = f"{int(abs(speed)/ce.track.max_speed*100)}%"
|
| 163 |
+
if done and laps >= LAPS_TARGET:
|
| 164 |
+
st, cls = "โ
PASS", "pass"
|
| 165 |
+
elif done:
|
| 166 |
+
st, cls = "๐ฅ CRASH", "fail"
|
| 167 |
+
else:
|
| 168 |
+
st, cls = "โถ RUNNING", "run"
|
| 169 |
+
return (f"<div class='stat-row'>"
|
| 170 |
+
+ _stat_card("STATUS", st, cls)
|
| 171 |
+
+ _stat_card("SPEED", spd)
|
| 172 |
+
+ _stat_card("LAPS", f"{laps}/{LAPS_TARGET}")
|
| 173 |
+
+ _stat_card("STEPS", f"{step}")
|
| 174 |
+
+ "</div>")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# โโ Callbacks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 178 |
+
|
| 179 |
+
def reset(track_label):
|
| 180 |
+
try:
|
| 181 |
+
_ensure_pygame()
|
| 182 |
+
track = _get_track(track_label)
|
| 183 |
+
track.build()
|
| 184 |
+
env = RaceEnvironment(track, max_steps=MAX_STEPS, laps_target=LAPS_TARGET, use_image=True)
|
| 185 |
+
obs = env.reset()
|
| 186 |
+
state = {"env": env, "obs": obs, "step": 0, "done": False, "trail": []}
|
| 187 |
+
return state, _game_frame(env), _headlight_frame(env), _status_html(state), None
|
| 188 |
+
except Exception as e:
|
| 189 |
+
traceback.print_exc()
|
| 190 |
+
return None, _placeholder_main(), _placeholder_pov(), _status_html(None, str(e)), None
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def step_agent(state):
|
| 194 |
+
"""Generator: yields a frame every 4 physics steps so the image streams smoothly."""
|
| 195 |
+
try:
|
| 196 |
+
if state is None:
|
| 197 |
+
yield (state, _placeholder_main(), _placeholder_pov(),
|
| 198 |
+
_status_html(None, "No session โ press Reset first"), None)
|
| 199 |
+
return
|
| 200 |
+
if state["done"]:
|
| 201 |
+
yield (state, _game_frame(state["env"], state.get("trail")),
|
| 202 |
+
_headlight_frame(state["env"]), _status_html(state), None)
|
| 203 |
+
return
|
| 204 |
+
|
| 205 |
+
env = state["env"]
|
| 206 |
+
obs = state["obs"]
|
| 207 |
+
trail = state.setdefault("trail", [])
|
| 208 |
+
YIELD_EVERY = 4 # stream a new frame every N physics steps
|
| 209 |
+
|
| 210 |
+
for i in range(STEP_CHUNK):
|
| 211 |
+
if state["done"]:
|
| 212 |
+
break
|
| 213 |
+
accel, steer = _agent_action(obs)
|
| 214 |
+
obs = env.step(DriveAction(accel=accel, steer=steer))
|
| 215 |
+
state["step"] += 1
|
| 216 |
+
trail.append((env._env._x, env._env._y))
|
| 217 |
+
if obs.done:
|
| 218 |
+
state["done"] = True
|
| 219 |
+
# stream intermediate frames
|
| 220 |
+
if (i + 1) % YIELD_EVERY == 0 or state["done"]:
|
| 221 |
+
yield (state,
|
| 222 |
+
_game_frame(env, trail),
|
| 223 |
+
_headlight_frame(env),
|
| 224 |
+
_status_html(state),
|
| 225 |
+
None)
|
| 226 |
+
|
| 227 |
+
state["obs"] = obs
|
| 228 |
+
yield state, _game_frame(env, trail), _headlight_frame(env), _status_html(state), None
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
traceback.print_exc()
|
| 232 |
+
yield state, _placeholder_main(), _placeholder_pov(), _status_html(state, str(e)), None
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def auto_drive(track_label):
|
| 236 |
+
"""Generator: streams live frames while driving, then yields the final MP4."""
|
| 237 |
+
try:
|
| 238 |
+
import imageio.v3 as iio
|
| 239 |
+
_ensure_pygame()
|
| 240 |
+
track = _get_track(track_label)
|
| 241 |
+
track.build()
|
| 242 |
+
env = RaceEnvironment(track, max_steps=MAX_STEPS, laps_target=LAPS_TARGET, use_image=True)
|
| 243 |
+
obs = env.reset()
|
| 244 |
+
trail = []
|
| 245 |
+
frames = [np.array(_game_frame(env))]
|
| 246 |
+
n = 0
|
| 247 |
+
YIELD_EVERY = 3 # stream a UI update every N steps
|
| 248 |
+
|
| 249 |
+
# Show initial frame
|
| 250 |
+
state = {"env": env, "obs": obs, "step": 0, "done": False, "trail": trail}
|
| 251 |
+
yield state, _game_frame(env, trail), _headlight_frame(env), _status_html(state), None
|
| 252 |
+
|
| 253 |
+
while not obs.done:
|
| 254 |
+
accel, steer = _agent_action(obs)
|
| 255 |
+
obs = env.step(DriveAction(accel=accel, steer=steer))
|
| 256 |
+
n += 1
|
| 257 |
+
trail.append((env._env._x, env._env._y))
|
| 258 |
+
|
| 259 |
+
frame_img = _game_frame(env, trail)
|
| 260 |
+
frames.append(np.array(frame_img))
|
| 261 |
+
|
| 262 |
+
if n % YIELD_EVERY == 0:
|
| 263 |
+
state["step"] = n
|
| 264 |
+
yield (state, frame_img, _headlight_frame(env), _status_html(state), None)
|
| 265 |
+
|
| 266 |
+
# Final state
|
| 267 |
+
ce = env._env
|
| 268 |
+
laps = ce._laps
|
| 269 |
+
crashes = getattr(ce, "_crash_count", 0)
|
| 270 |
+
result = "โ
PASS" if laps >= LAPS_TARGET and crashes == 0 else "๐ฅ FAIL"
|
| 271 |
+
|
| 272 |
+
vpath = os.path.join(tempfile.gettempdir(), f"race_{uuid.uuid4().hex[:8]}.mp4")
|
| 273 |
+
iio.imwrite(vpath, np.stack(frames), fps=20, codec="libx264", plugin="pyav")
|
| 274 |
+
|
| 275 |
+
state = {"env": env, "obs": obs, "step": n, "done": True, "trail": trail}
|
| 276 |
+
extra = (f"<div class='stat-row'>"
|
| 277 |
+
+ _stat_card("RESULT", result, "pass" if "PASS" in result else "fail")
|
| 278 |
+
+ _stat_card("LAPS", str(laps))
|
| 279 |
+
+ _stat_card("STEPS", str(n))
|
| 280 |
+
+ _stat_card("FRAMES", str(len(frames)))
|
| 281 |
+
+ "</div>")
|
| 282 |
+
yield state, _game_frame(env, trail), _headlight_frame(env), extra, vpath
|
| 283 |
+
|
| 284 |
+
except Exception as e:
|
| 285 |
+
traceback.print_exc()
|
| 286 |
+
yield None, _placeholder_main(), _placeholder_pov(), _status_html(None, str(e)), None
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# โโ CSS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 290 |
+
CSS = """
|
| 291 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 292 |
+
Car Racing Agent ยท premium light theme
|
| 293 |
+
palette: warm orange accent on neutral stone background
|
| 294 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 295 |
+
|
| 296 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500;700&display=swap');
|
| 297 |
+
|
| 298 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 299 |
+
|
| 300 |
+
body, .gradio-container {
|
| 301 |
+
background:
|
| 302 |
+
radial-gradient(1200px 600px at 10% -10%, #ffedd5 0%, transparent 50%),
|
| 303 |
+
radial-gradient(900px 500px at 110% 10%, #fef3c7 0%, transparent 45%),
|
| 304 |
+
linear-gradient(180deg, #fafafa 0%, #f3f4f6 100%) !important;
|
| 305 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
|
| 306 |
+
font-size: 13px !important;
|
| 307 |
+
color: #1f2937 !important;
|
| 308 |
+
min-height: 100vh;
|
| 309 |
+
}
|
| 310 |
+
.gradio-container { max-width: 100% !important; padding: 0 !important; }
|
| 311 |
+
|
| 312 |
+
/* strip default Gradio borders โ we'll add our own where needed */
|
| 313 |
+
.gr-box, .gr-form, .gr-panel,
|
| 314 |
+
div[class*="component-"], div[data-testid] {
|
| 315 |
+
border: none !important;
|
| 316 |
+
background: transparent !important;
|
| 317 |
+
box-shadow: none !important;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 321 |
+
BANNER โ hero strip
|
| 322 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 323 |
+
.banner {
|
| 324 |
+
position: relative;
|
| 325 |
+
background:
|
| 326 |
+
linear-gradient(135deg, #ffffff 0%, #fff7ed 100%);
|
| 327 |
+
border-bottom: 1px solid rgba(249, 115, 22, 0.15);
|
| 328 |
+
padding: 22px 32px 20px;
|
| 329 |
+
margin-bottom: 20px;
|
| 330 |
+
overflow: hidden;
|
| 331 |
+
}
|
| 332 |
+
.banner::before {
|
| 333 |
+
content: ""; position: absolute; inset: 0;
|
| 334 |
+
background:
|
| 335 |
+
radial-gradient(500px 200px at 15% 120%, rgba(249,115,22,.18), transparent 70%),
|
| 336 |
+
radial-gradient(400px 180px at 90% -30%, rgba(251,191,36,.22), transparent 70%);
|
| 337 |
+
pointer-events: none;
|
| 338 |
+
}
|
| 339 |
+
.banner-inner { max-width: 1280px; margin: 0 auto; position: relative; z-index: 1; }
|
| 340 |
+
.banner h1 {
|
| 341 |
+
font-size: 1.85rem; font-weight: 900; margin: 0 0 6px;
|
| 342 |
+
letter-spacing: -0.02em;
|
| 343 |
+
background: linear-gradient(92deg, #9a3412 0%, #ea580c 40%, #f97316 65%, #fbbf24 100%);
|
| 344 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 345 |
+
display: inline-block;
|
| 346 |
+
filter: drop-shadow(0 1px 0 rgba(255,255,255,.6));
|
| 347 |
+
}
|
| 348 |
+
.banner .sub {
|
| 349 |
+
color: #57534e; font-size: 0.83rem; margin: 0 0 12px;
|
| 350 |
+
font-weight: 500; line-height: 1.5;
|
| 351 |
+
}
|
| 352 |
+
.banner .sub strong { font-weight: 700; }
|
| 353 |
+
.badges { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
| 354 |
+
.badge {
|
| 355 |
+
background: rgba(255, 255, 255, 0.85);
|
| 356 |
+
border: 1px solid #fed7aa;
|
| 357 |
+
color: #9a3412;
|
| 358 |
+
padding: 4px 11px;
|
| 359 |
+
border-radius: 999px;
|
| 360 |
+
font-size: 0.66rem;
|
| 361 |
+
font-weight: 700;
|
| 362 |
+
letter-spacing: .05em;
|
| 363 |
+
backdrop-filter: blur(6px);
|
| 364 |
+
box-shadow: 0 1px 2px rgba(120, 53, 15, .04);
|
| 365 |
+
transition: transform .12s ease, box-shadow .12s ease;
|
| 366 |
+
}
|
| 367 |
+
.badge:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(120, 53, 15, .10); }
|
| 368 |
+
.badge.green {
|
| 369 |
+
background: rgba(240, 253, 244, .9); border-color: #86efac; color: #166534;
|
| 370 |
+
}
|
| 371 |
+
.badge.hf-badge {
|
| 372 |
+
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
| 373 |
+
border-color: #fbbf24; color: #78350f;
|
| 374 |
+
text-decoration: none; cursor: pointer;
|
| 375 |
+
font-weight: 800;
|
| 376 |
+
}
|
| 377 |
+
.badge.hf-badge:hover { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); }
|
| 378 |
+
|
| 379 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 380 |
+
STAT CARDS
|
| 381 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 382 |
+
.stat-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0; }
|
| 383 |
+
.sc {
|
| 384 |
+
flex: 1; min-width: 64px;
|
| 385 |
+
background: #ffffff;
|
| 386 |
+
border: 1px solid #e7e5e4 !important;
|
| 387 |
+
border-radius: 10px !important;
|
| 388 |
+
padding: 10px 8px;
|
| 389 |
+
text-align: center;
|
| 390 |
+
box-shadow:
|
| 391 |
+
0 1px 2px rgba(17, 24, 39, .04),
|
| 392 |
+
0 0 0 1px rgba(255, 255, 255, .6) inset;
|
| 393 |
+
transition: border-color .15s ease, box-shadow .15s ease;
|
| 394 |
+
}
|
| 395 |
+
.sc:hover {
|
| 396 |
+
border-color: #fdba74 !important;
|
| 397 |
+
box-shadow: 0 4px 12px rgba(249, 115, 22, .10);
|
| 398 |
+
}
|
| 399 |
+
.sl {
|
| 400 |
+
font-size: 0.55rem; color: #a8a29e;
|
| 401 |
+
letter-spacing: .14em; text-transform: uppercase;
|
| 402 |
+
margin-bottom: 5px; font-weight: 800;
|
| 403 |
+
}
|
| 404 |
+
.sv {
|
| 405 |
+
font-size: 1.1rem; font-weight: 800;
|
| 406 |
+
color: #1c1917;
|
| 407 |
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
| 408 |
+
letter-spacing: -0.01em;
|
| 409 |
+
}
|
| 410 |
+
.sv.pass { color: #15803d; text-shadow: 0 0 18px rgba(34, 197, 94, .25); }
|
| 411 |
+
.sv.fail { color: #dc2626; }
|
| 412 |
+
.sv.run { color: #2563eb; }
|
| 413 |
+
.sv.idle { color: #a8a29e; }
|
| 414 |
+
.sc.err {
|
| 415 |
+
flex: 100%;
|
| 416 |
+
border-color: #fecaca !important;
|
| 417 |
+
background: linear-gradient(135deg, #fef2f2 0%, #fff5f5 100%);
|
| 418 |
+
}
|
| 419 |
+
.sc.err .sv { color: #b91c1c; font-size: .72rem; word-break: break-all; }
|
| 420 |
+
|
| 421 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 422 |
+
IMAGES โ track view + agent POV
|
| 423 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 424 |
+
.main-view, .agent-pov {
|
| 425 |
+
position: relative;
|
| 426 |
+
border: 1px solid #e7e5e4 !important;
|
| 427 |
+
border-radius: 14px !important;
|
| 428 |
+
overflow: hidden;
|
| 429 |
+
background: #ffffff;
|
| 430 |
+
box-shadow:
|
| 431 |
+
0 10px 30px -12px rgba(17, 24, 39, .12),
|
| 432 |
+
0 0 0 1px rgba(255, 255, 255, .8) inset;
|
| 433 |
+
transition: box-shadow .2s ease, transform .2s ease;
|
| 434 |
+
}
|
| 435 |
+
.main-view:hover {
|
| 436 |
+
box-shadow:
|
| 437 |
+
0 18px 44px -14px rgba(17, 24, 39, .18),
|
| 438 |
+
0 0 0 1px rgba(255, 255, 255, .8) inset;
|
| 439 |
+
}
|
| 440 |
+
.main-view img,
|
| 441 |
+
.agent-pov img {
|
| 442 |
+
width: 100% !important; display: block !important;
|
| 443 |
+
border: none !important; border-radius: 0 !important;
|
| 444 |
+
}
|
| 445 |
+
.agent-pov {
|
| 446 |
+
border-color: #fb923c !important;
|
| 447 |
+
box-shadow:
|
| 448 |
+
0 8px 24px -10px rgba(249, 115, 22, .35),
|
| 449 |
+
0 0 0 1px rgba(255, 237, 213, .8) inset;
|
| 450 |
+
}
|
| 451 |
+
.agent-pov::after {
|
| 452 |
+
content: "CNN INPUT";
|
| 453 |
+
position: absolute; top: 8px; right: 8px;
|
| 454 |
+
background: rgba(249, 115, 22, .95);
|
| 455 |
+
color: #fff; font-size: 0.55rem;
|
| 456 |
+
letter-spacing: .16em; font-weight: 800;
|
| 457 |
+
padding: 2px 7px; border-radius: 999px;
|
| 458 |
+
box-shadow: 0 2px 6px rgba(249, 115, 22, .35);
|
| 459 |
+
pointer-events: none;
|
| 460 |
+
}
|
| 461 |
+
.agent-pov img { image-rendering: pixelated !important; }
|
| 462 |
+
|
| 463 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 464 |
+
PANEL TITLES (small orange labels above each box)
|
| 465 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 466 |
+
.panel-title {
|
| 467 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 468 |
+
font-size: 0.6rem; font-weight: 800;
|
| 469 |
+
letter-spacing: .18em; text-transform: uppercase;
|
| 470 |
+
color: #ea580c;
|
| 471 |
+
padding: 3px 10px;
|
| 472 |
+
background: linear-gradient(90deg, #fff7ed 0%, rgba(255,255,255,0) 100%);
|
| 473 |
+
border-left: 3px solid #fb923c;
|
| 474 |
+
border-radius: 2px;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 478 |
+
HELP BOX
|
| 479 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 480 |
+
.help-box {
|
| 481 |
+
background:
|
| 482 |
+
linear-gradient(180deg, #ffffff 0%, #fafaf9 100%);
|
| 483 |
+
border: 1px solid #e7e5e4 !important;
|
| 484 |
+
border-radius: 14px !important;
|
| 485 |
+
padding: 14px 16px;
|
| 486 |
+
font-size: 0.78rem; color: #44403c; line-height: 1.65;
|
| 487 |
+
box-shadow:
|
| 488 |
+
0 4px 14px -6px rgba(17, 24, 39, .08),
|
| 489 |
+
0 0 0 1px rgba(255, 255, 255, .6) inset;
|
| 490 |
+
}
|
| 491 |
+
.help-box strong { color: #1c1917; font-weight: 700; }
|
| 492 |
+
.help-box .help-title {
|
| 493 |
+
font-size: .7rem; font-weight: 800; letter-spacing: .14em;
|
| 494 |
+
color: #ea580c; text-transform: uppercase; margin-bottom: 10px;
|
| 495 |
+
display: flex; align-items: center; gap: 6px;
|
| 496 |
+
}
|
| 497 |
+
.help-box .step { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 9px; }
|
| 498 |
+
.help-box .num {
|
| 499 |
+
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
| 500 |
+
color: #fff;
|
| 501 |
+
width: 20px; height: 20px; border-radius: 50%;
|
| 502 |
+
display: flex; align-items: center; justify-content: center;
|
| 503 |
+
font-size: 0.65rem; font-weight: 800;
|
| 504 |
+
flex-shrink: 0; margin-top: 1px;
|
| 505 |
+
box-shadow: 0 2px 6px rgba(249, 115, 22, .35);
|
| 506 |
+
}
|
| 507 |
+
.help-box .footer-note {
|
| 508 |
+
margin-top: 10px; padding-top: 10px;
|
| 509 |
+
border-top: 1px dashed #e7e5e4;
|
| 510 |
+
font-size: 0.72rem; color: #78716c;
|
| 511 |
+
}
|
| 512 |
+
.help-box .footer-note .accent { color: #ea580c; font-weight: 700; }
|
| 513 |
+
|
| 514 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 515 |
+
VIDEO PANEL
|
| 516 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 517 |
+
.video-panel {
|
| 518 |
+
border: 1px solid #e7e5e4 !important;
|
| 519 |
+
border-radius: 14px !important;
|
| 520 |
+
overflow: hidden;
|
| 521 |
+
background: #0c0a09;
|
| 522 |
+
box-shadow:
|
| 523 |
+
0 10px 30px -12px rgba(17, 24, 39, .15),
|
| 524 |
+
0 0 0 1px rgba(255, 255, 255, .8) inset;
|
| 525 |
+
}
|
| 526 |
+
.video-panel video { width: 100% !important; display: block !important; }
|
| 527 |
+
|
| 528 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 529 |
+
BUTTONS โ premium tactile feel
|
| 530 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 531 |
+
button.gr-button, button {
|
| 532 |
+
font-family: 'Inter', sans-serif !important;
|
| 533 |
+
font-size: 0.8rem !important;
|
| 534 |
+
font-weight: 700 !important;
|
| 535 |
+
letter-spacing: .01em !important;
|
| 536 |
+
border-radius: 10px !important;
|
| 537 |
+
transition: transform .08s ease, box-shadow .15s ease, filter .15s ease !important;
|
| 538 |
+
border: 1px solid transparent !important;
|
| 539 |
+
}
|
| 540 |
+
button.gr-button:hover { transform: translateY(-1px); filter: brightness(1.04); }
|
| 541 |
+
button.gr-button:active { transform: translateY(0); }
|
| 542 |
+
|
| 543 |
+
/* primary = orange */
|
| 544 |
+
button.primary, button[variant="primary"], .gr-button-primary {
|
| 545 |
+
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
|
| 546 |
+
color: #ffffff !important;
|
| 547 |
+
border-color: #c2410c !important;
|
| 548 |
+
box-shadow:
|
| 549 |
+
0 4px 12px -2px rgba(249, 115, 22, .35),
|
| 550 |
+
0 0 0 1px rgba(255, 255, 255, .25) inset !important;
|
| 551 |
+
}
|
| 552 |
+
button.primary:hover { box-shadow: 0 6px 18px -2px rgba(249, 115, 22, .45) !important; }
|
| 553 |
+
|
| 554 |
+
/* secondary = neutral white */
|
| 555 |
+
button.secondary, button[variant="secondary"], .gr-button-secondary {
|
| 556 |
+
background: #ffffff !important;
|
| 557 |
+
color: #1c1917 !important;
|
| 558 |
+
border-color: #e7e5e4 !important;
|
| 559 |
+
box-shadow: 0 1px 2px rgba(17, 24, 39, .05) !important;
|
| 560 |
+
}
|
| 561 |
+
button.secondary:hover { border-color: #fb923c !important; color: #ea580c !important; }
|
| 562 |
+
|
| 563 |
+
/* stop = red */
|
| 564 |
+
button.stop, button[variant="stop"], .gr-button-stop {
|
| 565 |
+
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%) !important;
|
| 566 |
+
color: #ffffff !important;
|
| 567 |
+
border-color: #15803d !important;
|
| 568 |
+
box-shadow:
|
| 569 |
+
0 4px 12px -2px rgba(22, 163, 74, .35),
|
| 570 |
+
0 0 0 1px rgba(255, 255, 255, .25) inset !important;
|
| 571 |
+
}
|
| 572 |
+
button.stop:hover { box-shadow: 0 6px 18px -2px rgba(22, 163, 74, .45) !important; }
|
| 573 |
+
|
| 574 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 575 |
+
LABELS & INPUTS
|
| 576 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 577 |
+
label, .gr-input-label, span[data-testid="block-label"] {
|
| 578 |
+
color: #44403c !important;
|
| 579 |
+
font-size: 0.74rem !important;
|
| 580 |
+
font-weight: 600 !important;
|
| 581 |
+
letter-spacing: .01em !important;
|
| 582 |
+
}
|
| 583 |
+
select, input, textarea {
|
| 584 |
+
font-family: 'Inter', sans-serif !important;
|
| 585 |
+
font-size: 0.82rem !important;
|
| 586 |
+
background: #ffffff !important;
|
| 587 |
+
border: 1px solid #e7e5e4 !important;
|
| 588 |
+
border-radius: 10px !important;
|
| 589 |
+
color: #1c1917 !important;
|
| 590 |
+
transition: border-color .15s ease, box-shadow .15s ease !important;
|
| 591 |
+
}
|
| 592 |
+
select:focus, input:focus, textarea:focus {
|
| 593 |
+
outline: none !important;
|
| 594 |
+
border-color: #fb923c !important;
|
| 595 |
+
box-shadow: 0 0 0 3px rgba(251, 146, 60, .18) !important;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 599 |
+
FOOTER
|
| 600 |
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
| 601 |
+
.footer-strip {
|
| 602 |
+
margin-top: 24px; padding: 14px 32px 18px;
|
| 603 |
+
border-top: 1px solid #e7e5e4;
|
| 604 |
+
color: #78716c; font-size: 0.72rem;
|
| 605 |
+
text-align: center;
|
| 606 |
+
background: linear-gradient(180deg, rgba(255,255,255,0) 0%, #fafaf9 100%);
|
| 607 |
+
}
|
| 608 |
+
.footer-strip a {
|
| 609 |
+
color: #ea580c; text-decoration: none; font-weight: 700;
|
| 610 |
+
border-bottom: 1px dashed #fdba74;
|
| 611 |
+
}
|
| 612 |
+
.footer-strip a:hover { color: #c2410c; border-bottom-color: #ea580c; }
|
| 613 |
+
"""
|
| 614 |
+
|
| 615 |
+
BANNER_HTML = """
|
| 616 |
+
<div class="banner">
|
| 617 |
+
<div class="banner-inner">
|
| 618 |
+
<h1>๐๏ธ Car Racing Agent</h1>
|
| 619 |
+
<p class="sub">
|
| 620 |
+
A PPO agent trained from scratch ยท 10 tracks ยท egocentric vision ยท
|
| 621 |
+
<strong style="color:#15803d">10 / 10 tracks โ zero crashes</strong>
|
| 622 |
+
ยท OpenEnv Student Challenge 2026
|
| 623 |
+
</p>
|
| 624 |
+
<div class="badges">
|
| 625 |
+
<span class="badge">OpenEnv API</span>
|
| 626 |
+
<span class="badge">TorchRL PPO</span>
|
| 627 |
+
<span class="badge">Curriculum Learning</span>
|
| 628 |
+
<span class="badge">ImpalaCNN</span>
|
| 629 |
+
<span class="badge">~1.3 M Steps</span>
|
| 630 |
+
<span class="badge green">10 / 10 โ
</span>
|
| 631 |
+
<a class="badge hf-badge" href="https://huggingface.co/spaces/nirmalpratheep/curriculum-car-racer" target="_blank" rel="noopener">๐ Blog Post</a>
|
| 632 |
+
</div>
|
| 633 |
+
</div>
|
| 634 |
+
</div>
|
| 635 |
+
"""
|
| 636 |
+
|
| 637 |
+
HELP_HTML = f"""
|
| 638 |
+
<div class="help-box">
|
| 639 |
+
<div class="help-title">๐ฎ How to use</div>
|
| 640 |
+
<div class="step">
|
| 641 |
+
<div class="num">1</div>
|
| 642 |
+
<div>Pick any of the <strong>10 curriculum tracks</strong> โ from the easy Wide Oval up to the Hairpin and Chicane.</div>
|
| 643 |
+
</div>
|
| 644 |
+
<div class="step">
|
| 645 |
+
<div class="num">2</div>
|
| 646 |
+
<div>Click <strong>Reset</strong> to spawn the agent at the start line. The track view and agent POV appear immediately.</div>
|
| 647 |
+
</div>
|
| 648 |
+
<div class="step">
|
| 649 |
+
<div class="num">3</div>
|
| 650 |
+
<div>Click <strong>Step ร{STEP_CHUNK}</strong> to advance {STEP_CHUNK} physics steps at a time โ watch the path trail build up.</div>
|
| 651 |
+
</div>
|
| 652 |
+
<div class="step">
|
| 653 |
+
<div class="num">4</div>
|
| 654 |
+
<div>Click <strong>Auto-Drive</strong> to run a full lap and generate a <strong>replay video</strong> below.</div>
|
| 655 |
+
</div>
|
| 656 |
+
<div class="footer-note">
|
| 657 |
+
The <span class="accent">orange-bordered image</span> is the actual 64ร64 input the neural network receives โ
|
| 658 |
+
always rotated so the car faces upward.
|
| 659 |
+
</div>
|
| 660 |
+
</div>
|
| 661 |
+
"""
|
| 662 |
+
|
| 663 |
+
FOOTER_HTML = """
|
| 664 |
+
<div class="footer-strip">
|
| 665 |
+
Car Racing Agent ยท trained with
|
| 666 |
+
<a href="https://github.com/pytorch/rl" target="_blank">TorchRL</a> PPO ยท
|
| 667 |
+
served via <a href="https://openenv.dev" target="_blank">OpenEnv</a> ยท
|
| 668 |
+
<a href="https://huggingface.co/spaces/nirmalpratheep/Car-Racing-Agent" target="_blank">๐ค HF Space</a> ยท
|
| 669 |
+
<a href="https://huggingface.co/blog/NirmalPratheep/curriculum-car-racer" target="_blank">๐ Blog</a> ยท
|
| 670 |
+
<a href="https://github.com/NirmalPratheep/curriculum-car-racer" target="_blank">GitHub</a>
|
| 671 |
+
</div>
|
| 672 |
+
"""
|
| 673 |
+
|
| 674 |
+
# โโ Build UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 675 |
+
with gr.Blocks(title="Car Racing Agent โ OpenEnv Demo") as demo:
|
| 676 |
+
|
| 677 |
+
gr.HTML(BANNER_HTML)
|
| 678 |
+
|
| 679 |
+
session_state = gr.State(None)
|
| 680 |
+
|
| 681 |
+
with gr.Row(equal_height=False):
|
| 682 |
+
|
| 683 |
+
# โโ Left column โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 684 |
+
with gr.Column(scale=1, min_width=260):
|
| 685 |
+
gr.HTML("<div class='panel-title' style='margin:4px 0 6px'>Select Track</div>")
|
| 686 |
+
track_dd = gr.Dropdown(
|
| 687 |
+
choices=TRACK_CHOICES, value=TRACK_CHOICES[0],
|
| 688 |
+
label="", interactive=True,
|
| 689 |
+
)
|
| 690 |
+
gr.HTML("<div style='height:10px'></div>")
|
| 691 |
+
reset_btn = gr.Button("๐ฌ Reset", variant="secondary")
|
| 692 |
+
step_btn = gr.Button(f"โฉ Step ร{STEP_CHUNK}", variant="primary")
|
| 693 |
+
auto_btn = gr.Button("๐ Auto-Drive Full Lap", variant="stop")
|
| 694 |
+
gr.HTML("<div style='height:10px'></div>")
|
| 695 |
+
status_out = gr.HTML(_status_html(None))
|
| 696 |
+
gr.HTML("<div style='height:6px'></div>")
|
| 697 |
+
gr.HTML(HELP_HTML)
|
| 698 |
+
|
| 699 |
+
# โโ Right column โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 700 |
+
with gr.Column(scale=3):
|
| 701 |
+
gr.HTML("<div class='panel-title' style='margin:4px 0 6px'>Live Track View</div>")
|
| 702 |
+
with gr.Row(equal_height=False):
|
| 703 |
+
with gr.Column(scale=3, min_width=300):
|
| 704 |
+
frame_img = gr.Image(
|
| 705 |
+
label="Top-Down Track", type="pil",
|
| 706 |
+
height=360, interactive=False,
|
| 707 |
+
elem_classes=["main-view"],
|
| 708 |
+
value=_placeholder_main(),
|
| 709 |
+
)
|
| 710 |
+
with gr.Column(scale=1, min_width=160):
|
| 711 |
+
headlight_img = gr.Image(
|
| 712 |
+
label="Agent POV โ CNN Input (64ร64)",
|
| 713 |
+
type="pil", height=200, interactive=False,
|
| 714 |
+
elem_classes=["agent-pov"],
|
| 715 |
+
value=_placeholder_pov(),
|
| 716 |
+
)
|
| 717 |
+
gr.HTML("<div style='height:10px'></div>")
|
| 718 |
+
gr.HTML("<div class='panel-title' style='margin:4px 0 6px'>Auto-Drive Replay</div>")
|
| 719 |
+
video_out = gr.Video(
|
| 720 |
+
label="", height=300, show_label=False,
|
| 721 |
+
elem_classes=["video-panel"],
|
| 722 |
+
)
|
| 723 |
+
|
| 724 |
+
gr.HTML(FOOTER_HTML)
|
| 725 |
+
|
| 726 |
+
# โโ Wire callbacks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 727 |
+
step_event = step_btn.click(
|
| 728 |
+
fn=step_agent,
|
| 729 |
+
inputs=[session_state],
|
| 730 |
+
outputs=[session_state, frame_img, headlight_img, status_out, video_out],
|
| 731 |
+
)
|
| 732 |
+
auto_event = auto_btn.click(
|
| 733 |
+
fn=auto_drive,
|
| 734 |
+
inputs=[track_dd],
|
| 735 |
+
outputs=[session_state, frame_img, headlight_img, status_out, video_out],
|
| 736 |
+
)
|
| 737 |
+
# Reset cancels any running auto-drive or step generator, then resets.
|
| 738 |
+
reset_btn.click(
|
| 739 |
+
fn=reset,
|
| 740 |
+
inputs=[track_dd],
|
| 741 |
+
outputs=[session_state, frame_img, headlight_img, status_out, video_out],
|
| 742 |
+
cancels=[auto_event, step_event],
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
if __name__ == "__main__":
|
| 747 |
+
demo.queue(default_concurrency_limit=2)
|
| 748 |
+
demo.launch(
|
| 749 |
+
server_name="0.0.0.0",
|
| 750 |
+
server_port=int(os.environ.get("PORT", 7860)),
|
| 751 |
+
css=CSS,
|
| 752 |
+
theme=gr.themes.Soft(primary_hue="orange"),
|
| 753 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.12.0
|
| 2 |
+
torch
|
| 3 |
+
torchrl
|
| 4 |
+
tensordict
|
| 5 |
+
pygame
|
| 6 |
+
numpy
|
| 7 |
+
Pillow
|
| 8 |
+
imageio
|
| 9 |
+
imageio-ffmpeg
|
| 10 |
+
openenv-core
|
| 11 |
+
pydantic
|