# Project export: Chom, Nom!

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Chom, Nom! is JPEG for deep learning models.
- Devpost: https://devpost.com/software/om-nom-y0689d
- GitHub: https://github.com/hectorastrom/chom-nom
- Demo: http://chomnomnom.com/
- Video: https://www.youtube.com/embed/RwEgAEjml8Q?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — hectorastrom (11 commits), Albert Astrom (9 commits), Danny Lin (1 commits), bsflll (1 commits), Cursor (1 commits)

## Devpost submission (written by the team)

### Inspiration

Scaling laws have reshaped AI. Models grow deeper, wider, and more expressive. But they're also heavier, slower, and increasingly incompatible with embedded hardware. A Raspberry Pi cannot casually host frontier-scale intelligence. Edge deployment demands a different philosophy: task precision over generality, efficiency over excess, compression without compromise. Chom, Nom! exists to close that gap, transforming oversized PyTorch models into lean, deployable systems without rewriting architectures or sacrificing performance.

### What it does

Compress any machine learning model to 1/4 the size with a single click! Chom, Nom! gets any 50M parameter ready to deploy on a Raspberry Pi in <20s. Under the hood, Chom, Nom! is like a little AI Researcher. Chom, Nom! orchestrates a multi-agent pipeline to perform per-layer quantization ablations, leaving the choice of compression intensity as one that an intelligence can decide to balance footprint reduction and performance. The automated pipeline looks like this: Per-layer sensitivity analysis A Scanner agent inspects every layer's weight distribution, fitting a Beta distribution to estimate quantization sensitivity. Layers with heavy-tailed or high-kurtosis distributions are flagged as fragile; sparse, well-behaved layers are marked as robust. LLM-guided mixed-precision strategy A Strategist agent (backed by an LLM) reads the sensitivity table and proposes 3-5 quantization configurations -- including mixed-precision plans that assign FP32, FP16, or INT8 per layer according to each layer's sensitivity score. A Critic agent then reviews and refines these proposals before any quantization is applied. Automated quantization and evaluation An Executor agent applies each approved configuration via post-training quantization (dynamic INT8, static INT8 with calibration data, FP16, or mixed precision), then measures accuracy, model size, and inference latency. Static INT8 uses representative calibration batches to estimate per-layer activation ranges and zero-points. Pareto-optimal selection An Analyst agent computes the Pareto frontier across all experiment results -- balancing accuracy, size, and speed -- and recommends the best configuration. If coverage gaps exist, additional experiments are proposed iteratively. Guaranteed footprint reduction The final model can be up to 4x smaller (INT8) or even 8x smaller (INT4), enabling cheaper and more private inference while maintaining stable accuracy through sensitivity-aware layer protection. We put a lot of energy into making the user experience is ruthlessly minimal: Upload, Click to Compress, and Deploy.

### How we built it

We built the system on top of PyTorch's native quantization APIs and a pluggable quantizer registry to unify: Per-layer weight distribution analysis (Beta distribution fitting) Sensitivity-aware precision assignment (FP32 / FP16 / INT8 / INT4 per layer) Post-training quantization with optional calibration Multi-agent strategy, critique, and Pareto-optimal selection Architecture Agnostic Design The compression pipeline was designed to remain architecture-agnostic, meaning it does not assume convolutional, transformer, or custom module structure. Instead, the Scanner iterates over all named modules and parameter tensors, fitting statistical distributions to each layer's weights to assess quantization sensitivity. This allows it to generalize across bespoke research models, or your very own custom creations.

### Challenges we ran into

We tried a number of supplementary approaches to model compression including: Agentic self-distillation Structural pruning Low-rank factorization All required modifying the model’s architecture or training loop — pruning channels, changing ranks, or rewriting optimization logic. Automating these decisions meant letting agents redesign core structural components. Current AI systems can apply local edits, but they are unreliable at global architectural reasoning. The result was instability, silent accuracy degradation, and excessive debugging overhead. We ultimately prioritized graph-preserving methods like quantization, which compress models without requiring architectural redesign. In addition, we built custom Web GPU Kernels for on-device quantization through a web interface. However, we found this platform to be too limiting in the end.

