dressup-dance-room-mvcp.md 49.1 KB

换装跳舞直播间 — 技术设计文档(Model / View / Control / Process)

依据:产品需求 docs/todo/dressup-dance-room.md(下称"需求文档",本文各处 引用其章节号);系统架构 docs/structure/architecture.md(下称"架构文档")。 本文档把已确认的产品决策落到:数据库表结构(M)、前后端界面清单(V)、 按钮/交互动作(C)、各类用户的操作流程(P),供开发排期和实现参考。

约定:字段名延续现有 library.json/live-state.json 的英文小驼峰风格; 表结构在第一期仍以 JSON 文件承载,第三期(用户账号)后迁移 SQLite(见 架构文档 1.4 节)——本文按"关系表"描述,两种落地方式都能直接映射。


M — Model:数据结构设计

M.1 users

字段 类型 说明
id string (uuid) 主键
email string 邮箱,唯一(7.1 节,第一期登录方式)
passwordHash string bcrypt/argon2,邮箱登录用
emailVerified boolean 邮箱验证是否已完成(7.1 节"账号+邮箱验证")。未验证账号可登录、可投票,但不能发起任何付费操作(充值/传衣服/传人),避免用一次性邮箱刷积分退款链路
role enum viewer \
oauthProviders array 预留 ["google","apple","github"],第一期为空数组
nickname string 展示昵称,迁移自现有 cookie viewer_nickname
legacyUid string \ null
ageConfirmed boolean "我已满18岁" 勾选记录(5.1 节)
ageConfirmedAt number (ts)
creditBalance number 当前积分余额(冗余字段,权威值仍是 credit_ledger 流水汇总,见 M.2)
createdAt number (ts)

M.2 credit_ledger(积分流水,7.2 节)

字段 类型 说明
id string 主键
userId string 外键 → users.id
delta number 正数=充值/退款,负数=消费
reason enum topup \
relatedAssetId string \ null
balanceAfter number 变动后余额,写入时计算好,避免每次汇总流水
paymentRef string \ null
createdAt number (ts)

M.3 characters(人物库)

字段 类型 说明
id string 主键
name string
imageUrl string 人物参考图
tags array
source enum admin_seed \
ownerUserId string \ null
appearanceDescriptor string 管理员手写的人物外观特征(发色/发长/发型、五官、体态 2-3 条,如 "shoulder-length wavy black hair, delicate features, a slender build")。填入 M.15 模板的 {{appearanceDescriptor}}(一步法确认后不再与 garments.appearanceDescriptor 拼接——两者分别对应 <Picture 1>/<Picture 2> 两张独立参考图,见 M.15),跟 dance_refs.motionDescriptor 完全同一套路:录入时写一次、长期复用。admin_seed 来源必填;user_paid(模式三)来源无人手写,走视觉模型自动打标(第四期,需求文档 8.8)
isDefault boolean 是否为直播默认人物;第一期全库最多一条为 true(6.1 节已确认,不做多人物轮换)
moderationStatus enum auto_passed \
totalSegments number \ null
remainingSegments number \ null
status enum active \
createdAt number (ts)

M.4 garments(衣服库)

字段 类型 说明
id string 主键
name string
category string 分类标签(古风/泳装/校服…)
thumbnailUrl string
imageUrl string 原图
appearanceDescriptor string 管理员手写的服装特征(颜色/款式/材质,如 "a fitted red qipao with gold trim"),填入 M.15 模板的 {{garmentDescriptor}}(对应独立的 <Picture 2>,不再与人物特征拼接)。admin_seed 必填;模式二用户上传的走视觉模型打标(第四期)
source enum admin_seed \
ownerUserId string \ null
moderationStatus enum pending \
isActive boolean 上下架开关
createdAt number (ts)

M.5 dance_refs(参考跳舞视频/音乐库)

字段 类型 说明
id string 主键
name string
videoUrl string
styleTags array
durationSec number
motionDescriptor string 管理员手写的动作描述(如"拉丁风格摆胯+举臂旋转"),拼进 Stage2 prompt 模板的 detailed_description(已确认,需求文档 4.2/6.3 节),上传时必填
copyrightSource enum test_placeholder \
isActive boolean
createdAt number (ts)

M.6 composed_images(Stage1 换装图产物) —— 已作废

2026-09-02 S1 spike 实测确认路径 B(一步法)成立(需求文档 8.1、 spike/s1-s2-s4-2026-09-02/RESULTS.md):人物图+衣服图直接进 ref2va 的 reference_image_urls,无需先合成换装图这一步。Stage1、 这张表、appearanceDescriptor 的"合成图取值优先级"设计、图片模型选型、 按 (characterId, garmentId) 去重换装图缓存——全部不需要了。

去重缓存的落地位置改为直接在 M.7 generated_videos 上按 (characterId, garmentId) 查是否已有 status: done 的记录(4.6 节 仍然成立,只是缓存对象从"换装图"变成"成品视频",一步到位)。

M.6.1 补充验证(2026-09-04):一步法的身份漂移问题,及修复方向

S1 spike 证明了一步法"能生成",但没有在真实跳舞参考视频 + 完整播放 的条件下验证过身份稳不稳。这次用户实测发现了这个缺口——用真实素材重 新测了 4 版,都是对 H3 Console 的真实提交(不是模拟):

