Commit 92ffd6ee2ca993b051be02b5ddb18eac1e2f45b5

Authored by yibo
1 parent 85c67307

Plan fifth-phase tasks: mode 3, face blacklist, priority queue, fal.ai, my-works (T31-T39)

Breaks down dressup-dance-room.md 9节第5条 (paid character upload,
face-blacklist moderation, character lifecycle/quota, priority
scheduling for paid_boost content, fal.ai real-time compute, "我的
作品" page, playback management) into T31-T39 with acceptance
criteria, estimates, and a dependency graph, matching dev-plan.md
through dev-plan-4.md's format.

Flags one thing worth being upfront about: T27 (mode 2, completed in
the prior phase) never actually integrated fal.ai despite dev-plan-4.md's
own T27 row calling for it — it still runs entirely through H3 Console.
No FAL_API_KEY has ever been configured, so there was no credential to
build or test a real fal.ai path against; T27 shipped a fully working
H3 Console path instead, same call as T24 did with Stripe. T33 folds
paying off that debt into this phase (mode 3 needs the same fal.ai
integration anyway, so building it twice would be wasted work), with
the same "build the plumbing, real key unlocks real verification"
shape as T24.

Also asks the user a real question rather than defaulting silently:
T26's "no vendor configured -> auto-pass" precedent doesn't transfer
to face-blacklist review of real uploaded faces the way it did for
garment photos — the risk profile is different, so T32's default
policy when unconfigured is left for the user to decide rather than
assumed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
Showing 1 changed file with 169 additions and 0 deletions
docs/todo/dev-plan-5.md 0 → 100644
  1 +# 换装跳舞直播间 — 第五期开发任务规划
  2 +
  3 +需求见 `docs/todo/dressup-dance-room.md`(下称"需求文档")第 3.3 节模式
  4 +三、第 5 节内容审核、第 7.4 节"我的作品"、第 9 节第 5 条;设计见
  5 +`docs/todo/dressup-dance-room-mvcp.md`(下称 MVCP)M.3/M.7/M.8/M.9/M.11/
  6 +M.13;架构见 `docs/structure/architecture.md`(下称架构文档)2.3/2.6/2.7
  7 +节。第一(T1-T10)、第二(T11-T17)、第三(T18-T22)、第四期(T23-T30)
  8 +均已完成并提交。任务编号接着往下编。
  9 +
  10 +工时是**粗估**,单位人天,用来排先后不用来考核。
  11 +
  12 +---
  13 +
  14 +## 0. 第五期范围(需求文档 9 节第 5 条)
  15 +
  16 +**做**:付费传人物(模式三)+ 自动人脸黑名单审核 + 人物生命周期(购买
  17 +N 个 10 秒片段 / 播完自动下线 / 记录保留可续播)+ 排播优先级改造(付费
  18 +新人物插队播放,不参与投票)+ 个人中心"我的作品"页面 + 接入 fal.ai
  19 +实时算力路径(4.3 节双算力策略,**这一条同时补上第四期 T27 遗留的一个
  20 +缺口**——见下方 §0.1)+ 后台播放管理增强(插播/置顶/跳过/暂停填充/
  21 +拉黑)。
  22 +
  23 +### 0.1 一个需要先说清楚的事:T27(模式二)从未真正接入 fal.ai
  24 +
  25 +`dev-plan-4.md` 当时的 T27 验收标准里写了"fal.ai 路径:图片先转发到 fal
  26 +可达的存储…再提交生成",但实际完成的 T27 直接复用了 `submitGeneration`
  27 +——**这条路径始终只走 H3 Console,没有走 fal.ai**。也就是说现在付费
  28 +传衣服(模式二)用户跟免费衣柜用户排的是同一条约 700s 的生成队列,
  29 +没有拿到需求文档 4.3 节承诺的"付费=fal.ai 实时速度(约 4s 级别)"。
  30 +
  31 +原因:`.env` 里从未配置过 `FAL_API_KEY`(fal API key 目前仍只在
  32 +`newtask.html` 前端手填,架构文档 1.2 节早就点名这是技术债),没有凭证
  33 +就没法真正调用/验证 fal.ai。这不是"忘了写",是"当时没有可用凭证,选
  34 +了功能完整但慢的路径先跑通"——跟 T24 处理 Stripe 缺 key 时的思路一致:
  35 +先用可工作的路径把主链路跑通,快慢是后续优化。
  36 +
  37 +本期把这条债务一起还上(T33),而不是留到"第六期"——因为模式三本身
  38 +也需要走 fal.ai(同一份需求 4.3 节),两条付费路径的实时算力接入天然
  39 +应该一起做,不该再拖一次。
  40 +
  41 +**同样的约束仍然存在**:`.env` 里没有 `FAL_API_KEY`。T33 会把
  42 +`forwardToFalAssets`/`submitGeneration` 的 fal.ai 分支代码写完,但**真正
  43 +验证需要你提供 fal.ai 的 API key**(跟 T24 等 Stripe key 是同一种情况)
  44 +——没有 key 之前,T27/T35(模式二/三)会继续退化到走 H3 Console(能跑,
  45 +只是慢),不阻塞开发。
  46 +
  47 +**明确不做**(留到之后再评估,不是本期范围):
  48 +
  49 +- 支付渠道从 Stripe 拓展到支付宝/微信支付(需求 7.3 节:"需要时再加一
  50 + 个入口即可",本期不预先做)。
  51 +- "我的作品"页面的第三方实名认证(需求 §8 第 7 条,明确说"看实际运营
  52 + 风险再评估,不预设一定要做")。
  53 +- 参考跳舞视频/配乐正式版权素材替换(需求 §8 第 3 条,运营层面的素材
  54 + 采购,不是开发任务)。
  55 +
  56 +---
  57 +
  58 +## 1. 已确认的技术决策 + 需要你决定的开放问题
  59 +
  60 +### 1.1 已确认
  61 +
  62 +- **人物生命周期字段 schema 第一期就建好了**(T1,characters 表的
  63 + `totalSegments`/`remainingSegments`/`status`,见 MVCP M.3),本期不需要
  64 + 改 schema,只是第一次真正用上非 null 的取值。
  65 +- **排播优先级走 M.8 定义的 `priorityInbox` 通道**:`ensureDanceSchedule`
  66 + 每个 tick 优先消费 `priorityInbox`(`paid_boost` 产物),消费完/为空
  67 + 才回落到现有"`danceQueue` 里挑最高票 → 随机去重填充"逻辑(架构文档
  68 + 2.6 节已确认,本文档只是落地)。**这是对现有选片算法的新增分支,不
  69 + 是重写**——`pickBestIndexByStatus` 那条投票排序路径完全不动,模式一
  70 + /二的免费内容继续走它。
  71 +- **`blacklist`(M.11)和"人脸黑名单"是两张概念上独立的东西**:前者是
  72 + 运营手工拉黑的本站实体(用户/资产/IP),后者是模型侧对上传照片本身
  73 + 的自动识别过滤(moderateFaceBlacklist,T32)。两者都要做,但互不
  74 + 替代——见 MVCP M.11 的警告。
  75 +- **`credit_ledger`/`pricing_config` 复用第四期的机制不改**:模式三的
  76 + `spend_character_per_segment` 走 `pricing.json` 里 M.12 早就定义好的
  77 + `segmentsPerUnit` 字段(扣了多少积分换算成几个 10 秒片段),落
  78 + `credit_ledger` 的方式跟模式二完全一样,T23 的 `spend`/`refund`/
  79 + `topup`/`adjust` 四个函数不需要再加新方法。
  80 +- **fal.ai 分支是"有 key 就用,没 key 就退化",不是新建一套并行系统**:
  81 + `submitGeneration` 加一个内部路由:付费请求(模式二/三)且
  82 + `FAL_API_KEY` 已配置 → 走 fal;其余情况(免费模式一、或付费但没配
  83 + key)→ 走现有 H3 Console 路径。调用方(T27 现有代码、T35 新代码)不
  84 + 用感知这条路由逻辑,同 T20/T24 的可插拔函数设计一脉相承。
  85 +
  86 +### 1.2 需要你决定,但不阻塞开工
  87 +
  88 +| # | 开放问题 | 现在怎么办 |
  89 +|---|---|---|
  90 +| 1 | **人脸识别黑名单供应商**(需求文档 §8 第 4 条,"用什么模型/服务做涉政等敏感人物识别过滤,需要调研候选方案"——目前完全没有候选) | **这条我想先跟你确认默认策略,而不是直接照抄 T26 的"无供应商时默认放行"**:T26 审核的是衣服图片(低敏感度素材,误放行的后果很小);这里审核的是真人人脸+"涉政人物"过滤,误放行的后果是被过滤对象的形象直接进入生成并可能公开播放。默认放行 vs 默认更保守(比如未配置供应商时直接拒绝所有人物上传,模式三先不接受新用户上传,只能等供应商定了再放开)是两种完全不同的产品/风险取舍,我不该替你做这个决定——**见下面的问题**。等你确认后 T32 的可插拔函数按你选的默认值实现 |
  91 +| 2 | **fal.ai API key**(`FAL_API_KEY`) | 同 T24 处理 Stripe key 的方式:T33 把 fal.ai 集成代码写完(`forwardToFalAssets`、`submitGeneration` 的 fal 分支),**真正调用一次 fal.ai 验证需要你提供 key**;没有 key 时模式二/三继续退化到 H3 Console(能跑,只是慢,不阻塞其余任务) |
  92 +| 3 | **一步法 prompt 在 fal 模型上是否同样成立**(需求文档 §8 第 1 条历史遗留问题:"S1 的一步法/`attribute_transfer` 写法只在 H3 Console 上验证过,接入 fal 时需要先复核") | 这条**必须**拿到 #2 的 key 后才能真的验证(需要真实调用 fal 的 `minimax/h3*` 端点跑一次换装跳舞视频,对比 H3 Console 的产出)——T33 会把这条验证步骤写进验收标准,但没有 key 就没法执行,不是代码能自己解决的 |
  93 +| 4 | **模式三的具体积分数值**(充值多少积分对应几个 10 秒片段,`pricing.json` 的 `segmentsPerUnit`) | 跟 T23 处理 `spend_garment` 一样:先填占位数值(比如 `segmentsPerUnit: 1` 表示每份 `spend_character_per_segment` 积分买一个片段),机制打好,数值上线前再改 |
  94 +
  95 +---
  96 +
  97 +## 2. 依赖关系
  98 +
  99 +```mermaid
  100 +flowchart TD
  101 + T31[T31 人物生命周期字段落地] --> T36[T36 播放消耗配额+到期下线]
  102 + T32[T32 人脸黑名单审核] --> T35[T35 模式三后端]
  103 + T33[T33 fal.ai 集成] --> T35
  104 + T33 --> T27fix[回填 T27: 模式二走 fal 分支]
  105 + T34[T34 排播优先级 paid_boost/priorityInbox] --> T35
  106 + T31 --> T35
  107 + T35 --> T36
  108 + T35 --> T37[T37 dance.html 传人 tab 接线]
  109 + T35 --> T38[T38 我的作品页面]
  110 + T39[T39 播放管理增强 + 后台拉黑]
  111 +
  112 + classDef ready fill:#d4edda,stroke:#28a745
  113 + class T31,T32,T33,T34,T39 ready
  114 +```
  115 +
  116 +T31/T32/T33/T34/T39 互不依赖,可以完全并行开工;T35 是模式三的核心,
  117 +依赖前四者都好(桩/占位实现可以先联调,不必等真实供应商/key);T36
  118 +依赖 T31+T35;T37/T38 依赖 T35 的接口稳定。
  119 +
  120 +---
  121 +
  122 +## 3. 任务表
  123 +
  124 +| # | 任务 | 依赖 | 验收标准 | 工时 |
  125 +|---|---|---|---|---|
  126 +| T31 | **人物生命周期字段落地**:`characters` 表 `totalSegments`/`remainingSegments`/`status` 从"建好没用"变成"真的读写"——admin.html 人物库列表新增这三列的只读展示(运营排查用);`store.js` 层不需要改,字段已经在 | 无 | 现有 `admin_seed` 人物这三个字段继续是 `null`/`active`(不受影响);能手动构造一条 `totalSegments: 3, remainingSegments: 3, status: "active"` 的人物记录并在 admin.html 正确显示 | 0.5 |
  127 +| T32 | **人脸黑名单审核**:`moderateFaceBlacklist(imagePath)` 可插拔函数(同 T26 `moderateGarmentImage` 的接口形状),默认策略按你在 §1.2 问题 1 的回答实现;每次调用写一条 `moderation_log`(`assetType: character_upload`) | 你对 §1.2 问题 1 的回答 | 每次调用都落 `moderation_log`;未配置供应商时的行为跟你选定的默认策略一致(不是我自己假设的"默认放行") | 1 |
  128 +| T33 | **fal.ai 集成**:`forwardToFalAssets(absPath)`(同 T2 `forwardToH3Assets` 的思路,但目标是 fal 自己的存储/签名机制——需要先读 fal.ai 文档确认它接受本地文件转发的具体方式,不能假设跟 H3 Console 的 `/v1/assets` 一样);`FAL_API_KEY` 从 `newtask.html` 前端手填迁移到服务端 `.env`(架构文档 1.2 节技术债);`submitGeneration` 加路由分支:付费请求 + key 已配置 → fal,否则 → 现有 H3 Console 路径 | 无(但真实验证依赖 §1.2 问题 2 的 key) | 无 key 时:所有生成请求行为跟现在完全一致(H3 Console,回归测试见 T27/dance-channel 的既有测试不能红);有 key 时(**需要你提供后才能跑这一条**):一次真实 fal 调用产出的换装跳舞视频人脸/换装/动作三项跟 H3 Console 产出比对合格(§1.2 问题 3),且落库 `backend: "fal"` | 2 |
  129 +| T34 | **排播优先级改造**:`dance-state.json` 加 `priorityInbox`(M.8),`ensureDanceSchedule` 每个 tick 优先消费它(不经过 `pickBestIndexByStatus` 投票排序),消费完/为空才回落现有逻辑;`generated_videos`(library.json clip)加 `priority: normal \| paid_boost` 字段 | 无 | 写一条 `priorityInbox` 记录,下一个 tick 必须先播它,即使 `danceQueue` 里有票数更高的免费内容在排队;现有模式一/二的投票排序行为不受影响(回归:既有 dance-channel.test.js 不能红) | 1 |
  130 +| T35 | **模式三后端**:`POST /api/dance/upload-character`(需登录+`emailVerified`,同 T27 鉴权规则)——人物照走 T2 上传+EXIF清理 → T32 人脸黑名单审核 → 不通过直接 400 且不扣积分 → 通过则按 `pricing_config` 的 `spend_character_per_segment`×`segmentsPerUnit` 换算 `totalSegments`(需求 3.3 第 3 点,充多少积分买几个片段)→ T23 扣积分 → 自动打标(复用 T25 `describeGarmentImage` 的接口模式,新增 `describeCharacterImage`)→ `characters.create({source:"user_paid", ownerUserId, totalSegments, remainingSegments: totalSegments, status:"active", ...})` → 衣服可选(body 带 `garmentId` 就用它,否则随机挑一件衣柜库服装,需求 3.3 第 4 点)→ 参考跳舞视频固定随机(3.3 第 5 点,复用现有 `pickRefDanceVideo`)→ 提交生成(T33 fal 优先,回退 H3)→ 产物 `priority: "paid_boost"` 直接进 T34 的 `priorityInbox`,不进投票队列 → 生成失败退还积分(同 T27 的 `refund_stage2_fail` 套路) | T31, T32, T33, T34 | 提交一张人脸照能走完整条链路,产出的人物 `totalSegments` 跟花的积分对得上(改改 `pricing.json` 数值验证换算逻辑不是写死的);人脸黑名单拒绝的照片不创建 `characters` 记录、不扣积分;生成完成后的产物直接进下一个 tick 播放(插队,不用等投票);故意让生成失败能验证到积分退回 | 2 |
  131 +| T36 | **播放配额消耗 + 到期下线**:`ensureDanceSchedule`(或 `markPlayed`)播放一条 `characterId` 对应 `source:"user_paid"` 人物的 clip 时,该人物 `remainingSegments -= 1`;归零时 `status` 置 `"expired"` 并从排播候选池移除(`pickNextDanceClip`/去重缓存查询都要跳过 `expired` 人物),但**记录/已生成视频不删除**(需求 3.3 第 7 点,架构文档 2.7 节) | T31, T35 | 播放 N 次后(N = 该人物 `totalSegments`)人物自动从排播池消失,后续免费/其他付费请求不会再抽中它;`characters.json` 里这条记录和它名下的 `generated_videos` 全部还在,只是 `status: expired`;再充值续播(T38 之外的后续能力,这里只要求"已生成视频还能被找到复用,不需要重新生成"这条底线成立) | 1 |
  132 +| T37 | **`dance.html`"传人" tab 接线**(当前占位"开发中敬请期待",同 T28 套路):未登录/未验证先引导登录;选图 + 可选衣服 + 展示"充值 N 积分 = M 个 10 秒片段"换算提示(读 T33 前实现的 `/api/pricing`)→ 二次确认 → 调 T35 → "生成中"状态展示 | T35 | 未登录点"传人"先弹登录;确认框显示的换算跟 `pricing_config` 一致;提交后能看到生成中状态,完成后**直接插播**而不是进普通排队列表(跟模式一/二的"进队列"视觉上要有区分,用户能感知到自己是付费插队) | 1.5 |
  133 +| T38 | **"我的作品"个人中心页面**(需求 7.4 节):新页面(如 `my-works.html`)展示当前登录用户名下的 `user_paid` 人物/衣服及其生成视频,`status: expired` 的人物依然可见可查(不是软删除后不可访问,需求 7.4 节明确要求) | T35 | 登录用户能看到自己上传的人物/衣服列表;某个人物片段耗尽变 `expired` 后,仍能在这个页面看到它和它的历史视频;未登录访问跳去登录 | 1.5 |
  134 +| T39 | **播放管理增强 + 后台拉黑**(需求 6.5 节):`admin.html`/`dance.html` 后台加"插播/置顶下一条""手动跳过当前""暂停自动填充"控制;新增 `POST /api/admin/blacklist`(M.11,`targetType: user\|character\|garment\|generated_video\|ip`),`/api/dance/pick`、两个 upload 接口入口先查这张表命中即拒绝,已在库资产被拉黑后从排播候选/衣柜列表过滤(不删除记录) | 无 | 管理员能把某条排队内容置顶为下一条播放;能跳过当前播放;能暂停"票数不够时随机填充"这条兜底逻辑;拉黑一个 `garmentId` 后它立刻从 `/api/dance/wardrobe` 消失、`/api/dance/pick` 对它返回拒绝 | 1.5 |
  135 +
  136 +**总工时约 10.5 人天**。T31/T32/T33/T34/T39 可以完全并行(约 6 人天),
  137 +关键路径是 T32+T33+T34→T35→T36/T37/T38,约 6.5 人天。
  138 +
  139 +---
  140 +
  141 +## 4. 第五期出口
  142 +
  143 +- 第一~四期全部行为不受影响(免费模式一、付费模式二、账号、积分/
  144 + 充值继续照常工作);
  145 +- 上传一张人脸照片能走完整条模式三链路:人脸黑名单审核 → 按积分换算
  146 + 片段数扣费 → 自动打标 → 生成 → **插队播放**(不经过投票);
  147 +- 人物播完购买的片段数后自动下线,记录/视频保留,用户能在"我的作品"
  148 + 页面继续查看;
  149 +- fal.ai 真实凭证到位后(你提供 key),模式二/三的付费请求实测走
  150 + fal.ai 实时路径,等待时间从 H3 Console 的约 700s 降到 fal 的约 4s
  151 + 级别(这条是"到位后验证",key 不到位不算出口条件未达成——跟 T24
  152 + 处理 Stripe 一样,功能链路完整跑通即算完成,真实供应商验证是独立
  153 + 的收尾步骤);
  154 +- 管理员能手工拉黑用户/资产/IP,命中的请求在提交入口直接拒绝;能插播
  155 + /跳过/暂停自动填充。
  156 +
  157 +达成后第五期即完成需求文档 9 节列出的全部五期范围;后续(人脸识别真实
  158 +供应商选型落地、参考视频版权素材替换、支付渠道拓展、"我的作品"实名
  159 +认证)都是运营/采购/风险评估层面的收尾,不再是本项目待开发的功能缺口。
  160 +
  161 +---
  162 +
  163 +## 5. 明确不在第五期范围
  164 +
  165 +- 支付宝/微信支付等 Stripe 之外的支付渠道;
  166 +- "我的作品"页面的第三方实名认证;
  167 +- 参考跳舞视频/配乐正式版权素材采购与替换(运营任务,后台字段已预留);
  168 +- 多人物同时设为"直播默认人物"(第一期已确认的"全库最多一条"限制不
  169 + 在本期重新评估)。