### Accomplishments we're proud of

Accuracy preservation at 1/4 the precision Balance of accuracy and footprint reduction using agentic loops, evaluating per layer structure A stable, agentic loop to act as a mini AI-researcher The cutie patootie on the GUI

### What we learned

There's still a gap in agentic ability for sequential ML research (e.g. reacting to distillation attempts and refining experiments). However, this is rapidly progressing!

### What's next

Online container for asynchronous execution on more capable hardware Completion of autonomous self-distillation procedure

## README (from the GitHub repository)

# Chom, Nom!

### Shrink the size of any PyTorch model in a single click!

![chom nom logo](logo.png)


## Project Goal

This project aims to build a **one-click universal model compression tool** designed to shrink PyTorch models for real-time edge deployment on hardware like the Raspberry Pi 4. While the Pi 4 can hold 100–500MB models in its 2–8GB RAM, we target a post-compression footprint of **50MB or less** to guarantee low-latency inference. 

We are prioritizing a pipeline that balances aggressive size reduction with performance stability, starting with **INT8 Quantization** as our primary lever.

---

## The Four Levels of Compression

1.  **Quantization (INT8):** Our starting point. We utilize **Quantization-Aware Training (QAT)** on a provided dataset to secure massive size reductions while preserving accuracy by simulating quantization errors during fine-tuning.
2.  **Structural Pruning:** Unlike unstructured methods, this physically removes network blocks (entire filters or channels). This is the only pruning strategy that genuinely reduces RAM usage and compute operations on ARM architectures.
3.  **Low-Rank Factorization:** This stage uses the best **Rank-R Approximation** in the Frobenius norm to decompose large weight matrices into smaller, more efficient products.
4.  **Logit Distillation:** As a final safety measure, we use **KL Divergence** to align the compressed student model's logits with the original teacher model, recovering accuracy lost during the previous three stages.

---

## Technical Constraints & Hardware Notes

* **Target Hardware:** Raspberry Pi 4 / ARM Cortex-A72.
* **Inference Footprint:** < 50MB for real-time performance.
* **Agnostic Design:** The goal is to build a process compatible with any model
  architecture, including bespoke ones, without requiring external libraries.

---

Built by:
- Christina Lee
- Danny Lin
- Albert Astrom
- Hector Astrom

*for treehacks 2026*


## Detected evidence (automated analysis)

