系统架构文档
覆盖现状(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_transferprompt 写法只在 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)查是否已有产物,命中直接排入队列(跟 v1findLibraryMatch是同一个套路,只是匹配维度从文本相似度换成资产 id 组合),未命中才真正调用生成。免费内容排入队列后仍按 v1 现有投票机制排序。 - 付费模式三新人物内容插队播放:生成完成后不进入投票排序,
而是直接插入
schedule(紧跟在当前播放的片段之后,或队首),跳过pickBestIndexByStatus这条"按票数选"的路径。ensureSchedule需要 一个新的"优先插入"通道,跟现有"从 promptQueue 里挑最高票"逻辑并行 存在,而不是复用同一个排序比较器。 - 这两点意味着
dance.html的排片模块虽然复用 v1 的整体结构(数据 持久化到live-state.json风格的文件、ensureSchedule定时 tick 的 思路),但选片函数本身要重写,不是简单的参数化复用。
2.7 用户账号、积分与人物生命周期
- 现有
viewer_uid/viewer_nicknamecookie 身份保留作为过渡态;免费 模式一(选衣柜)不强制登录。上传新资产/花积分(模式二、三)才要求 邮箱登录(docs/todo7.1)。注册后把 cookieuid绑定到账号 id, 历史投票/点播记录不丢。 - 积分校验点:提交付费流水线前先校验+预扣积分,生成失败要退还——这个 扣减+退还路径要求原子性,是 1.4 节里"该上 SQLite 了"的触发点之一。
- 人物生命周期(
docs/todo3.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 |