☰
microduck Policy Playground:为鸭子机器人打造的可视化技能策略试玩空间
2026/9/25 3:02:42 网站建设 项目流程
  • 机器人
  • 嵌入式
  • 强化学习
  • 人工智能
  • 智能硬件
  • 计算机视觉
  • 音视频

【免费下载链接】microduck

A Tiny biped duck robot 🦆

项目地址:https://gitcode.com/gh_mirrors/mi/microduck
点击查看免费下载

导读

microduck 是一个微型双足鸭子机器人项目,而spaces/policy-playground/是该项目为它提供的 Hugging Face Space:一个纯浏览器端(Vite + TypeScript)的"技能试玩场"——登录后唤醒你的鸭子,按下任意一张技能卡片,该技能策略便会从 Hugging Face Hub 下载到机器人上运行,卡片实时告诉你它正在执行哪一步。本文将围绕该 Space 的架构与实现展开:本地开发与构建流程、为什么选择浏览器而非 Python 容器、四个 RPC 调用(policy.fetch、robot.setSkill、robot.policies、robot.do)如何贯通"获取 → 安装 → 等待 → 执行"全链路,以及rendezvous.ts、hub.ts、auth.ts三个核心模块的源码级原理。读完你既能照抄本地开发命令,也能理解这套"控制通道复用 rendezvous、Hub 目录即机器人目录"的设计取舍。

这个 Space 是什么

spaces/policy-playground/README.md 的 YAML frontmatter 定义了它的身份:sdk: docker、app_port: 7860、hf_oauth: true、OAuth 令牌有效期 1440 分钟(一天),一句话简介是 "Pick a trick, watch your duck do it."

整个交互体验可以概括为一段话:登录 → 唤醒你的鸭子 → 按下一张技能卡片 → 技能从 Hub 下载到鸭子身上并运行 → 卡片告诉你鸭子正在做的是哪一个。

README 特别强调了两点工程约束:

  • 不要直接编辑 Space 本体。它的源码就在本仓库的spaces/policy-playground/中,由 scripts/publish-space.sh 负责发布。Space 消费的机器人状态(rendezvous 协议、机器人自身的 RPC 方法名)都随仓库演进,副本放在 Space 仓库里会与上游漂移。
  • 发布顶层文件而非目录树。publish-space.sh只发布spaces/<name>/目录的顶层文件与符号链接(find -maxdepth 1 \( -type f -o -type l \)),所以前端必须用vite-plugin-singlefile把整个应用打包进单个自包含的index.html并提交到仓库。发布后的 Space 永远是四个文件:页面、Dockerfile、entrypoint.sh和 README 卡片——Hugging Face 侧没有任何构建步骤,也就不会出现"别人看不到的失败"。

本地开发与构建

启动开发服务器

cd spaces/policy-playground/web && npm install
npm run dev

开发服务器直接面向真实的 Hub 与真实的 rendezvous提供服务,页面并非打桩。登录需要一个 redirect URI 与开发地址完全一致的 OAuth 应用,通过查询参数传入:

open "http://localhost:5173/?client_id=<an app registered for localhost:5173>"

本地还有一种免 OAuth 注册的捷径,见下文auth.ts中的?token=分支。

构建单文件产物

npm run build

web/package.json 中该脚本是tsc --noEmit && vite build && cp dist/index.html ../index.html——类型检查、打包、然后把产物复制到上一级目录(Dockerfile 所在处),这个文件被提交进仓库。

web/vite.config.ts 解释了为什么必须单文件:publish-space.sh发布的是顶层文件而非目录树,而 bundler 默认会产出assets/子目录;viteSingleFile()把所有内容内联进一个index.html,代价只是一页约 60 KB 的 HTML 体积变大、失去缓存粒度,"对一个页面来说不是成本"。

Docker 运行环境

Dockerfile 基于python:3.13-slim,整个服务就是python -m http.server 7860 --directory /srv——一个文件、只读 GET、无上传路径、无配置。镜像只做一件事:拷贝已构建的index.html与 entrypoint.sh 并运行后者。

为什么是浏览器而不是 Python 容器

README 记录了一段重要的历史:这个页面的Gradio 版本至今仍在线可用,而它遇到的每一个难题都源于"客户端跑在数据中心里"这个前提:

Gradio 时代的难题根因浏览器版本如何解决
rendezvous 拒绝它requests库把所有调用签名为python-requests/2.x,Hugging Face 的边缘层把它识别为 bot:来自 Space 容器的第一个GET /api/robot-status就返回429+ HTML 页面(server=awselb/2.0),服务端根本没收到请求浏览器请求携带访问者自己的 IP 与浏览器签名
所有访问者共享一台机器人会话存在模块级全局对象里——模块全局量按进程计,而一个 Space 只有一个进程每位访问者就是自己的浏览器,按访客分配会话零成本
aiortc、av、DTLS 补丁、PyGObject为给 Python 凑一套 WebRTC 栈浏览器天生自带 WebRTC
服务端渲染页面前架了 Node 代理,启动十秒后无 traceback 停摆完全去掉

这些在浏览器版本里一个都不存在;同样形态的microduck-console从未经历过它们。详细证据可对照 spaces/shared/wire.py 中的实现与 spaces/shared/rendezvous.py 中对python-requests签名被限流一事的完整注释。

为谁而做:整页约束而非表面装饰

README 明说目标用户是**"一个十岁的、拥有一条鸭子的小孩"**——这是对整个页面的约束,不是一层皮:页面上没有任何地方出现方法名、传输层、socket 或 schema 的字眼。一个技能只有三样东西:一个名字、一句它做什么的话、一个按钮。

而真正的技术细节——四个调用是什么(policy.fetch、robot.setSkill、robot.policies、robot.do)、走哪条通道、为什么被拒绝——全部收纳在页面底部的"What just happened?"(发生了什么?)折叠面板里。那是出问题时去的地方,而不是想看鸭子鞠躬时去的地方。

这个设计在 web/src/main.ts 中有多处呼应:

  • 错误要说人话。inWords()会把 daemon 日志里的network error: Teethyfish/microduck-… will not run on this robot: it is for a microduck full_shell, and this is a microduck翻译成xxx is made for a full_shell, and yours is a microduck.——什么都不虚构、什么都不吞掉,完整原文始终保留在日志里。
  • 回答必须出现在提问处。Gradio 版把所有答案打在页面顶部一行,离按钮两千像素远,于是"拒绝"读起来就是"按钮没反应"。重建时改成答案贴在按下那张卡片上。
  • robot.relax刻意缺席。它切断力矩、鸭子当场摔倒,robotctl需要--yes守护,BLE 干脆拒绝承载;在给孩子看的页面上,那会是一个写着"休息一下"、实际是"摔一跤"的按钮。

三个核心模块的源码级拆解

rendezvous.ts:通往鸭子的控制通道

web/src/rendezvous.ts 是 spaces/shared/wire.py 的浏览器移植:GET /events开流、其余全部走POST /send,携带rpc键的peer信封就是一次控制调用。原理是 rendezvous 的handle_peer_message会把peer信封里除type与sessionId外的每个键原样转发给会话对端,于是rpc就成了一条控制通道,由mediad::relay的控制通道从 datachannel 所用的同一张路由表应答。

协议里四条"不读懂就吃亏"的规则(Python 版踩坑换来的,浏览器版依然成立):

  1. 先POST /send后GET /events是 400。身份来自 bearer token,而事件流才是绑定它的东西——所以必须先开流,一切等welcome。
  2. startSession与list的应答在 POST 响应体里,其余都从流上回来。这是读者最容易错一次的形状。
  3. CRLF 不是我们可以假设的。SSE 允许\r\n,代理也可能改写它;只按\n\n切分就会什么都匹配不到、所有消息静默消失。因此openStream()在解码时统一做replaceAll("\r\n", "\n")归一化。
  4. 每台机器人只有一个消费者。sessionRejected意味着别人正握着它,而且机器人的官方 console 也算一个。

