feat(framework): Provider 抽象 + 主体抠图 + 视频下载重试(#152 的前置 · Refs #171) - #179
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Review summary
Found three high-confidence issues affecting credential safety, fresh installs, and locked test environments.
Verification
git diff --checkpassed.python3 -m compileall -q backend/packages/framework/src backend/testspassed.- PR tests could not run because
uvis unavailable in this runner.
机器审 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>
机器审 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>
413dcf1 to
6c944be
Compare
|
三条 P1 已处理,6c944be。 ① 下载复用带鉴权的 client — 已修。泄漏点实际有两处: 取了 review 给的第二个方案(校验目标后剥离),没取第一个(一律用不带认证的 client)。理由:网关有可能在自家域名上签下载链接,那条路径缺 header 会 401;而不真跑一次付费调用无法区分两者。所以按目标地址判定 —— 新增
② / ③ 依赖与 lock — 在 变异测试 13 个,12 被杀。 两个存活分开判断:
未验证:没做任何付费调用,所以没有 live 证据说明真实网关的结果 URL 是同源还是 CDN —— 这正是修法做成"按地址判定"的原因:跨源则 key 不再泄漏,同源则行为与改前逐字节相同,两个分支都不会回归,但都没在真网关上确认过。另外 |
机器审 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>
e051fe2 to
66da7d0
Compare
|
已 force-push:把这条依赖链重排成真正的线性 stack。评审锚点会移位,说明原因。 问题:GitHub 的 机制:#181 / #182 此前是各自独立基于 main、靠"同步提交"携带前置分片内容的副本。合并 #181 时的共同祖先里没有 处理:改成 #172 → #179 → #180 → #181 → #182 的线性 stack,每个分支真正包含前置分支的提交。副作用是那些"同步上游/下游"的提交全部变成冗余,已在重排中丢弃。 内容变化(重排本身不改逻辑,两处例外,均已核对):
验收(重排后逐项跑过):
|
|
更正一条我此前的判断,并说明随之改变的实现。 我之前把交付帧上一处"背景透出来"读成抠图破洞。复核后这个诊断是错的:那一处是真实的两腿间隙,本来就该透明。交付帧(256²)上封闭透明域最大只有 4px,全是轮廓锯齿。 但在
更要紧的是:只按"不与画面边界连通"判空洞会把两腿之间填实。 迈步相里两只靴子在下方交叠,把腿间空隙彻底封死——它就是一块不与边界连通的背景域。实测 121 帧中 80 帧存在这种封闭空隙、共 25,173 px,朴素版会把它们全部填成主体(最惨单帧 3,172 px,两条腿焊死)。 所以判据是连通性与颜色两条一起:一个透明连通域只要"碰到画幅边界"或"内部存在任何一个确实是底色的像素",就不是洞。 实测:真空隙 25,173 px → 本实现填 0,朴素版填 25,173;121 帧共填回 51,273 个被误杀的主体像素。alpha 只增不减、只改成 1.0、RGB 不碰。 未引入 scipy(numpy 行/列游程传播,34ms/帧, 变异测试 9 个变异全部被杀,其中包含"去掉颜色守卫"这一条——它正对应我最初给出的错误设计。 另:本次把 如实说明未验的部分: |
|
这五个 PR 已达 ready:无待追加改动、CI 通过、AI review 意见全部 resolved。可以开始 review。 依赖顺序(已重排为线性 stack,逐级包含前一片的提交): #180 零依赖于前两片的业务逻辑(纯 PIL / numpy + 真实视频实测),想先看小的可以从它入手。 本地已验的三项(每次推送后重跑):
端到端实证:2026-08-11 用这条链路(不是旁路脚本)从零跑通两个全新角色的走路序列帧——文生图出母版 → i2v → 抽帧 → 选帧 → 抠图 → 像素化 → 对齐 → 打包。 三条已知缺陷,代码在本批 PR 内,已独立立项跟踪,不在本批修复:
三条都不影响流程成功与 CI,属品相问题。选择独立跟踪而不是塞进本批,是为了不让改动范围与 Issue 脱节;其中 #197 的可行方向尚未实现也未验证,如实说明。 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
第二条(硬编码)已按承诺改完,
分三个字段而不是复用已有的 请求形状仍留在代码里,理由写进了配置类的注释,方便后来人看到判据而不是只看到结论。 这批改动逮到一个真 bug: 测试 +5,5 条变异全部杀掉:共用一个字段 / 忽略配置写回硬编码 / 显式传参被配置覆盖 / 配置里补上请求形状字段 / 校验挪回解析之前。 另外两条的处理:
本 PR 与其上三片已同步 rebase 并全部重跑:逐分支 CI 全过(111 → 190 → 271 → 312 → 351),顺序合并 |
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 用一张全新角色母版跑 kling-v3-omni 端到端时实测发现,费用已产生。 现象:提交成功、status=completed、16 帧齐、逐帧时长齐、下游抽帧/选帧/抠图/脚线对齐 全部正常工作,最终产出一组构图完整的序列帧。但画面里是一个**与母版毫无关系的写实路人** ——母版是插画风、赭黄长外套、背铜管乐器的乐手,产出是深绿外套的写实人物,且只有下半身 (提示词里 "the legs clearly visible" 被当成了取景指令)。 根因:首帧字段按**模型**选,不是按"本地图/公网 URL"选。厂商文档写明 Kling 用 image_list、Sora 用 input_reference,而本仓只把 kling-video-o1 列进了 image_list 名单。 kling-v3-omni 收到 input_reference 后既不报错也不采纳,退化成纯文生视频。 危险在于失败形态:老模型(v2 系列)塞错字段会 failed,还能发现;kling-v3-omni 是 **成功返回一个错误结果**,整条管线无一处能察觉。这与本批 PR 已修的"未实现路线返回空帧" 属同一类问题,只是发生在更外层——空帧至少还能靠"帧是空的"判出来,这个连帧都是好的。 两处修复: 1) _needs_image_list 显式归类 + kling-v3 前缀兜底。仅对已确认的型号切换字段: v2-5-turbo / v2-1 已实测可吃 input_reference(2026-07-27 端到端到 completed), 不动既有通路,避免为修一个模型而破坏三个。 2) _assert_reference_registered 在**下载视频之前**拦截。网关在 billing_type_description 里明写计费口径,送了首帧却拿到"无参考视频"即为铁证。 提交后与轮询到 completed 时各查一次。字段缺失时不拦——不同网关字段不一定存在, 宁可漏判也不误伤。 四条回归测试,变异测试确认有效:把 v3-omni 退回 input_reference(复现原 bug)、 去掉计费口径检查,各有 1 条用例失败;还原后 8 passed。
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>
两处,都是 2026-08-07 用三个全新角色母版实测出来的。 1) u2netp 对闭合区域天然失灵 四足角色腿间的背景是一块被主体围住的空隙,显著性模型把它当成主体内部,整块底色 留在产物里;轮廓上还带一圈底色描边。母版底色是刻意生成的纯色、均匀度极高(实测 四角标准差 1.0–1.2),拿它做一次窄阈值清理正好补上这个洞。 阈值必须窄。实测一个铁锈橙毛 (222,130,70) 的角色配玫红底 (222,41,124):两者红通道 完全相同、欧氏距离仅 104。先后试过两版宽阈值 chroma,都把橙毛判成半透明并去"反解", 越解越坏(先成橄榄绿、再成亮绿)。取 38 时橙毛 d≈117 完全不受影响,而闭合空隙里的 背景 d≈0 干净移除。三个角色残留 2.54%/0.44%/1.25% → 0.17%/0.21%/0.26%。 与"按颜色抠是死路"那条规则的边界:那条说的是拿颜色当**主体判据**(白底浅色角色会 被抠穿)。这里主体判据仍是 u2netp,颜色只用来**做减法**,绝不新增主体像素;底色不够 均匀时(四角 std > 8)直接跳过,等于不清理。 2) 去掉 onnxruntime 缺失时的静默兜底 旧行为是回落到"取四角主色做 chroma-key"。两个问题:猜背景色——白底母版四角就是白色, 浅色角色与背景撞色会被抠穿;静默——开发机上看着能跑、输出其实是坏的,要到产物验收 才发现。改为抛 RuntimeError。 五条回归测试,变异测试确认有效:阈值放宽到 120(误伤橙毛)、去掉均匀性守卫、清理系数 允许 >1(凭空造主体)、恢复静默兜底,各有用例失败;还原后 7 passed。
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>
POST /generation/image 是可达端点,ImageTaskExecutor 默认实例化 SufyImageProvider, 而该类的 gen_image 直接抛 NotImplementedError —— 每个图像任务都稳定走到 FAILED。 端点看着可用、实际必失败,是本仓最忌讳的形态(机器审逮到)。 实现要点: - 走 OpenAI 兼容的 /chat/completions 面,参考图以 data URI 塞进 content 数组。 与 i2v 的提交-轮询-下载三段式是完全不同的调用形状,不复用 VideoProvider 通路。 - 对整个响应 JSON 正则取 data URI,不猜 message.content 的层级:不同网关包裹层级 不一致,猜错的代价是"调用成功、费用已产生、但我们报没图"。 - 空图重试 3 次。模型偶发返回一条不含图的正常响应;这与 _download 的网络重试是两 码事,后者治连接断。 - 校验 base64 解出的字节数下限 5000。响应里可能带几十字节的占位串,当图存下去就是 一个打不开的文件。 - 取不到有效图抛 RuntimeError,不返回空 bytes:上游会把返回值直接上传对象存储并写 进任务结果,0 字节的"成功"就是用户看到的裂图。 通路取自已跑通的实现(同日用它出过三张角色母版),非新写。 测试 5 条,全 mock 无付费调用,逐条做过变异测试: 把重试改成 1 次 / 去掉字节下限校验 / 丢掉参考图 / 拿不到图返回空 bytes / 成功后不 早退,五个变异各让 1~3 条用例变红。
对抗复查自己今天这笔实现时发现的两处:
一、路径此前硬编码 "/chat/completions",而 AIProviderSettings.chat_completions_path
本来就在配置里、零消费方 —— 正是本轮在删的那类字段。改成读配置。
二、更要紧:同一把 key 下不同网关的模型目录**不一样**。实测 GET /v1/models:
一个网关 73 个模型、一个图像模型都没有;另一个 134 个、含本模块的默认模型
(2026-08-10 实测)。配错 AI_BASE_URL 时原始报错只是一条裸 404,读的人无从判断
该改配置还是改模型名。现在 400/404 一律翻译成指向 GET {base}/models 的错误。
这条修的是"错误信息不可操作",不是"配置错误本身"——后者要在部署侧确认网关目录里
确实有所用模型,代码管不了。
测试 +3(路径来自配置、400/404 给出目录提示)。变异测试:路径写死 1 条红、去掉错误
翻译 2 条红。
放大看交付帧,主体内部有透明洞,背景直接透出来。2026-08-11 在归档角色 「林间斥候」走路的 121 帧真实视频帧(1280×720)上把成因拆开量了一遍: - u2netp 自己在主体内部造的洞:8 帧抽样里 6 帧为 0 —— 不是主要成因; - 真正的成因是键控误杀:_flat_bg_penalty 每帧杀掉 820~2346 个 u2netp 判为 主体的像素。角色浅肤色 (243,221,200) 到母版灰底 (219,219,220) 的欧氏距离 只有 31.3,窄于 _KEY_KILL=38,于是大腿、小臂这些浅色皮肤被当底色抠掉。 这些被误杀的像素被主体围住,就是「封闭空洞」,填回去即修复。 **只按「不与画面边界连通」判定会把两腿之间填实。** 直觉上腿间空隙从下方通到 画幅底边所以天然安全,实测不成立:迈步相里两只靴子在下方交叠,把空隙彻底封死。 121 帧里 80 帧存在这种封闭的底色空隙,共 25173 像素;只判连通性的朴素版把这 25173 像素**全部**填成主体(最惨单帧 src_024 填掉 3172 像素,两条腿焊在一起, 截图见验证记录)。归档的 04_走路_原画帧/frame_03 同样有 129 像素的封闭腿间空隙。 所以判据是连通性 + 颜色两条一起:一个透明连通域只要「碰到画幅边界」或者 「里面存在任何一个确实是底色的像素」,就不是洞。两条否决合成一次扩散,种子 = 边界上的透明像素 ∪ 底色像素。实测结果: - 25173 个真空隙像素,守卫版填掉 0 个,朴素版填掉 25173 个; - 121 帧合计填回 51273 个被误杀的主体像素(朴素版 82118,多出来的就是空隙); - alpha 只增不减,改动值只能是 1.0,RGB 通道不碰 —— 没有洞的帧逐像素不变。 _HOLE_BG_TOL=14 的取值有实测依据:视频帧里纯背景区域的色距 p99.9≈6.5、 最大 11.1(压缩噪点),而被误杀的浅肤色连通域中位色距 ≥17.1,14 落在这条间隙里。 扩散不用逐像素 BFS:1280×720 约 92 万像素,纯 Python BFS 要几十秒,抠图是逐帧 调用的扛不住。改成按行/列游程传播,一个 pass 推过整条游程。实测填洞单独耗时 34ms,cutout 端到端 0.44s/帧。scipy 不在依赖里,没有为此新增依赖。 顺手把四角估底色抽成 _bg_key(),让「底色是什么」只有一个真相源 —— 键控清理和 填洞必须按同一个 key 判,否则一个把某块当背景清掉、另一个又把它当主体填回来。 变异测试(9 个变异逐个改坏实现 → 确认对应用例变红 → 还原,全部被杀): M1 去掉颜色守卫(种子只剩边界,即朴素设计)→ closed_leg_gap 红 M2 去掉边界种子 → border_touching 红 M3/M4 _spread 只做行传播 / 只做列传播 → spread_is_four_connected 红 M5 去掉「底不是纯色就停手」的早退 → non_flat_background 红 M6 填成 0.5 而不是 1.0 → enclosed_hole_is_filled 红 M7 _HOLE_BG_TOL 放大到 200 → enclosed_hole_is_filled 红 M8 _HOLE_BG_TOL 归零 → closed_leg_gap 红 M9 丢掉「封闭」条件 → closed_leg_gap 等 4 条红 另外 _spread 与逐像素 BFS 在 300 组随机掩码 + 螺旋形上逐点等价(用例里留了 25 组)。 CI: ruff / lint-imports(2 contracts kept) / pytest 185 passed 全过。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
人工评审指出 providers 层硬编码过多。拆开看是三类,处理方式不同: **改进配置**(本次做的):三条能力各自的模型型号。 `AIProviderSettings` 加 `video_model` / `image_model` / `fal_video_model`,默认值即当前 实测在用的型号,部署侧可用 AI_VIDEO_MODEL / AI_IMAGE_MODEL / AI_FAL_VIDEO_MODEL 覆盖。 分成三个字段而不是共用已有的 `model`:三条能力同时在用不同模型,共用一个意味着换其中 一条把另外两条也换了。显式传参仍优先于配置,方便 A/B 对比时不必改环境变量。 **留在代码里**(本次不做,理由写进配置类的注释):哪个模型吃 image_list、哪个吃 input_reference、FAL 队列路径长什么样。这些不是运行参数,是该模型的 API 形状事实,改变 的是请求怎么构造。放进配置会把"填错了会怎样"从部署期推到运行期 —— 字段塞错不会立刻 报错,任务照常 queued,直到生成阶段才 failed,而费用可能已经产生(2026-07-29 实测)。 **暂不处理**:重试次数与字节下限。可配置化,但现在提出去只增加配置面,等真要调再说。 顺带修一个这批测试逮到的真 bug:`FalQueueVideoProvider` 的构造期校验发生在型号解析 **之前**,于是 `model=None`(表示"用配置里的")会被直接拿去查端点表,报"模型 None 不在 表里"—— 走默认路径就构造失败。改成先解析型号再校验。 测试 +5,5 条变异全部杀掉(共用一个字段 / 忽略配置写回硬编码 / 显式传参被配置覆盖 / 配置里补上请求形状字段 / 校验挪回解析之前)。
1fadabb to
7245818
Compare
评审质疑「为什么还需要一层不该理解业务的东西」(指 FirstFrameUploader)。查证后我认同, 但比他说的更彻底:**整个 FAL 队列面从未被真实调用过** —— app / ai_engine 里零引用, 产品链路走不到它,唯一的"引用"是 interfaces.py 里一句 docstring 指路。 删掉的理由与 GenRoute 只列有实现的路线是同一条,也是我在这批 PR 里反复引用的判据: 没有消费方的代码等于死代码,它让调用方以为该能力已具备。我一边用这条原则删掉 ActionSpec.fps / loop、一边留着 412 行未验证的 provider,是自相矛盾的。 删除:FalQueueVideoProvider / FirstFrameUploader / PreUploadedFirstFrame / FAL_I2V_ENDPOINTS 与端点映射 / 三个 FAL 专用异常 / config.fal_video_model / 28 条 FAL 测试。sufy.py 从 740 行降到 343 行。 保留一段注释记下两个实测挣来的事实,避免将来重新摸索:FAL 面只吃公网 URL 不吃 base64 (塞 base64 会 queued 之后在生成阶段才 failed,费用可能已产生);鉴权头是 `Authorization: Key`,路径与 /v1 平级。 顺带把 VideoProvider 的 docstring 改成正面依据:**入参恒为 bytes**,因为 ai_engine 必须 持有 bytes —— master_check 预检、master_prep 预处理、像素化锁色板全都读母版像素;改传 URL 的话 ai_engine 还得自己下载回来。某厂商只吃 URL 属该 provider 自己的适配问题, 在 provider 内部转换,不把差异漏给上层。 代价如实说明:veo / seedance 只在 FAL 面,而实测 veo 的走路步态比 kling 更自然。真要接 时连同一次真实调用一起加回,归档里有完整的接入记录,重写成本不高。
CI 的 codecov/patch 报红,查证后是真缺口:`SufyVideoProvider.i2v` —— **产品唯一的付费 路径** —— 一条测试都没有。sufy.py 覆盖率 72%,未覆盖的正是提交/轮询/下载三段式与首帧 处理。matte.py 的 `cutout` 装配顺序同样零覆盖。 补 sufy 7 条(sufy.py 72% → 99%): - 完整付费路径:提交拿 job id → 轮询到 completed → 下载 mp4 - 首帧必须是 JPEG data URI。PNG base64 会让任务 status=failed(VENDOR_FAILED, 2026-07-22 实测,33s fail-fast)—— 这条错在提交之后才报,本地看不出来 - 首帧按目标画布**补边不拉伸**:拉伸会改角色比例,而母版比例是角色一致性的一部分 - failed / cancelled 立刻抛,不把剩余轮询预算耗完(钱已经花了,尽快暴露原因更有用) - 轮询预算用尽抛错而不返回空 bytes(空 bytes 会被当视频送进抽帧,报"无可解码帧", 真正的原因被埋掉) - 首帧字段按模型选(塞错字段任务照常 queued,直到生成阶段才 failed,费用可能已产生) 补 matte 3 条(matte.py 71% → 93%):cutout 输出 RGBA、**RGB 通道不被改动**(改了会让 后续像素化锁色板取到被改过的颜色)、清理与填洞的**调用顺序**(反过来会把刚填上的像素 又清掉,且不报错)。真实推理需要 4.7MB onnx 权重,CI 里下不到也不该下,故用假 session 只覆盖装配逻辑。 顺带修一个测试逮到的真 bug:`poll_interval=0` 会在 `max_min * 60 // poll` 处除零,报 ZeroDivisionError,读的人完全看不出是配错了参数。改为构造期拒绝非正数。 9 条变异全部杀掉。其中"补边不拉伸"第一版是摆设 —— 纯色图拉伸后对称两点颜色照样相同, M3 存活;改成在源图里放一个偏心方块、量它在成品里的宽高比(补边≈1.0,拉伸≈2.67) 才真能杀掉。 另记一个操作教训:变异测试期间用 `git checkout -- <file>` 还原,会把同文件里**尚未提交** 的改动一起丢掉(守卫被静默还原,表现为"还原后测试仍红")。变异 harness 一律用脚本内的 文本备份还原,并在结束时校验 sha256。
变更内容
windup_framework/providers的抽象与实现:interfaces.py—ImageProvider/VideoProvider/MatteProvider三个 Protocol,零依赖,供上层按能力而非按厂商声明依赖matte.py—OnnxU2NetMatteProvider主体抠图。不用 rembg:其依赖链 pymatting → numba 0.53 / llvmlite 0.36 在 Python 3.12 无轮子(实测装不上);而 rembg 内核就是 u2netp 过 onnxruntime,默认alpha_matting=False时根本不碰 pymatting。直调 onnxruntime 甩掉整条死重依赖,同模型同质量sufy.py— 图生视频 provider,含按现行 FAL 队列接口的实现为什么先提这个
这是 #152 的前置。 #152 的
strategy/concrete.py:17import 了MatteProvider,而主线providers/下没有matte.py,合入即 import 失败。已在 #152 下附可复核位置。三处实测挣得的修复
1. 视频成品下载无重试导致整单作废(#129)
取视频那一步是单次读取、不校验长度,而它发生在提交任务、轮询、等待全部成功之后——钱已经付了、视频已经生成好,只差把数据取回来,此时连接断一次整单就废。实测同一角色连续两单死在这里各烧一次费用,第三单才成功。加三次退避重试与长度校验;四条回归测试拿修复前的旧实现做过对照,确认其中三条在修复前确实会失败。
2. onnxruntime 缺失时不再静默降级
旧行为是回落到「取四角主色做 chroma-key」。两个问题:猜背景色——白底母版四角就是白色,浅色角色(骨白 / 银甲)与背景撞色会被抠穿;静默——开发机上看着能跑、输出其实是坏的,要到产物验收才发现。改为抛
RuntimeError。3. 抠图对闭合区域天然失灵,补窄阈值底色清理
u2netp 是显著性模型,四足角色腿间那块被主体围住的背景空隙被判成主体内部,整块底色留在产物里;轮廓上还带一圈底色描边。母版底色是刻意生成的纯色、均匀度极高(实测四角标准差 1.0–1.2),拿它做一次窄阈值清理正好补上这个洞。三个角色残留 2.54% / 0.44% / 1.25% → 0.17% / 0.21% / 0.26%。
阈值必须窄。实测一个铁锈橙毛 (222,130,70) 的角色配玫红底 (222,41,124):两者红通道完全相同、欧氏距离仅 104。先后试过两版宽阈值 chroma,都把橙毛判成半透明并去「反解」,越解越坏(先成橄榄绿、再成亮绿)。取 38 时橙毛 d≈117 完全不受影响,闭合空隙 d≈0 干净移除。
与「按颜色抠是死路」那条既有结论的边界:那条说的是拿颜色当主体判据(白底浅色角色会被抠穿)。这里主体判据仍是 u2netp,颜色只用来做减法,绝不新增主体像素;底色不够均匀时(四角标准差 > 8)整体跳过。
采样要跳过最外圈。 视频帧最外一两行/列常是编码器边缘伪影而非底色:实测 9 段真 i2v × 16 帧 = 144 帧,贴边采样时 26 帧(18%)被判「底不均匀」而跳过清理——修复在真实路径上等于从不生效。逐一查证全部由最外圈造成(某视频最右一列整列纯黑 std 50.4)。往里让 2px 后 144 帧零误跳,三张静态母版取样中位色一字节未变。
依赖声明
onnxruntime>=1.17,<1.24— 1.24 起不再发布 macOS Intel(x86_64) wheel,Intel Mac 装不上。1.23.x 仍覆盖 Intel/arm64/Linux + py3.12,API 一致qiniu>=7.14— 此前未声明,镜像能起、/docs也 200,只有第一次POST /media/upload才ModuleNotFoundError(这两条主线现已具备,本 PR 不重复添加。)
关联与依赖
Refs #171 · Refs #129 · Refs #20 · 是 #152 的前置
stack 声明:本分支 stack 在
feat/character-domain-models(#172)之上——sufy.py的类型标注用到windup_common.models。#172 合并后本分支 rebase,届时 diff 只剩 providers 这一层。本地验证
待对齐
FAL 队列 provider 从未真实调用过。 认证方式(
Key而非Bearer)、/v1前缀剥离、请求体形状、轮询地址(六个 kling 模型共用/queue/fal-ai/kling-video/requests/{id},模型段与{mode}都消失)、结果回退、下载,全部只对着 OpenAPI 规范与 mock 验证(37 个用例、变异测试 11/11 被捕获)。首次真跑要花钱,可能暴露不一致——这条如实写在这里,不当作已验证。