Skip to content

feat: ai_engine 视频路线真实现落地(Refs #53 #35) #171

Description

@johnnyzhang-eng

背景

ai_engine#53 落了空骨架(ports 契约 + DerivationStrategy 分流 + 串联),该 Issue 验收标准第 4 条写明:

内部真实实现不在本 Issue —— 另开开发 Issue,迁 windup-pipeline,带测试。

本 Issue 即该开发 Issue。实现来自个人管线仓的实测通路(定妆母版 → 图生视频 → 抽帧 → 抠图 → 像素化 → 对齐 → 序列帧),本次迁入主仓并补齐测试。

main 上现状:ai_engine.gitkeep 外无任何实现;windup_common/models 为空;windup_framework/providers 只有三个 create_*_client 工厂,无可供上层依赖的抽象类型。

实现范围

backend/pyproject.toml 的 import-linter 分层契约自下而上切 4 个 PR,每个 PR 自身 CI 可绿、不依赖未合分支。

PR 内容 依赖 规模 用例
1 windup_common 角色卡 / 动作规格 / 生成路线枚举 2 文件 +78
2 windup_framework 三个 Provider Protocol + 抠图 + 视频/图像实现 8 文件 +792 +7
3 windup_ai_engine 叶子 抽帧 / 选帧 / 后处理 / 提示词 19 文件 +1550 +17
4 windup_ai_engine 契约层 ports / strategy / impl PR 1·2·3 35 文件 +2994 +9

PR 1·2·3 互不依赖,可并行评审。PR 4 stack 在三者之上,合并后 rebase。

实现内容

1. 共享 DTO(windup_common/models

  • CharacterCard — 角色身份(name / desc / palette / view / master_ref / version)
  • ActionSpec — 动作规格(action / fps / loop / poses / stylize / pixel_h / palette_size / facing)
  • ActionType — idle / walk / run / jump / attack / hit
  • GenRoute — video_i2v / per_frame(只列有实现的路线,理由见 Q4)

2. Provider 抽象与实现(windup_framework/providers

  • interfaces.pyImageProvider / VideoProvider / MatteProvider 三个 Protocol,零依赖,供上层按能力而非按厂商声明依赖
  • matte.pyOnnxU2NetMatteProvider:onnxruntime 直跑 u2netp。不用 rembg:其依赖链 pymatting → numba 0.53 / llvmlite 0.36 在 Python 3.12 无轮子;rembg 内核本身就是 u2netp 过 onnxruntime,alpha_matting=False 时不碰 pymatting
  • matte.pyGreenScreenMatteProvider:绿幕抠图,背景色由调用方显式给出。附带溢色抑制(绿幕反光在主体边缘留一圈绿,实测占主体像素 7.1%,送进图生 3D 会被烤进贴图)
  • sufy.py — 图生视频 / 文生图 provider

3. 出帧工具箱(windup_ai_engine slicing / postprocess / prompt / master_prep)

零 windup 依赖,只用 PIL + numpy,可独立测试。

  • slicing/extract 解码;slicing/loop 循环类动作抽单步态周期;slicing/oneshot 一次性动作裁区间;slicing/quality 帧质量诊断
  • postprocess/pixelate 母版是像素画时吸附母版网格 + 锁母版色板,否则通用量化;postprocess/pack 脚线对齐 / 图集 / GIF;postprocess/rootmotion 逐帧时长
  • master_prep 按动作预处理母版

4. 对外契约与分流(windup_ai_engine ports / strategy / impl)

  • CharacterGeneratorPort.generate(card, action, master, progress) -> GeneratedAction,server 只 import ai_engine.ports,由 import-linter 强制
  • ROUTE_MATRIX 动作类型 → 生成路线
  • CharacterGenerator 选路线 → strategy.derive 出帧 → 脚线对齐 → 出参

边界(与作者对齐)

ai_engine 只产出帧 bytes + 逐帧时长,不碰存储 / 数据库 / 任务状态。母版由 server 从 Character.reference_image_url 取好以 bytes 传入;产出的帧由 server 上传对象存储、写 character_data。故本层无 ArtifactStorePort

生成路线矩阵(架构契约)

动作 路线 状态 依据
walk / run VIDEO_I2V 实测通 逐帧独立生成锁不住"哪条腿在前"→ 踢踏舞;视频天生连贯、腿自然交替
jump / attack VIDEO_I2V 实测通 同上;但属一次性动作,抽帧不闭环,走 pick_oneshot
idle VIDEO_I2V 实测通 见下方与 #53 的差异 ①
hit PER_FRAME 未实现,调用即抛错 离散姿势,单帧可编辑价值高、无连续步态

改此矩阵 = 改产线,需实测支撑。依据见 #35

#53 原设计的三处差异

# #53 原设计 现状 依据
idle → PROC_IDLE(¥0 程序化局部呼吸 Idle-B) idle → VIDEO_I2VPROC_IDLE 枚举与 ProcIdleStrategy 一并移除 程序化呼吸做不出可用效果,放弃,认这份 i2v 的钱
抠图 = rembg onnxruntime 直跑 u2netp rembg 依赖链在 3.12 无轮子;同模型同质量
视频 = kling-video-o1,v2-5-turbo / v2-1 已下架 v2-5-turbo 未下架 2026-07-27 复测收回该结论

关键缺陷修复

问题 影响 修复
未实现的路线返回 [b""] * n_frames 调用方拿到帧数对、时长对、无异常的 GeneratedAction——完全像一次成功的生成。server 会把 N 个 0 字节文件传上对象存储并写入 character_data,用户看到 N 张裂图,排查时不会想到是路线没实现 边界上抛错:strategy 调用即抛 NotImplementedError;装配表缺该路线时抛错并报出已装配了哪些;strategy 吐出空帧时抛 ValueError
抠图 provider 在 onnxruntime 缺失时静默降级为"取四角主色 chroma-key" 白底母版四角即白色,浅色角色(骨白 / 银甲)与背景撞色被抠穿;且静默——开发机上看着能跑、输出实际是坏的 改为抛 RuntimeError;绿幕抠图另立 provider,背景色显式给出,不猜
align_bottom_center 三条缩放分支只按高度定标 横向长条主体缩放后宽度超出画布,被 alpha_composite 以负 dest 静默丢像素,PIL 不报错。裁切悬崖 w/h ≈ 1.61;实测某四足角色母版 w/h=1.78 丢 27px(鼻尖 + 尾尖),w/h=2.0 只剩 79.9% 内容 加宽度兜底 fill_w=0.96。人形 w/h 0.3–1.1 时该约束恒不生效,产物逐像素不变
步态周期误检(三个坑,见下) 半周期闭环 = 末帧接回首帧时左右腿瞬间互换 见下
视频成品下载单次读取、无重试、不校验长度 该步发生在提交任务、轮询、等待全部成功之后,费用已产生、视频已生成,连接断一次整单作废。实测同一角色连续两单死在此处各烧一次费用 三次退避重试 + 长度校验(#129,已单独修复并合入 upstream 开发分支,本次随 PR 2 迁入)

周期检测:三个坑与实测

坑 1|平移偏置。 角色在画面里横向位移,d(p) 被"挪了多远"主导、随 p 单调上升,真周期的凹陷被抬平,argmin 滑到搜索窗边界交出假周期。
实测某走路视频(源 121 帧,搜索窗 p ∈ [16,72]):未消平移时曲线 40/56 段在上升,只剩 22 / 42 / 52 三个浅坑,argmin = 22;加 _deskew 消整体平移后,整条曲线只剩一个局部极小,正是真周期 56(凹陷深度 2.68)。

坑 2|搜索窗过窄。 上界 n//2 把真周期挡在窗外(某待机视频真周期 62 > pmax 60)。改为 total*0.6

坑 3|谐波。 平移偏置让 d(p) 偏爱短 lag,常选中真周期的约 1/2。改为在基周期整数倍里按归一化接缝(末→首帧差 ÷ 组内相邻帧差均值)复选,并优先取最小倍数——倍数越大 = 一个 loop 里塞进越多周期 = 每周期帧数越少 = 动作变糙。

无周期时不硬闭环。 凹陷深度 < 0.25 判"测不到可信周期",退化成全片均匀取。
实测某待机视频(源 31 帧,搜索窗 p ∈ [6,18]):未消平移时曲线单调,argmin 落在搜索窗下界 6,交出纯粹的边界假值;消平移后 p=11 虽是局部极小,但其值 14.010 夹在 14.013 与 14.023 之间,凹陷深度 0.006,被门槛挡掉。

四段真视频实测(n=16;接缝越接近 1 越闭合)

视频 旧 · 接缝 旧 · 逐像素重复帧 新 · 接缝 新 · 重复帧
走路 A 3.07 0 1.96 0
走路 B 1.29 0 0.87 0
待机 9.31 16 1.39 0
奔跑 1.77 0 0.81 0

待机一行:旧算法写出的 GIF 只有 6 帧——16 帧里 10 帧逐像素重复,被 PIL 自动去重。

消融。 改善全部来自 _deskew + 谐波复选。另行追加的「死帧避让 + 冻结裁剪」两条,两个样本无变化、两个变差(奔跑接缝 0.81 → 2.00),已回退quality.py 保留作诊断,不进选帧。

测试覆盖

文件 用例 覆盖
test_ai_engine_skeleton.py 9 路由矩阵契约、端到端串联、未实现路线抛错、空帧拒绝、已移除枚举
test_loop.py 4 周期检测与循环选帧
test_oneshot.py 5 一次性动作裁区间
test_pixelate.py 8 像素化、母版规格探测、色板锁定
test_matte_provider.py 2 抠图 provider
test_sufy_video_download.py 5 视频下载重试与长度校验

四条抛错相关的用例已拿修复前的旧实现对照,确认在修复前全部失败。


对抗式自检

以下四条是评审历史上会被追问的点。Q1–Q3 给出答案与其失效边界;Q4 是自检抓出的自相矛盾,已在提交前改掉。

Q1|ROUTE_MATRIX 固定在这一层对吗?为什么不让调用方传路线?

固定在 ai_engine.strategy。理由:路线选择依据是动作的物理性质(有无连续步态、是否一次性、是否需要单帧可编辑),不是调用方的业务决定。让 server 传等于把实测挣得的结论推给不掌握依据的一方。

但这个理由在渲染出帧路线进来后不成立——同一个 walk 既可走 i2v 也可走渲染,选择依据变成"该角色有没有 3D 模型",那是 server 才知道的事。

该边界已写进 strategy/base.py 的注释,以免后来者按错误前提扩展:接入第三条路线前须先定「路线选择由谁决定」,并可能要把矩阵改成「动作类型 → 可选路线集合」+ 一个选择器。

Q2|GeneratedAction{frames, durations, fps} 三个字段够吗?

对视频路线够。对渲染路线不够:出帧台在产出帧的同一时刻还能给出 root_motion / sample_times / fps_equiv / 源片段时长,这些只有渲染当时拿得到——帧出完后在图像空间只能反推位移,采样时刻与源片段时长反推不回来。

已在 #122 提出扩展,本 Issue 不改,避免同一契约两处并行修改。

Q3|为什么 ai_engine 不碰存储?边界依据是什么?

依据是"谁掌握租户与配额上下文"。上传对象存储要知道 bucket、路径规则、归属项目、配额;这些全在 server。ai_engine 若自持存储,等于把租户概念下沉到一个只应该做图像计算的层。

代价是帧 bytes 要在内存里过一次。当前 16 帧 512×512 RGBA ≈ 16MB,可接受;帧数或分辨率显著上升时需重新评估。

Q4|自相矛盾:删 PROC_IDLE 的理由是"不留没有实现的枚举值",却加了同样没有实现的 RENDER_3D

自检发现的矛盾,已在提交前改掉:RENDER_3D 已移除。

原本保留它的理由是「契约先留位,将来接入时 GenRoute 不必二次改形,避免一次跨模块的
破坏性变更」。该理由不成立:全仓 grep 确认 GenRoute 只在内存里当分流键,无持久化、
无序列化,枚举加成员是纯加法,不构成破坏性变更。理由塌掉后,留位就只剩「在代码里
表达意图」,而按团队规范意图应写进 Issue 不写进代码。

三渲二的契约需求由 #81 / #122 承载,随实现一起加枚举成员。

GenRoute 现在只有 VIDEO_I2V / PER_FRAME,并由 test_genroute_only_lists_implemented_routes
锁死——该用例同时管住两个方向:已证否的路线要删,未来路线不提前留位。


需要评审拍板(本 Issue 不改,列出待议)

以下四条是自检时发现的既有契约问题,修改会动 windup_common 的公共契约,故不在本批 PR 内处理。

它们有共同根因:契约按逐帧路线(route A)设计,而落地的是视频路线。n_framesposes 推导、
CharacterCard 零读取、loop / view / palette 零消费方,都是同一副旧设计骨架的残留。

D0|CharacterCard 六个字段在 ai_engine 里零读取

generate(card, action, master, progress)strategy.derive(card, ...) 都接收 card
但全仓 grep card\.ai_engine零命中——name / desc / palette / view /
master_ref / version 一个都没被读过。

原因是视频路线的角色身份由母版图像承载,不由提示词承载:_build_prompt 只取
action.facing,服装 / 鞋子走默认值,所以不同角色跑出来的提示词是同一份。

这不是缺陷,是路线特性。但它意味着:CharacterCard 目前是为逐帧路线设计、却只在视频
路线上被传递的占位参数
。逐帧路线要用 desc 组提示词,渲染路线要用 model_3d_ref#122),
两条都还没实现。

建议:明确它是「为未实现路线预留的入参」并在 ports docstring 里写清,或暂时从签名移除、
随逐帧路线一起加回。此处不单方面决定。

D1|ActionSpec.n_frameslen(poses) 推导,但视频路线不用 poses

@property
def n_frames(self) -> int:
    return len(self.poses)

视频路线取 n = action.n_frames or 8。调用方要出 16 帧,必须传一个 16 元素的 poses 列表,而列表内容在视频路线上完全不被使用。

建议:n_frames 改为显式字段,poses 仅逐帧路线使用;n_frames 缺省时再回退到 len(poses)

D2|枚举风格前后不一致

ActionType / GenRouteEnum;而 ActionSpec.loop / stylize / facingCharacterCard.view 是裸 str,合法值只写在注释里(none/linear/pingpongpixel/noneside/frontside/topdown/isometric)。

facing 尤其要紧:master_prep 里写死的契约是"提示词朝向必须与母版一致,给正面母版喂侧走词会让模型靠转身调和图文矛盾"。这条约束靠字符串传递,拼错不报错。

建议:四者都改枚举。

D3|CharacterCard.palettestr 但无格式约定

字段存在但没有消费方,也没写清是色板名、十六进制串还是存储引用。

建议:定格式或删除。


第二轮对抗自检:可生成性与质量信号的缺口

第一轮自检查的是契约形状,这一轮查的是输入不合作时会怎样。结论:本批实现对任意输入
都会走完全程并报成功,失败全部是静默的

以「一张人物在画板前作画的图,请求 walk」为例,逐段追踪:

行为 报错
generate() 入口 不校验母版能否解码、有无主体、有无腿、朝向是否与 facing 一致
prepare_master walk 不做处理,原图直传
_build_prompt 只传 action.facing,提示词是固定的侧走模板,与母版内容无关
i2v 图文矛盾(图在作画、词在走路)→ 模型靠转身 / 变形调和(master_prep 已记录该机制)
matte.cutout 显著性抠图,全仓无连通域筛选 → 画板被当主体一并保留
pick_cycle 测不到步态周期 → 凹陷深度 < 0.25 → 静默降级为全片均匀取
align_bottom_center 按整幅 alpha bbox 定标,画板每帧都在,中位数同样被撑大 → 角色系统性缩小
出参 16 帧、逐帧时长齐、无异常

调用方得到一个"看起来成功"的结果。这与本 Issue 已修的「未实现路线返回空帧」属同一类问题,
只是发生在产品层而非代码层。

E1|输入侧零校验

portsimpl 内所有 raise 都在输出侧(空帧、未实现路线)。master: bytes 不做
任何前置判定。

建议:加可生成性预检并允许拒绝——母版可解码 / 有主体 / 主体连通域数 / 走路类动作是否可见腿 /
朝向与 facing 是否一致。

E2|pick_cycle 四条返回路径,调用方无法区分

帧数不足           → 原样返回
测不到可信周期      → 均匀取,不闭环
候选为空           → 均匀取,不闭环
正常               → 单周期闭环

四条返回类型相同、无任何标记。上层无法判断这 16 帧是干净的步态循环还是降级产物。

E3|抠图无连通域筛选

OnnxU2NetMatteProvider.cutout 直接用显著性 mask 合成,抠出几个连通域就保留几个。
画板、道具、第二个角色都会被保留。

E4|对齐实现与产品规则不一致

产品规则要求「脚底线 + 躯干中心锚点对齐,忽略剑 / 围巾等延展物」。实现为:

canvas.alpha_composite(crop, (cell // 2 - w // 2, int(cell * foot_line) - h - lift))

水平方向取的是 bbox 中心而非躯干中心;延展物仅靠"各帧高度取中位数"抵抗单帧异常
(如某帧武器高举),对每帧都存在的延展物(画板 / 披风 / 坐骑)无效。

E5|出参无质量信号

GeneratedAction{frames, durations, fps} 无法表达"这次生成成色如何"。而所需指标本仓已具备
——归一化接缝、死帧数、周期可信度均可由 slicing/qualityslicing/loop 给出,只是不进出参。

建议:出参增加质量字段,让上层据此决定交付 / 重试 / 提示用户换母版。

E6|提示词硬编码,不构成一个可运营的能力

prompt/*.py 为写死模板;build_walk_prompt(garment, feet, facing) 三个参数中实际只传
facing,服装 / 鞋子取默认值,CharacterCard.desc 未进入提示词(即 D0)。因此不同角色
使用同一份提示词。当前无版本、无 A/B、无回滚,调整一次需发一次代码。

提示词是"如何让模型听话"的知识,会随模型版本漂移,且用户有自定义诉求(前端曾出现
ActionType.CUSTOM + action_desc 的形状)。建议单列为独立能力,本期先把注入口留出:
_build_prompt 目前写死 builder 表,应改为可注入。

测试覆盖的已知空白

本批 34 个用例每个都有断言,无空跑用例。但以下模块的公开函数零直接覆盖:

模块 未覆盖函数
slicing/quality frame_deltas dead_frame_mask active_span blur_ratio
postprocess/rootmotion extract_root_motion frame_durations
prompt/actions build_idle_prompt build_attack_prompt
master_prep add_headroom prepare_master

其中 frame_durations 参与每次出参构造、prepare_master 参与每次 jump 生成,属实际执行
路径上的未覆盖点。

以上 E1–E6 与测试空白均不在本批 PR 修改范围,建议单开 Issue「可生成性判定与质量门禁」
承载。当前契约是「给我母版,我给你帧」,缺的是「判断这个母版能否做这个动作」与「告知这次
成色如何」两层。


补充:视频接口面已变更(2026-08-07 实测)

本节推翻上文对 PR 2 的部分描述,与之冲突时以本节为准。

现状

SufyVideoProvider 建在过时的接口形状上:打 /v1/videos,首帧走 input_reference
base64 dataURI。而平台当前有 69 个 POST 视频端点,其中 22 个是图生视频,全部是
FAL 队列格式 /queue/...,全部要 image_url 公网 URL

发现经过:用一张全新角色母版跑 kling-v3-omni 端到端,费用已产生。任务 completed、
16 帧齐、逐帧时长齐,选帧指标反而是历史最好的一组(接缝 0.66、零重复帧、零死帧),
但画面里是一个与母版毫无关系的写实路人,且只有下半身——提示词里 "the legs clearly
visible" 被当成了取景指令。参考图从未被采纳。

kling-v3-omni 的图生视频真实地址是 /queue/fal-ai/kling-video/o3/{mode}/image-to-video
/v1/videos 上给它塞 input_referenceimage_list,四种字段形状全部试过,都不
可能成功——地址就是错的。

已按现行接口重写

新增 FalQueueVideoProvider 与显式的模型→端点映射表(10 个模型),保留
SufyVideoProvider 不动(/v1/videos 对 sora 系可能仍有效,无证据说它坏了)。

规范里挖出四处不能靠直觉推断的地方:

事实
轮询地址 不是提交地址加 /requests。六个 kling 模型共用 /queue/fal-ai/kling-video/requests/{id},模型段与 {mode} 全部消失;vidu 也丢掉 q3/pro
认证 FAL 端点是 Authorization: Key {key},不是 Bearer
base 路径 AI_BASE_URL 结尾是 /v1,而 /queue 是它的兄弟。不剥掉 /v1,每个调用都是 404
mode 枚举 各模型不同:v2.6 只有 pro,v3-turbo 是 standard/pro没有 std)。现默认 mode="std" 在两个模型上会 400,故构造时即拒绝

另:请求体没有 model 字段(模型即路径),duration 有三种写法("5" / "8s" / 5)。

契约冲突与解法

VideoProvider.i2v(first_frame: bytes, ...) 收 bytes,FAL 要公网 URL。

未改 Protocol。 新增 FirstFrameUploader port,作为 FalQueueVideoProvider必填
无默认构造参数
——构造不出一个没有首帧发布途径的 FAL provider。

不选另外三条的理由:

  • 改 Protocol 收 URL → 把"取公网 URL"推给所有调用方,包括需要 bytes 的 SufyVideoProvider
    且让对象存储成为整条管线的关切,与 ai_engine ports 的"不碰存储"边界矛盾
  • provider 自持上传 → 要在 framework/providers 里塞一个对象存储客户端,无凭证无法测试;
    windup_framework/storage/ 目前是空的,那条线该由存储的负责人来落
  • i2v 加可选 URL 参数 → 一个实现必需、另一个忽略的参数,污染共享契约

影响面:当前零调用点。 git grep .i2v( / VideoProvider 只命中 provider 自身与文档字符串。
新增义务只落在组装层:构造 FAL provider 时必须提供 uploader。

测试 37 个用例全 mock、零网络;变异测试 11/11 全部被捕获。

未验证

  • 从未真实调用过 FAL 端点。 认证方式、/v1 剥离、请求体形状、轮询、结果回退、下载,
    全部只对着 OpenAPI 规范与 mock 验证过。首次真跑会花钱,且可能暴露不一致
  • 三份规范(seedance-2.0 / vidu-q3-pro / kling-v3-turbo)称图字段接受 "URL 或 base64",
    另 19 份只写 URL。而实测四种 base64 形状均未被采纳。实现统一要求 http(s),对 22 个都安全
  • 哪个模型对我们的角色效果最好——未测。映射表是能力清单,不是推荐

补充:抠图(2026-08-07 实测)

两个问题

一、u2netp 对闭合区域天然失灵。 四足角色腿间的背景是一块被主体围住的空隙,显著性
模型把它当成主体内部,整块底色留在产物里;轮廓上还带一圈底色描边。

二、底色不可指定。 母版生成提示词写 SOLID MAGENTA #FF00FF,模型实际给 #DE297C
(偏离 140);改要求纯绿 #00FF00,给 #9ACC5D(偏离 190)。两次都不听。 因此
"规定一个底色然后按死值抠"这个思路不成立,只能采样实际底色。

修法必须窄

实测一个铁锈橙毛 (222,130,70) 的角色配玫红底 (222,41,124):两者红通道完全相同
欧氏距离仅 104。先后试过两版宽阈值 chroma,都把橙毛判成半透明并去"反解",越解越坏
(先成橄榄绿、再成亮绿)。

最终解法:u2netp 定主体,只杀 d < 38近似精确等于底色的像素。橙毛 d≈117
完全不受影响,闭合空隙 d≈0 干净移除。三个角色底色残留
2.54% / 0.44% / 1.25% → 0.17% / 0.21% / 0.26%,主体内部逐像素未动。

与"按颜色抠是死路"那条规则的边界:那条说的是拿颜色当主体判据(白底浅色角色会被
抠穿)。这里主体判据仍是 u2netp,颜色只用来做减法,绝不新增主体像素;底色不够均匀
时(四角标准差 > 8)直接跳过。

顺带去掉 onnxruntime 缺失时"猜四角主色"的静默兜底,改为抛错。

方法论教训

修这个问题的过程中造了三个残留指标,全部漏判了橙毛被改坏——因为橙毛的 alpha 是
0.62,不在"内部像素 α>250"的统计范围里。每次都是先看画面才发现指标在骗人。涉及画面
的改动,指标只能用来复核,不能用来验收。


补充:新增验证(2026-08-07)

Holdout(8 段从未参与调参的真视频)pick_oneshot 首次在真视频上验证——它在
jump/attack 主路径上,此前只有合成数据的单元测试。4 段攻击视频无重复帧,选中段的运动
强度是全片平均的 1.7–2.3 倍。pick_cycle 4 段中 3 段干净,唯一告警是一段近乎静止的早期
废片,算法正确地判"无周期"并退化成不硬闭环。

主路径补测frame_durations(每次出参构造都走)与 prepare_master(每次 jump/attack
生成都走)此前零直接覆盖,补 21 个用例,变异测试 5/5 全部被捕获。

代码去重_gray() + _SMALL=48slicing/loop.pyslicing/quality.py 各有一份
完整拷贝;_png()/_img()strategy/concrete.pyimpl/character_generator.py 各一份。
两组都收成唯一定义。这类分叉不会报错,只在数据或画面上体现。

画布裁切修复的真实样本:新生成的四足角色主体 w/h = 1.92,超过 1.61 裁切悬崖。修复前
鼻尖与尾尖被切平、主体贴死画布左右边缘;修复后完整落在画布内。两个人形角色(w/h 0.48 /
0.70)修复前后逐像素相同,确认该约束不误伤既有资产。


关联

Metadata

Metadata

Labels

P1优先级 P1(次级)enhancementNew feature or request

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions