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
Files changed (6) hide show
  1. .gitattributes +2 -1
  2. README.md +0 -4
  3. README_KR.md +0 -4
  4. generate_demo_gif.py +0 -227
  5. record_real_web_demo.py +0 -75
  6. 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