From 9dfa49602ca31132d2f5a2ffe55b7dc009b15999 Mon Sep 17 00:00:00 2001 From: nan Date: Tue, 18 Aug 2026 13:34:22 +0800 Subject: [PATCH] chore: Update version to 0.12.11, upgrade BlendJob wheel to 0.1.13, and enhance documentation for environment installation and model management --- README.md | 14 ++-- docs/development.md | 4 +- docs/features.md | 17 +++-- docs/server.md | 9 +-- pyproject.toml | 4 +- src/snapform/__init__.py | 2 - src/snapform/blender/depth_plane.py | 31 ++++++--- src/snapform/blender/surface.py | 10 +-- src/snapform/blender_manifest.toml | 4 +- src/snapform/panel.py | 7 ++ src/snapform/preferences.py | 7 ++ tests/test_addon.py | 103 ++++++++++++++++++++++++++-- tests/test_pack.py | 2 +- tests/test_server.py | 14 ++-- uv.lock | 10 +-- 15 files changed, 176 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index edaccc5..054b32d 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,18 @@ SnapForm 是一个 Blender 扩展,可以把一张照片、一段视频或一 2. 打开 Blender,进入 `Preferences > Extensions` 3. 点击 `Install from Disk`,选择下载的压缩包 4. 在扩展偏好设置中确认 Storage Root 与 Device(保持默认即可);Device 默认取本机 - 最快的 DirectML/CoreML provider,否则 CPU。下载超时会自动重试国内镜像,无需手动配置 -5. 打开 3D 视图右侧的 `SnapForm` 面板,点击 `Install Environment` + 最快的 DirectML/CoreML provider,否则 CPU +5. 打开 3D 视图右侧的 `SnapForm` 面板:使用官方源点击 `Install Environment`,使用国内镜像点击 `Install Environment (Mirror)` 扩展不内置 uv 或 AI 运行库。首次使用会先下载并校验固定版本的 uv,随后自动安装轻量的 ONNX Runtime 环境,再下载 `MoGe-2 ViT-S Normal` 和 `BEN2 Base`;不安装 PyTorch,也不下载各模型的 Python 源码包。其他 AI 模型可在 `Models` panel 中按需下载,请保持网络畅通,首次安装需要一些时间。 -安装时 Environment、MoGe-2 和 BEN2 各自使用独立的 0–100% 进度;Environment 这一轮内部依次完成 uv、Python 环境和 packages。进入后续模型时进度归零,但不显示 `2/2` 等阶段编号。模型源发生瞬时错误时会切换到另一来源继续下载。 +Environment Bootstrap 使用连续的 0–100%:uv 为 1–20%、Python 为 20–50%、packages 为 50–98%,最终验证为 98–100%。三个阶段显示真实的 `1/3`、`2/3`、`3/3`,下载占各阶段的 84–87%;开始与最终验证不显示编号,单阶段状态不显示 `1/1`。官方按钮固定使用 GitHub、uv 默认 Python 下载源和 PyPI;镜像按钮固定使用 CNB uv Release、CNB Python 解释器镜像和清华 PyPI,两种方式都不竞速或自动切换。MoGe-2 与 BEN2 属于 post-install,各自使用独立进度,不计入 Environment 的这轮 0–100%,其模型下载源逻辑不受按钮选择影响。 任一必需模型在两个来源都下载失败时,`Install Environment` 会明确报错;Environment 本身仍会保留。 选中 Image Empty 后,右键 `SnapForm` 子菜单会在缺少 `MoGe-2 ViT-S Normal` 或 `BEN2 Base` 时显示 `Download Required Models`,一次补齐缺失项,不会重复下载已经就绪的模型。 -必需模型补齐前,菜单中的 `Convert to Depth Plane` 会保持禁用。 +Environment 未安装时,同一位置显示两个安装按钮。Environment 或必需模型未就绪时, +菜单仍显示 `Convert to Depth Plane`、`Convert to Surface` 和 `Remove Background`,但三个入口都会保持禁用。 这些 UI 状态直接检查 Storage Root 的 `models/` 文件,不会仅因打开菜单或面板而启动 Job Server。 ## 快速上手 @@ -84,11 +85,10 @@ Session 逐帧推理,显存峰值不随视频帧数平方增长;Debug 多帧 ## 常见问题 -- 从 0.12.0–0.12.5 升级到 0.12.6 后,请重新执行一次 `Install Environment`,重建不再单独安装 BlendJob 的 Server Environment +- 从 0.12.0–0.12.5 升级到 0.12.6 或更高版本后,请重新执行一次 `Install Environment`,重建不再单独安装 BlendJob 的 Server Environment - 首次使用需要下载模型,如果下载慢或失败,可在 Image Empty 的 `SnapForm` 右键菜单点击 `Download Required Models`,也可在 `Models` panel 里逐项重试 -- 安装 Environment 时 uv 下载失败会自动尝试 OmooLab 镜像;Python packages 需要指定镜像时设置 - `UV_INDEX_URL` 后重新安装 +- 安装 Environment 时下载状态会显示平均速度;网络适合海外源时选择 `Install Environment`,国内网络选择 `Install Environment (Mirror)`。每个按钮固定使用对应来源,失败时不会自动切换 - 默认模型下载会在 Hugging Face 与 OmooLab 镜像之间自动回退;某一来源暂时断线不会阻止后续模型尝试另一来源 - 模型直链建连超过 10 秒,或连接后连续 60 秒没有收到数据,会切换到下一来源;不限制总下载时长 - 找不到右键入口时,请确认选中了 Empty,且显示方式为 Image diff --git a/docs/development.md b/docs/development.md index a6a72d3..0eda2c1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -69,7 +69,7 @@ flowchart LR │ │ ├── onnx_moge2.py │ │ └── onnx_runtime.py │ └── wheels/ -│ └── blendjob-0.1.8-py3-none-any.whl +│ └── blendjob-0.1.13-py3-none-any.whl ├── tests/ └── tools/ ``` @@ -85,7 +85,7 @@ uv run pytest tests/test_addon.py tests/test_manifest.py tests/test_pack.py -v uv run python pack.py ``` -`pack.py` 根据 `blender_manifest.toml` 分别生成 Windows、macOS 和 Linux 扩展包,例如 `dist/SnapForm.v0.12.6.windows-x64.zip`。更换 `src/snapform/wheels/` 中随扩展分发的 wheel 后,先刷新锁文件再测试: +`pack.py` 根据 `blender_manifest.toml` 分别生成 Windows、macOS 和 Linux 扩展包,例如 `dist/SnapForm.v0.12.11.windows-x64.zip`。更换 `src/snapform/wheels/` 中随扩展分发的 wheel 后,先刷新锁文件再测试: ```bash uv lock --refresh diff --git a/docs/features.md b/docs/features.md index 33c0531..18a26ca 100644 --- a/docs/features.md +++ b/docs/features.md @@ -38,7 +38,8 @@ - Image Empty 右键菜单可直接运行 BEN2,保留原图并换用新建的透明背景图片 - 缺少必需的 `MoGe-2 ViT-S Normal` 或 `BEN2 Base` 时,Image Empty 的 `SnapForm` 右键子菜单显示 `Download Required Models`;单次任务只下载缺失项,并在补齐前禁用 - `Convert to Depth Plane` + `Convert to Depth Plane`、`Convert to Surface` 和 `Remove Background`。Environment 未安装时, + 同一位置改为显示 `Install Environment` 与 `Install Environment (Mirror)`,三个禁用的功能入口仍然可见 - 菜单、模型面板和转换对话框直接检查 Storage Root 的 `models/` 必需文件,显示模型状态时不会启动 Job Server - Image Empty 支持视频与序列帧输入:`Convert to Depth Plane` 逐帧估计深度, `Remove Background` 逐帧抠图,入口保持不变。DA3 转换时每帧独立执行 ONNX 推理并复用模型 Session; @@ -55,20 +56,19 @@ ## 使用 1. 运行 `uv run python pack.py`,分别构建 `windows-x64`、`macos-arm64`、 - `linux-x64` 三个平台的 Extension ZIP(如 `SnapForm.v0.12.6.macos-arm64.zip`)。 - 安装包不携带 uv 或 AI 项目源码;首次安装 Environment 时下载固定版本 uv,官方源建连超过 3 秒后回退 OmooLab 镜像,并校验固定 SHA-256。 + `linux-x64` 三个平台的 Extension ZIP(如 `SnapForm.v0.12.11.macos-arm64.zip`)。 + 安装包不携带 uv 或 AI 项目源码;首次安装 Environment 时下载固定版本 uv,并校验固定 SHA-256。官方安装按钮使用 GitHub、uv 默认 Python 下载源和 PyPI;镜像按钮使用 CNB uv Release、CNB Python 解释器镜像和清华 PyPI。 2. 在 Blender 的 `Preferences > Extensions` 中选择 `Install from Disk`。 3. 在 Extension Preferences 中确认 Storage Root 与 Device。Device 是正式功能 (Image Empty 右键菜单)使用的推理设备,默认取本机最快的 DirectML/CoreML provider,否则 CPU,且只列出 本机支持的设备。 -4. 在 `Job Server` 点击 `Install Environment`。首次安装会下载 Python、ONNX Runtime、FastAPI、Uvicorn; +4. 在 `Job Server` 选择 `Install Environment` 或 `Install Environment (Mirror)`。首次安装会下载 Python、ONNX Runtime、FastAPI、Uvicorn; uv 由 Job Runtime 的 Environment 安装器下载并校验,DA3、BEN2 与 MoGe 不安装源码包, 并在 Environment 安装后自动下载 `MoGe-2 ViT-S Normal` 和 FP16 `BEN2 Base` 推荐模型。首次安装 - 依次显示 `Environment`、`MoGe-2 ViT-S Normal` 与 `BEN2 Base`;每项都独立从 0 走到 100%, - 进入下一项时进度条归零,模型 post-install 不显示 `2/2` 等阶段编号。uv、Python Environment 与 packages 合并在第一轮, + Environment Bootstrap 的 0–100% 只包含 uv、Python 与 packages:三阶段显示 `1/3`、`2/3`、`3/3`,范围分别为 1–20%、20–50%、50–98%,最终验证使用 98–100%。下载占各阶段的 84–87%;开始与验证不显示编号,单阶段状态不显示 `1/1`。 两个默认模型由普通的 `post_install(runtime)` 发起正式 Server Job。 安装完成后直接写入 Environment Manifest,不再额外导入全部组件进行验证;`DA3 Base` 等其他模型可稍后在 `Models` 面板按需下载。 - 依赖安装显示当前安装输出;模型下载显示速度,例如 + Environment packages 使用固定的 Resolving、Downloading 与 Installing 状态,原始 uv 输出写入 `install.log`。模型下载显示速度,例如 `Downloading DA3 Small 14.2 MB/s`;镜像来源显示为 `Downloading DA3 Small (mirror) 14.2 MB/s`。下载按字节连续推进,重试或切换镜像时进度不会倒退。 Environment 安装阶段与 `Models` 面板单独下载使用相同的模型名称格式,不会改写为通用的 `Downloading packages`。 @@ -76,8 +76,7 @@ 可稍后在 Image Empty 的 `SnapForm` 右键菜单用 `Download Required Models` 一次补齐缺失的 MoGe-2 ViT-S 与 BEN2,也可在 `Models` 面板单独补下。 安装不做启动网络预检;每个模型都先尝试 Hugging Face,失败后尝试 OmooLab R2,两者都失败时当前 Job 失败。 - 下载源不写入持久状态;需要手动指定依赖镜像时可通过 - `UV_INDEX_URL` 环境变量覆盖。 + 官方安装固定使用 GitHub、uv 默认 Python 下载源和 PyPI;镜像安装固定使用 CNB uv Release、`python-build-standalone` 镜像和清华 PyPI。两种方式都不测速、不竞速、不自动切换,镜像配置只注入当前安装子进程。模型仍按自身的 Hugging Face 与 OmooLab R2 逻辑下载。 5. 在 3D View 按 `N`,打开 `SnapForm` 页签。 6. 如需其他模型,展开 `Models` panel,找到模型并点击该行的 `Download`。 7. 展开默认收起的 `Debug`,选择 `Output Type` 和本次执行使用的模型。 diff --git a/docs/server.md b/docs/server.md index 092f42f..f5ab424 100644 --- a/docs/server.md +++ b/docs/server.md @@ -87,13 +87,14 @@ DA3 ONNX 使用模型的 camera decoder 恢复外参与内参,不提供 ray-po ## 安装网络与镜像 -安装器直接开始工作,不做阻塞式网络预检。packages 由 uv 安装,可用 `UV_INDEX_URL` 显式指定镜像。Windows 系统代理会注入子进程;同时支持 `HTTP_PROXY`、`HTTPS_PROXY` 与 `ALL_PROXY`。 +安装器不做带宽测速、来源竞速或失败后的自动切换。`Install Environment` 固定使用 Astral GitHub Release、uv 默认 Python 下载源和 PyPI;`Install Environment (Mirror)` 固定使用 CNB uv Release、CNB `python-build-standalone` 与清华 PyPI。两个 uv 来源使用同一固定版本和 SHA-256;下载压缩包只作临时文件,安装完成后删除。 -uv 默认从 Astral 的 GitHub Release 下载,失败后回退到 `https://mirror.omoolab.xyz/uv//`。两个来源使用相同的连续字节进度并共用固定 SHA-256 校验。下载压缩包只作临时文件,提取完成后删除。模型下载不经过 GitHub。 +镜像模式只在当前安装子进程中设置 `UV_PYTHON_INSTALL_MIRROR` 与 `UV_INDEX_URL`,不修改用户环境变量。Windows 系统代理会注入子进程;同时支持 `HTTP_PROXY`、`HTTPS_PROXY` 与 `ALL_PROXY`。post-install 模型下载保持 Hugging Face 与 OmooLab R2 的独立来源逻辑,不读取 Environment 安装模式。 + +Environment Bootstrap 的单次 0–100% 只包含 uv、Python 与 packages,不包含后续模型下载。开始显示 `Starting installation ...`;uv 使用 1–20%,显示 `1/3 Starting uv download ...`、`1/3 Downloading uv [速度]` 或镜像形式、`1/3 Installing uv ...`;Python 使用 20–50%,显示 Checking、Downloading、Installing,镜像下载带 `(mirror)`;packages 使用 50–98%,显示 Resolving、`Downloading packages [已开始数]/[解析总数]`、Installing,镜像模式带 `(mirror)`;98–100% 显示 `Verifying installation ...`。三个下载区间分别占对应阶段的 84–87%,缓存命中时直接跳过。单阶段状态不显示 `1/1`。 首次安装依次显示 Environment、MoGe-2 下载与 BEN2 下载,每项分别使用 0–100%。Environment 由 Job Runtime 依次完成 uv 下载、Python Environment 创建与 packages 安装,uv 不再占用独立一轮进度。两个模型下载由 SnapForm 的 `post_install(runtime)` 通过 `runtime.request()` 顺序发起;切换模型时进度归零,但不显示 `2/2` 等阶段编号。 -任一 post-install 模型失败时,Environment 文件仍然保留,但安装 Operator 返回失败并显示聚合后的模型错误, -不再把带 `post_install_error` 的终态报告为成功。 +任一 post-install 模型失败时,Environment 文件仍然保留,但安装 Operator 返回失败并显示原始模型错误。 ## 生命周期 diff --git a/pyproject.toml b/pyproject.toml index 37d3508..d535f39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "snapform" -version = "0.12.6" +version = "0.12.11" description = "Fast image-guided structure building tools for Blender" readme = "README.md" requires-python = ">=3.11" @@ -14,4 +14,4 @@ dev = ["numpy", "pillow", "pytest"] package = false [tool.uv.sources] -blendjob = { path = "src/snapform/wheels/blendjob-0.1.8-py3-none-any.whl" } +blendjob = { path = "src/snapform/wheels/blendjob-0.1.13-py3-none-any.whl" } diff --git a/src/snapform/__init__.py b/src/snapform/__init__.py index b2b6b65..2b66022 100644 --- a/src/snapform/__init__.py +++ b/src/snapform/__init__.py @@ -1,7 +1,5 @@ import bpy -from .blender import depth_plane as image_depth -from .blender import surface as image_mesh from .blender.depth_plane import ( ConvertToDepthPlane, RemoveImageBackground, diff --git a/src/snapform/blender/depth_plane.py b/src/snapform/blender/depth_plane.py index 7b733d6..b9631bb 100644 --- a/src/snapform/blender/depth_plane.py +++ b/src/snapform/blender/depth_plane.py @@ -1970,12 +1970,13 @@ class RemoveImageBackground(bpy.types.Operator): @classmethod def poll(cls, context): from ..job_runtime import runtime - from ..properties import BEN2_MODEL_KEY, model_ready + from ..properties import required_models_ready return ( is_image_empty(context.object) + and runtime.environment_ready() and not runtime.server_busy() - and model_ready(BEN2_MODEL_KEY) + and required_models_ready() ) def execute(self, context): @@ -2045,10 +2046,13 @@ class ConvertToDepthPlane(bpy.types.Operator): @classmethod def poll(cls, context): from ..job_runtime import runtime + from ..properties import required_models_ready return ( is_image_empty(context.object) + and runtime.environment_ready() and not runtime.server_busy() + and required_models_ready() ) def invoke(self, context, _event): @@ -2170,35 +2174,46 @@ class SnapFormImageMenu(bpy.types.Menu): layout = self.layout layout.operator_context = "INVOKE_DEFAULT" - if not runtime.environment_ready(): + environment_ready = runtime.environment_ready() + if not environment_ready: layout.operator( "snapform.install_environment", text="Install Environment", icon="IMPORT", ) - return + layout.operator( + "snapform.install_environment_mirror", + text="Install Environment (Mirror)", + icon="IMPORT", + ) models_ready = required_models_ready() - if not models_ready: + if environment_ready and not models_ready: layout.operator( "snapform.download_required_models", text="Download Required Models", icon="IMPORT", ) + features_ready = environment_ready and models_ready + if not features_ready: layout.separator() depth_plane_row = layout.row() - depth_plane_row.enabled = models_ready + depth_plane_row.enabled = features_ready depth_plane_row.operator( "snapform.convert_to_depth_plane", text="Convert to Depth Plane", icon="MOD_DISPLACE", ) - layout.operator( + surface_row = layout.row() + surface_row.enabled = features_ready + surface_row.operator( "snapform.convert_to_surface", text="Convert to Surface", icon="MESH_GRID", ) layout.separator() - layout.operator( + background_row = layout.row() + background_row.enabled = features_ready + background_row.operator( "snapform.remove_image_background", text="Remove Background", icon="IMAGE_ALPHA", diff --git a/src/snapform/blender/surface.py b/src/snapform/blender/surface.py index 7c2ce5c..4cdd79c 100644 --- a/src/snapform/blender/surface.py +++ b/src/snapform/blender/surface.py @@ -598,18 +598,14 @@ class ConvertToSurface(bpy.types.Operator): @classmethod def poll(cls, context): from ..job_runtime import runtime - from ..properties import model_catalog + from ..properties import required_models_ready return ( is_image_empty(context.object) and video_total_frames(context.object.data) == 1 + and runtime.environment_ready() and not runtime.server_busy() - and any( - model.get("ready") - and model.get("enabled") - and model.get("family") in {"moge2", "da3"} - for model in model_catalog() - ) + and required_models_ready() ) def invoke(self, context, _event): diff --git a/src/snapform/blender_manifest.toml b/src/snapform/blender_manifest.toml index 24c1028..62385a5 100644 --- a/src/snapform/blender_manifest.toml +++ b/src/snapform/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "snapform" -version = "0.12.6" +version = "0.12.11" name = "SnapForm" tagline = "Build spatial structures from images, fast" maintainer = "OmooLab" @@ -10,7 +10,7 @@ type = "add-on" blender_version_min = "4.5.0" platforms = ["windows-x64", "macos-arm64", "linux-x64"] license = ["SPDX:GPL-3.0-or-later"] -wheels = ["./wheels/blendjob-0.1.8-py3-none-any.whl"] +wheels = ["./wheels/blendjob-0.1.13-py3-none-any.whl"] [permissions] network = "Access the local Job Server and download environment dependencies and model weights" diff --git a/src/snapform/panel.py b/src/snapform/panel.py index a6bef0b..54b21da 100644 --- a/src/snapform/panel.py +++ b/src/snapform/panel.py @@ -31,6 +31,13 @@ class ServerPanel(bpy.types.Panel): text="Install Environment", icon="IMPORT", ) + mirror_row = layout.row() + mirror_row.enabled = not running + mirror_row.operator( + "snapform.install_environment_mirror", + text="Install Environment (Mirror)", + icon="IMPORT", + ) return status = runtime.server_status() diff --git a/src/snapform/preferences.py b/src/snapform/preferences.py index 694233a..8951d92 100644 --- a/src/snapform/preferences.py +++ b/src/snapform/preferences.py @@ -42,6 +42,13 @@ class SnapFormPreferences(bpy.types.AddonPreferences): text="Install Environment", icon="IMPORT", ) + mirror_row = layout.row() + mirror_row.enabled = runtime.active_job is None + mirror_row.operator( + "snapform.install_environment_mirror", + text="Install Environment (Mirror)", + icon="IMPORT", + ) layout.separator() layout.operator( "snapform.open_server_log", diff --git a/tests/test_addon.py b/tests/test_addon.py index a81283f..2977eaf 100644 --- a/tests/test_addon.py +++ b/tests/test_addon.py @@ -261,7 +261,7 @@ class AddonTest(unittest.TestCase): self.assertIn("--config", command) self.assertNotIn("--venv-dir", command) self.assertNotIn("--models-dir", command) - self.assertEqual(command[command.index("--stages") + 1], "1") + self.assertNotIn("--stages", command) def test_runtime_exposes_declared_environment_and_post_install(self): runtime = self.addon.runtime @@ -372,12 +372,14 @@ class AddonTest(unittest.TestCase): controller._update_post_progress( {"job_id": "first", "job_type": "download-model", "progress": 1.0} ) - first_stage = controller.post_status["stage"] + self.assertEqual(controller.post_status["job_id"], "first") controller._update_post_progress( {"job_id": "second", "job_type": "download-model", "progress": 0.1} ) - self.assertGreater(controller.post_status["stage"], first_stage) + self.assertEqual(controller.post_status["job_id"], "second") + self.assertEqual(controller.post_status["progress"], 0.1) + self.assertNotIn("stage", controller.post_status) self.assertNotIn("stages", controller.post_status) def test_conversion_model_lists_default_to_moge2_vits(self): @@ -442,7 +444,94 @@ class AddonTest(unittest.TestCase): ("snapform.download_required_models", True), ) self.assertIn(("snapform.convert_to_depth_plane", False), calls) - self.assertIn(("snapform.convert_to_surface", True), calls) + self.assertIn(("snapform.convert_to_surface", False), calls) + self.assertIn(("snapform.remove_image_background", False), calls) + + def test_image_menu_keeps_disabled_features_when_environment_is_missing(self): + calls = [] + + class Layout: + operator_context = "" + enabled = True + + def __init__(self, enabled=True): + self.enabled = enabled + + def row(self): + return Layout(self.enabled) + + def operator(self, identifier, **_options): + calls.append((identifier, self.enabled)) + return SimpleNamespace() + + def separator(self): + calls.append(("separator", self.enabled)) + + menu = self.addon.SnapFormImageMenu() + menu.layout = Layout() + with patch.object( + self.addon.runtime, + "environment_ready", + return_value=False, + ), patch( + "snapform.properties.required_models_ready", + return_value=False, + ): + menu.draw(None) + + self.assertEqual(calls[0], ("snapform.install_environment", True)) + self.assertEqual( + calls[1], + ("snapform.install_environment_mirror", True), + ) + self.assertNotIn(("snapform.download_required_models", True), calls) + self.assertIn(("snapform.convert_to_depth_plane", False), calls) + self.assertIn(("snapform.convert_to_surface", False), calls) + self.assertIn(("snapform.remove_image_background", False), calls) + + def test_image_operators_require_environment_and_required_models(self): + context = SimpleNamespace( + object=SimpleNamespace(data=SimpleNamespace()), + ) + operators = ( + (self.addon.image_depth, self.addon.ConvertToDepthPlane), + (self.addon.image_mesh, self.addon.ConvertToSurface), + (self.addon.image_depth, self.addon.RemoveImageBackground), + ) + + states = ( + (False, True, False), + (True, False, False), + (True, True, True), + ) + for environment_ready, models_ready, expected in states: + with self.subTest( + environment_ready=environment_ready, + models_ready=models_ready, + expected=expected, + ), patch.object( + self.addon.runtime, + "environment_ready", + return_value=environment_ready, + ), patch.object( + self.addon.runtime, + "server_busy", + return_value=False, + ), patch( + "snapform.properties.required_models_ready", + return_value=models_ready, + ): + for module, operator in operators: + with self.subTest(operator=operator.__name__), patch.object( + module, + "is_image_empty", + return_value=True, + ), patch.object( + module, + "video_total_frames", + return_value=1, + ): + self.assertEqual(operator.poll(context), expected) def test_ui_reads_required_model_files_without_starting_server(self): properties = importlib.import_module("snapform.properties") @@ -490,8 +579,10 @@ class AddonTest(unittest.TestCase): ) def test_server_request_returns_job_result_and_calls_response(self): - blendjob = importlib.import_module("blendjob") - controller = blendjob.ServerController.__new__(blendjob.ServerController) + controller_type = importlib.import_module( + "blendjob.controller" + ).ServerController + controller = controller_type.__new__(controller_type) statuses = iter( ( {"state": "running", "progress": 0.5}, diff --git a/tests/test_pack.py b/tests/test_pack.py index 5bd31ba..e193c38 100644 --- a/tests/test_pack.py +++ b/tests/test_pack.py @@ -73,7 +73,7 @@ class PackTest(unittest.TestCase): self.assertNotIn("server/jobs.py", names) self.assertIn("server/__init__.py", names) self.assertIn( - "wheels/blendjob-0.1.8-py3-none-any.whl", + "wheels/blendjob-0.1.13-py3-none-any.whl", names, ) self.assertFalse( diff --git a/tests/test_server.py b/tests/test_server.py index 9bb08b4..15881fa 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -105,10 +105,10 @@ class ServerTest(unittest.TestCase): def wait_for_job(self, context, timeout=2.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: - if context.snapshot()["state"] in {"succeeded", "failed", "cancelled"}: - return context.snapshot() + if context._snapshot()["state"] in {"succeeded", "failed", "cancelled"}: + return context._snapshot() time.sleep(0.01) - self.fail(f"Job did not finish: {context.snapshot()}") + self.fail(f"Job did not finish: {context._snapshot()}") def test_ben2_model_is_reused_for_matching_runtime_key(self): loads = [] @@ -267,7 +267,7 @@ class ServerTest(unittest.TestCase): server = self.create_server() context = server.submit("remove-background", {}, job_id="job") self.assertIsNotNone(server.cancel("job")) - self.assertTrue(context.is_cancelled()) + self.assertTrue(context._is_cancelled()) status = self.wait_for_job(context) self.assertEqual(status["state"], "cancelled") @@ -291,8 +291,8 @@ class ServerTest(unittest.TestCase): self.assertRegex(context.job_id, r"^[0-9a-f]{32}$") self.assertEqual(context.directory.name, context.job_id) - self.assertEqual(context.snapshot()["state"], "succeeded") - self.assertEqual(context.snapshot()["progress"], 1.0) + self.assertEqual(context._snapshot()["state"], "succeeded") + self.assertEqual(context._snapshot()["progress"], 1.0) def test_runtime_storage_root_owns_the_jobs_directory(self): storage_root = self.storage_root @@ -308,7 +308,7 @@ class ServerTest(unittest.TestCase): self.assertEqual(server.jobs_directory, storage_root / "jobs") self.assertEqual(context.storage_root, storage_root.resolve()) - self.assertEqual(context.snapshot()["result"], {"value": 42}) + self.assertEqual(context._snapshot()["result"], {"value": 42}) def test_job_server_owns_and_clears_shared_resources(self): events = [] diff --git a/uv.lock b/uv.lock index 72b50af..d8a403b 100644 --- a/uv.lock +++ b/uv.lock @@ -8,10 +8,10 @@ resolution-markers = [ [[package]] name = "blendjob" -version = "0.1.8" -source = { path = "src/snapform/wheels/blendjob-0.1.8-py3-none-any.whl" } +version = "0.1.13" +source = { path = "src/snapform/wheels/blendjob-0.1.13-py3-none-any.whl" } wheels = [ - { filename = "blendjob-0.1.8-py3-none-any.whl", hash = "sha256:9e94b42dfe0dde62702dff119243d5db3bfd4b4b3f0553191aaada1452b4338d" }, + { filename = "blendjob-0.1.13-py3-none-any.whl", hash = "sha256:346aecb6bdd7f63c801c0e7c935fab25c3e3132af195d0f8794975fffa33feda" }, ] [package.metadata] @@ -305,7 +305,7 @@ wheels = [ [[package]] name = "snapform" -version = "0.12.6" +version = "0.12.11" source = { virtual = "." } dependencies = [ { name = "blendjob" }, @@ -320,7 +320,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "blendjob", path = "src/snapform/wheels/blendjob-0.1.8-py3-none-any.whl" }] +requires-dist = [{ name = "blendjob", path = "src/snapform/wheels/blendjob-0.1.13-py3-none-any.whl" }] [package.metadata.requires-dev] dev = [