From f4b42d54e370a03cc055492fb84311c94739d750 Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:24:15 +0200 Subject: [PATCH 1/2] Document a single-provider [pose] install recipe and repair command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rtmlib declares both opencv-python and opencv-contrib-python, unpinned, with no marker paces can override — measured (see PR body) as a hard requirement in rtmlib's own metadata, so it cannot be fixed with a pyproject constraint without forking rtmlib. rtmlib only calls plain cv2 APIs, so [media]'s opencv-contrib-python already covers it: document the two-step install that keeps the closure single-provider, and have check_pose_requirements() name the repair command (not just the collision) when both providers are already present. Closes #20 Claude-Session: https://claude.ai/code/session_016W3RcNo6snhKTw44jP6QWb --- README.md | 18 +++++++++++++++++- paces/pose.py | 11 +++++++++-- pyproject.toml | 7 ++++++- tests/test_pose.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0ebb4d6..da64541 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,23 @@ 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. +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 # skip rtmlib's own opencv-* re-declaration +pip install onnxruntime tqdm # rtmlib's other real, non-cv2 deps +``` + +`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..562209e 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,9 @@ 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" ) 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) ─ From 56c22bd8d09e5b35433849151b5470aff6ea207a Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:28:13 +0200 Subject: [PATCH 2/2] Address review: fix rtmlib version range, non-sticky repair note, per-version FFmpeg claim - README recipe now pins rtmlib>=0.0.16,<0.1 (matching paces' own floor/ceiling) and doesn't claim numpy among rtmlib's "other real deps" listed there. - check_pose_requirements()'s repair note now says the fix isn't sticky and points at the README recipe, matching the module docstring. - The GPL-on-macOS FFmpeg claim is scoped to the versions actually measured (4.12.0.88 / 4.13.0.92); the 5.0.0.93 macOS x86_64 wheel ships no FFmpeg. Claude-Session: https://claude.ai/code/session_016W3RcNo6snhKTw44jP6QWb --- README.md | 32 +++++++++++++++++--------------- paces/pose.py | 4 +++- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index da64541..7a9df89 100644 --- a/README.md +++ b/README.md @@ -71,23 +71,25 @@ 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. 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: +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 # skip rtmlib's own opencv-* re-declaration -pip install onnxruntime tqdm # rtmlib's other real, non-cv2 deps +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 diff --git a/paces/pose.py b/paces/pose.py index 562209e..1885675 100644 --- a/paces/pose.py +++ b/paces/pose.py @@ -456,7 +456,9 @@ def _version(distribution: str) -> str | None: f"({', '.join(cv2_providers)}) — rtmlib declares both; uninstalling " "either can leave the other's cv2 broken. Repair: " "pip uninstall -y opencv-python opencv-contrib-python && " - "pip install --force-reinstall 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,