摘要

公司官网首页挂了一个”视频复刻”体验位,之前一直是前端占位:游客点”开始创作”只存草稿。这一轮把它接成了真家伙——免费档 Minimax Turbo 480P 直接调用本机 RTX 5090 上的视频工作台出片,实测 44~56 秒一条,公网端到端验收 10/10 全绿。本文完整记录:对着一份编译型 .pyd 后端怎么反推 API 契约、422 报错怎么逐字段试出正确形态、跨域死结怎么用“同源反代 + 内网隧道”解开、以及验收脚本里那个让我白等 9 分钟的低级 bug。所有内网地址、端口、路径均已脱敏。

封面:一条从公网入口穿透到本机显卡的视频生成链路

一、目标形态:游客零登录,点一下就出片

产品诉求很朴素:官网首页的视频复刻面板里,模型档位选单中的 Minimax Turbo 480P 免费开放,游客不用注册登录,选一张模板首帧、点“开始创作”,等一分钟就能在线看片。其余 12 个会员档位维持原登录流不动。

听起来是“前端调个接口”,实际上横着三座山:

  1. 后端是个编译产物。本机视频工作台(ComfyUI 套壳 + 一个 api_models.cp312-win_amd64.pyd)对外只有 HTTP API,没有源码、没有可信文档,参数格式全靠报错反推。
  2. GPU 在我家里,官网在云上。出片引擎跑在本地工作站的 5090 上,公网入口是云上的一台 nginx,中间隔着两层内网。
  3. 不能碰别人的生产配置。云上那台 nginx 上还跑着同事维护的其他服务,改配置必须增量、可回滚、零波及。

二、契约侦察:对着一份闭源 .pyd 把 API 打穿

第一步不是写代码,是把真实契约挖出来。工作台自带的 OpenAPI 描述文件(/api/v1/openapi.json)是主要线索源,配合健康检查与只读接口交叉验证:

import requests
B = "http://127.0.0.1:8600/api/v1"          # 本机回环,端口已虚构

requests.get(B + "/health").json()
# {'status': 'ready', 'comfyui': 'ready', 'gpu': 'RTX 5090 online'}

requests.get(B + "/workflows").json()        # 支持的生成模式
requests.get(B + "/loras").json()            # 已装 LoRA 清单

真正有价值的情报全部来自受控的失败——用最小 payload 去提交任务,读 422 的逐字段报错,一次修一个:

✗ loras:[{name:"turbo", enabled:true}]            → 422 extra_forbidden: enabled
✗ loras:[{name:"turbo", type:"lora", weight:1.0}] → 422 ×3,三个字段全不被认识
✓ loras:[{name:"video_turbo_4step", strength:0.9}] → 200,任务受理

结论值得刻在桌上:loras 元素只认 {name, strength} 两键,多一个键就炸。同类暗坑还有:base_precision 收字符串不收浮点、scheduler 只认枚举值、steps 必须是整数。图片上传也不走寻常路——不是表单字段直塞任务体,而是先传资产再引用:

asset = requests.post(B + "/assets?asset_type=image",
                      files={"file": open("first.jpg", "rb")}).json()
# {'id': 'asset_ab12cd34ef56', 'url': '/api/v1/assets/...'}

提交与回收的完整形态(这条任务实测 56 秒出片):

body = {"type": "image_to_video",
        "inputs": {"image": asset["id"], "prompt": prompt,
                   "duration": 6, "resolution": "832*480"},
        "generation": {"max_new_tokens": 64,
                       "loras": [{"name": "video_turbo_4step",
                                  "strength": 0.9}]}}
job = requests.post(B + "/jobs", json=body).json()
# 轮询 /jobs/{id}/status → completed 后取 /view?filename=...&type=output

图1:侦察→422试错→契约固化→端到端验收的流水线

三、跨域死结:让官网域名和自己的 GPU 同源

浏览器在 https://www.example.com 里直接 fetch 一个内网地址 https://198.51.100.7:8600 是死路(混合内容 + CORS + 内网不可达)。备选方案里,最省事的是给工作台配一个与官网同源的公开路径:

浏览器 ──► https://www.example.com/qs/api/v1/...
             │ 云上 nginx:location /qs/ 纯增量
             ▼
        127.0.0.1:8600(隧道落点,ssh -R 反建的口)
             ▼
        本机 TLS 反代 :7600
             ▼
        127.0.0.1:8600 工作台 API(RTX 5090 出片)

