Add SnapForm panels, preferences, properties, and runtime management
- Implemented RuntimeModelsPanel and DebugPanel for UI integration in Blender. - Created SnapFormPreferences to manage user settings for storage and runtime configuration. - Defined SnapFormSettings to hold various properties related to input, model selection, and export options. - Developed runtime management functions for installation, model downloading, and reconstruction commands. - Added status bar functionality to display job progress and status in the Blender UI. - Implemented worker script for handling model downloads and depth reconstruction tasks. - Introduced unit tests for download progress reporting and image depth extraction functionalities.
This commit is contained in:
218
src/snapform/runtime.py
Normal file
218
src/snapform/runtime.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from .models import get_model, selected_model_key
|
||||
|
||||
|
||||
DA3_COMMIT = "3d835ec1a5802d64a8b8b15f817a1ab54809bfe4"
|
||||
RUNTIME_SCHEMA = 2
|
||||
DEFAULT_STORAGE_ROOT = Path.home() / ".snapform"
|
||||
|
||||
|
||||
def preferences():
|
||||
addon = bpy.context.preferences.addons.get(__package__)
|
||||
return addon.preferences if addon is not None else None
|
||||
|
||||
|
||||
def storage_root():
|
||||
addon_preferences = preferences()
|
||||
configured = getattr(addon_preferences, "storage_root", "")
|
||||
root = (
|
||||
Path(bpy.path.abspath(configured)).expanduser()
|
||||
if configured
|
||||
else DEFAULT_STORAGE_ROOT
|
||||
)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
def runtime_directory():
|
||||
return storage_root() / "runtime"
|
||||
|
||||
|
||||
def runtime_python():
|
||||
directory = runtime_directory()
|
||||
if os.name == "nt":
|
||||
return directory / "Scripts" / "python.exe"
|
||||
return directory / "bin" / "python"
|
||||
|
||||
|
||||
def runtime_ready():
|
||||
ready_path = runtime_directory() / "snapform_ready.json"
|
||||
if not runtime_python().is_file() or not ready_path.is_file():
|
||||
return False
|
||||
try:
|
||||
ready = json.loads(ready_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return False
|
||||
return ready.get("runtime_schema") == RUNTIME_SCHEMA
|
||||
|
||||
|
||||
def models_directory():
|
||||
path = storage_root() / "models"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def model_directory(model_key):
|
||||
return models_directory() / model_key.lower()
|
||||
|
||||
|
||||
def model_ready(model_key):
|
||||
path = model_directory(model_key)
|
||||
return (path / "config.json").is_file() and any(
|
||||
path.glob("*.safetensors")
|
||||
)
|
||||
|
||||
|
||||
def jobs_directory():
|
||||
path = storage_root() / "jobs"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def new_job_paths(job_type):
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
token = uuid.uuid4().hex[:8]
|
||||
stem = f"{timestamp}-{job_type.lower()}-{token}"
|
||||
directory = jobs_directory()
|
||||
return {
|
||||
"log": directory / f"{stem}.log",
|
||||
"status": directory / f"{stem}.json",
|
||||
"result": directory / stem,
|
||||
}
|
||||
|
||||
|
||||
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))
|
||||
raise RuntimeError("uv executable was not found")
|
||||
|
||||
|
||||
def install_runtime_command(status_path):
|
||||
executable = uv_executable()
|
||||
addon_preferences = preferences()
|
||||
backend = getattr(addon_preferences, "torch_backend", "CU128")
|
||||
script = Path(__file__).with_name("bootstrap_runtime.py")
|
||||
return [
|
||||
executable,
|
||||
"run",
|
||||
"--no-project",
|
||||
"--python",
|
||||
"3.12",
|
||||
str(script),
|
||||
"--uv",
|
||||
executable,
|
||||
"--runtime-dir",
|
||||
str(runtime_directory()),
|
||||
"--backend",
|
||||
backend.lower(),
|
||||
"--da3-commit",
|
||||
DA3_COMMIT,
|
||||
"--status",
|
||||
str(status_path),
|
||||
]
|
||||
|
||||
|
||||
def download_model_command(model_key, status_path):
|
||||
model = get_model(model_key)
|
||||
return [
|
||||
str(runtime_python()),
|
||||
str(Path(__file__).with_name("worker.py")),
|
||||
"download",
|
||||
"--repository",
|
||||
model.repository,
|
||||
"--model-dir",
|
||||
str(model_directory(model_key)),
|
||||
"--status",
|
||||
str(status_path),
|
||||
]
|
||||
|
||||
|
||||
def reconstruct_command(
|
||||
settings,
|
||||
export_format,
|
||||
status_path,
|
||||
result_directory,
|
||||
input_path=None,
|
||||
model_key=None,
|
||||
process_res=None,
|
||||
process_res_method=None,
|
||||
use_ray_pose=None,
|
||||
):
|
||||
snapform_selected_key = model_key or selected_model_key(settings, export_format)
|
||||
snapform_selected_process_res = (
|
||||
settings.process_res if process_res is None else process_res
|
||||
)
|
||||
snapform_selected_process_res_method = (
|
||||
settings.process_res_method
|
||||
if process_res_method is None
|
||||
else process_res_method
|
||||
)
|
||||
snapform_selected_ray_pose = (
|
||||
settings.use_ray_pose if use_ray_pose is None else use_ray_pose
|
||||
)
|
||||
return [
|
||||
str(runtime_python()),
|
||||
str(Path(__file__).with_name("worker.py")),
|
||||
"reconstruct",
|
||||
"--input",
|
||||
str(
|
||||
Path(input_path)
|
||||
if input_path is not None
|
||||
else Path(bpy.path.abspath(settings.input_path))
|
||||
),
|
||||
"--model-dir",
|
||||
str(model_directory(snapform_selected_key)),
|
||||
"--output-dir",
|
||||
str(result_directory),
|
||||
"--export-format",
|
||||
export_format,
|
||||
"--status",
|
||||
str(status_path),
|
||||
"--process-res",
|
||||
str(snapform_selected_process_res),
|
||||
"--process-res-method",
|
||||
snapform_selected_process_res_method,
|
||||
"--conf-thresh-percentile",
|
||||
str(settings.conf_thresh_percentile),
|
||||
"--num-max-points",
|
||||
str(settings.num_max_points),
|
||||
"--show-cameras",
|
||||
"1" if settings.show_cameras else "0",
|
||||
"--gs-views-interval",
|
||||
str(settings.gs_views_interval),
|
||||
"--video-frames",
|
||||
str(settings.video_frames),
|
||||
"--device",
|
||||
settings.device.lower(),
|
||||
"--use-ray-pose",
|
||||
"1" if snapform_selected_ray_pose else "0",
|
||||
"--ref-view-strategy",
|
||||
settings.ref_view_strategy,
|
||||
]
|
||||
|
||||
|
||||
def creation_flags():
|
||||
if os.name != "nt":
|
||||
return 0
|
||||
return subprocess.CREATE_NO_WINDOW
|
||||
|
||||
|
||||
def read_status(path):
|
||||
try:
|
||||
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||
return None
|
||||
Reference in New Issue
Block a user