A demo does not need a tunnel; it needs the laptop's own camera

Asked after the mobile-internet question: could a VPN let the office cameras
be shown in the demo. Three jobs get confused there and only one needs one.

Seeing the estate from anywhere already works and needs nothing - viewer mode
plus LiveHub is exactly that, outbound, no installation and no credential.

Demonstrating recognition is better done on the demo machine's own camera.
`webcam: 0` picks a capture index instead of building an RTSP URL and the
engine has supported it since the first version: CameraStore round-trips it,
source() returns the index, safe_url() reports webcam:0, and
POST /api/cameras {"id":"laptop","webcam":0} has always worked. No screen
offered it - the same gap this repo already records for the customer record
and per-camera tuning. It is now an option in the make picker, and it is the
strongest demo available: real faces, in the room, depending on no network.
A demo pointed at a camera in another building depends on two internet
connections and a tunnel staying up while somebody is talking.

The address and the index are alternatives, not extras: source() takes the
webcam first, so a half-typed host left behind would make the saved camera
describe two things and use one. The scan, the path and the camera password
are hidden for a local camera because none of them mean anything.

A tunnel is still right for one case - running the engine on a remote machine
against the office's own cameras - and still wrong for the product: it is
per-site infrastructure on every shop PC, and it gives head office
network-level access into a customer's LAN, where today we can read a
camera's picture and nothing else.

Also: installing httpx took the suite from 239 passed to 271. The HTTP tests
importorskip it so a bare checkout runs, which means the number at the bottom
of a run is not the number of tests that exist. Added to the dev extra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-30 18:18:57 +05:30
parent c9be9b5807
commit b296e8a74a
7 changed files with 205 additions and 70 deletions

View File

@@ -225,3 +225,42 @@ def test_an_inverted_per_camera_pair_is_rejected_not_stored(client):
tuning={"enroll_threshold": 0.8, "match_threshold": 0.5})
assert r.status_code == 400
assert c.get("/api/cameras").json() == []
# "This computer's own camera" — the demo case, and a capability the engine
# has always had with nothing able to reach it.
#
# It matters most for showing the product to somebody. A laptop's own camera
# gives real recognition, of real faces, in the room, depending on no network
# at all — where pointing a demo machine at a camera in another building
# depends on two internet connections and a tunnel staying up while you talk.
def test_this_computers_own_camera_can_be_added(client):
c, eng = client
r = c.post("/api/cameras", json={"id": "laptop", "webcam": 0})
assert r.status_code == 201, r.text
assert "laptop" in eng.workers
cam = next(x for x in c.get("/api/cameras").json() if x["id"] == "laptop")
# Returned, or the form cannot tell a webcam camera from a half-filled
# RTSP one when somebody opens it to edit.
assert cam["webcam"] == 0
assert eng.camera_store.get("laptop").source() == 0
def test_a_second_camera_index_is_kept(client):
"""0 is the built-in one; a plugged-in camera is usually 1. An index
silently coerced to 0 would open the wrong camera and look like the
setting had no effect."""
c, eng = client
assert c.post("/api/cameras", json={"id": "usb", "webcam": 1}).status_code == 201
assert eng.camera_store.get("usb").source() == 1
def test_a_webcam_camera_survives_a_reload(client, tmp_path):
"""It has to be on disk, not only in the running engine: a demo that
forgets its camera when the app restarts is worse than no demo."""
c, _ = client
c.post("/api/cameras", json={"id": "laptop", "webcam": 0})
again = CameraStore(tmp_path / "cameras.json").get("laptop")
assert again.source() == 0
assert again.safe_url() == "webcam:0"