# 系统架构文档
覆盖现状(v1,已上线)与规划中的「换装跳舞直播间」(v2,见
`docs/todo/dressup-dance-room.md`)。需求范围见对应文档,本文档只谈
组件划分、数据流、集成方式和存储演进。
## 1. 现状架构(v1)
### 1.1 组件
```mermaid
flowchart LR
subgraph Browser
live[live.html
直播间/浏览]
admin_ui[admin.html
素材后台]
end
subgraph Node["server.js (单文件 Node http server, PORT 8934)"]
api[路由处理
/api/*, /lib/video/*]
sched[schedule + promptQueue
2s tick]
ingest[ingest.js
syncLibrary 每5分钟]
end
libjson[(library.json)]
statejson[(live-state.json)]
h3[(H3 Console
内部 MiniMax H3 集群
/v1/videos)]
fal[(fal.ai queue API
/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
独立的 fal.ai 任务提交小工具
与直播间无数据关联] -.-> 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 复用/扩展参考)
```mermaid
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
按投票数从 ready/matched 里选片
不够则从素材库随机填充
loop 每 4s
V->>S: GET /api/live/state
S-->>V: {current, upNext, queue}
end
Note over V: 前端用 endAt 精确定时切片
轮询只是纠偏/兜底
```
### 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 新增组件
```mermaid
flowchart TB
subgraph Browser
dance[dance.html
衣柜/传衣服/传人,无文字输入]
admin2[admin.html
+ 人物/衣服/参考视频/用户 管理页]
end
subgraph Node["server.js(扩展)"]
api2[路由扩展
/api/dressup/*, /api/admin/characters, garments, dance-refs, users]
pipeline[生成流水线编排
一步法:人物图+衣服图+参考视频→跳舞视频
按请求类型路由到不同算力]
moderation[审核
模式二:图片内容审核
模式三:人脸黑名单过滤]
credit[积分校验/扣减/退还
+ 人物剩余片段配额]
end
charjson[(characters.json)]
garmjson[(garments.json)]
dancejson[(dance-refs.json)]
libjson2[(library.json
+ characterId/garmentId/refDanceVideoId 溯源字段)]
userdb[(users + credits
建议 SQLite,见 1.4)]
fal2[(fal.ai
付费实时视频生成,第五期接入)]
h32[(内部 H3 Console
冷启动/后台种子内容
一步法批量生成)]
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 |