Download app.py from kyoungsim/microduck: direct link, hf CLI and curl.
- Browser
- Download file 7.53 kB
-
https://huggingface.co/kyoungsim/microduck/resolve/main/app.py
- Command line
-
hf download hf://kyoungsim/microduck/app.py
-
curl -L -o app.py https://huggingface.co/kyoungsim/microduck/resolve/main/app.py
7.53 kB
| import os | |
| import tempfile | |
| import numpy as np | |
| import imageio | |
| import gradio as gr | |
| import mujoco | |
| from infer_engine import SpacePolicyInference | |
| CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| ROBOT_DIR = os.path.join(CURRENT_DIR, "robot") | |
| POLICIES_DIR = os.path.join(CURRENT_DIR, "policies") | |
| def simulate_motion(motion_type, duration, camera_angle, progress=gr.Progress()): | |
| progress(0, desc="์๋ฎฌ๋ ์ด์ ํ๊ฒฝ ์ด๊ธฐํ ์ค...") | |
| # Select XML model | |
| if motion_type == "โฝ ์ผ๋ฐ ๊ณต์ฐจ๊ธฐ (Ball Kick)": | |
| xml_path = os.path.join(ROBOT_DIR, "scene_ball.xml") | |
| else: | |
| xml_path = os.path.join(ROBOT_DIR, "scene.xml") | |
| model = mujoco.MjModel.from_xml_path(xml_path) | |
| model.opt.timestep = 0.005 | |
| data = mujoco.MjData(model) | |
| # Initialize policy | |
| engine = SpacePolicyInference(model, data, POLICIES_DIR) | |
| # Reset robot position to standing default | |
| freejoint_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, "trunk_base_freejoint") | |
| qpos_adr = model.jnt_qposadr[freejoint_id] | |
| data.qpos[qpos_adr + 0] = 0.0 | |
| data.qpos[qpos_adr + 1] = 0.0 | |
| data.qpos[qpos_adr + 2] = 0.125 | |
| data.qpos[qpos_adr + 3:qpos_adr + 7] = [1, 0, 0, 0] | |
| for i, idx in enumerate(engine.joint_qpos_indices): | |
| data.qpos[idx] = engine.default_pose[i] | |
| data.ctrl[:] = engine.default_pose | |
| mujoco.mj_forward(model, data) | |
| # Configure behavior | |
| if motion_type == "๐ถ ์ ์ง ๊ฑท๊ธฐ (Forward Walk)": | |
| engine.set_vel_cmd(0.25, 0.0, 0.0) | |
| elif motion_type == "๐ถ ํ์ง ๊ฑท๊ธฐ (Backward Walk)": | |
| engine.set_vel_cmd(-0.2, 0.0, 0.0) | |
| elif motion_type == "๐ ์ขํ์ (Turn Left)": | |
| engine.set_vel_cmd(0.1, 0.0, 0.8) | |
| elif motion_type == "๐ ์ฐํ์ (Turn Right)": | |
| engine.set_vel_cmd(0.1, 0.0, -0.8) | |
| elif motion_type == "๐ง ์ ์๋ฆฌ ์๊ธฐ (Standing Balance)": | |
| engine.set_vel_cmd(0.0, 0.0, 0.0) | |
| elif motion_type == "๐คธ ์๊ตฌ๋ฅด๊ธฐ (Roulade)": | |
| engine.trigger_behavior("roulade") | |
| elif motion_type == "โฝ ์ผ๋ฐ ๊ณต์ฐจ๊ธฐ (Ball Kick)": | |
| engine.trigger_behavior("kick_left") | |
| elif motion_type == "๐ง ๋ฐ๋ฅ ์ค๊ธฐ (Ground Pick)": | |
| engine.trigger_ground_pick() | |
| elif motion_type == "๐ช ์๊ธฐ (Sit Down)": | |
| engine.trigger_sit() | |
| # Offscreen renderer | |
| width, height = 640, 480 | |
| renderer = mujoco.Renderer(model, height, width) | |
| # Setup camera | |
| camera = mujoco.MjvCamera() | |
| mujoco.mjv_defaultCamera(camera) | |
| frames = [] | |
| fps = 30 | |
| control_freq = 50 # Hz | |
| decimation = 4 | |
| total_steps = int(duration * control_freq) | |
| dt = 1.0 / control_freq | |
| # Render frequency (every ~1.66 control steps to reach 30 fps) | |
| sim_time = 0.0 | |
| next_render_time = 0.0 | |
| for step_i in range(total_steps): | |
| engine.update(dt) | |
| engine.step() | |
| for _ in range(decimation): | |
| mujoco.mj_step(model, data) | |
| sim_time += dt | |
| if sim_time >= next_render_time: | |
| # Update camera to track robot | |
| robot_pos = data.qpos[qpos_adr:qpos_adr + 3] | |
| camera.lookat = [robot_pos[0], robot_pos[1], robot_pos[2] + 0.05] | |
| if camera_angle == "์ธก๋ฉด ์ถ์ (Side View)": | |
| camera.distance = 1.1 | |
| camera.elevation = -15 | |
| camera.azimuth = 90 | |
| elif camera_angle == "์ ๋ฉด ๋ทฐ (Front View)": | |
| camera.distance = 1.0 | |
| camera.elevation = -10 | |
| camera.azimuth = 180 | |
| elif camera_angle == "๋๊ฐ์ ๋ทฐ (Isometric)": | |
| camera.distance = 1.2 | |
| camera.elevation = -22 | |
| camera.azimuth = 135 | |
| elif camera_angle == "ํ๋ฐฉ 3์ธ์นญ (Behind View)": | |
| camera.distance = 1.1 | |
| camera.elevation = -12 | |
| camera.azimuth = 0 | |
| renderer.update_scene(data, camera=camera) | |
| img = renderer.render() | |
| frames.append(img) | |
| next_render_time += 1.0 / fps | |
| if step_i % 15 == 0: | |
| progress(step_i / total_steps, desc=f"์๋ฎฌ๋ ์ด์ ์งํ ์ค... ({step_i}/{total_steps})") | |
| progress(0.95, desc="๋น๋์ค ํ์ผ ์์ฑ ์ค...") | |
| temp_dir = tempfile.mkdtemp() | |
| video_path = os.path.join(temp_dir, "output.mp4") | |
| imageio.mimsave(video_path, frames, fps=fps) | |
| final_pos = data.qpos[qpos_adr:qpos_adr + 3] | |
| distance = np.linalg.norm(final_pos[:2]) | |
| status_md = f""" | |
| ### ๐ ์๋ฎฌ๋ ์ด์ ๊ฒฐ๊ณผ ์์ฝ | |
| - **์ ํ๋ ๋์**: `{motion_type}` | |
| - **์๋ฎฌ๋ ์ด์ ์๊ฐ**: `{duration:.1f}์ด` (์ด {len(frames)} ํ๋ ์ / 30 FPS) | |
| - **์ต์ข ์ด๋ ๊ฑฐ๋ฆฌ**: `{distance:.3f} m` (X: `{final_pos[0]:.2f}m`, Y: `{final_pos[1]:.2f}m`, Z: `{final_pos[2]:.2f}m`) | |
| - **๋ก๋ด ์ํ**: `์ ์ ์์ ์ํ ์ ์ง ์๋ฃ` | |
| """ | |
| return video_path, status_md | |
| # Gradio Interface | |
| custom_css = """ | |
| #container { max-width: 960px; margin: 0 auto; } | |
| .gr-button-primary { background: linear-gradient(90deg, #F59E0B 0%, #D97706 100%) !important; border: none !important; } | |
| """ | |
| with gr.Blocks(title="๐ฆ MicroDuck 3D Robot Simulator", css=custom_css, theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| """ | |
| # ๐ฆ MicroDuck 3D ๋ก๋ด ๊ฐํํ์ต ์๋ฎฌ๋ ์ดํฐ | |
| MuJoCo ๋ฌผ๋ฆฌ ์์ง๊ณผ ์จ๋์ค(ONNX) ์ฌ์ธต๊ฐํํ์ต(Deep RL) ์ ์ฑ ๋ชจ๋ธ๋ก ๊ตฌ๋๋๋ **MicroDuck** ๋ก๋ด์ ๋์์ ์น์์ ์ง์ ์คํํ๊ณ ๊ด์ฐฐํด๋ณด์ธ์. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| motion_input = gr.Radio( | |
| label="๐ฏ ๋ก๋ด ๋์ (Motion Policy)", | |
| choices=[ | |
| "๐ถ ์ ์ง ๊ฑท๊ธฐ (Forward Walk)", | |
| "๐ถ ํ์ง ๊ฑท๊ธฐ (Backward Walk)", | |
| "๐ ์ขํ์ (Turn Left)", | |
| "๐ ์ฐํ์ (Turn Right)", | |
| "๐ง ์ ์๋ฆฌ ์๊ธฐ (Standing Balance)", | |
| "๐คธ ์๊ตฌ๋ฅด๊ธฐ (Roulade)", | |
| "โฝ ์ผ๋ฐ ๊ณต์ฐจ๊ธฐ (Ball Kick)", | |
| "๐ง ๋ฐ๋ฅ ์ค๊ธฐ (Ground Pick)", | |
| "๐ช ์๊ธฐ (Sit Down)", | |
| ], | |
| value="๐ถ ์ ์ง ๊ฑท๊ธฐ (Forward Walk)" | |
| ) | |
| duration_input = gr.Slider( | |
| label="โฑ๏ธ ์๋ฎฌ๋ ์ด์ ์๊ฐ (์ด)", | |
| minimum=1.5, | |
| maximum=4.5, | |
| value=2.5, | |
| step=0.5 | |
| ) | |
| camera_input = gr.Dropdown( | |
| label="๐ฅ ์นด๋ฉ๋ผ ์ต๊ธ", | |
| choices=["์ธก๋ฉด ์ถ์ (Side View)", "์ ๋ฉด ๋ทฐ (Front View)", "๋๊ฐ์ ๋ทฐ (Isometric)", "ํ๋ฐฉ 3์ธ์นญ (Behind View)"], | |
| value="๋๊ฐ์ ๋ทฐ (Isometric)" | |
| ) | |
| run_btn = gr.Button("๐ฌ 3D ์๋ฎฌ๋ ์ด์ ๋ ๋๋ง ์์", variant="primary", size="lg") | |
| with gr.Column(scale=6): | |
| video_output = gr.Video(label="๐น ์๋ฎฌ๋ ์ด์ ๋ ๋๋ง ์์", autoplay=True, loop=True) | |
| status_output = gr.Markdown("์๋ฎฌ๋ ์ด์ ์ ์คํํ๋ฉด ๊ฒฐ๊ณผ ํต๊ณ๊ฐ ํ์๋ฉ๋๋ค.") | |
| run_btn.click( | |
| fn=simulate_motion, | |
| inputs=[motion_input, duration_input, camera_input], | |
| outputs=[video_output, status_output] | |
| ) | |
| gr.Markdown( | |
| """ | |
| --- | |
| ๐ก **MicroDuck ๊ธฐ์ ์คํ**: MuJoCo Physics, ONNX Runtime, Isaac Gym / Genesis RL Trained Policies. | |
| """ | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |