从一个 Chrome 插件到自己的桌面 app:四个 bug 全都跟钱有关
一个 Chrome 插件让我在网页上直接把军装穿到身上,我顺着它挖到背后的实时扩散模型,又用一个下午做了个自己的 macOS 版本,全程花了 $0.56。但真正值钱的不是那个 app,是它真跑起来之后冒出的四个 bug:共同点不是难,而是只有真跑才撞得见,并且全部导致多花钱。在按秒计费的产品里,UI 本身就是成本控制的一部分。
时间:2026-08-07 成本:$0.56(约 56 credits,含全部调试和实测) 产出:virtual-tryon-desktop
起因很随便:我在一个军装 prompt 资料库网站上闲逛,发现浏览器里多了个东西。
结论速查
| 结论 | |
|---|---|
| 插件叫什么 | Anywear,Chrome Web Store 上架,背后是 Decart.AI |
| 技术是什么 | 实时逐帧扩散(Live-Stream Diffusion),不是把衣服 P 上去 |
| 能不能本地跑 | M2 上不行,差约 200 倍算力,差距不是优化能补的 |
| 自己做要多久 | 一个下午,Python + PySide6,官方 SDK 明说不需要浏览器 |
| 花了多少钱 | 全程 $0.56,免费额度是 $10 |
| 最值钱的收获 | 四个 bug,全部只有真跑起来才会撞见,且全部跟计费有关 |
一、先是插件
那天我在看 uniform.wingzero.tw 的军装 prompt
库,页面上每张服装图的左上角都多了一个 ✦ Virtual Try-On
的小标。点下去,右下角弹出一个悬浮窗,里面是我的摄像头画面,只不过我身上那件衬衫已经变成了一套军装礼服,连大盘帽上的帽徽都在。
我转头,它跟着转。我抬手,袖子的褶皱跟着变。
这东西的体验密度很高:你不需要离开正在逛的页面。看到一件衣服,点一下,它就在你身上了。这比"打开某个试衣 app、上传照片、等 30 秒"要顺畅一个数量级。
二、然后是挖背后是谁
体验完的第一反应是:这算力谁出的?不可能在我的 MacBook 上跑。
顺着三条线索对上了:
- Chrome Web Store 的发布者信息:Anywear: Live
Virtual Try-On,发布者 Decart.AI, Inc.,联系邮箱
dev@decart.ai,注册地 Delaware。 - 产品站是
anywear.decart.ai,decart.ai 的子域名,页脚写着 Powered by decart,隐私政策和服务条款全部托管在docs.platform.decart.ai。 - 底层模型在他们自己的 API
文档里公开卖:
lucy-vton,Realtime 分类下。
所以这不是"某个团队做了个插件",而是"一家做实时视频模型的公司,用一个插件给自己的 API 做了个 demo"。插件是橱窗,API 才是货。
技术上它凭什么能实时
这是我最想搞明白的部分。普通的图片试穿模型(CatVTON、IDM-VTON 这些)一张图要跑 20–50 步去噪,根本追不上视频帧率。
Decart 的做法来自他们的 MirageLSD(Live-Stream Diffusion)路线,把问题的形状换掉了:
- 自回归、逐帧生成:每一帧的生成条件是上一帧的输出加上你的 prompt,而不是一次性扩散一整段视频。这就是为什么它是无限流,而不是固定时长的渲染。
- Diffusion forcing:训练时就让模型在没有完整时序上下文的情况下去噪,于是它不需要等未来的帧。
- Shortcut distillation:用小模型去拟合大模型的去噪轨迹,把几十步压缩到约一步。
代价是分辨率和细节。这一点后面会反复出现。
⭐ 关键认知:它不是把真实的那件衣服变形贴到你身上,而是以那件衣服为条件,重新生成你的每一帧。所以版型、垂坠、光影都是推理出来的,不是测量出来的。判断"这个廓形适不适合我"够用,判断"袖子会不会太长"完全没用。
三、三条路的账,我算完选了最贵的那条单价
想自己做,摆在面前三条路。
路线 A:本地跑
我的机器是 Apple M2 / 16GB。开源对标是 StreamDiffusionV2(causal DiT + rolling KV cache,基于 CausVid 和 Self-Forcing),报的数字是 1.3B 模型 61fps、14B 模型 31fps。
但那是数据中心 GPU 的数字。M2 的 GPU 大约 3.6 TFLOPS fp16,H100 约 1000。差距约 200 倍,不是 2 倍。 模型能塞进内存,但会变成"几秒一帧"而不是"每秒几十帧"。这个差距没有任何优化能补。
本地能做的是 CatVTON 那种静态试穿:899M 参数、8GB 显存以内、MPS 能跑,一张图几十秒。功能有了,"实时"没了。
路线 B:租 H100
$1.49–2.99/小时(Vast.ai / RunPod / Lambda)。
这条路有个容易被忽略的杀手:网络往返。帧预算是 40ms,如果 GPU 在美东而我在亚洲,光 RTT 就 150–250ms。模型再快也没用,你看到的是四分之一秒延迟的自己,手眼反馈已经断了。真要租得租东京或新加坡。
路线 C:直接用 API
$0.02/秒,按实际生成时间计费。
换算成小时是 $72/小时,比租 H100 贵将近 30 倍。但盈亏平衡点在"每小时串流 2 分钟":低于这个用量,API 更便宜;高于这个,自建更划算。
个人自用一天用几分钟,API 完胜。而且它一次性消掉了 GPU、区域、冷启动、WebRTC 服务端四件事。
选 C。
顺带看了条款
- 商用允许,输入输出所有权归自己,不要求署名。
- 不能转售 API:禁止对外提供模型端点让第三方接入。做产品可以,做中转不行。
- 有一条得看清楚:Decart 对你的输入和输出取得永久、不可撤销的许可用于模型训练。对一个摄像头对着人脸的应用来说,这不是可以一带而过的条款。自用无所谓,做产品就必须在隐私政策里写明白。
四、做成桌面端,而不是浏览器插件
我想要一个本机 app,不套浏览器壳。
一开始以为要绕路,结果查下来是官方支持的:Decart 有 Python
realtime
SDK,文档明说不需要浏览器(pip install decart[realtime],底层是
LiveKit 的 Python 客户端)。另外还有 Swift SDK,支持 macOS 12+。
于是技术栈定了:
| 选择 | |
|---|---|
| GUI | PySide6 |
| 实时传输 | decart[realtime] → LiveKit |
| 摄像头 | OpenCV VideoCapture |
架构上唯一有难度的地方:SDK 是 asyncio,Qt
不是。解法是让会话独占一个线程跑私有事件循环,进来的数据走 Qt
signals,出去的调用走 call_soon_threadsafe /
run_coroutine_threadsafe。
界面就三块:左边传服装图和写描述,右边看实时画面,底下一条状态栏放计时器和累计花费。
一个上来就纠正的错误认知
我最初按网上 MirageLSD 的介绍写文档,说模型是 768×432 / 20fps。实际查 SDK 是 1088×624 / 30fps。旧文章的数字过期了。
这件事的教训很直接:能 introspect 就别读二手资料。我后来所有的参数都是直接从安装好的包里问出来的,不是从博客里抄的。
五、四个 bug,全部跟钱有关
这是整件事里最值得写的部分。
我在写代码的时候,验证了模块导入、配置加载、帧格式转换、以及跟 Decart 的完整连接收流。全过。但 GUI 本身、摄像头、试穿效果,一次都没真跑过——那需要摄像头权限和显示器。
真跑起来之后,四个 bug 挨个冒出来。它们的共同点不是"难",而是"只有真跑才会撞见",以及"全部导致多花钱"。
Bug 1:Connect 在没有任何输入时是可点的
启动 app,没选服装、描述框是空的,Clear 和
Apply outfit 都正确置灰了,只有 Connect
亮着。点下去就开一个计费会话,但没有任何东西可以应用。纯烧钱。
原因很蠢:刷新按钮状态的函数只在用户操作时触发,__init__
里从来没调用过,所以启动时按钮停留在 Qt 的默认 enabled 状态。
一行修复,但只有把 app 打开盯着看才会发现。
Bug 2:镜像做在输出端,把水印翻转了
试穿画面右上角有 Decart 烧录的 AI Generated
水印。我第一版跑出来,它显示成 detareneG IA。
因为我把「镜像」做在了收到画面之后,整帧水平翻转,连水印一起翻了。
这不只是难看。Decart 的 AUP 原文禁止 "remove, obscure, disable or circumvent any watermark, label, provenance information",一个反着的水印很难说不算 obscure。
正确做法是在输入端镜像:翻转送出去的摄像头帧,回传画面本来就是镜像的,而水印是生成之后才打上去的,方向自然是对的。Decart
的 JS SDK 有个 mirror: "auto"
参数,做的就是这件事。我绕了一圈才想明白。
Bug 3:Retina 上先缩小再放大,丢掉一半细节
跑起来第一感觉是画面有点糊。我一开始怀疑是模型分辨率不够、或者摄像头输入被劣化了。
查下来两条都不是:摄像头稳定给 1280×720,我的裁剪缩放用的是
INTER_AREA,高质量降采样,没有损失。
真正的原因在显示端。我写的是:
self._pixmap.scaled(self.size(), ...)self.size() 是逻辑点,不是物理像素。在
2x 的 Retina 屏上实际发生的是:
- 1088px 的原图被缩小到约 670 点
- Qt 再把这 670 点渲染到 1340 个物理像素上,放大 2 倍
等于一张 670px 的图被拉到 1340px。先扔掉一半信息,再插值补回来——补不回来的。
改成按物理像素缩放之后只重采样一次,画面尺寸不变,清晰度肉眼可见地提升。修复后的截图里,超人胸口那个 S 能看到布料的编织纹理。
Bug 4:一张大图搞死控制通道,而 app 浑然不觉还在计费
传了一张超人剧照,点 Apply,弹出:
Could not apply outfit: Control channel disconnected
翻 SDK 源码,这句话来自 WebSocket 接收循环退出时的
finally 块。注意它跟另一条
Control channel not connected(第 429
行)不是一回事:我遇到的不是"还没连上",而是通道中途死了。
再往下看:set_image 把整个图片文件 base64
之后直接推进这个 WebSocket,SDK
里既没缩放也没大小上限,ws_connect(ws_url) 也没设
max_msg_size。一张桌面壁纸尺寸的图,base64 之后再涨
33%,很可能就是这么把通道撑死的。
加了一层预处理,长边压到 1024、转 JPEG。实测一张 4032×3024 的图:
4032x3024 35787KB → 1024x768 353KB
但比这个 bug 更糟的是它的两个后果:
后果一:会话死了还在计费。
控制通道断了之后,视频流还在回传,所以界面显示
Live、画面照动,实际上任何指令都执行不了,而钱一直在扣。光看画面无法判断会话是否健康。
修复是每 250ms 查一次 get_connection_state(),遇到
disconnected / reconnecting 直接停。
后果二:报错用的是模态弹窗。 弹窗等你点 OK 的整段时间,$0.02/秒照收。你去泡杯咖啡回来就是另一个数字了。会话类错误现在只写状态栏和 stderr,不拦路;只有摄像头打不开这种发生在计费之前的致命错误才保留弹窗。
六、计费的真相:我的估算高了整整一倍
整个过程我在报告花费时一直按"连接开始到断开"的墙钟时间乘 $0.02。
三次测试下来我报了 $0.61。翻 dashboard,实际是 32 credits = $0.32。
| 会话 | 墙钟 | 按墙钟推算 | 实际扣费 |
|---|---|---|---|
| 1 + 2 | 21 秒 | $0.42 | 22 credits($0.22) |
| 3 | 9 秒 | $0.19 | 10 credits($0.10) |
| 合计 | $0.61 | 32 credits($0.32) |
差在两处:连接握手的 2.7–3.8 秒不计费,以及回传帧率约 17fps 而标称 30fps,所以一秒墙钟对应的生成视频不足一秒。
我把这个偏差保留在了 app 的费用表里,并在 README 写明——宁可高估,让消费上限提前触发而不是延后触发。费用表是天花板,不是账单。
七、几点心得
1. 二手资料会过期,能问代码就问代码。 模型分辨率、SetInput 的字段、连接参数的默认值、set_image 到底怎么发的,全部是 introspect 安装好的包和读源码得到的,没有一条是从文章里抄的。那篇写 768×432 的介绍并没有错,只是过期了。
2. "验证过"要说清楚验证到哪一层。 我在交付时说过"跟 API 的完整连接和收流已验证",同时明确说"GUI 本身、摄像头、试穿效果没验证过"。后面四个 bug 全部落在没验证的那一层。这个边界如果含糊,就会变成"我以为它能用"。
3. 按秒计费的产品,UI 本身就是成本控制的一部分。 四个 bug 里有三个的真正危害是多花钱,不是功能不对。一个可点的按钮、一个模态弹窗、一个显示 Live 的死会话,在 $0.02/秒的语境下都是漏水的口子。写这类应用,得把"哪些界面状态会导致计费继续"当成一条独立的检查项。
4. 实时不总是更好。 1088×624、17fps、带水印的实时流,判断"这个廓形适不适合我"够用;但如果只是想看清一件衣服的细节,一张高清静态图更有用,而且便宜约两个数量级(Lucy Image 2 是 $0.01–0.02 一张)。实时的价值在于"动起来看版型",不在于清晰度。
附:如果你也想试
只想玩:装 Anywear 插件,免费,两分钟。
想自己做:注册 platform.decart.ai 送 1000
credits($10,约合 8 分钟生成时间,按上面那个 2
倍关系,够十几次完整试穿)。我的实现在 ribincao/virtual-tryon-desktop,MIT,make run
会自己建 venv,从零开始只需要填一个 API key。
README 里记了全部踩坑:摄像头 TCC 权限归属、RGB24 与 BGR、LiveKit 缓冲区复用、以及上面那四个 bug 的完整前因后果。