浏览器还带来一个改善:EventSource无法发送Authorization头,所以这里用fetch+ reader 读流——这不是 workaround,而是让 token 留在 header(而不是服务端只作为弃用回退保留的查询串)成为可能。代码还精确地处理了 401(重新登录)、429(限流,1200 请求/分钟)、400(peer 不存在,事件流已断,需重连)等错误并给出可操作的中文文案。

列表接口值得一提:rendezvous.py 与listDucks()都走GET /api/robot-status——它只做一次请求、不开事件流,所以会话进行中刷新列表是安全的;而 console 的list会在同一 token 上再开一条流,把会话所骑乘的 peer 挤下线。

hub.ts:机器人怎么读 Hub,页面就怎么读

web/src/hub.ts 精确复刻了updater/src/policy.rs的两个请求——?search=microduck搜仓库,命中后逐个拉manifest.json——因此画廊与机器人的policy.search永远不会对"世界上存在什么"产生分歧。默认端点https://huggingface.co/api/models,官方集合仓库是pollen-robotics/microduck-policies。

几个关键设计:

  • manifest 中repo级别以下的一切字段都是发布者的声明——展示、绝不作为安装依据;真正安装的内容来自机器人自己解析下载到的 manifest。页面上标着made by Pollen(官方)与for a xxx(面向其他机型)的 chip 就是这种"声明即展示"。
  • notATrick()二次实现:daemon 不做这道检查、robot.setSkill会照单全收——一个命令生成型策略(phase编码的地面拾取、posture_flag的坐下/站起)被喂进常量,机器人会"看似合理实则错误地动",比拒绝更糟。规则原本只在robotctl侧,而robotctl不在点击链路里,所以在这里再写一遍,并翻成小孩能行动的话:"这是你的鸭子自己驱动的东西,是它移动方式的一部分,不是技能。"
  • isATrick()判断"有没有结尾":roulade一秒结束;alpha_walking会一直走到有人叫停,且声明了无法被叫停(无command.idle、无unwind_s)。无结尾的"步态与姿态"不放进可按压网格,而是收进 "Part of how your duck moves" 折叠区——列出来、解释清楚、不提供按钮。
  • 预览视频按约定而非按猜测选取:优先media/preview.mp4、preview.mp4,再按路径深度/长度排序兜底;只有完全无视频时才退而用.gif。因为某个仓库里躺着一个被拒的训练产物experiments/…/rejected_phrase_609_…_front_split_v2.mp4,取第一个.mp4就会把它放上卡片。full=true参数让文件列表搭搜索请求的便车,预览视频零额外请求。

auth.ts:无密钥的 PKCE 登录

web/src/auth.ts 实现 PKCE(浏览器证明它发起了流程,因此应用不需要 client secret,页面可以公开给任何人读)。OAUTH_CLIENT_SECRET存在于 Space 环境变量中但永不进入页面。

三个值得注意的细节:

  • client id 由entrypoint.sh注入而非平台注入。sdk: static的 Space 会往<head>里注入window.huggingface.variables,但microduck-console经历了重建、隐私开关、重建……整整一天什么都没注入——console 的 Dockerfile 记录了这个下午。所以这里采用telepresenceSpace 的同款出路:hf_oauth: true把值放进环境变量,entrypoint.sh 用 Python(而非sed,因为被替换的值现在是 JSON,sed会把 provider URL 中的字符当语法)把唯一一个、必须恰好匹配一次的<head>行锚定替换,把window.huggingface.variables原样写进页面。这样auth.ts就是普通的@huggingface/hub代码,没有任何 Space 形状的东西。
  • 过期时间被记住过、但从没被读过。早期版本把整个 OAuth 结果(含 expiry)存进localStorage,此后把 token 永远交给 rendezvous——一天后页面仍显示"已登录",唯一信号是 rendezvous 拒绝报出鸭子名字。stillValid()补上了这道检查:过期的 token 视同不存在,页面重新要一个。
  • ?token=是本地运行专用捷径。hf auth login已存的 token(cat ~/.cache/huggingface/token)可以直接贴进地址栏,省去注册 redirect URI 是 dev server 的 OAuth 应用;但onThisMachine()严格限定localhost/127.0.0.1/[::1],因为查询串里的 token 会进浏览器历史、referrer 和中间代理——在正式 Space 上这条分支根本不可达。

