slide2vec 5.8.1__tar.gz → 5.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {slide2vec-5.8.1 → slide2vec-5.9.0}/PKG-INFO +3 -3
  2. {slide2vec-5.8.1 → slide2vec-5.9.0}/pyproject.toml +13 -9
  3. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/__init__.py +1 -1
  4. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/api.py +9 -9
  5. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/data/tile_reader.py +0 -1
  6. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/__init__.py +0 -3
  7. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/gigapath.py +5 -7
  8. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/slide.py +9 -17
  9. slide2vec-5.9.0/slide2vec/encoders/models/titan.py +180 -0
  10. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/inference.py +19 -53
  11. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/artifacts_collect.py +7 -8
  12. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/batching.py +1 -9
  13. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_sliding.py +2 -7
  14. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/model_settings.py +2 -6
  15. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/persist_callbacks.py +3 -0
  16. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/utils/log_utils.py +4 -9
  17. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/utils/tiling_io.py +0 -2
  18. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/PKG-INFO +3 -3
  19. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/SOURCES.txt +4 -1
  20. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/requires.txt +2 -2
  21. slide2vec-5.9.0/tests/test_architecture_runtime_split.py +19 -0
  22. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_sliding.py +4 -7
  23. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_hs2p_package_cutover.py +3 -3
  24. slide2vec-5.9.0/tests/test_load_model_hf_auth.py +113 -0
  25. slide2vec-5.9.0/tests/test_on_slide_persisted.py +383 -0
  26. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_regression_inference.py +1 -5
  27. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_regression_models.py +1 -1
  28. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_soma_migration.py +0 -40
  29. slide2vec-5.9.0/tests/test_titan.py +134 -0
  30. slide2vec-5.8.1/slide2vec/encoders/models/titan.py +0 -58
  31. slide2vec-5.8.1/tests/test_architecture_runtime_split.py +0 -60
  32. {slide2vec-5.8.1 → slide2vec-5.9.0}/LICENSE +0 -0
  33. {slide2vec-5.8.1 → slide2vec-5.9.0}/README.md +0 -0
  34. {slide2vec-5.8.1 → slide2vec-5.9.0}/setup.cfg +0 -0
  35. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/__main__.py +0 -0
  36. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/artifacts.py +0 -0
  37. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/cli.py +0 -0
  38. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/configs/__init__.py +0 -0
  39. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/configs/default.yaml +0 -0
  40. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/configs/resources.py +0 -0
  41. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/data/__init__.py +0 -0
  42. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/data/dataset.py +0 -0
  43. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/data/tile_store.py +0 -0
  44. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/dense_image_worker.py +0 -0
  45. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/dense_worker.py +0 -0
  46. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/direct_embed_worker.py +0 -0
  47. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/image_worker.py +0 -0
  48. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/pipeline_worker.py +0 -0
  49. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/distributed/worker_entry.py +0 -0
  50. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/__init__.py +0 -0
  51. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/base.py +0 -0
  52. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/__init__.py +0 -0
  53. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/conch.py +0 -0
  54. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/dinov2.py +0 -0
  55. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/genbio.py +0 -0
  56. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/gpfm.py +0 -0
  57. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/hibou.py +0 -0
  58. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/hoptimus.py +0 -0
  59. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/isight.py +0 -0
  60. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/lunit.py +0 -0
  61. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/midnight.py +0 -0
  62. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  63. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  64. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/case.py +0 -0
  65. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
  66. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/moozy/types.py +0 -0
  67. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/mstar.py +0 -0
  68. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/musk.py +0 -0
  69. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/phikon.py +0 -0
  70. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/prism.py +0 -0
  71. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/prism2.py +0 -0
  72. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/prost40m.py +0 -0
  73. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/rudolfv2.py +0 -0
  74. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/uni.py +0 -0
  75. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/virchow.py +0 -0
  76. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/models/waiv.py +0 -0
  77. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/registry.py +0 -0
  78. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/encoders/validation.py +0 -0
  79. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/progress.py +0 -0
  80. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/__init__.py +0 -0
  81. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/cpu_budget.py +0 -0
  82. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_encode.py +0 -0
  83. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_encoder_input.py +0 -0
  84. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_reading.py +0 -0
  85. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_recipe.py +0 -0
  86. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_shard.py +0 -0
  87. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_image_stage.py +0 -0
  88. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_regions.py +0 -0
  89. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_shard.py +0 -0
  90. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/dense_stage.py +0 -0
  91. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/distributed.py +0 -0
  92. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/distributed_stage.py +0 -0
  93. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/effective_encoder_input.py +0 -0
  94. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/embedding.py +0 -0
  95. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/embedding_persist.py +0 -0
  96. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
  97. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/encoder_input_contract.py +0 -0
  98. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/hierarchical.py +0 -0
  99. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/image_shard.py +0 -0
  100. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/image_specs.py +0 -0
  101. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/image_stage.py +0 -0
  102. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/manifest.py +0 -0
  103. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/patient_pipeline.py +0 -0
  104. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/persistence.py +0 -0
  105. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/pooled_encoder_input.py +0 -0
  106. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/preprocessing.py +0 -0
  107. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/process_list.py +0 -0
  108. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/progress_bridge.py +0 -0
  109. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/registry.py +0 -0
  110. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/serialization.py +0 -0
  111. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/sharding.py +0 -0
  112. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/slide_encode.py +0 -0
  113. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/tiling.py +0 -0
  114. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
  115. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/types.py +0 -0
  116. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/runtime/worker_io.py +0 -0
  117. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/utils/__init__.py +0 -0
  118. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/utils/config.py +0 -0
  119. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/utils/coordinates.py +0 -0
  120. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec/utils/utils.py +0 -0
  121. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/dependency_links.txt +0 -0
  122. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/entry_points.txt +0 -0
  123. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/not-zip-safe +0 -0
  124. {slide2vec-5.8.1 → slide2vec-5.9.0}/slide2vec.egg-info/top_level.txt +0 -0
  125. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_attention_extraction.py +0 -0
  126. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_encode_kit.py +0 -0
  127. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_encoder_input.py +0 -0
  128. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_extraction.py +0 -0
  129. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_image_reading.py +0 -0
  130. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_image_resume.py +0 -0
  131. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_image_shard.py +0 -0
  132. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_image_stage.py +0 -0
  133. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_regions.py +0 -0
  134. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_shard.py +0 -0
  135. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_source_spacing.py +0 -0
  136. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_stage.py +0 -0
  137. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dense_worker.py +0 -0
  138. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_dinov2_natimage.py +0 -0
  139. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_encoder_capabilities.py +0 -0
  140. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_encoder_input_contract.py +0 -0
  141. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_encoder_plugins.py +0 -0
  142. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_encoder_provider_failures.py +0 -0
  143. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_encoder_registry.py +0 -0
  144. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_gpfm_genbio_heavy.py +0 -0
  145. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_image_shard.py +0 -0
  146. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_image_stage.py +0 -0
  147. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_isight.py +0 -0
  148. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_mascaret.py +0 -0
  149. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_output_consistency.py +0 -0
  150. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_patch_size_metadata.py +0 -0
  151. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_patient_manifest.py +0 -0
  152. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_phaet.py +0 -0
  153. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_pooled_encoder_input.py +0 -0
  154. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_pooled_geometry.py +0 -0
  155. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_prism2.py +0 -0
  156. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_progress.py +0 -0
  157. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_regression_core.py +0 -0
  158. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_rudolfv2.py +0 -0
  159. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_runtime_batching.py +0 -0
  160. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_sharding.py +0 -0
  161. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_slide_coordinate_preparation.py +0 -0
  162. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_tile_store.py +0 -0
  163. {slide2vec-5.8.1 → slide2vec-5.9.0}/tests/test_tiling_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 5.8.1
