Skip to content

fix: make the first-use weight download resolve every id and fail loudly - #75

Merged
Hendrik-code merged 2 commits into
mainfrom
fix/model-download
Oct 19, 2026
Merged

Hendrik-code merged 2 commits into
mainfrom
fix/model-download

Conversation

@Hendrik-code

Copy link
Copy Markdown
Owner

Model weights download automatically on first use, so this code path is the very first thing a new user
hits. It had three defects.

1. Four advertised model ids crashed

auto_download registers each semantic model twice, t2w and t2w_semantic, but download_names only
has the canonical key:

>>> [k for k in semantic if canonical_model_key(k) not in download_names]   # on main
['t2w_semantic', 't1w_semantic', 'vibe_semantic', 'ct_semantic']

These ids are listed to the user in the "Model 'x' does not exist. Available semantic models: [...]"
error message, so the obvious next step -- copy one of the listed names -- died with
RuntimeError: Failed to download model 't2w_semantic' ...: KeyError: 't2w_semantic'.

ct_semantic had a second problem: phase_to_version is keyed SEMANTIC_ct, so the alias missed the CT
override and resolved to a CT_semantic_v1.0.9 folder while downloading the v2.0.0 archive.

One canonical_model_key() maps *_semantic onto the id the weights are registered under, and
download_if_missing raises a readable KeyError for a genuinely unknown id instead of a bare lookup
failure.

2. A failed download reported success

# on main
>>> download_weights("http://localhost:1/nope.zip", out)      # prints "Download attempt failed"
None                                                          # ... and returns
>>> download_if_missing("t2w", url, SpinepsPhase.SEMANTIC)
PosixPath('/tmp/.../T2w_semantic_v1.0.9')                      # path does not exist

The caller then handed that non-existent folder to the model loader, and the user got a config-not-found
error with no mention of the network. It now raises a RuntimeError naming the URL, the underlying cause
and the manual-download fallback from the README.

3. An interrupted download poisoned the cache permanently

out_path existing is the "model is installed" check, and the archive was extracted straight into it:

# on main, Ctrl-C during extraction:
interrupted: user pressed ctrl-c mid-extract
out_path exists now: True contents: ['weights.ckpt.partial']
 -> next run sees 'if not out_path.exists()' as False and never downloads again

A truncated download left a stray T2w_semantic_v1.0.9.zip in the models directory and a raw
BadZipFile. Download and extraction now happen in a unique staging folder, which is moved into place
only after its contents are verified to contain an inference_config.json, and is removed on any
failure. The unique staging name also means concurrent SPINEPS processes (a job array all starting at
once) no longer overwrite each other's partial download.

Tests

unit_tests/test_auto_download.py: every registry id resolves to a download name; alias and canonical id
give the same folder at the right release version; unknown id raises a readable error; unreachable URL,
truncated archive and config-less archive each raise and leave the models directory empty; flat and
nested archives both install correctly; an existing folder is never re-downloaded.

Full suite: 233 passed, ruff clean.

🤖 Generated with Claude Code

Hendrik-code and others added 2 commits October 5, 2026 08:21
Three defects in the automatic download:

* The `*_semantic` ids are registered as available models -- they even show up in the
  "Available semantic models: [...]" error message -- but had no `download_names` entry,
  so `--model-semantic t2w_semantic` on a machine without the weights died with
  `KeyError: 't2w_semantic'`. `ct_semantic` additionally missed the CT version override
  and resolved to the v1.0.9 folder name while downloading the v2.0.0 archive. A single
  `canonical_model_key()` now maps the alias onto the id the weights are registered under.
* `download_weights` caught every exception, logged "Download attempt failed" and
  returned normally, so `download_if_missing` handed back a path that does not exist and
  the real failure surfaced later as a config-not-found from the model loader. It now
  raises a RuntimeError naming the URL, the cause, and the manual-download fallback.
* The archive was extracted straight into the final model folder, whose existence is the
  only "is this model installed?" check there is. An extraction interrupted halfway
  (Ctrl-C, full disk) left a partial folder that was never retried and never worked, and
  a truncated download left a stray .zip plus a raw BadZipFile. Download and extraction
  now happen in a unique staging folder that is moved into place only after the contents
  are verified to hold an inference_config.json, and is removed on any failure. The
  unique staging name also stops concurrent SPINEPS processes (e.g. a job array) from
  overwriting each other's partial download.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Hendrik-code
Hendrik-code merged commit 19f0788 into main Oct 19, 2026
10 checks passed
@Hendrik-code
Hendrik-code deleted the fix/model-download branch October 19, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant