From 5411587a788e1b0f88db937d59c8491a518018c8 Mon Sep 17 00:00:00 2001 From: Jim Bohnslav Date: Mon, 19 Oct 2026 10:17:05 -0400 Subject: [PATCH 1/5] Publish individual Hugging Face models and pin CI checkpoints --- .github/workflows/gpu-tests.yml | 2 +- .github/workflows/main.yml | 2 +- README.md | 46 +- docs/getting_started.md | 19 +- docs/pretrained_models.md | 99 ++++ huggingface/README.md | 113 +++++ huggingface/RELEASING.md | 63 +++ huggingface/SHA256SUMS | 30 ++ huggingface/models.json | 178 +++++++ huggingface/models/deg-f/README.md | 108 ++++ huggingface/models/deg-f/SHA256SUMS | 4 + huggingface/models/deg-f/model_config.json | 18 + huggingface/models/deg-m/README.md | 108 ++++ huggingface/models/deg-m/SHA256SUMS | 4 + huggingface/models/deg-m/model_config.json | 18 + huggingface/models/deg-s/README.md | 109 ++++ huggingface/models/deg-s/SHA256SUMS | 4 + huggingface/models/deg-s/model_config.json | 19 + huggingface/models/motionnet/README.md | 101 ++++ huggingface/models/motionnet/SHA256SUMS | 4 + .../models/motionnet/model_config.json | 18 + huggingface/models/tinymotionnet/README.md | 101 ++++ huggingface/models/tinymotionnet/SHA256SUMS | 4 + .../models/tinymotionnet/model_config.json | 18 + huggingface/models/tinymotionnet3d/README.md | 101 ++++ huggingface/models/tinymotionnet3d/SHA256SUMS | 4 + .../models/tinymotionnet3d/model_config.json | 19 + modal_gpu_test.py | 13 +- pyproject.toml | 1 + setup_tests.py | 127 +++-- tests/test_pretrained_models.py | 81 +++ uv.lock | 471 +++++++++++++++--- 32 files changed, 1864 insertions(+), 143 deletions(-) create mode 100644 docs/pretrained_models.md create mode 100644 huggingface/README.md create mode 100644 huggingface/RELEASING.md create mode 100644 huggingface/SHA256SUMS create mode 100644 huggingface/models.json create mode 100644 huggingface/models/deg-f/README.md create mode 100644 huggingface/models/deg-f/SHA256SUMS create mode 100644 huggingface/models/deg-f/model_config.json create mode 100644 huggingface/models/deg-m/README.md create mode 100644 huggingface/models/deg-m/SHA256SUMS create mode 100644 huggingface/models/deg-m/model_config.json create mode 100644 huggingface/models/deg-s/README.md create mode 100644 huggingface/models/deg-s/SHA256SUMS create mode 100644 huggingface/models/deg-s/model_config.json create mode 100644 huggingface/models/motionnet/README.md create mode 100644 huggingface/models/motionnet/SHA256SUMS create mode 100644 huggingface/models/motionnet/model_config.json create mode 100644 huggingface/models/tinymotionnet/README.md create mode 100644 huggingface/models/tinymotionnet/SHA256SUMS create mode 100644 huggingface/models/tinymotionnet/model_config.json create mode 100644 huggingface/models/tinymotionnet3d/README.md create mode 100644 huggingface/models/tinymotionnet3d/SHA256SUMS create mode 100644 huggingface/models/tinymotionnet3d/model_config.json create mode 100644 tests/test_pretrained_models.py diff --git a/.github/workflows/gpu-tests.yml b/.github/workflows/gpu-tests.yml index 8a790bf..0608aff 100644 --- a/.github/workflows/gpu-tests.yml +++ b/.github/workflows/gpu-tests.yml @@ -20,7 +20,7 @@ jobs: with: python-version: "3.11" - - name: Run GPU tests on Modal + - name: Run GPU tests on Modal with pinned Hugging Face checkpoints env: # Required repository secrets: # MODAL_TOKEN_ID diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84d3243..350db9f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,7 +47,7 @@ jobs: - name: Install dependencies run: uv sync --locked --dev - - name: Setup test data + - name: Setup video fixtures and pinned Hugging Face checkpoints run: uv run python setup_tests.py - name: Run CPU tests diff --git a/README.md b/README.md index de9a9e5..b6c3c42 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,33 @@ We now support docker! Docker is a way to run `deepethogram` in completely repro with other system dependencies. [See docs/Docker for more information](docs/docker.md) ## Pretrained models -Rather than start from scratch, we will start with model weights pretrained on the Kinetics700 dataset. Go to -To download the pretrained weights, please use [this Google Drive link](https://drive.google.com/file/d/1ntIZVbOG1UAiFVlsAAuKEBEVCVevyets/view?usp=sharing). -Unzip the files in your `project/models` directory. Make sure that you don't add an extra directory when unzipping! The path should be -`your_project/models/pretrained_models/{models 1:6}`, not `your_project/models/pretrained_models/pretrained_models/{models1:6}`. + +Download individual models from the [DeepEthogram Hugging Face collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec). +Each model has its own weights, model card, configuration, license, and checksum manifest. + +| Model | Role | Typical use | +| --- | --- | --- | +| [TinyMotionNet](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet) | Optical-flow generator | DEG-f flow initialization | +| [MotionNet](https://huggingface.co/jbohnslav/deepethogram-motionnet) | Optical-flow generator | DEG-m flow initialization | +| [TinyMotionNet3D](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet3d) | Optical-flow generator | 3D optical flow | +| [DEG-f](https://huggingface.co/jbohnslav/deepethogram-deg-f) | Feature extractor | Fast behavior feature-extractor initialization | +| [DEG-m](https://huggingface.co/jbohnslav/deepethogram-deg-m) | Feature extractor | Medium behavior feature-extractor initialization | +| [DEG-s](https://huggingface.co/jbohnslav/deepethogram-deg-s) | Feature extractor | Slow behavior feature-extractor initialization | + +For DEG-f, install the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli) +and download its flow generator and feature extractor into your existing project's model directory: + +```bash +hf download jbohnslav/deepethogram-tinymotionnet --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200221_115158_TinyMotionNet +hf download jbohnslav/deepethogram-deg-f --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200415_125824_hidden_two_stream_kinetics_degf +``` + +Run `shasum -a 256 -c SHA256SUMS` inside each downloaded run directory to verify its files. +See [pretrained model setup](docs/pretrained_models.md) for DEG-m, individual downloads, and loading details. +These weights initialize training on your own data; they do not classify your behavior labels without fine-tuning. + +The [legacy bundle](https://huggingface.co/jbohnslav/deepethogram-pretrained) retains the original six-folder archive, +including the incomplete DEG-s feature extractor. It is available for historical compatibility. ## Licensing Copyright (c) 2020 - President and Fellows of Harvard College. All rights reserved. @@ -112,9 +135,18 @@ Some older ones might also be fine, like a `1080` or even `1070 Ti`/ `1070`. ## testing Test coverage is still low, but in the future we will be expanding our unit tests. -First, download a copy of [`testing_deepethogram_archive.zip`](https://drive.google.com/file/d/1IFz4ABXppVxyuhYik8j38k9-Fl9kYKHo/view?usp=sharing) -Make a directory in tests called `DATA`. Unzip this and move it to the `deepethogram/tests/DATA` -directory, so that the path is `deepethogram/tests/DATA/testing_deepethogram_archive/{DATA,models,project_config.yaml}`. +Prepare the video fixtures and the same Hugging Face checkpoints documented above: + +```bash +uv sync --locked --dev +uv run python setup_tests.py +``` + +Video fixtures still come from the existing test archive. All six pretrained models are downloaded +from the individual Hugging Face repos at the exact commits in [`huggingface/models.json`](huggingface/models.json) +and verified against checked-in checksums, even when the video fixtures already exist. +CPU CI and the Modal GPU test runner use this setup. The CPU suite checks checkpoint identity, +strict parameter loading, forward-pass outputs, and project discovery for all six releases. To run tests: ```bash diff --git a/docs/getting_started.md b/docs/getting_started.md index 68441b0..5eee8ee 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -53,10 +53,21 @@ and save them to disk. This might take a few moments (or minutes for very long v and inference. ## Download models -Rather than start from scratch, we will start with model weights pretrained on the Kinetics700 dataset. Go to -To download the pretrained weights, please use [this Google Drive link](https://drive.google.com/file/d/1ntIZVbOG1UAiFVlsAAuKEBEVCVevyets/view?usp=sharing). -Unzip the files in your `project/models` directory. The path should be -`your_project/models/pretrained/{models 1:6}`. + +Choose DEG-f or DEG-m from the [pretrained model setup guide](pretrained_models.md). +Each model is published separately on Hugging Face. For the DEG-f preset, download +[TinyMotionNet](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet) and +[DEG-f](https://huggingface.co/jbohnslav/deepethogram-deg-f) using the +[Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli): + +```bash +hf download jbohnslav/deepethogram-tinymotionnet --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200221_115158_TinyMotionNet +hf download jbohnslav/deepethogram-deg-f --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200415_125824_hidden_two_stream_kinetics_degf +``` + +Keep `checkpoint.pt` and `hyperparameters.yaml` at the root of each downloaded run folder. +Run `shasum -a 256 -c SHA256SUMS` inside each run folder to verify it. +DEG-m and DEG-s use their corresponding flow-generator/feature-extractor pairs; see the setup guide. ## Start training the flow generator These models (see paper) estimate local motion from video frames. They are trained in a self-supervised manner, so they diff --git a/docs/pretrained_models.md b/docs/pretrained_models.md new file mode 100644 index 0000000..fd90220 --- /dev/null +++ b/docs/pretrained_models.md @@ -0,0 +1,99 @@ +# Pretrained model setup + +The [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) groups six individual model repositories. +Download only the models your preset needs. Install DeepEthogram and the +[Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli) first. + +## Choose a preset + +| Preset | Optical-flow generator | Feature extractor | +| --- | --- | --- | +| `deg_f` | [TinyMotionNet](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet) | [DEG-f / ResNet18](https://huggingface.co/jbohnslav/deepethogram-deg-f) | +| `deg_m` | [MotionNet](https://huggingface.co/jbohnslav/deepethogram-motionnet) | [DEG-m / ResNet50](https://huggingface.co/jbohnslav/deepethogram-deg-m) | +| `deg_s` | [TinyMotionNet3D](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet3d) | [DEG-s / ResNet3D-34](https://huggingface.co/jbohnslav/deepethogram-deg-s) | + +For the standard DeepEthogram training workflow, download both entries in your preset's row. +Full DEG-f/DEG-m/DEG-s checkpoints include embedded flow weights and can also be loaded directly +without another download; the training workflow selects flow and feature-extractor initialization separately. +These models initialize training on your own labels; a trained behavior pipeline also requires +fine-tuning and a sequence model. + +## Individual downloads + +Each command creates a separate run folder under your existing project's `models/pretrained_models`. +The `v1.0` release tags pin the pretrained weights. + +### TinyMotionNet + +[Model card](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet) + +```bash +hf download jbohnslav/deepethogram-tinymotionnet --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200221_115158_TinyMotionNet +``` + +### MotionNet + +[Model card](https://huggingface.co/jbohnslav/deepethogram-motionnet) + +```bash +hf download jbohnslav/deepethogram-motionnet --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200310_174416_MotionNet_kinetics +``` + +### TinyMotionNet3D + +[Model card](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet3d) + +```bash +hf download jbohnslav/deepethogram-tinymotionnet3d --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200313_125331_TinyMotionNet3D_kinetics +``` + +### DEG-f + +[Model card](https://huggingface.co/jbohnslav/deepethogram-deg-f) + +```bash +hf download jbohnslav/deepethogram-deg-f --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200415_125824_hidden_two_stream_kinetics_degf +``` + +### DEG-m + +[Model card](https://huggingface.co/jbohnslav/deepethogram-deg-m) + +```bash +hf download jbohnslav/deepethogram-deg-m --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200410_142156_hidden_two_stream_kinetics_degm +``` + +### DEG-s + +[Model card](https://huggingface.co/jbohnslav/deepethogram-deg-s) + +```bash +hf download jbohnslav/deepethogram-deg-s --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200409_101808_hidden_two_stream_kinetics_degs +``` + +## Verify and use + +From each downloaded run folder: + +```bash +shasum -a 256 -c SHA256SUMS +``` + +DeepEthogram discovers each `checkpoint.pt` using the neighboring `hyperparameters.yaml`. +Keep the run folder names shown above. Select the appropriate preset in the GUI or CLI; +see [CLI training](using_CLI.md) for project setup and training commands. +Each model card also provides a strict-load Python example and synthetic forward-pass example. +No new package dependency or model-loader changes are required. + +The historical YAML files contain original filesystem paths; they describe pretraining, rather +than serving as a portable project config. Use the new run created by fine-tuning for behavior +inference: the initialization repos do not contain the newer `config.yaml` and inference metrics. + +## Legacy archive + +The [original six-folder bundle](https://huggingface.co/jbohnslav/deepethogram-pretrained) +remains available with historical plots, metrics, separate stream snapshots and an older incomplete DEG-s folder. +The individual DEG-s repository provides the complete checkpoint used in the test suite. +Its structure differs: downloading that bundle into `models` creates the entire `pretrained_models` tree. +The [historical Google Drive ZIP](https://drive.google.com/file/d/1ntIZVbOG1UAiFVlsAAuKEBEVCVevyets/view) +is retained as a fallback; it was not used for these releases. diff --git a/huggingface/README.md b/huggingface/README.md new file mode 100644 index 0000000..5c7d1d2 --- /dev/null +++ b/huggingface/README.md @@ -0,0 +1,113 @@ +--- +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +tags: +- deepethogram +- pytorch +- optical-flow +- animal-behavior +- kinetics +- pretrained +- legacy-archive +--- + +# DeepEthogram pretrained models — legacy archive + +For new projects, use the **[individual model repositories in the DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec)**. +Each usable model has its own card, architecture/task tags, checkpoint and checksum manifest. +This repository preserves the historical bundle for compatibility and archival use. + +Archived pretrained weights for [DeepEthogram](https://github.com/jbohnslav/deepethogram), +a pipeline for supervised behavior classification from raw video pixels. +These weights initialize optical-flow and feature-extractor training on your own behavior dataset. +They are not ready-to-use classifiers for your behavior labels. No videos, datasets, or trained +sequence classifiers are included. + +## Download + +Install the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli), then download +into an existing DeepEthogram project's `models` directory: + +```bash +hf download jbohnslav/deepethogram-pretrained --revision v1.0 --local-dir /path/to/your_project/models +``` + +The resulting layout must be `your_project/models/pretrained_models//...`. +Preserve the run names and nested `flow`/`spatial` directories. Downloading the full repository also +includes this card, the license, and `SHA256SUMS` in the destination directory. +For reproducible downloads, pass `--revision` with a release tag or commit ID. + +Verify the model files from that directory: + +```bash +cd /path/to/your_project/models +shasum -a 256 -c SHA256SUMS +``` + +Use the DeepEthogram GUI or training commands to select the pretrained weights. Models retain +the original PyTorch checkpoint format; use DeepEthogram rather than Transformers to load them. + +## Contents + +The archive contains 30 files (2,058,618,150 bytes). `SHA256SUMS` records the SHA-256 checksum of every archived file. Original checkpoints, YAML hyperparameters, model +definitions, HDF5 metrics, and TIFF training plots are retained without conversion. + +| Run | Component | Architecture | Status | +| --- | --- | --- | --- | +| `200221_115158_TinyMotionNet` | Flow generator | TinyMotionNet | Checkpoint present | +| `200310_174416_MotionNet_kinetics` | Flow generator | MotionNet | Checkpoint present | +| `200313_125331_TinyMotionNet3D_kinetics` | Flow generator | TinyMotionNet3D | Checkpoint present | +| `200409_101808_hidden_two_stream_kinetics_degs` | DEG-s feature extractor | ResNet3D-34 | Flow-classifier checkpoint only; incomplete | +| `200410_142156_hidden_two_stream_kinetics_degm` | DEG-m feature extractor | ResNet50 | Full and separate stream checkpoints present | +| `200415_125824_hidden_two_stream_kinetics_degf` | DEG-f feature extractor | ResNet18 | Full and separate stream checkpoints present | + +## Training and intended use + +The historical configuration files identify Kinetics700 as the pretraining dataset and specify +700 classes for feature extractors. The three flow models provide motion estimates; feature +extractors combine appearance and motion. Fine-tune on your own labeled videos, then train a +sequence model to classify your behaviors. Keep training, validation, and test videos separate. + +Historical hyperparameters describe the original training environment and contain old filesystem +paths and YAML serialization tags. They are provenance records, not portable project configs. +The archived `.pt` files use legacy PyTorch serialization. + +## Validation + +Using PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`, all five discoverable models loaded with strict +state-dict matching and produced finite outputs on synthetic CPU inputs. The two feature +extractors returned 700-class outputs. All five nested flow/spatial classifier checkpoints +were readable and contained finite tensors. These checks verify file and architecture +compatibility, not behavior-classification accuracy. + +## Known limitations + +- The DEG-s folder has `flow/checkpoint.pt` but no top-level checkpoint or spatial checkpoint. + DeepEthogram's current model discovery skips this incomplete feature extractor. This release + provides three discoverable flow generators and the DEG-m/DEG-f feature extractors. + The [individual DEG-s release](https://huggingface.co/jbohnslav/deepethogram-deg-s) + provides a complete checkpoint. +- DEG-f has no `classification_metrics.h5`; the legacy folders use `hyperparameters.yaml`, + rather than `config.yaml`. Some inference helpers require newer run files. Use this bundle + for pretraining initialization and use the new run produced by fine-tuning for inference. +- No task-specific behavior class names, thresholds, or sequence model are supplied. +- The publication's behavior-classification results describe trained pipelines and datasets; + they are not benchmark scores for this archive alone. + +## License and citation + +The accompanying [LICENSE](LICENSE) reproduces the DeepEthogram repository's Harvard +Academic and Non-Commercial Research Use Software License and Terms of Use unchanged. +It restricts commercial use and end-user redistribution. This mirror does not grant additional +permissions or relicense the original assets. Commercial licensing inquiries should go to the +Harvard Office of Technology Development as described in that license. + +Please cite: + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/RELEASING.md b/huggingface/RELEASING.md new file mode 100644 index 0000000..72f025b --- /dev/null +++ b/huggingface/RELEASING.md @@ -0,0 +1,63 @@ +# Maintaining the DeepEthogram model releases + +Primary releases are six individual model repos grouped in the +[DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec). +`models.json` records repo IDs, original run names, exact commits and verification evidence. +`models//` contains the corresponding model card, machine-readable configuration and checksum manifest. +Model binaries remain outside Git. + +## Per-model layout + +Stage only the chosen run's original `checkpoint.pt` and `hyperparameters.yaml`, this repository's +unchanged `license.txt` as `LICENSE`, and that model's card, `model_config.json` and `SHA256SUMS`: + +```text +README.md +LICENSE +checkpoint.pt +hyperparameters.yaml +model_config.json +SHA256SUMS +``` + +Keep checkpoint bytes unchanged. The configuration summary is specific to DeepEthogram; +it is not a Transformers configuration. Model cards specify the library, architecture and task. +DEG-f/DEG-m/DEG-s use the supported `video-classification` pipeline tag. Flow generators use the custom +`optical-flow` tag, since Hugging Face's current task registry has no optical-flow pipeline category. + +Each manifest covers weights, hyperparameters, license and model configuration. Recompute the +manifest whenever those files change. Verify the staged files using `shasum -a 256 -c SHA256SUMS`. +Upload a dedicated staging directory, not the source checkout: + +```bash +hf upload jbohnslav/deepethogram-MODEL /path/to/staged-model . --type model +``` + +Download the uploaded commit into a fresh run folder, verify checksums, execute the model card's +Python example, and check project discovery from its parent pretrained_models directory. Check +strict state-dict loading and a finite-output CPU forward pass with the corresponding architecture. +Read back Hub metadata to verify the model's task/library tags. Only then tag the verified commit. + +The initial per-model release tag is `v1.0`. Model cards and user documentation pin it. +When updating weights, create a new tag rather than moving an existing release tag. + +## Collection and legacy archive + +Add each verified model to the collection. Document the training pairs: TinyMotionNet + DEG-f, +MotionNet + DEG-m, and TinyMotionNet3D + DEG-s. Their full feature-extractor checkpoints include embedded flow weights +for direct Python loading, while the normal training workflow loads the components separately. + +The older [deepethogram-pretrained](https://huggingface.co/jbohnslav/deepethogram-pretrained) +repo remains an explicitly labeled legacy archive. Its card is `README.md` in this directory; +its original 30-file checksum manifest is `SHA256SUMS`. It retains plots, metrics, model definitions +and stream checkpoints, including an older incomplete DEG-s folder. The individual DEG-s +release provides the complete checkpoint from the existing test fixture archive. + +## CI checkpoint contract + +`setup_tests.py` reads `models.json`, downloads every model at its immutable `commit`, and verifies +all files against the checked-in per-model checksum manifests. Existing fixture folders still run +this step. CPU CI invokes the script directly; the Modal GPU runner invokes it after copying only +its video fixtures from the persistent test-data volume. `tests/test_pretrained_models.py` checks +artifact identity, strict architecture matching, finite forward outputs and discovery for all models. +When releasing new weights, update commits/checksums together and run setup plus the CPU suite. diff --git a/huggingface/SHA256SUMS b/huggingface/SHA256SUMS new file mode 100644 index 0000000..8631baf --- /dev/null +++ b/huggingface/SHA256SUMS @@ -0,0 +1,30 @@ +9940cc4db1b9730a6b8dd3554eb9a0b23aac1f7db1e9c1c34a29020d09e90910 pretrained_models/200221_115158_TinyMotionNet/TinyMotionNet_definition.pt +07d8617e5ee84bc1bbeac57d3a6844595d471b8446f44f3f0cd626c30907eeb4 pretrained_models/200221_115158_TinyMotionNet/checkpoint.pt +ca9dc207f8771c79e3c54f257fea095389eb63be3b9e510e769e5a695ba65acb pretrained_models/200221_115158_TinyMotionNet/hyperparameters.yaml +10eec7c0444c277566c135038e82a161f24220822ac23e7085ff00baf2597347 pretrained_models/200221_115158_TinyMotionNet/opticalflow_metrics.h5 +f05abfa77fecbe775a5986504859330a9b9b063628c48a0782c9ef27b0b399ff pretrained_models/200221_115158_TinyMotionNet/opticalflow_metrics.tiff +4a7e5ea029ae836cf7831469b2e6f78d23ebeb1426b001c17f96546f443b3de4 pretrained_models/200310_174416_MotionNet_kinetics/MotionNet_definition.pt +c75e175724ae04a94fc622cfe8949e63656a60639556b65f8d640eb2944a511a pretrained_models/200310_174416_MotionNet_kinetics/checkpoint.pt +fc8f58e37e37dccc2f6bddb0ad8ce1b39b0d448d78048adedf8a60b32f6668eb pretrained_models/200310_174416_MotionNet_kinetics/hyperparameters.yaml +606e55cc6b2da9336ef664b2dda3715af56fc6e32751e0fe2f25f40bd60b706a pretrained_models/200310_174416_MotionNet_kinetics/opticalflow_metrics.h5 +ee83903a80888664e6d15bcf19f0de31e2319c09dc8ad419dc6df67460dbba1e pretrained_models/200310_174416_MotionNet_kinetics/opticalflow_metrics.tiff +ee83ba63405e3e832e024ad034c90853ab4b9ccc2c954801d9c0197f6a8c38be pretrained_models/200313_125331_TinyMotionNet3D_kinetics/TinyMotionNet3D_definition.pt +6d84b4bc9536438f6687b75a9883f265b9a138365547fd06168790231662ad00 pretrained_models/200313_125331_TinyMotionNet3D_kinetics/checkpoint.pt +73041516074c0c5edf51e837361775941d3c1c902852bf82415ec6499e7c6f12 pretrained_models/200313_125331_TinyMotionNet3D_kinetics/hyperparameters.yaml +59943e49341281327f7f757bba5dcd2a392f3e60522234a1f284176dce051a11 pretrained_models/200313_125331_TinyMotionNet3D_kinetics/opticalflow_metrics.h5 +bab3c5aa9a7d0de262955979b2c8eb15608b1200ff6ec369a6328e1215362656 pretrained_models/200313_125331_TinyMotionNet3D_kinetics/opticalflow_metrics.tiff +b27204bdbe3c6406126854e76c40ef013c90a12059a6a0598e57544df20e7c59 pretrained_models/200409_101808_hidden_two_stream_kinetics_degs/classification_metrics.h5 +6228e911b4d7e8eb1a5c0cb19fe211bf373269b34e050c3ff2861661646c0c96 pretrained_models/200409_101808_hidden_two_stream_kinetics_degs/classification_metrics.tiff +ac5cc3886237e8a8c39996810aaf7cdd0d66fd2000cf139b931172edfea0f4da pretrained_models/200409_101808_hidden_two_stream_kinetics_degs/flow/checkpoint.pt +8d37f2e97b07a2bbb796525e1fdcccee5820f3b636e970a51b14256dc6345645 pretrained_models/200409_101808_hidden_two_stream_kinetics_degs/hyperparameters.yaml +c0dfbe2fe4e930265da2aec6bc20093bed96de7fffc828ac8cb9295640e2e665 pretrained_models/200410_142156_hidden_two_stream_kinetics_degm/checkpoint.pt +1add6cfbd87a08977b470af17a00fd0bc7fe5a90784fb2d0752bf017a5b79109 pretrained_models/200410_142156_hidden_two_stream_kinetics_degm/classification_metrics.h5 +55382efcdb11160d13736cc2d7120485528abc75c3d4d2f1014362ed2fb7d3d1 pretrained_models/200410_142156_hidden_two_stream_kinetics_degm/classification_metrics.tiff +ad8047b45cd94aa3b9e67b3c1e80b79d8b7f96725eff65d87bcb8118d8b269f9 pretrained_models/200410_142156_hidden_two_stream_kinetics_degm/flow/checkpoint.pt +a696a03829cc7ec7790d4bc5ceacf1a70d8aee49a0ba9a360a51078bb489b3ca pretrained_models/200410_142156_hidden_two_stream_kinetics_degm/hyperparameters.yaml +b39e09235ebfa2d51dc12d650df8813e8084d4e836b7de8c486d1eee2e23d7c1 pretrained_models/200410_142156_hidden_two_stream_kinetics_degm/spatial/checkpoint.pt +9dae6071064c461d2e29460e611254380135de6c7b8f62374c25f0344b140349 pretrained_models/200415_125824_hidden_two_stream_kinetics_degf/checkpoint.pt +7efa87c07bec7709cc78d81edfb8359b812901ac1b76a4bb8ebaf8518383b816 pretrained_models/200415_125824_hidden_two_stream_kinetics_degf/classification_metrics.tiff +eb20d8a5223e7b5f6f5a2e0aebaf1020f56dc7654ae97f6d6a524cd473a9421a pretrained_models/200415_125824_hidden_two_stream_kinetics_degf/flow/checkpoint.pt +a8fc5b01fe3ec87e2d329a391cae925bd225a06649bc5a3c450dae60621cfba7 pretrained_models/200415_125824_hidden_two_stream_kinetics_degf/hyperparameters.yaml +df093808e0d75f1095105a1b53659073009dc6115c93cc6764dac5269b62e361 pretrained_models/200415_125824_hidden_two_stream_kinetics_degf/spatial/checkpoint.pt diff --git a/huggingface/models.json b/huggingface/models.json new file mode 100644 index 0000000..54e0632 --- /dev/null +++ b/huggingface/models.json @@ -0,0 +1,178 @@ +{ + "collection": "https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec", + "release": "v1.0", + "models": [ + { + "slug": "tinymotionnet", + "name": "TinyMotionNet", + "repo_id": "jbohnslav/deepethogram-tinymotionnet", + "architecture": "TinyMotionNet", + "task": "optical-flow", + "flow_generator": null, + "checkpoint_bytes": 23450307, + "commit": "b481cbc41b96db5de248dc533e39ac6f979db33b", + "verification": { + "checksums_passed": 4, + "card_example": "strict load and finite CPU forward passed", + "output_shapes": [ + [ + 1, + 20, + 32, + 32 + ], + [ + 1, + 20, + 16, + 16 + ], + [ + 1, + 20, + 8, + 8 + ] + ] + }, + "run_directory": "200221_115158_TinyMotionNet" + }, + { + "slug": "motionnet", + "name": "MotionNet", + "repo_id": "jbohnslav/deepethogram-motionnet", + "architecture": "MotionNet", + "task": "optical-flow", + "flow_generator": null, + "checkpoint_bytes": 183604611, + "commit": "f282e2dae1dc15ce3ef45ce99f4c202b08ce7248", + "verification": { + "checksums_passed": 4, + "card_example": "strict load and finite CPU forward passed", + "output_shapes": [ + [ + 1, + 20, + 32, + 32 + ], + [ + 1, + 20, + 16, + 16 + ], + [ + 1, + 20, + 8, + 8 + ] + ] + }, + "run_directory": "200310_174416_MotionNet_kinetics" + }, + { + "slug": "tinymotionnet3d", + "name": "TinyMotionNet3D", + "repo_id": "jbohnslav/deepethogram-tinymotionnet3d", + "architecture": "TinyMotionNet3D", + "task": "optical-flow", + "flow_generator": null, + "checkpoint_bytes": 1769548, + "commit": "e68a968956658727394d7a8e1cc3b8763b7d60b6", + "verification": { + "checksums_passed": 4, + "card_example": "strict load and finite CPU forward passed", + "output_shapes": [ + [ + 1, + 2, + 10, + 32, + 32 + ], + [ + 1, + 2, + 10, + 16, + 16 + ], + [ + 1, + 2, + 10, + 8, + 8 + ] + ] + }, + "run_directory": "200313_125331_TinyMotionNet3D_kinetics" + }, + { + "slug": "deg-f", + "name": "DEG-f", + "repo_id": "jbohnslav/deepethogram-deg-f", + "architecture": "resnet18", + "task": "video-classification", + "flow_generator": "TinyMotionNet", + "checkpoint_bytes": 100453714, + "commit": "96c98a45421ace0a70bc287740e7167e4078b1c3", + "verification": { + "checksums_passed": 4, + "card_example": "strict load and finite CPU forward passed", + "output_shapes": [ + [ + 1, + 700 + ] + ] + }, + "run_directory": "200415_125824_hidden_two_stream_kinetics_degf" + }, + { + "slug": "deg-m", + "name": "DEG-m", + "repo_id": "jbohnslav/deepethogram-deg-m", + "architecture": "resnet50", + "task": "video-classification", + "flow_generator": "MotionNet", + "checkpoint_bytes": 383713235, + "commit": "582a5f28cdd931d3923d9ac065cbbe5a9f122fb1", + "verification": { + "checksums_passed": 4, + "card_example": "strict load and finite CPU forward passed", + "output_shapes": [ + [ + 1, + 700 + ] + ] + }, + "run_directory": "200410_142156_hidden_two_stream_kinetics_degm" + }, + { + "slug": "deg-s", + "name": "DEG-s", + "repo_id": "jbohnslav/deepethogram-deg-s", + "run_directory": "200409_101808_hidden_two_stream_kinetics_degs", + "architecture": "resnet3d_34", + "task": "video-classification", + "flow_generator": "TinyMotionNet3D", + "checkpoint_bytes": 512895194, + "commit": "1cd361321b7670e45f7d9ad385bc9cc4f0deb003", + "verification": { + "checksums_passed": 4, + "card_example": "strict load and finite CPU forward passed", + "output_shapes": [ + [ + 1, + 700 + ] + ] + } + } + ], + "uploaded_on": "2026-10-19" +} diff --git a/huggingface/models/deg-f/README.md b/huggingface/models/deg-f/README.md new file mode 100644 index 0000000..c8b0ccc --- /dev/null +++ b/huggingface/models/deg-f/README.md @@ -0,0 +1,108 @@ +--- +model_name: DeepEthogram DEG-f +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +pipeline_tag: video-classification +tags: +- deepethogram +- pytorch +- video-classification +- deg-f +- resnet18 +- kinetics700 +- animal-behavior +- pretrained +--- + +# DeepEthogram DEG-f + +DEG-f is a Kinetics700-pretrained hidden two-stream feature extractor for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +This repository contains this model's checkpoint and historical training hyperparameters. +See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. + +## Model details + +| Property | Value | +| --- | --- | +| Architecture | resnet18 with TinyMotionNet and average two-stream fusion | +| Task | video-classification | +| Input | 11 RGB frames stacked as `(N, 33, H, W)` | +| Output | 700 logits per input clip, before any task-specific behavior fine-tuning | +| Pretraining data | Kinetics700, as recorded in the historical hyperparameters | +| Framework | PyTorch; DeepEthogram | + +The full checkpoint includes its own TinyMotionNet optical-flow generator, RGB and flow +classifiers, and fusion parameters. Loading this model directly needs no other model download. +For DeepEthogram's normal training workflow, download the separate +[TinyMotionNet](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet) initialization checkpoint too: +the training code initializes flow and feature-extractor components separately. + +## Download and load + +Install DeepEthogram and the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli). +Download only this model into an existing DeepEthogram project's pretrained directory: + +```bash +hf download jbohnslav/deepethogram-deg-f --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200415_125824_hidden_two_stream_kinetics_degf +``` + +From that run directory, verify the artifacts with `shasum -a 256 -c SHA256SUMS`. +The `checkpoint.pt` and `hyperparameters.yaml` at its root let DeepEthogram discover +this model using its existing project layout. + +To load directly in Python, set `run_dir` to the directory downloaded above: + +```python +from pathlib import Path +import torch +from deepethogram import utils + +from deepethogram.feature_extractor.models.hidden_two_stream import deg_f +model = deg_f(num_classes=700, reload_imagenet=False) +run_dir = Path('/path/to/project/models/pretrained_models/200415_125824_hidden_two_stream_kinetics_degf') +state_dict, _, _ = utils.load_state_dict_from_file(run_dir / 'checkpoint.pt') +model.load_state_dict(state_dict, strict=True) +model.eval() +with torch.inference_mode(): + output = model(torch.zeros(1, 33, 64, 64)) +``` + +The zero-valued example checks shapes and loading. For real videos use DeepEthogram's +video preprocessing and normalization. Historical settings are in `hyperparameters.yaml`; +`model_config.json` summarizes architecture and input/output conventions. + +## Intended use and limitations + +Use these weights to initialize feature-extractor training on your own labeled behavior videos, +then train a sequence model. The 700-class pretraining head is not a classifier for your animal +behavior labels. Kinetics class-index names and trained behavior thresholds are not supplied. + +Historical YAML files contain old filesystem paths and legacy serialization tags; they are +provenance records, not portable project configs. Checkpoints retain their original PyTorch +serialization. Use DeepEthogram, rather than Transformers `AutoModel`, to load them. + +This release contains initialization weights, not a complete trained behavior inference run: +newer inference helpers expect `config.yaml` and associated metrics from your fine-tuned run. +The complete historical plots, metrics, and additional stream snapshots remain in the +[legacy archive](https://huggingface.co/jbohnslav/deepethogram-pretrained). + +## Validation + +`SHA256SUMS` covers the checkpoint, training hyperparameters, license and model configuration. +Validated with PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`: strict parameter loading and finite CPU +outputs on the synthetic input above. This is a compatibility check, not an accuracy benchmark. + +## License and citation + +[LICENSE](LICENSE) reproduces the original DeepEthogram Harvard Academic and Non-Commercial +Research Use Software License and Terms of Use unchanged. It restricts commercial use and +end-user redistribution. This mirror grants no additional permissions and does not relicense +the assets; commercial inquiries should go to Harvard OTD as described in the license. + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/models/deg-f/SHA256SUMS b/huggingface/models/deg-f/SHA256SUMS new file mode 100644 index 0000000..f824aec --- /dev/null +++ b/huggingface/models/deg-f/SHA256SUMS @@ -0,0 +1,4 @@ +899e15e791552500b93f3ddcc239f570670edd9d9016c05b482ba04cb87415b0 LICENSE +9dae6071064c461d2e29460e611254380135de6c7b8f62374c25f0344b140349 checkpoint.pt +a8fc5b01fe3ec87e2d329a391cae925bd225a06649bc5a3c450dae60621cfba7 hyperparameters.yaml +d2f322388587cb748b9847ec5f5c53d26b85c7606c56cbd922d4113cf7bcc5ea model_config.json diff --git a/huggingface/models/deg-f/model_config.json b/huggingface/models/deg-f/model_config.json new file mode 100644 index 0000000..2f5b639 --- /dev/null +++ b/huggingface/models/deg-f/model_config.json @@ -0,0 +1,18 @@ +{ + "format_version": 1, + "library_name": "deepethogram", + "model_name": "DEG-f", + "architecture": "resnet18", + "task": "video-classification", + "checkpoint": "checkpoint.pt", + "flow_generator": "TinyMotionNet", + "num_rgb_frames": 11, + "input_shape": [ + "N", + 33, + "H", + "W" + ], + "num_classes": 700, + "deepethogram_source_revision": "1541eec6958f90673b79c402e10b434c8cb7c868" +} diff --git a/huggingface/models/deg-m/README.md b/huggingface/models/deg-m/README.md new file mode 100644 index 0000000..114ccf6 --- /dev/null +++ b/huggingface/models/deg-m/README.md @@ -0,0 +1,108 @@ +--- +model_name: DeepEthogram DEG-m +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +pipeline_tag: video-classification +tags: +- deepethogram +- pytorch +- video-classification +- deg-m +- resnet50 +- kinetics700 +- animal-behavior +- pretrained +--- + +# DeepEthogram DEG-m + +DEG-m is a Kinetics700-pretrained hidden two-stream feature extractor for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +This repository contains this model's checkpoint and historical training hyperparameters. +See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. + +## Model details + +| Property | Value | +| --- | --- | +| Architecture | resnet50 with MotionNet and average two-stream fusion | +| Task | video-classification | +| Input | 11 RGB frames stacked as `(N, 33, H, W)` | +| Output | 700 logits per input clip, before any task-specific behavior fine-tuning | +| Pretraining data | Kinetics700, as recorded in the historical hyperparameters | +| Framework | PyTorch; DeepEthogram | + +The full checkpoint includes its own MotionNet optical-flow generator, RGB and flow +classifiers, and fusion parameters. Loading this model directly needs no other model download. +For DeepEthogram's normal training workflow, download the separate +[MotionNet](https://huggingface.co/jbohnslav/deepethogram-motionnet) initialization checkpoint too: +the training code initializes flow and feature-extractor components separately. + +## Download and load + +Install DeepEthogram and the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli). +Download only this model into an existing DeepEthogram project's pretrained directory: + +```bash +hf download jbohnslav/deepethogram-deg-m --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200410_142156_hidden_two_stream_kinetics_degm +``` + +From that run directory, verify the artifacts with `shasum -a 256 -c SHA256SUMS`. +The `checkpoint.pt` and `hyperparameters.yaml` at its root let DeepEthogram discover +this model using its existing project layout. + +To load directly in Python, set `run_dir` to the directory downloaded above: + +```python +from pathlib import Path +import torch +from deepethogram import utils + +from deepethogram.feature_extractor.models.hidden_two_stream import deg_m +model = deg_m(num_classes=700, reload_imagenet=False) +run_dir = Path('/path/to/project/models/pretrained_models/200410_142156_hidden_two_stream_kinetics_degm') +state_dict, _, _ = utils.load_state_dict_from_file(run_dir / 'checkpoint.pt') +model.load_state_dict(state_dict, strict=True) +model.eval() +with torch.inference_mode(): + output = model(torch.zeros(1, 33, 64, 64)) +``` + +The zero-valued example checks shapes and loading. For real videos use DeepEthogram's +video preprocessing and normalization. Historical settings are in `hyperparameters.yaml`; +`model_config.json` summarizes architecture and input/output conventions. + +## Intended use and limitations + +Use these weights to initialize feature-extractor training on your own labeled behavior videos, +then train a sequence model. The 700-class pretraining head is not a classifier for your animal +behavior labels. Kinetics class-index names and trained behavior thresholds are not supplied. + +Historical YAML files contain old filesystem paths and legacy serialization tags; they are +provenance records, not portable project configs. Checkpoints retain their original PyTorch +serialization. Use DeepEthogram, rather than Transformers `AutoModel`, to load them. + +This release contains initialization weights, not a complete trained behavior inference run: +newer inference helpers expect `config.yaml` and associated metrics from your fine-tuned run. +The complete historical plots, metrics, and additional stream snapshots remain in the +[legacy archive](https://huggingface.co/jbohnslav/deepethogram-pretrained). + +## Validation + +`SHA256SUMS` covers the checkpoint, training hyperparameters, license and model configuration. +Validated with PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`: strict parameter loading and finite CPU +outputs on the synthetic input above. This is a compatibility check, not an accuracy benchmark. + +## License and citation + +[LICENSE](LICENSE) reproduces the original DeepEthogram Harvard Academic and Non-Commercial +Research Use Software License and Terms of Use unchanged. It restricts commercial use and +end-user redistribution. This mirror grants no additional permissions and does not relicense +the assets; commercial inquiries should go to Harvard OTD as described in the license. + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/models/deg-m/SHA256SUMS b/huggingface/models/deg-m/SHA256SUMS new file mode 100644 index 0000000..746d391 --- /dev/null +++ b/huggingface/models/deg-m/SHA256SUMS @@ -0,0 +1,4 @@ +899e15e791552500b93f3ddcc239f570670edd9d9016c05b482ba04cb87415b0 LICENSE +c0dfbe2fe4e930265da2aec6bc20093bed96de7fffc828ac8cb9295640e2e665 checkpoint.pt +a696a03829cc7ec7790d4bc5ceacf1a70d8aee49a0ba9a360a51078bb489b3ca hyperparameters.yaml +c21abfacf96940db6e9ab5bee58ce0d540af2e94c908ef4de102b960278d3ea4 model_config.json diff --git a/huggingface/models/deg-m/model_config.json b/huggingface/models/deg-m/model_config.json new file mode 100644 index 0000000..ce7e674 --- /dev/null +++ b/huggingface/models/deg-m/model_config.json @@ -0,0 +1,18 @@ +{ + "format_version": 1, + "library_name": "deepethogram", + "model_name": "DEG-m", + "architecture": "resnet50", + "task": "video-classification", + "checkpoint": "checkpoint.pt", + "flow_generator": "MotionNet", + "num_rgb_frames": 11, + "input_shape": [ + "N", + 33, + "H", + "W" + ], + "num_classes": 700, + "deepethogram_source_revision": "1541eec6958f90673b79c402e10b434c8cb7c868" +} diff --git a/huggingface/models/deg-s/README.md b/huggingface/models/deg-s/README.md new file mode 100644 index 0000000..8b8e292 --- /dev/null +++ b/huggingface/models/deg-s/README.md @@ -0,0 +1,109 @@ +--- +model_name: DeepEthogram DEG-s +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +pipeline_tag: video-classification +tags: +- deepethogram +- pytorch +- video-classification +- deg-s +- resnet3d_34 +- kinetics700 +- animal-behavior +- pretrained +--- + +# DeepEthogram DEG-s + +DEG-s is a Kinetics700-pretrained hidden two-stream feature extractor for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +This repository contains this model's checkpoint and historical training hyperparameters. +See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. + +## Model details + +| Property | Value | +| --- | --- | +| Architecture | resnet3d_34 with TinyMotionNet3D and average two-stream fusion | +| Task | video-classification | +| Input | 11 RGB frames, shaped `(N, 3, 11, H, W)` | +| Output | 700 logits per input clip, before any task-specific behavior fine-tuning | +| Pretraining data | Kinetics700, as recorded in the historical hyperparameters | +| Framework | PyTorch; DeepEthogram | + +The full checkpoint includes its own TinyMotionNet3D optical-flow generator, RGB and flow +classifiers, and fusion parameters. Loading this model directly needs no other model download. +For DeepEthogram's normal training workflow, download the separate +[TinyMotionNet3D](https://huggingface.co/jbohnslav/deepethogram-tinymotionnet3d) initialization checkpoint too: +the training code initializes flow and feature-extractor components separately. + +## Download and load + +Install DeepEthogram and the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli). +Download only this model into an existing DeepEthogram project's pretrained directory: + +```bash +hf download jbohnslav/deepethogram-deg-s --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200409_101808_hidden_two_stream_kinetics_degs +``` + +From that run directory, verify the artifacts with `shasum -a 256 -c SHA256SUMS`. +The `checkpoint.pt` and `hyperparameters.yaml` at its root let DeepEthogram discover +this model using its existing project layout. + +To load directly in Python, set `run_dir` to the directory downloaded above: + +```python +from pathlib import Path +import torch +from deepethogram import utils + +from deepethogram.feature_extractor.models.hidden_two_stream import deg_s +model = deg_s(num_classes=700, reload_imagenet=False) +run_dir = Path('/path/to/project/models/pretrained_models/200409_101808_hidden_two_stream_kinetics_degs') +state_dict, _, _ = utils.load_state_dict_from_file(run_dir / 'checkpoint.pt') +model.load_state_dict(state_dict, strict=True) +model.eval() +with torch.inference_mode(): + output = model(torch.zeros(1, 3, 11, 64, 64)) +``` + +The zero-valued example checks shapes and loading. For real videos use DeepEthogram's +video preprocessing and normalization. Historical settings are in `hyperparameters.yaml`; +`model_config.json` summarizes architecture and input/output conventions. + +## Intended use and limitations + +Use these weights to initialize feature-extractor training on your own labeled behavior videos, +then train a sequence model. The 700-class pretraining head is not a classifier for your animal +behavior labels. Kinetics class-index names and trained behavior thresholds are not supplied. + +Historical YAML files contain old filesystem paths and legacy serialization tags; they are +provenance records, not portable project configs. Checkpoints retain their original PyTorch +serialization. Use DeepEthogram, rather than Transformers `AutoModel`, to load them. + +This release contains initialization weights, not a complete trained behavior inference run: +newer inference helpers expect `config.yaml` and associated metrics from your fine-tuned run. +This complete checkpoint was already included in DeepEthogram’s test fixture archive. +The older [legacy bundle](https://huggingface.co/jbohnslav/deepethogram-pretrained) contains +only a partial DEG-s snapshot; use this repository for DEG-s initialization. + +## Validation + +`SHA256SUMS` covers the checkpoint, training hyperparameters, license and model configuration. +Validated with PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`: strict parameter loading and finite CPU +outputs on the synthetic input above. This is a compatibility check, not an accuracy benchmark. + +## License and citation + +[LICENSE](LICENSE) reproduces the original DeepEthogram Harvard Academic and Non-Commercial +Research Use Software License and Terms of Use unchanged. It restricts commercial use and +end-user redistribution. This mirror grants no additional permissions and does not relicense +the assets; commercial inquiries should go to Harvard OTD as described in the license. + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/models/deg-s/SHA256SUMS b/huggingface/models/deg-s/SHA256SUMS new file mode 100644 index 0000000..a53bd1a --- /dev/null +++ b/huggingface/models/deg-s/SHA256SUMS @@ -0,0 +1,4 @@ +899e15e791552500b93f3ddcc239f570670edd9d9016c05b482ba04cb87415b0 LICENSE +fc75b96810afb948e392ef6c6e65f8a20c9ca5ebdd787cee1ae5703eb4e03802 checkpoint.pt +8d37f2e97b07a2bbb796525e1fdcccee5820f3b636e970a51b14256dc6345645 hyperparameters.yaml +131835e0b4270b839fa6ddb7eb429c607f64620332e75fa4ee283ea044fc5602 model_config.json diff --git a/huggingface/models/deg-s/model_config.json b/huggingface/models/deg-s/model_config.json new file mode 100644 index 0000000..b880082 --- /dev/null +++ b/huggingface/models/deg-s/model_config.json @@ -0,0 +1,19 @@ +{ + "format_version": 1, + "library_name": "deepethogram", + "model_name": "DEG-s", + "architecture": "resnet3d_34", + "task": "video-classification", + "checkpoint": "checkpoint.pt", + "flow_generator": "TinyMotionNet3D", + "num_rgb_frames": 11, + "input_shape": [ + "N", + 3, + 11, + "H", + "W" + ], + "num_classes": 700, + "deepethogram_source_revision": "1541eec6958f90673b79c402e10b434c8cb7c868" +} diff --git a/huggingface/models/motionnet/README.md b/huggingface/models/motionnet/README.md new file mode 100644 index 0000000..491f2f4 --- /dev/null +++ b/huggingface/models/motionnet/README.md @@ -0,0 +1,101 @@ +--- +model_name: DeepEthogram MotionNet +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +tags: +- deepethogram +- pytorch +- optical-flow +- motionnet +- motionnet +- kinetics700 +- animal-behavior +- pretrained +--- + +# DeepEthogram MotionNet + +MotionNet is a optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +This repository contains this model's checkpoint and historical training hyperparameters. +See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. + +## Model details + +| Property | Value | +| --- | --- | +| Architecture | MotionNet | +| Task | optical-flow | +| Input | 11 RGB frames stacked as `(N, 33, H, W)` | +| Output | a three-scale optical-flow pyramid; highest-resolution shape `(1, 20, 32, 32)` for the example | +| Pretraining data | Kinetics700, as recorded in the historical hyperparameters | +| Framework | PyTorch; DeepEthogram | + +The model predicts motion fields from video frames. It does not classify behaviors. +Hugging Face has no dedicated optical-flow pipeline category in its current task registry; +this repo uses the explicit `optical-flow` tag and DeepEthogram library metadata. + +## Download and load + +Install DeepEthogram and the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli). +Download only this model into an existing DeepEthogram project's pretrained directory: + +```bash +hf download jbohnslav/deepethogram-motionnet --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200310_174416_MotionNet_kinetics +``` + +From that run directory, verify the artifacts with `shasum -a 256 -c SHA256SUMS`. +The `checkpoint.pt` and `hyperparameters.yaml` at its root let DeepEthogram discover +this model using its existing project layout. + +To load directly in Python, set `run_dir` to the directory downloaded above: + +```python +from pathlib import Path +import torch +from deepethogram import utils + +from deepethogram.flow_generator.models.MotionNet import MotionNet +model = MotionNet() +run_dir = Path('/path/to/project/models/pretrained_models/200310_174416_MotionNet_kinetics') +state_dict, _, _ = utils.load_state_dict_from_file(run_dir / 'checkpoint.pt') +model.load_state_dict(state_dict, strict=True) +model.eval() +with torch.inference_mode(): + output = model(torch.zeros(1, 33, 64, 64)) +``` + +The zero-valued example checks shapes and loading. For real videos use DeepEthogram's +video preprocessing and normalization. Historical settings are in `hyperparameters.yaml`; +`model_config.json` summarizes architecture and input/output conventions. + +## Intended use and limitations + +Historical YAML files contain old filesystem paths and legacy serialization tags; they are +provenance records, not portable project configs. Checkpoints retain their original PyTorch +serialization. Use DeepEthogram, rather than Transformers `AutoModel`, to load them. + +This release contains initialization weights, not a complete trained behavior inference run: +newer inference helpers expect `config.yaml` and associated metrics from your fine-tuned run. +The complete historical plots, metrics, and additional stream snapshots remain in the +[legacy archive](https://huggingface.co/jbohnslav/deepethogram-pretrained). + +## Validation + +`SHA256SUMS` covers the checkpoint, training hyperparameters, license and model configuration. +Validated with PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`: strict parameter loading and finite CPU +outputs on the synthetic input above. This is a compatibility check, not an accuracy benchmark. + +## License and citation + +[LICENSE](LICENSE) reproduces the original DeepEthogram Harvard Academic and Non-Commercial +Research Use Software License and Terms of Use unchanged. It restricts commercial use and +end-user redistribution. This mirror grants no additional permissions and does not relicense +the assets; commercial inquiries should go to Harvard OTD as described in the license. + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/models/motionnet/SHA256SUMS b/huggingface/models/motionnet/SHA256SUMS new file mode 100644 index 0000000..5efd687 --- /dev/null +++ b/huggingface/models/motionnet/SHA256SUMS @@ -0,0 +1,4 @@ +899e15e791552500b93f3ddcc239f570670edd9d9016c05b482ba04cb87415b0 LICENSE +c75e175724ae04a94fc622cfe8949e63656a60639556b65f8d640eb2944a511a checkpoint.pt +fc8f58e37e37dccc2f6bddb0ad8ce1b39b0d448d78048adedf8a60b32f6668eb hyperparameters.yaml +91e733d0a63a7ed021314b1b488375720ac0099f5426fbfb505cdc49544c437c model_config.json diff --git a/huggingface/models/motionnet/model_config.json b/huggingface/models/motionnet/model_config.json new file mode 100644 index 0000000..b954a5d --- /dev/null +++ b/huggingface/models/motionnet/model_config.json @@ -0,0 +1,18 @@ +{ + "format_version": 1, + "library_name": "deepethogram", + "model_name": "MotionNet", + "architecture": "MotionNet", + "task": "optical-flow", + "checkpoint": "checkpoint.pt", + "flow_generator": "MotionNet", + "num_rgb_frames": 11, + "input_shape": [ + "N", + 33, + "H", + "W" + ], + "num_classes": null, + "deepethogram_source_revision": "1541eec6958f90673b79c402e10b434c8cb7c868" +} diff --git a/huggingface/models/tinymotionnet/README.md b/huggingface/models/tinymotionnet/README.md new file mode 100644 index 0000000..e8f3ce0 --- /dev/null +++ b/huggingface/models/tinymotionnet/README.md @@ -0,0 +1,101 @@ +--- +model_name: DeepEthogram TinyMotionNet +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +tags: +- deepethogram +- pytorch +- optical-flow +- tinymotionnet +- tinymotionnet +- kinetics700 +- animal-behavior +- pretrained +--- + +# DeepEthogram TinyMotionNet + +TinyMotionNet is a optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +This repository contains this model's checkpoint and historical training hyperparameters. +See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. + +## Model details + +| Property | Value | +| --- | --- | +| Architecture | TinyMotionNet | +| Task | optical-flow | +| Input | 11 RGB frames stacked as `(N, 33, H, W)` | +| Output | a three-scale optical-flow pyramid; highest-resolution shape `(1, 20, 32, 32)` for the example | +| Pretraining data | Kinetics700, as recorded in the historical hyperparameters | +| Framework | PyTorch; DeepEthogram | + +The model predicts motion fields from video frames. It does not classify behaviors. +Hugging Face has no dedicated optical-flow pipeline category in its current task registry; +this repo uses the explicit `optical-flow` tag and DeepEthogram library metadata. + +## Download and load + +Install DeepEthogram and the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli). +Download only this model into an existing DeepEthogram project's pretrained directory: + +```bash +hf download jbohnslav/deepethogram-tinymotionnet --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200221_115158_TinyMotionNet +``` + +From that run directory, verify the artifacts with `shasum -a 256 -c SHA256SUMS`. +The `checkpoint.pt` and `hyperparameters.yaml` at its root let DeepEthogram discover +this model using its existing project layout. + +To load directly in Python, set `run_dir` to the directory downloaded above: + +```python +from pathlib import Path +import torch +from deepethogram import utils + +from deepethogram.flow_generator.models.TinyMotionNet import TinyMotionNet +model = TinyMotionNet() +run_dir = Path('/path/to/project/models/pretrained_models/200221_115158_TinyMotionNet') +state_dict, _, _ = utils.load_state_dict_from_file(run_dir / 'checkpoint.pt') +model.load_state_dict(state_dict, strict=True) +model.eval() +with torch.inference_mode(): + output = model(torch.zeros(1, 33, 64, 64)) +``` + +The zero-valued example checks shapes and loading. For real videos use DeepEthogram's +video preprocessing and normalization. Historical settings are in `hyperparameters.yaml`; +`model_config.json` summarizes architecture and input/output conventions. + +## Intended use and limitations + +Historical YAML files contain old filesystem paths and legacy serialization tags; they are +provenance records, not portable project configs. Checkpoints retain their original PyTorch +serialization. Use DeepEthogram, rather than Transformers `AutoModel`, to load them. + +This release contains initialization weights, not a complete trained behavior inference run: +newer inference helpers expect `config.yaml` and associated metrics from your fine-tuned run. +The complete historical plots, metrics, and additional stream snapshots remain in the +[legacy archive](https://huggingface.co/jbohnslav/deepethogram-pretrained). + +## Validation + +`SHA256SUMS` covers the checkpoint, training hyperparameters, license and model configuration. +Validated with PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`: strict parameter loading and finite CPU +outputs on the synthetic input above. This is a compatibility check, not an accuracy benchmark. + +## License and citation + +[LICENSE](LICENSE) reproduces the original DeepEthogram Harvard Academic and Non-Commercial +Research Use Software License and Terms of Use unchanged. It restricts commercial use and +end-user redistribution. This mirror grants no additional permissions and does not relicense +the assets; commercial inquiries should go to Harvard OTD as described in the license. + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/models/tinymotionnet/SHA256SUMS b/huggingface/models/tinymotionnet/SHA256SUMS new file mode 100644 index 0000000..7f8b721 --- /dev/null +++ b/huggingface/models/tinymotionnet/SHA256SUMS @@ -0,0 +1,4 @@ +899e15e791552500b93f3ddcc239f570670edd9d9016c05b482ba04cb87415b0 LICENSE +07d8617e5ee84bc1bbeac57d3a6844595d471b8446f44f3f0cd626c30907eeb4 checkpoint.pt +ca9dc207f8771c79e3c54f257fea095389eb63be3b9e510e769e5a695ba65acb hyperparameters.yaml +0b7a895ac57bee7d11852038fb103b8124fddb48a715344148e4910def8827c1 model_config.json diff --git a/huggingface/models/tinymotionnet/model_config.json b/huggingface/models/tinymotionnet/model_config.json new file mode 100644 index 0000000..9138cd3 --- /dev/null +++ b/huggingface/models/tinymotionnet/model_config.json @@ -0,0 +1,18 @@ +{ + "format_version": 1, + "library_name": "deepethogram", + "model_name": "TinyMotionNet", + "architecture": "TinyMotionNet", + "task": "optical-flow", + "checkpoint": "checkpoint.pt", + "flow_generator": "TinyMotionNet", + "num_rgb_frames": 11, + "input_shape": [ + "N", + 33, + "H", + "W" + ], + "num_classes": null, + "deepethogram_source_revision": "1541eec6958f90673b79c402e10b434c8cb7c868" +} diff --git a/huggingface/models/tinymotionnet3d/README.md b/huggingface/models/tinymotionnet3d/README.md new file mode 100644 index 0000000..71d6f9c --- /dev/null +++ b/huggingface/models/tinymotionnet3d/README.md @@ -0,0 +1,101 @@ +--- +model_name: DeepEthogram TinyMotionNet3D +language: en +license: other +license_name: deepethogram-academic-noncommercial +license_link: LICENSE +library_name: deepethogram +tags: +- deepethogram +- pytorch +- optical-flow +- tinymotionnet3d +- tinymotionnet3d +- kinetics700 +- animal-behavior +- pretrained +--- + +# DeepEthogram TinyMotionNet3D + +TinyMotionNet3D is a optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +This repository contains this model's checkpoint and historical training hyperparameters. +See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. + +## Model details + +| Property | Value | +| --- | --- | +| Architecture | TinyMotionNet3D | +| Task | optical-flow | +| Input | 11 RGB frames, shaped `(N, 3, 11, H, W)` | +| Output | a three-scale optical-flow pyramid; highest-resolution shape `(1, 2, 10, 32, 32)` for the example | +| Pretraining data | Kinetics700, as recorded in the historical hyperparameters | +| Framework | PyTorch; DeepEthogram | + +The model predicts motion fields from video frames. It does not classify behaviors. +Hugging Face has no dedicated optical-flow pipeline category in its current task registry; +this repo uses the explicit `optical-flow` tag and DeepEthogram library metadata. + +## Download and load + +Install DeepEthogram and the [Hugging Face CLI](https://huggingface.co/docs/huggingface_hub/guides/cli). +Download only this model into an existing DeepEthogram project's pretrained directory: + +```bash +hf download jbohnslav/deepethogram-tinymotionnet3d --revision v1.0 --local-dir /path/to/project/models/pretrained_models/200313_125331_TinyMotionNet3D_kinetics +``` + +From that run directory, verify the artifacts with `shasum -a 256 -c SHA256SUMS`. +The `checkpoint.pt` and `hyperparameters.yaml` at its root let DeepEthogram discover +this model using its existing project layout. + +To load directly in Python, set `run_dir` to the directory downloaded above: + +```python +from pathlib import Path +import torch +from deepethogram import utils + +from deepethogram.flow_generator.models.TinyMotionNet3D import TinyMotionNet3D +model = TinyMotionNet3D() +run_dir = Path('/path/to/project/models/pretrained_models/200313_125331_TinyMotionNet3D_kinetics') +state_dict, _, _ = utils.load_state_dict_from_file(run_dir / 'checkpoint.pt') +model.load_state_dict(state_dict, strict=True) +model.eval() +with torch.inference_mode(): + output = model(torch.zeros(1, 3, 11, 64, 64)) +``` + +The zero-valued example checks shapes and loading. For real videos use DeepEthogram's +video preprocessing and normalization. Historical settings are in `hyperparameters.yaml`; +`model_config.json` summarizes architecture and input/output conventions. + +## Intended use and limitations + +Historical YAML files contain old filesystem paths and legacy serialization tags; they are +provenance records, not portable project configs. Checkpoints retain their original PyTorch +serialization. Use DeepEthogram, rather than Transformers `AutoModel`, to load them. + +This release contains initialization weights, not a complete trained behavior inference run: +newer inference helpers expect `config.yaml` and associated metrics from your fine-tuned run. +The complete historical plots, metrics, and additional stream snapshots remain in the +[legacy archive](https://huggingface.co/jbohnslav/deepethogram-pretrained). + +## Validation + +`SHA256SUMS` covers the checkpoint, training hyperparameters, license and model configuration. +Validated with PyTorch 2.4.1 and DeepEthogram source revision +`1541eec6958f90673b79c402e10b434c8cb7c868`: strict parameter loading and finite CPU +outputs on the synthetic input above. This is a compatibility check, not an accuracy benchmark. + +## License and citation + +[LICENSE](LICENSE) reproduces the original DeepEthogram Harvard Academic and Non-Commercial +Research Use Software License and Terms of Use unchanged. It restricts commercial use and +end-user redistribution. This mirror grants no additional permissions and does not relicense +the assets; commercial inquiries should go to Harvard OTD as described in the license. + +Bohnslav et al. (2021). *DeepEthogram, a machine learning pipeline for supervised behavior +classification from raw pixels*. eLife 10:e63377. +[doi:10.7554/eLife.63377](https://doi.org/10.7554/eLife.63377). diff --git a/huggingface/models/tinymotionnet3d/SHA256SUMS b/huggingface/models/tinymotionnet3d/SHA256SUMS new file mode 100644 index 0000000..cc48d31 --- /dev/null +++ b/huggingface/models/tinymotionnet3d/SHA256SUMS @@ -0,0 +1,4 @@ +899e15e791552500b93f3ddcc239f570670edd9d9016c05b482ba04cb87415b0 LICENSE +6d84b4bc9536438f6687b75a9883f265b9a138365547fd06168790231662ad00 checkpoint.pt +73041516074c0c5edf51e837361775941d3c1c902852bf82415ec6499e7c6f12 hyperparameters.yaml +7f8f273a28c9cb844a8657856c03ae47581f2b1d3608e0d057264f3f6433b317 model_config.json diff --git a/huggingface/models/tinymotionnet3d/model_config.json b/huggingface/models/tinymotionnet3d/model_config.json new file mode 100644 index 0000000..9b18edc --- /dev/null +++ b/huggingface/models/tinymotionnet3d/model_config.json @@ -0,0 +1,19 @@ +{ + "format_version": 1, + "library_name": "deepethogram", + "model_name": "TinyMotionNet3D", + "architecture": "TinyMotionNet3D", + "task": "optical-flow", + "checkpoint": "checkpoint.pt", + "flow_generator": "TinyMotionNet3D", + "num_rgb_frames": 11, + "input_shape": [ + "N", + 3, + 11, + "H", + "W" + ], + "num_classes": null, + "deepethogram_source_revision": "1541eec6958f90673b79c402e10b434c8cb7c868" +} diff --git a/modal_gpu_test.py b/modal_gpu_test.py index d801cf9..087873a 100644 --- a/modal_gpu_test.py +++ b/modal_gpu_test.py @@ -69,6 +69,7 @@ ) .add_local_file("pytest.ini", remote_path=str(REMOTE_WORKDIR / "pytest.ini")) .add_local_file("setup_tests.py", remote_path=str(REMOTE_WORKDIR / "setup_tests.py")) + .add_local_dir("huggingface", remote_path=str(REMOTE_WORKDIR / "huggingface")) ) @@ -91,7 +92,14 @@ def _prepare_test_data_copy() -> None: # setup_data.py rewrites paths inside project_config.yaml during import, # so the archive must live on writable local storage for the test run. - shutil.copytree(VOLUME_ARCHIVE_PATH, REMOTE_ARCHIVE_DIR) + # Checkpoints come from the published releases, not the volume's older snapshots. + shutil.copytree( + VOLUME_ARCHIVE_PATH, + REMOTE_ARCHIVE_DIR, + ignore=lambda directory, names: ( + {"pretrained_models"} if Path(directory) == VOLUME_ARCHIVE_PATH / "models" else set() + ), + ) def _run_and_stream(command: list[str]) -> tuple[int, str]: @@ -131,6 +139,9 @@ def run_gpu_pytest( extra_pytest_args: str = "", ) -> dict[str, str | int]: _prepare_test_data_copy() + returncode, output = _run_and_stream([str(VENV_BIN_DIR / "python"), "setup_tests.py"]) + if returncode != 0: + raise RuntimeError(f"Test fixture/checkpoint setup failed:\n{output}") REMOTE_JUNIT_PATH.unlink(missing_ok=True) command = [ diff --git a/pyproject.toml b/pyproject.toml index b18b27a..ae2cff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dev = [ "pytest-xvfb", "gdown", "ty>=0.0.1a17", + "huggingface-hub>=0.24", ] [project.scripts] diff --git a/setup_tests.py b/setup_tests.py index d12ba80..b53ab81 100644 --- a/setup_tests.py +++ b/setup_tests.py @@ -1,92 +1,85 @@ -"""This script downloads the test data archive and sets up the testing environment for DeepEthogram. +"""Prepare video fixtures and revision-pinned Hugging Face checkpoints for the tests. -`gdown` is included in the `dev` dependency group, so run `uv sync --dev` first. +Run ``uv sync --dev`` first to install gdown and huggingface_hub. """ -import sys +import hashlib +import json import zipfile from pathlib import Path import gdown -import requests - - -def download_file(url, destination): - """Downloads a file from a URL to a destination with progress indication.""" - response = requests.get(url, stream=True) - total_size = int(response.headers.get("content-length", 0)) - block_size = 8192 - - if total_size == 0: - print("Warning: Content length not provided by server") - - print(f"Downloading to: {destination}") - - with open(destination, "wb") as f: - downloaded = 0 - for data in response.iter_content(block_size): - downloaded += len(data) - f.write(data) - - # Print progress - if total_size > 0: - progress = int(50 * downloaded / total_size) - sys.stdout.write(f"\r[{'=' * progress}{' ' * (50 - progress)}] {downloaded}/{total_size} bytes") - sys.stdout.flush() - print("\nDownload completed!") - - -def setup_tests(): - """Sets up the testing environment for DeepEthogram.""" +from huggingface_hub import snapshot_download + +REPOSITORY_ROOT = Path(__file__).resolve().parent +MODEL_MANIFEST = REPOSITORY_ROOT / "huggingface" / "models.json" +TEST_ARCHIVE = REPOSITORY_ROOT / "tests" / "DATA" / "testing_deepethogram_archive" + + +def setup_pretrained_models(archive_path: Path) -> None: + """Download the published checkpoints at immutable revisions and verify their hashes.""" + manifest = json.loads(MODEL_MANIFEST.read_text()) + for model in manifest["models"]: + destination = archive_path / "models" / "pretrained_models" / model["run_directory"] + print(f"Preparing {model['name']} from {model['repo_id']}@{model['commit']}") + snapshot_download( + repo_id=model["repo_id"], + revision=model["commit"], + local_dir=destination, + allow_patterns=[ + "checkpoint.pt", + "hyperparameters.yaml", + "model_config.json", + "LICENSE", + "SHA256SUMS", + "README.md", + ], + ) + checksums = (MODEL_MANIFEST.parent / "models" / model["slug"] / "SHA256SUMS").read_text() + if (destination / "SHA256SUMS").read_text() != checksums: + raise ValueError(f"Published checksum manifest differs from the checked-in release: {model['repo_id']}") + for line in checksums.splitlines(): + expected, filename = line.split(" ", 1) + digest = hashlib.sha256() + with (destination / filename).open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + actual = digest.hexdigest() + if actual != expected: + raise ValueError(f"Checksum mismatch for {model['name']}/{filename}") + + +def setup_tests() -> bool: + """Set up video fixtures, then always prepare the published pretrained models.""" try: - # Create tests/DATA directory if it doesn't exist - tests_dir = Path("tests") - data_dir = tests_dir / "DATA" + data_dir = TEST_ARCHIVE.parent data_dir.mkdir(parents=True, exist_ok=True) - - # Define paths and requirements - archive_path = data_dir / "testing_deepethogram_archive" zip_path = data_dir / "testing_deepethogram_archive.zip" required_items = ["DATA", "models", "project_config.yaml"] - # Check if test data already exists and is complete - if archive_path.exists(): - missing_items = [item for item in required_items if not (archive_path / item).exists()] - if not missing_items: - print("Test data already exists and appears complete. Skipping download.") - return True - print("Test data exists but is incomplete. Re-downloading...") - - # Download and extract if needed - if not archive_path.exists() or not all((archive_path / item).exists() for item in required_items): - # Download if zip doesn't exist + if all((TEST_ARCHIVE / item).exists() for item in required_items): + print("Video fixtures already exist. Skipping archive download.") + else: if not zip_path.exists(): - print("Downloading test data archive...") + print("Downloading test video archive...") gdown.download(id="1IFz4ABXppVxyuhYik8j38k9-Fl9kYKHo", output=str(zip_path), quiet=False) - - # Extract archive - print("Extracting archive...") + print("Extracting test archive...") with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(data_dir) - - # Clean up zip file after successful extraction zip_path.unlink() - # Final verification - missing_items = [item for item in required_items if not (archive_path / item).exists()] + missing_items = [item for item in required_items if not (TEST_ARCHIVE / item).exists()] if missing_items: - print(f"Warning: The following items are missing: {missing_items}") - return False + raise FileNotFoundError(f"Test archive is missing: {missing_items}") - print("Setup completed successfully!") - print("\nYou can now run the tests using: pytest tests/") - print("Note: The gpu tests will take a few minutes to complete.") + # An existing video fixture directory must not bypass checkpoint updates. + setup_pretrained_models(TEST_ARCHIVE) + print("Test fixtures and published checkpoints are ready. Run: uv run pytest tests/") return True - - except Exception as e: - print(f"Error during setup: {str(e)}") + except Exception as error: + print(f"Error during setup: {error}") return False if __name__ == "__main__": - setup_tests() + raise SystemExit(0 if setup_tests() else 1) diff --git a/tests/test_pretrained_models.py b/tests/test_pretrained_models.py new file mode 100644 index 0000000..154d71d --- /dev/null +++ b/tests/test_pretrained_models.py @@ -0,0 +1,81 @@ +"""Check the published checkpoints installed by setup_tests.py, rather than synthetic weights.""" + +import hashlib +import json +from pathlib import Path + +import pytest +import torch + +from deepethogram import projects, utils +from deepethogram.feature_extractor.models.hidden_two_stream import deg_f, deg_m, deg_s +from deepethogram.flow_generator.models.MotionNet import MotionNet +from deepethogram.flow_generator.models.TinyMotionNet import TinyMotionNet +from deepethogram.flow_generator.models.TinyMotionNet3D import TinyMotionNet3D +from setup_data import archive_path + +MODEL_RELEASES = Path(__file__).resolve().parents[1] / "huggingface" +MODELS = json.loads((MODEL_RELEASES / "models.json").read_text())["models"] +PRETRAINED = Path(archive_path) / "models" / "pretrained_models" +CONSTRUCTORS = { + "tinymotionnet": TinyMotionNet, + "motionnet": MotionNet, + "tinymotionnet3d": TinyMotionNet3D, + "deg-f": lambda: deg_f(num_classes=700, reload_imagenet=False), + "deg-m": lambda: deg_m(num_classes=700, reload_imagenet=False), + "deg-s": lambda: deg_s(num_classes=700, reload_imagenet=False), +} + + +@pytest.fixture(scope="module", autouse=True) +def cpu_threads(): + previous = torch.get_num_threads() + torch.set_num_threads(min(previous, 4)) + yield + torch.set_num_threads(previous) + + +@pytest.mark.parametrize("release", MODELS, ids=lambda release: release["name"]) +def test_published_checkpoint(release): + directory = PRETRAINED / release["run_directory"] + assert directory.is_dir(), "Run uv run python setup_tests.py before testing published checkpoints" + checksums = (MODEL_RELEASES / "models" / release["slug"] / "SHA256SUMS").read_text() + assert (directory / "SHA256SUMS").read_text() == checksums + for line in checksums.splitlines(): + expected, filename = line.split(" ", 1) + digest = hashlib.sha256() + with (directory / filename).open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + assert digest.hexdigest() == expected, f"Wrong published artifact: {release['name']}/{filename}" + + config = json.loads((directory / "model_config.json").read_text()) + shape = [ + 1 if dimension == "N" else 64 if dimension in ("H", "W") else dimension for dimension in config["input_shape"] + ] + state, _, _ = utils.load_state_dict_from_file(directory / "checkpoint.pt") + model = CONSTRUCTORS[release["slug"]]() + model.load_state_dict(state, strict=True) + model.eval() + with torch.inference_mode(): + outputs = model(torch.randn(*shape)) + + if release["flow_generator"]: + assert outputs.shape == (1, 700) + assert torch.isfinite(outputs).all() + else: + assert len(outputs) == 3 + for output, size in zip(outputs, (32, 16, 8)): + expected_shape = (1, 2, 10, size, size) if release["slug"] == "tinymotionnet3d" else (1, 20, size, size) + assert output.shape == expected_shape + assert torch.isfinite(output).all() + + +def test_published_model_discovery(): + discovered = projects.get_weights_from_model_path(PRETRAINED) + assert set(discovered["flow_generator"]) == {"TinyMotionNet", "MotionNet", "TinyMotionNet3D"} + assert set(discovered["feature_extractor"]) == {"resnet18", "resnet50", "resnet3d_34"} + for release in MODELS: + model_type = "feature_extractor" if release["flow_generator"] else "flow_generator" + expected = str(PRETRAINED / release["run_directory"] / "checkpoint.pt") + assert discovered[model_type][release["architecture"]] == [expected] diff --git a/uv.lock b/uv.lock index 2c9261d..40a852e 100644 --- a/uv.lock +++ b/uv.lock @@ -111,12 +111,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617, upload-time = "2022-11-08T16:03:57.483Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + [[package]] name = "antlr4-python3-runtime" version = "4.9.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "exceptiongroup" }, + { name = "idna", version = "3.10", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna", version = "3.20", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -226,6 +282,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -359,6 +455,8 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "gdown" }, + { name = "huggingface-hub", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "huggingface-hub", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -392,6 +490,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "gdown" }, + { name = "huggingface-hub", specifier = ">=0.24" }, { name = "pre-commit", specifier = ">=2.20.0,<3.0.0" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -415,7 +514,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -609,6 +708,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/80/1aa2ba791207a13e314067209b48e1a0893ed8d1f43ef012e194aaa6c2de/grpcio-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:a31d7e3b529c94e930a117b2175b2efd179d96eb3c7a21ccb0289a8ab05b645c", size = 4303506, upload-time = "2025-01-23T17:56:03.842Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "h5py" version = "3.14.0" @@ -623,7 +731,7 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" } wheels = [ @@ -659,7 +767,7 @@ resolution-markers = [ "python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ @@ -680,6 +788,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" }, ] +[[package]] +name = "hf-xet" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/27/06d899ea7bd721d272f84aac98bdb238de98af4cc767a69056d967d68c71/hf_xet-1.7.0.tar.gz", hash = "sha256:d406ec79053c0871817f700c2ac8c36ba0d87f9c34b7458b0f0063bb218b0466", size = 985689, upload-time = "2026-10-06T20:18:43.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/0b/b03be21ffaada749ba0d3197d8aefbf1aa698bac149580421c15239b299e/hf_xet-1.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e3e88a7a75d7d95cbee1f37dc31341d6201124cf21c6c4b1dfab8ccba9b09e0f", size = 3796096, upload-time = "2026-10-06T20:18:28.43Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/a26ebdce7056a61e931f228439bc0ab08cbec239d1690f965e5e637cba79/hf_xet-1.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:59fba37039233c7fcbe196817d6cdcf1b40dfb17b410f229d85b0cf0a1848da4", size = 3560352, upload-time = "2026-10-06T20:18:30.365Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4c/2bf3b66c215d409655f28de1622393dde04c9461280d48c7924bb3b2decd/hf_xet-1.7.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2814a6e999d13464c4d679b788cc5d784eb5a4edfc638a31f10e9a11ab531ef8", size = 4212180, upload-time = "2026-10-06T20:18:32.292Z" }, + { url = "https://files.pythonhosted.org/packages/49/0c/a2f703a5a78267556e89e03316fa0805c86b72b50829bc67665746e8ebf0/hf_xet-1.7.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fcfd6c22418e57dd5b3aea649e813b2e2cfb2aebf317b210d90f1fe4b3018b52", size = 3990011, upload-time = "2026-10-06T20:18:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/a4/77/e52e4201b1cbf571530a61cc57f70182045a39a230089ee5f1df182a4de2/hf_xet-1.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80f79dae613ce9e0ea1fd1ae15616ca9ac74aed4c770aabc199c4f03ebecc863", size = 4190628, upload-time = "2026-10-06T20:18:36.062Z" }, + { url = "https://files.pythonhosted.org/packages/6c/dc/03a21b89f118664a0926ff25b0f8e44a519bf22724a6a8fc7a9abbc188b6/hf_xet-1.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0a9e802f33bf50c851abe45fc5380e61f959e2d369647d6742b79ad9d6c27cab", size = 4418814, upload-time = "2026-10-06T20:18:37.888Z" }, + { url = "https://files.pythonhosted.org/packages/4d/59/b35106dfa71b6eef605dc88bd038fe99c7f86fb132a15b60d0bf2f235b2c/hf_xet-1.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:2b7bb5727889b0f2436dbaaad8fc4c3e66b8240d992716989e0c086b4278b1bc", size = 3822644, upload-time = "2026-10-06T20:18:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/48/cd/072313585f74fe9d441e2eb5e0a4703c30586cd709810ea369675f61b74e/hf_xet-1.7.0-cp38-abi3-win_arm64.whl", hash = "sha256:acc3851cf2576a8fb2ae926da863f4efabe21303cf292e9a44332802ab0dcc6a", size = 3662436, upload-time = "2026-10-06T20:18:42.205Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpcore2" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ff/b53af1b49bb2cb5fa97c92215614b173ca5c1a99ded80e2d52f15a94fa6d/httpcore2-2.14.0.tar.gz", hash = "sha256:4f57a1a8249fe6af589d935cfd456f1d13bbdcb0f0fc72af9142368fc94811d7", size = 69096, upload-time = "2026-10-18T16:29:21.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/06/a8d6a8f90d11b76d9086242b6bb0ee4b503473bfc8be34c2d5b067275c22/httpcore2-2.14.0-py3-none-any.whl", hash = "sha256:4c4f64f4b544afeb778c42c0ffb5941b29e671157e90ed3f6dc787c4bca90cd1", size = 84291, upload-time = "2026-10-18T16:29:19.743Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" } }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna", version = "3.10", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx2" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.14.2", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "idna", version = "3.20", source = { registry = "https://pypi.org/simple" } }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/67/58353f1848b4ba568e1ec22ea1e0d8e039f0bca5fde7e2d1f07739fd83e7/httpx2-2.14.0.tar.gz", hash = "sha256:47ad61a3c40282cd8d0dc96311531f00f5bfc71a066c6fe3b36d8f63fb9e4f6e", size = 102036, upload-time = "2026-10-18T16:29:22.627Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/5a/469616aa8c2cbb0bb9dd67a3c18d07c01ab604cc0b236ee1c47d260c7f6e/httpx2-2.14.0-py3-none-any.whl", hash = "sha256:9d20c13a10bae1773a0be3b0f1e5beeb4bdd549bc3a2ba18efa32897c65403e4", size = 96901, upload-time = "2026-10-18T16:29:20.819Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/2a/a847fd02261cd051da218baf99f90ee7c7040c109a01833db4f838f25256/huggingface_hub-1.8.0.tar.gz", hash = "sha256:c5627b2fd521e00caf8eff4ac965ba988ea75167fad7ee72e17f9b7183ec63f3", size = 735839, upload-time = "2026-03-25T16:01:28.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ae/8a3a16ea4d202cb641b51d2681bdd3d482c1c592d7570b3fa264730829ce/huggingface_hub-1.8.0-py3-none-any.whl", hash = "sha256:d3eb5047bd4e33c987429de6020d4810d38a5bef95b3b40df9b17346b7f353f2", size = 625208, upload-time = "2026-03-25T16:01:26.603Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" } }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'ARM64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/47/6858d63643e66fb4f6585c3cfd4029c0b2bc1ae21688cee9b3335f20a10d/huggingface_hub-2.2.0.tar.gz", hash = "sha256:5d1b47537394e4215cb858aa12fd493d0f7ef7f58990f5dcd24bc173107b2871", size = 1041026, upload-time = "2026-10-08T15:30:59.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/b0/0f7b430fd100b3a3b037fdbb314878200241082e607b3383c63d91a13a72/huggingface_hub-2.2.0-py3-none-any.whl", hash = "sha256:1667f145dc56dc210d60966069397df9ecfca9607a5d43db88b308c89dae56b3", size = 839884, upload-time = "2026-10-08T15:30:57.914Z" }, +] + [[package]] name = "identify" version = "2.6.1" @@ -693,17 +934,45 @@ wheels = [ name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", +] sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + [[package]] name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ @@ -715,7 +984,7 @@ name = "importlib-resources" version = "6.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/be/f3e8c6081b684f176b761e6a2fef02a0be939740ed6f54109a2951d806f3/importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", size = 43372, upload-time = "2024-09-09T17:03:14.677Z" } wheels = [ @@ -886,6 +1155,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + [[package]] name = "markupsafe" version = "2.1.5" @@ -972,6 +1253,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/e0/03eba0a8c3775ef910dbb3a287114a64c47abbcaeab2543c59957f155a86/matplotlib-3.7.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e530ab6a0afd082d2e9c17eb1eb064a63c5b09bb607b2b74fa41adbe3e162286", size = 7521729, upload-time = "2024-02-16T10:50:50.063Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -1119,7 +1409,7 @@ name = "nvidia-cudnn-cu12" version = "9.1.0.70" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" }, @@ -1146,9 +1436,9 @@ name = "nvidia-cusolver-cu12" version = "11.6.0.99" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux')" }, - { name = "nvidia-cusparse-cu12", marker = "(python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/b3/6f/692057afbe7cf0a17b7bf26065b833d45a0a1b2eaa51b43deb07295b8a83/nvidia_cusolver_cu12-11.6.0.99-py3-none-manylinux2014_x86_64.whl", hash = "sha256:30693d3865ae3f5a6aacf46e42549fb2d1fbb15b0189bc14fc0b2894e173769f", size = 128402308, upload-time = "2024-03-06T01:15:51.544Z" }, @@ -1159,7 +1449,7 @@ name = "nvidia-cusparse-cu12" version = "12.3.0.142" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/89/f7a823867068550f8908c469a66bb2dc8ea60ddcdc7f45b99a919a7a836e/nvidia_cusparse_cu12-12.3.0.142-py3-none-manylinux2014_x86_64.whl", hash = "sha256:364106d1d9825901dc70fb7d19ef6fdd0f534627b824abfea494abe319c1402a", size = 207454570, upload-time = "2024-03-06T01:16:17.757Z" }, @@ -1473,6 +1763,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/9c/173f3cf770e66f3c9592318806aebb8617ba405d6d4c09493dabea75985c/pyDeprecate-0.3.2-py3-none-any.whl", hash = "sha256:ed86b68ed837e6465245904a3de2f59bf9eef78ac7a2502ee280533d04802457", size = 10400, upload-time = "2021-06-11T09:33:40.149Z" }, ] +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + [[package]] name = "pyparsing" version = "3.1.4" @@ -1690,7 +1989,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, - { name = "idna" }, + { name = "idna", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "idna", version = "3.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } @@ -1716,6 +2016,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "rsa" version = "4.9.1" @@ -1817,6 +2130,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "shiboken6" version = "6.6.3.1" @@ -1928,17 +2250,17 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "filelock", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "fsspec", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "jinja2", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "networkx", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "sympy", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "sympy" }, + { name = "typing-extensions" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.4.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d36a8ef100f5bff3e9c3cea934b9e0d7ea277cb8210c7152d34a9a6c5830eadd" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.4.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:ddddbd8b066e743934a4200b3d54267a46db02106876d21cf31f7da7a96f98ea" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.4.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:a38de2803ee6050309aac032676536c3d3b6a9804248537e38e098d0e14817ec" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.4.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d36a8ef100f5bff3e9c3cea934b9e0d7ea277cb8210c7152d34a9a6c5830eadd", upload-time = "2024-09-06T21:25:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.4.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:ddddbd8b066e743934a4200b3d54267a46db02106876d21cf31f7da7a96f98ea", upload-time = "2024-09-06T21:25:43Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.4.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:a38de2803ee6050309aac032676536c3d3b6a9804248537e38e098d0e14817ec", upload-time = "2024-09-06T21:26:05Z" }, ] [[package]] @@ -1951,17 +2273,17 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", ] dependencies = [ - { name = "filelock", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "fsspec", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "jinja2", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "networkx", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "sympy", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "sympy" }, + { name = "typing-extensions" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cu124/torch-2.4.1-cp310-cp310-linux_aarch64.whl", hash = "sha256:baa065a4fb7805c78f16841cfc4f3fc3c6823d1de726087e583c68abe553dad7" }, - { url = "https://download.pytorch.org/whl/cu124/torch-2.4.1-cp311-cp311-linux_aarch64.whl", hash = "sha256:2a2eaded7637aae3e4ed783a1539ecb06edf4a9ea9465628b5bd2c68b3b5e9da" }, - { url = "https://download.pytorch.org/whl/cu124/torch-2.4.1-cp39-cp39-linux_aarch64.whl", hash = "sha256:f643b222e0cea34dd5181a4e9b5f8b4ef51d2b2b19cf3ccf6b4790d89e4ecff1" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.4.1-cp310-cp310-linux_aarch64.whl", hash = "sha256:baa065a4fb7805c78f16841cfc4f3fc3c6823d1de726087e583c68abe553dad7", upload-time = "2024-09-06T22:11:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.4.1-cp311-cp311-linux_aarch64.whl", hash = "sha256:2a2eaded7637aae3e4ed783a1539ecb06edf4a9ea9465628b5bd2c68b3b5e9da", upload-time = "2024-09-06T22:13:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.4.1-cp39-cp39-linux_aarch64.whl", hash = "sha256:f643b222e0cea34dd5181a4e9b5f8b4ef51d2b2b19cf3ccf6b4790d89e4ecff1", upload-time = "2024-09-06T22:18:48Z" }, ] [[package]] @@ -1976,17 +2298,17 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'linux'", ] dependencies = [ - { name = "filelock", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "fsspec", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "jinja2", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "networkx", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "sympy", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "sympy" }, + { name = "typing-extensions" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.4.1%2Bcpu-cp310-cp310-win_amd64.whl", hash = "sha256:1dd062d296fb78aa7cfab8690bf03704995a821b5ef69cfc807af5c0831b4202" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.4.1%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:76a6fe7b10491b650c630bc9ae328df40f79a948296b41d3b087b29a8a63cbad" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.4.1%2Bcpu-cp39-cp39-win_amd64.whl", hash = "sha256:c4f2c3c026e876d4dad7629170ec14fff48c076d6c2ae0e354ab3fdc09024f00" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.4.1%2Bcpu-cp310-cp310-win_amd64.whl", hash = "sha256:1dd062d296fb78aa7cfab8690bf03704995a821b5ef69cfc807af5c0831b4202", upload-time = "2024-09-06T21:24:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.4.1%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:76a6fe7b10491b650c630bc9ae328df40f79a948296b41d3b087b29a8a63cbad", upload-time = "2024-09-06T21:24:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.4.1%2Bcpu-cp39-cp39-win_amd64.whl", hash = "sha256:c4f2c3c026e876d4dad7629170ec14fff48c076d6c2ae0e354ab3fdc09024f00", upload-time = "2024-09-06T21:25:24Z" }, ] [[package]] @@ -2000,30 +2322,30 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", ] dependencies = [ - { name = "filelock", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "fsspec", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "jinja2", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "networkx", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "sympy", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cu124/torch-2.4.1%2Bcu124-cp310-cp310-linux_x86_64.whl", hash = "sha256:464cb998fa46d34317b10811dad88486aa8aeb96ebbcefc3ed3f00ddf6c3249b" }, - { url = "https://download.pytorch.org/whl/cu124/torch-2.4.1%2Bcu124-cp311-cp311-linux_x86_64.whl", hash = "sha256:16e4ef3b32b45a278a0c512723f81cfa57035ebd5a75dbc2fb1360197ae06acd" }, - { url = "https://download.pytorch.org/whl/cu124/torch-2.4.1%2Bcu124-cp39-cp39-linux_x86_64.whl", hash = "sha256:eb02dde23157dfce5cb1e6b72eddec32f9ebbdd54ab5a9928bd893cf0250fe98" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.4.1%2Bcu124-cp310-cp310-linux_x86_64.whl", hash = "sha256:464cb998fa46d34317b10811dad88486aa8aeb96ebbcefc3ed3f00ddf6c3249b", upload-time = "2024-09-06T21:58:49Z" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.4.1%2Bcu124-cp311-cp311-linux_x86_64.whl", hash = "sha256:16e4ef3b32b45a278a0c512723f81cfa57035ebd5a75dbc2fb1360197ae06acd", upload-time = "2024-09-06T22:01:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.4.1%2Bcu124-cp39-cp39-linux_x86_64.whl", hash = "sha256:eb02dde23157dfce5cb1e6b72eddec32f9ebbdd54ab5a9928bd893cf0250fe98", upload-time = "2024-09-06T22:08:50Z" }, ] [[package]] @@ -2061,7 +2383,7 @@ name = "triton" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", marker = "(python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux')" }, + { name = "filelock" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/45/27/14cc3101409b9b4b9241d2ba7deaa93535a217a211c86c4cc7151fb12181/triton-3.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1efef76935b2febc365bfadf74bcb65a6f959a9872e5bddf44cc9e0adce1e1a", size = 209376304, upload-time = "2024-07-19T20:56:41.483Z" }, @@ -2069,6 +2391,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/bf/55cccf57c14787ad81ee827526ddd48fd0aff0291fcc7b8c2e2bdf28da0a/triton-3.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6e5727202f7078c56f91ff13ad0c1abab14a0e7f2c87e91b12b6f64f3e8ae609", size = 209377082, upload-time = "2024-07-19T20:57:41.314Z" }, ] +[[package]] +name = "truststore" +version = "0.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd", size = 28091, upload-time = "2026-10-12T22:27:31.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c", size = 19017, upload-time = "2026-10-12T22:27:30.377Z" }, +] + [[package]] name = "ty" version = "0.0.1a17" @@ -2094,6 +2425,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/c6/207bbc2f3bb71df4b1aeabe8e9c31a1cd22c72aff0ab9c1a832b9ae54f6e/ty-0.0.1a17-py3-none-win_arm64.whl", hash = "sha256:636eacc1dceaf09325415a70a03cd57eae53e5c7f281813aaa943a698a45cddb", size = 7782847, upload-time = "2025-08-06T12:13:54.243Z" }, ] +[[package]] +name = "typer" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" } }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/93d16574e66dfe4c2284ffdaca4b0320ade32858cb2cc586c8dd79f127c5/typer-0.23.2.tar.gz", hash = "sha256:a99706a08e54f1aef8bb6a8611503808188a4092808e86addff1828a208af0de", size = 120162, upload-time = "2026-02-16T18:52:40.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2c/dee705c427875402200fe779eb8a3c00ccb349471172c41178336e9599cc/typer-0.23.2-py3-none-any.whl", hash = "sha256:e9c8dc380f82450b3c851a9b9d5a0edf95d1d6456ae70c517d8b06a50c7a9978", size = 56834, upload-time = "2026-02-16T18:52:39.308Z" }, +] + [[package]] name = "typing-extensions" version = "4.13.2" @@ -2176,7 +2522,8 @@ name = "yarl" version = "1.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, + { name = "idna", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "idna", version = "3.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "multidict" }, { name = "propcache" }, ] From 31d037e1ad87a5b23f93de2ee0cacca50a203e62 Mon Sep 17 00:00:00 2001 From: Jim Bohnslav Date: Mon, 19 Oct 2026 14:12:04 -0400 Subject: [PATCH 2/5] Harden checkpoint setup, Docker test images and auto review - Rebuild pretrained_models from a separate verified download cache, so stale runs or Lightning checkpoints cannot shadow the pinned weights. - Ship huggingface/ in the Docker test images; a missing manifest now fails one test instead of aborting collection. - Check each v1.0 tag resolves to the pinned commit, share the checksum helper, classify models by task, and align models.json flow_generator with model_config.json. - Cache Hugging Face checkpoints in CPU CI. - Fix flow-model card tags and grammar; update testing and release docs. - Give the review workflow full history and the plugin's tools. - Add network-free regression tests in tests/test_setup_tests.py. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 2 + .github/workflows/claude-code-review.yml | 6 +- .github/workflows/main.yml | 7 + .gitignore | 1 + docker/Dockerfile | 3 + docs/testing.md | 11 +- huggingface/RELEASING.md | 15 +- huggingface/models.json | 6 +- huggingface/models/motionnet/README.md | 3 +- huggingface/models/tinymotionnet/README.md | 3 +- huggingface/models/tinymotionnet3d/README.md | 3 +- pytest.ini | 2 + setup_tests.py | 161 +++++++++---- tests/test_pretrained_models.py | 51 +++-- tests/test_setup_tests.py | 225 +++++++++++++++++++ 15 files changed, 419 insertions(+), 80 deletions(-) create mode 100644 tests/test_setup_tests.py diff --git a/.dockerignore b/.dockerignore index 349597a..6612a51 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,8 @@ !uv.lock !pytest.ini !setup_tests.py +!huggingface/ +!huggingface/** !deepethogram/ !deepethogram/** !tests/ diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b7c5097..57284f8 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -29,7 +29,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - fetch-depth: 1 + # Full history so the review can diff the PR against its merge base. + fetch-depth: 0 - name: Run Claude Code Review id: claude-review @@ -41,4 +42,5 @@ jobs: prompt: '/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' use_sticky_comment: true track_progress: true - claude_args: '--allowedTools "Bash(gh pr:*),Bash(gh api:*),Bash(gh diff:*)"' + # Covers the code-review plugin's own allowed-tools, plus read-only git for diffs and history. + claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Bash(gh api:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git merge-base:*),Bash(git blame:*)"' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 350db9f..e26613e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,6 +47,13 @@ jobs: - name: Install dependencies run: uv sync --locked --dev + # About 1.2 GB of checkpoints; setup_tests.py re-verifies the cached files against the checked-in checksums. + - name: Cache pinned Hugging Face checkpoints + uses: actions/cache@v6 + with: + path: tests/DATA/huggingface_checkpoints + key: hf-checkpoints-${{ runner.os }}-${{ hashFiles('huggingface/models.json') }} + - name: Setup video fixtures and pinned Hugging Face checkpoints run: uv run python setup_tests.py diff --git a/.gitignore b/.gitignore index c96fcdc..7cf46e9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ deepethogram/models/* tests/DATA/testing_deepethogram tests/DATA/testing_deepethogram_archive tests/DATA/testing_deepethogram_archive.zip +tests/DATA/huggingface_checkpoints gui_logs/ .vscode/ # line profiling diff --git a/docker/Dockerfile b/docker/Dockerfile index 184a805..f957cdb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -201,6 +201,7 @@ CMD ["python", "-m", "deepethogram"] FROM builder-gui AS test-gui ENV DEG_VERSION=gui COPY pytest.ini setup_tests.py ./ +COPY huggingface ./huggingface COPY tests ./tests RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ @@ -215,6 +216,7 @@ CMD ["sh", "-lc", "python setup_tests.py && xvfb-run --auto-servernum --server-a FROM builder-headless AS test-headless COPY pytest.ini setup_tests.py ./ +COPY huggingface ./huggingface COPY tests ./tests RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ uv sync --locked --no-editable @@ -222,6 +224,7 @@ CMD ["pytest", "-v"] FROM builder-full AS test-full COPY pytest.ini setup_tests.py ./ +COPY huggingface ./huggingface COPY tests ./tests RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ uv sync --locked --no-editable diff --git a/docs/testing.md b/docs/testing.md index 0fbf42b..e42c42c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -34,12 +34,13 @@ uv run pytest -m "" ### Test Data Setup -Before running tests: +`uv run python setup_tests.py` prepares everything the tests need: -1. Download [`testing_deepethogram_archive.zip`](https://drive.google.com/file/d/1IFz4ABXppVxyuhYik8j38k9-Fl9kYKHo/view?usp=sharing) -2. Create a directory called `DATA` in the tests directory -3. Unzip the archive and move its contents to `deepethogram/tests/DATA/testing_deepethogram_archive/` -4. Verify the path structure: `deepethogram/tests/DATA/testing_deepethogram_archive/{DATA,models,project_config.yaml}` +1. It downloads the video fixture archive from Google Drive into `tests/DATA/testing_deepethogram_archive/`, unless that folder already has `DATA`, `models` and `project_config.yaml`. +2. It downloads the six published checkpoints from Hugging Face at the commits pinned in `huggingface/models.json`, into `tests/DATA/huggingface_checkpoints/`. It checks each `v1.0` tag resolves to its pinned commit, and verifies every file against the checksums in `huggingface/models//SHA256SUMS`. +3. It rebuilds `tests/DATA/testing_deepethogram_archive/models/pretrained_models/` so it holds only those six runs. + +Re-run it after pulling changes to `huggingface/models.json`. Later runs reuse the downloads. Don't unzip the archive by hand: its bundled checkpoints are older snapshots without the release manifests, and `tests/test_pretrained_models.py` fails on them. ## Writing Tests diff --git a/huggingface/RELEASING.md b/huggingface/RELEASING.md index 72f025b..3c01fe0 100644 --- a/huggingface/RELEASING.md +++ b/huggingface/RELEASING.md @@ -40,6 +40,7 @@ Read back Hub metadata to verify the model's task/library tags. Only then tag th The initial per-model release tag is `v1.0`. Model cards and user documentation pin it. When updating weights, create a new tag rather than moving an existing release tag. +Card-only fixes (`README.md`, which is not in `SHA256SUMS`) can go straight to `main`. They don't need a new tag, and `models.json` keeps pinning the verified release commit. ## Collection and legacy archive @@ -55,9 +56,13 @@ release provides the complete checkpoint from the existing test fixture archive. ## CI checkpoint contract -`setup_tests.py` reads `models.json`, downloads every model at its immutable `commit`, and verifies -all files against the checked-in per-model checksum manifests. Existing fixture folders still run -this step. CPU CI invokes the script directly; the Modal GPU runner invokes it after copying only -its video fixtures from the persistent test-data volume. `tests/test_pretrained_models.py` checks -artifact identity, strict architecture matching, finite forward outputs and discovery for all models. +`setup_tests.py` reads `models.json`. For every model it checks that the release tag resolves to the pinned +`commit`, downloads that commit into `tests/DATA/huggingface_checkpoints/`, and verifies all files against the +checked-in per-model checksum manifest. It then rebuilds the archive's `pretrained_models` directory with only +these runs, so stale folders cannot shadow the pinned weights. Existing fixture folders still run this step. +CPU CI caches the download directory, keyed on `models.json`; the Modal GPU runner copies only its video +fixtures from the persistent test-data volume and downloads the checkpoints fresh. +`tests/test_pretrained_models.py` checks artifact identity, strict architecture matching, finite forward outputs +and discovery for all models. `tests/test_setup_tests.py` checks that `models.json` agrees with each +`model_config.json` and that the Docker test images ship `huggingface/`. When releasing new weights, update commits/checksums together and run setup plus the CPU suite. diff --git a/huggingface/models.json b/huggingface/models.json index 54e0632..73e1ec5 100644 --- a/huggingface/models.json +++ b/huggingface/models.json @@ -8,7 +8,7 @@ "repo_id": "jbohnslav/deepethogram-tinymotionnet", "architecture": "TinyMotionNet", "task": "optical-flow", - "flow_generator": null, + "flow_generator": "TinyMotionNet", "checkpoint_bytes": 23450307, "commit": "b481cbc41b96db5de248dc533e39ac6f979db33b", "verification": { @@ -43,7 +43,7 @@ "repo_id": "jbohnslav/deepethogram-motionnet", "architecture": "MotionNet", "task": "optical-flow", - "flow_generator": null, + "flow_generator": "MotionNet", "checkpoint_bytes": 183604611, "commit": "f282e2dae1dc15ce3ef45ce99f4c202b08ce7248", "verification": { @@ -78,7 +78,7 @@ "repo_id": "jbohnslav/deepethogram-tinymotionnet3d", "architecture": "TinyMotionNet3D", "task": "optical-flow", - "flow_generator": null, + "flow_generator": "TinyMotionNet3D", "checkpoint_bytes": 1769548, "commit": "e68a968956658727394d7a8e1cc3b8763b7d60b6", "verification": { diff --git a/huggingface/models/motionnet/README.md b/huggingface/models/motionnet/README.md index 491f2f4..da21f9c 100644 --- a/huggingface/models/motionnet/README.md +++ b/huggingface/models/motionnet/README.md @@ -10,7 +10,6 @@ tags: - pytorch - optical-flow - motionnet -- motionnet - kinetics700 - animal-behavior - pretrained @@ -18,7 +17,7 @@ tags: # DeepEthogram MotionNet -MotionNet is a optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +MotionNet is an optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). This repository contains this model's checkpoint and historical training hyperparameters. See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. diff --git a/huggingface/models/tinymotionnet/README.md b/huggingface/models/tinymotionnet/README.md index e8f3ce0..9968552 100644 --- a/huggingface/models/tinymotionnet/README.md +++ b/huggingface/models/tinymotionnet/README.md @@ -10,7 +10,6 @@ tags: - pytorch - optical-flow - tinymotionnet -- tinymotionnet - kinetics700 - animal-behavior - pretrained @@ -18,7 +17,7 @@ tags: # DeepEthogram TinyMotionNet -TinyMotionNet is a optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +TinyMotionNet is an optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). This repository contains this model's checkpoint and historical training hyperparameters. See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. diff --git a/huggingface/models/tinymotionnet3d/README.md b/huggingface/models/tinymotionnet3d/README.md index 71d6f9c..6cfe418 100644 --- a/huggingface/models/tinymotionnet3d/README.md +++ b/huggingface/models/tinymotionnet3d/README.md @@ -10,7 +10,6 @@ tags: - pytorch - optical-flow - tinymotionnet3d -- tinymotionnet3d - kinetics700 - animal-behavior - pretrained @@ -18,7 +17,7 @@ tags: # DeepEthogram TinyMotionNet3D -TinyMotionNet3D is a optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). +TinyMotionNet3D is an optical-flow generator pretrained on Kinetics700 videos for [DeepEthogram](https://github.com/jbohnslav/deepethogram). This repository contains this model's checkpoint and historical training hyperparameters. See the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec) for the other models. diff --git a/pytest.ini b/pytest.ini index a9ed2cb..ab716f2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,7 @@ [pytest] testpaths = tests +# setup_tests.py lives at the repository root and is imported by the tests. +pythonpath = . filterwarnings = ignore::DeprecationWarning:pkg_resources.*: diff --git a/setup_tests.py b/setup_tests.py index b53ab81..b96040e 100644 --- a/setup_tests.py +++ b/setup_tests.py @@ -3,58 +3,142 @@ Run ``uv sync --dev`` first to install gdown and huggingface_hub. """ +from __future__ import annotations + import hashlib import json +import os +import shutil +import traceback import zipfile from pathlib import Path -import gdown -from huggingface_hub import snapshot_download - REPOSITORY_ROOT = Path(__file__).resolve().parent -MODEL_MANIFEST = REPOSITORY_ROOT / "huggingface" / "models.json" -TEST_ARCHIVE = REPOSITORY_ROOT / "tests" / "DATA" / "testing_deepethogram_archive" +RELEASES_DIR = REPOSITORY_ROOT / "huggingface" +MODEL_MANIFEST = RELEASES_DIR / "models.json" +TEST_DATA = REPOSITORY_ROOT / "tests" / "DATA" +TEST_ARCHIVE = TEST_DATA / "testing_deepethogram_archive" +# Downloads live outside the archive, so CI can cache them and re-extracting the archive cannot clobber them. +CHECKPOINT_CACHE = TEST_DATA / "huggingface_checkpoints" +RELEASE_FILES = ("checkpoint.pt", "hyperparameters.yaml", "model_config.json", "LICENSE", "SHA256SUMS", "README.md") + + +def load_release_manifest(path: Path | None = None) -> dict: + path = path or MODEL_MANIFEST + if not path.is_file(): + raise FileNotFoundError( + f"Model release manifest not found: {path}. It ships with the repository in huggingface/; " + "copy that directory next to setup_tests.py." + ) + return json.loads(path.read_text()) + + +def checksum_manifest_path(model: dict, releases_dir: Path | None = None) -> Path: + return (releases_dir or RELEASES_DIR) / "models" / model["slug"] / "SHA256SUMS" + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def verify_release_files(directory: Path, manifest_path: Path, skip_hash: tuple[str, ...] = ()) -> None: + """Raise ValueError unless ``directory`` holds every file in ``manifest_path`` with a matching SHA-256.""" + expected_manifest = manifest_path.read_text() + published_manifest = directory / "SHA256SUMS" + if not published_manifest.is_file(): + raise ValueError(f"{directory} has no SHA256SUMS") + if published_manifest.read_text() != expected_manifest: + raise ValueError(f"{published_manifest} differs from the checked-in release manifest {manifest_path}") + for line in expected_manifest.splitlines(): + expected, filename = line.split(" ", 1) + path = directory / filename + if not path.is_file(): + raise ValueError(f"{directory} is missing {filename}") + if filename in skip_hash: + continue + actual = sha256_file(path) + if actual != expected: + raise ValueError(f"Checksum mismatch for {path}: expected SHA-256 {expected}, got {actual}") + + +def check_release_tag(api, model: dict, tag: str) -> None: + """The docs tell users to download ``tag``, so it must resolve to the commit CI verifies.""" + resolved = api.model_info(model["repo_id"], revision=tag).sha + if resolved != model["commit"]: + raise ValueError( + f"{model['repo_id']}@{tag} resolves to {resolved}, but models.json pins {model['commit']}. " + "Docs and CI would use different weights." + ) + + +def download_release(model: dict, destination: Path) -> None: + from huggingface_hub import snapshot_download + + snapshot_download( + repo_id=model["repo_id"], + revision=model["commit"], + local_dir=destination, + allow_patterns=list(RELEASE_FILES), + ) + + +def install_release(source: Path, destination: Path) -> None: + """Make ``destination`` contain exactly the release files, hard-linked from the download cache when possible.""" + destination.mkdir(parents=True) + for filename in RELEASE_FILES: + if not (source / filename).is_file(): + continue + try: + os.link(source / filename, destination / filename) + except OSError: + shutil.copy2(source / filename, destination / filename) + +def setup_pretrained_models( + archive_path: Path, + releases_dir: Path | None = None, + cache_dir: Path | None = None, + api=None, + download=download_release, +) -> None: + """Install the published checkpoints, at their pinned commits, as the archive's only pretrained models. -def setup_pretrained_models(archive_path: Path) -> None: - """Download the published checkpoints at immutable revisions and verify their hashes.""" - manifest = json.loads(MODEL_MANIFEST.read_text()) + The whole pretrained_models directory is rebuilt, so stale run folders or Lightning checkpoints from the + archive or earlier runs cannot shadow the pinned weights during discovery. + """ + releases_dir = releases_dir or RELEASES_DIR + cache_dir = cache_dir or CHECKPOINT_CACHE + if api is None: + from huggingface_hub import HfApi + + api = HfApi() + + manifest = load_release_manifest(releases_dir / "models.json") for model in manifest["models"]: - destination = archive_path / "models" / "pretrained_models" / model["run_directory"] print(f"Preparing {model['name']} from {model['repo_id']}@{model['commit']}") - snapshot_download( - repo_id=model["repo_id"], - revision=model["commit"], - local_dir=destination, - allow_patterns=[ - "checkpoint.pt", - "hyperparameters.yaml", - "model_config.json", - "LICENSE", - "SHA256SUMS", - "README.md", - ], - ) - checksums = (MODEL_MANIFEST.parent / "models" / model["slug"] / "SHA256SUMS").read_text() - if (destination / "SHA256SUMS").read_text() != checksums: - raise ValueError(f"Published checksum manifest differs from the checked-in release: {model['repo_id']}") - for line in checksums.splitlines(): - expected, filename = line.split(" ", 1) - digest = hashlib.sha256() - with (destination / filename).open("rb") as stream: - for chunk in iter(lambda: stream.read(1024 * 1024), b""): - digest.update(chunk) - actual = digest.hexdigest() - if actual != expected: - raise ValueError(f"Checksum mismatch for {model['name']}/{filename}") + check_release_tag(api, model, manifest["release"]) + download(model, cache_dir / model["slug"]) + verify_release_files(cache_dir / model["slug"], checksum_manifest_path(model, releases_dir)) + + pretrained = archive_path / "models" / "pretrained_models" + if pretrained.exists(): + shutil.rmtree(pretrained) + pretrained.mkdir(parents=True) + for model in manifest["models"]: + install_release(cache_dir / model["slug"], pretrained / model["run_directory"]) def setup_tests() -> bool: """Set up video fixtures, then always prepare the published pretrained models.""" try: - data_dir = TEST_ARCHIVE.parent - data_dir.mkdir(parents=True, exist_ok=True) - zip_path = data_dir / "testing_deepethogram_archive.zip" + import gdown + + TEST_DATA.mkdir(parents=True, exist_ok=True) + zip_path = TEST_DATA / "testing_deepethogram_archive.zip" required_items = ["DATA", "models", "project_config.yaml"] if all((TEST_ARCHIVE / item).exists() for item in required_items): @@ -65,7 +149,7 @@ def setup_tests() -> bool: gdown.download(id="1IFz4ABXppVxyuhYik8j38k9-Fl9kYKHo", output=str(zip_path), quiet=False) print("Extracting test archive...") with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(data_dir) + zip_ref.extractall(TEST_DATA) zip_path.unlink() missing_items = [item for item in required_items if not (TEST_ARCHIVE / item).exists()] @@ -78,6 +162,7 @@ def setup_tests() -> bool: return True except Exception as error: print(f"Error during setup: {error}") + traceback.print_exc() return False diff --git a/tests/test_pretrained_models.py b/tests/test_pretrained_models.py index 154d71d..2fc769c 100644 --- a/tests/test_pretrained_models.py +++ b/tests/test_pretrained_models.py @@ -1,22 +1,27 @@ """Check the published checkpoints installed by setup_tests.py, rather than synthetic weights.""" -import hashlib import json -from pathlib import Path import pytest import torch +import setup_tests from deepethogram import projects, utils from deepethogram.feature_extractor.models.hidden_two_stream import deg_f, deg_m, deg_s from deepethogram.flow_generator.models.MotionNet import MotionNet from deepethogram.flow_generator.models.TinyMotionNet import TinyMotionNet from deepethogram.flow_generator.models.TinyMotionNet3D import TinyMotionNet3D -from setup_data import archive_path -MODEL_RELEASES = Path(__file__).resolve().parents[1] / "huggingface" -MODELS = json.loads((MODEL_RELEASES / "models.json").read_text())["models"] -PRETRAINED = Path(archive_path) / "models" / "pretrained_models" +# A missing manifest fails one test below instead of aborting collection for the whole suite. +try: + RELEASES = setup_tests.load_release_manifest()["models"] + MANIFEST_ERROR = None +except FileNotFoundError as error: + RELEASES = [] + MANIFEST_ERROR = str(error) + +PRETRAINED = setup_tests.TEST_ARCHIVE / "models" / "pretrained_models" +MODEL_TYPES = {"optical-flow": "flow_generator", "video-classification": "feature_extractor"} CONSTRUCTORS = { "tinymotionnet": TinyMotionNet, "motionnet": MotionNet, @@ -35,19 +40,19 @@ def cpu_threads(): torch.set_num_threads(previous) -@pytest.mark.parametrize("release", MODELS, ids=lambda release: release["name"]) +def test_release_manifest_present(): + assert MANIFEST_ERROR is None, MANIFEST_ERROR + + +@pytest.mark.parametrize("release", RELEASES, ids=lambda release: release["name"]) def test_published_checkpoint(release): directory = PRETRAINED / release["run_directory"] assert directory.is_dir(), "Run uv run python setup_tests.py before testing published checkpoints" - checksums = (MODEL_RELEASES / "models" / release["slug"] / "SHA256SUMS").read_text() - assert (directory / "SHA256SUMS").read_text() == checksums - for line in checksums.splitlines(): - expected, filename = line.split(" ", 1) - digest = hashlib.sha256() - with (directory / filename).open("rb") as stream: - for chunk in iter(lambda: stream.read(1024 * 1024), b""): - digest.update(chunk) - assert digest.hexdigest() == expected, f"Wrong published artifact: {release['name']}/{filename}" + # setup_tests.py already hashed the checkpoint; check its identity cheaply here. + setup_tests.verify_release_files( + directory, setup_tests.checksum_manifest_path(release), skip_hash=("checkpoint.pt",) + ) + assert (directory / "checkpoint.pt").stat().st_size == release["checkpoint_bytes"] config = json.loads((directory / "model_config.json").read_text()) shape = [ @@ -60,10 +65,11 @@ def test_published_checkpoint(release): with torch.inference_mode(): outputs = model(torch.randn(*shape)) - if release["flow_generator"]: + if release["task"] == "video-classification": assert outputs.shape == (1, 700) assert torch.isfinite(outputs).all() else: + assert release["task"] == "optical-flow" assert len(outputs) == 3 for output, size in zip(outputs, (32, 16, 8)): expected_shape = (1, 2, 10, size, size) if release["slug"] == "tinymotionnet3d" else (1, 20, size, size) @@ -71,11 +77,14 @@ def test_published_checkpoint(release): assert torch.isfinite(output).all() +@pytest.mark.skipif(not RELEASES, reason="model release manifest is missing") def test_published_model_discovery(): discovered = projects.get_weights_from_model_path(PRETRAINED) - assert set(discovered["flow_generator"]) == {"TinyMotionNet", "MotionNet", "TinyMotionNet3D"} - assert set(discovered["feature_extractor"]) == {"resnet18", "resnet50", "resnet3d_34"} - for release in MODELS: - model_type = "feature_extractor" if release["flow_generator"] else "flow_generator" + for release in RELEASES: + model_type = MODEL_TYPES[release["task"]] expected = str(PRETRAINED / release["run_directory"] / "checkpoint.pt") assert discovered[model_type][release["architecture"]] == [expected] + # Nothing besides the pinned releases may be discoverable. + for model_type in ("flow_generator", "feature_extractor", "sequence"): + expected_runs = sum(MODEL_TYPES[release["task"]] == model_type for release in RELEASES) + assert sum(len(runs) for runs in discovered[model_type].values()) == expected_runs diff --git a/tests/test_setup_tests.py b/tests/test_setup_tests.py new file mode 100644 index 0000000..9c6dcc5 --- /dev/null +++ b/tests/test_setup_tests.py @@ -0,0 +1,225 @@ +"""Regression tests for setup_tests.py, the Docker test images and the release metadata. No network needed.""" + +import hashlib +import importlib.util +import json +import re +from pathlib import Path +from types import SimpleNamespace + +import pytest + +import setup_tests +from deepethogram import projects + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +RELEASE_TEXT = { + "hyperparameters.yaml": "run:\n model: flow_generator\nflow_generator:\n arch: {arch}\n", + "model_config.json": '{{"architecture": "{arch}"}}\n', + "LICENSE": "license\n", +} + + +class FakeHub: + """Stands in for both HfApi and snapshot_download.""" + + def __init__(self, models, tag_commits=None, corrupt=()): + self.models = {model["slug"]: model for model in models} + self.tag_commits = tag_commits or {} + self.corrupt = set(corrupt) + self.downloads = [] + + def model_info(self, repo_id, revision): + model = next(model for model in self.models.values() if model["repo_id"] == repo_id) + return SimpleNamespace(sha=self.tag_commits.get(model["slug"], model["commit"])) + + def download(self, model, destination): + self.downloads.append(model["slug"]) + destination.mkdir(parents=True, exist_ok=True) + for filename, content in release_files(model).items(): + (destination / filename).write_bytes(content) + if model["slug"] in self.corrupt: + (destination / "checkpoint.pt").write_bytes(b"truncated") + + +def release_files(model): + files = {name: text.format(arch=model["architecture"]).encode() for name, text in RELEASE_TEXT.items()} + files["checkpoint.pt"] = f"weights for {model['slug']}".encode() + sums = "".join(f"{hashlib.sha256(content).hexdigest()} {name}\n" for name, content in sorted(files.items())) + files["SHA256SUMS"] = sums.encode() + return files + + +@pytest.fixture +def releases(tmp_path): + """A releases directory shaped like huggingface/, with two flow generators.""" + models = [ + { + "slug": slug, + "name": arch, + "repo_id": f"owner/{slug}", + "architecture": arch, + "task": "optical-flow", + "commit": f"{index}" * 40, + "run_directory": f"20010{index}_000000_{arch}", + } + for index, (slug, arch) in enumerate([("tinymotionnet", "TinyMotionNet"), ("motionnet", "MotionNet")], 1) + ] + releases_dir = tmp_path / "huggingface" + for model in models: + model_dir = releases_dir / "models" / model["slug"] + model_dir.mkdir(parents=True) + (model_dir / "SHA256SUMS").write_bytes(release_files(model)["SHA256SUMS"]) + (releases_dir / "models.json").write_text(json.dumps({"release": "v1.0", "models": models})) + return releases_dir, models + + +def write_run(directory, arch, weights=b"stale"): + directory.mkdir(parents=True) + (directory / "hyperparameters.yaml").write_text(RELEASE_TEXT["hyperparameters.yaml"].format(arch=arch)) + (directory / "checkpoint.pt").write_bytes(weights) + + +def run_setup(tmp_path, releases_dir, hub): + archive = tmp_path / "archive" + setup_tests.setup_pretrained_models( + archive, releases_dir=releases_dir, cache_dir=tmp_path / "cache", api=hub, download=hub.download + ) + return archive / "models" / "pretrained_models" + + +def test_stale_runs_cannot_shadow_pinned_checkpoints(tmp_path, releases): + releases_dir, models = releases + pretrained = tmp_path / "archive" / "models" / "pretrained_models" + pinned = pretrained / models[0]["run_directory"] + # An older run of the same architecture, as the Google Drive archive can contain. + write_run(pretrained / "190101_000000_TinyMotionNet", "TinyMotionNet") + # Leftovers inside a pinned run folder: a Lightning checkpoint wins over checkpoint.pt in discovery. + write_run(pinned, "TinyMotionNet") + (pinned / "lightning_checkpoints").mkdir() + (pinned / "lightning_checkpoints" / "epoch=3.ckpt").write_bytes(b"stale") + (pinned / "notes.txt").write_text("stale") + + pretrained = run_setup(tmp_path, releases_dir, FakeHub(models)) + + assert sorted(path.name for path in pretrained.iterdir()) == sorted(model["run_directory"] for model in models) + for model in models: + run = pretrained / model["run_directory"] + assert {path.name for path in run.iterdir()} == set(release_files(model)) + setup_tests.verify_release_files(run, setup_tests.checksum_manifest_path(model, releases_dir)) + discovered = projects.get_weights_from_model_path(pretrained) + for model in models: + expected = str(pretrained / model["run_directory"] / "checkpoint.pt") + assert discovered["flow_generator"][model["architecture"]] == [expected] + + +def test_rerun_reuses_cache_and_reinstalls(tmp_path, releases): + releases_dir, models = releases + hub = FakeHub(models) + pretrained = run_setup(tmp_path, releases_dir, hub) + (pretrained / "extra_run").mkdir() + + pretrained = run_setup(tmp_path, releases_dir, hub) + + assert not (pretrained / "extra_run").exists() + assert (tmp_path / "cache" / "tinymotionnet" / "checkpoint.pt").is_file() + + +def test_corrupt_download_fails_with_digests(tmp_path, releases): + releases_dir, models = releases + with pytest.raises(ValueError, match=r"Checksum mismatch .*checkpoint\.pt: expected SHA-256 [0-9a-f]{64}, got"): + run_setup(tmp_path, releases_dir, FakeHub(models, corrupt={"motionnet"})) + + +def test_release_tag_must_resolve_to_pinned_commit(tmp_path, releases): + releases_dir, models = releases + hub = FakeHub(models, tag_commits={"motionnet": "f" * 40}) + with pytest.raises(ValueError, match="owner/motionnet@v1.0 resolves to f+, but models.json pins"): + run_setup(tmp_path, releases_dir, hub) + + +def test_setup_tests_reports_missing_manifest(tmp_path, monkeypatch, capsys): + """Without huggingface/ next to it, setup must fail clearly rather than with a bare traceback.""" + fixtures = tmp_path / "DATA" + archive = fixtures / "testing_deepethogram_archive" + for item in ("DATA", "models"): + (archive / item).mkdir(parents=True) + (archive / "project_config.yaml").write_text("") + monkeypatch.setattr(setup_tests, "TEST_DATA", fixtures) + monkeypatch.setattr(setup_tests, "TEST_ARCHIVE", archive) + monkeypatch.setattr(setup_tests, "RELEASES_DIR", tmp_path / "missing") + monkeypatch.setattr(setup_tests, "MODEL_MANIFEST", tmp_path / "missing" / "models.json") + + assert setup_tests.setup_tests() is False + assert "Model release manifest not found" in capsys.readouterr().out + + +def test_pretrained_model_tests_collect_without_manifest(tmp_path, monkeypatch): + """A missing manifest must fail one test, not abort collection of the whole suite.""" + monkeypatch.setattr(setup_tests, "MODEL_MANIFEST", tmp_path / "missing.json") + path = REPOSITORY_ROOT / "tests" / "test_pretrained_models.py" + spec = importlib.util.spec_from_file_location("pretrained_models_without_manifest", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + assert module.RELEASES == [] + with pytest.raises(AssertionError, match="Model release manifest not found"): + module.test_release_manifest_present() + + +def dockerfile_stages(): + stages, name = {}, None + for line in (REPOSITORY_ROOT / "docker" / "Dockerfile").read_text().splitlines(): + match = re.match(r"FROM\s+\S+\s+AS\s+(\S+)", line, re.IGNORECASE) + if match: + name = match.group(1) + stages[name] = [] + elif name: + stages[name].append(line) + return stages + + +# The Docker build files are not copied into the test images themselves. +requires_docker_files = pytest.mark.skipif( + not (REPOSITORY_ROOT / ".dockerignore").is_file() or not (REPOSITORY_ROOT / "docker" / "Dockerfile").is_file(), + reason="Docker build files are not in this checkout", +) + + +@requires_docker_files +@pytest.mark.parametrize("stage", ["test-gui", "test-headless", "test-full"]) +def test_docker_test_images_include_release_manifests(stage): + lines = dockerfile_stages()[stage] + assert any(re.match(r"COPY\s+huggingface\s+\./huggingface/?$", line) for line in lines) + assert any(re.match(r"COPY\s+.*\bsetup_tests\.py\b", line) for line in lines) + + +@requires_docker_files +def test_dockerignore_allows_release_manifests(): + rules = [line.strip() for line in (REPOSITORY_ROOT / ".dockerignore").read_text().splitlines()] + assert "!huggingface/**" in rules + # A later exclusion would win over the allow rule. + allowed_at = rules.index("!huggingface/**") + assert not any(rule.startswith("huggingface") for rule in rules[allowed_at + 1 :]) + + +# test_pretrained_models.py reports a missing manifest; don't abort collection here. +RELEASES_ON_DISK = setup_tests.load_release_manifest()["models"] if setup_tests.MODEL_MANIFEST.is_file() else [] + + +@pytest.mark.parametrize("release", RELEASES_ON_DISK, ids=lambda release: release["slug"]) +def test_manifest_matches_model_config(release): + config_path = setup_tests.RELEASES_DIR / "models" / release["slug"] / "model_config.json" + config = json.loads(config_path.read_text()) + for field in ("architecture", "task", "flow_generator"): + assert release[field] == config[field], f"models.json and {config_path} disagree on {field}" + assert release["task"] in ("optical-flow", "video-classification") + + +@pytest.mark.parametrize("release", RELEASES_ON_DISK, ids=lambda release: release["slug"]) +def test_model_card_metadata(release): + card = (setup_tests.RELEASES_DIR / "models" / release["slug"] / "README.md").read_text() + front_matter = card.split("---")[1] + tags = re.findall(r"^- (\S+)$", front_matter.split("tags:")[1], re.MULTILINE) + assert len(tags) == len(set(tags)), f"duplicate tags: {tags}" + assert not re.search(r"\ba (?=[aeiou])", card), "use 'an' before a vowel" From f7f12fc2b58f00359dffe2814f5dcb9946cfa527 Mon Sep 17 00:00:00 2001 From: Jim Bohnslav Date: Mon, 19 Oct 2026 16:39:22 -0400 Subject: [PATCH 3/5] Make test setup offline-friendly and address PR review - setup_tests.py stamps verified downloads, so reruns skip the Hub and re-hashing; touched files are re-hashed and corrupt ones re-downloaded - Pin a release tag per model in models.json instead of one global tag - Copy release files into the archive instead of hard-linking, and skip rebuilding pretrained_models when it already matches (read-only mounts) - Docker test images all run setup before pytest - Replace pytest pythonpath with a conftest that appends the repo root, so tests import the installed package - Share one CI checkpoint cache across OSes; persist Modal checkpoints on a writable volume - Read architecture/task/flow_generator from model_config.json - Fix the article check in the model card test - Require pre-commit>=3.2, which pre-commit-hooks v6 needs Co-Authored-By: Claude Opus 5.5 --- .github/workflows/main.yml | 6 +- docker/Dockerfile | 4 +- docs/testing.md | 6 +- huggingface/RELEASING.md | 26 +++++--- huggingface/models.json | 25 ++----- modal_gpu_test.py | 11 +++- pyproject.toml | 2 +- pytest.ini | 2 - setup_tests.py | 129 ++++++++++++++++++++++++++++++------- tests/conftest.py | 6 ++ tests/test_setup_tests.py | 128 +++++++++++++++++++++++++++++++++--- uv.lock | 65 +++++++++++++++++-- 12 files changed, 333 insertions(+), 77 deletions(-) create mode 100644 tests/conftest.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e26613e..e630ea5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,12 +47,14 @@ jobs: - name: Install dependencies run: uv sync --locked --dev - # About 1.2 GB of checkpoints; setup_tests.py re-verifies the cached files against the checked-in checksums. + # About 1.2 GB of checkpoints, identical on every OS, so all three jobs share one cache entry. + # setup_tests.py re-hashes restored files against the checked-in checksums without contacting the Hub. - name: Cache pinned Hugging Face checkpoints uses: actions/cache@v6 with: path: tests/DATA/huggingface_checkpoints - key: hf-checkpoints-${{ runner.os }}-${{ hashFiles('huggingface/models.json') }} + key: hf-checkpoints-${{ hashFiles('huggingface/models.json') }} + enableCrossOsArchive: true - name: Setup video fixtures and pinned Hugging Face checkpoints run: uv run python setup_tests.py diff --git a/docker/Dockerfile b/docker/Dockerfile index f957cdb..ba582ff 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -220,7 +220,7 @@ COPY huggingface ./huggingface COPY tests ./tests RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ uv sync --locked --no-editable -CMD ["pytest", "-v"] +CMD ["sh", "-c", "python setup_tests.py && pytest -v"] FROM builder-full AS test-full COPY pytest.ini setup_tests.py ./ @@ -228,4 +228,4 @@ COPY huggingface ./huggingface COPY tests ./tests RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked \ uv sync --locked --no-editable -CMD ["pytest", "-v"] +CMD ["sh", "-c", "python setup_tests.py && pytest -v"] diff --git a/docs/testing.md b/docs/testing.md index e42c42c..6c14e2c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -37,10 +37,10 @@ uv run pytest -m "" `uv run python setup_tests.py` prepares everything the tests need: 1. It downloads the video fixture archive from Google Drive into `tests/DATA/testing_deepethogram_archive/`, unless that folder already has `DATA`, `models` and `project_config.yaml`. -2. It downloads the six published checkpoints from Hugging Face at the commits pinned in `huggingface/models.json`, into `tests/DATA/huggingface_checkpoints/`. It checks each `v1.0` tag resolves to its pinned commit, and verifies every file against the checksums in `huggingface/models//SHA256SUMS`. -3. It rebuilds `tests/DATA/testing_deepethogram_archive/models/pretrained_models/` so it holds only those six runs. +2. It downloads the six published checkpoints from Hugging Face at the commits pinned in `huggingface/models.json`, into `tests/DATA/huggingface_checkpoints/`. It checks each model's release tag resolves to its pinned commit, and verifies every file against the checksums in `huggingface/models//SHA256SUMS`. +3. It copies those six runs into `tests/DATA/testing_deepethogram_archive/models/pretrained_models/`, removing anything else there. -Re-run it after pulling changes to `huggingface/models.json`. Later runs reuse the downloads. Don't unzip the archive by hand: its bundled checkpoints are older snapshots without the release manifests, and `tests/test_pretrained_models.py` fails on them. +Re-run it after pulling changes to `huggingface/models.json`. Once the downloads are verified, later runs reuse them without contacting Hugging Face, so setup also works offline. Don't unzip the archive by hand: its bundled checkpoints are older snapshots without the release manifests, and `tests/test_pretrained_models.py` fails on them. ## Writing Tests diff --git a/huggingface/RELEASING.md b/huggingface/RELEASING.md index 3c01fe0..b59f618 100644 --- a/huggingface/RELEASING.md +++ b/huggingface/RELEASING.md @@ -2,7 +2,8 @@ Primary releases are six individual model repos grouped in the [DeepEthogram collection](https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec). -`models.json` records repo IDs, original run names, exact commits and verification evidence. +`models.json` records repo IDs, original run names, exact commits, release tags and verification evidence. +Architecture, task and flow generator come from each model's `model_config.json`. `models//` contains the corresponding model card, machine-readable configuration and checksum manifest. Model binaries remain outside Git. @@ -39,7 +40,8 @@ strict state-dict loading and a finite-output CPU forward pass with the correspo Read back Hub metadata to verify the model's task/library tags. Only then tag the verified commit. The initial per-model release tag is `v1.0`. Model cards and user documentation pin it. -When updating weights, create a new tag rather than moving an existing release tag. +When updating weights, create a new tag rather than moving an existing release tag, and set that model's +`tag` and `commit` in `models.json`. Other models keep their tags. Card-only fixes (`README.md`, which is not in `SHA256SUMS`) can go straight to `main`. They don't need a new tag, and `models.json` keeps pinning the verified release commit. ## Collection and legacy archive @@ -56,13 +58,17 @@ release provides the complete checkpoint from the existing test fixture archive. ## CI checkpoint contract -`setup_tests.py` reads `models.json`. For every model it checks that the release tag resolves to the pinned -`commit`, downloads that commit into `tests/DATA/huggingface_checkpoints/`, and verifies all files against the -checked-in per-model checksum manifest. It then rebuilds the archive's `pretrained_models` directory with only -these runs, so stale folders cannot shadow the pinned weights. Existing fixture folders still run this step. -CPU CI caches the download directory, keyed on `models.json`; the Modal GPU runner copies only its video -fixtures from the persistent test-data volume and downloads the checkpoints fresh. +`setup_tests.py` reads `models.json`. For every model it checks that the model's release tag resolves to the +pinned `commit`, downloads that commit into `tests/DATA/huggingface_checkpoints/` (or `$DEG_CHECKPOINT_CACHE`), +and verifies all files against the checked-in per-model checksum manifest. A `.verified.json` stamp then lets +later runs reuse the download without contacting the Hub; files whose size or modification time changed are +re-hashed, and a corrupt download is fetched again. Setup copies these runs into the archive's +`pretrained_models` directory, rebuilding it unless it already holds exactly these files, so stale folders +cannot shadow the pinned weights. Existing fixture folders still run this step. +CPU CI shares one download cache across operating systems, keyed on `models.json`; the Modal GPU runner copies +only its video fixtures from the persistent test-data volume and keeps checkpoints on the writable +`deepethogram-hf-checkpoints` volume. `tests/test_pretrained_models.py` checks artifact identity, strict architecture matching, finite forward outputs -and discovery for all models. `tests/test_setup_tests.py` checks that `models.json` agrees with each -`model_config.json` and that the Docker test images ship `huggingface/`. +and discovery for all models. `tests/test_setup_tests.py` checks that each checked-in `model_config.json` is the +published one and that the Docker test images ship `huggingface/` and run setup before pytest. When releasing new weights, update commits/checksums together and run setup plus the CPU suite. diff --git a/huggingface/models.json b/huggingface/models.json index 73e1ec5..cf67f31 100644 --- a/huggingface/models.json +++ b/huggingface/models.json @@ -1,16 +1,13 @@ { "collection": "https://huggingface.co/collections/jbohnslav/deepethogram-pretrained-models-6ad61e8af1ac368c56d8deec", - "release": "v1.0", "models": [ { "slug": "tinymotionnet", "name": "TinyMotionNet", "repo_id": "jbohnslav/deepethogram-tinymotionnet", - "architecture": "TinyMotionNet", - "task": "optical-flow", - "flow_generator": "TinyMotionNet", "checkpoint_bytes": 23450307, "commit": "b481cbc41b96db5de248dc533e39ac6f979db33b", + "tag": "v1.0", "verification": { "checksums_passed": 4, "card_example": "strict load and finite CPU forward passed", @@ -41,11 +38,9 @@ "slug": "motionnet", "name": "MotionNet", "repo_id": "jbohnslav/deepethogram-motionnet", - "architecture": "MotionNet", - "task": "optical-flow", - "flow_generator": "MotionNet", "checkpoint_bytes": 183604611, "commit": "f282e2dae1dc15ce3ef45ce99f4c202b08ce7248", + "tag": "v1.0", "verification": { "checksums_passed": 4, "card_example": "strict load and finite CPU forward passed", @@ -76,11 +71,9 @@ "slug": "tinymotionnet3d", "name": "TinyMotionNet3D", "repo_id": "jbohnslav/deepethogram-tinymotionnet3d", - "architecture": "TinyMotionNet3D", - "task": "optical-flow", - "flow_generator": "TinyMotionNet3D", "checkpoint_bytes": 1769548, "commit": "e68a968956658727394d7a8e1cc3b8763b7d60b6", + "tag": "v1.0", "verification": { "checksums_passed": 4, "card_example": "strict load and finite CPU forward passed", @@ -114,11 +107,9 @@ "slug": "deg-f", "name": "DEG-f", "repo_id": "jbohnslav/deepethogram-deg-f", - "architecture": "resnet18", - "task": "video-classification", - "flow_generator": "TinyMotionNet", "checkpoint_bytes": 100453714, "commit": "96c98a45421ace0a70bc287740e7167e4078b1c3", + "tag": "v1.0", "verification": { "checksums_passed": 4, "card_example": "strict load and finite CPU forward passed", @@ -135,11 +126,9 @@ "slug": "deg-m", "name": "DEG-m", "repo_id": "jbohnslav/deepethogram-deg-m", - "architecture": "resnet50", - "task": "video-classification", - "flow_generator": "MotionNet", "checkpoint_bytes": 383713235, "commit": "582a5f28cdd931d3923d9ac065cbbe5a9f122fb1", + "tag": "v1.0", "verification": { "checksums_passed": 4, "card_example": "strict load and finite CPU forward passed", @@ -157,11 +146,9 @@ "name": "DEG-s", "repo_id": "jbohnslav/deepethogram-deg-s", "run_directory": "200409_101808_hidden_two_stream_kinetics_degs", - "architecture": "resnet3d_34", - "task": "video-classification", - "flow_generator": "TinyMotionNet3D", "checkpoint_bytes": 512895194, "commit": "1cd361321b7670e45f7d9ad385bc9cc4f0deb003", + "tag": "v1.0", "verification": { "checksums_passed": 4, "card_example": "strict load and finite CPU forward passed", diff --git a/modal_gpu_test.py b/modal_gpu_test.py index 087873a..6e93dae 100644 --- a/modal_gpu_test.py +++ b/modal_gpu_test.py @@ -11,6 +11,7 @@ APP_NAME = "deepethogram-gpu-tests" VOLUME_NAME = "deepethogram-test-data" +CHECKPOINT_VOLUME_NAME = "deepethogram-hf-checkpoints" CUDA_IMAGE = "nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04" UV_VERSION = "0.6.14" DEFAULT_GPU = os.environ.get("DEG_MODAL_GPU", "T4") @@ -20,11 +21,14 @@ REMOTE_ARCHIVE_DIR = REMOTE_TEST_DATA_DIR / "testing_deepethogram_archive" VOLUME_MOUNT_PATH = Path("/data") VOLUME_ARCHIVE_PATH = VOLUME_MOUNT_PATH / "testing_deepethogram_archive" +CHECKPOINT_MOUNT_PATH = Path("/hf-checkpoints") REMOTE_JUNIT_PATH = Path("/tmp/pytest-gpu.xml") VENV_BIN_DIR = Path("/opt/venv/bin") app = modal.App(APP_NAME) test_data_volume = modal.Volume.from_name(VOLUME_NAME, create_if_missing=False) +# Writable, so verified checkpoint downloads persist between GPU runs instead of being fetched every time. +checkpoint_volume = modal.Volume.from_name(CHECKPOINT_VOLUME_NAME, create_if_missing=True) image = ( modal.Image.from_registry(CUDA_IMAGE, add_python="3.11") @@ -45,6 +49,7 @@ "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "PYTHONPATH": "/workspace:/workspace/tests", "DEG_VERSION": "headless", + "DEG_CHECKPOINT_CACHE": str(CHECKPOINT_MOUNT_PATH), } ) .pip_install(f"uv=={UV_VERSION}") @@ -132,7 +137,10 @@ def _run_and_stream(command: list[str]) -> tuple[int, str]: image=image, gpu=DEFAULT_GPU, timeout=3 * 60 * 60, - volumes={str(VOLUME_MOUNT_PATH): test_data_volume.read_only()}, + volumes={ + str(VOLUME_MOUNT_PATH): test_data_volume.read_only(), + str(CHECKPOINT_MOUNT_PATH): checkpoint_volume, + }, ) def run_gpu_pytest( pytest_target: str = "tests/test_integration.py", @@ -142,6 +150,7 @@ def run_gpu_pytest( returncode, output = _run_and_stream([str(VENV_BIN_DIR / "python"), "setup_tests.py"]) if returncode != 0: raise RuntimeError(f"Test fixture/checkpoint setup failed:\n{output}") + checkpoint_volume.commit() REMOTE_JUNIT_PATH.unlink(missing_ok=True) command = [ diff --git a/pyproject.toml b/pyproject.toml index 5dbbc83..4a196cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ [dependency-groups] dev = [ "ruff>=0.1.0", - "pre-commit>=2.20.0,<3.0.0", + "pre-commit>=3.2.0", "pytest", "pytest-cov", "pytest-qt", diff --git a/pytest.ini b/pytest.ini index ab716f2..a9ed2cb 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,5 @@ [pytest] testpaths = tests -# setup_tests.py lives at the repository root and is imported by the tests. -pythonpath = . filterwarnings = ignore::DeprecationWarning:pkg_resources.*: diff --git a/setup_tests.py b/setup_tests.py index b96040e..f16f132 100644 --- a/setup_tests.py +++ b/setup_tests.py @@ -19,8 +19,13 @@ TEST_DATA = REPOSITORY_ROOT / "tests" / "DATA" TEST_ARCHIVE = TEST_DATA / "testing_deepethogram_archive" # Downloads live outside the archive, so CI can cache them and re-extracting the archive cannot clobber them. -CHECKPOINT_CACHE = TEST_DATA / "huggingface_checkpoints" +# DEG_CHECKPOINT_CACHE moves them, e.g. onto the persistent volume the Modal GPU runner mounts. +CHECKPOINT_CACHE = Path(os.environ.get("DEG_CHECKPOINT_CACHE") or TEST_DATA / "huggingface_checkpoints") RELEASE_FILES = ("checkpoint.pt", "hyperparameters.yaml", "model_config.json", "LICENSE", "SHA256SUMS", "README.md") +# Fields models.json takes from each checked-in model_config.json, which is also the published copy. +CONFIG_FIELDS = ("architecture", "task", "flow_generator") +# Records that a cached release passed verification, so later runs need neither the Hub nor a re-hash. +STAMP_NAME = ".verified.json" def load_release_manifest(path: Path | None = None) -> dict: @@ -30,7 +35,11 @@ def load_release_manifest(path: Path | None = None) -> dict: f"Model release manifest not found: {path}. It ships with the repository in huggingface/; " "copy that directory next to setup_tests.py." ) - return json.loads(path.read_text()) + manifest = json.loads(path.read_text()) + for model in manifest["models"]: + config = json.loads((path.parent / "models" / model["slug"] / "model_config.json").read_text()) + model.update({field: config[field] for field in CONFIG_FIELDS}) + return manifest def checksum_manifest_path(model: dict, releases_dir: Path | None = None) -> Path: @@ -65,12 +74,56 @@ def verify_release_files(directory: Path, manifest_path: Path, skip_hash: tuple[ raise ValueError(f"Checksum mismatch for {path}: expected SHA-256 {expected}, got {actual}") -def check_release_tag(api, model: dict, tag: str) -> None: - """The docs tell users to download ``tag``, so it must resolve to the commit CI verifies.""" - resolved = api.model_info(model["repo_id"], revision=tag).sha +def file_signatures(directory: Path, manifest_text: str) -> dict: + """Size and modification time of each verified file; raises OSError if one is missing.""" + filenames = ["SHA256SUMS"] + [line.split(" ", 1)[1] for line in manifest_text.splitlines()] + signatures = {} + for filename in filenames: + stat = (directory / filename).stat() + signatures[filename] = [stat.st_size, stat.st_mtime_ns] + return signatures + + +def release_identity(model: dict, manifest_path: Path) -> dict: + return {"commit": model["commit"], "tag": model["tag"], "sha256sums": manifest_path.read_text()} + + +def write_stamp(directory: Path, model: dict, manifest_path: Path) -> None: + stamp = release_identity(model, manifest_path) + stamp["files"] = file_signatures(directory, stamp["sha256sums"]) + try: + (directory / STAMP_NAME).write_text(json.dumps(stamp, indent=2)) + except OSError as error: + # A read-only cache still works; the next run re-hashes instead of trusting the stamp. + print(f"Could not record verification in {directory}: {error}") + + +def cached_release_is_current(directory: Path, model: dict, manifest_path: Path) -> bool: + """True if ``directory`` holds this exact release, checked without contacting the Hub.""" + identity = release_identity(model, manifest_path) + try: + stamp = json.loads((directory / STAMP_NAME).read_text()) + except (OSError, ValueError): + return False + if {key: stamp.get(key) for key in identity} != identity: + return False + try: + if stamp.get("files") == file_signatures(directory, identity["sha256sums"]): + return True + # Files were touched since verification, e.g. restored from a CI cache: re-hash them. + verify_release_files(directory, manifest_path) + except (OSError, ValueError): + return False + write_stamp(directory, model, manifest_path) + return True + + +def check_release_tag(api, model: dict) -> None: + """The docs tell users to download the model's tag, so it must resolve to the commit CI verifies.""" + resolved = api.model_info(model["repo_id"], revision=model["tag"]).sha if resolved != model["commit"]: raise ValueError( - f"{model['repo_id']}@{tag} resolves to {resolved}, but models.json pins {model['commit']}. " + f"{model['repo_id']}@{model['tag']} resolves to {resolved}, but models.json pins {model['commit']}. " "Docs and CI would use different weights." ) @@ -87,17 +140,34 @@ def download_release(model: dict, destination: Path) -> None: def install_release(source: Path, destination: Path) -> None: - """Make ``destination`` contain exactly the release files, hard-linked from the download cache when possible.""" + """Copy the release files into ``destination``. + + Copies rather than hard links, so editing a file in the archive cannot change the download cache. + """ destination.mkdir(parents=True) for filename in RELEASE_FILES: - if not (source / filename).is_file(): - continue - try: - os.link(source / filename, destination / filename) - except OSError: + if (source / filename).is_file(): shutil.copy2(source / filename, destination / filename) +def installed_runs_match(pretrained: Path, models: list[dict], cache_dir: Path) -> bool: + """True if ``pretrained`` holds exactly the release runs, each file an unchanged copy of the cached one.""" + if not pretrained.is_dir(): + return False + if sorted(path.name for path in pretrained.iterdir()) != sorted(model["run_directory"] for model in models): + return False + for model in models: + source, run = cache_dir / model["slug"], pretrained / model["run_directory"] + expected = {filename for filename in RELEASE_FILES if (source / filename).is_file()} + if not run.is_dir() or {path.name for path in run.iterdir()} != expected: + return False + for filename in expected: + cached, installed = (source / filename).stat(), (run / filename).stat() + if (cached.st_size, cached.st_mtime_ns) != (installed.st_size, installed.st_mtime_ns): + return False + return True + + def setup_pretrained_models( archive_path: Path, releases_dir: Path | None = None, @@ -107,24 +177,37 @@ def setup_pretrained_models( ) -> None: """Install the published checkpoints, at their pinned commits, as the archive's only pretrained models. - The whole pretrained_models directory is rebuilt, so stale run folders or Lightning checkpoints from the - archive or earlier runs cannot shadow the pinned weights during discovery. + A release already verified in the cache is reused without contacting the Hub. Otherwise its tag is checked, + the pinned commit downloaded and every file verified. pretrained_models is rebuilt unless it already holds + exactly these runs, so stale run folders or Lightning checkpoints cannot shadow the pinned weights. """ releases_dir = releases_dir or RELEASES_DIR cache_dir = cache_dir or CHECKPOINT_CACHE - if api is None: - from huggingface_hub import HfApi - - api = HfApi() manifest = load_release_manifest(releases_dir / "models.json") for model in manifest["models"]: - print(f"Preparing {model['name']} from {model['repo_id']}@{model['commit']}") - check_release_tag(api, model, manifest["release"]) - download(model, cache_dir / model["slug"]) - verify_release_files(cache_dir / model["slug"], checksum_manifest_path(model, releases_dir)) + directory = cache_dir / model["slug"] + manifest_path = checksum_manifest_path(model, releases_dir) + if cached_release_is_current(directory, model, manifest_path): + print(f"{model['name']}: using verified download in {directory}") + continue + print(f"Downloading {model['name']} from {model['repo_id']}@{model['commit']}") + if api is None: + from huggingface_hub import HfApi + + api = HfApi() + check_release_tag(api, model) + # Start clean, so files from an outdated or corrupt download cannot survive. + if directory.exists(): + shutil.rmtree(directory) + download(model, directory) + verify_release_files(directory, manifest_path) + write_stamp(directory, model, manifest_path) pretrained = archive_path / "models" / "pretrained_models" + if installed_runs_match(pretrained, manifest["models"], cache_dir): + print(f"{pretrained} already holds exactly the pinned releases.") + return if pretrained.exists(): shutil.rmtree(pretrained) pretrained.mkdir(parents=True) @@ -133,7 +216,7 @@ def setup_pretrained_models( def setup_tests() -> bool: - """Set up video fixtures, then always prepare the published pretrained models.""" + """Set up video fixtures, then prepare the published pretrained models.""" try: import gdown diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f0da485 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +# The tests import setup_tests.py from the repository root. Append rather than prepend it, so `import deepethogram` +# still finds the installed package; the Docker test images install it with --no-editable to test the built wheel. +sys.path.append(str(Path(__file__).resolve().parents[1])) diff --git a/tests/test_setup_tests.py b/tests/test_setup_tests.py index 9c6dcc5..9874f4b 100644 --- a/tests/test_setup_tests.py +++ b/tests/test_setup_tests.py @@ -15,7 +15,7 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1] RELEASE_TEXT = { "hyperparameters.yaml": "run:\n model: flow_generator\nflow_generator:\n arch: {arch}\n", - "model_config.json": '{{"architecture": "{arch}"}}\n', + "model_config.json": '{{"architecture": "{arch}", "task": "optical-flow", "flow_generator": "{arch}"}}\n', "LICENSE": "license\n", } @@ -28,9 +28,11 @@ def __init__(self, models, tag_commits=None, corrupt=()): self.tag_commits = tag_commits or {} self.corrupt = set(corrupt) self.downloads = [] + self.tag_checks = [] def model_info(self, repo_id, revision): model = next(model for model in self.models.values() if model["repo_id"] == repo_id) + self.tag_checks.append((model["slug"], revision)) return SimpleNamespace(sha=self.tag_commits.get(model["slug"], model["commit"])) def download(self, model, destination): @@ -61,6 +63,7 @@ def releases(tmp_path): "architecture": arch, "task": "optical-flow", "commit": f"{index}" * 40, + "tag": "v1.0", "run_directory": f"20010{index}_000000_{arch}", } for index, (slug, arch) in enumerate([("tinymotionnet", "TinyMotionNet"), ("motionnet", "MotionNet")], 1) @@ -69,8 +72,11 @@ def releases(tmp_path): for model in models: model_dir = releases_dir / "models" / model["slug"] model_dir.mkdir(parents=True) - (model_dir / "SHA256SUMS").write_bytes(release_files(model)["SHA256SUMS"]) - (releases_dir / "models.json").write_text(json.dumps({"release": "v1.0", "models": models})) + for filename in ("SHA256SUMS", "model_config.json"): + (model_dir / filename).write_bytes(release_files(model)[filename]) + # Like the real manifest, models.json leaves the architecture fields to model_config.json. + entries = [{key: value for key, value in model.items() if key not in setup_tests.CONFIG_FIELDS} for model in models] + (releases_dir / "models.json").write_text(json.dumps({"models": entries})) return releases_dir, models @@ -125,6 +131,91 @@ def test_rerun_reuses_cache_and_reinstalls(tmp_path, releases): assert (tmp_path / "cache" / "tinymotionnet" / "checkpoint.pt").is_file() +def test_verified_rerun_needs_no_hub_and_no_writes(tmp_path, releases, monkeypatch): + """Offline runs and read-only fixture mounts work once everything is verified.""" + releases_dir, models = releases + pretrained = run_setup(tmp_path, releases_dir, FakeHub(models)) + + class Offline: + def model_info(self, *args, **kwargs): + raise AssertionError("contacted the Hub") + + def download(self, *args, **kwargs): + raise AssertionError("downloaded again") + + def read_only(*args, **kwargs): + raise PermissionError("read-only file system") + + monkeypatch.setattr(setup_tests, "sha256_file", read_only) + for name in ("rmtree", "copy2"): + monkeypatch.setattr(setup_tests.shutil, name, read_only) + monkeypatch.setattr(Path, "write_text", read_only) + offline = Offline() + setup_tests.setup_pretrained_models( + pretrained.parents[1], + releases_dir=releases_dir, + cache_dir=tmp_path / "cache", + api=offline, + download=offline.download, + ) + + +def test_touched_cache_is_rehashed_without_hub(tmp_path, releases): + """A CI cache restore changes modification times but not contents.""" + releases_dir, models = releases + run_setup(tmp_path, releases_dir, FakeHub(models)) + checkpoint = tmp_path / "cache" / "motionnet" / "checkpoint.pt" + checkpoint.write_bytes(checkpoint.read_bytes()) + + hub = FakeHub(models) + run_setup(tmp_path, releases_dir, hub) + + assert hub.downloads == [] and hub.tag_checks == [] + + +def test_corrupt_cache_is_downloaded_again(tmp_path, releases): + releases_dir, models = releases + run_setup(tmp_path, releases_dir, FakeHub(models)) + (tmp_path / "cache" / "motionnet" / "checkpoint.pt").write_bytes(b"bit rot") + + hub = FakeHub(models) + pretrained = run_setup(tmp_path, releases_dir, hub) + + assert hub.downloads == ["motionnet"] + assert hub.tag_checks == [("motionnet", "v1.0")] + setup_tests.verify_release_files( + pretrained / models[1]["run_directory"], setup_tests.checksum_manifest_path(models[1], releases_dir) + ) + + +def test_pinning_a_new_commit_downloads_it(tmp_path, releases): + releases_dir, models = releases + run_setup(tmp_path, releases_dir, FakeHub(models)) + manifest_path = releases_dir / "models.json" + manifest = json.loads(manifest_path.read_text()) + manifest["models"][0]["commit"] = models[0]["commit"] = "9" * 40 + manifest_path.write_text(json.dumps(manifest)) + + hub = FakeHub(models) + run_setup(tmp_path, releases_dir, hub) + + assert hub.downloads == ["tinymotionnet"] + + +def test_editing_the_archive_leaves_the_cache_intact(tmp_path, releases): + releases_dir, models = releases + pretrained = run_setup(tmp_path, releases_dir, FakeHub(models)) + installed = pretrained / models[0]["run_directory"] / "hyperparameters.yaml" + cached = tmp_path / "cache" / models[0]["slug"] / "hyperparameters.yaml" + original = cached.read_bytes() + with installed.open("w") as stream: + stream.write("edited in place\n") + + assert cached.read_bytes() == original + run_setup(tmp_path, releases_dir, FakeHub(models)) + assert installed.read_bytes() == original + + def test_corrupt_download_fails_with_digests(tmp_path, releases): releases_dir, models = releases with pytest.raises(ValueError, match=r"Checksum mismatch .*checkpoint\.pt: expected SHA-256 [0-9a-f]{64}, got"): @@ -138,6 +229,20 @@ def test_release_tag_must_resolve_to_pinned_commit(tmp_path, releases): run_setup(tmp_path, releases_dir, hub) +def test_each_model_checks_its_own_tag(tmp_path, releases): + """Updating one model's weights gives it a new tag; the others keep theirs.""" + releases_dir, models = releases + manifest_path = releases_dir / "models.json" + manifest = json.loads(manifest_path.read_text()) + manifest["models"][1]["tag"] = "v1.1" + manifest_path.write_text(json.dumps(manifest)) + + hub = FakeHub(models) + run_setup(tmp_path, releases_dir, hub) + + assert hub.tag_checks == [("tinymotionnet", "v1.0"), ("motionnet", "v1.1")] + + def test_setup_tests_reports_missing_manifest(tmp_path, monkeypatch, capsys): """Without huggingface/ next to it, setup must fail clearly rather than with a bare traceback.""" fixtures = tmp_path / "DATA" @@ -192,6 +297,9 @@ def test_docker_test_images_include_release_manifests(stage): lines = dockerfile_stages()[stage] assert any(re.match(r"COPY\s+huggingface\s+\./huggingface/?$", line) for line in lines) assert any(re.match(r"COPY\s+.*\bsetup_tests\.py\b", line) for line in lines) + # The published-checkpoint tests fail unless setup ran first. + command = next(line for line in lines if line.startswith("CMD")) + assert re.search(r"python setup_tests\.py && .*pytest", command), command @requires_docker_files @@ -208,11 +316,11 @@ def test_dockerignore_allows_release_manifests(): @pytest.mark.parametrize("release", RELEASES_ON_DISK, ids=lambda release: release["slug"]) -def test_manifest_matches_model_config(release): - config_path = setup_tests.RELEASES_DIR / "models" / release["slug"] / "model_config.json" - config = json.loads(config_path.read_text()) - for field in ("architecture", "task", "flow_generator"): - assert release[field] == config[field], f"models.json and {config_path} disagree on {field}" +def test_checked_in_model_config_is_the_published_one(release): + """models.json reads architecture and task from this file, so it must match the release checksums.""" + model_dir = setup_tests.RELEASES_DIR / "models" / release["slug"] + sums = dict(reversed(line.split(" ", 1)) for line in (model_dir / "SHA256SUMS").read_text().splitlines()) + assert setup_tests.sha256_file(model_dir / "model_config.json") == sums["model_config.json"] assert release["task"] in ("optical-flow", "video-classification") @@ -222,4 +330,6 @@ def test_model_card_metadata(release): front_matter = card.split("---")[1] tags = re.findall(r"^- (\S+)$", front_matter.split("tags:")[1], re.MULTILINE) assert len(tags) == len(set(tags)), f"duplicate tags: {tags}" - assert not re.search(r"\ba (?=[aeiou])", card), "use 'an' before a vowel" + # Words like "unified", "user" and "one" start with a consonant sound and take "a". + match = re.search(r"\ba (?!one\b|uni|us[eu]|eu)[aeiou]", card, re.IGNORECASE) + assert match is None, f"use 'an' before a vowel: {match and card[match.start() : match.start() + 20]!r}" diff --git a/uv.lock b/uv.lock index 6a122a5..abc434f 100644 --- a/uv.lock +++ b/uv.lock @@ -457,7 +457,9 @@ dev = [ { name = "gdown" }, { name = "huggingface-hub", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "huggingface-hub", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pre-commit" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "pre-commit", version = "4.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-qt" }, @@ -491,7 +493,7 @@ requires-dist = [ dev = [ { name = "gdown" }, { name = "huggingface-hub", specifier = ">=0.24" }, - { name = "pre-commit", specifier = ">=2.20.0,<3.0.0" }, + { name = "pre-commit", specifier = ">=3.2.0" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-qt" }, @@ -1643,8 +1645,61 @@ wheels = [ [[package]] name = "pre-commit" -version = "2.21.0" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64' and sys_platform == 'linux'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", +] dependencies = [ { name = "cfgv" }, { name = "identify" }, @@ -1652,9 +1707,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/00/1637ae945c6e10838ef5c41965f1c864e59301811bb203e979f335608e7c/pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658", size = 174966, upload-time = "2022-12-25T22:53:01.144Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/6b/6cfe3a8b351b54f4b6c6d2ad4286804e3367f628dce379c603d3b96635f4/pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad", size = 201938, upload-time = "2022-12-25T22:52:59.649Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" }, ] [[package]] From adf42e9be13cd09f7ffa014494c875fb75da3431 Mon Sep 17 00:00:00 2001 From: Jim Bohnslav Date: Mon, 19 Oct 2026 16:47:39 -0400 Subject: [PATCH 4/5] Keep Hugging Face release files byte-exact on Windows checkouts Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..49e134d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Release files must keep the exact bytes published to Hugging Face, which SHA256SUMS covers. +# Without this, Windows checkouts convert them to CRLF and the checksums stop matching. +huggingface/** -text From 6fca94e781d6440da5029285cc2e44f222f4093a Mon Sep 17 00:00:00 2001 From: Jim Bohnslav Date: Mon, 19 Oct 2026 22:05:34 -0400 Subject: [PATCH 5/5] Fix pytest runs in docker/build_and_test.sh The tests write into tests/DATA (the archive's project_config.yaml and a project copy), so mounting all of tests/DATA read-only broke them. Mount the archive read-only elsewhere and copy it in, mount only the verified checkpoint cache in place, and run setup_tests.py before pytest. echo_run already runs its command, so drop the duplicate docker run after each one; smoke tests, GPU checks and pytest were running twice. Co-Authored-By: Claude Opus 5.5 --- docker/build_and_test.sh | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docker/build_and_test.sh b/docker/build_and_test.sh index adaf0be..82ebebf 100755 --- a/docker/build_and_test.sh +++ b/docker/build_and_test.sh @@ -38,10 +38,7 @@ verify_gpu() { print_header "Verifying GPU access for ${image}" echo_run docker run ${gpu_flag:+$gpu_flag} --rm "$image" nvidia-smi - docker run ${gpu_flag:+$gpu_flag} --rm "$image" nvidia-smi >/dev/null - echo_run docker run ${gpu_flag:+$gpu_flag} --rm "$image" python -c "import torch; assert torch.cuda.is_available(); print('CUDA available')" - docker run ${gpu_flag:+$gpu_flag} --rm "$image" python -c "import torch; assert torch.cuda.is_available(); print('CUDA available')" >/dev/null } smoke_test_runtime() { @@ -50,11 +47,9 @@ smoke_test_runtime() { print_header "Smoke testing $target runtime image" echo_run docker run ${gpu_flag:+$gpu_flag} --rm "deepethogram:$target" python -c "import deepethogram" - docker run ${gpu_flag:+$gpu_flag} --rm "deepethogram:$target" python -c "import deepethogram" if [[ "$target" == "gui" || "$target" == "full" ]]; then echo_run docker run ${gpu_flag:+$gpu_flag} --rm "deepethogram:$target" python -c "from deepethogram.gui import main" - docker run ${gpu_flag:+$gpu_flag} --rm "deepethogram:$target" python -c "from deepethogram.gui import main" fi } @@ -63,18 +58,26 @@ run_pytest_target() { local gpu_flag=$2 local marker=$3 local image="deepethogram:$target" - local mount_args=() + local data="$PWD/tests/DATA" - if [[ -d tests/DATA ]]; then - mount_args=(-v "$PWD/tests/DATA:/app/tests/DATA:ro") - else - echo -e "${YELLOW}Skipping pytest for ${target}: tests/DATA is not present locally.${NC}" + if [[ ! -d "$data/testing_deepethogram_archive" ]]; then + echo -e "${YELLOW}Skipping pytest for ${target}: run 'uv run python setup_tests.py' first.${NC}" return 0 fi + # The tests write into tests/DATA: they rewrite the archive's project_config.yaml and copy it into a + # project. So the archive is mounted read-only elsewhere and copied into the container. Verified checkpoint + # downloads are only read, so they are mounted in place and setup_tests.py reuses them without the Hub. + local mount_args=(-v "$data/testing_deepethogram_archive:/fixtures/testing_deepethogram_archive:ro") + if [[ -d "$data/huggingface_checkpoints" ]]; then + mount_args+=(-v "$data/huggingface_checkpoints:/app/tests/DATA/huggingface_checkpoints:ro") + fi + local command="mkdir -p tests/DATA && cp -a /fixtures/testing_deepethogram_archive tests/DATA/" + command+=" && python setup_tests.py && pytest -v -m '$marker' tests/" + print_header "Running ${marker} tests in ${target}" - echo_run docker run ${gpu_flag:+$gpu_flag} --rm "${mount_args[@]}" "$image" pytest -v -m "$marker" tests/ - docker run ${gpu_flag:+$gpu_flag} --rm "${mount_args[@]}" "$image" pytest -v -m "$marker" tests/ + echo_run docker run ${gpu_flag:+$gpu_flag} --rm -e QT_QPA_PLATFORM=offscreen "${mount_args[@]}" "$image" \ + sh -c "$command" } main() {