Skip to content

feat(common): 角色卡与动作规格共享 DTO(#152 的前置 · Refs #171) - #172

Merged
xiaocheny214 merged 3 commits into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/character-domain-models
Aug 11, 2026
Merged

feat(common): 角色卡与动作规格共享 DTO(#152 的前置 · Refs #171)#172
xiaocheny214 merged 3 commits into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/character-domain-models

Conversation

@johnnyzhang-eng

@johnnyzhang-eng johnnyzhang-eng commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

变更内容

windup_common/models 的共享 DTO:CharacterCardActionSpec,以及 ActionType / GenRoute / Facing / CharacterView / LoopMode / Stylize 六个枚举。跨层契约,无内部依赖。

为什么先提这个

这是 #152 的前置。 #152ai_engine/ports/__init__.py:15strategy/concrete.py:16 都 import windup_common.modelsCharacterCard / ActionSpec,而主线上 models/ 目录只有 .gitkeep——合入即 import 失败。已在 #152 下附可复核位置。

设计要点

受限取值一律用枚举。 facing 承载一条实测挣得的硬约束——提示词朝向必须与母版朝向一致(给正面母版喂侧走词,模型会靠转身调和图文矛盾)。它此前是裸 str、合法值只写在行尾注释里:写成 "Side" / "sidee" 不报错、不告警,调用链一路放行,几分钟和一次真金白银的视频调用之后才在画面上看出角色转了身。枚举把这类错误从「生成完靠肉眼发现」提前到「构造 ActionSpecValidationError」。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_hto_pixel_art 对 <1 直接 raise;palette_sizequantize(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)。

uv run ruff check .   All checks passed!
uv run lint-imports   Contracts: 2 kept, 0 broken.
uv run pytest -q      76 passed

144 个 warning 全部来自主线既有测试(starlette.testclient 弃用提示、测试用短 HMAC key),与本 PR 无关。

待对齐

CharacterCard 的字段在 ai_engine 里目前零读取(视频路线的角色身份由母版图像承载,提示词只取 action.facing)。它是为逐帧路线(用 desc 组提示词)与渲染路线(model_3d_ref,见 #122)预留的入参,已在 ports docstring 写明。是否该现在就精简,听评审意见。

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 10, 2026 2:00pm

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread backend/packages/common/src/windup_common/models/character.py
@johnnyzhang-eng
johnnyzhang-eng marked this pull request as draft August 10, 2026 03:34
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor Author

转 Draft。本 PR 的 windup_common/models/character.py 正在按 #171 待议节的 D1/D2/D3 做契约收紧(受限取值改枚举、n_frames 独立成字段、删除零消费方的 palette),改完会更新本 PR 再转回 Ready。

按「PR ready 才请人 review」的第一条(没有要继续追加的改动),当前状态不该占用评审带宽。

@johnnyzhang-eng
johnnyzhang-eng force-pushed the feat/character-domain-models branch from a8ddb46 to ec855e9 Compare August 10, 2026 06:53
@johnnyzhang-eng johnnyzhang-eng changed the title feat(common): 角色卡与动作规格共享 DTO(Refs #171 #53) feat(common): 角色卡与动作规格共享 DTO(#152 的前置 · Refs #171) Aug 10, 2026
@johnnyzhang-eng
johnnyzhang-eng marked this pull request as ready for review August 10, 2026 06:53

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread backend/packages/common/src/windup_common/models/character.py
跨层契约,无内部依赖。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
@johnnyzhang-eng
johnnyzhang-eng force-pushed the feat/character-domain-models branch from ec855e9 to 3add033 Compare August 10, 2026 10:24
机器审在本 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 条红。
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor Author

这五个 PR 已达 ready:无待追加改动、CI 通过、AI review 意见全部 resolved。可以开始 review。

依赖顺序(已重排为线性 stack,逐级包含前一片的提交):

#172 共享契约  →  #179 Provider 与抠图  →  #180 出帧工具箱  →  #181 引擎契约与串联  →  #182 任务编排

#180 零依赖于前两片的业务逻辑(纯 PIL / numpy + 真实视频实测),想先看小的可以从它入手。

本地已验的三项(每次推送后重跑):

  • 按依赖顺序合并 main → #172 → #179 → #180 → #181 → #182 五步全干净
  • 逐分支 CI 原样命令全过,测试数 111 → 185 → 266 → 307 → 337 单调递增
  • 全部合入后应用可启动,app.openapi() 口径 29 条路由,与 main 一字不差(零新增零删除)

端到端实证:2026-08-11 用这条链路(不是旁路脚本)从零跑通两个全新角色的走路序列帧——文生图出母版 → i2v → 抽帧 → 选帧 → 抠图 → 像素化 → 对齐 → 打包。


三条已知缺陷,代码在本批 PR 内,已独立立项跟踪,不在本批修复:

三条都不影响流程成功与 CI,属品相问题。选择独立跟踪而不是塞进本批,是为了不让改动范围与 Issue 脱节;其中 #197 的可行方向尚未实现也未验证,如实说明。

@johnnyzhang-eng
johnnyzhang-eng requested a review from nighca August 11, 2026 05:48
@nighca

nighca commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

@johnnyzhang-eng 也找其他队友 review 下?

@johnnyzhang-eng

Copy link
Copy Markdown
Contributor Author

好的

@xiaocheny214 xiaocheny214 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看起来没什么问题

@xiaocheny214
xiaocheny214 merged commit fbb86e6 into 1024XEngineer:main Aug 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants