Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ WINDUP_SSE_REDIS_CHANNEL=windup:pubsub:generation-task-events
# WINDUP_MATTE_REFINE=0
# 浅肤色脸颊/小腿可能漏检。ORT CPU arena 已在代码里关闭,不必再配。
WINDUP_MATTE_REFINE=1

# 抠图 provider。u2net(默认)/ birefnet。
# birefnet 在同一帧上把主体内部非实心从 5541px 降到 106px,但**单帧峰值 6.85GB** ——
# 生产 worker 容器上限 5GiB、宿主 7.7GB,**服务器上不要开**,开了会 OOM。
# 它是给组员本机(16GB+)做高质量素材用的。取值拼错会回落 u2net 并留一条 WARNING。
WINDUP_MATTE_PROVIDER=u2net
# web / worker 各有一份连接池。生成已短 session,不必再按 handler 并发配满。
POSTGRES_POOL_SIZE=5
POSTGRES_MAX_OVERFLOW=10
Expand Down
4 changes: 2 additions & 2 deletions backend/packages/app/src/windup_app/bootstrap/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ def _warmup_local_inference() -> None:
except Exception:
logger.warning("抽帧后端预热失败", exc_info=True)
try:
from windup_framework.providers import OnnxU2NetMatteProvider
from windup_framework.providers import make_matte_provider

matte = OnnxU2NetMatteProvider()
matte = make_matte_provider()
Comment thread
xiaocheny214 marked this conversation as resolved.
matte.warmup()
bind_matte(matte)
logger.info("ONNX 抠图会话已预热")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -848,12 +848,12 @@ def _assemble(self, directions: int) -> CharacterGeneratorPort:
from windup_common.models import GenRoute
from windup_framework.gateway import build_image_gateway, build_video_gateway
from windup_framework.gateway.image import _CIRCUIT
from windup_framework.providers import OnnxU2NetMatteProvider
from windup_framework.providers import make_matte_provider

from windup_app.server.media.first_frame import MediaFirstFrameUploader

if self._matte is None:
self._matte = OnnxU2NetMatteProvider()
self._matte = make_matte_provider()
if self._image is None:
self._image = build_image_gateway(circuit=_CIRCUIT)
# uploader 在这里注入而不是让 provider 自己去拿:framework 不认识 app 的对象存储。
Expand Down Expand Up @@ -974,7 +974,7 @@ def __init__(
self,
*,
image=None, # None → 懒加载 ImageGateway
matte: MatteProvider | None = None, # None → 懒加载 OnnxU2NetMatteProvider
matte: MatteProvider | None = None, # None → 懒加载,按 WINDUP_MATTE_PROVIDER 选
upload: Callable[[bytes], str] | None = None, # None → 真实对象存储上传
fetch_ref: Callable[[str], bytes]
| None = None, # None → 下载 reference_image_url
Expand Down Expand Up @@ -1185,9 +1185,9 @@ def _get_matte(self):
return self._matte
with self._assembly_lock:
if self._matte is None:
from windup_framework.providers import OnnxU2NetMatteProvider
from windup_framework.providers import make_matte_provider

self._matte = OnnxU2NetMatteProvider()
self._matte = make_matte_provider()
return self._matte

def _download(self, url: str) -> bytes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,9 @@ def _get_matte(self):
return self._matte
with self._assembly_lock:
if self._matte is None:
from windup_framework.providers import OnnxU2NetMatteProvider
from windup_framework.providers import make_matte_provider

self._matte = OnnxU2NetMatteProvider()
self._matte = make_matte_provider()
return self._matte

def _download(self, url: str) -> bytes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
)
from windup_framework.providers.matte import OnnxU2NetMatteProvider
from windup_framework.providers.matte_birefnet import BiRefNetMatteProvider
from windup_framework.providers.matte_factory import make_matte_provider
from windup_framework.providers.sufy import (
SufyImageProvider,
SufyVideoProvider,
Expand All @@ -34,6 +35,7 @@
# FAL 队列面的 i2v(现役接口形态);首帧要公网 URL,故与 uploader 成对出现
"SufyImageProvider",
"BiRefNetMatteProvider",
"make_matte_provider",
"OnnxU2NetMatteProvider",
# Gateway 工厂(executor 从 windup_framework.gateway 取;此处再导出方便装配)
"bind_call_context",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ def _get_session(self):
)
return self._session

