Bump version to 0.10.11 and update installation instructions; enhance progress reporting and estimated time features
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "snapform"
|
||||
version = "0.10.10"
|
||||
version = "0.10.11"
|
||||
name = "SnapForm"
|
||||
tagline = "Build spatial structures from images, fast"
|
||||
maintainer = "OmooLab"
|
||||
|
||||
@@ -39,16 +39,24 @@ TORCH_INDEXES = {
|
||||
"cpu": "https://download.pytorch.org/whl/cpu",
|
||||
}
|
||||
_resolved_pypi_index = None
|
||||
DEFAULT_MODEL_KEYS = ("SMALL", "BASE", BEN2_MODEL.key)
|
||||
RUNTIME_SLICE_START = 0.07
|
||||
RUNTIME_SLICE_END = 0.88
|
||||
VERIFY_SLICE_START = 0.88
|
||||
VERIFY_SLICE_END = 0.90
|
||||
MODELS_SLICE_START = 0.90
|
||||
DEFAULT_MODEL_KEYS = ("SMALL", BEN2_MODEL.key)
|
||||
RUNTIME_STAGE_COUNT = 3
|
||||
ENVIRONMENT_SLICE = (0.02, 0.06)
|
||||
PYTORCH_SLICE = (0.06, 0.48)
|
||||
XFORMERS_SLICE = (0.48, 0.62)
|
||||
AI_COMPONENTS_SLICE = (0.62, 0.85)
|
||||
MODELS_SLICE_START = 0.85
|
||||
MODELS_SLICE_END = 1.0
|
||||
PLAIN_SLICE_SPAN = 0.02
|
||||
RAMP_RATE = 0.02
|
||||
CREEP_CAP = 0.60
|
||||
INSTALL_ESTIMATES = {
|
||||
"PyTorch": 8 * 60,
|
||||
"AI components": 8 * 60,
|
||||
}
|
||||
INSTALL_ESTIMATE_RANGES = {
|
||||
"PyTorch": PYTORCH_SLICE,
|
||||
"AI components": (XFORMERS_SLICE[0], AI_COMPONENTS_SLICE[1]),
|
||||
}
|
||||
CHECK_CODE = (
|
||||
"import json; import torch; "
|
||||
"from depth_anything_3.api import DepthAnything3; "
|
||||
@@ -70,6 +78,7 @@ def write_status(
|
||||
stage=None,
|
||||
stages=None,
|
||||
stage_label=None,
|
||||
eta_seconds=None,
|
||||
):
|
||||
payload = {"progress": progress, "message": message}
|
||||
if error:
|
||||
@@ -78,6 +87,8 @@ def write_status(
|
||||
payload["stage"] = stage
|
||||
payload["stages"] = stages
|
||||
payload["stage_label"] = stage_label
|
||||
if eta_seconds is not None:
|
||||
payload["eta_seconds"] = max(int(eta_seconds), 0)
|
||||
Path(path).write_text(
|
||||
json.dumps(payload, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
@@ -120,6 +131,21 @@ def parse_package_count(line):
|
||||
return 0
|
||||
|
||||
|
||||
def estimated_remaining(label, progress, elapsed=0.0):
|
||||
expected_seconds = INSTALL_ESTIMATES.get(label)
|
||||
progress_range = INSTALL_ESTIMATE_RANGES.get(label)
|
||||
if expected_seconds is None or progress_range is None:
|
||||
return None
|
||||
start, end = progress_range
|
||||
if end <= start:
|
||||
return 0.0
|
||||
remaining_fraction = 1.0 - min(
|
||||
max((progress - start) / (end - start), 0.0),
|
||||
1.0,
|
||||
)
|
||||
return max(expected_seconds * remaining_fraction - elapsed, 0.0)
|
||||
|
||||
|
||||
def effective_torch_index(backend):
|
||||
return os.environ.get("SNAPFORM_TORCH_INDEX") or TORCH_INDEXES.get(backend)
|
||||
|
||||
@@ -166,16 +192,6 @@ def _run_install_once(
|
||||
|
||||
threading.Thread(target=read_output, daemon=True).start()
|
||||
|
||||
def write(progress, message):
|
||||
write_status(
|
||||
status_path,
|
||||
progress,
|
||||
message,
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
)
|
||||
|
||||
def eased(fraction):
|
||||
return start + min(max(fraction, 0.0), 1.0) * (end - start)
|
||||
|
||||
@@ -186,6 +202,21 @@ def _run_install_once(
|
||||
started = clock()
|
||||
last_tick = started
|
||||
last_heartbeat = 0.0
|
||||
def detail(message):
|
||||
write_status(
|
||||
status_path,
|
||||
displayed,
|
||||
message,
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
eta_seconds=estimated_remaining(
|
||||
label,
|
||||
displayed,
|
||||
clock() - started,
|
||||
),
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
raw_line = output_queue.get(timeout=0.2)
|
||||
@@ -206,7 +237,7 @@ def _run_install_once(
|
||||
if empty:
|
||||
if elapsed - last_heartbeat >= 5.0:
|
||||
last_heartbeat = elapsed
|
||||
write(displayed, f"downloading {format_elapsed(elapsed)}")
|
||||
detail("Downloading packages")
|
||||
continue
|
||||
line = raw_line.rstrip()
|
||||
print(line)
|
||||
@@ -215,17 +246,17 @@ def _run_install_once(
|
||||
resolved_progress = eased(0.02)
|
||||
displayed = max(displayed, resolved_progress)
|
||||
target = max(target, resolved_progress)
|
||||
write(displayed, f"{total} packages")
|
||||
detail(f"Resolving {total} packages")
|
||||
elif line.startswith("Prepared ") and "package" in line:
|
||||
done = parse_package_count(line)
|
||||
if total:
|
||||
target = max(target, eased(0.10 + 0.60 * done / total))
|
||||
write(displayed, f"{done}/{total} packages")
|
||||
detail(f"Downloading packages {done}/{total}")
|
||||
elif line.startswith("Installed ") and "package" in line:
|
||||
done = parse_package_count(line)
|
||||
if total:
|
||||
target = max(target, eased(0.75 + 0.25 * done / total))
|
||||
write(displayed, f"{done}/{total} packages")
|
||||
detail(f"Installing packages {done}/{total}")
|
||||
elif line.startswith("+"):
|
||||
entry = line[1:].strip()
|
||||
installed.append(entry)
|
||||
@@ -315,19 +346,19 @@ def install_stages(args, runtime_dir, python, uv):
|
||||
torch_command.extend(["--index-url", torch_index])
|
||||
stages = [
|
||||
(
|
||||
"uv",
|
||||
"Environment",
|
||||
[uv, "venv", "--clear", "--python", "3.12", runtime_dir],
|
||||
None,
|
||||
"plain",
|
||||
),
|
||||
(
|
||||
"pytorch deps",
|
||||
"PyTorch",
|
||||
torch_command,
|
||||
None,
|
||||
"install",
|
||||
),
|
||||
(
|
||||
"ai deps",
|
||||
"AI components",
|
||||
[
|
||||
uv,
|
||||
"pip",
|
||||
@@ -363,7 +394,7 @@ def install_stages(args, runtime_dir, python, uv):
|
||||
xformers_command.append("--no-deps")
|
||||
stages.insert(
|
||||
2,
|
||||
("xformers", xformers_command, None, "install"),
|
||||
("AI components", xformers_command, None, "install"),
|
||||
)
|
||||
models = {**MODELS, BEN2_MODEL.key: BEN2_MODEL}
|
||||
worker_script = Path(__file__).with_name("worker.py")
|
||||
@@ -380,12 +411,14 @@ def install_stages(args, runtime_dir, python, uv):
|
||||
models_dir / model.directory_name,
|
||||
"--status",
|
||||
args.status,
|
||||
"--label",
|
||||
model.label,
|
||||
]
|
||||
for pattern in model.download_patterns:
|
||||
download_command.extend(["--allow-pattern", pattern])
|
||||
stages.append(
|
||||
(
|
||||
model.label.lower(),
|
||||
model.label,
|
||||
download_command,
|
||||
None,
|
||||
"download",
|
||||
@@ -405,6 +438,17 @@ def runtime_ready(runtime_dir):
|
||||
return payload.get("runtime_schema") == RUNTIME_SCHEMA
|
||||
|
||||
|
||||
def installed_devices(args):
|
||||
devices = []
|
||||
machine = platform.machine().lower()
|
||||
if sys.platform == "darwin" and machine in {"arm64", "aarch64"}:
|
||||
devices.append("mps")
|
||||
elif args.backend.startswith("cu"):
|
||||
devices.append("cuda")
|
||||
devices.append("cpu")
|
||||
return devices
|
||||
|
||||
|
||||
def verify_command(python):
|
||||
return [python, "-c", CHECK_CODE]
|
||||
|
||||
@@ -412,9 +456,12 @@ def verify_command(python):
|
||||
def run_verify(
|
||||
python,
|
||||
status_path,
|
||||
start=VERIFY_SLICE_START,
|
||||
end=VERIFY_SLICE_END,
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
clock=time.monotonic,
|
||||
stage=None,
|
||||
stages=None,
|
||||
stage_label=None,
|
||||
):
|
||||
"""Run the runtime import check with heartbeat progress; return the device report."""
|
||||
command = verify_command(python)
|
||||
@@ -457,7 +504,11 @@ def run_verify(
|
||||
write_status(
|
||||
status_path,
|
||||
progress,
|
||||
f"verifying runtime {format_elapsed(elapsed)}",
|
||||
"Checking Runtime",
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=stage_label,
|
||||
eta_seconds=max(60.0 - elapsed, 0.0),
|
||||
)
|
||||
continue
|
||||
line = raw_line.rstrip()
|
||||
@@ -585,13 +636,20 @@ def run_stage(
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
):
|
||||
if stage is None:
|
||||
opening_message = f"Downloading {label}"
|
||||
elif kind == "install":
|
||||
opening_message = "Preparing packages"
|
||||
else:
|
||||
opening_message = ""
|
||||
write_status(
|
||||
status_path,
|
||||
start,
|
||||
"",
|
||||
opening_message,
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
eta_seconds=estimated_remaining(label, start),
|
||||
)
|
||||
installed = []
|
||||
if kind == "install":
|
||||
@@ -606,28 +664,31 @@ def run_stage(
|
||||
end=end,
|
||||
)
|
||||
elif kind == "download":
|
||||
run(
|
||||
[
|
||||
*command,
|
||||
"--progress-start",
|
||||
str(start),
|
||||
"--progress-end",
|
||||
str(end),
|
||||
"--stage",
|
||||
str(stage),
|
||||
"--stages",
|
||||
str(stages),
|
||||
"--stage-label",
|
||||
label,
|
||||
],
|
||||
environment,
|
||||
)
|
||||
progress_command = [
|
||||
*command,
|
||||
"--progress-start",
|
||||
str(start),
|
||||
"--progress-end",
|
||||
str(end),
|
||||
]
|
||||
if stage is not None:
|
||||
progress_command.extend(
|
||||
[
|
||||
"--stage",
|
||||
str(stage),
|
||||
"--stages",
|
||||
str(stages),
|
||||
"--stage-label",
|
||||
label,
|
||||
]
|
||||
)
|
||||
run(progress_command, environment)
|
||||
else:
|
||||
run(command, environment)
|
||||
write_status(
|
||||
status_path,
|
||||
end,
|
||||
"",
|
||||
f"{label} ready" if stage is None else "",
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
@@ -662,44 +723,45 @@ def install(args):
|
||||
stages = install_stages(args, runtime_dir, python, uv)
|
||||
runtime_stages = [stage for stage in stages if stage[3] != "download"]
|
||||
model_stages = [stage for stage in stages if stage[3] == "download"]
|
||||
plain_count = sum(1 for stage in runtime_stages if stage[3] != "install")
|
||||
runtime_slice = RUNTIME_SLICE_END - RUNTIME_SLICE_START
|
||||
install_count = len(runtime_stages) - plain_count
|
||||
install_span = (
|
||||
(runtime_slice - PLAIN_SLICE_SPAN * plain_count) / install_count
|
||||
if install_count
|
||||
else 0.0
|
||||
)
|
||||
model_span = (
|
||||
(MODELS_SLICE_END - MODELS_SLICE_START) / len(model_stages)
|
||||
if model_stages
|
||||
else 0.0
|
||||
)
|
||||
installed_packages = []
|
||||
runtime_offset = RUNTIME_SLICE_START
|
||||
for index, (label, command, environment, kind) in enumerate(
|
||||
runtime_stages,
|
||||
start=1,
|
||||
):
|
||||
stage_start = runtime_offset
|
||||
stage_end = stage_start + (
|
||||
install_span if kind == "install" else PLAIN_SLICE_SPAN
|
||||
)
|
||||
if uv_missing and index == 1:
|
||||
component_stages = runtime_stages[2:]
|
||||
component_ranges = (
|
||||
[XFORMERS_SLICE, AI_COMPONENTS_SLICE]
|
||||
if len(component_stages) == 2
|
||||
else [(XFORMERS_SLICE[0], AI_COMPONENTS_SLICE[1])]
|
||||
)
|
||||
for index, (label, command, environment, kind) in enumerate(runtime_stages):
|
||||
if index == 0:
|
||||
stage = 1
|
||||
stage_start, stage_end = ENVIRONMENT_SLICE
|
||||
elif index == 1:
|
||||
stage = 2
|
||||
stage_start, stage_end = PYTORCH_SLICE
|
||||
else:
|
||||
stage = 3
|
||||
component_index = index - 2
|
||||
stage_start, stage_end = component_ranges[component_index]
|
||||
if uv_missing and index == 0:
|
||||
midpoint = stage_start + (stage_end - stage_start) * 0.5
|
||||
install_uv(
|
||||
uv,
|
||||
args.status,
|
||||
stage=index,
|
||||
stages=len(stages),
|
||||
stage=stage,
|
||||
stages=RUNTIME_STAGE_COUNT,
|
||||
stage_label=label,
|
||||
start=stage_start,
|
||||
end=stage_start + PLAIN_SLICE_SPAN * 0.5,
|
||||
end=midpoint,
|
||||
)
|
||||
stage_start += PLAIN_SLICE_SPAN * 0.5
|
||||
stage_start = midpoint
|
||||
stage_installed = run_stage(
|
||||
args.status,
|
||||
index,
|
||||
len(stages),
|
||||
stage,
|
||||
RUNTIME_STAGE_COUNT,
|
||||
label,
|
||||
command,
|
||||
environment,
|
||||
@@ -709,17 +771,7 @@ def install(args):
|
||||
)
|
||||
if isinstance(stage_installed, list):
|
||||
installed_packages.extend(stage_installed)
|
||||
runtime_offset = stage_end
|
||||
write_status(args.status, VERIFY_SLICE_START, "verifying runtime")
|
||||
device_report = run_verify(python, args.status)
|
||||
write_status(args.status, VERIFY_SLICE_END, "Runtime is ready")
|
||||
|
||||
devices = []
|
||||
if device_report.get("cuda"):
|
||||
devices.append("cuda")
|
||||
if device_report.get("mps"):
|
||||
devices.append("mps")
|
||||
devices.append("cpu")
|
||||
devices = installed_devices(args)
|
||||
runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||
ready_path = runtime_dir / "snapform_ready.json"
|
||||
ready_path.write_text(
|
||||
@@ -741,19 +793,16 @@ def install(args):
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
offset = len(runtime_stages)
|
||||
model_offset = MODELS_SLICE_START
|
||||
for index, (label, command, environment, kind) in enumerate(
|
||||
model_stages,
|
||||
start=offset + 1,
|
||||
):
|
||||
failed_models = []
|
||||
for label, command, environment, kind in model_stages:
|
||||
stage_start = model_offset
|
||||
stage_end = stage_start + model_span
|
||||
try:
|
||||
run_stage(
|
||||
args.status,
|
||||
index,
|
||||
len(stages),
|
||||
None,
|
||||
None,
|
||||
label,
|
||||
command,
|
||||
environment,
|
||||
@@ -763,14 +812,22 @@ def install(args):
|
||||
)
|
||||
except Exception as error:
|
||||
print(f"Model download failed: {label}: {error}")
|
||||
failed_models.append(label)
|
||||
write_status(
|
||||
args.status,
|
||||
MODELS_SLICE_END,
|
||||
f"Runtime is ready; {label} failed",
|
||||
stage_end,
|
||||
f"Skipping {label}; continuing",
|
||||
)
|
||||
return
|
||||
model_offset = stage_end
|
||||
write_status(args.status, 1.0, "Runtime and models ready")
|
||||
if failed_models:
|
||||
labels = ", ".join(failed_models)
|
||||
write_status(
|
||||
args.status,
|
||||
1.0,
|
||||
f"Runtime is ready; retry {labels} from Models",
|
||||
)
|
||||
return
|
||||
write_status(args.status, 1.0, "Runtime and recommended models ready")
|
||||
|
||||
|
||||
def parse_args():
|
||||
|
||||
@@ -475,23 +475,53 @@ class RunJob(bpy.types.Operator):
|
||||
if status is None:
|
||||
return
|
||||
settings = context.scene.snapform_settings
|
||||
settings.progress = float(status.get("progress", settings.progress))
|
||||
reported_progress = float(
|
||||
status.get("progress", settings.progress)
|
||||
)
|
||||
settings.progress = max(
|
||||
settings.progress,
|
||||
min(max(reported_progress, 0.0), 1.0),
|
||||
)
|
||||
settings.status = self._format_status(status, settings.status)
|
||||
self._redraw_ui(context, force=True)
|
||||
|
||||
def _format_status(self, status, fallback):
|
||||
message = status.get("message", fallback)
|
||||
message = status.get("message")
|
||||
stage = status.get("stage")
|
||||
stages = status.get("stages")
|
||||
stage_label = status.get("stage_label")
|
||||
if not stage or not stages:
|
||||
return message
|
||||
text = (
|
||||
f"{stage}/{stages} {stage_label}"
|
||||
if stage_label
|
||||
else f"{stage}/{stages}"
|
||||
)
|
||||
return f"{text}: {message}" if message else text
|
||||
parts = []
|
||||
if stage and stages:
|
||||
stage_text = f"{stage}/{stages}"
|
||||
if stage_label:
|
||||
stage_text = f"{stage_text} {stage_label}"
|
||||
parts.append(stage_text)
|
||||
elif stage_label:
|
||||
parts.append(str(stage_label))
|
||||
if message:
|
||||
normalized = str(message).strip()
|
||||
if normalized.lower() != str(stage_label or "").lower():
|
||||
parts.append(normalized)
|
||||
eta = self._format_eta(status.get("eta_seconds"))
|
||||
if eta:
|
||||
parts.append(eta)
|
||||
return " | ".join(parts) if parts else (message or fallback)
|
||||
|
||||
@staticmethod
|
||||
def _format_eta(seconds):
|
||||
if seconds is None:
|
||||
return ""
|
||||
seconds = max(float(seconds), 0.0)
|
||||
if seconds <= 10:
|
||||
return "A few seconds left"
|
||||
if seconds < 60:
|
||||
rounded = max(5, int((seconds + 2.5) // 5) * 5)
|
||||
return f"About {rounded} sec left"
|
||||
if seconds < 3600:
|
||||
minutes = int((seconds + 59) // 60)
|
||||
return f"About {minutes} min left"
|
||||
hours = int((seconds + 3599) // 3600)
|
||||
return f"About {hours} hr left"
|
||||
|
||||
def _finish(self, context, job, return_code):
|
||||
settings = context.scene.snapform_settings
|
||||
|
||||
@@ -14,7 +14,7 @@ def draw_status_bar(self, context):
|
||||
progress = min(max(settings.progress, 0.0), 1.0)
|
||||
row = self.layout.row(align=True)
|
||||
progress_row = row.row(align=True)
|
||||
progress_row.ui_units_x = 12.0
|
||||
progress_row.ui_units_x = 18.0
|
||||
progress_row.progress(
|
||||
factor=progress,
|
||||
type="BAR",
|
||||
|
||||
@@ -49,6 +49,7 @@ def write_status(
|
||||
stage=None,
|
||||
stages=None,
|
||||
stage_label=None,
|
||||
eta_seconds=None,
|
||||
):
|
||||
payload = {"progress": progress, "message": message}
|
||||
if error:
|
||||
@@ -57,6 +58,8 @@ def write_status(
|
||||
payload["stage"] = stage
|
||||
payload["stages"] = stages
|
||||
payload["stage_label"] = stage_label
|
||||
if eta_seconds is not None:
|
||||
payload["eta_seconds"] = max(int(eta_seconds), 0)
|
||||
if state is None:
|
||||
if error:
|
||||
state = "failed"
|
||||
@@ -229,6 +232,7 @@ class DownloadProgressReporter:
|
||||
self.label = label
|
||||
self.progress = start
|
||||
self.last_write_time = 0.0
|
||||
self.started_at = self.clock()
|
||||
|
||||
def report(self, current, total, rate=None):
|
||||
if not total or current <= 0:
|
||||
@@ -244,14 +248,25 @@ class DownloadProgressReporter:
|
||||
return
|
||||
self.progress = progress
|
||||
self.last_write_time = now
|
||||
speed = format_rate(rate) if rate else ""
|
||||
effective_rate = rate
|
||||
elapsed = now - self.started_at
|
||||
if not effective_rate and elapsed > 0:
|
||||
effective_rate = current / elapsed
|
||||
speed = format_rate(effective_rate) if effective_rate else ""
|
||||
eta_seconds = None
|
||||
if effective_rate and effective_rate > 0:
|
||||
eta_seconds = max(total - current, 0) / effective_rate
|
||||
if self.stage is not None:
|
||||
message = f"downloading {speed}" if speed else "downloading"
|
||||
message = (
|
||||
f"Downloading packages | {speed}"
|
||||
if speed
|
||||
else "Downloading packages"
|
||||
)
|
||||
else:
|
||||
prefix = self.label or self.repository
|
||||
message = f"downloading {prefix}"
|
||||
message = f"Downloading {prefix}"
|
||||
if speed:
|
||||
message = f"{message} {speed}"
|
||||
message = f"{message} | {speed}"
|
||||
write_status(
|
||||
self.status_path,
|
||||
progress,
|
||||
@@ -259,6 +274,7 @@ class DownloadProgressReporter:
|
||||
stage=self.stage,
|
||||
stages=self.stages,
|
||||
stage_label=self.stage_label,
|
||||
eta_seconds=eta_seconds,
|
||||
)
|
||||
|
||||
|
||||
@@ -356,7 +372,7 @@ def download_model(args):
|
||||
failures.append((endpoint, "endpoint unreachable"))
|
||||
continue
|
||||
os.environ["HF_ENDPOINT"] = endpoint
|
||||
message = f"downloading {label}"
|
||||
message = f"Downloading {label}"
|
||||
if endpoint != HF_ENDPOINT:
|
||||
message = f"{message} (mirror)"
|
||||
write_status(args.status, start, message, **stage_kwargs)
|
||||
@@ -375,7 +391,7 @@ def download_model(args):
|
||||
write_status(
|
||||
args.status,
|
||||
start + span,
|
||||
"model ready",
|
||||
f"{label} ready",
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
@@ -384,7 +400,7 @@ def download_model(args):
|
||||
write_status(
|
||||
args.status,
|
||||
start,
|
||||
f"downloading {label} (mirror: modelscope.cn)",
|
||||
f"Downloading {label} | ModelScope mirror",
|
||||
**stage_kwargs,
|
||||
)
|
||||
download_from_modelscope(
|
||||
@@ -402,7 +418,7 @@ def download_model(args):
|
||||
write_status(
|
||||
args.status,
|
||||
start + span,
|
||||
"model ready",
|
||||
f"{label} ready",
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user