feat(ai_engine): 出帧工具箱 —— 抽帧 / 选帧 / 后处理 / 提示词(Refs #171) - #180
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Found three issues that should be addressed before merging.
Findings without inline locations
backend/uv.lock:1565: [P1] Regenerate this lockfile from the current workspace manifests. Thewindup-frameworkentry now omits declared runtime dependencies such aslangchain-openai,onnxruntime,passlib[bcrypt],qiniu,redis, andresend; the same lock update also dropspytest-covand the app'spydantic[email]extra. A locked install can therefore fail lock validation or produce an environment missing required runtime packages.
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151099a to
250f0f7
Compare
|
补一个我上一条漏报的数:流式抽帧的 CPU 代价。 上一条只给了内存收益(488 → 126 MiB),没量时间。补测(同 5 段真实视频,各预热一次后取单次):
慢 2–3 倍,绝对值 +130 ms 左右。原因是 判断:这条链路的耗时由 i2v 网络调用(30–60 s)与 16 次本地抠图推理主导,+130 ms 是噪声;而内存是并发的硬约束(4 个并发 worker 按改前要 ~2 GB)。所以换得值,但这个代价此前没写出来,补在这里。 想两头都要的话得让 pyav 按关键帧 seek、只解需要的段,那是另一件事,没做。 顺带更正上一条里一处措辞:那张表里"抽 150 帧降 42%"的场景,说的是 |
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ee80dbd to
123c420
Compare
|
已 force-push:把这条依赖链重排成真正的线性 stack。评审锚点会移位,说明原因。 问题:GitHub 的 机制:#181 / #182 此前是各自独立基于 main、靠"同步提交"携带前置分片内容的副本。合并 #181 时的共同祖先里没有 处理:改成 #172 → #179 → #180 → #181 → #182 的线性 stack,每个分支真正包含前置分支的提交。副作用是那些"同步上游/下游"的提交全部变成冗余,已在重排中丢弃。 内容变化(重排本身不改逻辑,两处例外,均已核对):
验收(重排后逐项跑过):
|
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123c420 to
e0f06e4
Compare
|
这五个 PR 已达 ready:无待追加改动、CI 通过、AI review 意见全部 resolved。可以开始 review。 依赖顺序(已重排为线性 stack,逐级包含前一片的提交): #180 零依赖于前两片的业务逻辑(纯 PIL / numpy + 真实视频实测),想先看小的可以从它入手。 本地已验的三项(每次推送后重跑):
端到端实证:2026-08-11 用这条链路(不是旁路脚本)从零跑通两个全新角色的走路序列帧——文生图出母版 → i2v → 抽帧 → 选帧 → 抠图 → 像素化 → 对齐 → 打包。 三条已知缺陷,代码在本批 PR 内,已独立立项跟踪,不在本批修复:
三条都不影响流程成功与 CI,属品相问题。选择独立跟踪而不是塞进本批,是为了不让改动范围与 Issue 脱节;其中 #197 的可行方向尚未实现也未验证,如实说明。 |
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e0f06e4 to
6519cab
Compare
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6519cab to
d39166d
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d39166d to
bd18bb3
Compare
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bd18bb3 to
f513d18
Compare
providers/ 此前只有三个 create_*_client 工厂,没有可供上层依赖的抽象类型, ai_engine 无法在不 import 具体实现的前提下声明它需要什么能力。 - interfaces.py:ImageProvider / VideoProvider / MatteProvider 三个 Protocol, 零依赖,供上层按能力而非按厂商声明依赖。 - matte.py:OnnxU2NetMatteProvider,onnxruntime 直跑 u2netp。不用 rembg:其底层 同样依赖 onnxruntime,且 numba 老链在 3.12 无轮子。onnxruntime 导入失败时降级 到 Pillow 兜底而非崩溃。 - sufy.py:SufyImageProvider / SufyVideoProvider。视频成品下载加三次退避重试与 长度校验 —— 该步发生在提交任务、轮询、等待全部成功之后,此时费用已产生、视频 已生成好,只差取回数据,连接断一次整单作废。实测同一角色连续两单死在这里各烧 一次费用。test_sufy_video_download 的四条断言拿修复前的旧实现做过对照,确认其中 三条在修复前会失败。 依赖声明: - qiniu>=7.14 —— 此前未声明,镜像能起、/docs 也 200,只有第一次 POST /media/upload 才 ModuleNotFoundError。 - onnxruntime>=1.17,<1.24 —— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac 装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致,抠图代码零改动。 本 PR 不依赖其他未合分支:providers 不 import windup_common.models。
2026-08-07 拉网关 OpenAPI spec 逐个核对:平台现有 69 个 POST 视频端点,其中 22 个
图生视频**全部**在 FAL 队列面 /queue/... 下,首帧一律是 URL 形态字段(image_url /
start_image_url),同日实测送 base64 dataURI 无一能用。原 SufyVideoProvider 建在
OpenAI 风格 /v1/videos + input_reference dataURI 上,是过时的接口形状——在它上面打的
两处补丁方向错了,一并回退:
- _needs_image_list / _IMAGE_LIST_MODELS 里新增的 kling-v3-omni / kling-v3
- _assert_reference_registered / ReferenceIgnoredError 及其 3 条测试
新增 FalQueueVideoProvider 与旧实现并存(没有实测证据说 /v1/videos 已坏,sora 系可能
仍只在那一面)。要点:
1) 模型 → 端点的显式硬表 FAL_I2V_ENDPOINTS,不拼路径。每家有三样东西不同且都猜不出
来:提交路径的型号段;首帧字段名(同是 kling,o3 / v2.5-turbo 叫 image_url,
v3 / v2.6 / o1 叫 start_image_url);轮询前缀(**不是**提交路径 + /requests,
kling 六个型号共用 /queue/fal-ai/kling-video/requests/{id})。未登记的模型抛
UnknownVideoModelError,不做前缀匹配、不做兜底——猜出一条"存在但语义不同"的路径
(如把 image-to-video 猜成 reference-to-video)会正常出片、正常计费。
2) i2v 契约冲突:Protocol 收 bytes,FAL 面只吃公网 URL。选择"provider 自己适配",
Protocol 签名不动——新增 FirstFrameUploader port,provider 构造时必传,内部把补边
后的首帧换成 URL。调用方零改动;母版已在公网时用 PreUploadedFirstFrame 复用该
URL、不重传。
3) 失败一律显式抛错,不静默降级:spec 明写「任务失败时后端也返回 COMPLETED,通过
detail 区分」,故 COMPLETED 还要查 detail;认不出的 status 当失败(继续轮询会把
"协议变了"伪装成"生成太慢");超时抛 VideoJobTimeoutError;参数校验在上传首帧之前
完成;下载复用既有 _download(重试 + 长度校验,治"视频已生成、费用已产生,下载断
一次整单作废")。
FAL 面鉴权是 Authorization: Key(不是 Bearer),base_url 需从 /v1 退回网关根
(/queue 与 /v1 平级)。两处都有 spec 依据,已写进注释与测试。
37 条新测试全程 mock 不联网;11 个变异(错端点 / 错字段名 / 错轮询前缀 / 去掉各处抛错
/ 去掉下载重试 / 参数校验挪到上传后)逐个确认能被测到,全部 KILLED。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rebase 到 main 时解冲突取了主线的 uv.lock,但 framework/pyproject.toml 取了本分支的, 后者少了主线用户模块加的 passlib[bcrypt] / redis / resend —— CI 装依赖时按 pyproject 解析,于是 conftest.py 导入 bcrypt 失败(ModuleNotFoundError,本地因 venv 里已装而没暴露)。 主线的 pyproject 已含本分支需要的全部依赖(onnxruntime<1.24 / qiniu / pillow / numpy, 连注释都是从这条线过去的),故直接取主线版本,两边并集自然成立。uv lock --check 通过。
机器审 PR 1024XEngineer#179 P1。成品 URL 是网关响应里的绝对地址(正常指向 CDN,异常可以是 网关返回的任意地址),原实现复用带 Authorization 的网关 client 直接 GET。httpx 只在跨源**重定向**时才自动摘 Authorization,对一开始就跨源的直连请求会原样带上 client 级 headers —— API key 因此发给了那个域名。 改法: - 按目标地址判定后显式摘凭证,不是一律摘。网关也可能签发自己域名下的下载链接, 那条路径摘了头就是 401,所以同源保留、跨源摘掉 Authorization 与 Cookie。 - Proxy-Authorization 不动:它是给代理的,与目标是否同源无关。 - 同源判据对齐 httpx 自己的 `_redirect_headers`(scheme + host + 端口), 未 import 其私有函数,免得被上游改名。 - 请求改为进重试循环之前构造,非 http(s) 地址在发出任何一次请求之前就炸。 - 2026-08-05 实测挣来的三次退避重试与 Content-Length 校验原样保留(视频已生成、 费用已产生,断一次不能整单作废),FAL 面调用处那句"用同一个 client 带鉴权头取" 的注释同步更正 —— 它正是这个泄漏的出处。 变异验证 13 个:12 被杀。唯一存活的是单独拆掉"默认端口补齐" —— httpx 0.28 已把 :443/:80 归一化成 port=None,该行与 scheme 比较互为冗余,两条同时拆即被杀。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
视频路线的纯计算层,零 windup 依赖(只用 PIL + numpy),可独立测试。
slicing/ 视频 → 帧序列
extract 解码;loop 循环类动作抽单步态周期;oneshot 一次性动作裁区间;
quality 帧质量诊断(死帧 / 糊帧判据,只作诊断不进选帧,理由见 loop docstring)
postprocess/ 帧 → 交付级序列帧
pixelate 母版是像素画时吸附母版网格 + 锁母版色板,否则通用量化
pack 脚线对齐 / sprite sheet / GIF
rootmotion 逐帧时长(关键帧加长定格,等时长会让动作发飘)
prompt/ + master_prep.py 按动作类型选提示词、按动作预处理母版
两处实测挣得的修复一并带上:
1) 画布横向裁切(postprocess/pack.py)
align_bottom_center 的三条缩放分支只按高度定标,是"主体是纵向长条"的人形先验。
横向长条主体按同一系数缩放后宽度超出 cell,被 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 时该约束恒不生效,
产物逐像素不变。
2) 步态周期误检(slicing/loop.py)三个坑,四段真 i2v 视频实测
a. 角色整体平移让 d(p) 单调上升,argmin 滑到搜索窗边界交出假周期。
实测骷髅走路:不消平移时曲线 40/56 段在上升,只剩 22/42/52 三个浅坑,argmin=22;
加 _deskew 消平移后整条曲线只剩一个局部极小,正是真周期 56(凹陷深度 2.68)。
b. 搜索窗上界 n//2 把真周期挡在窗外(待机真周期 62 > pmax 60)。改为 total*0.6。
c. 谐波:22 接近真周期的一半,半周期闭环 = 末帧接回首帧时左右腿瞬间互换。
改为在基周期整数倍里按归一化接缝复选,优先最小倍数。
测不到可信凹陷(prominence < 0.25)时判"无周期",退化成全片均匀取、不硬闭环——
实测骑士待机只有 31 帧,旧算法曲线单调、argmin 落在搜索窗下界 6 交出边界假值。
实测对照(n=16,接缝 = 末→首差 ÷ 组内相邻差均值,越接近 1 越闭合)
骷髅走路 3.07→1.96 | 骑士走路 1.29→0.87 | 骑士待机 9.31→1.39 | 骑士奔跑 1.77→0.81
待机那条最直观:旧算法写出的 GIF 只有 6 帧——16 帧里 10 帧逐像素重复,被 PIL 自动去重。
消融:改善全部来自 _deskew + 谐波复选。追加的"死帧避让 + 冻结裁剪"两个样本无变化、
两个变差(奔跑接缝 0.81→2.00),已回退,quality 只留作诊断。
`frame_durations` 参与每一次出参构造,`prepare_master` 参与每一次 jump / attack 生成, 此前两者均无直接覆盖。21 个用例,锁行为不锁具体数值。 frame_durations - 动作间必须有区分度(idle > walk > run)——等时长会让动作发飘、没有重量感 - 关键帧定格必须真的比邻帧长,且只定格一帧 - 未知动作要有可用兜底,不能返回 0 或抛错(上游动作类型可能先于本模块扩展) - 越界 key_frame 不炸(帧数由选帧决定,调用方未必对齐) - hold_ms 小于基准时长时取基准,定格不能反而变快 prepare_master - jump / attack 必须补顶部空间,否则腾空 / 过顶挥砍会顶出视频画面上沿被裁 (实测 attack 15/72 帧触顶) - 其余动作必须**原样**返回同一对象,无谓重编码会引入压缩损失 - 补的边在顶部、原图贴底(贴反了动作会往下出画) - ratio 越小顶部留白越多;jump 需要的空间多于 attack - 非法 ratio 抛 ValueError 已做变异测试,五处故意引入的错误全部被捕获: 抹掉动作间时长区分度 → 2 failed 关键帧不定格 → 1 failed jump/attack 不补顶部空间 → 3 failed 补边加在底部而非顶部 → 1 failed jump 与 attack 用同一 ratio → 1 failed 还原后 21 passed。不是写完就绿。
_gray() 与 _SMALL=48 此前在 slicing/loop.py 与 slicing/quality.py 各有一份完整拷贝。 两处必须在同一尺度上看帧,否则算出的差异量不可比;而分叉不会报错、只在数据上体现 ——调一边的降采样尺寸,另一边悄悄保持 48,两个模块的指标从此不再可比。 收成 slicing/_frames.py 唯一定义(SMALL / gray)。行为不变。
ai_engine 的 pyproject 声明了 imageio / av(抽帧必需),而 rebase 解冲突时 uv.lock 取的是主线版本,两者不一致:uv lock --check 报 lockfile needs to be updated。 本地 venv 里恰好装过这两个包,故本地测试没暴露;CI 用 --frozen 装依赖时会缺。 重锁时带 UV_DEFAULT_INDEX=阿里云镜像 —— 直接 uv lock 会把全仓 90 处包源改写成 pypi.org,产出两千多行与本次改动无关的 diff(主线 Dockerfile 定的就是这个镜像源)。 重锁后:阿里源 90 处、pypi 0 处,只新增 627 行(两个新包及其依赖树)。
机器审在 PR 1024XEngineer#180 报的两条 P1,加相邻边界扫描的发现。共同判据:返回长度恒等于 n, 凡是给不出 n 帧的入参一律报错,绝不静默交出一个长度自洽的短序列。 pick_oneshot: - n=1 撞 /(n-1) 除零(P1)。改为取"关键姿势"单帧:airborne 取脚线最高(顶点), swing 取能量峰后一帧(命中瞬间);不取区间首帧(蓄力,和待机一个样)也不取中点 (动作区间前后不对称,中点落在蓄力段)。 - n<=0 原本静默返回 [](range(n) 为空,连除零都不报)→ 显式拒绝。 - 源帧不足原本原样返回一个短序列 → 报错并报出两个数字。 - 动作区间被裁到不足 n 帧时原本直接返回该区间(14 帧输入请求 12 帧只回 9 帧), 改为把窗口放宽回来;动作贴视频尾部时缺口退回左边补,保证 n 帧互不重复。 - kind 拼错不再静默按 swing 处理(判据用错会裁出"看起来对"的错区间)。 pick_cycle: - n<=0 拒绝(P1)。实际机制与机器审所述不同:_offsets(P, 0) 并不除零(range(n) 为空, k*P/n 没被求值);真实路径是检出周期时走到 M[idx[-1], idx[0]] 抛 IndexError,而 测不到周期时**静默返回 []** —— 后者更危险。 - 源帧不足改为报错(原本原样返回)。 - n=1 显式取 medoid:单帧"循环"没有接缝也没有相位,原实现靠 nan 比较的意外结果返回首帧。 测试:两个文件各补边界用例,含 n=1..len(frames) 全量扫长度与不重复性。 变异验证 22 个错法,21 个被杀;唯一存活(区间放宽只往右补)经 1,565,565 组 (total, n, start, end) 穷举确认为等价变异 —— 长度契约完全一致,只有窗口位置不同。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
机器审 P2:iio.imread 会把 (T, H, W, C) 整个 materialize 出来,而我们只要其中 8~16 帧。 实测(14 段真实 i2v 视频,进程 RSS 峰值,非 tracemalloc): - 121 帧 720p,抽 16 帧:488 MiB → 126 MiB,降 74% - 同一段抽 8 帧:460 MiB → 98 MiB,降 79% - 抽 150 帧(周期检测用的 extract_all_frames_bytes):858 MiB → 498 MiB,降 42% 最后一条如实说明降幅边界:它保留全部 121 帧,省掉的只是那个完整 ndarray,保留帧本身 该占的内存还在。并发 worker 叠加时这仍是主要占用项。 正确性:改造前后在 14 段真实视频 × n=8/16/150 共 42 组上抽出的帧**逐像素完全相同**。 n=1 取首帧的既有约定保持不变(关键姿势的选择归 pick_oneshot,不由抽帧层猜)。 帧数来源:先读容器元数据(在 14 段真实视频上与实际帧数全部一致),拿不到正整数就退回 逐帧计数——计数不保留帧、内存不涨。按错的帧数算下标会抽出错位的帧,那是"帧数对、内容 错"的静默失败,多解一遍换一个确定的数划算。 顺带:imageio 分支的 except Exception 加了 warning 日志。它此前把"我们自己算错下标"和 "环境里没装 imageio"混为一谈,两者都表现为悄悄换用 ffmpeg 分支、产出看着正常的帧。 测试 11 条(tests/test_extract_streaming.py),含下标边界、抽到的是首尾与均匀分布那几帧、 要的比有的多时不补帧、元数据报 0 或抛错时退回计数。做过变异测试:改回 imread 让两条变红。 其中一条最初是摆设——炸弹抛 AssertionError 被 except Exception 吞掉、静默走 ffmpeg 后照 样绿,改成 BaseException 子类才真能杀掉变异;docstring 里写明了这个坑。
契约断言(DTO 自身)留在 feat/character-domain-models,不 import 上层包;本分片引入 prompt 模块,配套的实现侧断言就该落在这里:类型注解不是运行期约束,把校验写成 `SIDE if facing == Facing.SIDE else FRONT` 的二分时,"sidee" 会静默落到 FRONT 模板—— 正面走的提示词配侧面母版,模型靠转身调和矛盾,调用方什么错都收不到。 4 条:四个 build_* 拒绝非法 facing、枚举与合法字符串等价、walk 按 facing 选对模板体、 其余 build_* 同样按 facing 切换。
交付帧一直是 256×256 方形,而项目的 sprite 尺寸是 sprite_width×sprite_height
(API 允许 32~2048,且宽高各自独立、可非方)。上层拿到 256 的帧再缩到项目尺寸,
问题不是"糊一点":那一步用 Image.thumbnail,而 **thumbnail 只缩不放**。
2026-08-11 实测复刻上层这段逻辑,喂一张主体高 157px、脚线 0.92 的 256 交付帧:
目标 512×512 → 画布 512,主体仍 157px(根本没放大),脚线 0.92 → 0.709
目标 384×384 → 画布 384,主体仍 157px, 脚线 0.92 → 0.779
目标 128×128 → 画布 128,主体 78px, 脚线 0.914(缩小这侧正常)
也就是说放大方向上,align_bottom_center 刚对齐好的脚线被整体挪高,角色不站在地上,
跨动作对齐(ref_height 那套)也一起失效。根治办法是引擎一次就出到目标尺寸。
align_bottom_center 本来就接受 cell,按 cell 出 512 时主体高度实测 154 → 308,
确实翻倍;缺的只是**非方形**能力:cell 只能出方形,非方 sprite 仍得回到上层补边。
本次加 cell_h(None = 方形 cell×cell,默认行为不变),并把体内的几何拆成
cw(宽:水平居中、宽度兜底)与 ch(高:脚线、占高定标),不许串轴。
实测(同一组帧,ref_height=300):
默认 canvas 256×256 主体高 159 脚线 0.918 水平中心 0.498
cell=512 canvas 512×512 主体高 317 脚线 0.920 水平中心 0.500
cell=384,h=512 canvas 384×512 主体高 317 脚线 0.920 水平中心 0.500
cell=128,h=192 canvas 128×192 主体高 119 脚线 0.917 水平中心 0.496
**默认行为逐像素不变**:default / ref_height / preserve_lift / 宽主体兜底 /
cell=128 / cell=512 六个用例改动前后 sha256 完全一致;另有用例钉死
"不传 cell_h" 与 "cell_h=cell" 两种写法逐像素相同。
几何用比例表达(foot_line / fill_h / fill_w),换画布尺寸不改变构图,所以母版入口
预检与出帧仍共用同一套几何 —— master_check.REJECT_ASPECT = 2*FILL_W/FILL_H 里
本来就没有 cell,与画布像素尺寸无关。用例
test_subject_fill_ratio_is_scale_invariant 在 128/256/512/1024 四档上钉死这条。
顺带:cell/cell_h 非正数改为报错。PIL 允许建 0×0 的图、alpha_composite 也不报错,
静默出一张空图要到落库或前端才暴露。
变异测试(8 个变异逐个改坏 → 确认变红 → 还原,全部被杀):
P1 cell_h 默认写死 256 → doubling_cell_doubles_subject_height 红
P2 主体占高改按画布宽算 → non_square_applies_each_axis 红
P3 脚线改按画布宽算 → non_square_applies_each_axis 红
P4 水平居中改按画布高算 → non_square_applies_each_axis 红
P5 宽度兜底改按画布高算 → width_fallback_uses_canvas_width 红
P6 去掉画布尺寸校验 → non_positive_canvas_raises 红
P7 全透明兜底退回 256 方形 → all_transparent_honour_canvas 红
P8 出帧画布忽略请求值 → 另外 3 条红(默认档下该变异是恒等,测不到属正常)
本提交只动 ai_engine;把尺寸从 app 传进引擎的接线在上层分支。
CI: ruff / lint-imports(2 contracts kept) / pytest 255 passed 全过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FAL 队列面已随 1024XEngineer#179 移除,这个测试文件也一并删了。rebase 到新 main 时它被重放回来, 而它引用的 8 个 FAL 符号已不存在 —— 收集期直接 ImportError。
f513d18 to
86f4c0e
Compare
|
已重排到最新 main。#179 合入后本分支里那 7 个 framework 文件变成冗余,现在只剩 ai_engine 一层。 |
变更内容
windup_ai_engine的纯计算层,零 windup 依赖(只用 PIL + numpy),可独立测试:slicing/—extract解码;loop循环类动作抽单步态周期;oneshot一次性动作裁区间;quality帧质量诊断postprocess/—pixelate像素化;pack脚线对齐 / 图集 / GIF;rootmotion逐帧时长prompt/+master_prep— 按动作类型选提示词、按动作预处理母版周期检测:三个坑与实测
slicing/loop.py原实现直接取argmin,三处会交出假周期。坑 1|平移偏置。 角色在画面里横向位移(实测走位达画宽 18%),
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 里塞进越多周期 = 每周期帧数越少 = 动作变糙。测不到可信凹陷时判「无周期」,退化成全片均匀取、不硬闭环——硬闭环反而制造接缝。实测某待机视频(源 31 帧):未消平移时曲线单调,
argmin落在搜索窗下界 6,交出纯粹的边界假值;消平移后 p=11 虽是局部极小,但其值 14.010 夹在 14.013 与 14.023 之间,凹陷深度 0.006,被门槛挡掉。四段真视频实测(n=16;归一化接缝越接近 1 越闭合)
待机那行最直观:旧算法写出的 GIF 只有 6 帧——16 帧里 10 帧逐像素重复,被 PIL 自动去重。
消融记录。 改善全部来自
_deskew+ 谐波复选。另行追加的「死帧避让 + 冻结裁剪」两条,两个样本无变化、两个变差(奔跑接缝 0.81 → 2.00),已回退。quality.py只作诊断,不进选帧——这条写进了pick_cycle的 docstring,免得后来者重犯。画布横向裁切
align_bottom_center的三条缩放分支只按高度定标,是「主体是纵向长条」的人形先验。横向长条主体(四足兽 / 坐骑 / 龙)按同一系数缩放后宽度超出 cell,会被alpha_composite以负 dest 静默丢像素,PIL 不报错。裁切悬崖在主体 w/h >1/fill_h≈ 1.61。实测:某 w/h=1.78 的四足母版丢 27px(鼻尖 + 尾尖);w/h=2.0 只剩 79.9% 内容。加宽度兜底
fill_w=0.96后,w/h=1.92 的角色完整落在画布内(修复前主体贴死画布左右边缘 x∈[0,255],修复后 x∈[5,250])。两个人形角色(w/h 0.48 / 0.70)修复前后逐像素相同——该约束在人形区间恒不生效,不误伤既有资产。主路径测试补齐
frame_durations(每次出参构造都走)与prepare_master(每次 jump / attack 生成都走)此前零直接覆盖。补 21 个用例,锁行为不锁具体数值:动作间必须有时长区分度(idle > walk > run,等时长会让动作发飘)、关键帧定格必须真的比邻帧长且只定格一帧、未知动作要有兜底不能返回 0、越界key_frame不炸、jump/attack 必须补顶部空间(否则腾空/过顶挥砍会顶出视频画面,实测 attack 15/72 帧触顶)、其余动作必须原样返回同一对象(无谓重编码引入压缩损失)。Holdout 验证
8 段从未参与调参的真视频。
pick_oneshot首次在真视频上验证——它在 jump/attack 主路径上,此前只有合成数据的单元测试:4 段攻击视频无重复帧,选中段的运动强度是全片平均的 1.7–2.3 倍。pick_cycle4 段中 3 段干净,唯一告警是一段近乎静止的早期废片,算法正确地判「无周期」并退化成不硬闭环。变异测试
每批改动都做过:故意把实现改错,确认对应用例真的失败,再还原。逐帧时长与母版预处理 5/5 被捕获;周期检测与画布裁切亦同。其中一条最初没被杀——「主体过小」的样本用小方块,而小方块的面积占比也不达标,占比那条判据接住了它,于是删掉最短边检查测试照样绿。换成细长条后两条判据才真正独立。写完就绿的测试等于没写。
关联与依赖
Refs #171 · 是 #152 的前置
stack 声明:本分支 stack 在 #172(
windup_common/models)与 #179(framework/providers)之上。那两个合并后本分支 rebase,届时 diff 只剩ai_engine叶子这一层。本地验证