CoolFace
Apppublic

ysharma/gr-workflow-microduck-lab

sourceHugging Faceupdated 24d agoView on Hugging Face
2likes
App README

๐Ÿฆ† Microduck Lab

Describe a routine in plain English. A language model compiles it into a move plan, and the real reinforcement-learning policies that ship with [Microduck](https://pollen-robotics.com/microduck/) execute it against MuJoCo physics โ€” then you get the video, the telemetry and a report.

[routine text] โ”€โ–ถ (fn) choreograph โ”€โ”ฌโ”€โ–ถ ๐Ÿ“ Move plan
                    LLM             โ”‚
                                    โ–ผ
[seed] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ (fn) perform โ”€โ”ฌโ”€โ–ถ ๐ŸŽฌ Routine video
              MuJoCo + 9 ONNX policies โ”œโ”€โ–ถ ๐Ÿ“Š Telemetry
                                       โ””โ”€โ–ถ ๐Ÿ“ Report

This is not a re-implementation

Nothing about the duck is faked or hand-animated. At runtime the app downloads, from the official playground Space `pollen-robotics/microduck-simulator`:

  • โ€”robot_allcollisions.xml and robot_allcollisions_rollers.xml โ€” the MJCFs for the walking robot and the roller-skate variant, plus their meshes
  • โ€”all nine trained ONNX policies โ€” BEST_alpha_walking, BEST_alpha_stand, BEST_alpha_sitstand, ball_kick_left, ball_kick_right, roulade, alpha_ground_pick, BEST_roller, BEST_roller_crouch

and steps them exactly the way the robot's runtime does:

physicsMuJoCo, timestep 0.005 s, decimation 4 โ†’ 50 Hz control
observation61-D: ang_vel(3) + projected_gravity(3) + joint_pos(14) + joint_vel(14) + last_action(14) + command(13)
actionctrl[j] = DEFAULT_POSE[j] + action[j]
walkingforward 0.25 m/s, backward โˆ’0.2 m/s, yaw ยฑ1.0 rad/s
skatingforward 0.6 m/s, backward โˆ’0.5 m/s, yaw ยฑ0.3 rad/s

The 13-slot command is overloaded per mode, and each encoding is the one the policy was trained against:

modecommand
walk / skatecmd[0:3] = vx, vy, wz
sitcmd[0] = posture flag (0 stand, 1 sit)
ground pickcmd[0:2] = cos, sin of a 4.0 s phase clock, exits at 0.7
roller crouchcmd[0:2] = same encoding, 5.0 s period
headcmd[3:7] = neck pitch, head pitch, yaw, roll โ€” EMA-smoothed at ฮฑ=0.2

The one-shot state machines and the fall-recovery machine are ported from that Space's app/src/game/game.js and constants.js, the reference implementation. Sitting hands over gently (0.8 s under the sitstand policy before the sit is commanded, 2.0 s to stand back up) because an abrupt policy switch knocks the duck over โ€” same as the original.

Falls are real. Ask for a barrel roll and the duck genuinely tumbles, the fall detector fires, and the stand policy picks it back up โ€” visible in the uprightness trace.

Moves

On foot, timed โ€” forward, backward, strafe_left, strafe_right, turn_left, turn_right, stand, sit, and the head gestures look_up, look_down, look_left, look_right, tilt_head.

On foot, one-shot โ€” kick_left, kick_right, roll, pick.

On roller skates โ€” skate_forward, skate_backward, skate_turn_left, skate_turn_right, and the one-shot skate_crouch. Switching between the walking body and the skating body rebuilds the model and respawns the duck at the origin, exactly as the official playground does.

Because that respawn is jarring mid-routine, once the skates are on they stay on: a walking move that appears after the first skate move is treated as the planner losing track, and is swapped for its skating equivalent (forward โ†’ skate_forward) or dropped if it has none (sit, kick_*). A walking move before any skating is left alone, so "walk around, then put the skates on" still does exactly that. Any such swap is listed in the report.

Running it

bash
pip install -r apps/09_microduck_lab/requirements.txt
python apps/09_microduck_lab/app.py

First launch downloads ~10 MB of meshes and policies into .microduck_cache/ (override with MICRODUCK_CACHE); the roller body is pulled lazily the first time a routine skates.

The choreographer uses Qwen/Qwen3-4B-Instruct-2507 through HF Inference Providers when a token is available โ€” sign in on the Space, or set HF_TOKEN locally. Without a token it still works: it falls back to a keyword planner, so nothing is behind a sign-in wall.

Voice input

Record or upload a spoken routine instead of typing one and openai/whisper-large-v3-turbo transcribes it; a transcript overrides the text box, so speaking replaces the typed routine rather than competing with it.

Speech is the one part that cannot degrade gracefully โ€” there is no offline fallback for it โ€” so transcribe returns two things: the transcript (empty unless it really got one) and a status line. The status is what reaches the Heard output, which is how a signed-out visitor is told that voice needs an account instead of watching the recording silently do nothing. Keeping those on separate ports matters: a status message on the transcript port would be handed to the choreographer as if it were the routine.

samples/spoken_routine.flac is a synthesised clip used by the test suite so the speech path is actually covered.

Files

filewhat it is
duck.pyasset fetch, MJCF assembly, the 50 Hz policy/physics loop, rendering
routine.pythe move vocabulary, plan parsing, rollout + telemetry
nodes.pythe two bound fn nodes: choreograph, perform
build_workflow.pygenerates workflow.json โ€” edit this, not the JSON
test_pipelines.pyruns every subject through the real WorkflowExecutor

Notes for anyone extending it

  • โ€”The kick policies are blind โ€” there is no ball in the 61-D observation; on the real robot the operator aims the duck first. A scripted routine has no operator, so the ball is placed on the swinging foot's path (0.12 m ahead, 0.06 m to the kicking side, swept empirically). The 0.30โ€“0.35 m the interactive playground spawns at simply misses.
  • โ€”Pin `model.stat.extent`. MuJoCo scales the camera near/far planes by it, and it is derived from the bounding box โ€” the ball parked off-screen pushed it to ~52 m, putting the near plane at 0.52 m. With the chase camera 0.62 m out, the near plane sliced through the duck and clipped the neck away, so the head rendered as if detached.
  • โ€”Both operators are fn nodes on purpose. The canvas rewrites model node ports to the endpoint schema, so anything wanting a richer control surface has to be an fn.
  • โ€”Structured data travels as text, never json or dataframe โ€” the canvas serializes those with String(obj) and the receiver gets "[object Object]".
  • โ€”The video and chart outputs are returned as {"path", "url"} dicts. The REST API reads path; the canvas reads url and needs a data: URI.
  • โ€”workflow.json is autosaved whenever the app runs โ€” a browser is not required, a gradio_client call is enough. Re-run build_workflow.py after any local launch.