3
+ Version: 5.9.0
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2
18
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3
19
19
  Requires-Dist: omegaconf
20
20
  Requires-Dist: matplotlib
21
21
  Requires-Dist: numpy<2
@@ -75,7 +75,7 @@ Requires-Dist: numpy<2; extra == "fm"
75
75
  Requires-Dist: pandas; extra == "fm"
76
76
  Requires-Dist: pillow; extra == "fm"
77
77
  Requires-Dist: rich; extra == "fm"
78
- Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2; extra == "fm"
78
+ Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3; extra == "fm"
79
79
  Requires-Dist: wandb; extra == "fm"
80
80
  Requires-Dist: torch<2.8,>=2.3; extra == "fm"
81
81
  Requires-Dist: torchvision>=0.18.0; extra == "fm"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "5.8.1"
7
+ version = "5.9.0"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.13",
22
22
  ]
23
23
  dependencies = [
24
- "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
24
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3",
25
25
  "omegaconf",
26
26
  "matplotlib",
27
27
  "numpy<2",
@@ -100,7 +100,7 @@ fm = [
100
100
  "pandas",
101
101
  "pillow",
102
102
  "rich",
103
- "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
103
+ "hs2p[asap,cucim,openslide,sam2,vips]>=4.4.3",
104
104
  "wandb",
105
105
  "torch>=2.3,<2.8",
106
106
  "torchvision>=0.18.0",
@@ -158,7 +158,7 @@ testpaths = [
158
158
  "tests",
159
159
  ]
160
160
  markers = [
161
- "heavy: real-weight foundation-model inference on CPU; minutes per test. Excluded from the PR suite via `-m 'not heavy'`; run on the scheduled/manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
161
+ "heavy: real-weight foundation-model inference on CPU; minutes per test. Included in the PR suite and available separately through the manual heavy workflow (.github/workflows/nightly-heavy.yaml).",
162
162
  "gpu_integration: real one-GPU versus multi-GPU parity; requires at least two visible CUDA devices. Run explicitly with `CUDA_VISIBLE_DEVICES=0,1 python -m pytest -m gpu_integration --no-cov`.",
163
163
  ]
164
164
 
@@ -180,14 +180,15 @@ no_implicit_reexport = true
180
180
  max-line-length = 160
181
181
 
182
182
  [tool.bumpver]
183
- current_version = "5.8.1"
183
+ current_version = "5.9.0"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
- commit = false # We do version bumping in CI, not as a commit
186
- tag = false # Git tag already exists — we don't auto-tag
187
- push = false # Don't push anything in CI
185
+ commit = false
186
+ tag = false
187
+ push = false
188
188
  files = [
189
189
  "pyproject.toml",
190
- "slide2vec/__init__.py"
190
+ "slide2vec/__init__.py",
191
+ "docs/conf.py",
191
192
  ]
192
193
 
193
194
  [tool.bumpver.file_patterns]
@@ -198,3 +199,6 @@ files = [
198
199
  "slide2vec/__init__.py" = [
199
200
  '^__version__ = "{version}"$',
200
201
  ]
202
+ "docs/conf.py" = [
203
+ '^release = "{version}"$',
204
+ ]
@@ -27,7 +27,7 @@ from slide2vec.encoders import (
27
27
  )
28
28
 
29
29
 
30
- __version__ = "5.8.1"
30
+ __version__ = "5.9.0"
31
31
 
32
32
  __all__ = [
33
33
  "Model",
@@ -7,7 +7,7 @@ import warnings
7
7
  from dataclasses import dataclass, field, replace
8
8
  from contextlib import contextmanager
9
9
  from pathlib import Path
10
- from typing import Any, Mapping, Protocol, Sequence
10
+ from typing import Any, Callable, Mapping, Protocol, Sequence
11
11
 
12
12
  import torch
13
13
  from hs2p import SlideSpec
@@ -128,15 +128,12 @@ def resolve_masks(masks: Mapping[str, Any] | None) -> dict[str, Any]:
128
128
 
129
129
  def _masks_to_plain_dict(node: Any) -> dict[str, Any]:
130
130
  """Normalize a masks config node (OmegaConf, mapping, or namespace) to a plain dict."""
131
+ from omegaconf import OmegaConf
132
+
131
133
  if node is None:
132
134
  return {}
133
- try:
134
- from omegaconf import OmegaConf
135
-
136
- if OmegaConf.is_config(node):
137
- return copy.deepcopy(OmegaConf.to_container(node, resolve=True)) # type: ignore[return-value]
138
- except ImportError:
139
- pass
135
+ if OmegaConf.is_config(node):
136
+ return copy.deepcopy(OmegaConf.to_container(node, resolve=True)) # type: ignore[return-value]
140
137
  if isinstance(node, Mapping):
141
138
  return copy.deepcopy(dict(node))
142
139
  return copy.deepcopy(dict(vars(node)))
@@ -243,7 +240,6 @@ class PreprocessingConfig:
243
240
  "preview",
244
241
  _deep_merge_dicts(DEFAULT_PREPROCESSING["preview"], self.preview),
245
242
  )
246
- # Complete a (possibly partial) masks mapping against the shipped default.
247
243
  object.__setattr__(self, "masks", resolve_masks(self.masks))
248
244
 
249
245
  @classmethod
@@ -710,6 +706,7 @@ class Model:
710
706
  *,
711
707
  preprocessing: PreprocessingConfig | None = None,
712
708
  execution: ExecutionOptions | None = None,
709
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
713
710
  ) -> list[TileEmbeddingArtifact] | list[HierarchicalEmbeddingArtifact]:
714
711
  from slide2vec.inference import embed_tiles
715
712
 
@@ -724,6 +721,7 @@ class Model:
724
721
  tiling_results,
725
722
  execution=resolved,
726
723
  preprocessing=resolved_preprocessing,
724
+ on_slide_persisted=on_slide_persisted,
727
725
  )
728
726
 
729
727
  def aggregate_tiles(
@@ -1217,6 +1215,7 @@ class Pipeline:
1217
1215
  coordinates_dir: str | Path,
1218
1216
  *,
1219
1217
  slides: SlideSequence | None = None,
1218
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
1220
1219
  ) -> RunResult:
1221
1220
  from slide2vec.inference import run_pipeline_with_coordinates
1222
1221
 
@@ -1229,6 +1228,7 @@ class Pipeline:
1229
1228
  slides=slides,
1230
1229
  preprocessing=resolved_preprocessing,
1231
1230
  execution=self.execution,
1231
+ on_slide_persisted=on_slide_persisted,
1232
1232
  )
