architecture.md 14.3 KB

系统架构文档

覆盖现状(v1,已上线)与规划中的「换装跳舞直播间」(v2,见 docs/todo/dressup-dance-room.md)。需求范围见对应文档,本文档只谈 组件划分、数据流、集成方式和存储演进。

1. 现状架构(v1)

1.1 组件

flowchart LR
  subgraph Browser
    live[live.html<br/>直播间/浏览]
    admin_ui[admin.html<br/>素材后台]
  end

  subgraph Node["server.js (单文件 Node http server, PORT 8934)"]
    api[路由处理<br/>/api/*, /lib/video/*]
    sched[schedule + promptQueue<br/>2s tick]
    ingest[ingest.js<br/>syncLibrary 每5分钟]
  end

  libjson[(library.json)]
  statejson[(live-state.json)]
  h3[(H3 Console<br/>内部 MiniMax H3 集群<br/>/v1/videos)]
  fal[(fal.ai queue API<br/>/fal/* 反代)]

  live -- 轮询 /api/live/state, /api/chat --> api
  live -- POST /api/chat, /api/prompt, /api/queue/vote --> api
  admin_ui -- CRUD /api/admin/library --> api
  api -- 读写 --> libjson
  sched -- 落盘 --> statejson
  api --> sched
  sched -- POST /v1/videos, 轮询状态 --> h3
  api -- GET /v1/videos/:id/file 代理 --> h3
  ingest --> h3
  ingest -- 重建 --> libjson

  legacy[index.html/app.js/newtask.html<br/>独立的 fal.ai 任务提交小工具<br/>与直播间无数据关联] -.-> fal

1.2 关键设计点

  • 单进程、无框架、无数据库:server.js 用 Node 原生 http 模块手写 路由,持久化就是两个 JSON 文件(library.json 素材库、live-state.json 直播状态),全量读写、无锁、无并发写保护——量级小的场景下够用,见 1.4 存储演进建议里什么时候该换。
  • 服务端权威时钟:直播"当前播放的是哪条"由服务端 schedule 数组 决定,浏览器只是被动轮询同步,杜绝"每个观众各自播各自的"——这是 "直播感"的核心,v2 任何改造都不能破坏这个前提。
  • H3 Console 是外部服务,本项目只是它的一个消费方:内部 MiniMax H3 视频生成集群,独立部署、独立鉴权(H3_CONSOLE_TOKEN),契约见 H3-CONSOLE.md。server.js 里所有跟"生成视频"相关的逻辑,都是在编排 这个外部 API(提交任务 → 轮询 → 落库),本身不做任何 GPU 推理。
  • /fal/* 反代通道:转发到 queue.fal.run(fal.ai 官方队列 API), 目前只被仓库里独立的 index.html/app.js/newtask.html/taskdetail.html 这套"手动提交 H3/H3-Max 生成任务"小工具使用,跟直播间(live.html) 没有数据关联。v2 的付费实时生成路径会正式启用这条通道,理由见 2.3。
  • 无鉴权边界:管理后台 (admin.html) 和所有写接口都没有应用层鉴权, 依赖网络可达性(内网/反代层面控制)。v2 引入用户账号体系后,管理后台 这块的"零鉴权"现状需要重新评估(至少后台管理接口应该要求管理员身份, 这不是本次需求主线但是个不能一直拖的技术债)。

1.3 直播时刻表状态机(供 v2 复用/扩展参考)

sequenceDiagram
  participant V as 观众浏览器
  participant S as server.js
  participant H3 as H3 Console

  V->>S: POST /api/chat {text}
  S->>S: moderateText() 关键词审核
  alt 命中 library.json
    S-->>V: {status: matched}
  else 未命中
    S->>H3: POST /v1/videos {prompt}
    S-->>V: {status: generating}
    loop 轮询直到 done/error
      S->>H3: GET /v1/videos/:id
    end
    S->>S: 写入 library.json, 状态置 ready
  end
  Note over S: ensureSchedule() 每 2s tick<br/>按投票数从 ready/matched 里选片<br/>不够则从素材库随机填充
  loop 每 4s
    V->>S: GET /api/live/state
    S-->>V: {current, upNext, queue}
  end
  Note over V: 前端用 endAt 精确定时切片<br/>轮询只是纠偏/兜底

1.4 存储演进建议

JSON 文件方案在 v1(单一素材类型、无账号、无并发写压力)是合适的 "够用就好"选择。v2 一旦引入下列任一项,建议评估迁移到嵌入式数据库 (如 SQLite,单文件、无需额外部署,跟现有"零运维依赖"的风格最接近):

  • 用户账号 + 密码/凭证存储(JSON 明文文件不适合放凭证,即便是 hash 后 的也建议至少上 SQLite)。
  • 积分流水 + 人物"剩余可播 10 秒片段数"这类计数器(需要"扣积分 + 创建 生成任务 + 扣减片段配额"这几步操作的原子性,全量重写 JSON 文件在 并发下有丢更新风险)。
  • 资产种类变多(人物/衣服/参考视频/合成图/生成视频五张表,互相有外键 引用关系,继续用一个大 JSON 数组的心智负担会明显上升)。

不建议现在就迁移——v2 第一期(docs/todo 第 9 节分期建议)如果只是先 用内部 H3 Console 跑冷启动种子内容、不涉及账号和并发写,继续加 JSON 文件(比如新增 characters.json/garments.json/dance-refs.json, 沿用 library.json 的读写模式)完全够用;到第三/四期引入账号+积分时 再迁移,避免过早优化。

2. 规划架构(v2:换装跳舞直播间)

2.1 两个独立的直播间页面

live.html(文字点播)与 dance.html(换装点播)是两条并行的产品 线,不是同一个页面的两个 tab:

  • 各自维护独立的 schedule/promptQueue(各自的播出时刻表和排队), 互不影响对方的播放进度。
  • 共用同一套底层基础设施:素材库读写模式(library.json 风格)、 H3 Console/fal.ai 生成编排、观众身份(cookie/账号)、投票机制、 在线人数统计。
  • dance.html 复用 live.html 的视觉语言(同一套 CSS 风格、播放器、 排队列表 UI),保留在线人数/点赞/排队展示,唯一的差别是左侧点播 面板:live.html 是文字输入框,dance.html 是"衣柜选择 / 传衣服 / 传人"操作面板,没有文字输入。

2.2 新增组件

flowchart TB
  subgraph Browser
    dance[dance.html<br/>衣柜/传衣服/传人,无文字输入]
    admin2[admin.html<br/>+ 人物/衣服/参考视频/用户 管理页]
  end

  subgraph Node["server.js(扩展)"]
    api2[路由扩展<br/>/api/dressup/*, /api/admin/characters, garments, dance-refs, users]
    pipeline[生成流水线编排<br/>一步法:人物图+衣服图+参考视频→跳舞视频<br/>按请求类型路由到不同算力]
    moderation[审核<br/>模式二:图片内容审核<br/>模式三:人脸黑名单过滤]
    credit[积分校验/扣减/退还<br/>+ 人物剩余片段配额]
  end

  charjson[(characters.json)]
  garmjson[(garments.json)]
  dancejson[(dance-refs.json)]
  libjson2[(library.json<br/>+ characterId/garmentId/refDanceVideoId 溯源字段)]
  userdb[(users + credits<br/>建议 SQLite,见 1.4)]

  fal2[(fal.ai<br/>付费实时视频生成,第五期接入)]
  h32[(内部 H3 Console<br/>冷启动/后台种子内容<br/>一步法批量生成)]

  dance -- 免费:选衣柜 --> api2
  dance -- 付费:传衣服/传人 --> api2
  api2 --> moderation
  api2 --> credit
  moderation -- 通过 --> pipeline
  credit -- 扣减通过 --> pipeline
  pipeline -- 冷启动/种子 --> h32
  pipeline -- 付费实时(第五期) --> fal2
  h32 -- 跳舞视频 --> pipeline
  fal2 -- 跳舞视频 --> pipeline
  pipeline -- 落库 --> libjson2
  admin2 --> charjson
  admin2 --> garmjson
  admin2 --> dancejson
  admin2 --> userdb

2.3 双算力策略:冷启动 vs 付费实时(已确认,见 docs/todo 4.3)

2026-09-02 更新:原设计是"Stage1 换装 + Stage2 视频生成"两段式, 第 0 期 S1 spike 实测确认一步法成立——人物图+衣服图+参考视频一次 调用直接出换装跳舞视频,没有独立的换装图合成步骤,见 docs/todo 4.1/4.2 和 spike/s1-s2-s4-2026-09-02/RESULTS.md。下面按一步法描述。

生成(一次调用)按"是否是付费实时请求"路由到两条不同算力:

  • 冷启动/后台种子内容(admin 批量预生成,铺满衣柜库/默认人物库) → 走内部 H3 Console(免费,但单条约 700s——这是产品默认的 10 秒产物 实测耗时,H3-CONSOLE.md 早期记录的约 200s 是 4 秒测试片段的耗时, 帧数越多耗时越长)。这条路径不面向单个观众的实时请求,批量跑不受 时间压力。第一~三期只用这条路径,不调用 fal。
  • 观众付费实时请求(模式二付费传衣服、模式三付费传人物,第五期 接入)→ 走 fal.ai 云端(minimax/h3* 系列端点,约 4s 级别,见 H3-CONSOLE.md 的实测对比),成本由观众支付的积分覆盖。S1 的一步法 /attribute_transfer prompt 写法只在 H3 Console 上验证过,接入 fal 时需要先复核同一套技巧在 fal 的模型上是否同样成立,不能直接假设 成立。

/fal/* 反代通道(1.2 节)现状只服务于仓库里跟直播间无关的旧工具, 第五期接入付费实时路径时才会被换装跳舞直播间业务启用。fal API Key 现状是前端 newtask.html 里手填(见 1.2 节代码原样),到时候必须挪到 服务端 .env,不能再让 key 走浏览器。

reference_video_urls 这个字段已实测确认真实生效(2026-09-02 S2 spike,docs/todo 8.2):同一份参考图、同一句动作文字描述,只切换有无 这个字段,产出的动作幅度/风格有明显可见差异,不是摆设,也不能退化成 纯文字描述驱动。

2.4 视频规格:默认竖屏,移动端优先

dance.html 的生成请求默认用 480×832(早期横屏默认 832×480 的 宽高对调),总像素量与现状完全一致,理论上不会拖慢 H3 Console/fal.ai 的生成耗时(docs/todo 4.4 节)。两个直播间的规格是各自独立的 H3_GEN_PARAMS 配置,互不共享——live.html 一侧后来也独立改成了 竖屏默认(docs/done 2.2 节),只是刚好两边选型一致,不是同一份配置 联动的结果。播放器 UI 上,dance.html 需要一套按 9:16 设计的播放器 容器,不能直接复用 live.html 现有播放器的宽高比假设。

2.5 与现状的集成点

  • 浏览三 tab 不动:v2 生成的视频最终落地也是 library.json 风格的 一条 clip(多了几个溯源字段),复用现状浏览逻辑,dance.html 只是 拥有自己独立的一份 schedule 实例(2.1 节)。
  • "生成中/排队中"展示复用:换装流水线本质也是"提交 → 后台异步生成 → 完成后进队列",跟现有 generateForPrompt 的状态机(generating → ready/failed,任务 id 落盘可断点续传)同构,一步法下直接复用这套逻辑, 不需要处理"Stage1 完成但 Stage2 未开始"这种中间态。
  • ensureSchedule 选片逻辑需要实质性扩展,不是"复用不动"——见 2.6。

2.6 排播优先级 + 去重缓存(已确认,对 v1 排片算法的改动)

dance.html 的 promptQueue/schedule 不能直接照抄 v1 的"纯投票排序" 逻辑,需要两处新行为(docs/todo 3 节"共同后置流程"、4.6 节):

  • 免费模式一走"先查缓存再生成":提交时先用 (characterId, garmentId) 查是否已有产物,命中直接排入队列(跟 v1 findLibraryMatch 是同一个套路,只是匹配维度从文本相似度换成资产 id 组合),未命中才真正调用生成。免费内容排入队列后仍按 v1 现有投票机制排序。
  • 付费模式三新人物内容插队播放:生成完成后不进入投票排序, 而是直接插入 schedule(紧跟在当前播放的片段之后,或队首),跳过 pickBestIndexByStatus 这条"按票数选"的路径。ensureSchedule 需要 一个新的"优先插入"通道,跟现有"从 promptQueue 里挑最高票"逻辑并行 存在,而不是复用同一个排序比较器。
  • 这两点意味着 dance.html 的排片模块虽然复用 v1 的整体结构(数据 持久化到 live-state.json 风格的文件、ensureSchedule 定时 tick 的 思路),但选片函数本身要重写,不是简单的参数化复用。

2.7 用户账号、积分与人物生命周期

  • 现有 viewer_uid/viewer_nickname cookie 身份保留作为过渡态;免费 模式一(选衣柜)不强制登录。上传新资产/花积分(模式二、三)才要求 邮箱登录(docs/todo 7.1)。注册后把 cookie uid 绑定到账号 id, 历史投票/点播记录不丢。
  • 积分校验点:提交付费流水线前先校验+预扣积分,生成失败要退还——这个 扣减+退还路径要求原子性,是 1.4 节里"该上 SQLite 了"的触发点之一。
  • 人物生命周期(docs/todo 3.3 节第 7 点、9.5 期):付费生成的新 人物带一个"剩余可播 10 秒片段数"配额,每播放一段消耗一份;配额耗尽 自动从排播池移除(但人物、已生成视频全部保留,不删除),用户再充值 可以直接续播,无需重新生成——这意味着"人物" 和"该人物名下的已生成视频"要能独立于"当前是否在直播池中"被检索到, 数据模型上是一个显式的 status: active|expired + 关联的积分流水, 不是简单的软删除。

3. 待验证事项汇总(技术验证,非产品决策分歧)

避免重复散落在需求文档各处,详见 docs/todo 第 8 节。2026-09-02 第 0 期 spike(S1/S2/S4)跑完后,下表前两项已解决,删去,保留仍待验证的:

待验证项 影响范围
fal.ai 端 minimax/h3* 端点是否支持同等的一步法/attribute_transfer 多参考图生视频 第五期付费实时路径是否可行——S1 只在 H3 Console 上验证过,不能直接假设 fal 也成立
用户上传图片如何被 fal.ai 访问到(公网 URL / 上传到 fal 存储) 第五期付费实时路径是否跑得通,见 docs/todo 8.10 与 MVCP M.13——注意 S4 的 POST /v1/assets 方案只解决 H3 Console 这一侧,不适用于 fal
人脸识别黑名单模型/服务选型 模式三自动审核能力
图片内容审核供应商/接口 模式二审核能力
JSON 文件 → SQLite 的迁移时机 存储层改造范围,见本文档 1.4
支付网关选型(初步倾向 Stripe,需确认是否要叠加支付宝/微信支付) 充值流程开发方向,见 docs/todo 7.3