- Renamed DA3 and BEN2 debug panels for clarity and consistency. - Added a new function to draw DA3 input settings to reduce code duplication. - Implemented mesh export functionality, including confidence thresholding and edge filtering. - Updated export format options to include new mesh type and adjusted related properties. - Enhanced job handling to move generated inputs into job root and manage temporary directories. - Improved error handling and file management during export processes. - Added unit tests for new mesh functionality and ensured existing tests cover changes.
884 lines
24 KiB
Python
884 lines
24 KiB
Python
import json
|
|
import os
|
|
import platform
|
|
import secrets
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import ProxyHandler, Request, build_opener
|
|
|
|
import bpy
|
|
|
|
from .models import (
|
|
BEN2_MODEL,
|
|
get_downloadable_model,
|
|
model_directory_name,
|
|
selected_model_key,
|
|
)
|
|
|
|
|
|
DA3_COMMIT = "3d835ec1a5802d64a8b8b15f817a1ab54809bfe4"
|
|
BEN2_COMMIT = "2c99a5da477b5523585bfa5c893888a6e818a8f6"
|
|
RUNTIME_SCHEMA = 4
|
|
HOST_SCHEMA = 2
|
|
HOST_ADDRESS = "127.0.0.1"
|
|
DEFAULT_STORAGE_ROOT = Path.home() / ".snapform"
|
|
JOB_FOLDER_NAMES = {
|
|
"INSTALL_RUNTIME": "install-runtime",
|
|
"DOWNLOAD_MODEL": "download-model",
|
|
"RECONSTRUCT": "export-reconstruction",
|
|
"IMAGE_DEPTH": "generate-depth-plane",
|
|
"REMOVE_BACKGROUND": "remove-background",
|
|
"IMAGE_BACKGROUND": "remove-image-background",
|
|
"ORIGINAL_BACKGROUND": "generate-original-depth-plane",
|
|
"ORIGINAL_FRAMES": "extract-original-frames",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HostJobRequest:
|
|
payload: dict
|
|
|
|
|
|
@dataclass
|
|
class HostConnection:
|
|
port: int
|
|
token: str
|
|
pid: int
|
|
instance_id: str
|
|
|
|
|
|
@dataclass
|
|
class LaunchedHost:
|
|
process: subprocess.Popen
|
|
log_file: object
|
|
|
|
|
|
_host_connection = None
|
|
_launched_host = None
|
|
_registered_instance_id = None
|
|
_client_id = uuid.uuid4().hex
|
|
_host_auto_start = False
|
|
_host_snapshot = {"state": "STOPPED", "message": "Stopped"}
|
|
_local_opener = build_opener(ProxyHandler({}))
|
|
_client_label = f"Blender {getattr(bpy.app, 'version_string', '')}".strip()
|
|
|
|
|
|
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):
|
|
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):
|
|
path = model_directory(model_key)
|
|
return (path / "config.json").is_file() and any(
|
|
path.glob("*.safetensors")
|
|
)
|
|
|
|
|
|
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)
|
|
return path
|
|
|
|
|
|
def temporary_input_path():
|
|
directory = Path(tempfile.mkdtemp(prefix="snapform-input-"))
|
|
return directory / "input.png"
|
|
|
|
|
|
def temporary_input_directory():
|
|
return Path(tempfile.mkdtemp(prefix="snapform-input-"))
|
|
|
|
|
|
def move_input_into_job(input_path, job_directory):
|
|
"""Move a Blender-generated input copy into the flat job directory."""
|
|
input_path = Path(input_path)
|
|
job_directory = Path(job_directory)
|
|
if input_path.is_file():
|
|
destination = job_directory / "input.png"
|
|
input_path.replace(destination)
|
|
try:
|
|
input_path.parent.rmdir()
|
|
except OSError:
|
|
pass
|
|
return destination
|
|
if not input_path.is_dir():
|
|
raise ValueError(f"Temporary input does not exist: {input_path}")
|
|
for source in input_path.iterdir():
|
|
if not source.is_file():
|
|
raise ValueError(f"Temporary input contains a directory: {source}")
|
|
destination = job_directory / source.name
|
|
if destination.exists():
|
|
raise ValueError(f"Job input filename already exists: {source.name}")
|
|
source.replace(destination)
|
|
input_path.rmdir()
|
|
return job_directory
|
|
|
|
|
|
def job_folder_name(job_type):
|
|
"""Return the folder name segment for a job type."""
|
|
return JOB_FOLDER_NAMES.get(job_type, job_type.lower())
|
|
|
|
|
|
def new_job_paths(job_type):
|
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
|
|
stem = f"{job_folder_name(job_type)}_{timestamp}"
|
|
directory = jobs_directory() / stem
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
return {
|
|
"log": directory / f"{directory.name}.log",
|
|
"status": directory / f"{directory.name}.json",
|
|
"result": directory,
|
|
}
|
|
|
|
|
|
def uv_executable():
|
|
"""Return the bundled uv path inside the shared storage."""
|
|
return str(uv_cache_path())
|
|
|
|
|
|
def uv_cache_path():
|
|
if os.name == "nt":
|
|
return storage_root() / "uv" / "uv.exe"
|
|
return storage_root() / "uv" / "uv"
|
|
|
|
|
|
def install_runtime_command(status_path):
|
|
executable = uv_executable()
|
|
backend = "cu128" if "cuda" in machine_devices() else "cpu"
|
|
script = Path(__file__).with_name("bootstrap_runtime.py")
|
|
command = [
|
|
sys.executable,
|
|
str(script),
|
|
"--uv",
|
|
executable,
|
|
"--runtime-dir",
|
|
str(runtime_directory()),
|
|
"--models-dir",
|
|
str(models_directory()),
|
|
"--backend",
|
|
backend,
|
|
"--da3-commit",
|
|
DA3_COMMIT,
|
|
"--ben2-commit",
|
|
BEN2_COMMIT,
|
|
"--status",
|
|
str(status_path),
|
|
]
|
|
return command
|
|
|
|
|
|
def host_log_path():
|
|
return storage_root() / "ai-host.log"
|
|
|
|
|
|
def host_discovery_path():
|
|
return storage_root() / "host.json"
|
|
|
|
|
|
def host_lock_path():
|
|
return storage_root() / "host.lock"
|
|
|
|
|
|
def _available_host_port():
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
|
|
listener.bind((HOST_ADDRESS, 0))
|
|
return listener.getsockname()[1]
|
|
|
|
|
|
def _host_url(connection, path):
|
|
return f"http://{HOST_ADDRESS}:{connection.port}{path}"
|
|
|
|
|
|
def _host_http_request(connection, method, path, payload=None, timeout=2.0):
|
|
data = None
|
|
headers = {"X-SnapForm-Token": connection.token}
|
|
if payload is not None:
|
|
data = json.dumps(payload).encode("utf-8")
|
|
headers["Content-Type"] = "application/json"
|
|
request = Request(
|
|
_host_url(connection, path),
|
|
data=data,
|
|
headers=headers,
|
|
method=method,
|
|
)
|
|
try:
|
|
with _local_opener.open(request, timeout=timeout) as response:
|
|
return json.loads(response.read().decode("utf-8"))
|
|
except HTTPError as error:
|
|
try:
|
|
detail = json.loads(error.read().decode("utf-8")).get("detail")
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
detail = None
|
|
raise RuntimeError(detail or f"AI Host returned HTTP {error.code}")
|
|
except (OSError, URLError) as error:
|
|
raise RuntimeError(f"Unable to reach AI Host: {error}")
|
|
|
|
|
|
def _read_host_discovery():
|
|
try:
|
|
payload = json.loads(host_discovery_path().read_text(encoding="utf-8"))
|
|
return HostConnection(
|
|
port=int(payload["port"]),
|
|
token=str(payload["token"]),
|
|
pid=int(payload["pid"]),
|
|
instance_id=str(payload["instance_id"]),
|
|
)
|
|
except (KeyError, TypeError, ValueError, OSError, json.JSONDecodeError):
|
|
return None
|
|
|
|
|
|
def _check_host(connection, timeout=0.3):
|
|
health = _host_http_request(
|
|
connection,
|
|
"GET",
|
|
"/health",
|
|
timeout=timeout,
|
|
)
|
|
if health.get("host_schema") != HOST_SCHEMA:
|
|
raise RuntimeError("AI Host protocol is out of date")
|
|
if health.get("instance_id") != connection.instance_id:
|
|
raise RuntimeError("AI Host discovery is stale")
|
|
return health
|
|
|
|
|
|
def _discover_live_host(timeout=0.3):
|
|
connection = _read_host_discovery()
|
|
if connection is None:
|
|
return None, None
|
|
try:
|
|
return connection, _check_host(connection, timeout=timeout)
|
|
except RuntimeError:
|
|
return None, None
|
|
|
|
|
|
def start_host():
|
|
global _launched_host
|
|
|
|
if _launched_host is not None and _launched_host.process.poll() is None:
|
|
return
|
|
_close_launched_host()
|
|
port = _available_host_port()
|
|
token = secrets.token_urlsafe(32)
|
|
instance_id = uuid.uuid4().hex
|
|
log_file = host_log_path().open("a", encoding="utf-8")
|
|
command = [
|
|
str(runtime_python()),
|
|
"-u",
|
|
str(Path(__file__).with_name("ai_host.py")),
|
|
"--host",
|
|
HOST_ADDRESS,
|
|
"--port",
|
|
str(port),
|
|
"--token",
|
|
token,
|
|
"--instance-id",
|
|
instance_id,
|
|
"--discovery",
|
|
str(host_discovery_path()),
|
|
"--lock",
|
|
str(host_lock_path()),
|
|
]
|
|
try:
|
|
process = subprocess.Popen(
|
|
command,
|
|
stdout=log_file,
|
|
stderr=subprocess.STDOUT,
|
|
cwd=storage_root(),
|
|
creationflags=creation_flags(),
|
|
)
|
|
except OSError:
|
|
log_file.close()
|
|
raise
|
|
_launched_host = LaunchedHost(process, log_file)
|
|
|
|
|
|
def ensure_host(timeout=60.0):
|
|
global _host_auto_start, _host_connection, _host_snapshot
|
|
|
|
_host_auto_start = True
|
|
deadline = time.monotonic() + timeout
|
|
last_launch = 0.0
|
|
starting = False
|
|
while time.monotonic() < deadline:
|
|
connection, health = _discover_live_host(timeout=0.5)
|
|
if connection is not None:
|
|
_host_connection = connection
|
|
_register_host_client(connection)
|
|
_host_snapshot = _format_host_snapshot(connection, health)
|
|
_redraw_host_ui()
|
|
return connection
|
|
if not starting:
|
|
_host_snapshot = {"state": "STARTING", "message": "Starting"}
|
|
_redraw_host_ui()
|
|
starting = True
|
|
_close_finished_launched_host()
|
|
if (
|
|
_launched_host is None
|
|
and time.monotonic() - last_launch >= 1.0
|
|
):
|
|
start_host()
|
|
last_launch = time.monotonic()
|
|
time.sleep(0.05)
|
|
raise RuntimeError(f"AI Host did not become ready; log: {host_log_path()}")
|
|
|
|
|
|
def submit_host_job(request):
|
|
connection = _host_connection or ensure_host()
|
|
payload = dict(request.payload)
|
|
payload["client_id"] = _client_id
|
|
_host_http_request(
|
|
connection,
|
|
"POST",
|
|
"/jobs",
|
|
payload=payload,
|
|
timeout=5.0,
|
|
)
|
|
return connection
|
|
|
|
|
|
def stop_host():
|
|
global _host_auto_start, _host_connection, _host_snapshot
|
|
global _registered_instance_id
|
|
|
|
connection = _host_connection
|
|
if connection is None:
|
|
connection, _health = _discover_live_host(timeout=0.5)
|
|
if connection is None:
|
|
_close_launched_host(terminate=True)
|
|
_host_auto_start = False
|
|
_host_snapshot = {"state": "STOPPED", "message": "Stopped"}
|
|
return
|
|
_register_host_client(connection)
|
|
_host_http_request(
|
|
connection,
|
|
"POST",
|
|
"/shutdown",
|
|
payload={"client_id": _client_id},
|
|
timeout=2.0,
|
|
)
|
|
_host_auto_start = False
|
|
deadline = time.monotonic() + 10.0
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
_check_host(connection, timeout=0.2)
|
|
except RuntimeError:
|
|
break
|
|
time.sleep(0.05)
|
|
else:
|
|
raise RuntimeError("AI Host did not stop")
|
|
_host_connection = None
|
|
_registered_instance_id = None
|
|
_close_finished_launched_host()
|
|
_host_snapshot = {"state": "STOPPED", "message": "Stopped"}
|
|
|
|
|
|
def restart_host():
|
|
global _host_auto_start, _host_connection, _registered_instance_id
|
|
global _host_snapshot
|
|
|
|
connection = ensure_host()
|
|
_host_http_request(
|
|
connection,
|
|
"POST",
|
|
"/shutdown",
|
|
payload={"client_id": _client_id},
|
|
timeout=2.0,
|
|
)
|
|
_host_auto_start = True
|
|
_host_connection = None
|
|
_registered_instance_id = None
|
|
_host_snapshot = {"state": "STARTING", "message": "Restarting"}
|
|
|
|
|
|
def enable_host():
|
|
global _host_auto_start, _host_connection, _host_snapshot
|
|
|
|
_host_auto_start = True
|
|
connection, health = _discover_live_host(timeout=0.2)
|
|
if connection is not None:
|
|
_host_connection = connection
|
|
_register_host_client(connection)
|
|
_host_snapshot = _format_host_snapshot(connection, health)
|
|
_redraw_host_ui()
|
|
return
|
|
start_host()
|
|
_host_snapshot = {"state": "STARTING", "message": "Starting"}
|
|
_redraw_host_ui()
|
|
|
|
|
|
def clear_host_models():
|
|
connection = ensure_host()
|
|
return _host_http_request(
|
|
connection,
|
|
"POST",
|
|
"/models/clear",
|
|
timeout=5.0,
|
|
)
|
|
|
|
|
|
def cancel_host_job(job_id):
|
|
connection = ensure_host()
|
|
return _host_http_request(
|
|
connection,
|
|
"DELETE",
|
|
f"/jobs/{job_id}",
|
|
payload={"client_id": _client_id},
|
|
timeout=2.0,
|
|
)
|
|
|
|
|
|
def detach_host_client():
|
|
global _host_connection, _registered_instance_id
|
|
|
|
connection = _host_connection
|
|
if connection is not None and _registered_instance_id is not None:
|
|
try:
|
|
_host_http_request(
|
|
connection,
|
|
"DELETE",
|
|
f"/clients/{_client_id}",
|
|
timeout=0.5,
|
|
)
|
|
except RuntimeError:
|
|
pass
|
|
_host_connection = None
|
|
_registered_instance_id = None
|
|
# 如果 Host 是由当前 Blender 启动的,关闭 Blender 时直接终止它,无需等待空闲超时
|
|
_close_launched_host(terminate=True)
|
|
|
|
|
|
def schedule_host_start():
|
|
timers = getattr(bpy.app, "timers", None)
|
|
if timers is None:
|
|
return
|
|
if timers.is_registered(_service_host_timer):
|
|
return
|
|
timers.register(_service_host_timer, first_interval=1.0, persistent=True)
|
|
|
|
|
|
def unschedule_host():
|
|
timers = getattr(bpy.app, "timers", None)
|
|
if timers is None:
|
|
return
|
|
if timers.is_registered(_service_host_timer):
|
|
timers.unregister(_service_host_timer)
|
|
|
|
|
|
def host_status():
|
|
return dict(_host_snapshot)
|
|
|
|
|
|
def host_busy():
|
|
return _host_snapshot.get("state") == "BUSY"
|
|
|
|
|
|
def mark_host_job_complete(job_id):
|
|
"""Release a stale local busy snapshot after its terminal status is observed."""
|
|
global _host_snapshot
|
|
|
|
active = _host_snapshot.get("active_job")
|
|
if active and active.get("job_id") != job_id:
|
|
return
|
|
if _host_snapshot.get("state") != "BUSY":
|
|
return
|
|
_host_snapshot = {
|
|
**_host_snapshot,
|
|
"busy": False,
|
|
"active_job": None,
|
|
"state": "READY",
|
|
"message": "Ready",
|
|
}
|
|
|
|
|
|
def _service_host_timer():
|
|
global _host_connection, _host_snapshot, _registered_instance_id
|
|
|
|
if not runtime_ready():
|
|
_host_snapshot = {
|
|
"state": "UNAVAILABLE",
|
|
"message": "Runtime is not installed",
|
|
}
|
|
return 2.0
|
|
connection, health = _discover_live_host(timeout=0.2)
|
|
if connection is None:
|
|
_host_connection = None
|
|
_registered_instance_id = None
|
|
_close_finished_launched_host()
|
|
if not _host_auto_start:
|
|
if _host_snapshot.get("state") != "STOPPED":
|
|
_host_snapshot = {"state": "STOPPED", "message": "Stopped"}
|
|
_redraw_host_ui()
|
|
return 2.0
|
|
try:
|
|
start_host()
|
|
except OSError as error:
|
|
_host_snapshot = {"state": "ERROR", "message": str(error)}
|
|
return 2.0
|
|
_host_snapshot = {"state": "STARTING", "message": "Starting"}
|
|
_redraw_host_ui()
|
|
return 1.0
|
|
try:
|
|
_host_connection = connection
|
|
if _registered_instance_id == connection.instance_id:
|
|
try:
|
|
_host_http_request(
|
|
connection,
|
|
"POST",
|
|
f"/clients/{_client_id}/heartbeat",
|
|
timeout=0.5,
|
|
)
|
|
except RuntimeError:
|
|
_registered_instance_id = None
|
|
_register_host_client(connection)
|
|
health = _check_host(connection, timeout=0.5)
|
|
_host_snapshot = _format_host_snapshot(connection, health)
|
|
except RuntimeError as error:
|
|
_host_connection = None
|
|
_registered_instance_id = None
|
|
_host_snapshot = {"state": "ERROR", "message": str(error)}
|
|
_redraw_host_ui()
|
|
return 5.0
|
|
|
|
|
|
def _register_host_client(connection):
|
|
global _registered_instance_id
|
|
|
|
if _registered_instance_id == connection.instance_id:
|
|
return
|
|
_host_http_request(
|
|
connection,
|
|
"POST",
|
|
"/clients",
|
|
payload={"client_id": _client_id, "label": _client_label},
|
|
timeout=2.0,
|
|
)
|
|
_registered_instance_id = connection.instance_id
|
|
|
|
|
|
def _format_host_snapshot(connection, health):
|
|
state = "BUSY" if health.get("busy") else "READY"
|
|
return {
|
|
**health,
|
|
"state": state,
|
|
"message": "Busy" if state == "BUSY" else "Ready",
|
|
"port": connection.port,
|
|
}
|
|
|
|
|
|
def _redraw_host_ui():
|
|
if threading.current_thread() is not threading.main_thread():
|
|
return
|
|
try:
|
|
from .status_bar import redraw_ui
|
|
|
|
redraw_ui(bpy.context, force=True)
|
|
except (AttributeError, RuntimeError):
|
|
pass
|
|
|
|
|
|
def _close_finished_launched_host():
|
|
if _launched_host is None or _launched_host.process.poll() is None:
|
|
return
|
|
_close_launched_host()
|
|
|
|
|
|
def _close_launched_host(terminate=False):
|
|
global _launched_host
|
|
|
|
launched = _launched_host
|
|
_launched_host = None
|
|
if launched is None:
|
|
return
|
|
if terminate and launched.process.poll() is None:
|
|
launched.process.terminate()
|
|
try:
|
|
launched.process.wait(timeout=3.0)
|
|
except subprocess.TimeoutExpired:
|
|
launched.process.kill()
|
|
launched.process.wait(timeout=3.0)
|
|
launched.log_file.close()
|
|
|
|
|
|
def download_model_command(model_key, status_path):
|
|
model = get_downloadable_model(model_key)
|
|
command = [
|
|
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),
|
|
"--label",
|
|
model.label,
|
|
]
|
|
for pattern in model.download_patterns:
|
|
command.extend(["--allow-pattern", pattern])
|
|
return command
|
|
|
|
|
|
def remove_background_request(
|
|
settings,
|
|
input_path,
|
|
status_path,
|
|
result_directory,
|
|
model_key,
|
|
video_frames=None,
|
|
start_frame=1,
|
|
device=None,
|
|
background_depth_distance=0.0,
|
|
):
|
|
return HostJobRequest(
|
|
{
|
|
"job_id": Path(status_path).stem,
|
|
"command": "remove-background",
|
|
"parameters": {
|
|
"input": str(input_path),
|
|
"model_dir": str(model_directory(model_key)),
|
|
"output_dir": str(result_directory),
|
|
"status": str(status_path),
|
|
"device": (device or settings.device).lower(),
|
|
"video_frames": (
|
|
settings.video_frames
|
|
if video_frames is None
|
|
else video_frames
|
|
),
|
|
"start_frame": start_frame,
|
|
"background_depth_distance": float(
|
|
background_depth_distance
|
|
),
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
def extract_frames_request(
|
|
settings,
|
|
input_path,
|
|
status_path,
|
|
result_directory,
|
|
video_frames=None,
|
|
start_frame=1,
|
|
):
|
|
return HostJobRequest(
|
|
{
|
|
"job_id": Path(status_path).stem,
|
|
"command": "extract-frames",
|
|
"parameters": {
|
|
"input": str(input_path),
|
|
"output_dir": str(result_directory),
|
|
"status": str(status_path),
|
|
"video_frames": (
|
|
settings.video_frames
|
|
if video_frames is None
|
|
else video_frames
|
|
),
|
|
"start_frame": start_frame,
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
def reconstruct_request(
|
|
settings,
|
|
export_format,
|
|
status_path,
|
|
result_directory,
|
|
input_path=None,
|
|
model_key=None,
|
|
process_res=None,
|
|
process_res_method=None,
|
|
use_ray_pose=None,
|
|
remove_background=False,
|
|
background_depth_distance=0.0,
|
|
start_frame=1,
|
|
device=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
|
|
)
|
|
parameters = {
|
|
"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": snapform_selected_process_res,
|
|
"process_res_method": snapform_selected_process_res_method,
|
|
"conf_thresh_percentile": settings.conf_thresh_percentile,
|
|
"num_max_points": settings.num_max_points,
|
|
"show_cameras": 1 if settings.show_cameras else 0,
|
|
"gs_views_interval": settings.gs_views_interval,
|
|
"video_frames": settings.video_frames,
|
|
"start_frame": start_frame,
|
|
"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": "",
|
|
"background_depth_distance": float(background_depth_distance),
|
|
"mesh_filter_edges": bool(
|
|
getattr(settings, "mesh_filter_edges", True)
|
|
),
|
|
"mesh_max_edge_length": float(
|
|
getattr(settings, "mesh_max_edge_length", 0.1)
|
|
),
|
|
}
|
|
if remove_background or background_depth_distance > 0.0:
|
|
parameters["remove_background_model_dir"] = str(
|
|
model_directory(BEN2_MODEL.key)
|
|
)
|
|
return HostJobRequest(
|
|
{
|
|
"job_id": Path(status_path).stem,
|
|
"command": "reconstruct",
|
|
"parameters": parameters,
|
|
}
|
|
)
|
|
|
|
|
|
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
|