1233
1233
 
1234
1234
 
@@ -52,7 +52,6 @@ def _open_wsi_backend(image_path: str, backend: str, gpu_decode: bool):
52
52
  return VIPSReader(image_path)
53
53
  elif backend == "asap":
54
54
  from hs2p.wsi.backends.asap import ASAPReader
55
- from slide2vec.utils.log_utils import suppress_c_stderr
56
55
  with suppress_c_stderr():
57
56
  return ASAPReader(image_path)
58
57
  else:
@@ -133,7 +133,6 @@ class _TorchDistributedEnvironment:
133
133
 
134
134
  # Single node job with preset environment (i.e. torchrun)
135
135
  def _set_from_preset_env(self):
136
- # logger.info("Initialization from preset environment")
137
136
  self.rank = int(os.environ["RANK"])
138
137
  self.world_size = int(os.environ["WORLD_SIZE"])
139
138
  assert self.rank < self.world_size
@@ -143,7 +142,6 @@ class _TorchDistributedEnvironment:
143
142
 
144
143
  # Single node and GPU job (i.e. local script run)
145
144
  def _set_from_local(self):
146
- # logger.info("Initialization from local")
147
145
  self.rank = 0
148
146
  self.world_size = 1
149
147
  self.local_rank = 0
@@ -196,7 +194,6 @@ def enable(
196
194
  if set_cuda_current_device:
197
195
  torch.cuda.set_device(torch_env.local_rank)
198
196
 
199
- # Finalize setup
200
197
  _RANK = torch_env.rank
201
198
  _WORLD_SIZE = torch_env.world_size
202
199
  _LOCAL_RANK = torch_env.local_rank
@@ -48,13 +48,8 @@ class GigaPath(TimmTileEncoder):
48
48
  )