nginx 侧的补丁保持最小面积,逐字复制自同机上已稳定运行三个月的另一条反代(只换路径前缀与 rewrite),连 WebSocket 升级头都原样保留——进度推送要用它:

location /qs/ {
    rewrite ^/qs/(.*)$ /$1 break;
    proxy_pass http://127.0.0.1:8600;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 300s;
}

四、改生产配置的纪律:备份、语法闸、自动回滚

云机上有别人的服务,所以这枚补丁写成了带安全规程的脚本,而不是手敲 sed:

  1. 先 cp -a 出带时间戳的备份;
  2. 用标记块做幂等注入(重复执行不产生第二个 location);
  3. nginx -t 不过就自动还原备份并重新加载——语法闸门永远在 reload 之前;
  4. 验证双管齐下:nginx -T 里 grep 到自己的块 + 公网 curl /qs/api/v1/health 拿到 ready;
  5. 对照测试:同事维护的另一条业务路径行为不变,确认零波及。

还有一颗排掉的雷:仓库里那份历史部署脚本带 rsync --delete,构建母本早已过期,跑一次就会静默删光线上多余文件。正确姿势永远是只上传目标小文件 + 双端 md5 核对,而不是整目录同步。

五、前端引擎:拦截一个按钮,长出一条真链路

官网面板是原生 JS,没有框架。用“接管 CTA 按钮”的方式接入,对存量逻辑零侵入:

const FREE = tierId === "minimax_turbo" && /480p/.test(spec);
document.querySelectorAll(".console-cta").forEach(b =>
  b.addEventListener("click", (e) => {
    if (!FREE || !state.userImage) return;      // 非免费档 → 原登录流
    e.stopImmediatePropagation();               // 抢在旧 handler 前
    runTurbo(state.userImage, prompt, spec);
  }));

要点清单:先传图、再建任务;进度浮标轮询 /jobs/{id}/status,刷新页面后可续查(job id 落 localStorage,下次开机自动恢复);出片 URL 是相对路径,统一加 /qs 前缀改写;任何一步失败 → toast 提示后自动降级回原“存草稿”行为,用户永远有退路。顺手修掉一个 UX 打架:免费档生成时旧 handler 的“草稿已存”toast 和引擎进度 toast 先后弹出——在旧 handler 开头对免费档静默放行即可。

六、验收翻车实录:id 当 class 查,白等九分钟

端到端脚本第一版跑出来永远超时:任务提交成功、后端 status 明明已 completed,前端断言却死活等不到成片浮层。逐段排查后看到这种令人窒息的操作:

// DOM 长这样:
//   <div id="yl-replica-result">…</div>
// 脚本却写成:
document.querySelector(".yl-replica-result")   // 永远 null
// 正确姿势:
document.getElementById("yl-replica-result")

一个选择器笔误,让“等 90 秒”的轮询窗口改成 300 秒也照样挂——因为断言永远不可能成立。修完之后本地 14 项断言全绿。教训:断言失败先证明断言本身可执行,再怀疑链路。

七、三重验收与最终数据

场景 结果 出片耗时
本机直连 API(侦察期) 832×480,576KB 56s
本地同构镜像端到端(Playwright 真实点击) 14/14 PASS 48s
公网官网真实首页端到端 10/10 PASS 44s

真实用户路径全程:选模板卡 → 首帧自动抽图入槽 → 点“开始创作”被引擎接管 → 进度浮标 0→100 → 成片浮层可播可下载 → 刷新后“Done ✓”回看入口 → 点击重开浮层。零 JS 异常,游客全程未登录。

八、可迁移的经验

  1. 对着闭源后端,422 是最诚实的文档——最小 payload + 逐字段试探,比读压缩包里的 bundle 快一个数量级。
  2. 同域反代 > CORS 白名单:给内网引擎开一条公网域名的子路径,前端零配置、无预检、无混合内容问题;反代块照抄已验证的存量配置最安全。
  3. 改生产配置的三板斧:时间戳备份、语法闸(nginx -t 不过不 reload)、自动回滚。再加一条:部署前先 diff 线上基线,证明队友没动过,再动手。
  4. 给同事留接管口:网关地址、模型名、档位参数全部集中在一个 JSON 契约文件里,未来换成带鉴权的收费网关,只改一处配置,前端零改动——这条要写进交接文档。
  5. 免费 + GPU 必须谈限流:每个游客点一次就真占 5090 五十秒,上线当天就要把 IP 级配额想好,别等账单教育。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注