Indexed codebase: 40 recognized source files, 293 KB.
- Anthropic (technology) — detected in the code
- Hugging Face (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (49 of 49)

```
.DS_Store
.gitignore
.python-version
agent/.gitignore
agent/demo.py
agent/README.md
agent/requirements.txt
agent/results/agent_trace.json
agent/src/__init__.py
agent/src/agent.py
agent/src/clip_wrapper.py
agent/src/evaluator.py
agent/src/main.py
agent/src/pareto.py
agent/src/quantizers/__init__.py
agent/src/quantizers/base.py
agent/src/quantizers/bnb.py
agent/src/quantizers/pytorch_native.py
agent/src/scanner.py
agent/src/trace.py
agent/src/visualize.py
compression/__init__.py
compression/compress.py
compression/distill.py
compression/evaluate.py
compression/lower_to_pte.py
compression/prepare_resnet50.py
compression/prepare_yolov8.py
compression/quantize.py
compression/results.md
compression/utils.py
eval/__init__.py
eval/argmax_accuracy.py
eval/eval_suite.py
eval/live_yolo.py
eval/results.md
main.py
pyproject.toml
README.md
ui/__init__.py
ui/.DS_Store
ui/app.py
ui/blob_widget.py
ui/compress_button.py
ui/compression_card.py
ui/info_panel.py
ui/resources.py
ui/styles.py
uv.lock
```

### Dependencies

- agent/requirements.txt: anthropic@>=0.18.0, bitsandbytes@>=0.41.0, matplotlib@>=3.8.0, numpy@>=1.24.0, openai@>=1.12.0, pyyaml@>=6.0, rich@>=13.0.0, scipy@>=1.11.0, torch@>=2.1.0, torchvision@>=0.16.0, transformers@>=4.36.0
- pyproject.toml: executorch@==1.1.0, huggingface-hub@>=1.4.1, PyQt6@>=6.6.0, torch@>=2.10.0, torchao@==0.15.0, torchvision@>=0.22.0, ultralytics@>=8.3.0

### Recent commits (newest first)

- fix size bug
- Add 'agent/' from commit 'f3b375ee15667463b5e5520fcfce69caf107903b'
- Add compression-agent: agentic quantization pipeline, demo, evaluator
- fix stats display
- add logo
- Merge pull request #1 from hectorastrom/frontend
- notation correction
- visual clean up
- test script for distillation
- Added chewing
- increase calibration batch size
- improved drop zone
- Merge remote-tracking branch 'origin/main' into frontend
- enhance evals on compress
- Merge branch 'frontend' of https://github.com/hectorastrom/one-click-compress into frontend
- GUI v1
- GUI v1
- swap from yolo -> resnet demo
- standalone compression script; inference latency bench
- executorch yolov8 compression

## Key source files (fetched from GitHub, selected and truncated for size)

### eval/results.md

```markdown
# YOLO
YOLO completely fails from quanitization.

The best hypothesis is that the obscure output shape of YOLOv8 `[1,84,8000]`
which is totally imbalanced over classes. Dim 1 is structured as 0-3 bounding
boxes, and 4-83 as class predictions. Since the bounding boxes have much higher
values than the class predictions (they represent absolute coordinates), the
attempted preservation of the output range completely skews the data
post-quantization. 

As such, we pivoted to using Resnet as a demonstrative model.

# Resnet
Argmax Accuracy Benchmark (categorical models only)
------------------------------------------------------------
FP32 model:      weights/resnet50.pt2 (pt2)
Quantized model: out/resnet50_int8_xnnpack.pte (pte)
Dataset:         data/imagenette_calibration.pt
Batch size:      1
------------------------------------------------------------
Samples evaluated:      500
FP32 top-1 accuracy:    0.9640
INT8 top-1 accuracy:    0.9700
Prediction agreement:   0.9900
Accuracy delta (INT8):  +0.0060
```

### compression/results.md

```markdown
# YoloV8
| Artifact | Size | Description |
| :--- | :--- | :--- |
| **FP32 .pt2** | 48.15 MB | Original full-precision model |
| **INT8 q/dq .pt2** | 59.37 MB | Intermediate ("bloated") |
| **XNNPACK .pte** | 10.91 MB | Deployable on Raspberry Pi |

---
+----------------------------------------------------------+
|                      FINAL RESULTS                       |
+----------------------------------------------------------+

  Model:                  weights/yolov8s.pt2
  Format:                 pt2
  Model size:             48.15 MB
  Total frames:           152

  Latency
    Mean:                   56.04 ms
    Min:                    49.39 ms
    Max:                    104.58 ms
    p50:                    54.15 ms
    p95:                    67.57 ms
    p99:                    79.88 ms

  Throughput
    Rolling FPS:            17.8
    Overall FPS:            17.8

+----------------------------------------------------------+

+----------------------------------------------------------+
|                      FINAL RESULTS                       |
+----------------------------------------------------------+

  Model:                  weights/yolov8s_int8_xnnpack.pte
  Format:                 pte
  Model size:             10.91 MB
  Total frames:           87

  Latency
    Mean:                   45.06 ms
    Min:                    43.18 ms
    Max:                    50.01 ms
    p50:                    44.75 ms
    p95:                    46.85 ms
    p99:                    50.01 ms

  Throughput
    Rolling FPS:            22.2
    Overall FPS:            22.2

+----------------------------------------------------------+
```

### pyproject.toml

```
[project]
name = "one-click-compress"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "executorch==1.1.0",
    "huggingface-hub>=1.4.1",
    "PyQt6>=6.6.0",
    "torch>=2.10.0",
    "torchao==0.15.0",
    "torchvision>=0.22.0",
    "ultralytics>=8.3.0",
]

```

### agent/requirements.txt

```
torch>=2.1.0
torchvision>=0.16.0
transformers>=4.36.0
bitsandbytes>=0.41.0
scipy>=1.11.0
openai>=1.12.0
anthropic>=0.18.0
matplotlib>=3.8.0
numpy>=1.24.0
rich>=13.0.0
pyyaml>=6.0

```

### main.py

```python
import sys

from PyQt6.QtWidgets import QApplication

from ui import MainWindow


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

```

### ui/app.py

```python
from pathlib import Path

from PyQt6.QtCore import QObject, QThread, Qt, pyqtSignal
from PyQt6.QtWidgets import (
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)

from . import styles
from .blob_widget import BlobDropWidget
from .compress_button import CompressButton
from .info_panel import InfoPanel
from .resources import logo_scaled


# ── Compression worker (runs in background thread) ────────────


class _CompressionWorker(QObject):
    finished = pyqtSignal(dict)
    error = pyqtSignal(str)
    progress = pyqtSignal(str)

    def __init__(self, model_path: str, dataset_path: str):
        super().__init__()
        self._model_path = model_path
        self._dataset_path = dataset_path

    def run(self):
        try:
            self.progress.emit("Loading model and dataset...")
            from compression.compress import compress_and_evaluate

            output_dir = str(Path(self._model_path).parent / "compressed")
            self.progress.emit("Running INT8 quantization...")

            results = compress_and_evaluate(
                model_path=self._model_path,
                dataset_path=self._dataset_path,
                output_dir=output_dir,
            )
            self.finished.emit(results)
        except Exception as exc:
            self.error.emit(str(exc))


# ── Main window ───────────────────────────────────────────────


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Chomnom")
        self.setFixedSize(600, 560)
        self.setStyleSheet(styles.main_window_stylesheet())

        self._worker_thread: QThread | None = None

        central = QWidget()
        central.setObjectName("central_surface")
        central.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
        central.setStyleSheet(f"QWidget#central_surface {{ background-color: {styles.BG_DEEP}; }}")
        self.setCentralWidget(central)
        root = QVBoxLayout(central)
        root.setContentsMargins(40, 24, 40, 12)
        root.setSpacing(16)

        # ── Header — centered logo ─────────────────────────────
        logo_label = QLabel()
        logo_label.setPixmap(logo_scaled(340))
        logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        root.addWidget(logo_label)

        # ── Blob drop zones ────────────────────────────────────
        blobs_layout = QHBoxLayout()
        blobs_layout.setSpacing(32)
        blobs_layout.addStretch()

        self._dataset_blob = BlobDropWidget(
            label="Dataset",
            file_filter=".pt",
            file_description=".pt file",
        )
        blobs_layout.addWidget(self._dataset_blob)

        self._model_blob = BlobDropWidget(
            label="Model Weights",
            file_filter=".pt2",
            file_description=".pt2 file",
        )
        blobs_layout.addWidget(self._model_blob)

        blobs_layout.addStretch()
        root.addLayout(blobs_layout)

        # ── Compress button ────────────────────────────────────
        btn_layout = QHBoxLayout()
        btn_layout.addStretch()
        self._compress_btn = CompressButton()
        btn_layout.addWidget(self._compress_btn)
        btn_layout.addStretch()
        root.addLayout(btn_layout)

        # ── Info panel ─────────────────────────────────────────
        self._info_panel = InfoPanel()
        root.addWidget(self._info_panel)

        # ── Signal wiring ──────────────────────────────────────
        self._dataset_blob.file_accepted.connect(self._on_file_changed)
        self._dataset_blob.file_cleared.connect(self._on_file_changed)
        self._model_blob.file_accepted.connect(self._on_model_accepted)
        self._model_blob.file_cleared.connect(self._on_model_cleared)
        self._compress_btn.clicked.connect(self._on_compress)

    # ── Readiness check ────────────────────────────────────────

    def _check_readiness(self):
        has_dataset = self._dataset_blob._accepted_path is not None
        has_model = self._model_blob._accepted_path is not None
        is_running = self._worker_thread is not None and self._worker_thread.isRunning()
        self._compress_btn.set_ready(has_dataset and has_model and not is_running)

    def _on_file_changed(self, *_args):
        self._check_readiness()

    def _on_model_accepted(self, path: str):
        self._info_panel.update_info(path)
        self._check_readiness()

    def _on_model_cleared(self):
        self._info_panel.clear_info()
        self._check_readiness()

    # ── Compression ────────────────────────────────────────────

    def _on_compress(self):
        dataset = self._dataset_blob._accepted_path
        model = self._model_blob._accepted_path
        if not dataset or not model:
            return

        self._compress_btn.set_ready(False)
        self._compress_btn.setText("COMPRESSING...")
        self._info_panel.show_progress("Starting compression pipeline...")

        # Start chewing animation on both blobs
        self._dataset_blob.start_chewing()
        self._model_blob.start_chewing()

        # Launch worker thread
        self._worker_thread = QThread()
        self._worker = _CompressionWorker(model, dataset)
        self._worker.moveToThread(self._worker_thread)

        self._worker_thread.started.connect(self._worker.run)
        self._worker.progress.connect(self._on_compress_progress)
        self._worker.finished.connect(self._on_compress_finished)
        self._worker.error.connect(self._on_compress_error)
        self._worker.finished.connect(self._worker_thread.quit)
        self._worker.error.connect(self._worker_thread.quit)

        self._worker_thread.start()

    def _on_compress_progress(self, message: str):
        self._info_panel.show_progress(message)

    def _on_compress_finished(self, results: dict):
        self._dataset_blob.stop_chewing()
        self._model_blob.stop_chewing()
        self._compress_btn.setText("COMPRESS")
        self._
[truncated — 320 more characters]
```

### agent/src/main.py

```python
"""
CLI entry point for the agentic quantization pipeline.

Usage:
    python -m src.main --model resnet18 --api-key <KEY>
    python -m src.main --model clip-vit-l --api-key <KEY> --show-examples
    python -m src.main --model mobilenet_v2 --provider anthropic --api-key <KEY>
"""

from __future__ import annotations

import argparse
import logging
import os
import ssl
import sys

# Fix SSL certificate issue for dataset downloads
if hasattr(ssl, "_create_unverified_context"):
    ssl._create_default_https_context = ssl._create_unverified_context

import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Subset
from rich.console import Console
from rich.panel import Panel

from .agent import LLMClient, QuantizationOrchestrator
from .quantizers.base import build_default_registry

console = Console()
logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Model loading
# ---------------------------------------------------------------------------

TORCHVISION_MODELS = {
    "resnet18": {
        "factory": torchvision.models.resnet18,
        "weights": torchvision.models.ResNet18_Weights.DEFAULT,
        "input_size": 224,
    },
    "mobilenet_v2": {
        "factory": torchvision.models.mobilenet_v2,
        "weights": torchvision.models.MobileNet_V2_Weights.DEFAULT,
        "input_size": 224,
    },
    "vit_b_16": {
        "factory": torchvision.models.vit_b_16,
        "weights": torchvision.models.ViT_B_16_Weights.DEFAULT,
        "input_size": 224,
    },
}

CLIP_MODELS = {
    "clip-vit-l": {
        "hf_name": "openai/clip-vit-large-patch14",
        "input_size": 224,
        "params": "428M",
    },
    "clip-vit-b": {
        "hf_name": "openai/clip-vit-base-patch16",
        "input_size": 224,
        "params": "150M",
    },
}

SUPPORTED_MODELS = list(TORCHVISION_MODELS.keys()) + list(CLIP_MODELS.keys())


def load_model(model_name: str) -> tuple:
    """Load a pretrained model.

    Returns (model, input_size).
    For CLIP models, returns the CLIPClassifier wrapper with pre-computed text embeddings.
    """
    if model_name in CLIP_MODELS:
        return _load_clip_model(model_name)
    elif model_name in TORCHVISION_MODELS:
        return _load_torchvision_model(model_name)
    else:
        raise ValueError(
            f"Unknown model '{model_name}'. "
            f"Supported: {SUPPORTED_MODELS}"
        )


def _load_clip_model(model_name: str) -> tuple:
    """Load a CLIP model as a zero-shot classifier."""
    from .clip_wrapper import load_clip_model

    info = CLIP_MODELS[model_name]
    console.print(
        f"Loading [bold]{model_name}[/bold] ({info['hf_name']}, ~{info['params']} params)..."
    )
    console.print("  This is a VLM — using zero-shot classification on CIFAR-10.")

    model, input_size, _ = load_clip_model(
        model_name=info["hf_name"],
    )
    model.eval()
    return model, input_size


def _load_torchvision_model(model_name: str) -> tuple:
    """Load a torchvision model with CIFAR-10 head replacement."""
    info = TORCHVISION_MODELS[model_name]
    console.print(f"Loading pretrained [bold]{model_name}[/bold]...")
    model = info["factory"](weights=info["weights"])
    model.eval()

    # For CIFAR-10 (10 classes), replace the final classifier head
    # since pretrained models are for ImageNet (1000 classes)
    num_classes = 10  # CIFAR-10
    if model_name == "resnet18":
        model.fc = torch.nn.Linear(model.fc.in_features, num_classes)
    elif model_name == "mobilenet_v2":
        model.classifier[1] = torch.nn.Linear(
            model.classifier[1].in_features, num_classes
        )
    elif model_name == "vit_b_16":
        model.heads.head = torch.nn.Linear(
            model.heads.head.in_features, num_classes
        )

    return model, info["input_size"]


# ---------------------------------------------------------------------------
# Dataset loading
# ---------------------------------------------------------------------------

def load_cifar10(
    input_size: int = 224,
    batch_size: int = 64,
    max_test_samples: int | None = 2000,
    max_calib_samples: int = 500,
    data_dir: str = "./data",
    use_clip_transform: bool = False,
) -> tuple[DataLoader, DataLoader]:
    """
    Load CIFAR-10 test set and a calibration subset.

    Args:
        input_size: Resize images to this size.
        batch_size: Batch size for DataLoaders.
        max_test_samples: Limit test set size for faster eval. None = full set.
        max_calib_samples: Number of calibration samples.
        data_dir: Where to download/cache the dataset.
        use_clip_transform: If True, use CLIP-specific normalization.

    Returns:
        (test_loader, calibration_loader)
    """
    if use_clip_transform:
        from .clip_wrapper import get_clip_transform
        transform = get_clip_transform(input_size)
        console.print(f"Loading CIFAR-10 with [bold]CLIP transforms[/bold] ({input_size}x{input_size})...")
    else:
        transform = transforms.Compose([
            transforms.Resize((input_size, input_size)),
            transforms.ToTensor(),
            transforms.Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225],
            ),
        ])
        console.print(f"Loading CIFAR-10 dataset (resize to {input_size}x{input_size})...")

    test_dataset = torchvision.datasets.CIFAR10(
        root=data_dir, train=False, download=True, transform=transform
    )
    train_dataset = torchvision.datasets.CIFAR10(
        root=data_dir, train=True, download=True, transform=transform
    )

    # Subset test set for speed
    if max_test_samples is not None and len(test_dataset) > max_test_samples:
        indices = list(range(max_test_samples))
        test_dataset = Subset(test_dataset, indices)

    # Calibration subset from training set
    calib_indices = list(range(min
[truncated — 6337 more characters]
```

### compression/__init__.py

```python
# compression package

```

### ui/__init__.py

```python
from .app import MainWindow

```

### ui/compress_button.py

```python
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QPushButton

from . import styles


class CompressButton(QPushButton):
    def __init__(self, parent=None):
        super().__init__("COMPRESS", parent)
        self.setFixedHeight(50)
        self.setMinimumWidth(220)
        self.set_ready(False)

    def set_ready(self, ready: bool):
        self._ready = ready
        self.setEnabled(ready)
        self.setStyleSheet(styles.button_stylesheet(ready))
        if ready:
            self.setCursor(Qt.CursorShape.PointingHandCursor)
        else:
            self.setCursor(Qt.CursorShape.ArrowCursor)

```

[28 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]