Skip to content

feat(render3d): 三渲二接进编排 - #277

Open
johnnyzhang-eng wants to merge 5 commits into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/render3d-engine
Open

feat(render3d): 三渲二接进编排#277
johnnyzhang-eng wants to merge 5 commits into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/render3d-engine

Conversation

@johnnyzhang-eng

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

Copy link
Copy Markdown
Contributor

三渲二的后半段:引擎契约 + 编排接线 + 分区动量。前半段是 #270(provider 层)。

合并顺序:本 PR 依赖 #270,请在它之后合

本 PR 的代码要用 windup_framework.providers.render3d,那个包由 #270 引入。

为了不让这条依赖把整个后端拖下水,出帧台 provider 的 import 全部改成
函数内延迟 import / TYPE_CHECKING:

位置 处理
strategy/concrete.py 类型标注进 TYPE_CHECKING;RENDER_SIZE 原是类定义期求值的默认参数,改成 size=None + __init__ 内取
orchestrator/render3d_assets.py 三个符号只做类型标注,进 TYPE_CHECKING
orchestrator/executor.py 已是函数内 import,保持

结果:没装 provider 时,i2v / 逐帧两条路线的 import 与运行完全不受影响,
本 PR 自身 CI 也是绿的。真需要 provider 的 2 条用例显式 skip 并打印缺的模块名
(pytest.skip(allow_module_level=True) / importorskip),不静默通过。

核心设计

  1. 路线选择由 server 读 DB 决定,不是引擎判断。 判据只有一条:
    character_data.outfits[].model_3d_url 有没有值。有 → 三渲二;没有 → 照旧 i2v。
    引擎侧因此不提供"能不能用"的预查询 —— 那份数据在 DB 里,让引擎回答它,
    要么引擎去反查存储(破它"只吃 bytes"的分层),要么它只能猜。
  2. CharacterGeneratorPortgenerate_rendered 方法,而不是另立一个 port。
    server 调 ai_engine 只该有一个入口;generate 对应视频截帧,generate_rendered
    对应三渲二,两者共用同一段尾段(帧数对账 → 脚线对齐 → 量成色)。
  3. 三渲二不进 ROUTE_MATRIX 那张表的隐含前提是"路线由动作的物理性质唯一决定",
    对 i2v / 逐帧成立;三渲二还取决于"该造型有没有 3D 资产",硬塞进去会破坏前提。
    所以表的形状不动,这条路线由 server 直接选方法。
  4. 不静默回退。 拿到了 model_3d_url 却下载/渲染失败就报错,不改走 i2v ——
    两条路线的画风、成本、多朝向能力都不同,悄悄换一条会让调用方拿着错误前提做后续决定,
    而帧数、时长、成色全部正常,没有任何一道会红。
  5. 分区动量(limb_motion)。 整幅的 motion_scale 与死帧判据看不见"腿在迈、
    手臂僵成柱子",而那正是自动绑骨漏认一条肢体的典型产物(那块网格没有骨骼驱动,
    每帧同姿势)。只报各区占比 + 最静区名,不给合格线 —— 几何分区区分不了
    "该动没动"和"本来就不该动",判决交给调用方。

本地验证(均在 backend/ 下)

门禁 结果
uv run ruff check . All checks passed
uv run lint-imports Analyzed 110 files, 207 dependencies;2 contracts kept, 0 broken
uv run pytest -q -p no:cacheprovider 463 passed, 2 skipped(skip 的就是缺 provider 那两条)

另外把 #270 的 provider 层临时铺进工作区跑了一遍合并后的状态,确认延迟 import
在 provider 真的存在时也正确、两条 skip 会变成真跑:

489 passed, 0 skipped

(临时文件已全部清除,本分支 diff 不含 #270 的任何文件。)

codecov/patch 会红,原因已定位

48.79% of diff hit (target 86.21%)。缺口全部来自那两条被 skip 的用例。
同样两个状态各跑一次覆盖率,差异只落在三渲二相关模块:

模块 #270 #270
orchestrator/render3d_assets.py 36% 100%
strategy/concrete.py 69% 94%
web/api/generation.py 86% 89%

#270 合入后这项自然转绿,不需要为它补测试。

两处测试改动的说明

  • tests/test_orchestrator_hardening.py:本 PR 给 GenRoute 加了 RENDER_3D,
    而 main 上那条 test_real_generator_assembly_covers_every_declared_route 断言
    每个 GenRoute 成员都被装配。缺 provider 时它必然红 —— 那是缺件不是漏装,
    所以加了一条 importorskip 显式跳过,没有放宽断言feat(providers): 三渲二三段能力的 provider 层 #270 合入后它照常跑。
  • tests/test_master_check_and_quality.py:两条 limb_motion 用例从
    test_render3d_route_and_assets.py 挪了过来。它们只用 PIL 和 slicing.quality,
    与 provider 无关,留在那个文件里会被整体 skip 一并带走 —— 那样本 PR 新增的这项读数
    feat(providers): 三渲二三段能力的 provider 层 #270 合入前一条断言都不跑。

Refs #192 #122 #121

@vercel

vercel Bot commented Aug 13, 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 14, 2026 9:37am

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

fennoai[bot]

This comment was marked as outdated.

引擎契约与 server 编排的后半段。路线选择由 server 读 DB 决定:造型上有
model_3d_url 就调 CharacterGeneratorPort.generate_rendered,没有则照旧走 i2v。
三渲二不进 ROUTE_MATRIX —— 那张表的隐含前提是"路线由动作物理性质唯一决定",
而这条还取决于该造型有没有 3D 资产。

引擎侧新增 generate_rendered 而不另立 port,并补一项分区动量读数:整幅的
motion_scale 与死帧判据看不见"腿在迈、手臂僵成柱子",而那正是自动绑骨漏认
肢体的典型产物。

出帧台 provider 走函数内延迟 import,没装它时 i2v / 逐帧两条路线仍可用。

Refs 1024XEngineer#192 1024XEngineer#122 1024XEngineer#121
这两条只用 PIL 和 slicing.quality,与三渲二 provider 无关。留在
test_render3d_route_and_assets.py 里会被那个文件的整体 skip 一并带走,
于是本 PR 新增的这项读数在 provider 合入前一条断言都不跑。
1024XEngineer#241 合入后 rebase 撞出来的:它给装配表加了一条断言,要求每个 GenRoute 成员都被装配,
而本 PR 新增的 RENDER_3D 依赖 1024XEngineer#270 的 provider 模块——那个模块在这条分支上还不存在,
装配期就 import 会让本来走 i2v 的任务也起不来。

改成装配表里放一个惰性策略:真走到三渲二那条路线时才 import 出帧台依赖。
断言因此仍然成立(表里确实有这个键),而缺 provider 也不影响其余路线。

两个状态都验过:不带 1024XEngineer#270 时 496 passed / 2 skipped;把 1024XEngineer#270 的 provider 层铺进工作区
再跑,591 passed / 14 skipped,惰性策略能真的实例化出 RenderFrameStrategy。

Refs 1024XEngineer#192
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor Author

已同步到含 #270 的最新 main(9b509f9)。之前 codecov 红是因为这个 PR 的 26 个测试整体 skip——它们要 #270 的 provider 层才跑得起来,而 #270 那时还没合。现在全部跑起来了,同步过程中暴露并修了两个测试桩的签名过期。708 passed。

@minorcell

Copy link
Copy Markdown
Member

/review

@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.

审查结论

这组改动已经串起了 DB 选路、模型下载、引擎渲帧和质量读数,但与 #192 已定契约仍有两处关键偏差:多朝向产物被压成单朝向,渲染浏览器仍运行在 FastAPI 进程内。另外,人工驳回模型后当前缓存流程无法真正重新生成。

验证

  • 按固定范围 12793606d44bf3a089d4540c5e1c307cc94e6d25...9b509f9cfab91d9dba6ae889bca28879d17c3751 审阅全部 18 个变更文件。
  • git diff --check 与变更模块 compileall 通过。
  • 当前环境缺少 uv/pytest,未能本地复跑测试;GitHub 的后端、前端与 patch coverage 检查均为通过。

View job run

f"出帧台没有产出朝向 {want}(动作 {action.action.value}、facing "
f"{action.facing.value});实际产出 {available}。"
)
frames = list(chosen.frames)

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.

[P1] 保留全部渲染朝向而不是只返回一个

这里从 sheet.sequences 只取请求朝向的 frames,随后明确丢弃其余序列;同时 server 的 ProjectConstraints.directions(1/4/8)没有传入该策略。结果是四向/八向项目即使 provider 已零成本渲出全部朝向,任务结果仍只有一组平铺帧,无法形成前端 sequences[].direction 契约。这直接违背 #192 的 D18 和验收标准 3,也让三渲二的核心多朝向能力对调用方不可用。请让出参/上传层承载每个方向的独立序列,并按项目方向数请求和持久化;单向调用再保持现有平铺形状。

from windup_ai_engine.strategy.concrete import RenderFrameStrategy
from windup_framework.providers.render3d import LocalSpriteRenderProvider

self._inner = RenderFrameStrategy(LocalSpriteRenderProvider())

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.

[P1] 将浏览器渲染移出 FastAPI 进程

LocalSpriteRenderProviderActionTaskExecutor 的惰性策略里直接实例化并执行,而该 executor 由 app.state.run_action_task 提交给进程内 ThreadPoolExecutor。因此即使队列并发为 1,常驻浏览器、约 1GB 峰值内存和高 CPU 负载仍与 FastAPI 共享进程,浏览器崩溃或资源耗尽会直接影响 API 服务。#192 的 D11 已明确要求部署服务器上的独立 worker、不得与 FastAPI 同进程;请把渲染任务投递到独立 worker/进程边界,再由编排层取回结果。


# 图生 3D 的产物单独存一份。**这不是冗余** —— 待审期间会有第二次、第三次调用走到
# 这里,若不存,每次都要重付一遍图生 3D 的钱,而停点的本意恰恰是省钱。
model = self._store.get(raw_key)

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.

[P2] 驳回模型时清除原始模型缓存

待审模型被判定不合格后,异常文案要求删除待审 .glb 以重新生成,但这里会一直从 raw:{key} 读取首次生成的 bytes。删除 review 目录里的文件后,下一次 ensure() 只会把同一份缓存重新提交,image_to_3d() 永远不会再调用,造型因此无法从一次坏模型中恢复;残留的 .approved 标记还可能放行后续替换的模型。请提供显式 reject/invalidate 操作,同时清除 raw 缓存、待审文件和批准标记,或用模型版本/摘要绑定审批状态。

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.

2 participants