49
49
 
50
50
  def get_transform(self) -> Callable:
51
- # POOLED transform only: center-crops the 256px tile to the model's 224px
52
- # native input (paper recipe, center 224 @ native 0.5 mpp). Dense extraction
53
- # must NOT route through this — it needs the full uncropped tile so the grid
54
- # covers the whole source tile. The dense path supplies its own no-crop
55
- # transform (Resize(256), no CenterCrop) → a 16x16 grid over the full tile;
56
- # encode_tiles_dense itself is transform-agnostic (inherited from
57
- # TimmTileEncoder) and operates on whatever batch the dense pipeline feeds.
51
+ # Pooled recipe: center 224px at native spacing. Dense extraction uses
52
+ # get_normalization_transform() to preserve the caller's tile geometry.
58
53
  return v2.Compose([
59
54
  v2.ToImage(),
60
55
  v2.Resize(256, interpolation=v2.InterpolationMode.BICUBIC, antialias=True),
@@ -123,6 +118,9 @@ class GigaPathSlideEncoder(SlideEncoder):
123
118
  tile_features = tile_features.unsqueeze(0)
124
119
  if coordinates.ndim == 2:
125
120
  coordinates = coordinates.unsqueeze(0)
121
+ # prov-gigapath's reference usage feeds fp32 tile embeddings under autocast;
122
+ # fp16 input risks dtype mismatches in ops autocast does not cover.
123
+ tile_features = tile_features.float()
126
124
  # gigapath_slide_enc12l768d.forward always returns a list of per-layer
127
125
  # embeddings (a single element when all_layer_embed is False); the final
128
126
  # slide embedding is the last layer, matching prov-gigapath's own usage.
@@ -50,7 +50,7 @@ class MOOZYSlideEncoder(nn.Module):
50
50
  dropout=dropout,
51
51
  attn_dropout=attn_dropout,
52
52
  alibi=self.pos_bias,
53
- drop_path_rate=dpr[i] if i < len(dpr) else 0.0,
53
+ drop_path_rate=dpr[i],
54
54
  qk_norm=qk_norm,
55
55
  layerscale_init=layerscale_init,
56
56
  )
@@ -109,26 +109,18 @@ class MOOZYSlideEncoder(nn.Module):
109
109
  x = torch.cat(tokens, dim=1)
110
110
 
111
111
  coords_xy = coords_xy.to(device=x.device, dtype=torch.float32).reshape(bsz, n_tokens, 2)
112
- zeros_cls = torch.zeros(bsz, 1, 2, dtype=coords_xy.dtype, device=coords_xy.device)
113
- if self.num_registers > 0:
114
- zeros_reg = torch.zeros(bsz, self.num_registers, 2, dtype=coords_xy.dtype, device=coords_xy.device)
115
- positions = torch.cat([zeros_cls, zeros_reg, coords_xy], dim=1)
116
- else:
117
- positions = torch.cat([zeros_cls, coords_xy], dim=1)
118
-
119
- valid_flat = ~invalid_flat
120
- reg_valid = (
121
- torch.ones(bsz, self.num_registers, dtype=torch.bool, device=x.device)
122
- if self.num_registers > 0
123
- else torch.zeros(bsz, 0, dtype=torch.bool, device=x.device)
112
+ num_prefix = 1 + self.num_registers
113
+ prefix_positions = torch.zeros(
114
+ bsz, num_prefix, 2, dtype=coords_xy.dtype, device=coords_xy.device
124
115
  )
125
- valid_with_cls = torch.cat([torch.ones(bsz, 1, dtype=torch.bool, device=x.device), reg_valid, valid_flat], dim=1)
116
+ positions = torch.cat([prefix_positions, coords_xy], dim=1)
117
+ prefix_valid = torch.ones(bsz, num_prefix, dtype=torch.bool, device=x.device)
118
+ valid_with_cls = torch.cat([prefix_valid, ~invalid_flat], dim=1)
126
119
 
127
120
  if mask is None and bsz == 1:
128
121
  keep = valid_with_cls[0]
129
- if keep.sum() > 0:
130
- x = x[:, keep, :]
131
- positions = positions[:, keep, :]
122
+ x = x[:, keep, :]
123
+ positions = positions[:, keep, :]
132
124
  attn_mask = None
133
125
  else:
134
126
  pair_valid = valid_with_cls.unsqueeze(2) & valid_with_cls.unsqueeze(1)
@@ -0,0 +1,180 @@
1
+ """TITAN slide encoder implementation."""
2
+
3
+ import math
4
+ import sys
5
+
6
+ import numpy as np
7
+ import torch
8
+ from transformers import AutoModel
9
+
10
+ from slide2vec.encoders.base import SlideEncoder, preferred_default_device, resolve_requested_output_variant
11
+ from slide2vec.encoders.registry import register_encoder
12
+
13
+ # Pinned so the remote-code patches below stay valid (and so runs are reproducible —
14
+ # an unpinned from_pretrained re-downloads whatever is at the repo HEAD).
15
+ _TITAN_REVISION = "dac6773d9961cfc75503440676ff157a2c6e8d2e"
16
+
17
+
18
+ def _alibi_slopes(n: int) -> list[float]:
19
+ # verbatim ALiBi slope schedule from TITAN's get_alibi at the pinned revision
20
+ if math.log2(n).is_integer():
21
+ p = 2 ** (-(2 ** -(math.log2(n) - 3)))
22
+ return [p * (p ** i) for i in range(n)]
23
+ nearest = 2 ** math.floor(math.log2(n))
24
+ base = _alibi_slopes(nearest)
25
+ extra = _alibi_slopes(2 * nearest)[0::2][: n - nearest]
26
+ return base + extra
27
+
28
+
29
+ def _lean_alibi_bias(module, w, h, bg_mask, device, dtype):
30
+ """Bitwise-identical replacement for TITAN's get_alibi + the caller's cast/move.
31
+
32
+ The reference builds the [1, heads, N, N] bias through N x N float64 numpy on
33
+ the CPU (>100 GB of host RAM for slides in the tens of thousands of tiles) and
34
+ hands SDPA an unaligned bias that forces the math kernel, which materializes a
35
+ second N^2 tensor on the GPU. This builds the same values on-device in fp16, in
36
+ row chunks, with rows padded to a multiple of 8 elements so the memory-efficient
37
+ SDPA kernel accepts the bias — peak memory drops from ~4x to ~1x the bias size.
38
+ """
39
+ ii, jj = torch.meshgrid(
40
+ torch.arange(w, device=device), torch.arange(h, device=device), indexing="ij"
41
+ )
42
+ if bg_mask is not None:
43
+ mask = bg_mask.to(device).squeeze(0)
44
+ ii, jj = ii[mask], jj[mask]
45
+ points = torch.stack([ii.reshape(-1), jj.reshape(-1)], dim=1).float()
46
+ n = points.shape[0]
47
+ length = n + 1 # +1 for the cls token; its bias row/col stays zero
48
+ padded = ((length + 7) // 8) * 8
49
+ slopes = torch.tensor(
50
+ _alibi_slopes(module.num_heads), device=device, dtype=torch.float32
51
+ ).view(module.num_heads, 1, 1)
52
+ bias = torch.zeros(1, module.num_heads, length, padded, device=device, dtype=dtype)
53
+ # chunk intermediate is (heads, step, n) fp32 — stays under ~1.5 GB even at 53k tiles
54
+ step = 512
55
+ for start in range(0, n, step):
56
+ diff = points[start : start + step].unsqueeze(1) - points.unsqueeze(0)
57
+ dist = diff.square().sum(-1).sqrt()
58
+ bias[0, :, 1 + start : 1 + start + dist.shape[0], 1:length] = (
59
+ dist.unsqueeze(0) * slopes * -1
60
+ ).to(dtype)
61
+ return bias[..., :length]
62
+
63
+
64
+ def _patch_titan_remote_code(model) -> bool:
65
+ """Patch TITAN's remote code for fp16 input and bounded bias memory.
66
+
67
+ Two patches on the dynamically loaded module (it exists only after
68
+ from_pretrained): preprocess_features runs its grid index_add_ in fp32 (the op
69
+ rejects fp16 features) but returns the grid in the features' own dtype — an
70
+ exact roundtrip that keeps the whole forward on the fp16 path — and
71
+ forward_features' single-slide alibi branch swaps in _lean_alibi_bias.
72
+ Returns False without patching if the module does not look like the pinned
73
+ revision; the caller then falls back to fp32 features (correct, but with the
74
+ reference implementation's memory behavior).
75
+ """
76
+ try:
77
+ vision_encoder = model.vision_encoder
78
+ vit = sys.modules[type(vision_encoder).__module__]
79
+ if getattr(vit, "_slide2vec_titan_patched", False):
80
+ return True
81
+ for attr in ("pos_encode_type", "num_heads", "patch_embed", "_pos_embed", "norm_pre", "blocks", "norm"):
82
+ if not hasattr(vision_encoder, attr):
83
+ return False
84
+ if not callable(getattr(vit, "preprocess_features", None)):
85
+ return False
86
+ except Exception:
87
+ return False
88
+
89
+ orig_preprocess = vit.preprocess_features
90
+
91
+ def preprocess_features(features, coords, patch_size_lv0):
92
+ grid, coords_grid, bg_mask = orig_preprocess(features.float(), coords, patch_size_lv0)
93
+ return grid.to(features.dtype), coords_grid, bg_mask
94
+
95
+ orig_forward_features = type(vision_encoder).forward_features
96
+
97
+ def forward_features(self, x, coords=None, mask=None, bg_mask=None):
98
+ # single-slide alibi path only; anything else falls through to the original
99
+ if self.pos_encode_type != "alibi" or x.shape[0] != 1 or self.masked_im_modeling:
100
+ return orig_forward_features(self, x, coords=coords, mask=mask, bg_mask=bg_mask)
101
+ B, nc, w, h = x.shape
102
+ # bias dtype = input grid dtype, as in the original, which builds the bias
103
+ # before patch_embed; post-norm activations can be fp32 under autocast
104
+ in_dtype = x.dtype
105
+ x = x.flatten(2, 3).transpose(1, 2)
106
+ x = self.patch_embed(x)
107
+ x = self._pos_embed(x, coords, w, h)
108
+ x = self.norm_pre(x)
109
+ if bg_mask is not None:
110
+ keep = torch.cat(
111
+ (torch.ones((1, 1), dtype=torch.bool, device=x.device), bg_mask.view(1, -1)),
112
+ dim=1,
113
+ )
114
+ x = x[keep].unsqueeze(0)
115
+ attn_bias = _lean_alibi_bias(self, w, h, bg_mask, device=x.device, dtype=in_dtype)
116
+ x = self.blocks(x, attn_bias, bg_mask)
117
+ x = self.norm(x)
118
+ return x
119
+
120
+ vit.preprocess_features = preprocess_features
121
+ type(vision_encoder).forward_features = forward_features
122
+ vit._slide2vec_titan_patched = True
123
+ return True
124
+
125
+
126
+ @register_encoder(
127
+ "titan",
128
+ level="slide",
129
+ tile_encoder="conchv15",
130
+ tile_encoder_output_variant="default",
131
+ output_variants={"default": {"encode_dim": 768}},
132
+ default_output_variant="default",
133
+ supported_spacing_um=0.5,
134
+ precision="fp16",
135
+ source="MahmoodLab/TITAN",
136
+ )
137
+ class TitanSlideEncoder(SlideEncoder):
138
+ def __init__(self, *, output_variant: str | None = None):
139
+ self._model = AutoModel.from_pretrained(
140
+ "MahmoodLab/TITAN", revision=_TITAN_REVISION, trust_remote_code=True
141
+ ).eval()
142
+ self._remote_code_patched = _patch_titan_remote_code(self._model)
143
+ self._device = preferred_default_device()
144
+ self._output_variant = resolve_requested_output_variant(output_variant)
145
+
146
+ @property
147
+ def encode_dim(self) -> int:
148
+ return 768
149
+
150
+ @property
151
+ def device(self) -> torch.device:
152
+ return self._device
153
+
154
+ def to(self, device: torch.device | str) -> "TitanSlideEncoder":
155
+ self._device = torch.device(device)
156
+ self._model = self._model.to(self._device)
157
+ return self
158
+
159
+ def encode_slide(
160
+ self,
161
+ tile_features: torch.Tensor,
162
+ coordinates: torch.Tensor | None = None,
163
+ *,
164
+ tile_size_lv0: int | None = None,
165
+ ) -> torch.Tensor:
166
+ if coordinates is None or tile_size_lv0 is None:
167
+ raise ValueError("TITAN slide encoding requires coordinates and tile_size_lv0")
168
+ if tile_features.ndim == 2:
169
+ tile_features = tile_features.unsqueeze(0)
170
+ if coordinates.ndim == 2:
171
+ coordinates = coordinates.unsqueeze(0)
172
+ if not self._remote_code_patched:
173
+ # fallback for unrecognized remote code: fp32 features satisfy its fp32
174
+ # grid index_add_, at the cost of the reference memory behavior
175
+ tile_features = tile_features.float()
176
+ return self._model.encode_slide_from_patch_features(
177
+ tile_features,
178
+ coordinates.long(),
179
+ np.int64(tile_size_lv0),
180
+ ).squeeze(0)
@@ -1,26 +1,14 @@
1
- import json
2
- import importlib
3
1
  import os
4
- import tempfile
5
- import threading
6
- import time
7
- from contextlib import contextmanager, nullcontext
8
- from dataclasses import replace
9
2
  from pathlib import Path
10
- from types import SimpleNamespace
11
3
  from typing import Any, Callable, Sequence
12
4
 
13
- import logging
14
- import pandas as pd
15
5
  import torch
16
- from hs2p import SlideSpec, tile_slides
17
- from hs2p.utils.stderr import run_with_filtered_stderr
6
+ from hs2p import SlideSpec
18
7
 
19
8
  from slide2vec.runtime import (
20
9
  artifacts_collect,
21
10
  batching,
22
11
  cpu_budget,
23
- distributed,
24
12
  distributed_stage,
25
13
  embedding,
26
14
  embedding_persist,
@@ -31,11 +19,9 @@ from slide2vec.runtime import (
31
19
  persist_callbacks,
32
20
  persistence,
33
21
  process_list,
34
- serialization,
35
22
  slide_encode,
36
23
  tiling,
37
24
  tiling_pipeline,
38
- worker_io,
39
25
  )
40
26
  from slide2vec.api import (
41
27
  EmbeddedPatient,
@@ -43,43 +29,21 @@ from slide2vec.api import (
43
29
  ExecutionOptions,
44
30
  PreprocessingConfig,
45
31
  RunResult,
46
- _resolve_hierarchical_preprocessing,
47
32
  )
48
33
  from slide2vec.artifacts import (
49
34
  HierarchicalEmbeddingArtifact,
50
- PatientEmbeddingArtifact,
51
35
  SlideEmbeddingArtifact,
52
36
  TileEmbeddingArtifact,
53
- write_hierarchical_embeddings,
54
37
  load_array,
55
- write_patient_embeddings,
56
- write_tile_embedding_metadata,
57
38
  )
58
39
  from slide2vec.encoders.registry import (
59
40
  encoder_registry,
60
- resolve_encoder_output,
61
41
  resolve_patch_size,
62
- resolve_preprocessing_defaults,
63
42
  )
64
43
  from slide2vec.runtime.model_settings import canonicalize_model_name
65
44
  from slide2vec.runtime.types import LoadedModel
66
45
  from slide2vec.runtime.encoder_input_contract import EncoderInputContract
67
- from slide2vec.progress import (
68
- emit_progress,
69
- read_tiling_progress_snapshot,
70
- )
71
- from slide2vec.utils.log_utils import suppress_c_stderr
72
- from slide2vec.data.dataset import BatchTileCollator, TileIndexDataset
73
- from slide2vec.data.tile_reader import OnTheFlyBatchTileCollator, OnTheFlyHierarchicalBatchCollator
74
- from slide2vec.utils.tiling_io import (
75
- load_embedding_process_df,
76
- load_patient_id_mapping,
77
- load_slide_manifest,
78
- load_tiling_process_df,
79
- load_tiling_result_from_row,
80
- _optional_float,
81
- )
82
- from slide2vec.utils.utils import cpu_worker_limit, slurm_cpu_limit
46
+ from slide2vec.progress import emit_progress
83
47
 
84
48
  from slide2vec.runtime.hierarchical import num_embedding_items
85
49
 
@@ -126,13 +90,15 @@ def load_model(
126
90
  info = encoder_registry.info(name)
127
91
  resolved_level = info["level"]
128
92
 
129
- if token is None and "HF_TOKEN" in os.environ:
130
- token = os.environ["HF_TOKEN"]
131
-
93
+ # Never call ``huggingface_hub.login()`` here. Every encoder resolves its auth
94
+ # through the process-global ``huggingface_hub.get_token()``, which reads
95
+ # ``HF_TOKEN`` from the environment ahead of any stored token file, so an
96
+ # exported ``HF_TOKEN`` is all the hub needs. ``login()`` additionally rewrites a
97
+ # shared ``stored_tokens`` file without a lock; under distributed extraction every
98
+ # rank re-runs load_model per chunk and the concurrent truncate/re-read races,
99
+ # crashing runs with ``ValueError: Token ... not found``.
132
100
  if token is not None:
133
- from huggingface_hub import login as hf_login
134
-
135
- hf_login(token=token, add_to_git_credential=False)
101
+ os.environ["HF_TOKEN"] = token
136
102
 
137
103
  encoder_cls = encoder_registry.require(name)
138
104
  # Pass allow_non_recommended_settings ONLY to encoders whose constructor accepts it
@@ -625,6 +591,7 @@ def embed_tiles(
625
591
  *,
626
592
  execution: ExecutionOptions,
627
593
  preprocessing: PreprocessingConfig | None = None,
594
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
628
595
  ) -> list[TileEmbeddingArtifact] | list[HierarchicalEmbeddingArtifact]:
629
596
  if execution.output_dir is None:
630
597
  raise ValueError("ExecutionOptions.output_dir is required to persist tile embeddings")
@@ -694,6 +661,8 @@ def embed_tiles(
694
661
  annotation=embedding.tiling_result_annotation(tiling_result),
695
662
  )
696
663
  artifacts.append(artifact)
664
+ if on_slide_persisted is not None:
665
+ on_slide_persisted(artifact)
697
666
  return artifacts
698
667
 
699
668
 
@@ -714,11 +683,7 @@ def aggregate_tiles(
714
683
  outputs: list[SlideEmbeddingArtifact] = []
715
684
  for artifact in tile_artifacts:
716
685
  metadata = artifact.metadata
717
- if "coordinates_npz_path" not in metadata or "coordinates_meta_path" not in metadata:
718
- raise ValueError(
719
- f"Tile artifact for {artifact.sample_id} is missing tiling metadata paths required for slide aggregation"
720
- )
721
- if not metadata["coordinates_npz_path"] or not metadata["coordinates_meta_path"]:
686
+ if not metadata.get("coordinates_npz_path") or not metadata.get("coordinates_meta_path"):
722
687
  raise ValueError(
723
688
  f"Tile artifact for {artifact.sample_id} is missing tiling metadata paths required for slide aggregation"
724
689
  )
@@ -735,13 +700,12 @@ def aggregate_tiles(
735
700
  tiling_result,
736
701
  execution=execution,
737
702
  )
738
- latents = None
739
703
  slide_artifact = embedding.write_slide_embedding_artifact(
740
704
  artifact.sample_id,
741
705
  slide_embedding,
742
706
  execution=execution,
743
707
  metadata=embedding.build_slide_embedding_metadata(model, image_path=metadata["image_path"]),
744
- latents=latents,
708
+ latents=None,
745
709
  )
746
710
  outputs.append(slide_artifact)
747
711
  return outputs
@@ -921,9 +885,8 @@ def run_pipeline(
921
885
  execution=execution,
922
886
  process_list_path=process_list_path,
923
887
  )
924
- embedded_slides: list[EmbeddedSlide] = []
925
888
  if pending_slides:
926
- embedded_slides = embedding_pipeline.compute_embedded_slides(
889
+ embedding_pipeline.compute_embedded_slides(
927
890
  model,
928
891
  pending_slides,
929
892
  pending_tiling_results,
@@ -972,6 +935,7 @@ def run_pipeline_with_coordinates(
972
935
  slides=None,
973
936
  preprocessing: PreprocessingConfig | None = None,
974
937
  execution: ExecutionOptions,
938
+ on_slide_persisted: Callable[[TileEmbeddingArtifact | HierarchicalEmbeddingArtifact], None] | None = None,
975
939
  ) -> RunResult:
976
940
  if execution.output_dir is None:
977
941
  raise ValueError("ExecutionOptions.output_dir is required for Pipeline.run_with_coordinates(...)")
@@ -1031,6 +995,7 @@ def run_pipeline_with_coordinates(
1031
995
  execution=execution,
1032
996
  output_dir=output_dir,
1033
997
  tiling_input_dir=Path(coordinates_dir),
998
+ on_slide_persisted=on_slide_persisted,
1034
999
  )
1035
1000
  return RunResult(
1036
1001
  tile_artifacts=tile_artifacts,
@@ -1043,6 +1008,7 @@ def run_pipeline_with_coordinates(
1043
1008
  preprocessing=resolved_preprocessing,
1044
1009
  execution=execution,
1045
1010
  process_list_path=process_list_path,
1011
+ on_artifact=on_slide_persisted,
1046
1012
  )
1047
1013
  embedding_pipeline.compute_embedded_slides(
1048
1014
  model,