diff --git a/configs/experiment/preprocessing/embeddings_virchow2_05mpp.yaml b/configs/experiment/preprocessing/embeddings_virchow2_05mpp.yaml new file mode 100644 index 0000000..8a3e120 --- /dev/null +++ b/configs/experiment/preprocessing/embeddings_virchow2_05mpp.yaml @@ -0,0 +1,7 @@ +# @package _global_ + +defaults: + - /data: dataset + - _self_ + +model: virchow2 diff --git a/configs/experiment/preprocessing/tile_masks_05mpp.yaml b/configs/experiment/preprocessing/tile_masks_05mpp.yaml index 8cfc5a9..4fb3ab5 100644 --- a/configs/experiment/preprocessing/tile_masks_05mpp.yaml +++ b/configs/experiment/preprocessing/tile_masks_05mpp.yaml @@ -4,8 +4,6 @@ defaults: - /data: dataset - _self_ -slides_artifact_path: train_split/slides.parquet -tiles_artifact_path: train_split/tiles.parquet tile_percentage_cols: - tile_coverage_Nerve - tile_coverage_Blood diff --git a/configs/preprocessing/embeddings.yaml b/configs/preprocessing/embeddings.yaml new file mode 100644 index 0000000..db9ceaa --- /dev/null +++ b/configs/preprocessing/embeddings.yaml @@ -0,0 +1,15 @@ +# @package _global_ + +model: ??? +output_dir: ${project_path}/embeddings +concurrency: 512 +block_size: 2048 +rows_per_file: 5000 + +metadata: + run_name: "Embeddings: ${model}" + description: Tile embeddings using model ${model} + hyperparams: + model: ${model} + concurrency: ${concurrency} + block_size: ${block_size} diff --git a/configs/preprocessing/tile_masks.yaml b/configs/preprocessing/tile_masks.yaml index a287440..27fbd96 100644 --- a/configs/preprocessing/tile_masks.yaml +++ b/configs/preprocessing/tile_masks.yaml @@ -1,7 +1,5 @@ # @package _global_ -slides_artifact_path: ??? -tiles_artifact_path: ??? tile_percentage_cols: ??? max_concurrent: 1 diff --git a/preprocessing/embeddings.py b/preprocessing/embeddings.py new file mode 100644 index 0000000..de98143 --- /dev/null +++ b/preprocessing/embeddings.py @@ -0,0 +1,139 @@ +import asyncio +import shutil +import time +from pathlib import Path +from typing import Any + +import httpx +import hydra +import mlflow.artifacts +import pandas as pd +import pyarrow.dataset as pads +import ray +from omegaconf import DictConfig +from rationai import AsyncClient # type: ignore[attr-defined] +from rationai.mlkit import autolog, with_cli_args +from rationai.mlkit.lightning.loggers import MLFlowLogger +from ratiopath.tiling.read_slide_tiles import read_slide_tiles +from ray.data.expressions import col + + +class EmbedTiles: + def __init__(self, model: str, concurrency: int) -> None: + self.model = model + self.client = AsyncClient( + limits=httpx.Limits( + max_connections=concurrency, max_keepalive_connections=concurrency + ), + timeout=200, + ) + + async def __call__(self, row: dict[str, Any]) -> dict[str, Any]: + try: + last_exc: httpx.HTTPError | None = None + for attempt in range(6): + try: + embedding = ( + (await self.client.models.embed_image(self.model, row["tile"])) + .reshape(-1) + .tolist() + ) + break + except httpx.HTTPError as exc: + last_exc = exc + if attempt < 5: + await asyncio.sleep(min(60, 2**attempt)) + else: + raise RuntimeError("embed_image failed after 6 attempts") from last_exc + finally: + del row["tile"] + row["embedding"] = embedding + return row + + +@with_cli_args(["+preprocessing=embeddings"]) +@hydra.main(config_path="../configs", config_name="preprocessing", version_base=None) +@autolog +def main(config: DictConfig, logger: MLFlowLogger) -> None: + for name in ["train", "test"]: + split_folder = Path( + mlflow.artifacts.download_artifacts( + run_id=config.dataset.mlflow_artifacts.tiling_run_id, + artifact_path=f"{name}_split", + ) + ) + slides = pd.read_parquet(split_folder / "slides.parquet") + slide_info = slides.set_index("id")[ + ["path", "level", "tile_extent_x", "tile_extent_y"] + ].to_dict("index") + + tiles_path = Path( + mlflow.artifacts.download_artifacts( + run_id=config.dataset.mlflow_artifacts.filter_tiles_run_id, + artifact_path=f"filter_tiles/{name}_tiles.parquet", + ) + ) + num_rows = pads.dataset(str(tiles_path), format="parquet").count_rows() + num_blocks = max(1, num_rows // config.block_size) + + ds = ray.data.read_parquet( + str(tiles_path), + columns=["slide_id", "x", "y"], + ray_remote_args={"memory": 8 * 1024**3}, + override_num_blocks=num_blocks, + ).map( + lambda row, si: {**row, **si[row["slide_id"]]}, + fn_kwargs={"si": slide_info}, + ) + ds = ds.with_column( + "tile", + read_slide_tiles( # pyright: ignore[reportCallIssue] + col("path"), + col("x"), + col("y"), + col("tile_extent_x"), + col("tile_extent_y"), + col("level"), + ), + num_cpus=1, + memory=4 * 1024**3, + ) + ds = ds.drop_columns(["path", "level", "tile_extent_x", "tile_extent_y"]) + ds = ds.map( + EmbedTiles, # pyright: ignore[reportArgumentType] + fn_constructor_args=(config.model, config.concurrency), + compute=ray.data.ActorPoolStrategy( + min_size=4, + max_size=4, + max_tasks_in_flight_per_actor=max(1, config.concurrency // 4), + ), + max_concurrency=config.concurrency, + ) + + split_dir = Path(config.output_dir) / str(name) + split_dir.mkdir(parents=True, exist_ok=True) + tiles_parquet_dir = split_dir / "tiles" + if tiles_parquet_dir.exists(): + shutil.rmtree(tiles_parquet_dir) + + slides.to_parquet(split_dir / "slides.parquet", index=False) + + t = time.monotonic() + print(f"[main] starting write_parquet for split={name}") + ds.write_parquet(str(tiles_parquet_dir), min_rows_per_file=config.rows_per_file) + print(f"[main] write_parquet finished in {time.monotonic() - t:.1f}s") + + logger.log_artifacts(str(split_dir), str(name)) + + +if __name__ == "__main__": + ctx = ray.data.DataContext.get_current() + ctx.enable_rich_progress_bars = False + ctx.use_ray_tqdm = True + ctx.target_max_block_size = 64 * 1024 * 1024 + + with ray.init( + runtime_env={"excludes": [".git", ".venv"]}, + object_store_memory=16 * 1024**3, + ): + main() diff --git a/preprocessing/tile_masks.py b/preprocessing/tile_masks.py index 9738f3d..721a1cd 100644 --- a/preprocessing/tile_masks.py +++ b/preprocessing/tile_masks.py @@ -74,34 +74,36 @@ def process_slide( @autolog def main(config: DictConfig, logger: MLFlowLogger) -> None: tiling_run_id = config.dataset.mlflow_artifacts.tiling_run_id + filter_tiles_run_id = config.dataset.mlflow_artifacts.filter_tiles_run_id tile_percentage_cols: list[str] = list(config.tile_percentage_cols) - slides_path = mlflow.artifacts.download_artifacts( - run_id=tiling_run_id, - artifact_path=config.slides_artifact_path, - ) - tiles_path = mlflow.artifacts.download_artifacts( - run_id=tiling_run_id, - artifact_path=config.tiles_artifact_path, - ) - slides = pd.read_parquet(slides_path) + for split_name in ("train", "test"): + slides_path = mlflow.artifacts.download_artifacts( + run_id=tiling_run_id, + artifact_path=f"{split_name}_split/slides.parquet", + ) + tiles_path = mlflow.artifacts.download_artifacts( + run_id=filter_tiles_run_id, + artifact_path=f"filter_tiles/{split_name}_tiles.parquet", + ) + slides = pd.read_parquet(slides_path) + items = cast("list[dict[str, Any]]", slides.to_dict(orient="records")) - items = cast("list[dict[str, Any]]", slides.to_dict(orient="records")) + with TemporaryDirectory() as output_dir: + Path(output_dir, "outlines").mkdir() + for slide in tqdm(items, desc=split_name): + process_slide( + slide, + output_dir=output_dir, + tile_percentage_cols=tile_percentage_cols, + tiles_path=tiles_path, + ) - with TemporaryDirectory() as output_dir: - Path(output_dir, "outlines").mkdir() - for slide in tqdm(items): - process_slide( - slide, - output_dir=output_dir, - tile_percentage_cols=tile_percentage_cols, - tiles_path=tiles_path, + logger.log_artifacts( + local_dir=output_dir, + artifact_path=f"{config.mlflow_artifact_path}/{split_name}_split", ) - logger.log_artifacts( - local_dir=output_dir, artifact_path=config.mlflow_artifact_path - ) - if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index 71f8309..183bdd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,10 @@ dependencies = [ "numpy>=2.3.5", "rationai-tiling>=1.1.1", "tifffile>=2025.12.20", + "torch>=2.0.0", + "torchvision>=0.15.0", + "timm>=1.0.0", + "einops>=0.8.0", "matplotlib>=3.10.7", "pyarrow>=19.0.1", "datasets>=4.0.0", @@ -40,7 +44,7 @@ dev = [ [tool.uv.sources] rationai-mlkit = { git = "https://gitlab.ics.muni.cz/rationai/digital-pathology/libraries/mlkit.git" } -rationai-sdk = { git = "https://gitlab.ics.muni.cz/rationai/infrastructure/rationai-sdk-python.git" } +rationai-sdk = { git = "https://github.com/RationAI/rationai-sdk-python.git" } rationai-tiling = { git = "https://gitlab.ics.muni.cz/rationai/digital-pathology/libraries/tiling.git" } [tool.uv] diff --git a/scripts/submit_embeddings.py b/scripts/submit_embeddings.py new file mode 100644 index 0000000..d2f287d --- /dev/null +++ b/scripts/submit_embeddings.py @@ -0,0 +1,18 @@ +from kube_jobs import storage, submit_job + + +submit_job( + job_name="tissue-classification-embeddings", + username=..., + public=False, + cpu=8, + memory="64Gi", + shm="24Gi", + script=[ + "git clone https://github.com/RationAI/tissue-classification.git workdir", + "cd workdir", + "uv sync", + "uv run -m preprocessing.embeddings +experiment=...", + ], + storage=[storage.secure.DATA, storage.secure.PROJECTS], +) diff --git a/uv.lock b/uv.lock index d0c7571..30b783b 100644 --- a/uv.lock +++ b/uv.lock @@ -388,6 +388,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, ] +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + [[package]] name = "fastapi" version = "0.121.2" @@ -1792,7 +1801,7 @@ dependencies = [ [[package]] name = "rationai-sdk" version = "0.1.0" -source = { git = "https://gitlab.ics.muni.cz/rationai/infrastructure/rationai-sdk-python.git#a4d25084850cd26678783485dda87bbeed949492" } +source = { git = "https://github.com/RationAI/rationai-sdk-python.git#3fb74c0f867432422dacde29ec5c0a4584af4830" } dependencies = [ { name = "httpx" }, { name = "lz4" }, @@ -1975,6 +1984,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, ] +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + [[package]] name = "scikit-image" version = "0.26.0" @@ -2237,12 +2268,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/fe/e59859aa1134fac065d36864752daf13215c98b379cb5d93f954dc0ec830/tifffile-2025.12.20-py3-none-any.whl", hash = "sha256:bc0345a20675149353cfcb3f1c48d0a3654231ee26bd46beebaab4d2168feeb6", size = 232031, upload-time = "2025-12-21T06:23:53.003Z" }, ] +[[package]] +name = "timm" +version = "1.0.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/1e/e924b3b2326a856aaf68586f9c52a5fc81ef45715eca408393b68c597e0e/timm-1.0.26.tar.gz", hash = "sha256:f66f082f2f381cf68431c22714c8b70f723837fa2a185b155961eab90f2d5b10", size = 2419859, upload-time = "2026-03-23T18:12:10.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/e9/bebf3d50e3fc847378988235f87c37ad3ac26d386041ab915d15e92025cd/timm-1.0.26-py3-none-any.whl", hash = "sha256:985c330de5ccc3a2aa0224eb7272e6a336084702390bb7e3801f3c91603d3683", size = 2568766, upload-time = "2026-03-23T18:12:08.062Z" }, +] + [[package]] name = "tissue-classification" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "datasets" }, + { name = "einops" }, { name = "hydra-core" }, { name = "matplotlib" }, { name = "mlflow" }, @@ -2259,6 +2307,9 @@ dependencies = [ { name = "ray" }, { name = "scikit-learn" }, { name = "tifffile" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, { name = "tqdm" }, ] @@ -2274,6 +2325,7 @@ dev = [ requires-dist = [ { name = "datasets", specifier = ">=3.0.0" }, { name = "datasets", specifier = ">=4.0.0" }, + { name = "einops", specifier = ">=0.8.0" }, { name = "hydra-core", specifier = ">=1.3.2" }, { name = "matplotlib", specifier = ">=3.10.7" }, { name = "mlflow", specifier = "<3.0.0" }, @@ -2285,12 +2337,15 @@ requires-dist = [ { name = "pyarrow", specifier = ">=19.0.1" }, { name = "rationai-masks" }, { name = "rationai-mlkit", git = "https://gitlab.ics.muni.cz/rationai/digital-pathology/libraries/mlkit.git" }, - { name = "rationai-sdk", git = "https://gitlab.ics.muni.cz/rationai/infrastructure/rationai-sdk-python.git" }, + { name = "rationai-sdk", git = "https://github.com/RationAI/rationai-sdk-python.git" }, { name = "rationai-tiling", git = "https://gitlab.ics.muni.cz/rationai/digital-pathology/libraries/tiling.git" }, { name = "ratiopath", specifier = ">=1.2.0" }, { name = "ray", specifier = ">=2.51.1" }, { name = "scikit-learn", specifier = ">=1.8.0" }, { name = "tifffile", specifier = ">=2025.12.20" }, + { name = "timm", specifier = ">=1.0.0" }, + { name = "torch", specifier = ">=2.0.0" }, + { name = "torchvision", specifier = ">=0.15.0" }, { name = "tqdm", specifier = ">=4.66.0" }, ] @@ -2353,6 +2408,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl", hash = "sha256:08382fd96b923e39e904c4d570f3d49e2cc71ccabd2a94e0f895d1f0dac86242", size = 983161, upload-time = "2025-09-03T14:00:51.921Z" }, ] +[[package]] +name = "torchvision" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/af/18e2c6b9538a045f60718a0c5a058908ccb24f88fde8e6f0fc12d5ff7bd3/torchvision-0.24.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193", size = 1891433, upload-time = "2025-11-12T15:25:03.232Z" }, + { url = "https://files.pythonhosted.org/packages/9d/43/600e5cfb0643d10d633124f5982d7abc2170dfd7ce985584ff16edab3e76/torchvision-0.24.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050", size = 2386737, upload-time = "2025-11-12T15:25:08.288Z" }, + { url = "https://files.pythonhosted.org/packages/93/b1/db2941526ecddd84884132e2742a55c9311296a6a38627f9e2627f5ac889/torchvision-0.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7", size = 8049868, upload-time = "2025-11-12T15:25:13.058Z" }, + { url = "https://files.pythonhosted.org/packages/69/98/16e583f59f86cd59949f59d52bfa8fc286f86341a229a9d15cbe7a694f0c/torchvision-0.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:4aa6cb806eb8541e92c9b313e96192c6b826e9eb0042720e2fa250d021079952", size = 4302006, upload-time = "2025-11-12T15:25:16.184Z" }, +] + [[package]] name = "tqdm" version = "4.67.1"