feat(common): 角色卡与动作规格共享 DTO(#152 的前置 · Refs #171) - #172
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Found one compatibility issue in the proposed shared action contract; see the inline finding.
Validation: python3 -m compileall passed for the changed package. Full lint/tests were not runnable because uv and pytest are unavailable in this workspace.
|
转 Draft。本 PR 的 按「PR ready 才请人 review」的第一条(没有要继续追加的改动),当前状态不该占用评审带宽。 |
a8ddb46 to
ec855e9
Compare
There was a problem hiding this comment.
Found one compatibility issue in the shared action contract; see the inline finding. No additional high-confidence issues were identified in the performance, security, or documentation passes. Validation: python3 -m compileall -q backend/packages/common/src passed; the environment does not have pytest installed.
跨层契约,无内部依赖。ai_engine 与 app 均依赖此,避免两层各自定义同名结构。 - CharacterCard 角色身份(一致性主键,资产库基础) - ActionSpec 动作规格(帧数 / 帧率 / 循环模式 / 风格化 / 朝向) - ActionType idle / walk / run / jump / attack / hit - GenRoute video_i2v / per_frame GenRoute 只列有实现的路线。没有实现的枚举值等于死代码:它让调用方以为该能力存在, 而分流到它只能得到运行时错误。未来路线(三渲二渲染出帧)的契约需求记在 1024XEngineer#81 / 1024XEngineer#122, 随实现一起加成员——枚举加成员是纯加法,不构成破坏性变更。 Refs 1024XEngineer#53
本 PR 是 1024XEngineer#152 的前置:它的 `ai_engine/ports` 与 `strategy/concrete` 都 import `windup_common.models` 的 `CharacterCard` / `ActionSpec`,而那两个符号在主线上不存在 (`models/` 下只有 `.gitkeep`),合入即 import 失败。 ## 为什么受限取值一律用枚举 `facing` 承载一条实测挣得的硬约束——提示词朝向必须与母版朝向一致(给正面母版喂侧走词, 模型会靠转身调和图文矛盾)。它此前是裸 str、合法值只写在行尾注释里:写成 "Side" / "sidee" 不报错、不告警,调用链一路放行,几分钟和一次真金白银的视频调用之后才在画面上 看出角色转了身。枚举把这类错误从"生成完靠肉眼发现"提前到"构造 ActionSpec 时 ValidationError",成本从一次付费生成降到零。`loop` / `stylize` / `view` 同理。 `view` 的取值与前端契约(frontend/API_CONTRACT.md 的映射表)逐字一致,免得将来做 int ↔ str 映射时再造一套别名(topdown / top_down / top-down 三写)。 ## extra="forbid" 理由与用枚举同源:字段名也是靠字符串传递的约束。`ActionSpec(action=..., n_frame=16)` (少个 s)在 pydantic 默认的 extra="ignore" 下不报错、不生效,调用方以为要了 16 帧、 实际拿到默认 8 帧。已删字段(如 palette)同理会被静默吞掉。 ## n_frames 独立成字段 原先由 `len(poses)` 推导,但视频路线根本不读 poses——推导意味着"想要 16 帧就得先编 16 条用不上的姿势描述",而那 16 条描述读者会以为真的进了提示词。 只传 poses 的旧调用方零改动(回退到 len(poses));两个都给且不等时抛错而不是猜一个—— common 层看不到 ROUTE_MATRIX,判不出走哪条路线,猜的代价是静默出错帧数。 ## 删掉 palette 它会变成"看起来生效、实则被忽略"的第二真相源:真正锁色的色板由 postprocess.master_pixel_spec 从母版像素里量出来,而这个字段零消费方、无格式约定。 调用方填了 "#1a1a2e,#e94560" 期待锁色,管线照旧用母版色板,不报错也不生效。 将来若要支持用户指定色板,连同消费它的代码一起加回,并用结构化类型而非自由 str。 ## 数值下界抄的是实现里已有的真实取值域 把"实现悄悄纠正入参"提前成入参报错:`pixel_h` → to_pixel_art 对 <1 直接 raise; `palette_size` → `quantize(colors=max(2, palette_size))` 会把 1 静默抬成 2,于是 "我要 1 色"拿到 2 色且无任何提示;`fps` → 0 对播放侧是除零/静止,没有合法语义。 Refs 1024XEngineer#171 Refs 1024XEngineer#53
ec855e9 to
3add033
Compare
机器审在本 PR 报的问题指向同一件事:契约里存在"能填、但填了不生效"的字段。本 PR 自己的 GenRoute docstring 已经写了这条原则("只列有实现的路线;没有实现的枚举值等于死代码"), 只是没对 ActionSpec 执行。 删除: - ActionSpec.fps —— 零写入方(编排层构造 ActionSpec 时从不传),而 postprocess. frame_durations 按动作查表、根本不看它。留着的后果是同一段素材有两个互相矛盾的播放 速度:fps=20 宣称 50ms/帧,walk 实际给 125ms/帧。播放时序的唯一真相源定为出参的 durations(逐帧 ms),比单一帧率严格更能表达(关键帧定格)。 - ActionSpec.loop 与 LoopMode 枚举 —— 零消费方。闭环行为写死在 slicing.pick_cycle: 循环类动作一律抽单周期闭环,传 pingpong / none 不改变任何产出。调用方能为一段往返 动画付费、拿到一段线性循环,正是本项目最忌讳的静默成功。此前的处理是写注释说明"别 指望它生效",并写了一条把"三种 loop 产出相同"钉成事实的测试 —— 那是把缺陷固化,不是 修。真要支持 pingpong,连同 pick_cycle 的分支与出参时序契约一起加回。 补文档: - ActionType docstring 显式声明本枚举是"引擎能生成的动作",与入口侧 orchestrator.model.ActionType(另有 custom,少 run/jump/hit)刻意分离,跨越靠编排层 的显式适配函数 _to_engine_action。不把 custom 加进来的理由与上面同一条:ROUTE_MATRIX 没有它的分流,加成员等于接收一个无法履约的请求。API 入口枚举不变,既有调用方兼容性 不受影响。 补测试(本 PR 此前零测试,而它是其余分片的硬前置): - 新增 tests/test_character_contract.py,35 条,只测 DTO 自身、不 import 上层包 —— 契约包要能独立验证。提示词构造器那几条依赖 ai_engine,随实现分片走。 - 覆盖:受限取值拒绝拼写错误、字段名打错不被静默丢弃(extra="forbid")、n_frames 与 poses 自相矛盾时抛错而非二选一、取值域下界、以及 palette / fps / loop 三个已删字段 传入时必须听得见响。 - 做过变异测试:把 fps 加回去 2 条红,把 loop + LoopMode 加回去 3 条红。
|
这五个 PR 已达 ready:无待追加改动、CI 通过、AI review 意见全部 resolved。可以开始 review。 依赖顺序(已重排为线性 stack,逐级包含前一片的提交): #180 零依赖于前两片的业务逻辑(纯 PIL / numpy + 真实视频实测),想先看小的可以从它入手。 本地已验的三项(每次推送后重跑):
端到端实证:2026-08-11 用这条链路(不是旁路脚本)从零跑通两个全新角色的走路序列帧——文生图出母版 → i2v → 抽帧 → 选帧 → 抠图 → 像素化 → 对齐 → 打包。 三条已知缺陷,代码在本批 PR 内,已独立立项跟踪,不在本批修复:
三条都不影响流程成功与 CI,属品相问题。选择独立跟踪而不是塞进本批,是为了不让改动范围与 Issue 脱节;其中 #197 的可行方向尚未实现也未验证,如实说明。 |
|
@johnnyzhang-eng 也找其他队友 review 下? |
|
好的 |
变更内容
windup_common/models的共享 DTO:CharacterCard、ActionSpec,以及ActionType/GenRoute/Facing/CharacterView/LoopMode/Stylize六个枚举。跨层契约,无内部依赖。为什么先提这个
这是 #152 的前置。 #152 的
ai_engine/ports/__init__.py:15与strategy/concrete.py:16都 importwindup_common.models的CharacterCard/ActionSpec,而主线上models/目录只有.gitkeep——合入即 import 失败。已在 #152 下附可复核位置。设计要点
受限取值一律用枚举。
facing承载一条实测挣得的硬约束——提示词朝向必须与母版朝向一致(给正面母版喂侧走词,模型会靠转身调和图文矛盾)。它此前是裸str、合法值只写在行尾注释里:写成"Side"/"sidee"不报错、不告警,调用链一路放行,几分钟和一次真金白银的视频调用之后才在画面上看出角色转了身。枚举把这类错误从「生成完靠肉眼发现」提前到「构造ActionSpec时ValidationError」。loop/stylize/view同理。CharacterView的取值与frontend/API_CONTRACT.md的映射表逐字一致,免得将来做 int ↔ str 映射时再造一套别名(topdown/top_down/top-down三写)。extra="forbid"。 同源理由:字段名也是靠字符串传递的约束。ActionSpec(action=..., n_frame=16)(少个 s)在 pydantic 默认的extra="ignore"下不报错、不生效,调用方以为要了 16 帧、实际拿到默认 8 帧。n_frames独立成字段,不再由len(poses)推导。视频路线根本不读poses,推导意味着「想要 16 帧就得先编 16 条用不上的姿势描述」,而那 16 条描述读者会以为真的进了提示词。只传poses的旧调用方零改动;两个都给且不等时抛错而不是猜——common 层看不到ROUTE_MATRIX,判不出走哪条路线,猜的代价是静默出错帧数。删掉
palette。 它会变成「看起来生效、实则被忽略」的第二真相源:真正锁色的色板由postprocess.master_pixel_spec从母版像素里量出来,而这个字段零消费方、无格式约定。调用方填了"#1a1a2e,#e94560"期待锁色,管线照旧用母版色板,不报错也不生效。将来要支持用户指定色板时,连同消费它的代码一起加回,并用结构化类型而非自由str。数值下界抄的是实现里已有的真实取值域,把「实现悄悄纠正入参」提前成入参报错:
pixel_h→to_pixel_art对 <1 直接 raise;palette_size→quantize(colors=max(2, palette_size))会把 1 静默抬成 2,于是「我要 1 色」拿到 2 色且无任何提示;fps→ 0 对播放侧是除零/静止。GenRoute只列有实现的路线(video_i2v/per_frame)。没有实现的枚举值等于死代码:它让调用方以为该能力存在,而分流到它只能得到运行时错误。未来路线(三渲二渲染出帧)的契约需求记在 #81 / #122,随实现一起加成员——枚举加成员是纯加法,不构成破坏性变更。关联
Refs #171 · Refs #53 · 是 #152 的前置
本地验证
已 rebase 到当前 main(
1680781)。144 个 warning 全部来自主线既有测试(
starlette.testclient弃用提示、测试用短 HMAC key),与本 PR 无关。待对齐
CharacterCard的字段在ai_engine里目前零读取(视频路线的角色身份由母版图像承载,提示词只取action.facing)。它是为逐帧路线(用desc组提示词)与渲染路线(model_3d_ref,见 #122)预留的入参,已在 ports docstring 写明。是否该现在就精简,听评审意见。