任务 id 参考图 跳舞素材 结果
7ebf616e-c4df...(本机 generationJobs 任务) 1 张人物原图 + 1 张衣服图 误用的旧 mock 素材 衣服换对了,但用错了跳舞素材,不能作为身份判断依据
c0b0879be2b3(H3 Console 原生任务,非本项目提交) 1 张人物原图 + 1 张衣服图 真实素材 衣服换对,身份大体保持,但用户反馈播放到后段人脸有点糊——静态抽帧没抓到,说明是播放过程中的瞬时问题,不是每帧都稳
53854aa8c25b 3 张已换装的人衣图(dressgen 工具产物:正/侧/背,替代"1 人物图+1 衣服图") 真实素材 衣服对,人脸清晰、身份稳定,结尾不糊。三张参考图背景本不统一(正/背白棚、侧面拱廊庭院),模型自己收敛成了统一的拱廊庭院背景
edaa822af944 7 张人物原始多角度/表情图(未换装)+ 1 张衣服图 真实素材(同上) 效果与 53854aa8c25b 相当:人脸清晰、身份稳定、结尾不糊

结论:53854aa8c25b 和 edaa822af944 效果相当,都明显比单图版本 稳,说明问题不在"要不要先合成换装图"(M.6 的结论不变,Stage1 依然 不需要),而在"给身份定义的参考图张数够不够"——单张 <Picture 1> 不足以在整段真实跳舞动作里稳住脸,多张(不管是换装前的原始多角度照, 还是换装后的合成图)都能顶住。edaa822af944 这条路径更简单:不用先 跑 dressgen 生成换装图,直接把人物的多张原始参考图 + 1 张衣服图一起 传给一步法生成即可。

这把 M.3 的 characters 表推向一个之前没记录的缺口:现在 characters.imageUrl 是单张图,跟这次验证需要的"每个人物多张参考图 (正/侧/背、表情等)"对不上——这次用户提供的真实素材本身就是按这个 分类给的(根目录 8 张图:三视图+八视图+表情参考,见对应导入记录)。 待实现(未写代码,先记录决策):

  • characters.imageUrl(单图)→ characters.imageUrls(数组),或新 增 imageUrls 数组字段、保留 imageUrl 作默认展示图/向后兼容。
  • 人物库的新建/编辑 UI 要支持一次传多张。
  • generateDanceVideoViaH3/ViaFal 组装 reference_image_urls 时要 把该人物的全部参考图都塞进去(现在是 ensureH3AssetUrl(characters, character, "imageUrl") 单张调用,需要改成对每张图分别调用/缓存), 衣服图仍放最后一张。
  • admin-dressgen*.html(本次会话新增的独立小工具)不接入这条生产 流水线,继续作为管理员事先核对"某件衣服穿某个人物大概什么样"的 可视化辅助工具单独存在。

M.7 generated_videos(Stage2 产物,library.json 风格 clip 的超集)

在现有 library.json clip 字段基础上(见 docs/done 第 3 节)新增:

新增字段 类型 说明
characterId string
garmentId string
refDanceVideoId string 外键 → M.5
channel enum live \
backend enum h3_console \
priority enum normal \
requestedByUserId string \ null
width/height number dance 频道默认 480×832;live 频道走独立的 H3_GEN_PARAMS,目前也是 480×832(docs/done 2.2 节),两者不共享同一份配置

M.8 直播时刻表状态(非持久化数据库表,live-state.json 风格的内存+落盘结构,dance.html 独立一份)

{
  "channel": "dance",
  "schedule": [{ "videoId": "...", "startAt": 0, "endAt": 0, "requestedBy": null, "priority": "normal" }],
  "priorityInbox": [ /* paid_boost 产物就绪后先进这里,ensureSchedule 优先消费,见架构文档 2.6 节 */ ],
  "freeQueue": [ /* 免费模式一提交,走投票排序,结构同 v1 promptQueue */ ]
}

⚠️ 与 v1 的一处有意差异:现有 live-state.json 的 schedule 条目内联 存的是整个 clip 对象({ clip, startAt, endAt, requestedBy }),而这里 存的是 videoId 引用。dance 频道资产多、外键关系复杂(人物/衣服/ 参考视频),内联整份快照会让"后台改了一条素材、时刻表里还是旧副本"变成常 态。实现时不要照抄 v1 的内联写法,按 id 引用 + 读取时 join。

M.9 moderation_log

字段 类型 说明
id string 主键
assetType enum garment_upload \
assetId string
verdict enum auto_pass \
reason string \ null
reviewedBy string system 或管理员 userId
createdAt number (ts)

M.10 sessions(登录会话 + 邮箱验证令牌)

需求 7.1 的登录方式是"账号+邮箱验证",M.1 只存了 passwordHash,验证和 登录态需要单独的短期令牌表(也是唯一有 TTL 的表,落地时按过期时间清理):

字段 类型 说明
id string 主键
userId string 外键 → users.id
kind enum session(登录态)\
token string 随机串(crypto.randomUUID/32 字节随机);session 类型写进 HttpOnly cookie,不复用现有的明文 viewer_uid cookie
expiresAt number (ts) session 建议 30 天,email_verify/password_reset 建议 24 小时
createdAt number (ts)

