Enhance device management and model handling in SnapForm

- Updated frame extraction logic to allow for single frame extraction.
- Integrated model readiness checks for BEN2 and DA3 models in relevant operators.
- Added directory name attribute to model specifications for better organization.
- Improved device selection logic for production and debug tasks.
- Refactored preferences to expose device settings and ensure they reflect available hardware.
- Enhanced runtime device detection and management, including support for CUDA and MPS.
- Updated tests to cover new device management features and model directory handling.
- Removed deprecated download model tests to streamline codebase.
This commit is contained in:
2026-08-08 20:16:11 +08:00
parent aee460fe7f
commit 9c896213a5
18 changed files with 699 additions and 196 deletions

View File

@@ -1,5 +1,6 @@
import json
import os
import platform
import secrets
import shutil
import socket
@@ -15,7 +16,12 @@ from urllib.request import ProxyHandler, Request, build_opener
import bpy
from .models import BEN2_MODEL, get_downloadable_model, selected_model_key
from .models import (
BEN2_MODEL,
get_downloadable_model,
model_directory_name,
selected_model_key,
)
DA3_COMMIT = "3d835ec1a5802d64a8b8b15f817a1ab54809bfe4"
@@ -107,7 +113,16 @@ def models_directory():
def model_directory(model_key):
return models_directory() / model_key.lower()
root = models_directory()
directory = root / model_directory_name(model_key)
legacy_directory = root / model_key.lower()
if (
not directory.is_dir()
and legacy_directory.is_dir()
and legacy_directory != directory
):
legacy_directory.rename(directory)
return directory
def model_ready(model_key):
@@ -117,6 +132,61 @@ def model_ready(model_key):
)
def runtime_devices():
"""Devices detected by the installed Runtime, or None when unknown."""
ready_path = runtime_directory() / "snapform_ready.json"
if not ready_path.is_file():
return None
try:
payload = json.loads(ready_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
devices = payload.get("devices")
if not isinstance(devices, list) or not devices:
return None
return [str(name).lower() for name in devices]
def machine_devices():
"""Devices the machine can run, fastest first; used before Runtime install."""
devices = ["cpu"]
if shutil.which("nvidia-smi"):
devices.insert(0, "cuda")
elif sys.platform == "darwin" and platform.machine().lower() in {
"arm64",
"aarch64",
}:
devices.insert(0, "mps")
return devices
def supported_devices():
"""Inference devices available on this machine, fastest first."""
return runtime_devices() or machine_devices()
DEVICE_LABELS = {
"cuda": ("CUDA", "NVIDIA CUDA"),
"mps": ("MPS", "Apple Metal (MPS)"),
"cpu": ("CPU", "CPU"),
}
def device_enum_items(_owner, _context):
"""Blender EnumProperty items listing only devices supported on this machine."""
return [
(name.upper(), *DEVICE_LABELS[name], index)
for index, name in enumerate(supported_devices())
]
def production_device():
"""Device configured for production tasks, falling back to Auto."""
addon_preferences = preferences()
configured = getattr(addon_preferences, "device", "")
return configured or "AUTO"
def jobs_directory():
path = storage_root() / "jobs"
path.mkdir(parents=True, exist_ok=True)
@@ -153,13 +223,7 @@ def new_job_paths(job_type):
def uv_executable():
addon_preferences = preferences()
configured = getattr(addon_preferences, "uv_executable", "uv")
resolved = shutil.which(configured)
if resolved:
return resolved
if Path(configured).is_file():
return str(Path(configured))
"""Return the bundled uv path inside the shared storage."""
return str(uv_cache_path())
@@ -171,9 +235,7 @@ def uv_cache_path():
def install_runtime_command(status_path):
executable = uv_executable()
addon_preferences = preferences()
backend = getattr(addon_preferences, "torch_backend", "CU128")
pypi_index = getattr(addon_preferences, "pypi_index_url", "") or ""
backend = "cu128" if "cuda" in machine_devices() else "cpu"
script = Path(__file__).with_name("bootstrap_runtime.py")
command = [
sys.executable,
@@ -185,7 +247,7 @@ def install_runtime_command(status_path):
"--models-dir",
str(models_directory()),
"--backend",
backend.lower(),
backend,
"--da3-commit",
DA3_COMMIT,
"--ben2-commit",
@@ -193,8 +255,6 @@ def install_runtime_command(status_path):
"--status",
str(status_path),
]
if pypi_index:
command.extend(["--pypi-index", pypi_index])
return command
@@ -640,6 +700,7 @@ def remove_background_request(
model_key,
video_frames=None,
start_frame=1,
device=None,
):
return HostJobRequest(
{
@@ -650,7 +711,7 @@ def remove_background_request(
"model_dir": str(model_directory(model_key)),
"output_dir": str(result_directory),
"status": str(status_path),
"device": settings.device.lower(),
"device": (device or settings.device).lower(),
"video_frames": (
settings.video_frames
if video_frames is None
@@ -701,6 +762,7 @@ def reconstruct_request(
use_ray_pose=None,
remove_background=False,
start_frame=1,
device=None,
):
snapform_selected_key = model_key or selected_model_key(settings, export_format)
snapform_selected_process_res = (
@@ -732,7 +794,7 @@ def reconstruct_request(
"gs_views_interval": settings.gs_views_interval,
"video_frames": settings.video_frames,
"start_frame": start_frame,
"device": settings.device.lower(),
"device": (device or settings.device).lower(),
"use_ray_pose": 1 if snapform_selected_ray_pose else 0,
"ref_view_strategy": settings.ref_view_strategy,
"remove_background_model_dir": "",