def warmup(self) -> None:
"""把会话装进内存。**必须有这个方法** —— 没有它整条接线会静默失效。

``bootstrap.worker`` 是 ``matte.warmup()`` 然后 ``bind_matte(matte)``,两句包在
同一个 ``except Exception`` 里。缺 ``warmup`` 时第一句抛 AttributeError,
**``bind_matte`` 就到不了** —— 于是三个 executor 各自惰性 new 一份 provider,
而本类默认 ``union_with_u2net=True``,每份内部再 new 一个 u2net,进程里就是
6 个 ONNX 会话。生产 worker 容器上限 5GiB,而本模型单帧峰值 6.85GB。
表面上只有一条 "ONNX 预热失败" 的 WARNING,开发机上完全跑得通。

并集那一路的 u2net 也一并预热:它是每帧都要跑的,留到首帧再装等于把两次冷启动
叠在一起。
"""
self._get_session()
if self._union:
if self._u2net is None:
from .matte import OnnxU2NetMatteProvider

self._u2net = OnnxU2NetMatteProvider()
self._u2net.warmup()

def _predict_mask(self, img: Image.Image) -> Image.Image:
session = self._get_session()
arr = np.asarray(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""按配置选抠图 provider。

为什么要这个开关:BiRefNet 在同一帧上把主体内部非实心从 5,541 px 降到 106 px(-98%),
但单帧峰值 6.85GB —— 生产 worker 容器上限 5GiB、宿主总共 7.7GB,**跑不了**,而组员
本机 16GB 跑得很轻松。同一份代码两种装配,好过为它开一条分支:分支一定会漂,而漂出来
的"更好的管线"产出的素材,产品复现不出来。

默认必须是 u2net:忘配等于用得起的那个,而不是忘配就把生产打 OOM。
"""

from __future__ import annotations

import logging
import os

from .interfaces import MatteProvider

logger = logging.getLogger("windup.matte.factory")

#: 环境变量名。取值 ``u2net``(默认) / ``birefnet``。
ENV = "WINDUP_MATTE_PROVIDER"
Comment thread
xiaocheny214 marked this conversation as resolved.
_U2NET = "u2net"
_BIREFNET = "birefnet"


def make_matte_provider(name: str | None = None) -> MatteProvider:
"""按名字造 provider;不认识的名字回落 u2net 并留一条 WARNING。

不认识就抛错的话,一个拼错的环境变量会让整个 worker 起不来;而回落是安全方向 ——
u2net 在任何机器上都跑得起来,坏处只是抠图差一点,且这条 WARNING 说明了原因。
"""
choice = (name or os.environ.get(ENV) or _U2NET).strip().lower()
if choice == _BIREFNET:
from .matte_birefnet import BiRefNetMatteProvider

logger.info("抠图用 BiRefNet(与 u2net 取并集);单帧峰值约 6.85GB,别在小内存机器上开")
return BiRefNetMatteProvider()
if choice != _U2NET:
logger.warning("%s=%r 不认识,回落 u2net;可选:%s / %s", ENV, choice, _U2NET, _BIREFNET)
from .matte import OnnxU2NetMatteProvider

return OnnxU2NetMatteProvider()
6 changes: 3 additions & 3 deletions backend/tests/test_custom_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def _factory(*_args, **_kwargs):
return object()
return _factory

monkeypatch.setattr(providers, "OnnxU2NetMatteProvider", _counting("matte"))
monkeypatch.setattr(providers, "make_matte_provider", _counting("matte"))
monkeypatch.setattr(gateway, "build_image_gateway", _counting("image"))
monkeypatch.setattr(gateway, "build_video_gateway", _counting("video"))

Expand Down Expand Up @@ -319,9 +319,9 @@ def test_injected_matte_is_not_replaced_on_assemble(monkeypatch):
sent = object()

def _boom(*_a, **_k):
raise AssertionError("不该再构造 OnnxU2NetMatteProvider")
raise AssertionError("不该再构造抠图 provider")

monkeypatch.setattr(providers, "OnnxU2NetMatteProvider", _boom)
monkeypatch.setattr(providers, "make_matte_provider", _boom)
monkeypatch.setattr(gateway, "build_image_gateway", lambda **_k: object())
monkeypatch.setattr(gateway, "build_video_gateway", lambda **_k: object())

Expand Down
4 changes: 4 additions & 0 deletions backend/tests/test_env_example_keys_are_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"WINDUP_SSE_REDIS_CHANNEL",
# windup_framework.providers.matte._refine_enabled:os.environ,不走 BaseSettings
"WINDUP_MATTE_REFINE",
# providers.matte_factory.make_matte_provider:同上,os.environ。
# 走 BaseSettings 的话 framework 的配置层就要认识 provider 的名字,而选哪个 provider
# 是装配决定,不是配置数据。
"WINDUP_MATTE_PROVIDER",
# render3d._tc3.TencentCredentials.resolve:环境变量 → 加锁文件,不走 BaseSettings
"TENCENT_SECRET_ID", "TENCENT_SECRET_KEY", "TENCENT_REGION",
# orchestrator.client_bake / docker-compose 的构建目标,都是 os.getenv
Expand Down
76 changes: 76 additions & 0 deletions backend/tests/test_matte_provider_choice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""抠图 provider 的选择开关。

这个开关存在的理由:BiRefNet 单帧峰值 6.85GB,生产 worker 上限 5GiB —— 开错方向的代价
不是"抠图差一点",是把 worker 打 OOM。所以默认值与回落方向都要有用例钉住。
"""
from __future__ import annotations

import pytest

from windup_framework.providers import make_matte_provider
from windup_framework.providers.matte import OnnxU2NetMatteProvider
from windup_framework.providers.matte_factory import ENV


def test_default_is_u2net_so_a_missing_env_cannot_oom_the_worker(monkeypatch):
"""拦的坏例:默认值给成 BiRefNet。

忘配就该拿到跑得起来的那个。反过来的话,一台没设这个变量的机器会在第一帧抠图时
被 OOM kill,而表现是 worker 无声重启、任务卡在 RUNNING。
"""
monkeypatch.delenv(ENV, raising=False)
assert isinstance(make_matte_provider(), OnnxU2NetMatteProvider)


def test_an_unknown_value_falls_back_instead_of_killing_the_worker(monkeypatch):
"""拦的坏例:不认识的值直接抛错。

一个拼错的环境变量(``bierfnet``)会让整个 worker 起不来,而回落只是抠图差一点。
两个方向的代价不对称。
"""
monkeypatch.setenv(ENV, "bierfnet")
assert isinstance(make_matte_provider(), OnnxU2NetMatteProvider)


def test_explicit_argument_beats_the_environment(monkeypatch):
"""显式传参优先于环境变量 —— 否则测试与本地脚本没法覆盖部署的设置。"""
monkeypatch.setenv(ENV, "birefnet")
assert isinstance(make_matte_provider("u2net"), OnnxU2NetMatteProvider)


def test_birefnet_is_reachable_by_name_not_dead_code(monkeypatch):
"""拦的坏例:provider 合进仓里却没有任何路径能选到它(#686 就是这么变成死代码的)。

只断言"造出来的是那个类",不真跑推理:权重 224MB,CI 上不该下载,而这条要证明的是
**接线通了**,不是模型好不好。
"""
pytest.importorskip("onnxruntime")
from windup_framework.providers.matte_birefnet import BiRefNetMatteProvider

monkeypatch.setenv(ENV, "birefnet")
assert isinstance(make_matte_provider(), BiRefNetMatteProvider)


def test_every_selectable_provider_survives_the_bootstrap_warmup_call(monkeypatch):
"""拦的坏例:某个 provider 缺 ``warmup``,整条共享接线静默失效。

``bootstrap.worker`` 是 ``matte.warmup()`` 然后 ``bind_matte(matte)``,两句包在同一个
``except Exception`` 里。缺 ``warmup`` 时第一句抛 AttributeError,``bind_matte``
**就到不了** —— 三个 executor 各自惰性 new 一份,而 BiRefNet 默认与 u2net 取并集,
每份内部再 new 一个 u2net,进程里 6 个 ONNX 会话。生产 worker 上限 5GiB,
BiRefNet 单帧峰值 6.85GB。表面上只有一条 "ONNX 预热失败" 的 WARNING。
(FennoAI 式审查在 #823 上指出;本用例把它钉住。)

断言的是**协议齐全**,不真跑推理:权重 224MB + 176MB,CI 上不该下载。
"""
from windup_framework.providers import make_matte_provider
from windup_framework.providers.matte_factory import ENV, _BIREFNET, _U2NET

for choice in (_U2NET, _BIREFNET):
monkeypatch.setenv(ENV, choice)
provider = make_matte_provider()
assert callable(getattr(provider, "warmup", None)), (
f"{type(provider).__name__} 缺 warmup —— bind_matte 会被跳过,"
"共享实例失效,进程里会装多份 ONNX 会话"
)
assert callable(getattr(provider, "cutout", None))
2 changes: 1 addition & 1 deletion backend/tests/test_mq_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,7 @@ def warmup(self):

fake = _Fake()
monkeypatch.setattr(
"windup_framework.providers.OnnxU2NetMatteProvider", lambda *a, **k: fake
"windup_framework.providers.make_matte_provider", lambda *a, **k: fake
)
prev_a, prev_i = ex.executor._matte, ex.image_executor._matte
try:
Expand Down
Loading