feat: Bump version to 0.11.0 and update dependencies
- Updated ONNX Runtime to 1.24.4 and replaced OpenCV with PyAV 17.0.0 in the installation environment. - Enhanced model downloading logic to prioritize Hugging Face and fallback to OmooLab R2, with improved error handling and status reporting. - Introduced a new environment revision file for shared metadata. - Refactored video frame extraction to utilize the PyAV library instead of OpenCV. - Improved installation process to handle retries across specified PyPI indexes. - Added tests for download progress reporting and model downloading behavior.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "snapform"
|
||||
version = "0.10.12"
|
||||
version = "0.11.0"
|
||||
name = "SnapForm"
|
||||
tagline = "Build spatial structures from images, fast"
|
||||
maintainer = "OmooLab"
|
||||
|
||||
@@ -18,6 +18,7 @@ from urllib.request import ProxyHandler, Request, build_opener
|
||||
|
||||
import bpy
|
||||
|
||||
from .env_revision import MANIFEST_FILENAME, MANIFEST_REVISION
|
||||
from .models import (
|
||||
BEN2_MODEL,
|
||||
get_downloadable_model,
|
||||
@@ -26,8 +27,6 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
MANIFEST_REVISION = 7
|
||||
SERVICE_ADDRESS = "127.0.0.1"
|
||||
DEFAULT_STORAGE_ROOT = Path.home() / ".snapform"
|
||||
JOB_FOLDER_NAMES = {
|
||||
|
||||
4
src/snapform/env_revision.py
Normal file
4
src/snapform/env_revision.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Shared environment metadata for the Blender UI and the install subprocess."""
|
||||
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
MANIFEST_REVISION = 8
|
||||
@@ -19,6 +19,7 @@ SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIRECTORY) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIRECTORY))
|
||||
|
||||
from env_revision import MANIFEST_REVISION
|
||||
from models import BEN2_MODEL, DOWNLOADABLE_MODELS, MOGE2_MODEL
|
||||
|
||||
|
||||
@@ -32,10 +33,9 @@ UVICORN_VERSION = "0.51.0"
|
||||
ONNXRUNTIME_VERSION = "1.24.4"
|
||||
NUMPY_VERSION = "2.4.2"
|
||||
PILLOW_VERSION = "12.1.1"
|
||||
OPENCV_VERSION = "4.13.0.92"
|
||||
AV_VERSION = "17.0.0"
|
||||
TRIMESH_VERSION = "4.11.2"
|
||||
HUGGINGFACE_HUB_VERSION = "1.4.1"
|
||||
MANIFEST_REVISION = 7
|
||||
_resolved_pypi_index = None
|
||||
DEFAULT_MODEL_KEYS = (MOGE2_MODEL.key, BEN2_MODEL.key)
|
||||
RUNTIME_STAGE_COUNT = 2
|
||||
@@ -45,15 +45,9 @@ MODELS_SLICE_START = 0.85
|
||||
MODELS_SLICE_END = 1.0
|
||||
RAMP_RATE = 0.02
|
||||
CREEP_CAP = 0.60
|
||||
INSTALL_ESTIMATES = {
|
||||
"ONNX Runtime": 3 * 60,
|
||||
}
|
||||
INSTALL_ESTIMATE_RANGES = {
|
||||
"ONNX Runtime": AI_COMPONENTS_SLICE,
|
||||
}
|
||||
CHECK_CODE = (
|
||||
"import json; import fastapi, uvicorn, onnxruntime; "
|
||||
"import cv2, numpy, PIL, trimesh; "
|
||||
"import av, numpy, PIL, trimesh; "
|
||||
"print('SNAPFORM_DEVICES=' + json.dumps({"
|
||||
"'onnxruntime': onnxruntime.__version__, "
|
||||
"'providers': onnxruntime.get_available_providers()}))"
|
||||
@@ -76,7 +70,6 @@ def write_status(
|
||||
stage=None,
|
||||
stages=None,
|
||||
stage_label=None,
|
||||
eta_seconds=None,
|
||||
):
|
||||
payload = {"progress": progress, "message": message}
|
||||
if error:
|
||||
@@ -85,8 +78,6 @@ 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",
|
||||
@@ -103,7 +94,7 @@ def child_environment(environment=None):
|
||||
if env_key not in process_environment:
|
||||
process_environment[env_key] = value
|
||||
if _resolved_pypi_index:
|
||||
process_environment["PIP_INDEX_URL"] = _resolved_pypi_index
|
||||
process_environment["UV_INDEX_URL"] = _resolved_pypi_index
|
||||
if environment:
|
||||
process_environment.update(environment)
|
||||
return process_environment
|
||||
@@ -117,13 +108,6 @@ def run(command, environment=None):
|
||||
)
|
||||
|
||||
|
||||
def format_elapsed(seconds):
|
||||
seconds = int(seconds)
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
return f"{seconds // 60}m {seconds % 60:02d}s"
|
||||
|
||||
|
||||
def parse_package_count(line):
|
||||
try:
|
||||
return int(line.split()[1])
|
||||
@@ -131,26 +115,12 @@ 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 resolve_indexes(args):
|
||||
"""???????????????????????????????????"""
|
||||
"""确定用户指定的 PyPI 源,优先命令行参数,其次 uv/pip 的环境变量。"""
|
||||
global _resolved_pypi_index
|
||||
_resolved_pypi_index = (
|
||||
getattr(args, "pypi_index", None)
|
||||
or os.environ.get("UV_INDEX_URL")
|
||||
or os.environ.get("PIP_INDEX_URL")
|
||||
)
|
||||
|
||||
@@ -206,11 +176,6 @@ def _run_install_once(
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
eta_seconds=estimated_remaining(
|
||||
label,
|
||||
displayed,
|
||||
clock() - started,
|
||||
),
|
||||
)
|
||||
|
||||
while True:
|
||||
@@ -233,7 +198,21 @@ def _run_install_once(
|
||||
if empty:
|
||||
if elapsed - last_heartbeat >= 5.0:
|
||||
last_heartbeat = elapsed
|
||||
detail("Downloading packages")
|
||||
# uv 的下载阶段没有增量进度,只报告已解析的包总数,
|
||||
# 让用户对规模有预期,而不承诺剩余时间或已耗时。
|
||||
message = (
|
||||
f"Downloading {total} packages"
|
||||
if total
|
||||
else "Downloading packages"
|
||||
)
|
||||
write_status(
|
||||
status_path,
|
||||
displayed,
|
||||
message,
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
)
|
||||
continue
|
||||
line = raw_line.rstrip()
|
||||
print(line)
|
||||
@@ -273,13 +252,20 @@ def run_install(
|
||||
end=1.0,
|
||||
clock=time.monotonic,
|
||||
):
|
||||
"""Run a uv install command, retrying with mirrors when the default index fails."""
|
||||
attempts = [environment]
|
||||
if _resolved_pypi_index in (None, PYPI_DEFAULT_INDEX):
|
||||
attempts.extend(
|
||||
{"PIP_INDEX_URL": mirror} for mirror in PYPI_MIRRORS
|
||||
)
|
||||
for index, attempt_environment in enumerate(attempts):
|
||||
"""Run a uv install command, retrying across PyPI indexes on failure."""
|
||||
if _resolved_pypi_index is None:
|
||||
# 未指定源时优先国内镜像,pypi.org 作为最终兜底。
|
||||
indexes = (*PYPI_MIRRORS, PYPI_DEFAULT_INDEX)
|
||||
elif _resolved_pypi_index == PYPI_DEFAULT_INDEX:
|
||||
# 明确选择官方源时,失败后回退国内镜像。
|
||||
indexes = (PYPI_DEFAULT_INDEX, *PYPI_MIRRORS)
|
||||
else:
|
||||
# 用户指定了特定源,不再回退。
|
||||
indexes = (_resolved_pypi_index,)
|
||||
attempts = [
|
||||
dict(environment or {}, UV_INDEX_URL=index) for index in indexes
|
||||
]
|
||||
for position, attempt_environment in enumerate(attempts):
|
||||
try:
|
||||
return _run_install_once(
|
||||
status_path,
|
||||
@@ -293,9 +279,12 @@ def run_install(
|
||||
clock,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
if index + 1 == len(attempts):
|
||||
if position + 1 == len(attempts):
|
||||
raise
|
||||
print(f"Retrying with PyPI mirror: {PYPI_MIRRORS[0]}")
|
||||
print(
|
||||
"Retrying with next PyPI index: "
|
||||
f"{attempts[position + 1]['UV_INDEX_URL']}"
|
||||
)
|
||||
|
||||
|
||||
def venv_python(venv_dir):
|
||||
@@ -330,7 +319,7 @@ def install_stages(args, venv_dir, python, uv):
|
||||
f"fastapi=={FASTAPI_VERSION}",
|
||||
f"huggingface-hub=={HUGGINGFACE_HUB_VERSION}",
|
||||
f"numpy=={NUMPY_VERSION}",
|
||||
f"opencv-python-headless=={OPENCV_VERSION}",
|
||||
f"av=={AV_VERSION}",
|
||||
runtime_package,
|
||||
f"pillow=={PILLOW_VERSION}",
|
||||
f"trimesh=={TRIMESH_VERSION}",
|
||||
@@ -465,7 +454,6 @@ def run_verify(
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=stage_label,
|
||||
eta_seconds=max(60.0 - elapsed, 0.0),
|
||||
)
|
||||
continue
|
||||
line = raw_line.rstrip()
|
||||
@@ -596,7 +584,6 @@ def run_stage(
|
||||
stage=stage,
|
||||
stages=stages,
|
||||
stage_label=label,
|
||||
eta_seconds=estimated_remaining(label, start),
|
||||
)
|
||||
installed = []
|
||||
if kind == "install":
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.request import (
|
||||
HTTPSHandler,
|
||||
ProxyHandler,
|
||||
Request,
|
||||
build_opener,
|
||||
urlopen,
|
||||
)
|
||||
|
||||
|
||||
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
||||
@@ -40,7 +48,29 @@ EXPORT_FORMATS = (
|
||||
)
|
||||
HF_ENDPOINT = "https://huggingface.co"
|
||||
R2_ENDPOINT = "https://models.omoolab.xyz"
|
||||
DOWNLOAD_TIMEOUT = 60.0
|
||||
# 连接(TCP + TLS 握手)超时,源不可达时快速失败切换;下载阶段不设读超时。
|
||||
CONNECT_TIMEOUT = 6.0
|
||||
|
||||
|
||||
class _ShortConnectHTTPSConnection(http.client.HTTPSConnection):
|
||||
"""HTTPS 连接:TCP + TLS 握手用短超时,读操作不设超时。"""
|
||||
|
||||
def connect(self):
|
||||
try:
|
||||
self.timeout = CONNECT_TIMEOUT
|
||||
super().connect()
|
||||
finally:
|
||||
self.timeout = None
|
||||
# 连接建立后读操作不设超时,下载大文件不会被读超时中断。
|
||||
self.sock.settimeout(None)
|
||||
|
||||
|
||||
class _ShortConnectHTTPSHandler(HTTPSHandler):
|
||||
def https_open(self, req):
|
||||
return self.do_open(_ShortConnectHTTPSConnection, req)
|
||||
|
||||
|
||||
_download_opener = build_opener(ProxyHandler(), _ShortConnectHTTPSHandler())
|
||||
|
||||
|
||||
class TaskCancelled(RuntimeError):
|
||||
@@ -107,34 +137,32 @@ def extract_video_frames(
|
||||
video_path,
|
||||
output_directory,
|
||||
maximum_frames,
|
||||
cv2,
|
||||
av,
|
||||
start_frame=1,
|
||||
cancel_check=None,
|
||||
):
|
||||
output_directory = Path(output_directory)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
capture = cv2.VideoCapture(str(video_path))
|
||||
if not capture.isOpened():
|
||||
raise RuntimeError(f"Unable to open video: {video_path}")
|
||||
try:
|
||||
container = av.open(str(video_path))
|
||||
except Exception as error:
|
||||
raise RuntimeError(f"Unable to open video: {video_path}") from error
|
||||
image_paths = []
|
||||
skipped = 0
|
||||
try:
|
||||
while len(image_paths) < maximum_frames:
|
||||
stream = container.streams.video[0]
|
||||
for frame in container.decode(stream):
|
||||
check_cancelled(cancel_check)
|
||||
success, frame = capture.read()
|
||||
if not success:
|
||||
break
|
||||
if skipped < start_frame - 1:
|
||||
skipped += 1
|
||||
continue
|
||||
image_path = (
|
||||
output_directory / f"frame.{len(image_paths) + 1:04d}.jpg"
|
||||
)
|
||||
if not cv2.imwrite(str(image_path), frame):
|
||||
raise RuntimeError(f"Unable to write video frame: {image_path}")
|
||||
image_path = output_directory / f"frame.{len(image_paths) + 1:04d}.jpg"
|
||||
frame.to_image().save(image_path, format="JPEG")
|
||||
image_paths.append(image_path)
|
||||
if len(image_paths) >= maximum_frames:
|
||||
break
|
||||
finally:
|
||||
capture.release()
|
||||
container.close()
|
||||
|
||||
if not image_paths:
|
||||
raise RuntimeError(f"Unable to read any video frame from: {video_path}")
|
||||
@@ -162,7 +190,7 @@ def prepare_input(
|
||||
supported = ", ".join(sorted(IMAGE_SUFFIXES | VIDEO_SUFFIXES))
|
||||
raise RuntimeError(f"Unsupported input type; expected one of: {supported}")
|
||||
|
||||
import cv2
|
||||
import av
|
||||
|
||||
temporary_directory = Path(
|
||||
tempfile.mkdtemp(prefix="snapform-video-", dir=temporary_root)
|
||||
@@ -172,7 +200,7 @@ def prepare_input(
|
||||
input_path,
|
||||
temporary_directory,
|
||||
video_frames,
|
||||
cv2,
|
||||
av,
|
||||
start_frame=start_frame,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
@@ -254,33 +282,6 @@ def unproject_camera_depth(depth, intrinsics):
|
||||
return (rays * depth.reshape(-1, 1)).reshape(height, width, 3)
|
||||
|
||||
|
||||
def smooth_depth_map(depth, smoothing):
|
||||
import numpy as np
|
||||
|
||||
factor = min(max(float(smoothing) / 100.0, 0.0), 1.0)
|
||||
if factor <= 0.0:
|
||||
return np.asarray(depth, dtype=np.float32).copy()
|
||||
import cv2
|
||||
|
||||
values = np.asarray(depth, dtype=np.float32)
|
||||
valid = np.isfinite(values) & (values > 0.0)
|
||||
if not valid.any():
|
||||
raise ValueError("DA3 returned no valid depth values")
|
||||
low, high = np.percentile(values[valid], (5.0, 95.0))
|
||||
depth_range = max(float(high - low), 1e-6)
|
||||
diameter = 3 + 2 * int(round(3.0 * factor))
|
||||
working_values = values.copy()
|
||||
working_values[~valid] = float(np.median(values[valid]))
|
||||
smoothed = cv2.bilateralFilter(
|
||||
working_values,
|
||||
diameter,
|
||||
depth_range * (0.01 + 0.09 * factor),
|
||||
1.0 + 4.0 * factor,
|
||||
)
|
||||
smoothed[~valid] = values[~valid]
|
||||
return smoothed
|
||||
|
||||
|
||||
def compact_mesh(vertices, faces):
|
||||
import numpy as np
|
||||
|
||||
@@ -961,8 +962,10 @@ def create_download_progress_class(
|
||||
class DownloadProgress(tqdm_type):
|
||||
def __init__(self, *args, **kwargs):
|
||||
description = str(kwargs.get("desc", ""))
|
||||
# huggingface_hub 1.x 的字节进度条描述是 "Downloading (incomplete total...)",
|
||||
# 用前缀匹配而非精确匹配,否则真实下载进度会被忽略。
|
||||
self.snapform_reports_progress = (
|
||||
description == "Downloading bytes"
|
||||
description.startswith("Downloading")
|
||||
or description.startswith("Fetching ")
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -998,6 +1001,35 @@ def progress_range(args, staged):
|
||||
return 0.05, 0.90
|
||||
|
||||
|
||||
def _discovery_path(model_dir):
|
||||
"""storage_root/service.json,从 storage_root/models/<name> 反推 storage_root。"""
|
||||
return Path(model_dir).parent.parent / "service.json"
|
||||
|
||||
|
||||
def _models_from_r2(model_dir):
|
||||
try:
|
||||
payload = json.loads(
|
||||
_discovery_path(model_dir).read_text(encoding="utf-8")
|
||||
)
|
||||
except (OSError, ValueError, json.JSONDecodeError):
|
||||
return False
|
||||
return bool(payload.get("models_from_r2"))
|
||||
|
||||
|
||||
def _mark_models_from_r2(model_dir):
|
||||
"""在 service.json 上记录 models_from_r2,保留文件里已有的其它字段。"""
|
||||
path = _discovery_path(model_dir)
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError, json.JSONDecodeError):
|
||||
payload = {}
|
||||
payload["models_from_r2"] = True
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(f"{path.name}.tmp")
|
||||
temporary.write_text(json.dumps(payload), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def download_model(args):
|
||||
label = getattr(args, "label", None) or args.repository
|
||||
model_dir = Path(args.model_dir)
|
||||
@@ -1009,6 +1041,77 @@ def download_model(args):
|
||||
"stages": getattr(args, "stages", None),
|
||||
"stage_label": getattr(args, "stage_label", None),
|
||||
}
|
||||
r2_directory = getattr(args, "r2_directory", "")
|
||||
r2_files = getattr(args, "r2_file", [])
|
||||
if r2_directory and r2_files:
|
||||
_download_declared_or_fail(
|
||||
args,
|
||||
model_dir,
|
||||
label,
|
||||
start,
|
||||
span,
|
||||
stage_kwargs,
|
||||
)
|
||||
return
|
||||
_download_snapshot_or_fail(args, model_dir, label, start, span, stage_kwargs)
|
||||
|
||||
|
||||
def _download_declared_or_fail(args, model_dir, label, start, span, stage_kwargs):
|
||||
"""Stream declared files from Hugging Face, then the R2 mirror, remembering fallback."""
|
||||
if _models_from_r2(model_dir):
|
||||
sources = (
|
||||
(
|
||||
R2_ENDPOINT,
|
||||
download_from_r2,
|
||||
f"Downloading {label} | OmooLab mirror",
|
||||
),
|
||||
)
|
||||
else:
|
||||
sources = (
|
||||
(HF_ENDPOINT, download_from_hf, f"Downloading {label}"),
|
||||
(
|
||||
R2_ENDPOINT,
|
||||
download_from_r2,
|
||||
f"Downloading {label} | OmooLab mirror",
|
||||
),
|
||||
)
|
||||
failures = []
|
||||
for position, (source, downloader, message) in enumerate(sources):
|
||||
write_status(args.status, start, message, **stage_kwargs)
|
||||
try:
|
||||
downloader(
|
||||
args,
|
||||
model_dir,
|
||||
start=start,
|
||||
span=span,
|
||||
label=label,
|
||||
stage_kwargs=stage_kwargs,
|
||||
)
|
||||
_verify_model_complete(
|
||||
model_dir,
|
||||
getattr(args, "required_pattern", None),
|
||||
)
|
||||
except Exception as error:
|
||||
failures.append((source, error))
|
||||
continue
|
||||
# 非首个源成功意味着 hf 已失败,记住本次 service 周期后续直接走 r2。
|
||||
if position > 0:
|
||||
_mark_models_from_r2(model_dir)
|
||||
write_status(
|
||||
args.status,
|
||||
start + span,
|
||||
f"{label} ready",
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
detail = "; ".join(
|
||||
f"{source}: {error}" for source, error in failures
|
||||
)
|
||||
raise RuntimeError(f"All download sources failed: {detail}")
|
||||
|
||||
|
||||
def _download_snapshot_or_fail(args, model_dir, label, start, span, stage_kwargs):
|
||||
"""Fallback download for repositories without declared files, via huggingface_hub."""
|
||||
failures = []
|
||||
try:
|
||||
from huggingface_hub import snapshot_download
|
||||
@@ -1027,7 +1130,9 @@ def download_model(args):
|
||||
)
|
||||
for endpoint in download_endpoints() if snapshot_download else ():
|
||||
os.environ["HF_ENDPOINT"] = endpoint
|
||||
message = f"Downloading {label}"
|
||||
# snapshot_download 先拉取文件清单并校验 ETag,还没有字节下载,
|
||||
# 用 Checking 提示当前阶段,避免进度条看起来卡住。
|
||||
message = f"Checking {label} files"
|
||||
if endpoint != HF_ENDPOINT:
|
||||
message = f"{message} (mirror)"
|
||||
write_status(args.status, start, message, **stage_kwargs)
|
||||
@@ -1053,43 +1158,6 @@ def download_model(args):
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
# Hugging Face 失败后使用由项目维护的静态模型镜像。
|
||||
r2_directory = getattr(args, "r2_directory", "")
|
||||
r2_files = getattr(args, "r2_file", [])
|
||||
if not r2_directory or not r2_files:
|
||||
detail = "; ".join(
|
||||
f"{source}: {error}" for source, error in failures
|
||||
)
|
||||
raise RuntimeError(f"All download sources failed: {detail}")
|
||||
try:
|
||||
write_status(
|
||||
args.status,
|
||||
start,
|
||||
f"Downloading {label} | OmooLab mirror",
|
||||
**stage_kwargs,
|
||||
)
|
||||
download_from_r2(
|
||||
args,
|
||||
model_dir,
|
||||
start=start,
|
||||
span=span,
|
||||
label=label,
|
||||
stage_kwargs=stage_kwargs,
|
||||
)
|
||||
_verify_model_complete(
|
||||
model_dir,
|
||||
getattr(args, "required_pattern", None),
|
||||
)
|
||||
except Exception as error:
|
||||
failures.append((R2_ENDPOINT, error))
|
||||
else:
|
||||
write_status(
|
||||
args.status,
|
||||
start + span,
|
||||
f"{label} ready",
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
detail = "; ".join(
|
||||
f"{source}: {error}" for source, error in failures
|
||||
)
|
||||
@@ -1121,25 +1189,24 @@ def _verify_model_complete(model_dir, required_patterns=None):
|
||||
)
|
||||
|
||||
|
||||
def _download_r2_file(
|
||||
directory,
|
||||
filename,
|
||||
def _download_file_from_url(
|
||||
url,
|
||||
destination,
|
||||
expected_size,
|
||||
expected_checksum,
|
||||
progress=None,
|
||||
retries=2,
|
||||
):
|
||||
"""Download and verify one OmooLab R2 model file atomically."""
|
||||
url = f"{R2_ENDPOINT}/{quote(directory)}/{quote(filename)}"
|
||||
"""Download and verify one model file atomically from a direct URL."""
|
||||
request = Request(url, headers={"User-Agent": "SnapForm"})
|
||||
opener = _download_opener.open if request.type == "https" else urlopen
|
||||
last_error = None
|
||||
for _attempt in range(retries):
|
||||
temporary = destination.with_name(f"{destination.name}.part")
|
||||
try:
|
||||
checksum = hashlib.sha256()
|
||||
downloaded = 0
|
||||
with urlopen(request, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||
with opener(request) as response:
|
||||
with open(temporary, "wb") as output:
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024)
|
||||
@@ -1152,11 +1219,11 @@ def _download_r2_file(
|
||||
progress(len(chunk))
|
||||
if downloaded != expected_size:
|
||||
raise RuntimeError(
|
||||
f"Size mismatch for {filename}: "
|
||||
f"Size mismatch for {destination.name}: "
|
||||
f"{downloaded} != {expected_size}"
|
||||
)
|
||||
if checksum.hexdigest().lower() != expected_checksum.lower():
|
||||
raise RuntimeError(f"SHA-256 mismatch for {filename}")
|
||||
raise RuntimeError(f"SHA-256 mismatch for {destination.name}")
|
||||
temporary.replace(destination)
|
||||
return
|
||||
except Exception as error:
|
||||
@@ -1168,20 +1235,37 @@ def _download_r2_file(
|
||||
raise last_error
|
||||
|
||||
|
||||
def download_from_r2(
|
||||
args,
|
||||
model_dir,
|
||||
start=0.05,
|
||||
span=0.90,
|
||||
label=None,
|
||||
stage_kwargs=None,
|
||||
):
|
||||
"""Download the declared model files from OmooLab R2."""
|
||||
repository = args.repository
|
||||
def r2_file_url(directory, filename):
|
||||
return f"{R2_ENDPOINT}/{quote(directory)}/{quote(filename)}"
|
||||
|
||||
|
||||
def hf_file_url(repository, filename):
|
||||
"""Hugging Face resolve URL for a direct, metadata-free download."""
|
||||
endpoint = (os.environ.get("HF_ENDPOINT") or HF_ENDPOINT).rstrip("/")
|
||||
return f"{endpoint}/{repository}/resolve/main/{quote(filename)}"
|
||||
|
||||
|
||||
def _parse_declared_files(args):
|
||||
selected = []
|
||||
for value in args.r2_file:
|
||||
filename, size, checksum = value.split("|", 2)
|
||||
selected.append((filename, int(size), checksum))
|
||||
return selected
|
||||
|
||||
|
||||
def _download_declared_files(
|
||||
args,
|
||||
model_dir,
|
||||
url_builder,
|
||||
start,
|
||||
span,
|
||||
label,
|
||||
stage_kwargs,
|
||||
retries=2,
|
||||
):
|
||||
"""Download each declared file through url_builder with aggregated progress."""
|
||||
repository = args.repository
|
||||
selected = _parse_declared_files(args)
|
||||
reporter = DownloadProgressReporter(
|
||||
args.status,
|
||||
repository,
|
||||
@@ -1201,16 +1285,57 @@ def download_from_r2(
|
||||
for path, size, checksum in selected:
|
||||
destination = Path(model_dir) / path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
_download_r2_file(
|
||||
args.r2_directory,
|
||||
path,
|
||||
_download_file_from_url(
|
||||
url_builder(path),
|
||||
destination,
|
||||
size,
|
||||
checksum,
|
||||
progress=on_chunk,
|
||||
retries=retries,
|
||||
)
|
||||
|
||||
|
||||
def download_from_r2(
|
||||
args,
|
||||
model_dir,
|
||||
start=0.05,
|
||||
span=0.90,
|
||||
label=None,
|
||||
stage_kwargs=None,
|
||||
):
|
||||
"""Download the declared model files from OmooLab R2."""
|
||||
_download_declared_files(
|
||||
args,
|
||||
model_dir,
|
||||
lambda filename: r2_file_url(args.r2_directory, filename),
|
||||
start,
|
||||
span,
|
||||
label,
|
||||
stage_kwargs,
|
||||
)
|
||||
|
||||
|
||||
def download_from_hf(
|
||||
args,
|
||||
model_dir,
|
||||
start=0.05,
|
||||
span=0.90,
|
||||
label=None,
|
||||
stage_kwargs=None,
|
||||
):
|
||||
"""Download the declared model files directly from Hugging Face resolve URLs."""
|
||||
_download_declared_files(
|
||||
args,
|
||||
model_dir,
|
||||
lambda filename: hf_file_url(args.repository, filename),
|
||||
start,
|
||||
span,
|
||||
label,
|
||||
stage_kwargs,
|
||||
retries=1,
|
||||
)
|
||||
|
||||
|
||||
def format_rate(rate):
|
||||
size = float(rate)
|
||||
if size <= 0:
|
||||
|
||||
Reference in New Issue
Block a user