Bump version to 0.10.11 and update installation instructions; enhance progress reporting and estimated time features
This commit is contained in:
@@ -11,11 +11,7 @@ SnapForm 是一个 Blender 扩展,可以把一张照片、一段视频或一
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
1. 下载对应系统的扩展压缩包:
|
1. 下载对应系统的扩展压缩包
|
||||||
- Windows:`SnapForm.v0.10.10.windows-x64.zip`
|
|
||||||
- macOS(Apple 芯片):`SnapForm.v0.10.10.macos-arm64.zip`
|
|
||||||
- macOS(Intel):`SnapForm.v0.10.10.macos-x64.zip`
|
|
||||||
- Linux:`SnapForm.v0.10.10.linux-x64.zip`
|
|
||||||
2. 打开 Blender,进入 `Preferences > Extensions`
|
2. 打开 Blender,进入 `Preferences > Extensions`
|
||||||
3. 点击 `Install from Disk`,选择下载的压缩包
|
3. 点击 `Install from Disk`,选择下载的压缩包
|
||||||
4. 在扩展偏好设置中确认 Storage Root 与 Device(保持默认即可);Device 默认取本机
|
4. 在扩展偏好设置中确认 Storage Root 与 Device(保持默认即可);Device 默认取本机
|
||||||
@@ -23,7 +19,8 @@ SnapForm 是一个 Blender 扩展,可以把一张照片、一段视频或一
|
|||||||
5. 打开 3D 视图右侧的 `SnapForm` 面板,点击 `Install Runtime`
|
5. 打开 3D 视图右侧的 `SnapForm` 面板,点击 `Install Runtime`
|
||||||
|
|
||||||
DA3、BEN2 源码包和 uv 已内置在扩展里,安装时直接从本地解包,不需要访问 GitHub。首次使用仍会自动
|
DA3、BEN2 源码包和 uv 已内置在扩展里,安装时直接从本地解包,不需要访问 GitHub。首次使用仍会自动
|
||||||
下载运行环境依赖(PyTorch 等)和 AI 模型,请保持网络畅通,下载需要一些时间。
|
下载运行环境依赖(PyTorch 等),随后自动下载 `DA3 Small` 和 `BEN2 Base`。其他 AI 模型可在
|
||||||
|
`Host > Models` 中按需下载,请保持网络畅通,首次安装需要一些时间。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
|
|
||||||
|
|||||||
@@ -55,13 +55,14 @@
|
|||||||
本机支持的设备。
|
本机支持的设备。
|
||||||
4. 在 `Host` 点击 `Install Runtime`。首次安装会下载 Python、PyTorch、FastAPI、Uvicorn;
|
4. 在 `Host` 点击 `Install Runtime`。首次安装会下载 Python、PyTorch、FastAPI、Uvicorn;
|
||||||
DA3、BEN2 源码包与 uv 已内置在扩展中,安装时从 `~/.snapform/sources/` 解包使用,
|
DA3、BEN2 源码包与 uv 已内置在扩展中,安装时从 `~/.snapform/sources/` 解包使用,
|
||||||
并默认下载 `DA3 Small`、`DA3 Base` 和 `BEN2 Base` 模型。安装按 8 个阶段依次推进,每段进度
|
并在 Runtime 就绪后自动下载 `DA3 Small` 和 `BEN2 Base` 推荐模型。Runtime 安装
|
||||||
从 0 到 100%:解包内置 uv、创建 Python Runtime、下载 PyTorch、安装 xFormers(macOS 跳过,
|
固定显示 `Environment`、`PyTorch`、`AI components` 三个用户阶段;xFormers(macOS 跳过)、
|
||||||
内置 DA3 源码包为官方原版并已移除 xformers 硬依赖,自动回退纯 PyTorch 实现)、安装 AI 依赖,
|
FastAPI 等内部安装步骤合并在 `AI components` 中。耗时最长的 PyTorch 与 AI 组件占主要进度
|
||||||
然后是三个模型的下载。安装完成后进入 `验证 Runtime` 阶段,会导入 Torch 与全部 AI 依赖;
|
区间。安装完成后直接写入 Runtime 就绪信息,不再额外导入全部组件进行验证。推荐模型属于安装
|
||||||
macOS 首次导入 Torch 会初始化 Metal 后端(编译 shader 缓存),该阶段可能持续较久,状态栏
|
后的自动步骤,不计入阶段编号;`DA3 Base` 等其他模型可稍后在 `Models` 面板按需下载。
|
||||||
会显示已用时间而不是卡住。进度文字统一为 `阶段/总数 阶段名: 状态`,
|
状态文字使用 `阶段/总数 阶段名 | 当前操作 | 预计剩余时间`,依赖安装会结合预设耗时与实际
|
||||||
模型下载阶段会实时显示下载速度,例如 `6/8 da3 small: downloading 14.2MB/s`。
|
进度修正预期;模型下载根据字节数和速度计算剩余时间,例如
|
||||||
|
`Downloading DA3 Small | 14.2 MB/s | About 2 min left`。重试或切换镜像时进度不会倒退。
|
||||||
Runtime 就绪后即使模型下载中断也不影响 Host 使用,可稍后在 `Models` 面板单独补下。
|
Runtime 就绪后即使模型下载中断也不影响 Host 使用,可稍后在 `Models` 面板单独补下。
|
||||||
安装不做启动网络预检:pip 安装失败会自动依次重试清华、阿里镜像,模型下载由 worker
|
安装不做启动网络预检:pip 安装失败会自动依次重试清华、阿里镜像,模型下载由 worker
|
||||||
自行探测回退到 HF 镜像;需要手动指定镜像时可通过 `PIP_INDEX_URL` 环境变量覆盖。
|
自行探测回退到 HF 镜像;需要手动指定镜像时可通过 `PIP_INDEX_URL` 环境变量覆盖。
|
||||||
@@ -138,8 +139,9 @@ Cyclic 沿用原 Empty 的设置。序列帧无法被 Blender 整体 Pack,因
|
|||||||
`8×12` 个面。随后的 Simple Subdivision 修改器使用原生名称,Viewport Levels 固定为
|
`8×12` 个面。随后的 Simple Subdivision 修改器使用原生名称,Viewport Levels 固定为
|
||||||
`1`,Render Levels 固定为 `2`,不再随 Resolution Scale 或 Subdivision 动态变化。
|
`1`,Render Levels 固定为 `2`,不再随 Resolution Scale 或 Subdivision 动态变化。
|
||||||
|
|
||||||
所有任务使用统一的状态栏进度显示。变化中的阶段说明显示在右侧的紧凑进度条内部,取消按钮
|
所有任务使用统一的状态栏进度显示。变化中的阶段说明显示在右侧的进度条内部,取消按钮
|
||||||
位于进度条右侧。Image Depth 的阶段包括 Runtime 启动、输入准备、模型加载、深度估算、缓存
|
位于进度条右侧。状态字段支持阶段、当前操作与预计剩余秒数,界面统一组合为紧凑文案并保证
|
||||||
|
进度单调递增。Image Depth 的阶段包括 Runtime 启动、输入准备、模型加载、深度估算、缓存
|
||||||
写出和面片构建。进度更新会强制刷新状态栏与窗口,无需移动鼠标即可实时显示。安装中断后残留的
|
写出和面片构建。进度更新会强制刷新状态栏与窗口,无需移动鼠标即可实时显示。安装中断后残留的
|
||||||
半成品 Runtime 会在下次安装前自动清理,不会因为中断而无法再次安装。
|
半成品 Runtime 会在下次安装前自动清理,不会因为中断而无法再次安装。
|
||||||
|
|
||||||
@@ -281,7 +283,7 @@ GLB 任务会关闭其默认附带的深度预览,使四种 job 产物保持
|
|||||||
|
|
||||||
Extension 本身不包含 Torch。`Install Runtime` 使用 `uv` 创建独立 Python 3.12
|
Extension 本身不包含 Torch。`Install Runtime` 使用 `uv` 创建独立 Python 3.12
|
||||||
环境,并安装。机器上即使没有安装 `uv` 或 Python 也没关系:安装程序由 Blender
|
环境,并安装。机器上即使没有安装 `uv` 或 Python 也没关系:安装程序由 Blender
|
||||||
自带的 Python 启动,缺 `uv` 时会自动下载官方独立二进制(约 30MB),再用它下载
|
自带的 Python 启动,缺 `uv` 时会解包扩展内置的对应平台二进制,再用它下载
|
||||||
Python 3.12 与全部依赖。`uv` 会缓存在扩展存储目录下,后续安装直接复用。
|
Python 3.12 与全部依赖。`uv` 会缓存在扩展存储目录下,后续安装直接复用。
|
||||||
安装不做网络预检,直接开始;环境变量仍可切换镜像与代理:`PIP_INDEX_URL`(PyPI 镜像)、
|
安装不做网络预检,直接开始;环境变量仍可切换镜像与代理:`PIP_INDEX_URL`(PyPI 镜像)、
|
||||||
`SNAPFORM_TORCH_INDEX`(PyTorch 索引)、`HF_ENDPOINT`(Hugging Face 镜像)、
|
`SNAPFORM_TORCH_INDEX`(PyTorch 索引)、`HF_ENDPOINT`(Hugging Face 镜像)、
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "snapform"
|
name = "snapform"
|
||||||
version = "0.10.10"
|
version = "0.10.11"
|
||||||
description = "Fast image-guided structure building tools for Blender"
|
description = "Fast image-guided structure building tools for Blender"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "snapform"
|
id = "snapform"
|
||||||
version = "0.10.10"
|
version = "0.10.11"
|
||||||
name = "SnapForm"
|
name = "SnapForm"
|
||||||
tagline = "Build spatial structures from images, fast"
|
tagline = "Build spatial structures from images, fast"
|
||||||
maintainer = "OmooLab"
|
maintainer = "OmooLab"
|
||||||
|
|||||||
@@ -39,16 +39,24 @@ TORCH_INDEXES = {
|
|||||||
"cpu": "https://download.pytorch.org/whl/cpu",
|
"cpu": "https://download.pytorch.org/whl/cpu",
|
||||||
}
|
}
|
||||||
_resolved_pypi_index = None
|
_resolved_pypi_index = None
|
||||||
DEFAULT_MODEL_KEYS = ("SMALL", "BASE", BEN2_MODEL.key)
|
DEFAULT_MODEL_KEYS = ("SMALL", BEN2_MODEL.key)
|
||||||
RUNTIME_SLICE_START = 0.07
|
RUNTIME_STAGE_COUNT = 3
|
||||||
RUNTIME_SLICE_END = 0.88
|
ENVIRONMENT_SLICE = (0.02, 0.06)
|
||||||
VERIFY_SLICE_START = 0.88
|
PYTORCH_SLICE = (0.06, 0.48)
|
||||||
VERIFY_SLICE_END = 0.90
|
XFORMERS_SLICE = (0.48, 0.62)
|
||||||
MODELS_SLICE_START = 0.90
|
AI_COMPONENTS_SLICE = (0.62, 0.85)
|
||||||
|
MODELS_SLICE_START = 0.85
|
||||||
MODELS_SLICE_END = 1.0
|
MODELS_SLICE_END = 1.0
|
||||||
PLAIN_SLICE_SPAN = 0.02
|
|
||||||
RAMP_RATE = 0.02
|
RAMP_RATE = 0.02
|
||||||
CREEP_CAP = 0.60
|
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 = (
|
CHECK_CODE = (
|
||||||
"import json; import torch; "
|
"import json; import torch; "
|
||||||
"from depth_anything_3.api import DepthAnything3; "
|
"from depth_anything_3.api import DepthAnything3; "
|
||||||
@@ -70,6 +78,7 @@ def write_status(
|
|||||||
stage=None,
|
stage=None,
|
||||||
stages=None,
|
stages=None,
|
||||||
stage_label=None,
|
stage_label=None,
|
||||||
|
eta_seconds=None,
|
||||||
):
|
):
|
||||||
payload = {"progress": progress, "message": message}
|
payload = {"progress": progress, "message": message}
|
||||||
if error:
|
if error:
|
||||||
@@ -78,6 +87,8 @@ def write_status(
|
|||||||
payload["stage"] = stage
|
payload["stage"] = stage
|
||||||
payload["stages"] = stages
|
payload["stages"] = stages
|
||||||
payload["stage_label"] = stage_label
|
payload["stage_label"] = stage_label
|
||||||
|
if eta_seconds is not None:
|
||||||
|
payload["eta_seconds"] = max(int(eta_seconds), 0)
|
||||||
Path(path).write_text(
|
Path(path).write_text(
|
||||||
json.dumps(payload, ensure_ascii=False),
|
json.dumps(payload, ensure_ascii=False),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -120,6 +131,21 @@ def parse_package_count(line):
|
|||||||
return 0
|
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):
|
def effective_torch_index(backend):
|
||||||
return os.environ.get("SNAPFORM_TORCH_INDEX") or TORCH_INDEXES.get(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()
|
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):
|
def eased(fraction):
|
||||||
return start + min(max(fraction, 0.0), 1.0) * (end - start)
|
return start + min(max(fraction, 0.0), 1.0) * (end - start)
|
||||||
|
|
||||||
@@ -186,6 +202,21 @@ def _run_install_once(
|
|||||||
started = clock()
|
started = clock()
|
||||||
last_tick = started
|
last_tick = started
|
||||||
last_heartbeat = 0.0
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
raw_line = output_queue.get(timeout=0.2)
|
raw_line = output_queue.get(timeout=0.2)
|
||||||
@@ -206,7 +237,7 @@ def _run_install_once(
|
|||||||
if empty:
|
if empty:
|
||||||
if elapsed - last_heartbeat >= 5.0:
|
if elapsed - last_heartbeat >= 5.0:
|
||||||
last_heartbeat = elapsed
|
last_heartbeat = elapsed
|
||||||
write(displayed, f"downloading {format_elapsed(elapsed)}")
|
detail("Downloading packages")
|
||||||
continue
|
continue
|
||||||
line = raw_line.rstrip()
|
line = raw_line.rstrip()
|
||||||
print(line)
|
print(line)
|
||||||
@@ -215,17 +246,17 @@ def _run_install_once(
|
|||||||
resolved_progress = eased(0.02)
|
resolved_progress = eased(0.02)
|
||||||
displayed = max(displayed, resolved_progress)
|
displayed = max(displayed, resolved_progress)
|
||||||
target = max(target, 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:
|
elif line.startswith("Prepared ") and "package" in line:
|
||||||
done = parse_package_count(line)
|
done = parse_package_count(line)
|
||||||
if total:
|
if total:
|
||||||
target = max(target, eased(0.10 + 0.60 * done / 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:
|
elif line.startswith("Installed ") and "package" in line:
|
||||||
done = parse_package_count(line)
|
done = parse_package_count(line)
|
||||||
if total:
|
if total:
|
||||||
target = max(target, eased(0.75 + 0.25 * done / 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("+"):
|
elif line.startswith("+"):
|
||||||
entry = line[1:].strip()
|
entry = line[1:].strip()
|
||||||
installed.append(entry)
|
installed.append(entry)
|
||||||
@@ -315,19 +346,19 @@ def install_stages(args, runtime_dir, python, uv):
|
|||||||
torch_command.extend(["--index-url", torch_index])
|
torch_command.extend(["--index-url", torch_index])
|
||||||
stages = [
|
stages = [
|
||||||
(
|
(
|
||||||
"uv",
|
"Environment",
|
||||||
[uv, "venv", "--clear", "--python", "3.12", runtime_dir],
|
[uv, "venv", "--clear", "--python", "3.12", runtime_dir],
|
||||||
None,
|
None,
|
||||||
"plain",
|
"plain",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"pytorch deps",
|
"PyTorch",
|
||||||
torch_command,
|
torch_command,
|
||||||
None,
|
None,
|
||||||
"install",
|
"install",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"ai deps",
|
"AI components",
|
||||||
[
|
[
|
||||||
uv,
|
uv,
|
||||||
"pip",
|
"pip",
|
||||||
@@ -363,7 +394,7 @@ def install_stages(args, runtime_dir, python, uv):
|
|||||||
xformers_command.append("--no-deps")
|
xformers_command.append("--no-deps")
|
||||||
stages.insert(
|
stages.insert(
|
||||||
2,
|
2,
|
||||||
("xformers", xformers_command, None, "install"),
|
("AI components", xformers_command, None, "install"),
|
||||||
)
|
)
|
||||||
models = {**MODELS, BEN2_MODEL.key: BEN2_MODEL}
|
models = {**MODELS, BEN2_MODEL.key: BEN2_MODEL}
|
||||||
worker_script = Path(__file__).with_name("worker.py")
|
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,
|
models_dir / model.directory_name,
|
||||||
"--status",
|
"--status",
|
||||||
args.status,
|
args.status,
|
||||||
|
"--label",
|
||||||
|
model.label,
|
||||||
]
|
]
|
||||||
for pattern in model.download_patterns:
|
for pattern in model.download_patterns:
|
||||||
download_command.extend(["--allow-pattern", pattern])
|
download_command.extend(["--allow-pattern", pattern])
|
||||||
stages.append(
|
stages.append(
|
||||||
(
|
(
|
||||||
model.label.lower(),
|
model.label,
|
||||||
download_command,
|
download_command,
|
||||||
None,
|
None,
|
||||||
"download",
|
"download",
|
||||||
@@ -405,6 +438,17 @@ def runtime_ready(runtime_dir):
|
|||||||
return payload.get("runtime_schema") == RUNTIME_SCHEMA
|
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):
|
def verify_command(python):
|
||||||
return [python, "-c", CHECK_CODE]
|
return [python, "-c", CHECK_CODE]
|
||||||
|
|
||||||
@@ -412,9 +456,12 @@ def verify_command(python):
|
|||||||
def run_verify(
|
def run_verify(
|
||||||
python,
|
python,
|
||||||
status_path,
|
status_path,
|
||||||
start=VERIFY_SLICE_START,
|
start=0.0,
|
||||||
end=VERIFY_SLICE_END,
|
end=1.0,
|
||||||
clock=time.monotonic,
|
clock=time.monotonic,
|
||||||
|
stage=None,
|
||||||
|
stages=None,
|
||||||
|
stage_label=None,
|
||||||
):
|
):
|
||||||
"""Run the runtime import check with heartbeat progress; return the device report."""
|
"""Run the runtime import check with heartbeat progress; return the device report."""
|
||||||
command = verify_command(python)
|
command = verify_command(python)
|
||||||
@@ -457,7 +504,11 @@ def run_verify(
|
|||||||
write_status(
|
write_status(
|
||||||
status_path,
|
status_path,
|
||||||
progress,
|
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
|
continue
|
||||||
line = raw_line.rstrip()
|
line = raw_line.rstrip()
|
||||||
@@ -585,13 +636,20 @@ def run_stage(
|
|||||||
start=0.0,
|
start=0.0,
|
||||||
end=1.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(
|
write_status(
|
||||||
status_path,
|
status_path,
|
||||||
start,
|
start,
|
||||||
"",
|
opening_message,
|
||||||
stage=stage,
|
stage=stage,
|
||||||
stages=stages,
|
stages=stages,
|
||||||
stage_label=label,
|
stage_label=label,
|
||||||
|
eta_seconds=estimated_remaining(label, start),
|
||||||
)
|
)
|
||||||
installed = []
|
installed = []
|
||||||
if kind == "install":
|
if kind == "install":
|
||||||
@@ -606,28 +664,31 @@ def run_stage(
|
|||||||
end=end,
|
end=end,
|
||||||
)
|
)
|
||||||
elif kind == "download":
|
elif kind == "download":
|
||||||
run(
|
progress_command = [
|
||||||
[
|
*command,
|
||||||
*command,
|
"--progress-start",
|
||||||
"--progress-start",
|
str(start),
|
||||||
str(start),
|
"--progress-end",
|
||||||
"--progress-end",
|
str(end),
|
||||||
str(end),
|
]
|
||||||
"--stage",
|
if stage is not None:
|
||||||
str(stage),
|
progress_command.extend(
|
||||||
"--stages",
|
[
|
||||||
str(stages),
|
"--stage",
|
||||||
"--stage-label",
|
str(stage),
|
||||||
label,
|
"--stages",
|
||||||
],
|
str(stages),
|
||||||
environment,
|
"--stage-label",
|
||||||
)
|
label,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
run(progress_command, environment)
|
||||||
else:
|
else:
|
||||||
run(command, environment)
|
run(command, environment)
|
||||||
write_status(
|
write_status(
|
||||||
status_path,
|
status_path,
|
||||||
end,
|
end,
|
||||||
"",
|
f"{label} ready" if stage is None else "",
|
||||||
stage=stage,
|
stage=stage,
|
||||||
stages=stages,
|
stages=stages,
|
||||||
stage_label=label,
|
stage_label=label,
|
||||||
@@ -662,44 +723,45 @@ def install(args):
|
|||||||
stages = install_stages(args, runtime_dir, python, uv)
|
stages = install_stages(args, runtime_dir, python, uv)
|
||||||
runtime_stages = [stage for stage in stages if stage[3] != "download"]
|
runtime_stages = [stage for stage in stages if stage[3] != "download"]
|
||||||
model_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 = (
|
model_span = (
|
||||||
(MODELS_SLICE_END - MODELS_SLICE_START) / len(model_stages)
|
(MODELS_SLICE_END - MODELS_SLICE_START) / len(model_stages)
|
||||||
if model_stages
|
if model_stages
|
||||||
else 0.0
|
else 0.0
|
||||||
)
|
)
|
||||||
installed_packages = []
|
installed_packages = []
|
||||||
runtime_offset = RUNTIME_SLICE_START
|
component_stages = runtime_stages[2:]
|
||||||
for index, (label, command, environment, kind) in enumerate(
|
component_ranges = (
|
||||||
runtime_stages,
|
[XFORMERS_SLICE, AI_COMPONENTS_SLICE]
|
||||||
start=1,
|
if len(component_stages) == 2
|
||||||
):
|
else [(XFORMERS_SLICE[0], AI_COMPONENTS_SLICE[1])]
|
||||||
stage_start = runtime_offset
|
)
|
||||||
stage_end = stage_start + (
|
for index, (label, command, environment, kind) in enumerate(runtime_stages):
|
||||||
install_span if kind == "install" else PLAIN_SLICE_SPAN
|
if index == 0:
|
||||||
)
|
stage = 1
|
||||||
if uv_missing and index == 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(
|
install_uv(
|
||||||
uv,
|
uv,
|
||||||
args.status,
|
args.status,
|
||||||
stage=index,
|
stage=stage,
|
||||||
stages=len(stages),
|
stages=RUNTIME_STAGE_COUNT,
|
||||||
stage_label=label,
|
stage_label=label,
|
||||||
start=stage_start,
|
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(
|
stage_installed = run_stage(
|
||||||
args.status,
|
args.status,
|
||||||
index,
|
stage,
|
||||||
len(stages),
|
RUNTIME_STAGE_COUNT,
|
||||||
label,
|
label,
|
||||||
command,
|
command,
|
||||||
environment,
|
environment,
|
||||||
@@ -709,17 +771,7 @@ def install(args):
|
|||||||
)
|
)
|
||||||
if isinstance(stage_installed, list):
|
if isinstance(stage_installed, list):
|
||||||
installed_packages.extend(stage_installed)
|
installed_packages.extend(stage_installed)
|
||||||
runtime_offset = stage_end
|
devices = installed_devices(args)
|
||||||
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")
|
|
||||||
runtime_dir.mkdir(parents=True, exist_ok=True)
|
runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||||
ready_path = runtime_dir / "snapform_ready.json"
|
ready_path = runtime_dir / "snapform_ready.json"
|
||||||
ready_path.write_text(
|
ready_path.write_text(
|
||||||
@@ -741,19 +793,16 @@ def install(args):
|
|||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
offset = len(runtime_stages)
|
|
||||||
model_offset = MODELS_SLICE_START
|
model_offset = MODELS_SLICE_START
|
||||||
for index, (label, command, environment, kind) in enumerate(
|
failed_models = []
|
||||||
model_stages,
|
for label, command, environment, kind in model_stages:
|
||||||
start=offset + 1,
|
|
||||||
):
|
|
||||||
stage_start = model_offset
|
stage_start = model_offset
|
||||||
stage_end = stage_start + model_span
|
stage_end = stage_start + model_span
|
||||||
try:
|
try:
|
||||||
run_stage(
|
run_stage(
|
||||||
args.status,
|
args.status,
|
||||||
index,
|
None,
|
||||||
len(stages),
|
None,
|
||||||
label,
|
label,
|
||||||
command,
|
command,
|
||||||
environment,
|
environment,
|
||||||
@@ -763,14 +812,22 @@ def install(args):
|
|||||||
)
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"Model download failed: {label}: {error}")
|
print(f"Model download failed: {label}: {error}")
|
||||||
|
failed_models.append(label)
|
||||||
write_status(
|
write_status(
|
||||||
args.status,
|
args.status,
|
||||||
MODELS_SLICE_END,
|
stage_end,
|
||||||
f"Runtime is ready; {label} failed",
|
f"Skipping {label}; continuing",
|
||||||
)
|
)
|
||||||
return
|
|
||||||
model_offset = stage_end
|
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():
|
def parse_args():
|
||||||
|
|||||||
@@ -475,23 +475,53 @@ class RunJob(bpy.types.Operator):
|
|||||||
if status is None:
|
if status is None:
|
||||||
return
|
return
|
||||||
settings = context.scene.snapform_settings
|
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)
|
settings.status = self._format_status(status, settings.status)
|
||||||
self._redraw_ui(context, force=True)
|
self._redraw_ui(context, force=True)
|
||||||
|
|
||||||
def _format_status(self, status, fallback):
|
def _format_status(self, status, fallback):
|
||||||
message = status.get("message", fallback)
|
message = status.get("message")
|
||||||
stage = status.get("stage")
|
stage = status.get("stage")
|
||||||
stages = status.get("stages")
|
stages = status.get("stages")
|
||||||
stage_label = status.get("stage_label")
|
stage_label = status.get("stage_label")
|
||||||
if not stage or not stages:
|
parts = []
|
||||||
return message
|
if stage and stages:
|
||||||
text = (
|
stage_text = f"{stage}/{stages}"
|
||||||
f"{stage}/{stages} {stage_label}"
|
if stage_label:
|
||||||
if stage_label
|
stage_text = f"{stage_text} {stage_label}"
|
||||||
else f"{stage}/{stages}"
|
parts.append(stage_text)
|
||||||
)
|
elif stage_label:
|
||||||
return f"{text}: {message}" if message else text
|
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):
|
def _finish(self, context, job, return_code):
|
||||||
settings = context.scene.snapform_settings
|
settings = context.scene.snapform_settings
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def draw_status_bar(self, context):
|
|||||||
progress = min(max(settings.progress, 0.0), 1.0)
|
progress = min(max(settings.progress, 0.0), 1.0)
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
progress_row = row.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(
|
progress_row.progress(
|
||||||
factor=progress,
|
factor=progress,
|
||||||
type="BAR",
|
type="BAR",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ def write_status(
|
|||||||
stage=None,
|
stage=None,
|
||||||
stages=None,
|
stages=None,
|
||||||
stage_label=None,
|
stage_label=None,
|
||||||
|
eta_seconds=None,
|
||||||
):
|
):
|
||||||
payload = {"progress": progress, "message": message}
|
payload = {"progress": progress, "message": message}
|
||||||
if error:
|
if error:
|
||||||
@@ -57,6 +58,8 @@ def write_status(
|
|||||||
payload["stage"] = stage
|
payload["stage"] = stage
|
||||||
payload["stages"] = stages
|
payload["stages"] = stages
|
||||||
payload["stage_label"] = stage_label
|
payload["stage_label"] = stage_label
|
||||||
|
if eta_seconds is not None:
|
||||||
|
payload["eta_seconds"] = max(int(eta_seconds), 0)
|
||||||
if state is None:
|
if state is None:
|
||||||
if error:
|
if error:
|
||||||
state = "failed"
|
state = "failed"
|
||||||
@@ -229,6 +232,7 @@ class DownloadProgressReporter:
|
|||||||
self.label = label
|
self.label = label
|
||||||
self.progress = start
|
self.progress = start
|
||||||
self.last_write_time = 0.0
|
self.last_write_time = 0.0
|
||||||
|
self.started_at = self.clock()
|
||||||
|
|
||||||
def report(self, current, total, rate=None):
|
def report(self, current, total, rate=None):
|
||||||
if not total or current <= 0:
|
if not total or current <= 0:
|
||||||
@@ -244,14 +248,25 @@ class DownloadProgressReporter:
|
|||||||
return
|
return
|
||||||
self.progress = progress
|
self.progress = progress
|
||||||
self.last_write_time = now
|
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:
|
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:
|
else:
|
||||||
prefix = self.label or self.repository
|
prefix = self.label or self.repository
|
||||||
message = f"downloading {prefix}"
|
message = f"Downloading {prefix}"
|
||||||
if speed:
|
if speed:
|
||||||
message = f"{message} {speed}"
|
message = f"{message} | {speed}"
|
||||||
write_status(
|
write_status(
|
||||||
self.status_path,
|
self.status_path,
|
||||||
progress,
|
progress,
|
||||||
@@ -259,6 +274,7 @@ class DownloadProgressReporter:
|
|||||||
stage=self.stage,
|
stage=self.stage,
|
||||||
stages=self.stages,
|
stages=self.stages,
|
||||||
stage_label=self.stage_label,
|
stage_label=self.stage_label,
|
||||||
|
eta_seconds=eta_seconds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -356,7 +372,7 @@ def download_model(args):
|
|||||||
failures.append((endpoint, "endpoint unreachable"))
|
failures.append((endpoint, "endpoint unreachable"))
|
||||||
continue
|
continue
|
||||||
os.environ["HF_ENDPOINT"] = endpoint
|
os.environ["HF_ENDPOINT"] = endpoint
|
||||||
message = f"downloading {label}"
|
message = f"Downloading {label}"
|
||||||
if endpoint != HF_ENDPOINT:
|
if endpoint != HF_ENDPOINT:
|
||||||
message = f"{message} (mirror)"
|
message = f"{message} (mirror)"
|
||||||
write_status(args.status, start, message, **stage_kwargs)
|
write_status(args.status, start, message, **stage_kwargs)
|
||||||
@@ -375,7 +391,7 @@ def download_model(args):
|
|||||||
write_status(
|
write_status(
|
||||||
args.status,
|
args.status,
|
||||||
start + span,
|
start + span,
|
||||||
"model ready",
|
f"{label} ready",
|
||||||
**stage_kwargs,
|
**stage_kwargs,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -384,7 +400,7 @@ def download_model(args):
|
|||||||
write_status(
|
write_status(
|
||||||
args.status,
|
args.status,
|
||||||
start,
|
start,
|
||||||
f"downloading {label} (mirror: modelscope.cn)",
|
f"Downloading {label} | ModelScope mirror",
|
||||||
**stage_kwargs,
|
**stage_kwargs,
|
||||||
)
|
)
|
||||||
download_from_modelscope(
|
download_from_modelscope(
|
||||||
@@ -402,7 +418,7 @@ def download_model(args):
|
|||||||
write_status(
|
write_status(
|
||||||
args.status,
|
args.status,
|
||||||
start + span,
|
start + span,
|
||||||
"model ready",
|
f"{label} ready",
|
||||||
**stage_kwargs,
|
**stage_kwargs,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ class AddonTest(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
"fallback",
|
"fallback",
|
||||||
),
|
),
|
||||||
"3/8 pytorch: 2 downloaded",
|
"3/8 pytorch | 2 downloaded",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
operator._format_status(
|
operator._format_status(
|
||||||
@@ -636,6 +636,33 @@ class AddonTest(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
"Estimating depth",
|
"Estimating depth",
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
operator._format_status(
|
||||||
|
{
|
||||||
|
"progress": 0.25,
|
||||||
|
"message": "Downloading packages",
|
||||||
|
"stage": 2,
|
||||||
|
"stages": 3,
|
||||||
|
"stage_label": "PyTorch",
|
||||||
|
"eta_seconds": 125,
|
||||||
|
},
|
||||||
|
"fallback",
|
||||||
|
),
|
||||||
|
"2/3 PyTorch | Downloading packages | About 3 min left",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
operator._format_status(
|
||||||
|
{
|
||||||
|
"progress": 0.5,
|
||||||
|
"message": "",
|
||||||
|
"stage": 2,
|
||||||
|
"stages": 3,
|
||||||
|
"stage_label": "PyTorch",
|
||||||
|
},
|
||||||
|
"2/3 PyTorch | Downloading packages",
|
||||||
|
),
|
||||||
|
"2/3 PyTorch",
|
||||||
|
)
|
||||||
|
|
||||||
def test_model_options_mark_only_models_that_are_not_downloaded(self):
|
def test_model_options_mark_only_models_that_are_not_downloaded(self):
|
||||||
items_callback = self.addon.SnapFormSettings.__annotations__["model"][
|
items_callback = self.addon.SnapFormSettings.__annotations__["model"][
|
||||||
|
|||||||
@@ -153,7 +153,13 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
self.assertEqual(len(default_downloads), 3)
|
self.assertEqual(len(default_downloads), 2)
|
||||||
|
self.assertFalse(
|
||||||
|
any(
|
||||||
|
"depth-anything/DA3-BASE" in map(str, command)
|
||||||
|
for command in default_downloads
|
||||||
|
)
|
||||||
|
)
|
||||||
ben2_download = next(
|
ben2_download = next(
|
||||||
command
|
command
|
||||||
for command in default_downloads
|
for command in default_downloads
|
||||||
@@ -165,9 +171,12 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
if value == "--allow-pattern"
|
if value == "--allow-pattern"
|
||||||
]
|
]
|
||||||
self.assertEqual(patterns, ["config.json", "model.safetensors"])
|
self.assertEqual(patterns, ["config.json", "model.safetensors"])
|
||||||
self.assertIn("--stage", ben2_download)
|
self.assertNotIn("--stage", ben2_download)
|
||||||
self.assertIn("--stage-label", ben2_download)
|
self.assertNotIn("--stage-label", ben2_download)
|
||||||
self.assertEqual(ben2_download[-1], "ben2 base")
|
self.assertEqual(
|
||||||
|
ben2_download[ben2_download.index("--label") + 1],
|
||||||
|
"BEN2 Base",
|
||||||
|
)
|
||||||
|
|
||||||
def test_darwin_install_skips_cuda_index_and_xformers(self):
|
def test_darwin_install_skips_cuda_index_and_xformers(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
@@ -400,7 +409,15 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
all(starts[i] < starts[i + 1] for i in range(len(starts) - 1))
|
all(starts[i] < starts[i + 1] for i in range(len(starts) - 1))
|
||||||
)
|
)
|
||||||
self.assertEqual(ends[-1], 1.0)
|
self.assertEqual(ends[-1], 1.0)
|
||||||
self.assertEqual(starts[0], bootstrap.RUNTIME_SLICE_START)
|
self.assertEqual(starts[0], bootstrap.ENVIRONMENT_SLICE[0])
|
||||||
|
runtime_ranges = stage_ranges[:4]
|
||||||
|
self.assertEqual(
|
||||||
|
[item[0] for item in runtime_ranges],
|
||||||
|
[1, 2, 3, 3],
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(item[0] is None for item in stage_ranges[4:])
|
||||||
|
)
|
||||||
status = json.loads(
|
status = json.loads(
|
||||||
(root / "status.json").read_text(encoding="utf-8")
|
(root / "status.json").read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
@@ -443,9 +460,9 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
messages,
|
messages,
|
||||||
[
|
[
|
||||||
"3 packages",
|
"Resolving 3 packages",
|
||||||
"3/3 packages",
|
"Downloading packages 3/3",
|
||||||
"3/3 packages",
|
"Installing packages 3/3",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertEqual(mocked_write.call_args_list[0].args[1], 0.02)
|
self.assertEqual(mocked_write.call_args_list[0].args[1], 0.02)
|
||||||
@@ -508,8 +525,8 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
messages = [call.args[2] for call in mocked_write.call_args_list]
|
messages = [call.args[2] for call in mocked_write.call_args_list]
|
||||||
self.assertIn("3 packages", messages)
|
self.assertIn("Resolving 3 packages", messages)
|
||||||
self.assertIn("3/3 packages", messages)
|
self.assertIn("Downloading packages 3/3", messages)
|
||||||
|
|
||||||
def test_run_install_maps_progress_into_stage_range(self):
|
def test_run_install_maps_progress_into_stage_range(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
@@ -585,6 +602,22 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
self.assertEqual(bootstrap.format_elapsed(12), "12s")
|
self.assertEqual(bootstrap.format_elapsed(12), "12s")
|
||||||
self.assertEqual(bootstrap.format_elapsed(83), "1m 23s")
|
self.assertEqual(bootstrap.format_elapsed(83), "1m 23s")
|
||||||
|
|
||||||
|
def test_ai_component_estimate_continues_across_internal_steps(self):
|
||||||
|
bootstrap = load_bootstrap_runtime()
|
||||||
|
|
||||||
|
first = bootstrap.estimated_remaining(
|
||||||
|
"AI components",
|
||||||
|
bootstrap.XFORMERS_SLICE[0],
|
||||||
|
)
|
||||||
|
second = bootstrap.estimated_remaining(
|
||||||
|
"AI components",
|
||||||
|
bootstrap.AI_COMPONENTS_SLICE[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(first, bootstrap.INSTALL_ESTIMATES["AI components"])
|
||||||
|
self.assertGreater(second, 0)
|
||||||
|
self.assertLess(second, first)
|
||||||
|
|
||||||
def test_resolve_indexes_uses_configured_pypi_index(self):
|
def test_resolve_indexes_uses_configured_pypi_index(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
bootstrap.resolve_indexes(
|
bootstrap.resolve_indexes(
|
||||||
@@ -739,8 +772,10 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_model_download_failure_keeps_runtime_ready(self):
|
def test_model_download_failure_keeps_runtime_ready(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
|
commands = []
|
||||||
|
|
||||||
def fake_run(command, environment=None):
|
def fake_run(command, environment=None):
|
||||||
|
commands.append(command)
|
||||||
if any("DA3-SMALL" in str(value) for value in command):
|
if any("DA3-SMALL" in str(value) for value in command):
|
||||||
raise subprocess.CalledProcessError(1, command)
|
raise subprocess.CalledProcessError(1, command)
|
||||||
|
|
||||||
@@ -775,6 +810,12 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(status["progress"], 1.0)
|
self.assertEqual(status["progress"], 1.0)
|
||||||
self.assertIn("Runtime is ready", status["message"])
|
self.assertIn("Runtime is ready", status["message"])
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
"PramaLLC/BEN2" in map(str, command)
|
||||||
|
for command in commands
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_install_fails_when_bundled_uv_missing(self):
|
def test_install_fails_when_bundled_uv_missing(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
@@ -806,6 +847,10 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
), patch.object(
|
), patch.object(
|
||||||
bootstrap.shutil,
|
bootstrap.shutil,
|
||||||
"rmtree",
|
"rmtree",
|
||||||
|
), patch.object(
|
||||||
|
bootstrap,
|
||||||
|
"bundled_uv_archive",
|
||||||
|
return_value=None,
|
||||||
):
|
):
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
RuntimeError,
|
RuntimeError,
|
||||||
@@ -1011,7 +1056,7 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
self.assertIn(f"depth-anything-3 @ {da3_local}", ai_command)
|
self.assertIn(f"depth-anything-3 @ {da3_local}", ai_command)
|
||||||
self.assertIn(f"ben2 @ {ben2_local}", ai_command)
|
self.assertIn(f"ben2 @ {ben2_local}", ai_command)
|
||||||
|
|
||||||
def test_install_stages_download_models_into_prefixed_directories(self):
|
def test_install_stages_download_only_default_models(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
@@ -1045,9 +1090,42 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
for label, command, _environment, kind in stages
|
for label, command, _environment, kind in stages
|
||||||
if kind == "download"
|
if kind == "download"
|
||||||
}
|
}
|
||||||
self.assertIn("da3_small", " ".join(map(str, downloads["da3 small"])))
|
self.assertIn("da3_small", " ".join(map(str, downloads["DA3 Small"])))
|
||||||
self.assertIn("da3_base", " ".join(map(str, downloads["da3 base"])))
|
self.assertIn("ben2_base", " ".join(map(str, downloads["BEN2 Base"])))
|
||||||
self.assertIn("ben2_base", " ".join(map(str, downloads["ben2 base"])))
|
self.assertNotIn("DA3 Base", downloads)
|
||||||
|
|
||||||
|
def test_install_does_not_verify_runtime(self):
|
||||||
|
bootstrap = load_bootstrap_runtime()
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
runtime_dir = root / "runtime"
|
||||||
|
runtime_dir.mkdir()
|
||||||
|
self.create_bundled_sources(root)
|
||||||
|
uv_path = root / "uv"
|
||||||
|
uv_path.write_bytes(b"uv")
|
||||||
|
args = SimpleNamespace(
|
||||||
|
uv=str(uv_path),
|
||||||
|
runtime_dir=str(runtime_dir),
|
||||||
|
models_dir=str(root / "models"),
|
||||||
|
backend="cu128",
|
||||||
|
da3_commit="test-commit",
|
||||||
|
ben2_commit="test-ben2-commit",
|
||||||
|
status=str(root / "status.json"),
|
||||||
|
)
|
||||||
|
with patch.object(
|
||||||
|
bootstrap,
|
||||||
|
"resolve_indexes",
|
||||||
|
return_value=None,
|
||||||
|
), patch.object(bootstrap, "run"), patch.object(
|
||||||
|
bootstrap,
|
||||||
|
"run_install",
|
||||||
|
), patch.object(
|
||||||
|
bootstrap,
|
||||||
|
"run_verify",
|
||||||
|
) as mocked_verify:
|
||||||
|
bootstrap.install(args)
|
||||||
|
|
||||||
|
mocked_verify.assert_not_called()
|
||||||
|
|
||||||
def test_run_verify_returns_detected_devices(self):
|
def test_run_verify_returns_detected_devices(self):
|
||||||
bootstrap = load_bootstrap_runtime()
|
bootstrap = load_bootstrap_runtime()
|
||||||
@@ -1111,7 +1189,7 @@ class BootstrapRuntimeTest(unittest.TestCase):
|
|||||||
|
|
||||||
messages = [call.args[2] for call in mocked_write.call_args_list]
|
messages = [call.args[2] for call in mocked_write.call_args_list]
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any("verifying runtime" in message for message in messages)
|
any("Checking Runtime" in message for message in messages)
|
||||||
)
|
)
|
||||||
self.assertEqual(report, {})
|
self.assertEqual(report, {})
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class DownloadProgressTest(unittest.TestCase):
|
|||||||
|
|
||||||
status = json.loads(status_path.read_text(encoding="utf-8"))
|
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||||
self.assertAlmostEqual(status["progress"], 0.275)
|
self.assertAlmostEqual(status["progress"], 0.275)
|
||||||
self.assertEqual(status["message"], "downloading owner/model")
|
self.assertEqual(status["message"], "Downloading owner/model")
|
||||||
|
|
||||||
def test_reports_download_speed(self):
|
def test_reports_download_speed(self):
|
||||||
class FakeTqdmWithRate(FakeTqdm):
|
class FakeTqdmWithRate(FakeTqdm):
|
||||||
@@ -87,9 +87,31 @@ class DownloadProgressTest(unittest.TestCase):
|
|||||||
status = json.loads(status_path.read_text(encoding="utf-8"))
|
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
status["message"],
|
status["message"],
|
||||||
"downloading da3 small 13.4 MB/s",
|
"Downloading da3 small | 13.4 MB/s",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_reports_estimated_time_remaining(self):
|
||||||
|
times = iter((0.0, 10.0))
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
status_path = Path(directory) / "status.json"
|
||||||
|
progress_class = self.worker.create_download_progress_class(
|
||||||
|
status_path,
|
||||||
|
"owner/model",
|
||||||
|
tqdm_type=FakeTqdm,
|
||||||
|
clock=lambda: next(times),
|
||||||
|
)
|
||||||
|
progress = progress_class(
|
||||||
|
desc="Downloading bytes",
|
||||||
|
total=100,
|
||||||
|
initial=0,
|
||||||
|
unit="B",
|
||||||
|
)
|
||||||
|
|
||||||
|
progress.update(50)
|
||||||
|
|
||||||
|
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(status["eta_seconds"], 10)
|
||||||
|
|
||||||
def test_staged_download_prefixes_speed_with_downloading(self):
|
def test_staged_download_prefixes_speed_with_downloading(self):
|
||||||
class FakeTqdmWithRate(FakeTqdm):
|
class FakeTqdmWithRate(FakeTqdm):
|
||||||
@property
|
@property
|
||||||
@@ -118,7 +140,10 @@ class DownloadProgressTest(unittest.TestCase):
|
|||||||
progress.update(50)
|
progress.update(50)
|
||||||
|
|
||||||
status = json.loads(status_path.read_text(encoding="utf-8"))
|
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||||
self.assertEqual(status["message"], "downloading 13.4 MB/s")
|
self.assertEqual(
|
||||||
|
status["message"],
|
||||||
|
"Downloading packages | 13.4 MB/s",
|
||||||
|
)
|
||||||
self.assertEqual(status["stage"], 6)
|
self.assertEqual(status["stage"], 6)
|
||||||
self.assertEqual(status["stages"], 8)
|
self.assertEqual(status["stages"], 8)
|
||||||
self.assertEqual(status["stage_label"], "da3 small")
|
self.assertEqual(status["stage_label"], "da3 small")
|
||||||
|
|||||||
Reference in New Issue
Block a user