按下一张卡片会发生什么:四阶段流程

README 给出了核心流程,并强调"等待你的鸭子"不是凑数:

getting it → putting it on your duck → waiting for your duck → doing it

(获取它 → 装到你的鸭子身上 → 等待你的鸭子 → 执行它)

web/src/main.ts 把四个阶段逐一落在卡片上:

  1. Getting it:policy.fetch(参数{repo, file},超时 180 秒——这是经鸭子自己 WiFi 的下载,不是状态询问),从 Hub 把策略取到机器人。
  2. Putting it on your duck:robot.setSkill——参数由skillFor()依据policy.fetch的应答(即机器人自己读到的 manifest 字节)构造:name(空格转连字符)、path、duration(无自有时长则用HOLD_SECONDS = 3的按住时长)、可选的chain/unwind/unwind_s/action_scale,与robotctl policy add逐字段对应。注意accepted 不等于已安装:setSkill触发重载,重载失败会体现在随后robot.policies应答的change_error里,只有这里能看见。
  3. Waiting for your duck:把技能装上鸭子会令它重载,重载中的鸭子会回到站立姿态,在回到姿态前拒绝执行任何操作——所以没有这一步,安装的那次按压就是被拒绝的那次按压。waitForHome()以robot.policies应答中的homed(API_VERSION30)为等待标志,500ms 轮询、15 秒超时;发布不了该字段的老鸭子什么都不发,那就没有可等的东西,直接继续。
  4. Doing it:robot.do({skill: name})。同样先waitForHome()再执行。

按压进行时页面其余部分全部惰性——鸭子一次只做一件事,接受第二次按压的页面就是在承诺它守不住的东西。state.busy在整段流程中非空,所有按钮随之disabled。

流程之外还有三个"鸭子需要、但不是技能"的操作:robot.init站立(永不被拒)、robot.enable启动(页面只在鸭子确实关闭时才给出 Start 按钮——一个平时是空操作的按钮没人会信)、以及刻意缺席的robot.relax。站立按钮背后还藏着两个调用:鸭子坐着时由sit_toggle闩锁保持,init会与闩锁打架,所以先robot.do {skill: sit_toggle}再站。

发布与部署

发布是手工脚本 scripts/publish-space.sh:

scripts/publish-space.sh policy-playground # 推到 pollen-robotics/microduck-policy-playground scripts/publish-space.sh policy-playground --dry-run # 只打印将发布的文件清单

要点:从spaces/<name>/顶层**复制(而非同步)**文件(cp -L跟随并展平符号链接,让多个 Space 共享spaces/shared/下的协议模块而不复制漂移);git add -A后先暂存再比对,避免"只有新文件"时误报"已经是最新";提交信息带仓库短 SHA。发布后 Space 重建只需一两分钟。

小结

policy-playground是一个以"十岁小孩也能操作"为硬约束、但内部全是严肃工程的浏览器应用:用 rendezvous 的peer/rpc信封做免 WebRTC 的控制通道,用与updater完全一致的 Hub 读取逻辑保证画廊与机器人所见一致,用 PKCE + entrypoint 注入绕开静态 Space 注入失效的坑,再用四阶段按压流程把"下载、安装、回位、执行"的每次拒绝都变成小孩看得懂的一句话。想继续深入,推荐按此顺序阅读:README.md → web/src/main.ts → web/src/rendezvous.ts → web/src/hub.ts → web/src/auth.ts,对照 Python 原型 spaces/shared/wire.py 与 spaces/shared/rendezvous.py,可以完整还原这套架构从 Gradio 到纯浏览器的演进理由。

  • 机器人
  • 嵌入式
  • 强化学习
  • 人工智能
  • 智能硬件
  • 计算机视觉
  • 音视频

【免费下载链接】microduck

A Tiny biped duck robot 🦆

项目地址:https://gitcode.com/gh_mirrors/mi/microduck
点击查看免费下载

相关推荐

上一篇:revanced-patches备份恢复:确保配置数据不丢失
下一篇:让训练告别"黑盒":mmsegmentation 语义分割可视化全流程实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询