cartpole-v1-ppo / record_replay_mp4.py
Hwihwa_Lab
feat: upgrade replay.mp4 to 1:1 square (720x720) close-up viewport for HF video preview widget
e780fe4
Raw History Blame Contribute Delete
4.35 kB
# -*- coding: utf-8 -*-
"""
HWIHWA LAB // Ultra-High-Definition 1:1 Square (720x720) Replay Video Generator
Optimized specifically for Hugging Face Model Hub's Video Preview Sidebar Widget.
- 1:1 Square Aspect Ratio (720x720)
- Huge, bold Cart & Pole dynamics centered in the viewport
- Big shock impulse laser & particle physics
- Clean HUD overlay (Active force, score, shock banner)
- Ultra-sharp H.264 (CRF 17, 25 FPS)
"""
import time
import io
from pathlib import Path
import numpy as np
from PIL import Image
import imageio
from playwright.sync_api import sync_playwright
def generate_square_canvas_replay():
current_dir = Path(__file__).resolve().parent
html_url = f"file:///{current_dir.as_posix()}/index.html"
output_mp4 = current_dir / "replay.mp4"
raw_frames = []
print("๐Ÿš€ Launching Headless Chromium to record 1:1 SQUARE (720x720) CartPole Dynamics...")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# 720x720 Square Viewport with device_scale_factor=1
context = browser.new_context(viewport={"width": 720, "height": 720}, device_scale_factor=1)
page = context.new_page()
page.goto(html_url)
page.wait_for_timeout(1000) # Wait for weights & chart initialization
# Maximize Stage to 100% of 720x720 viewport
page.evaluate("""() => {
const header = document.querySelector('.header-bar');
if (header) header.style.display = 'none';
const bento = document.querySelector('.bento-deck-golden');
if (bento) bento.style.display = 'none';
const labMain = document.querySelector('.lab-main');
if (labMain) {
labMain.style.padding = '0';
labMain.style.height = '100vh';
}
const stageSec = document.querySelector('.stage-section');
if (stageSec) stageSec.style.height = '100vh';
const area = document.querySelector('.canvas-viewport-area');
if (area) {
area.style.height = '100vh';
area.style.borderRadius = '0';
area.style.border = 'none';
}
window.dispatchEvent(new Event('resize'));
}""")
page.wait_for_timeout(300)
stage = page.locator(".canvas-viewport-area")
box = stage.bounding_box()
if not box:
stage = page.locator("#simCanvas")
box = stage.bounding_box()
cx = 360
cy = 360
print(f" ๐ŸŽฏ Stage Bounding Box: Width={box['width']}, Height={box['height']}")
total_frames = 110 # ~4.4 seconds @ 25 FPS
for i in range(total_frames):
# Apply dynamic mouse disturbance impulse
if i == 25:
page.mouse.move(cx - 20, cy + 50)
page.mouse.down()
elif i == 30:
page.mouse.move(cx + 140, cy - 40)
elif i == 35:
page.mouse.up()
# Optional second gentle tap at frame 70
if i == 70:
page.mouse.move(cx + 20, cy + 40)
page.mouse.down()
elif i == 74:
page.mouse.move(cx - 80, cy - 30)
elif i == 78:
page.mouse.up()
screenshot_bytes = stage.screenshot(type="png")
img = Image.open(io.BytesIO(screenshot_bytes))
# Resize precisely to 720x720 if slight pixel sub-pixel rounding occurs
if img.width != 720 or img.height != 720:
img = img.resize((720, 720), Image.Resampling.LANCZOS)
raw_frames.append(np.array(img.convert("RGB")))
time.sleep(0.035)
browser.close()
print(f"๐ŸŽฌ Encoding {len(raw_frames)} square frames (720x720) to H.264 replay.mp4...")
writer = imageio.get_writer(
str(output_mp4),
fps=25,
codec="libx264",
pixelformat="yuv420p",
ffmpeg_params=["-crf", "17", "-preset", "slow"]
)
for f in raw_frames:
writer.append_data(f)
writer.close()
size_kb = round(output_mp4.stat().st_size / 1024, 1)
print(f"[โœ“] SUCCESS: 1:1 Square (720x720) Replay saved to: {output_mp4} ({size_kb} KB)")
return output_mp4
if __name__ == "__main__":
generate_square_canvas_replay()