Reinforcement Learning
stable-baselines3
PyTorch
English
Korean
deep-reinforcement-learning
ppo
proximal-policy-optimization
cartpole-v1
gymnasium
openai-gym
classic-control
robotics
physical-ai
sim-to-real
zero-shot-transfer
domain-randomization
inverted-pendulum
optimal-control
lqr-control
control-theory
interactive-demo
Eval Results (legacy)
Instructions to use hwihwalab/cartpole-v1-ppo with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- stable-baselines3
How to use hwihwalab/cartpole-v1-ppo with stable-baselines3:
from huggingface_sb3 import load_from_hub checkpoint = load_from_hub( repo_id="hwihwalab/cartpole-v1-ppo", filename="{MODEL FILENAME}.zip", ) - Notebooks
- Google Colab
- Kaggle
Hwihwa_Lab commited on
Commit ·
2545b31
1
Parent(s): 639e026
feat: add official Hugging Face replay.mp4 video preview and clean up markdown body
Browse files- .gitattributes +2 -1
- README.md +0 -4
- README_KR.md +0 -4
- generate_demo_gif.py +0 -227
- record_real_web_demo.py +0 -75
- replay.mp4 +3 -0
.gitattributes
CHANGED
|
@@ -2,4 +2,5 @@ models/*.zip filter=lfs diff=lfs merge=lfs -text
|
|
| 2 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
|
|
|
|
|
|
| 2 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
replay.mp4 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -65,10 +65,6 @@ model-index:
|
|
| 65 |
> 🎮 **Try Live in Browser (Zero Install)**: [👉 Open Hugging Face Spaces Live Demo](https://huggingface.co/spaces/hwihwalab/cartpole-v1-ppo)
|
| 66 |
> 📦 **Official Model Hub**: [🤗 hwihwalab/cartpole-v1-ppo](https://huggingface.co/hwihwalab/cartpole-v1-ppo) | 🐙 **GitHub Repository**: [Hwihwa-Lab/cartpole-v1-ppo](https://github.com/Hwihwa-Lab/cartpole-v1-ppo)
|
| 67 |
|
| 68 |
-
<p align="center">
|
| 69 |
-
<img src="assets/cartpole_disturbance_recovery.gif" width="100%" alt="CartPole Physical AI Disturbance Rejection Demo">
|
| 70 |
-
</p>
|
| 71 |
-
|
| 72 |
---
|
| 73 |
|
| 74 |
## 🎮 Interactive Live Demo (Hugging Face Spaces)
|
|
|
|
| 65 |
> 🎮 **Try Live in Browser (Zero Install)**: [👉 Open Hugging Face Spaces Live Demo](https://huggingface.co/spaces/hwihwalab/cartpole-v1-ppo)
|
| 66 |
> 📦 **Official Model Hub**: [🤗 hwihwalab/cartpole-v1-ppo](https://huggingface.co/hwihwalab/cartpole-v1-ppo) | 🐙 **GitHub Repository**: [Hwihwa-Lab/cartpole-v1-ppo](https://github.com/Hwihwa-Lab/cartpole-v1-ppo)
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
---
|
| 69 |
|
| 70 |
## 🎮 Interactive Live Demo (Hugging Face Spaces)
|
README_KR.md
CHANGED
|
@@ -19,10 +19,6 @@
|
|
| 19 |
> 🎮 **브라우저에서 무설치 즉시 체험**: [👉 Hugging Face Spaces 라이브 데모 실행](https://huggingface.co/spaces/hwihwalab/cartpole-v1-ppo)
|
| 20 |
> 📦 **공식 모델 허브**: [🤗 hwihwalab/cartpole-v1-ppo](https://huggingface.co/hwihwalab/cartpole-v1-ppo) | 🐙 **GitHub 리포지토리**: [Hwihwa-Lab/cartpole-v1-ppo](https://github.com/Hwihwa-Lab/cartpole-v1-ppo)
|
| 21 |
|
| 22 |
-
<p align="center">
|
| 23 |
-
<img src="assets/cartpole_disturbance_recovery.gif" width="100%" alt="CartPole Physical AI 외란 복원 물리 시뮬레이션 데모">
|
| 24 |
-
</p>
|
| 25 |
-
|
| 26 |
---
|
| 27 |
|
| 28 |
## 🎮 인터랙티브 라이브 체험관 (Hugging Face Spaces)
|
|
|
|
| 19 |
> 🎮 **브라우저에서 무설치 즉시 체험**: [👉 Hugging Face Spaces 라이브 데모 실행](https://huggingface.co/spaces/hwihwalab/cartpole-v1-ppo)
|
| 20 |
> 📦 **공식 모델 허브**: [🤗 hwihwalab/cartpole-v1-ppo](https://huggingface.co/hwihwalab/cartpole-v1-ppo) | 🐙 **GitHub 리포지토리**: [Hwihwa-Lab/cartpole-v1-ppo](https://github.com/Hwihwa-Lab/cartpole-v1-ppo)
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
---
|
| 23 |
|
| 24 |
## 🎮 인터랙티브 라이브 체험관 (Hugging Face Spaces)
|
generate_demo_gif.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
HWIHWA LAB // CartPole-v1 Cybernetic Animated GIF Generator
|
| 4 |
-
Renders a high-resolution, futuristic dark neon physical AI demo showing:
|
| 5 |
-
1) Nominal balance
|
| 6 |
-
2) Instant Mouse/Force Impulse Shock (⚡ +25.0 N)
|
| 7 |
-
3) PPO 2-Phase Catching & Centering Maneuver
|
| 8 |
-
4) Real-time Phase Portrait Attractor Convergence (θ vs θ_dot)
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import os
|
| 12 |
-
import json
|
| 13 |
-
import math
|
| 14 |
-
import numpy as np
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
from PIL import Image, ImageDraw, ImageFont
|
| 17 |
-
|
| 18 |
-
# Physics Setup
|
| 19 |
-
TAU = 0.02
|
| 20 |
-
FORCE_MAG = 10.0
|
| 21 |
-
MASSCART = 1.0
|
| 22 |
-
MASSPOLE = 0.1
|
| 23 |
-
TOTAL_MASS = MASSCART + MASSPOLE
|
| 24 |
-
LENGTH = 0.5
|
| 25 |
-
POLEMASS_LENGTH = MASSPOLE * LENGTH
|
| 26 |
-
GRAVITY = 9.81
|
| 27 |
-
X_THRESHOLD = 2.4
|
| 28 |
-
THETA_THRESHOLD = 12.0 * 2.0 * math.pi / 360.0
|
| 29 |
-
|
| 30 |
-
def load_weights(path: Path):
|
| 31 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 32 |
-
data = json.load(f)
|
| 33 |
-
return {
|
| 34 |
-
"w1": np.array(data["w1"]),
|
| 35 |
-
"b1": np.array(data["b1"]),
|
| 36 |
-
"w2": np.array(data["w2"]),
|
| 37 |
-
"b2": np.array(data["b2"]),
|
| 38 |
-
"w_out": np.array(data["w_out"]),
|
| 39 |
-
"b_out": np.array(data["b_out"]),
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
def ppo_policy(obs: np.ndarray, w: dict) -> int:
|
| 43 |
-
h1 = np.tanh(np.dot(w["w1"], obs) + w["b1"])
|
| 44 |
-
h2 = np.tanh(np.dot(w["w2"], h1) + w["b2"])
|
| 45 |
-
logits = np.dot(w["w_out"], h2) + w["b_out"]
|
| 46 |
-
p1 = 1.0 / (1.0 + np.exp(-(logits[1] - logits[0])))
|
| 47 |
-
return 1 if p1 > 0.5 else 0
|
| 48 |
-
|
| 49 |
-
def step_physics(state: np.ndarray, action: int, shock_force: float = 0.0):
|
| 50 |
-
x, x_dot, theta, theta_dot = state
|
| 51 |
-
force = (FORCE_MAG if action == 1 else -FORCE_MAG) + shock_force
|
| 52 |
-
|
| 53 |
-
costheta = math.cos(theta)
|
| 54 |
-
sintheta = math.sin(theta)
|
| 55 |
-
|
| 56 |
-
temp = (force + POLEMASS_LENGTH * theta_dot**2 * sintheta) / TOTAL_MASS
|
| 57 |
-
thetaacc = (GRAVITY * sintheta - costheta * temp) / (
|
| 58 |
-
LENGTH * (4.0 / 3.0 - MASSPOLE * costheta**2 / TOTAL_MASS)
|
| 59 |
-
)
|
| 60 |
-
xacc = temp - POLEMASS_LENGTH * thetaacc * costheta / TOTAL_MASS
|
| 61 |
-
|
| 62 |
-
# Euler-Cromer
|
| 63 |
-
x = x + TAU * x_dot
|
| 64 |
-
x_dot = x_dot + TAU * xacc
|
| 65 |
-
theta = theta + TAU * theta_dot
|
| 66 |
-
theta_dot = theta_dot + TAU * thetaacc
|
| 67 |
-
|
| 68 |
-
return np.array([x, x_dot, theta, theta_dot])
|
| 69 |
-
|
| 70 |
-
def render_frame(state, step, total_steps, shock_active, phase_history, W=800, H=420):
|
| 71 |
-
img = Image.new("RGB", (W, H), "#060913")
|
| 72 |
-
draw = ImageDraw.Draw(img)
|
| 73 |
-
|
| 74 |
-
# 1. Subtle Cybernetic Grid
|
| 75 |
-
for gx in range(0, W, 40):
|
| 76 |
-
draw.line([(gx, 0), (gx, H)], fill="#0d1527", width=1)
|
| 77 |
-
for gy in range(0, H, 40):
|
| 78 |
-
draw.line([(0, gy), (W, gy)], fill="#0d1527", width=1)
|
| 79 |
-
|
| 80 |
-
# 2. Header Bar HUD
|
| 81 |
-
draw.rectangle([(0, 0), (W, 46)], fill="#090e1d", outline="#1e293b", width=1)
|
| 82 |
-
draw.text((20, 14), "🤖 CARTPOLE-V1 PHYSICAL AI // PPO DISTURBANCE RECOVERY", fill="#38bdf8")
|
| 83 |
-
draw.text((W - 220, 14), f"SIM TIME: {step*TAU:.2f}s | 60 FPS", fill="#94a3b8")
|
| 84 |
-
|
| 85 |
-
# 3. Main Stage Rail
|
| 86 |
-
rail_y = 260
|
| 87 |
-
draw.line([(60, rail_y), (W - 260, rail_y)], fill="#1e293b", width=4)
|
| 88 |
-
# Center tick
|
| 89 |
-
draw.line([( (W - 200) // 2, rail_y - 8), ( (W - 200) // 2, rail_y + 8)], fill="#38bdf8", width=2)
|
| 90 |
-
|
| 91 |
-
# World to Screen Coordinates
|
| 92 |
-
stage_center_x = (W - 200) // 2
|
| 93 |
-
meters_to_pixels = 95.0
|
| 94 |
-
|
| 95 |
-
cart_x_screen = stage_center_x + state[0] * meters_to_pixels
|
| 96 |
-
cart_w, cart_h = 76, 36
|
| 97 |
-
cart_left = cart_x_screen - cart_w // 2
|
| 98 |
-
cart_top = rail_y - cart_h - 6
|
| 99 |
-
|
| 100 |
-
# Glow under cart
|
| 101 |
-
draw.ellipse([(cart_left - 10, rail_y - 12), (cart_left + cart_w + 10, rail_y + 8)], fill="#003b46")
|
| 102 |
-
|
| 103 |
-
# Draw Cart Body
|
| 104 |
-
draw.rounded_rectangle([(cart_left, cart_top), (cart_left + cart_w, cart_top + cart_h)], radius=6, fill="#0ea5e9", outline="#38bdf8", width=2)
|
| 105 |
-
# Cart Wheels
|
| 106 |
-
wheel_r = 6
|
| 107 |
-
draw.ellipse([(cart_left + 10, rail_y - wheel_r*2), (cart_left + 10 + wheel_r*2, rail_y)], fill="#334155", outline="#94a3b8", width=1)
|
| 108 |
-
draw.ellipse([(cart_left + cart_w - 10 - wheel_r*2, rail_y - wheel_r*2), (cart_left + cart_w - 10, rail_y)], fill="#334155", outline="#94a3b8", width=1)
|
| 109 |
-
|
| 110 |
-
# Pivot Point
|
| 111 |
-
pivot_x = cart_x_screen
|
| 112 |
-
pivot_y = cart_top + 10
|
| 113 |
-
draw.ellipse([(pivot_x - 5, pivot_y - 5), (pivot_x + 5, pivot_y + 5)], fill="#ffffff")
|
| 114 |
-
|
| 115 |
-
# Pole Tip
|
| 116 |
-
pole_len_px = LENGTH * 2 * meters_to_pixels * 1.3
|
| 117 |
-
tip_x = pivot_x + pole_len_px * math.sin(state[2])
|
| 118 |
-
tip_y = pivot_y - pole_len_px * math.cos(state[2])
|
| 119 |
-
|
| 120 |
-
# Pole Rod (Neon Emerald Green)
|
| 121 |
-
draw.line([(pivot_x, pivot_y), (tip_x, tip_y)], fill="#10b981", width=6)
|
| 122 |
-
draw.ellipse([(tip_x - 7, tip_y - 7), (tip_x + 7, tip_y + 7)], fill="#34d399", outline="#ffffff", width=2)
|
| 123 |
-
|
| 124 |
-
# 4. Shock Impulse Visualizer
|
| 125 |
-
if shock_active:
|
| 126 |
-
# Laser force arrow
|
| 127 |
-
arrow_y = cart_top + cart_h // 2
|
| 128 |
-
draw.line([(cart_left - 80, arrow_y), (cart_left - 8, arrow_y)], fill="#ef4444", width=5)
|
| 129 |
-
draw.polygon([(cart_left - 8, arrow_y - 8), (cart_left + 4, arrow_y), (cart_left - 8, arrow_y + 8)], fill="#ef4444")
|
| 130 |
-
# Shock text banner
|
| 131 |
-
draw.rounded_rectangle([(stage_center_x - 130, 70), (stage_center_x + 130, 105)], radius=6, fill="#7f1d1d", outline="#ef4444", width=2)
|
| 132 |
-
draw.text((stage_center_x - 110, 78), "⚡ MOUSE SHOCK: +25.0 N", fill="#fecaca")
|
| 133 |
-
|
| 134 |
-
# 5. Right Panel: Live Phase Portrait (θ vs θ_dot)
|
| 135 |
-
radar_x, radar_y, radar_size = W - 230, 65, 205
|
| 136 |
-
draw.rounded_rectangle([(radar_x, radar_y), (radar_x + radar_size, radar_y + radar_size + 95)], radius=8, fill="#0b1120", outline="#1e293b", width=2)
|
| 137 |
-
draw.text((radar_x + 15, radar_y + 12), "🌀 PHASE PORTRAIT", fill="#38bdf8")
|
| 138 |
-
draw.text((radar_x + 15, radar_y + 30), "Origin Attractor (0, 0)", fill="#64748b")
|
| 139 |
-
|
| 140 |
-
center_rx = radar_x + radar_size // 2
|
| 141 |
-
center_ry = radar_y + 40 + (radar_size) // 2
|
| 142 |
-
|
| 143 |
-
# Radar Grid
|
| 144 |
-
draw.ellipse([(center_rx - 65, center_ry - 65), (center_rx + 65, center_ry + 65)], outline="#1e293b", width=1)
|
| 145 |
-
draw.ellipse([(center_rx - 35, center_ry - 35), (center_rx + 35, center_ry + 35)], outline="#1e293b", width=1)
|
| 146 |
-
draw.line([(center_rx - 75, center_ry), (center_rx + 75, center_ry)], fill="#1e293b", width=1)
|
| 147 |
-
draw.line([(center_rx, center_ry - 75), (center_rx, center_ry + 75)], fill="#1e293b", width=1)
|
| 148 |
-
|
| 149 |
-
# Plot Spiral Trajectory
|
| 150 |
-
if len(phase_history) > 1:
|
| 151 |
-
points = []
|
| 152 |
-
for th, th_d in phase_history:
|
| 153 |
-
px = center_rx + th * 420.0
|
| 154 |
-
py = center_ry - th_d * 22.0
|
| 155 |
-
points.append((px, py))
|
| 156 |
-
for i in range(len(points) - 1):
|
| 157 |
-
alpha_color = "#38bdf8" if i > len(points) - 15 else "#0369a1"
|
| 158 |
-
draw.line([points[i], points[i+1]], fill=alpha_color, width=2)
|
| 159 |
-
|
| 160 |
-
# Current State Dot
|
| 161 |
-
curr_px = center_rx + state[2] * 420.0
|
| 162 |
-
curr_py = center_ry - state[3] * 22.0
|
| 163 |
-
draw.ellipse([(curr_px - 4, curr_py - 4), (curr_px + 4, curr_py + 4)], fill="#00f2fe", outline="#ffffff", width=1)
|
| 164 |
-
|
| 165 |
-
# 6. Bottom Telemetry Pills
|
| 166 |
-
telemetry_y = H - 42
|
| 167 |
-
draw.rectangle([(0, H - 52), (W, H)], fill="#090e1d", outline="#1e293b", width=1)
|
| 168 |
-
draw.text((20, telemetry_y), f"CTRL: TRAINED PPO (20K)", fill="#10b981")
|
| 169 |
-
draw.text((220, telemetry_y), f"CART POS: x = {state[0]:+.3f} m", fill="#f8fafc")
|
| 170 |
-
draw.text((430, telemetry_y), f"POLE ANGLE: θ = {math.degrees(state[2]):+.2f}°", fill="#fbbf24")
|
| 171 |
-
draw.text((640, telemetry_y), f"STABILITY: 100% SOLVED", fill="#38bdf8")
|
| 172 |
-
|
| 173 |
-
return img
|
| 174 |
-
|
| 175 |
-
def generate_animation():
|
| 176 |
-
current_dir = Path(__file__).resolve().parent
|
| 177 |
-
assets_dir = current_dir / "assets"
|
| 178 |
-
assets_dir.mkdir(parents=True, exist_ok=True)
|
| 179 |
-
|
| 180 |
-
weights_path = current_dir / "cartpole_weights.json"
|
| 181 |
-
weights = load_weights(weights_path)
|
| 182 |
-
|
| 183 |
-
# Simulation setup
|
| 184 |
-
state = np.array([0.0, 0.0, 0.01, 0.0])
|
| 185 |
-
phase_history = []
|
| 186 |
-
frames = []
|
| 187 |
-
|
| 188 |
-
print("🎬 Simulating and rendering high-resolution Cybernetic GIF...")
|
| 189 |
-
|
| 190 |
-
total_steps = 140 # ~2.8 seconds @ 50 FPS
|
| 191 |
-
for step in range(total_steps):
|
| 192 |
-
# Trigger shock between step 25 and 35
|
| 193 |
-
shock_active = (25 <= step <= 34)
|
| 194 |
-
shock_force = 26.0 if shock_active else 0.0
|
| 195 |
-
|
| 196 |
-
# Action from PPO
|
| 197 |
-
action = ppo_policy(state, weights)
|
| 198 |
-
|
| 199 |
-
# Record phase
|
| 200 |
-
phase_history.append((state[2], state[3]))
|
| 201 |
-
if len(phase_history) > 60:
|
| 202 |
-
phase_history.pop(0)
|
| 203 |
-
|
| 204 |
-
# Render frame
|
| 205 |
-
if step % 2 == 0: # Render at 25 FPS effective
|
| 206 |
-
frame_img = render_frame(state, step, total_steps, shock_active, phase_history)
|
| 207 |
-
frames.append(frame_img)
|
| 208 |
-
|
| 209 |
-
# Physics update
|
| 210 |
-
state = step_physics(state, action, shock_force=shock_force)
|
| 211 |
-
|
| 212 |
-
# Save Animated GIF
|
| 213 |
-
gif_path = assets_dir / "cartpole_disturbance_recovery.gif"
|
| 214 |
-
frames[0].save(
|
| 215 |
-
gif_path,
|
| 216 |
-
save_all=True,
|
| 217 |
-
append_images=frames[1:],
|
| 218 |
-
duration=40, # 25 FPS (40ms per frame)
|
| 219 |
-
loop=0,
|
| 220 |
-
optimize=True
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
print(f"[✓] Successfully generated Animated Demo GIF: {gif_path} ({len(frames)} frames, {round(gif_path.stat().st_size / 1024, 1)} KB)")
|
| 224 |
-
return gif_path
|
| 225 |
-
|
| 226 |
-
if __name__ == "__main__":
|
| 227 |
-
generate_animation()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
record_real_web_demo.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
HWIHWA LAB // Real In-Browser Screen Recorder for CartPole Physical AI Suite
|
| 4 |
-
Optimized WebP/GIF compression for ultra-fast loading on Hugging Face & GitHub.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import time
|
| 8 |
-
import io
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from PIL import Image
|
| 11 |
-
from playwright.sync_api import sync_playwright
|
| 12 |
-
|
| 13 |
-
def record_real_web_demo():
|
| 14 |
-
current_dir = Path(__file__).resolve().parent
|
| 15 |
-
html_url = f"file:///{current_dir.as_posix()}/index.html"
|
| 16 |
-
assets_dir = current_dir / "assets"
|
| 17 |
-
assets_dir.mkdir(parents=True, exist_ok=True)
|
| 18 |
-
|
| 19 |
-
raw_frames = []
|
| 20 |
-
|
| 21 |
-
print("🚀 Launching Headless Chromium to record REAL CartPole Simulator...")
|
| 22 |
-
with sync_playwright() as p:
|
| 23 |
-
browser = p.chromium.launch(headless=True)
|
| 24 |
-
context = browser.new_context(viewport={"width": 1280, "height": 720}, device_scale_factor=1)
|
| 25 |
-
page = context.new_page()
|
| 26 |
-
|
| 27 |
-
page.goto(html_url)
|
| 28 |
-
page.wait_for_timeout(800)
|
| 29 |
-
|
| 30 |
-
canvas = page.locator("#simCanvas")
|
| 31 |
-
box = canvas.bounding_box()
|
| 32 |
-
cx = box["x"] + box["width"] / 2
|
| 33 |
-
cy = box["y"] + box["height"] / 2
|
| 34 |
-
|
| 35 |
-
total_frames = 65
|
| 36 |
-
for i in range(total_frames):
|
| 37 |
-
# Mouse disturbance at frame 16-20
|
| 38 |
-
if i == 15:
|
| 39 |
-
page.mouse.move(cx - 20, cy + 20)
|
| 40 |
-
page.mouse.down()
|
| 41 |
-
elif i == 18:
|
| 42 |
-
page.mouse.move(cx + 140, cy - 30)
|
| 43 |
-
elif i == 21:
|
| 44 |
-
page.mouse.up()
|
| 45 |
-
|
| 46 |
-
screenshot_bytes = page.screenshot(type="png")
|
| 47 |
-
img = Image.open(io.BytesIO(screenshot_bytes))
|
| 48 |
-
|
| 49 |
-
# Resize to 800x450 for crisp, fast-loading web display
|
| 50 |
-
img_resized = img.resize((800, 450), Image.Resampling.LANCZOS)
|
| 51 |
-
# Quantize to adaptive 128-color palette to shrink GIF file size drastically
|
| 52 |
-
img_quant = img_resized.convert("P", palette=Image.Palette.ADAPTIVE, colors=128)
|
| 53 |
-
raw_frames.append(img_quant)
|
| 54 |
-
|
| 55 |
-
time.sleep(0.04)
|
| 56 |
-
|
| 57 |
-
browser.close()
|
| 58 |
-
|
| 59 |
-
output_gif = assets_dir / "cartpole_disturbance_recovery.gif"
|
| 60 |
-
|
| 61 |
-
raw_frames[0].save(
|
| 62 |
-
output_gif,
|
| 63 |
-
save_all=True,
|
| 64 |
-
append_images=raw_frames[1:],
|
| 65 |
-
duration=50, # 20 FPS (50ms per frame)
|
| 66 |
-
loop=0,
|
| 67 |
-
optimize=True
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
size_mb = round(output_gif.stat().st_size / 1024 / 1024, 2)
|
| 71 |
-
print(f"[✓] SUCCESS: Optimized Real Demo GIF generated: {output_gif} ({len(raw_frames)} frames, {size_mb} MB)")
|
| 72 |
-
return output_gif
|
| 73 |
-
|
| 74 |
-
if __name__ == "__main__":
|
| 75 |
-
record_real_web_demo()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
replay.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a3af8a5e678207173543d07b67f6efa785267bbd54a1a74dfbd9bb5393df576d
|
| 3 |
+
size 142069
|