AI 演示随处可见。一个简单的 Python 脚本就能完成静态照片的人脸替换,效果堪称神奇。但要构建一个让真实用户能够上传文件、离开后再回来查看的产品?那是完全不同的工作。我最近开发了一个 Web 工具,只需输入一个动画 GIF 和一张参考人脸,它就能返回一个每一帧都完成了人脸替换的相同动画。模型承担了主要的视觉计算工作,但真正的工程投入在于其周围的架构:保持连接活跃、应对页面刷新,并确保长达两分钟的推理任务不会消失在 504 Gateway Timeout(网关超时)中。
这就是原型与产品之间的差距。工程师们喜欢谈论扩散架构和推理参数。但当用户上传一个包含两百帧的高密度 GIF 时,如果浏览器标签页在三十秒后就崩溃了,没人会在意你的模型有多强。推理周边的基础设施与推理本身同样重要。
等待带来的问题
标准的 Web 架构假设响应是快速的。用户点击按钮,服务器响应,页面更新。在数十帧 GIF 中进行人脸替换会立即打破这一假设。模型运行在 Replicate 的 GPU 上,而不是我的服务器上,处理一个长 GIF 很容易需要一分钟或更长时间。如果你试图保持 HTTP 请求开启这么久,就是在自找麻烦。负载均衡器会切断空闲连接。浏览器会认为网络失败并尝试重试。用户盯着冻结的加载图标,会认为应用坏了。
我通过将提交与结果解耦,完全避免了这个问题。当用户上传 GIF 和人脸图像时,我的 Next.js 后端会在 Replicate 上启动预测,并立即返回一个任务 ID。用户会立即得到确认。一旦 GPU 计算完成,实际结果会通过 Webhook 随后送达。这种模式并不新鲜,但对于耗时较长的媒体任务来说,它是绝对必要的。它将不可预测的等待变成了一次可靠的握手:任务已被接受,完成后会通知你。
构建一个值得信赖的状态机
一旦采用异步模式,你就需要可见性。用户会刷新页面。他们会关闭标签页后再重新打开。他们会复制链接发给同事,而同事可能在两小时后才查看。如果没有对发生过程的持久化记录,局面就会陷入混乱。
我使用 Supabase 作为单一事实来源(single source of truth)。每次上传都会创建一个带有唯一任务 ID 的行,该行会在特定状态之间流转:排队中(queued)、处理中(processing)、成功(succeeded)、失败(failed)或已过期(expired)。当用户首次点击提交时,该行处于“排队中”状态。一旦 Replicate 接受了预测,它就会转为“处理中”。Webhook 会将其推送到“成功”或“失败”。我增加了“已过期”状态,用于处理那些长时间没有回调的任务,这样系统就不会无休止地去追踪那些“幽灵”任务。
使用 TypeScript 构建的前端会以短间隔轮询 Supabase,并根据当前状态进行渲染。这种轮询看起来很原始,但它彻底解决了刷新问题。用户可以关上笔记本电脑,明天再打开,并准确看到进度,因为进度是由数据库(而不是浏览器内存)掌控的。Supabase 还负责追踪积分(credits),因此账务与追踪状态的同一个任务记录紧密关联。一切都集中在一个地方。
处理文件而不压垮浏览器
GIF 比人们想象的要重。一个非常适合 AI 流水线的文件,体积可能仍有几兆字节。在浏览器内将其渲染为循环预览会破坏性能,尤其是在低端设备上。我需要两套完全独立的流水线:一套用于 AI,一套用于用户界面。
对于预览,我使用编译为 WebAssembly 的 FFmpeg 将大型 GIF 转换为动画 WebP。这完全在浏览器的客户端运行。结果是一个轻量级的预览,在不触动原始文件的情况下保持界面的流畅。发送到 Replicate 的 GIF 保持原样。这种分离非常重要。你不希望预览产生的压缩伪影(compression artifacts)混入你的训练数据或最终渲染结果中,也不希望在 AI 思考时,用户界面因为处理一个数兆字节的大文件而卡顿。
在文件离开浏览器进行上传之前,FFmpeg WASM 还可以处理其他 GIF 任务。我用它来解析帧数、验证尺寸并及早发现损坏的文件。在消耗 GPU 积分之前发现问题,既能省钱,也能节省用户的耐心。
将 Demo 转化为用户信赖的软件
这其中蕴含着一个更深层的启示,几乎适用于市场上所有的生成式 AI 工具。模型本身可能只占工作量的 30%。剩下的 70% 是那些没人会在推特上谈论的、并不起眼的基础设施:状态恢复、webhook 签名、文件转换、额度追踪以及优雅的失败处理。
我的技术栈简单且经过深思熟虑。Next.js 和 TypeScript 处理界面和 API 路由。Replicate 运行模型。Supabase 管理状态、数据和额度。FFmpeg WASM 处理客户端媒体工作。Animated WebP 保持 UI 的快速响应。每个组件各司其职,它们通过明确的状态转换进行连接,而不是依赖脆弱的长连接请求。
当用户消耗额度进行换脸时,他们期待的是可靠性,而不是一份研究论文。如果预测失败,系统应该能够感知并告知用户。如果用户需要等待,他们应该能看到一个轻量级的预览,并且其进度状态在浏览器重启后依然能够保留。这些细节在正常工作时是隐形的,但一旦失效,则是致命的。
换脸本身只是一个巧妙的技巧。但这个工具之所以让人感觉像个真正的产品,是因为用户可以上传、离开、然后再回来,而不会丢失进度。这才是将一次 API 调用转化为用户真正信赖的软件的关键。