鉴权规则(一处集中实现,别散在各路由里):

  • /api/dance/* 的免费接口:无需 session,沿用 cookie viewer_uid。
  • 任何扣积分/写用户资产的接口:要求有效 session + users.emailVerified = true。
  • /api/admin/* 全部接口:要求有效 session + users.role = 'admin'。

M.11 blacklist(黑名单)

V.3 的 POST /api/admin/blacklist、C.2 的"拉黑用户/资产"、P.6 的人工兜底 都在写这张表,原稿漏了它:

字段 类型 说明
id string 主键
targetType enum user \
targetId string 对应实体 id;ip 类型存 IP 字符串
reason string 管理员填写
createdBy string 管理员 userId
createdAt number (ts)

拦截点:提交类接口(/api/dance/pick、两个 upload)入口先查这张表,命中 直接拒绝,不进流水线、不扣积分;已在库的资产被拉黑时,同时从排播候选 和衣柜列表里过滤掉(不删除记录,跟 5 节"记录保留"的口径一致)。

⚠️ 这张表和需求文档 5 节说的人脸黑名单是两回事:这里是运营手工拉黑的 本站实体,人脸黑名单是模型侧的敏感人物识别(选型见需求文档 8.4)。

M.12 pricing_config(计费配置)

需求 7.2 的原话是"先把余额+流水+配置化扣费额这套机制打好,数值上线前 再定"——数值待定不等于载体待定,这张表就是那个载体(第一期可以是一个 pricing.json,字段不变):

字段 类型 说明
key string 主键,如 spend_garment / spend_character_per_segment / topup_tier_1
kind enum spend(扣费项)\
credits number 扣费项=消耗积分数;充值档位=到账积分数
priceCents number \ null
segmentsPerUnit number \ null
isActive boolean 下架旧档位不删记录(历史流水要能解释当时的价格)
updatedAt number (ts)

扣费金额一律从这张表读,不写死在代码里;credit_ledger 每条流水落库时 把当时的 credits 一起记下来,改价不影响历史账目的可解释性。

M.13 上传图片/视频的存储与访问(已按 S4 spike 结论简化)

原稿里 garments.imageUrl/characters.imageUrl 都只是 string,没说文件 存哪、怎么对外服务、上限多少。v1 只有视频、且走 GET /lib/video/:id 反代 H3 Console(本地不落盘),用户上传图片是一条全新的链路,先定死这几点:

  • 落地位置:第一期落本地盘 uploads/{yyyymm}/{uuid}.{ext},server.js 新增静态路由 GET /uploads/* 只读服务(与 /lib/video/* 并列),供 admin.html/dance.html 自己展示缩略图用。imageUrl 存相对路径 /uploads/...,不存绝对域名,方便以后整体迁到对象存储时只改一处前缀。
  • 入站限制(trust boundary,必须在服务端做,不能只靠前端校验): 单文件 ≤ 8 MB;仅接受 image/jpeg/image/png/image/webp,且按实际 文件头判断而不是扩展名或客户端 Content-Type;服务端重新编码一遍再 落盘(顺带剥掉 EXIF——用户上传的人物照大概率带 GPS 定位)。
  • 文件名:一律服务端生成 uuid,不使用用户提供的文件名(路径穿越)。
  • 喂给 H3 Console 的那一份,走 /v1/assets 转发,不是自建公网签名 ——2026-09-02 S4 spike 已推翻原方案(原方案是"给 uploads/ 挂 公网可读的临时签名地址")。H3 Console 自带 POST /v1/assets(multipart, 字段名 file),是控制台自己的文件选择器在用的同一个端点,返回的 URL H3 后端保证能拉到(H3-CONSOLE.md 已补充此端点文档)。落地方式:图片/ 视频先落本地 uploads/(给我们自己的页面用),需要提交生成任务时再 转发一次到 /v1/assets,拿到的 URL 直接填 reference_image_urls/ reference_video_urls,characters/garments/dance_refs 表可以选择 多存一个 h3AssetUrl 字段缓存这个转发结果,避免每次提交都重新转发同一 份素材。这条只解决 H3 Console 这一侧;第四期若接 fal.ai 实时路径, fal 是完全不同的服务,仍要单独解决入站可达性(需求文档 8.10,推迟到 第四期,不影响第一~三期)。
  • 清理:审核驳回/生成失败的上传文件,随退款流程一起删除物理文件, 不留孤儿。

M.14 ER 关系图

erDiagram
  users ||--o{ sessions : "登录态/验证令牌"
  users ||--o{ credit_ledger : "流水"
  users ||--o{ blacklist : "被拉黑 / 创建拉黑"
  pricing_config ||--o{ credit_ledger : "扣费/充值取价"
  users ||--o{ characters : "user_paid 拥有者"
  users ||--o{ garments : "user_paid 拥有者"
  users ||--o{ generated_videos : "requestedBy"
  characters ||--o{ generated_videos : "一步法直接输入(无 Stage1)"
  garments ||--o{ generated_videos : "一步法直接输入(无 Stage1)"
  dance_refs ||--o{ generated_videos : "Stage2 输入"
  characters ||--o{ moderation_log : "审核记录"
  garments ||--o{ moderation_log : "审核记录"

M.15 Stage2 默认 Prompt 模板(已确认,需求文档 4.2 节;一步法版,2026-09-02 实测通过)

依据仓库里的 docs/h3skill.md(内部 H3 提示词写作方法论)"全能参考 模式六段式"结构写成。{{...}} 是占位符,由 M.3/M.4/M.5 的 appearanceDescriptor/motionDescriptor 字段在拼装时填入:

  • {{appearanceDescriptor}} 来源:优先取 characters + garments 两个手写字段拼接,视觉模型自动打标只是模式二/三的兜底(第四期)
  • <Picture 1> = 人物参考图(characters.imageUrl),供人脸/发型/体型 的视觉身份
  • <Picture 2> = 衣服参考图(garments.imageUrl),retention_analysis 标 attribute_transfer(H9 白名单取值,语义就是属性转移)——只转移 服装的外观属性,不引入这张图的其他内容
  • <Video 1> = 参考跳舞视频(dance_refs.videoUrl),只作动作/运镜/ 节奏参考,不是视觉身份来源
  • <Audio 1> = <Video 1> 携带的配乐轨。H3 规范里参考视频不因为含声 就自动产生 <Audio N>(docs/h3skill.md H15),所以它必须在 subject_definitions 里显式立一条定义。

素材总量 2 图 + 1 视频 + 1 音频,音频有视频伴随、未单独输入,满足 H13。 没有 Stage1、没有换装合成图这一步——需求文档 8.1 的 S1 spike 已实测 确认这条一步法路径成立(人脸/换装/动作三项验收全过,见 spike/s1-s2-s4-2026-09-02/RESULTS.md),原来为兜底两步法保留的 <Picture 1>=合成图版本已删除,不再维护两份模板。

⚠️ 实测中发现的一个副作用,已在下面的模板里修正(T7,2026-09-02): 产出视频里角色的鞋子原本是模型自己选的(S1 实测自作主张配了黑色短靴), prompt 里没提鞋子这条约束。跟 CODEX-IMAGEGEN.md 记录的 Stage1 图片编辑 "鞋子自作主张"是同一类问题——规则定为"沿用人物图 <Picture 1> 的鞋, <Picture 2> 衣服图不影响鞋子",已写进 subject_definitions/ retention_analysis 两段(各加一句,不占 detailed_description 的词数 预算)。

subject_definitions:
<Subject 1> is the young woman whose visual identity is defined by <Picture 1>: {{appearanceDescriptor}}. Preserve her face, hairstyle, and body exactly as shown; only her outfit, pose, motion, and framing change in the target video.
<Picture 1> is the character reference frame for <Subject 1>, showing her face, hairstyle, and body in a neutral standing pose, wearing a neutral outfit that does not appear in the target video; her footwear as shown here is retained unchanged in the target video.
<Picture 2> is the garment reference image: {{garmentDescriptor}}, laid flat with no person wearing it. Only its garment attributes (silhouette, color, fabric, trim, cut) transfer onto <Subject 1>; no other content of this image appears in the target video, and it does not affect her footwear.
<Video 1> is the reference choreography clip. It defines the dance structure, phrasing, and camera rhythm that the target video follows; it is not a visual identity source, and none of its performer, background, or imagery appears in the target video.
<Audio 1> is the background music track carried on <Video 1>, reused verbatim as the music bed of the target video. It contains no speech.

summary:
[reference generation + attribute transfer + audio reuse] The target video shows <Subject 1>, now wearing the outfit transferred from <Picture 2>, performing {{motionDescriptor}} in a vertical studio setting, following the pacing and camera rhythm established in <Video 1>, with the music of <Audio 1> reused as its track.

retention_analysis:
<Subject 1> (appears in [Shot 1]): fully_preserved - her face, hairstyle, and body are retained unchanged throughout; her outfit changes to the garment shown in <Picture 2>, her footwear stays as shown in <Picture 1>.
<Picture 1>: fully_preserved - it supplies the face, hairstyle, body, and footwear of <Subject 1> and the opening framing of the shot.
<Picture 2>: attribute_transfer - only the garment's silhouette, color, fabric, and trim transfer onto <Subject 1>; the image itself does not appear.
<Video 1>: weak_reference - only the choreography phrasing and the camera rhythm are referenced; no performer, background, or imagery from it is retained.
<Audio 1>: fully_copy - the music track is reused verbatim as the non-diegetic music bed.

detailed_description:
A vibrant, mobile-first vertical dance video with warm, glossy studio lighting, saturated color, and a clean contemporary look sustained from the first frame to the last. [Shot 1] The shot begins from <Picture 1>, with <Subject 1> now wearing the outfit transferred from <Picture 2>. She stands centered in a medium-full vertical frame against a softly lit seamless backdrop that falls off into gentle shadow toward the edges. A warm key light sits slightly camera-left, a cooler rim light separates her shoulders and hair from the background, and a soft fill keeps her face evenly exposed. She begins in a relaxed neutral stance, weight settled on her back foot, hands loose at her sides, chin level with the lens. On the first musical accent she starts {{motionDescriptor}}, and the routine unfolds continuously from there with no cut. Her weight transfers smoothly from foot to foot on the beat, hips leading each transfer and shoulders following a fraction behind, so the movement reads as one connected chain rather than a series of separate poses. Her arms trace clean, unhurried arcs that extend fully at the peak of each phrase and soften on the release, wrists relaxed and fingers naturally extended. Between phrases she adds small directional changes: a quarter turn away from the lens and back, a step that closes distance with the camera, a shift of gaze that returns to the lens on the downbeat. The camera holds a Static Shot through the opening beats, then performs a slow Push In with small amplitude at slow speed, tightening from medium-full to medium while keeping her head and hands inside the frame at every point of the routine. Her expression stays confident and playful, with a light smile and eyes that meet the lens at the end of each phrase. The fabric of the transferred outfit swings and settles naturally with each step, turn, and arm extension, and her hair carries the momentum of her head movement half a beat behind it. The pacing, phrasing, and camera rhythm follow the structure established in <Video 1>: the routine builds energy through the middle of the clip, reaches its widest and fastest movement around the two-thirds point, then settles back toward stillness. The closing beats return her to a stance, framing, and lighting that closely match her opening position, so the clip loops cleanly for repeated live playback. No text, graphics, watermarks, or overlays appear anywhere in the frame at any point.

overall_soundscape:
Soft ambient room tone of an enclosed studio sits under the music, with faint fabric rustle as <Subject 1> moves and light rhythmic footfall on the floor as her weight transfers. No speech, no crowd, and no off-screen sound events occur at any point.

non_diegetic_music:
An upbeat instrumental track drives the routine from the first frame to the last; its tempo, instrumentation, and dynamics are copied verbatim from <Audio 1>. Its accents align with the weight transfers and arm extensions of <Subject 1>, and its energy build through the middle of the clip matches the build in her movement.

填入示例(真实跑过的组合,见 spike/s1-s2-s4-2026-09-02/prompt-s1-onestep.txt): {{appearanceDescriptor}} = "shoulder-length straight black hair, a slender build, natural fair skin, and a friendly neutral expression"; {{garmentDescriptor}} = "a fitted red qipao-style dress with gold trim embroidery and a side slit";{{motionDescriptor}} = "a lively hip-hop inspired routine with hip sways and raised-arm turns"。

M.15.1 机械验收结果(对照 docs/h3skill.md §1.7 的 22 项 + 真实调用结果)

条款 要求 本模板
H6 detailed_description 350–500 英文词 骨架约 400 词(3 个占位符),代表性填充后未超上限 ✅
H9 关系标记只用 8 个固定取值 fully_preserved ×2 / attribute_transfer / weak_reference / fully_copy,全在白名单 ✅
H10 soundscape 1–4 句、music 1–3 句 2 句 / 2 句 ✅
H13 图 ≤9、视频 ≤3、音频 ≤3 且不可单独输入 2/1/1,音频随视频输入 ✅
H14 标签在五处同义 <Subject 1>/<Picture 1>/<Picture 2>/<Video 1>/<Audio 1> 五个标签的含义在六段中一致,无重定义 ✅
H15 <Video N>/<Audio N> 独立编号、参考视频不自动产生音频标签 <Audio 1> 已在 subject_definitions 显式定义 ✅
H5 首镜不写时间戳,后续 At MM:SS.mmm 递增 单镜头 [Shot 1],零处时间戳 ✅
H2/H3 4–15 秒;正文不写"总时长 X 秒"指令行 10 秒(frames = 240)由参数层给定,正文未提时长/帧率 ✅
H1 交付件 ≤ 7000 字符 约 3.6k 字符 ✅
H12/H7/H11 台词/画外音/口型相关 无台词、无说话人,不适用(retention_analysis 亦无 (Sx))✅
summary 前缀 六类型之一,可 + 组合 [reference generation + attribute transfer + audio reuse] ✅
实测(2026-09-02,job eac9f8fe1c36) 人脸/换装/动作三项验收 全过——见 spike/s1-s2-s4-2026-09-02/RESULTS.md;人脸有轻微漂移(头发变长变蓬松),换装/动作准确

词数预算(拼装时的唯一动态风险):三个占位符填入内容合计不宜超过 ~100 词,超限就截断 appearanceDescriptor/garmentDescriptor 到 2-3 条 特征(H3 规范对人物特征的要求本来就是 2-3 条,不是越多越好)。

一行校验(拼好的 prompt 存成文件后跑):

awk '/^detailed_description:/{f=1;next}/^overall_soundscape:/{f=0}f' prompt.txt | wc -w

⚠️ 实测发现、模板尚未修正的一条:产出视频里鞋子是模型自选的,正式 模板应显式规定鞋子处理方式,见上方"实测中发现的一个副作用"。


V — View:前后端界面设计

V.1 观众端页面

页面 路径(建议) 说明
换装跳舞直播间 dance.html 主页面,见 V.1.1
登录/注册 弹窗(不单独开页,跟 v1 昵称弹窗交互一致,见架构文档/docs/done 2.5 节) 邮箱输入 + 验证码/密码,首次触发付费操作时弹出
我的作品 my-works.html 或 dance.html?tab=mine(建议做成 dance.html 内的一个面板而不是独立页面,减少一个页面壳) 7.4 节
充值 弹窗,from 积分不足拦截 或 顶部"我的积分"入口 7.3 节,具体支付渠道待定

V.1.1 dance.html 区域划分(复用 live.html 视觉语言,见需求文档 2 节)

┌─────────────────────────────────────────────┐
│ 顶部条:Logo | 在线人数 | 我的积分 | 头像/登录 │
├───────────┬───────────────────┬─────────────┤
│           │                   │             │
│  左侧栏    │   播放器(9:16)   │  右侧栏      │
│ 衣柜/传衣 │   + 当前播放信息   │ 在线互动展示 │
│ /传人 tab │   (复用 live.html│ (点赞/排队 │
│           │   的 now-playing)│  列表,无输 │
│           │                   │  入框)      │
│           │                   │             │
├───────────┴───────────────────┴─────────────┤
│ 移动端:底部 tab 切换(衣柜/传衣/传人/我的) │
└─────────────────────────────────────────────┘
  • 播放器容器按 9:16 设计(4.4 节),不是 live.html 现有横向容器的简单 拉伸。
  • 左侧栏三个 tab:衣柜(默认)/ 传衣服 / 传人,对应需求文档 3 节三种模式。
  • 首次进入弹年龄确认(5.1 节),非模态强制,一次性记忆(cookie/账号)。

V.2 后台管理页面

在现有 admin.html(docs/done 2.8 节)基础上新增,交互风格保持一致 (表格 + 行内编辑 + 新增表单,见需求文档 6 节开篇):

页面 对应资源 需求文档章节
admin-characters.html M.3 6.1
admin-garments.html M.4 6.2
admin-dance-refs.html M.5 6.3
admin.html(扩展现有表格新增列) M.7 6.4
admin-playback.html M.8(dance 频道排播干预) 6.5
admin-users.html M.1 + M.2 用户/积分管理,7 节
admin-batch-gen.html M.6 + M.7 冷启动批量生成(人物×衣服×参考视频组合勾选 → 批量提交 → 进度表),需求文档 9 节第一期的主体功能,原稿漏了这一页
admin-pricing.html M.12 扣费项/充值档位配置,7.2 节"配置化扣费额"
admin-blacklist.html M.11 黑名单条目 CRUD + 命中日志,5 节人工兜底

后台导航建议在现有 admin.html 顶部加一排二级导航链到这些新页面, 不新起一套独立的后台框架。

全部后台页面和 /api/admin/* 接口要求 role: admin 会话(M.1/M.10) ——v1 的"零鉴权靠网络边界"在引入积分后不能继续,admin-users.html 上 一个按钮就能改别人的余额。

V.3 后端 API 清单

延续 server.js 手写路由风格(无框架),按资源分组。所有提交类接口 (pick/两个 upload/vote)入口先过 M.11 黑名单 + 限流(见 P.8):

观众端:
GET  /api/dance/library?sort=              # dance 频道浏览三 tab,等价 v1 /api/library
GET  /api/dance/live/state?cid=&uid=        # dance 频道播出状态,等价 v1 /api/live/state
GET  /api/dance/wardrobe                    # 衣柜列表(garments,isActive=true)
POST /api/dance/pick                        # 模式一:{characterId?, garmentId} 免费提交
POST /api/dance/upload-garment              # 模式二:multipart 图片 + 走审核+扣积分
POST /api/dance/upload-character             # 模式三:multipart 图片 + 年龄确认+黑名单审核+积分档位(决定 remainingSegments)
POST /api/dance/queue/vote/:id              # 免费队列投票,等价 v1 /api/queue/vote
POST /api/dance/age-confirm                 # 年龄确认落库:已登录写 users.ageConfirmed,未登录写 cookie
GET  /api/dance/my-works                    # 我的作品(需登录)
POST /api/auth/register                     # 邮箱注册 → 发验证邮件,写 sessions(kind: email_verify)
GET  /api/auth/verify?token=                # 邮箱验证回调,置 users.emailVerified=true
POST /api/auth/resend-verify                # 重发验证邮件(同一 userId 有频率限制)
POST /api/auth/login                        # 成功后写 sessions(kind: session) + HttpOnly cookie
POST /api/auth/logout                       # 删除该 session 记录
GET  /api/auth/me                           # 当前登录态 + 余额 + emailVerified,供前端渲染顶部条
POST /api/credit/topup                      # 发起充值(对接支付网关,7.3 节)
POST /api/credit/topup/webhook              # 支付回调
GET  /api/credit/pricing                    # 读 pricing_config 的 isActive 项,前端展示"这将消耗 N 积分"

后台:
CRUD /api/admin/characters[/:id]
CRUD /api/admin/garments[/:id]
CRUD /api/admin/dance-refs[/:id]
CRUD /api/admin/generated-videos[/:id]      # 扩展现有 /api/admin/library
POST /api/admin/playback/insert             # 插播/置顶
POST /api/admin/playback/skip               # 跳过当前
POST /api/admin/playback/pause-fill         # 暂停自动填充
CRUD /api/admin/blacklist[/:id]             # 拉黑/解除,读写 M.11
CRUD /api/admin/pricing[/:key]              # 扣费项/充值档位配置,读写 M.12
POST /api/admin/batch-gen                   # 冷启动批量生成:{characterId, garmentIds[], refDanceVideoIds[]}
                                            #   → 展开成组合逐条入队,走 H3 Console(4.3 节)
GET  /api/admin/batch-gen/status            # 批量任务进度(每条组合的生成状态)
GET  /api/admin/moderation-log
GET  /api/admin/users
POST /api/admin/credit/adjust               # 人工调整积分(客服场景)

内部(生成流水线编排,不对前端暴露;**一步法,无 Stage1**,2026-09-02 S1 spike 确认):
pickRefDanceVideo()                                # 从 dance_refs(isActive=true) 里随机挑一条
                                                   #   (需求 3.3 第 5 点"参考视频固定走系统随机"的落点,
                                                   #    三种模式统一在这里选,调用方不传)
generateDanceVideo(characterId, garmentId, refDanceVideoId, backend)
                                                   # 查缓存(M.7 (characterId,garmentId) 唯一索引)→未命中才生成;
                                                   # 按 M.15 一步法模板拼 prompt(人物图+衣服图两张参考图直接进
                                                   # ref2va),backend 按 4.3 节路由(第一~三期恒为 H3 Console)

C — Control:界面按钮/交互动作设计

C.1 dance.html

按钮/交互 触发条件 动作 反馈
衣柜卡片点击 始终可点(免费,无需登录) POST /api/dance/pick {garmentId} —— 未命中缓存的提交要过限流(P.8),命中缓存的不计入配额 立即在"生成中"区出现一条;命中缓存(4.6节)秒级进"接下来播放";触发限流时按钮置灰并提示剩余等待时间
"传衣服"上传按钮 需登录 + 邮箱已验证(M.1 emailVerified) 若未登录先弹登录;选图 → 客户端基础校验(大小/格式)→ 上传 → 展示预计消耗积分 → 二次确认 提交后进图片审核队列;通过后自动排入生成,展示状态
"传人"上传按钮 需登录 + 邮箱已验证 + 已完成年龄确认(5.1节) 选图 → 选择/展示积分档位(决定 remainingSegments,7.2节)→ 可选搭配衣服(选衣柜或再传一张,不选则随机)→ 二次确认扣积分 提交后走黑名单自动审核(5节),通过直接生成,不阻塞用户等待
年龄确认弹窗"确认"按钮 首次进入 dance.html POST /api/dance/age-confirm → 已登录写 users.ageConfirmed,未登录写 cookie(登录后由 P.4 合并回账号) 弹窗关闭,露出主界面
登录/注册弹窗"提交" 首次付费操作或主动点登录 POST /api/auth/register / login 注册后提示"验证邮件已发送",未验证状态下付费按钮保持禁用并显示"重发验证邮件"入口
点赞(❤️) 浏览三 tab 内 POST /api/like/:id(复用 v1) 数字 +1
排队投票(▲) 免费队列条目 POST /api/dance/queue/vote/:id(复用 v1 投票逻辑,仅对 priority: normal 条目生效——paid_boost 条目不展示投票按钮,见需求文档 3 节"共同后置流程") 高亮 + 票数更新
"我的作品"入口 需登录 打开 V.1 的我的作品面板 列出该账号名下 generated_videos(含 status: expired 的人物产出)
"我的积分"/充值入口 始终可见(未登录显示"登录后查看") 打开充值弹窗 对接支付网关(7.3节,渠道待定)
静音/全屏 复用 live.html 现有 muteBtn/播放器交互 同 v1 同 v1

C.2 后台管理页面

按钮/交互 所在页面 动作
新增/编辑/删除 人物、衣服、参考视频 各自 admin 页 对应 CRUD API(V.3),风格照抄现有 admin.html 行内编辑
"设为直播默认人物" 开关 admin-characters.html 置该条 isDefault=true,同时把其他所有条目置 false(第一期全局唯一,6.1节)
审核通过/驳回(pending→approved/rejected) admin-garments.html(user_paid 来源条目) 更新 moderationStatus,写 moderation_log
人工下架人物/内容 admin-characters.html/admin-generated-videos 5节兜底手段,写 moderation_log(verdict: admin_takedown)
插播/置顶下一条 admin-playback.html POST /api/admin/playback/insert,写入 M.8 priorityInbox
跳过当前 admin-playback.html POST /api/admin/playback/skip
暂停/恢复自动填充 admin-playback.html POST /api/admin/playback/pause-fill,暂停期间 ensureSchedule 只消费 priorityInbox/freeQueue,不再随机填充素材库
拉黑用户/资产 admin-playback.html 或 admin-users.html 写黑名单表,后续该用户/资产提交直接拒绝,不进流水线
人工调整积分 admin-users.html POST /api/admin/credit/adjust,写 credit_ledger(reason: admin_adjust)
勾选人物×衣服组合 → "批量生成" admin-batch-gen.html POST /api/admin/batch-gen,展开组合逐条入队走 H3 Console(4.3节);已有 generated_videos 缓存的组合(M.7 唯一索引)默认跳过,避免重复烧 GPU
批量任务进度表刷新/重试单条 admin-batch-gen.html GET /api/admin/batch-gen/status;失败条目可单独重投,不用整批重跑
编辑扣费项/充值档位 admin-pricing.html CRUD /api/admin/pricing,改价只影响之后的流水(M.12)
新增/解除黑名单条目 admin-blacklist.html CRUD /api/admin/blacklist(M.11)

P — Process:用户流程设计

P.1 匿名免费观众:选衣柜(对应需求文档 3 节模式一)

sequenceDiagram
  participant V as 观众(未登录)
  participant S as server.js
  participant Cache as generated_videos 缓存(M.7 唯一索引)
  participant H3 as H3 Console

  V->>S: 首次进入 dance.html
  S-->>V: 弹年龄确认(5.1节)
  V->>S: 确认已满18岁
  V->>S: POST /api/dance/pick {garmentId}
  S->>S: 查 blacklist(M.11) + 限流(P.8),命中直接拒绝
  S->>Cache: 查 (defaultCharacterId, garmentId)
  alt 命中缓存
    S-->>V: 直接排入队列(秒级)
  else 未命中
    S->>S: pickRefDanceVideo() 随机挑一条参考跳舞视频(需求 3.3 第 5 点)
    S->>S: 按 M.15 一步法模板拼装 prompt(人物图+衣服图两张参考图,无 Stage1),实测词数(M.15.1)
    S->>H3: 人物图+衣服图+参考视频一次提交 ref2va(冷启动算力,4.3节)
    H3-->>S: 视频完成
    S->>S: 写入 generated_videos(同时即缓存)
    S-->>V: 排入队列
  end
  Note over S: ensureSchedule 按 v1 投票逻辑排序(priority: normal)

P.2 注册观众:付费传衣服(模式二)

  1. 点"传衣服" → 未登录则先走 P.4 登录/注册。
  2. 选图 → 前端展示"这将消耗 N 积分"(数值待定,8节)。
  3. 确认 → 后端校验余额 → 不足则拦截,引导充值(P.4 充值流程)。
  4. 余额充足 → 预扣积分(credit_ledger 写一条负数流水)→ 图片走轻量 内容审核。
  5. 审核不过 → 全额退还积分(写正数流水,reason: refund_moderation_reject) → 前端提示。
  6. 审核通过 → garments 新建一条 moderationStatus: approved,默认 isActive: true(沉淀进公共库)→ 走一步法生成(M.15;"默认人物+这件 新衣服"缓存必然不命中,因为这件衣服刚创建)→ 完成后排入队列, priority: normal(模式二不享受插队特权,只有模式三新人物才有,见 需求文档 3 节)。
  7. 生成失败 → 退还积分,garments 该条标记不可用/删除(避免衣柜库里 出现一件"存在但没有可用换装产物"的死资产)。

P.3 注册观众:付费传人物(模式三,风险最高的流程)

sequenceDiagram
  participant V as 观众(已登录,已年龄确认)
  participant S as server.js
  participant Blk as 人脸黑名单模型
  participant Fal as fal.ai
  participant Credit as credit_ledger

  V->>S: 选积分档位(决定 remainingSegments)+ 上传人物照 + (可选)选/传衣服
  S->>Credit: 预扣积分
  S->>Blk: 自动人脸识别过滤(5节,非强制人工预审)
  alt 命中黑名单
    S->>Credit: 全额退还
    S-->>V: 拒绝,提示原因
  else 未命中,自动通过
    S->>S: characters 新建一条 source:user_paid, remainingSegments 按 pricing_config(M.12) 换算
    S->>Fal: 一步法生成(若未指定衣服则随机挑一件衣柜库;参考视频由 pickRefDanceVideo 随机挑;付费实时算力,4.3节,约4s级——**需第五期接入 fal 时复核 attribute_transfer 这套 prompt 写法在 fal 的模型上是否同样成立,S1 只验证过 H3 Console**)
    alt 生成失败/超时(含重试耗尽)
      S->>Credit: 全额退还(reason: refund_stage2_fail)
      S->>S: characters 该条置 status: expired,不进排播池
      S-->>V: 提示生成失败、积分已退还
    else 生成成功
      S->>S: generated_videos 新建,priority: paid_boost
      S-->>V: 排入 priorityInbox,插队播放(不参与投票)
    end
  end
  Note over S: 每播完一段,remainingSegments -= 1
  Note over S: 配额耗尽 → characters.status = expired,从排播/复用池移除<br/>但记录保留,用户可在"我的作品"回看,可再充值续播

P.4 登录/注册 + 身份迁移

  1. 触发点:模式二/三的任一付费操作、或主动点"登录"。
  2. 注册 → 写 users(emailVerified: false) + sessions(kind: email_verify) → 发验证邮件 → 用户点链接命中 GET /api/auth/verify → 置 emailVerified: true。未验证期间可以登录、可以投票、可以用免费 模式一,但所有扣积分接口一律拒绝(M.1/M.10),避免一次性邮箱刷 "扣费→失败→退款"这条链路。
  3. 登录成功后,检查是否存在 viewer_uid/ viewer_nickname cookie(v1 遗留身份)——若存在,把该 uid 写入 新账号的 legacyUid 字段,历史投票/点播记录(挂在旧 uid 上的) 通过 legacyUid 关联查询,不做数据搬迁,只做关联(架构文档 2.7 节过渡方案)。同时把匿名期写在 cookie 里的年龄确认合并进 users.ageConfirmed,避免登录后又弹一次。
  4. 后续 uid 仍然写在 cookie 里(免费投票等匿名场景继续用它),但 付费操作一律要求 userId(登录态)。

P.5 充值

  1. 触发点:付费操作余额不足自动弹出,或主动点"我的积分"。
  2. 选充值档位 → 跳转/弹出支付网关(7.3节,渠道待定,标注 Stripe 对 国内支付方式支持有限的风险,见架构文档待验证表)。
  3. 支付成功回调 POST /api/credit/topup/webhook → 写 credit_ledger (reason: topup, delta: +N, paymentRef) → 更新 users.creditBalance。
  4. 支付失败/取消 → 不产生流水,前端提示重试。

P.6 运营/管理员:素材维护 + 事后审核 + 播放干预

  • 日常巡查(对接 5 节"事后人工兜底"):定期查看 moderation_log 里 verdict: auto_pass 但被举报/人工发现有问题的人物内容 → 在 admin-characters.html 手工下架 → 写 admin_takedown 记录。
  • 播放干预:直播过程中运营通过 admin-playback.html 插播/跳过/ 暂停自动填充(6.5节),不需要走观众端的正常提交流程。
  • 素材库维护:定期给 garments/dance_refs 上下架,给 admin_seed 人物/衣服打标签分类。

P.7 系统自动化流程

  1. 冷启动批量生成(第一期,9节):运营在 admin-batch-gen.html (V.2)勾选人物×衣服×参考视频组合 → POST /api/admin/batch-gen 展开成组合逐条入队 → generateDanceVideo(一步法,走内部 H3 Console, 4.3节)→ 进度在 GET /api/admin/batch-gen/status 里逐条可见,失败 条目可单独重投。已有 generated_videos 缓存(M.7 唯一索引)的组合 默认跳过。铺满初始衣柜库/缓存,不涉及观众端。这是第一期唯一的 用户界面,不能只留一个手写脚本。
  2. 排播 tick:dance.html 独立的 ensureSchedule 定时任务(同 v1 每 2s,架构文档 1.3/2.6节),优先消费 priorityInbox(付费插队), 其次按投票消费 freeQueue,都为空时随机填充素材库(同 v1 逻辑, 避开冷却期)。
  3. 人物到期检测:每次消费完 priorityInbox 里某人物的一段后, remainingSegments -= 1,归零则置 characters.status = expired 并从后续排播候选中过滤掉(架构文档 2.7节)。扣减必须与"这一段确实 排进了 schedule"是同一次原子写——JSON 文件方案下用单进程串行写 保证,迁 SQLite 后用事务(架构文档 1.4 节)。

P.8 限流与滥用防护

v1 有按 IP 5 分钟 3 次的提交限流(docs/done 2.7 节),原稿把它漏了, 但 dance.html 的免费入口无需登录、无需积分,不设限等于把生成的 GPU 成本敞口对公网开放。三道闸,从便宜到贵:

闸 位置 规则
黑名单 所有提交类接口入口 查 M.11,命中直接 403,不进后续任何逻辑
去重缓存 /api/dance/pick 命中 generated_videos 缓存(M.7 唯一索引)的提交零成本,不计入限流配额(4.6 节;这是免费入口的主路径,限它没意义)
限流 所有提交类接口 未命中缓存的免费提交按 IP + viewer_uid 双维度计数,沿用 v1 的 RATE_LIMIT_MAX/WINDOW_MS 量级(5 分钟 3 次);付费提交按 userId 计数,额度可以放宽(有积分天然兜底),但仍需要上限防止脚本刷退款链路

另外两条:

  • 上传接口的入站校验(大小/真实文件头/重编码)见 M.13,那是 trust boundary,不能只靠前端。
  • /api/auth/resend-verify 单独限频(同一 userId 60 秒 1 次), 否则是个免费的对外发信接口。