ADR 0009: Put the pose model behind a backend switch so the licence is a choice
- Status: โ Accepted
- Date: 2026-08-20
- Deciders: @akshay
- Tags: gpu-backend, licensing, ml
Contextโ
ultralytics==8.4.104 sits in the inference path of both products. It is
AGPL-3.0. Serving analysis over a network from a derived work triggers the
source-disclosure obligation unless an Ultralytics Enterprise licence is held.
This is not a hypothetical the team had missed. The R&D track already treats it
as settled: racketcv/CLAUDE.md lists "anything AGPL (Ultralytics YOLO*,
boxmot)" under hard bans for a commercial product. The diagnosis existed;
what did not exist was a way to act on it short of a rewrite.
Two things use Ultralytics, and they are not equally stuck:
| Use | Model | Status before this ADR |
|---|---|---|
| Person detection | yolo11m | Already had an escape hatch. FasterRCNNPersonDetector (torchvision, BSD-3) has shipped all along behind --player-detector faster-rcnn. It is simply not the default. |
| Pose | yolo11x-pose | Welded in. Three call sites constructed YOLO(...) directly. |
So the real exposure was one stage, and the blocker was that swapping it meant editing the pipeline rather than setting a variable.
Options Consideredโ
Option A โ buy the Enterprise licence, change nothingโ
- Pros: zero engineering, zero accuracy risk.
- Cons: recurring cost, and it leaves a single vendor able to reprice a dependency that sits on the critical path. Does not remove the need to know whether an alternative works.
Option B โ rip Ultralytics out now and replace itโ
- Pros: ends the exposure outright.
- Cons: swaps the model that feeds contacts, stroke windows and STGCN++ on the strength of an untested assumption. Accuracy would move and nobody would know by how much until users noticed.
Option C โ put the model behind an interface, ship both (chosen)โ
- Pros: the licence becomes a runtime choice. Production keeps running what it runs today; the alternative is one variable away and can be measured on the corpus before anyone commits to it.
- Cons: one more seam, and a second code path to keep honest.
Decisionโ
Pose runs through src/shot_detection/pose_backend.py, which defines a
PoseBackend returning PoseResult(xy, conf, boxes_xywh) โ COCO-17 keypoints,
per-keypoint confidence, and the person boxes the slot assignment needs.
Two implementations:
| Backend | Model | Licence | Default |
|---|---|---|---|
ultralytics | yolo11x-pose | AGPL-3.0 | โ yes |
torchvision | keypointrcnn_resnet50_fpn | BSD-3-Clause | no |
Selected with ACESENSE_POSE_BACKEND (UNFORCE_POSE_BACKEND in the sibling).
Three details are load-bearing:
- Boxes are in the contract, not bolted on. The consumer uses box height to size the player and box position to assign near/far court slots. A backend returning keypoints alone would import cleanly and then mis-assign every player.
- An unknown backend name raises. It does not fall back to the default. Falling back would mean somebody who asked for the clean path, and misspelled it, ships the licensed one believing otherwise โ the single failure this switch exists to prevent.
- torchvision does its own resizing. Keypoint R-CNN carries a
GeneralizedRCNNTransformand maps keypoints back to input pixels itself, soimgszis accepted for interface parity and deliberately ignored. Forcing a letterbox would put the output in the wrong coordinate space.
Consequencesโ
What this buys. The commercial decision is now open. Buy the licence, or migrate โ either is a config change plus an evaluation, not a project.
What it explicitly does not buy: measured accuracy. The two backends are
different architectures on different training schedules. The keypoint schema
matches, which is what makes the swap mechanically safe. Whether the numbers
hold is a question for regression_panel.py over the reference corpus on a GPU,
and that run has not happened. Until it does, torchvision is the
licence-clean option whose accuracy is unknown โ not a drop-in equal, and this
ADR is not permission to treat it as one.
Person detection needs no work, only a decision: faster-rcnn is already
there and already BSD-3. Making it the default is a one-line change gated on the
same evaluation.
Still on AGPL regardless of this switch: yolo11m remains the default
person detector, and ultralytics stays in requirements/base.txt because the
default pose backend needs it. This ADR makes the exposure optional; it does not
remove it. Removing it means flipping both defaults and dropping the dependency,
after the corpus run says what that costs.
Relatedโ
- Current State โ what ships today
racketcv/CLAUDE.mdโ the R&D track's licence tiers and hard bans