Compare commits
1 Commits
v0.5.5-dem
...
v0.5.6-dem
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f07dee048 |
54
CLAUDE.md
54
CLAUDE.md
@@ -3599,3 +3599,57 @@ is not up yet and a shop PC showing a stopped engine for five minutes after
|
|||||||
install looks broken. `tests/test_model_download.py` asserts them, and the
|
install looks broken. `tests/test_model_download.py` asserts them, and the
|
||||||
rewritten fetch was checked against the real URL: 232,589 bytes, sha256
|
rewritten fetch was checked against the real URL: 232,589 bytes, sha256
|
||||||
identical to the model already on disk.
|
identical to the model already on disk.
|
||||||
|
|
||||||
|
## The engine stopped updating, and said "installed" every time
|
||||||
|
|
||||||
|
The SSL fix above was released and **did not reach the machine it was written
|
||||||
|
for**. Its log said so, one line above the tick:
|
||||||
|
|
||||||
|
```
|
||||||
|
behavision is already installed with the same version as the provided wheel.
|
||||||
|
Use --force-reinstall to force an installation of the wheel.
|
||||||
|
[ok] Engine and dependencies installed
|
||||||
|
```
|
||||||
|
|
||||||
|
and the traceback that followed still pointed at `model_assets.py", line 59,
|
||||||
|
in _fetch / urllib.request.urlretrieve` — code the fix had deleted.
|
||||||
|
|
||||||
|
Two frozen literals caused it: `version = "1.1.0"` in `pyproject.toml` and
|
||||||
|
`__version__ = "1.0.0"` in `behavision/__init__.py`. They disagreed with each
|
||||||
|
other and neither tracked a release, so **every release built
|
||||||
|
`behavision-1.1.0-py3-none-any.whl`** and `pip install --upgrade` on a machine
|
||||||
|
that already had 1.1.0 is a no-op. The comment beside that call even claimed
|
||||||
|
the opposite: *"`--upgrade` so re-running after a new release replaces the
|
||||||
|
engine rather than leaving the old one in place and reporting success."*
|
||||||
|
|
||||||
|
The shape of the damage is what makes it bad. The Go binaries — the app, the
|
||||||
|
agent, the setup tool — are rebuilt every release and updated normally. So a
|
||||||
|
shop PC ran a current app supervising an engine several releases old, and
|
||||||
|
nothing anywhere said which: `/api/health` reported the model, the paths, the
|
||||||
|
cameras and the gallery, and **no version at all**.
|
||||||
|
|
||||||
|
Three changes, and the second is the one that does not depend on the first
|
||||||
|
being remembered:
|
||||||
|
|
||||||
|
- **One version, in the package**, read by `pyproject.toml` through
|
||||||
|
`[tool.setuptools.dynamic]`. Two literals in two files is how they came to
|
||||||
|
disagree. In a checkout it reads `0.0.0+dev`, because a plausible-looking
|
||||||
|
number on a developer's `/api/health` would be worse than none.
|
||||||
|
- **`pip install --force-reinstall --no-deps <wheel>`, after the ordinary
|
||||||
|
`--upgrade`.** `--upgrade` settles the dependencies; the second call
|
||||||
|
guarantees our own code is the code in the folder. `--no-deps` is what keeps
|
||||||
|
it cheap — forcing the dependencies too would re-download ~300 MB of numpy,
|
||||||
|
OpenCV and onnxruntime on every run. A rebuild at an unchanged version is the
|
||||||
|
ordinary case while developing, so this must not rely on the version moving.
|
||||||
|
- **`release.sh` stamps the tag**: `v0.5.6-demo` → `0.5.6+demo`, valid PEP 440
|
||||||
|
(a local segment takes alphanumerics and dots, never hyphens). Into a copy of
|
||||||
|
the line, reverted in a `trap`, so the tree is never left dirty.
|
||||||
|
|
||||||
|
`/api/health` reports `version` now. Without it there is no way to tell a shop
|
||||||
|
PC three releases behind from a current one, which is precisely how this
|
||||||
|
survived several releases.
|
||||||
|
|
||||||
|
Reproduced end to end before fixing, against real wheels on Python 3.12: two
|
||||||
|
builds of the same version, `--upgrade` leaves the old code in place and prints
|
||||||
|
the same sentence the colleague's Mac printed, `--force-reinstall --no-deps`
|
||||||
|
replaces it.
|
||||||
|
|||||||
@@ -539,7 +539,30 @@ func pipInstall(vpy, src string) error {
|
|||||||
// 'behavision.egg-info': Read-only file system". Falling back to source
|
// 'behavision.egg-info': Read-only file system". Falling back to source
|
||||||
// copies it somewhere writable first, for the same reason.
|
// copies it somewhere writable first, for the same reason.
|
||||||
if wheels, _ := filepath.Glob(filepath.Join(src, "behavision-*.whl")); len(wheels) > 0 {
|
if wheels, _ := filepath.Glob(filepath.Join(src, "behavision-*.whl")); len(wheels) > 0 {
|
||||||
return stream(exec.Command(vpy, "-m", "pip", "install", "--upgrade", wheels[0]),
|
// TWO calls, and the second is the one that matters.
|
||||||
|
//
|
||||||
|
// `--upgrade` alone is not an upgrade when the version has not moved:
|
||||||
|
// pip skips the wheel and says so, one line above this program
|
||||||
|
// printing "[ok] Engine and dependencies installed". The wheel version
|
||||||
|
// was a frozen literal for several releases, so every engine fix in
|
||||||
|
// them silently failed to reach any machine that had run setup once -
|
||||||
|
// while the Go binaries beside it, rebuilt every release, updated
|
||||||
|
// normally. Half the product current, half of it months old, and
|
||||||
|
// nothing saying which.
|
||||||
|
//
|
||||||
|
// release.sh stamps the tag into the version now, so the versions do
|
||||||
|
// differ. This does not rely on that: a rebuild at the same version is
|
||||||
|
// the ordinary case while developing, and "installed" has to mean the
|
||||||
|
// code in this folder either way.
|
||||||
|
if err := stream(exec.Command(vpy, "-m", "pip", "install", "--upgrade", wheels[0]),
|
||||||
|
"installing the engine"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// --no-deps so this is our own package only: the call above has
|
||||||
|
// already settled the dependencies, and forcing those too would
|
||||||
|
// re-download ~300 MB of numpy, OpenCV and onnxruntime every run.
|
||||||
|
return stream(exec.Command(vpy, "-m", "pip", "install",
|
||||||
|
"--force-reinstall", "--no-deps", wheels[0]),
|
||||||
"installing the engine")
|
"installing the engine")
|
||||||
}
|
}
|
||||||
tmp, err := os.MkdirTemp("", "behavision-src-")
|
tmp, err := os.MkdirTemp("", "behavision-src-")
|
||||||
|
|||||||
@@ -4,4 +4,15 @@ Pipeline: capture -> detect (YuNet) -> track (IoU) -> align + encode
|
|||||||
(ArcFace ONNX) -> match / auto-enroll (FAISS + SQLite) -> events + API.
|
(ArcFace ONNX) -> match / auto-enroll (FAISS + SQLite) -> events + API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
# Stamped by release.sh from the git tag, into a copy of this line, so a
|
||||||
|
# release always produces a wheel nobody has installed before. It was a frozen
|
||||||
|
# "1.0.0" here and a frozen "1.1.0" in pyproject.toml - two literals that
|
||||||
|
# disagreed with each other and tracked nothing - which is how every engine fix
|
||||||
|
# for several releases silently failed to reach a machine that had run setup
|
||||||
|
# once. pip skips a wheel whose version is already installed and says so, one
|
||||||
|
# line above setup printing "[ok] Engine and dependencies installed".
|
||||||
|
#
|
||||||
|
# In a checkout it stays obviously a checkout: "0.0.0+dev" on /api/health is
|
||||||
|
# the truth about a developer's machine, and a plausible-looking number there
|
||||||
|
# would be worse than none.
|
||||||
|
__version__ = "0.0.0+dev"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from fastapi.responses import HTMLResponse, Response, StreamingResponse
|
|||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from .config import ApiSection, CameraConfig, CameraTuning
|
from .config import ApiSection, CameraConfig, CameraTuning
|
||||||
from .commission import CommissionRun
|
from .commission import CommissionRun
|
||||||
from .events import Event
|
from .events import Event
|
||||||
@@ -142,7 +143,7 @@ def _reencode(jpeg: bytes, width: int, quality: int) -> "bytes | None":
|
|||||||
|
|
||||||
|
|
||||||
def create_app(engine: Engine) -> FastAPI:
|
def create_app(engine: Engine) -> FastAPI:
|
||||||
app = FastAPI(title="Behavision", version="1.0.0",
|
app = FastAPI(title="Behavision", version=__version__,
|
||||||
dependencies=_auth_dependencies(engine.cfg.api))
|
dependencies=_auth_dependencies(engine.cfg.api))
|
||||||
|
|
||||||
def worker_or_404(camera_id: str):
|
def worker_or_404(camera_id: str):
|
||||||
@@ -172,6 +173,10 @@ def create_app(engine: Engine) -> FastAPI:
|
|||||||
# are up, and the shop recognises nobody it already knows.
|
# are up, and the shop recognises nobody it already knows.
|
||||||
stranded = engine.gallery.health["stranded"]
|
stranded = engine.gallery.health["stranded"]
|
||||||
return {"status": "ok" if engine.started_at else "starting",
|
return {"status": "ok" if engine.started_at else "starting",
|
||||||
|
# Which build is actually running. Without it there was no way
|
||||||
|
# to tell a shop PC three releases behind from a current one -
|
||||||
|
# which is precisely how a silent install failure survived.
|
||||||
|
"version": __version__,
|
||||||
"recognition_model": engine.encoder.model_name,
|
"recognition_model": engine.encoder.model_name,
|
||||||
"gallery_unreadable_embeddings": stranded,
|
"gallery_unreadable_embeddings": stranded,
|
||||||
# "where is my database" must be answerable from the API: the
|
# "where is my database" must be answerable from the API: the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "behavision"
|
name = "behavision"
|
||||||
version = "1.1.0"
|
dynamic = ["version"]
|
||||||
description = "Production face recognition over RTSP"
|
description = "Production face recognition over RTSP"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -32,3 +32,9 @@ behavision = ["static/*"]
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
# One version for the package and the wheel. Two literals in two files is how
|
||||||
|
# they came to disagree (1.0.0 here, 1.1.0 there) and how neither tracked a
|
||||||
|
# release.
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "behavision.__version__"}
|
||||||
|
|||||||
21
release.sh
21
release.sh
@@ -50,7 +50,28 @@ step "2. Agent and setup tool"
|
|||||||
step "3. Engine source and wheel"
|
step "3. Engine source and wheel"
|
||||||
# The wheel is built with the checkout's own interpreter; requires-python is a
|
# The wheel is built with the checkout's own interpreter; requires-python is a
|
||||||
# statement about the SHOP PC, which setup enforces when it finds Python there.
|
# statement about the SHOP PC, which setup enforces when it finds Python there.
|
||||||
|
# Stamp the tag into the wheel version. Without this every release built
|
||||||
|
# behavision-1.1.0-py3-none-any.whl, and `pip install --upgrade` on a machine
|
||||||
|
# that already had 1.1.0 is a no-op - so an engine fix reached nobody who had
|
||||||
|
# ever run setup, while the Go binaries beside it updated normally. Nothing
|
||||||
|
# reported a version either, so there was no way to tell a shop PC three
|
||||||
|
# releases behind from a current one.
|
||||||
|
#
|
||||||
|
# v0.5.6-demo -> 0.5.6+demo, which is valid PEP 440: a local segment takes
|
||||||
|
# alphanumerics and dots, never hyphens.
|
||||||
|
TMPVER=$(mktemp)
|
||||||
|
PEP440=$(printf '%s' "${TAG#v}" | sed 's/-/+/; s/[^0-9A-Za-z.+]/./g')
|
||||||
|
trap 'git checkout -- behavision/__init__.py 2>/dev/null || true; rm -f "$TMPVER"' EXIT
|
||||||
|
# A temp file rather than `sed -i`, whose argument handling differs between
|
||||||
|
# BSD and GNU - this script is run from a Mac today and that is not a reason
|
||||||
|
# to plant a portability trap in a release path.
|
||||||
|
sed "s/^__version__ = .*/__version__ = \"$PEP440\"/" behavision/__init__.py > "$TMPVER"
|
||||||
|
cat "$TMPVER" > behavision/__init__.py
|
||||||
.venv/bin/python -m pip wheel --no-deps --ignore-requires-python -q -w "$STAGE/engine-src" . 2>&1 | grep -v "DEPRECATION\|WARNING: Ignoring" || true
|
.venv/bin/python -m pip wheel --no-deps --ignore-requires-python -q -w "$STAGE/engine-src" . 2>&1 | grep -v "DEPRECATION\|WARNING: Ignoring" || true
|
||||||
|
git checkout -- behavision/__init__.py
|
||||||
|
rm -f "$TMPVER"
|
||||||
|
trap - EXIT
|
||||||
|
echo " engine wheel version $PEP440"
|
||||||
ls "$STAGE"/engine-src/behavision-*.whl >/dev/null || { echo "wheel was not built" >&2; exit 1; }
|
ls "$STAGE"/engine-src/behavision-*.whl >/dev/null || { echo "wheel was not built" >&2; exit 1; }
|
||||||
cp pyproject.toml requirements.txt "$STAGE/engine-src/"
|
cp pyproject.toml requirements.txt "$STAGE/engine-src/"
|
||||||
mkdir -p "$STAGE/engine-src/config" && cp config/default.yaml "$STAGE/engine-src/config/"
|
mkdir -p "$STAGE/engine-src/config" && cp config/default.yaml "$STAGE/engine-src/config/"
|
||||||
|
|||||||
Reference in New Issue
Block a user