diff --git a/README.md b/README.md index 0ebb4d6..7a9df89 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,29 @@ quarantined into one — is the **ultralytics** distribution, which is AGPL-3.0. What it pulls in is a different question, and worth stating plainly: rtmlib requires opencv, and opencv's *bundled FFmpeg* is **GPL-3.0-or-later on macOS -wheels** (built `--enable-gpl` with libx264/libx265) though LGPL-2.1-or-later -on manylinux and Windows. This is measured from the shipped binaries — the -wheels' own `LICENSE-3RD-PARTY.txt` never mentions x264. `paces[media]` -already brings such a wheel, so `[pose]` adds a second copy rather than a -higher tier. `paces.pose.check_pose_requirements()` reports what you have. +wheels of the versions measured (4.12.0.88 / 4.13.0.92)** (built `--enable-gpl` +with libx264/libx265) though LGPL-2.1-or-later on manylinux and Windows — a +per-version fact, not a per-platform one: the 5.0.0.93 macOS x86_64 wheel ships +no FFmpeg at all. This is measured from the shipped binaries — the wheels' own +`LICENSE-3RD-PARTY.txt` never mentions x264. `paces[media]` already brings such +a wheel, so `[pose]` adds a second copy rather than a higher tier. Worse: +rtmlib's own metadata requires *both* `opencv-python` *and* +`opencv-contrib-python`, unpinned, so a plain `pip install paces[media,pose]` +ends up with two distributions owning one `cv2` — harmless until either is +uninstalled, at which point the survivor's `cv2` can be left with files +missing (issue #20). rtmlib only calls plain `cv2` APIs (`VideoCapture`, +`dnn.readNetFromONNX`, drawing helpers — nothing contrib-only), so `[media]`'s +`opencv-contrib-python` already covers it; to keep the closure +single-provider, install in two steps instead of one: + +```bash +pip install "paces[media]" # opencv-contrib-python, the fleet's one cv2 +pip install --no-deps "rtmlib>=0.0.16,<0.1" # skip rtmlib's own opencv-* re-declaration +pip install onnxruntime tqdm # rtmlib's other real deps (numpy already arrives via opencv) +``` + +`paces.pose.check_pose_requirements()` reports what you have, and names the +repair command if both providers are already present. ## How it thinks diff --git a/paces/pose.py b/paces/pose.py index eafe966..1885675 100644 --- a/paces/pose.py +++ b/paces/pose.py @@ -412,7 +412,12 @@ def check_pose_requirements() -> dict: ``opencv-contrib-python``, while the fleet standardises on the contrib superset (mixing's single cv2 provider). An install can therefore end up with two distributions owning one ``cv2`` package. Reported, not fatal: - it works until one of them is uninstalled, and a reader deserves to know. + it works until one of them is uninstalled, and a reader deserves to know + — and, since neither package's uninstall is safe once both are present + (each claims files the other also claims), the note names the repair + command rather than only the collision. See README's "Auto-crop" section + for the install recipe that avoids the pair in the first place + (issue #20). Downloads nothing: weights arrive on first inference, not on this check. """ @@ -449,7 +454,11 @@ def _version(distribution: str) -> str | None: notes.append( "two distributions provide cv2 " f"({', '.join(cv2_providers)}) — rtmlib declares both; uninstalling " - "either can leave the other's cv2 broken" + "either can leave the other's cv2 broken. Repair: " + "pip uninstall -y opencv-python opencv-contrib-python && " + "pip install --force-reinstall opencv-contrib-python — not sticky: " + "the next plain `pip install paces[pose]` re-adds opencv-python, " + "so use README's Auto-crop install recipe to avoid it recurring" ) return { "ok": rtmlib_version is not None and onnxruntime_version is not None, diff --git a/pyproject.toml b/pyproject.toml index c8bb4f1..59df59a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,12 @@ media = [ # unaudited wheel in silently. # Not mirrored into [dev] on purpose: the tests are offline and stay on fake # locators (no weights, no network), and CI has no reason to take on the second -# cv2 provider. `check_pose_requirements()` reports that pair when it happens. +# cv2 provider. `check_pose_requirements()` reports that pair when it happens +# and names the repair command; README's "Auto-crop" section has the two-step +# install recipe that avoids the pair in the first place (issue #20). Not +# fixable from here: rtmlib declares both wheels unconditionally, with no +# marker paces can override, so a single-provider closure needs either that +# recipe or an upstream rtmlib change — not a pin in this file. pose = [ "rtmlib>=0.0.16,<0.1", "onnxruntime", diff --git a/tests/test_pose.py b/tests/test_pose.py index 4328556..cd6bdb2 100644 --- a/tests/test_pose.py +++ b/tests/test_pose.py @@ -439,6 +439,36 @@ def test_check_pose_requirements_reports_channels_without_downloading(): assert any("paces[pose]" in note for note in report["notes"]) +def test_check_pose_requirements_names_the_repair_when_both_cv2_providers_present( + monkeypatch, +): + # issue #20: rtmlib declares both opencv-python and opencv-contrib-python. + # Naming the collision isn't enough — a reader who hits it needs the + # repair command, not just the two package names, since neither package's + # uninstall is safe once both are present (each claims the other's files). + import importlib.metadata + + installed = { + "rtmlib": "0.0.16", + "onnxruntime": "1.2.3", + "opencv-python": "4.12.0.88", + "opencv-contrib-python": "4.13.0.92", + } + + def fake_version(distribution, *args, **kwargs): + try: + return installed[distribution] + except KeyError: + raise importlib.metadata.PackageNotFoundError(distribution) + + monkeypatch.setattr(importlib.metadata, "version", fake_version) + report = check_pose_requirements() + assert report["cv2_providers"] == ["opencv-python", "opencv-contrib-python"] + assert any( + "pip install --force-reinstall" in note for note in report["notes"] + ) + + # ── the licence perimeter (packaging facts, guarded because nothing else is) ─