Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -1,4 +1,44 @@
- Use PyQt6 for GUI.
- Split code into files when possible.
- Make code clean and understandable.
- Optimize code for performance and memory usage.
# anylabeling — guidance for AI assistants

This is a desktop image-annotation app on PyQt6, with an auto-labeling
backend that runs ONNX models (YOLOv5/v8, SAM1/MobileSAM, SAM2, SAM3)
and a CoreML path for SAM2 on macOS. PyPI ships two parallel packages
from the same source tree: `anylabeling` (CPU, default) and
`anylabeling-gpu` (Linux/Windows, swaps in `onnxruntime-gpu`).

## Conventions
- Use **PyQt6** (not PyQt5). The migration happened in commit 9735fe8.
- The macOS install path **excludes PyQt6** from `pyproject.toml`
(`platform_system != 'Darwin'`); macOS users get it via conda.
Don't add a Darwin-side floor without changing the install story.
- Keep code split into focused files; avoid growing `label_widget.py`
(already ~3.2k LOC) further when a new widget would do.

## Architecture cheat sheet
- Entry point: `anylabeling/app.py` → `MainWindow` → `LabelingWrapper` →
`LabelingWidget` (the "god widget" — owns canvas, file list, toolbars).
- Auto-labeling: `anylabeling/services/auto_labeling/`
- `registry.py` → `@ModelRegistry.register("type-name")` decorator
- `model_manager.py` → loads `models.yaml`, downloads weights to
`~/anylabeling_data/models/`, dispatches `predict_shapes_threading()`
- `models.yaml` (`anylabeling/configs/auto_labeling/`) is the model
catalog the UI reads. New model = new entry **and** registered class.
- `segment_anything.py` auto-detects SAM1/SAM2/SAM3 from ONNX inputs.
- CPU/GPU duality: `setup.py` reads `__preferred_device__` from
`anylabeling/app_info.py`; publish workflows `sed` it before `python -m build`.

## Pre-publish gate
- `.github/workflows/tests.yml` runs a 9-cell matrix (Ubuntu/Windows/macOS
× py3.11/3.12/3.13). All publish/release workflows declare `needs: test`.
- Why it exists: `anylabeling-gpu==0.4.30` shipped to PyPI broken because
no automated step ran `pip install .` against current dep floors before
publish (issue #227, `imgviz>=2.0` returned a read-only colormap).
- Always run `python -m unittest discover -s tests` in a fresh venv before
tagging a release. See `CLAUDE.md` for the full pre-publish playbook.

## Resource regeneration
- PyQt6 dropped `pyrcc`. To rebuild `anylabeling/resources/resources.py`
use `python scripts/compile_languages.py` — it shells out to
`pyside6-rcc` and `pyside6-lrelease` and rewrites imports back to PyQt6.
- `PySide6-Essentials` is a `[dev]` extra for this reason only; runtime
has no PySide6 dependency.
9 changes: 7 additions & 2 deletions .github/workflows/python-publish-cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ on:
- 'v*'

jobs:
test:
name: Test before publish
uses: ./.github/workflows/tests.yml

build-n-publish:
needs: test
if: startsWith(github.ref, 'refs/tags/')
name: Build and publish CPU 🐍📦 to PyPI
runs-on: ubuntu-latest
Expand All @@ -15,9 +20,9 @@ jobs:
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install pypa/build
Expand Down
26 changes: 24 additions & 2 deletions .github/workflows/python-publish-gpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ on:

jobs:

test:
name: Test before publish
uses: ./.github/workflows/tests.yml

build-n-publish-gpu:
needs: test
if: startsWith(github.ref, 'refs/tags/')
name: Build and publish GPU 🐍📦 to PyPI
runs-on: ubuntu-latest
Expand All @@ -17,9 +22,9 @@ jobs:
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install pypa/build
Expand All @@ -29,9 +34,26 @@ jobs:
run: >-
sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "GPU"/g' anylabeling/app_info.py

# PEP 621 makes pyproject.toml `[project]` metadata authoritative — setup.py
# cannot override `name` or replace `dependencies`. Rewrite pyproject.toml
# in place so the GPU wheel ships as `anylabeling-gpu` with onnxruntime-gpu.
- name: Rewrite pyproject.toml for the GPU package
run: |
sed -i 's/^name = "anylabeling"$/name = "anylabeling-gpu"/' pyproject.toml
sed -i 's/"onnxruntime>=1.20.0"/"onnxruntime-gpu>=1.20.0"/' pyproject.toml
echo "--- after rewrite ---"
grep -E '^name|onnxruntime' pyproject.toml

- name: Build a binary wheel and a source tarball
run: >-
python -m build --wheel --outdir dist/ .

- name: Verify built wheel is anylabeling-gpu
run: |
ls dist/
whl=$(ls dist/anylabeling_gpu-*.whl)
python -m zipfile -e "$whl" /tmp/whl_extract/
grep -E '^Name:|^Requires-Dist: onnxruntime' /tmp/whl_extract/anylabeling_gpu-*.dist-info/METADATA
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ permissions:
contents: write

jobs:
test:
name: Test before release
uses: ./.github/workflows/tests.yml

release:
needs: test
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest

Expand Down Expand Up @@ -62,11 +67,11 @@ jobs:
contents: write

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: true

- uses: conda-incubator/setup-miniconda@v2
- uses: conda-incubator/setup-miniconda@v3
with:
python-version: "3.12"
miniconda-version: "latest"
Expand Down Expand Up @@ -123,11 +128,11 @@ jobs:
device: [CPU, GPU]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: true

- uses: conda-incubator/setup-miniconda@v2
- uses: conda-incubator/setup-miniconda@v3
with:
python-version: "3.12"
miniconda-version: "latest"
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Tests

on:
push:
branches: [main, master]
tags: ['v*']
pull_request:
branches: [main, master]
workflow_call:

jobs:
test:
name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11", "3.12", "3.13"]
env:
# Headless Qt — otherwise PyQt6 tries to talk to a display
QT_QPA_PLATFORM: offscreen

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install Qt system libraries (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 libxkbcommon-x11-0 libdbus-1-3 libxcb-cursor0 \
libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \
libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1

- name: Install PyQt6 (macOS — pyproject.toml excludes it on Darwin)
if: runner.os == 'macOS'
run: python -m pip install "PyQt6>=6.7.0"

- name: Install package
run: |
python -m pip install --upgrade pip
pip install .

- name: Run unit tests
run: python -m unittest discover -s tests -v
Loading
Loading