Elliott Duke commited on
Commit
b5442b0
·
1 Parent(s): fb71747

PawTrace read-only demo: reid model + 1000-dog haystack

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .gitignore +11 -1
  3. DEPLOY.md +0 -113
  4. Dockerfile +23 -17
  5. README.md +11 -0
  6. backend/app/api/geo.py +17 -1
  7. backend/app/config.py +19 -4
  8. backend/app/main.py +20 -0
  9. backend/app/ml/breed.py +1 -1
  10. backend/app/ml/detector.py +0 -55
  11. backend/app/ml/embedder.py +53 -47
  12. backend/demo_data/app.db +2 -2
  13. backend/demo_data/media/known/1/1e6abffb49aa43f1822fb9ed29356967.jpg +0 -3
  14. backend/demo_data/media/known/1/41bc76f5b36340b8b59e95f25a406113.jpg +0 -3
  15. backend/demo_data/media/known/1/ca4f3fabe3034731bfb7e47e98244bc4.jpg +0 -3
  16. backend/demo_data/media/known/11/4b97be269fac4e63953eba8b0e41c43e.jpg +0 -3
  17. backend/demo_data/media/known/12/7f83295941134d89b033d8fec8dffd2f.jpg +0 -3
  18. backend/demo_data/media/known/12/a64dc67d5085476f881f971e73f52b2e.jpg +0 -3
  19. backend/demo_data/media/known/12/f93f2f92e16e43a79c0e1497ded8410a.jpg +0 -3
  20. backend/demo_data/media/known/13/450ea8a48cad492a9c49a11393eb8495.jpg +0 -3
  21. backend/demo_data/media/known/13/b1d596d553fe4e3cae4c67fe46b379c3.jpg +0 -3
  22. backend/demo_data/media/known/13/ee2e8e1152d740e6abfb0af8ef3c3f19.jpg +0 -3
  23. backend/demo_data/media/known/14/42294b0c875541fa878d6ab8225fb4de.jpg +0 -3
  24. backend/demo_data/media/known/14/c6f1598561454e65a522127bd8f01c6f.jpg +0 -3
  25. backend/demo_data/media/known/14/d86a962b033f466295244ea36f1789a9.jpg +0 -3
  26. backend/demo_data/media/known/15/101251303df44d1bac54e58b7b5e10d0.jpg +0 -3
  27. backend/demo_data/media/known/15/3e1114b46fa64b98bb50d1f14bc38bda.jpg +0 -3
  28. backend/demo_data/media/known/15/596faa34b27d411c9d7928bf41606f7b.jpg +0 -3
  29. backend/demo_data/media/known/16/16023f39dfdf40ff8b10f46f9718ab2b.jpg +0 -3
  30. backend/demo_data/media/known/16/2f7b765e96bf4a9b834574b55403c196.jpg +0 -3
  31. backend/demo_data/media/known/16/955a2d571eed41909ecf81bbbf34fd13.jpg +0 -3
  32. backend/demo_data/media/known/18/630f9c5a28604f14a5c50d2b1fca5996.jpg +0 -3
  33. backend/demo_data/media/known/18/a3694f54285e4aedb13b1502c5fd41b7.jpg +0 -3
  34. backend/demo_data/media/known/18/fef796259d264e43a3efa784d59b789f.jpg +0 -3
  35. backend/demo_data/media/known/19/2668ad9747c64280b49442b408892a00.jpg +0 -3
  36. backend/demo_data/media/known/19/3a8fcb16eea8449191a72b1e1f66b418.jpg +0 -3
  37. backend/demo_data/media/known/19/e3e4286484ff4295a3c2151d25ff8c1d.jpg +0 -3
  38. backend/demo_data/media/known/2/229eef530f064b99b72a4cf1a04f3eab.jpg +0 -3
  39. backend/demo_data/media/known/2/97bfa63313074cd888736d131f82bc6c.jpg +0 -3
  40. backend/demo_data/media/known/2/d745cafed42f43478a4f37fec4981352.jpg +0 -3
  41. backend/demo_data/media/known/2/dd63a482808b4ba1884d42c7e99b116c.jpg +0 -3
  42. backend/demo_data/media/known/21/0620a5b6c9794547925baeb807b1ecef.jpg +0 -3
  43. backend/demo_data/media/known/21/5c0acc4e4c964f6d813093cf62d22ded.jpg +0 -3
  44. backend/demo_data/media/known/21/6d9c1ee992d44a26a6350f6eeb1b3de7.jpg +0 -3
  45. backend/demo_data/media/known/22/68e56438501c460abc3aaf05e91b8346.jpg +0 -3
  46. backend/demo_data/media/known/22/a1a82403e1d0442e96ac9be580db1f35.jpg +0 -3
  47. backend/demo_data/media/known/22/d7101488750c4478802eb72c0d6fa8ce.jpg +0 -3
  48. backend/demo_data/media/known/23/091b968a546e47278d5d22054e73ab33.jpg +0 -3
  49. backend/demo_data/media/known/23/954a1c7a56ea46e5ae34b8b1fa2b5d24.jpg +0 -3
  50. backend/demo_data/media/known/23/a42d8b7dec254893a8b4fbaf8ef34979.jpg +0 -3
.gitattributes CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  backend/demo_data/app.db filter=lfs diff=lfs merge=lfs -text
2
  *.jpg filter=lfs diff=lfs merge=lfs -text
3
  *.jpeg filter=lfs diff=lfs merge=lfs -text
 
1
+ # Large model weights go through git-LFS (needed for the Hugging Face Space repo).
2
+ best.pt filter=lfs diff=lfs merge=lfs -text
3
+ *.pt filter=lfs diff=lfs merge=lfs -text
4
  backend/demo_data/app.db filter=lfs diff=lfs merge=lfs -text
5
  *.jpg filter=lfs diff=lfs merge=lfs -text
6
  *.jpeg filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -17,8 +17,11 @@ venv/
17
  backend/data/
18
  *.db
19
  *.sqlite3
 
 
 
20
  # NOTE: keep data/zip_centroids.csv tracked — the app needs it.
21
- # EXCEPTION: the demo snapshot shipped to Render (the 115-dog HF haystack) IS tracked.
22
  !backend/demo_data/
23
  !backend/demo_data/app.db
24
  !backend/demo_data/media/
@@ -50,3 +53,10 @@ Thumbs.db
50
 
51
  # Analysis notebooks — kept locally, excluded from the repo
52
  *.ipynb
 
 
 
 
 
 
 
 
17
  backend/data/
18
  *.db
19
  *.sqlite3
20
+ *.db-wal
21
+ *.db-shm
22
+ app.db.pre-*
23
  # NOTE: keep data/zip_centroids.csv tracked — the app needs it.
24
+ # EXCEPTION: the shipped demo snapshot (the 1,000-dog haystack DB + its media) IS tracked.
25
  !backend/demo_data/
26
  !backend/demo_data/app.db
27
  !backend/demo_data/media/
 
53
 
54
  # Analysis notebooks — kept locally, excluded from the repo
55
  *.ipynb
56
+
57
+ # Local re-ID training data (YT-BB-Dog + Sibetan, ~2GB) — never commit
58
+ reid_data/
59
+
60
+ # Local-only: personal test photos + one-off data-prep scripts (kept on disk, not deployed)
61
+ DobbyTesting/
62
+ reid_research/
DEPLOY.md DELETED
@@ -1,113 +0,0 @@
1
- # Deploying PawTrace to Render
2
-
3
- This gets the whole app (frontend + backend) online as **one service** on Render, linkable from your
4
- personal site. It starts in **mock mode** (free, fake match scores) so your first deploy costs nothing
5
- and is low-risk; flipping to the real ML model is a small change at the end.
6
-
7
- The repo already contains everything you need: a `Dockerfile` that builds the React app and runs the
8
- FastAPI API that serves it, plus `render.yaml`.
9
-
10
- ---
11
-
12
- ## Step 1 — Put the code on GitHub
13
-
14
- You've pushed frontends before, so this is familiar. One repo-specific thing first: **don't push the
15
- giant dataset folders** — they're already in `.gitignore`, so just make sure you don't force-add them.
16
-
17
- ```bash
18
- # from the project root
19
- git init # if it isn't a git repo yet
20
- git add .
21
- git commit -m "PawTrace: add Render deploy (Dockerfile, render.yaml)"
22
- ```
23
-
24
- Create a new **empty** repo on GitHub, then:
25
-
26
- ```bash
27
- git branch -M main
28
- git remote add origin https://github.com/<you>/<repo>.git
29
- git push -u origin main
30
- ```
31
-
32
- Sanity check on GitHub: the repo should be small (a few MB). If it's hundreds of MB, a dataset folder
33
- slipped in — remove it (`git rm -r --cached "Multi-pose dog dataset"`), commit, and push again.
34
-
35
- ## Step 2 — Create the Render service
36
-
37
- 1. Sign up at **render.com** with your GitHub account.
38
- 2. **New + → Web Service →** pick your repo (authorize Render if asked).
39
- 3. Render detects the `Dockerfile`. Set:
40
- - **Name:** `pawtrace` (this becomes `pawtrace.onrender.com`)
41
- - **Branch:** `main`
42
- - **Instance type:** **Free**
43
- 4. **Environment variables** (Advanced → Add):
44
- - `JWT_SECRET` → click **Generate** (or paste a long random string)
45
- - `EMBEDDER` → `mock`
46
- - `BREED_CLASSIFIER` → `mock`
47
- *(`DATABASE_URL` and `MEDIA_DIR` are already set inside the Dockerfile.)*
48
- 5. **Create Web Service.** First build takes a few minutes (it builds the React app, then the API).
49
-
50
- > Shortcut: instead of steps 2–4 you can use **New + → Blueprint**, pick the repo, and Render reads
51
- > `render.yaml` for you.
52
-
53
- ## Step 3 — Try it
54
-
55
- Open your `https://pawtrace.onrender.com` URL. You should be able to register, add a dog, upload
56
- photos, create cases, and click through everything. **Breed estimation works for real even in mock
57
- mode** — it's the best thing to show off. (Match *scores* are placeholder until Step 5.)
58
-
59
- Note: the **free** instance sleeps after ~15 min idle, so the first visit after a nap takes ~30s to
60
- wake. That's normal for free hosting and fine for a demo.
61
-
62
- ## Step 4 — Link it from your personal site
63
-
64
- Just a link:
65
-
66
- ```html
67
- <a href="https://pawtrace.onrender.com">Try PawTrace</a>
68
- ```
69
-
70
- Optional nicer URL: in Render → your service → **Settings → Custom Domains**, add e.g.
71
- `pawtrace.yourdomain.com`. Render shows you a **CNAME** to create in your DigitalSpace domain/DNS
72
- settings. Once it propagates, that address serves your app over HTTPS (Render issues the certificate).
73
-
74
- ---
75
-
76
- ## Step 5 — Turn on real matching (when you're ready)
77
-
78
- Real matching loads a PyTorch model (~1–2 GB RAM), so it needs a paid instance.
79
-
80
- 1. In the root **`Dockerfile`**, uncomment the `REAL MATCHING` block (the two `pip install` lines for
81
- `torch` / `transformers`), commit, and push.
82
- 2. In Render → **Environment**, change:
83
- - `EMBEDDER` → `hf`
84
- - `BREED_CLASSIFIER` → `hf`
85
- - add `HF_HOME` → `/app/data/hf-cache` (so the model download is cached on the disk)
86
- 3. In Render → **Settings**, bump the **instance type** to one with **≥ 1 GB RAM**.
87
- 4. **Add persistence** so uploads/DB survive redeploys: uncomment the `disk:` block in `render.yaml`
88
- (or in the dashboard add a **Disk** mounted at **`/app/data`**, ~1 GB). *Do this when you go paid —
89
- free instances can't have a disk.*
90
- 5. Redeploy. The first request downloads the ~43 MB model once (cached afterward).
91
-
92
- ## Demo data (so the site isn't empty)
93
-
94
- `render.yaml` sets **`SEED_DEMO=true`**, so on startup an empty database is auto-filled with a small
95
- Houston-area set: **4 registered dogs (2 reported lost) + 5 found dogs**, a couple of which match a
96
- lost dog. On the free tier the DB resets on each cold start, so this repopulates every time.
97
-
98
- Demo logins (all password **`password123`**):
99
- - **admin@example.com** — the admin dashboard (all dogs/cases/owners)
100
- - **owner0@example.com** … **owner3@example.com** — owner views; owner0 ("Rex") and owner1 ("Bella")
101
- have a lost case with a candidate match already waiting.
102
-
103
- When you move to a **real** deployment with actual users, set `SEED_DEMO=false` (or remove it) so the
104
- demo dogs don't get added.
105
-
106
- ---
107
-
108
- ## Troubleshooting
109
- - **Build fails on `npm run build`** → run `npm run build` in `frontend/` locally to see the real
110
- error (usually a TypeScript issue).
111
- - **App loads but API calls fail** → check the service **Logs** in Render; confirm `JWT_SECRET` is set.
112
- - **Everything resets after a deploy** → expected without a Disk (Step 5.4). Add one on a paid plan.
113
- - **Repo too big to push** → a dataset folder got added; `git rm -r --cached <folder>` and re-commit.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,9 +1,8 @@
1
- # PawTrace — single-service image: builds the React frontend, then runs the FastAPI API which
2
- # also serves that frontend (one origin, no CORS). This image runs in REAL (HF) mode: it bundles
3
- # the PyTorch re-ID / breed model AND a 115-dog demo database, so the live site reproduces the full
4
- # photo-matching demo. Measured ~356 MB RAM peak while serving, so it fits a Render Starter (512 MB)
5
- # instance. For a tiny free/mock image instead, drop the torch block below and set
6
- # EMBEDDER/BREED_CLASSIFIER=mock + SEED_DEMO=true.
7
 
8
  # ---- Stage 1: build the React frontend -> /web/dist ----
9
  FROM node:20-slim AS frontend
@@ -31,13 +30,17 @@ RUN pip install torch torchvision --index-url https://download.pytorch.org/whl/c
31
  # Pre-download the model into the image (into HF_HOME) so the first live match doesn't stall on a
32
  # runtime download. This is a big, stable layer — kept cached across code changes below.
33
  ENV HF_HOME=/app/hf_cache
34
- RUN python -c "from transformers import AutoImageProcessor, AutoModelForImageClassification as M; \
35
- k='jhoppanne/Dogs-Breed-Image-Classification-V1'; AutoImageProcessor.from_pretrained(k); M.from_pretrained(k)"
 
36
  # ------------------------------------------------------------------------------------------------
37
 
38
  # Backend source (includes backend/demo_data/ — the shipped snapshot).
39
  COPY backend/ ./
40
 
 
 
 
41
  # Geo centroid CSV lives at the repo root; copy it in and point the app at it.
42
  COPY data/zip_centroids.csv /app/geo/zip_centroids.csv
43
  ENV ZIP_CENTROID_FILE=/app/geo/zip_centroids.csv
@@ -45,24 +48,27 @@ ENV ZIP_CENTROID_FILE=/app/geo/zip_centroids.csv
45
  # Built frontend from stage 1 (the API serves this at "/").
46
  COPY --from=frontend /web/dist ./frontend_dist
47
 
48
- # Bake the 115-dog demo snapshot (SQLite DB + processed photos) into the image's data dir, then drop
49
- # the source copy. On a paid instance the container stays warm, so demo registrations persist for the
50
- # session; a redeploy resets to this clean snapshot. (Mount a Render Disk at /app/data for true
51
- # persistence across restarts — see render.yaml.)
52
  RUN mkdir -p /app/data \
53
  && cp /app/demo_data/app.db /app/data/app.db \
54
  && cp -r /app/demo_data/media /app/data/media \
55
  && rm -rf /app/demo_data
56
  ENV DATABASE_URL=sqlite:////app/data/app.db \
57
  MEDIA_DIR=/app/data/media \
58
- EMBEDDER=hf \
 
 
59
  BREED_CLASSIFIER=hf \
 
 
60
  HF_HUB_OFFLINE=1 \
61
  TRANSFORMERS_OFFLINE=1
62
  # HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE: use the model baked into HF_HOME above; never call
63
  # huggingface.co at runtime (faster cold start, no external dependency during a demo).
64
 
65
- # No EXPOSE: Render routes public traffic to the port the app actually opens (below). An EXPOSE that
66
- # differs from the bound port makes Render send public traffic to the wrong port -> 502.
67
- # Render provides $PORT (default 10000); bind to it.
68
- CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-10000}"]
 
1
+ # PawTrace — single-service image for the READ-ONLY demo (Hugging Face Spaces / any Docker host).
2
+ # Builds the React frontend, then runs the FastAPI API which also serves that frontend (one origin,
3
+ # no CORS). Bundles the fine-tuned PyTorch re-ID model (best.pt) + the HF breed classifier + the
4
+ # 1,000-dog demo database, and runs with DEMO_MODE=true so every write is blocked server-side.
5
+ # Needs ~1 GB RAM with both models loaded fine on a Spaces CPU-basic (16 GB) box.
 
6
 
7
  # ---- Stage 1: build the React frontend -> /web/dist ----
8
  FROM node:20-slim AS frontend
 
30
  # Pre-download the model into the image (into HF_HOME) so the first live match doesn't stall on a
31
  # runtime download. This is a big, stable layer — kept cached across code changes below.
32
  ENV HF_HOME=/app/hf_cache
33
+ RUN python -c "from transformers import AutoImageProcessor, AutoModel, AutoModelForImageClassification as M; \
34
+ k='jhoppanne/Dogs-Breed-Image-Classification-V1'; AutoImageProcessor.from_pretrained(k); \
35
+ M.from_pretrained(k); AutoModel.from_pretrained(k)"
36
  # ------------------------------------------------------------------------------------------------
37
 
38
  # Backend source (includes backend/demo_data/ — the shipped snapshot).
39
  COPY backend/ ./
40
 
41
+ # Fine-tuned re-ID model weights (git-LFS in the Space repo) -> loaded when EMBEDDER=reid.
42
+ COPY best.pt /app/best.pt
43
+
44
  # Geo centroid CSV lives at the repo root; copy it in and point the app at it.
45
  COPY data/zip_centroids.csv /app/geo/zip_centroids.csv
46
  ENV ZIP_CENTROID_FILE=/app/geo/zip_centroids.csv
 
48
  # Built frontend from stage 1 (the API serves this at "/").
49
  COPY --from=frontend /web/dist ./frontend_dist
50
 
51
+ # Bake the 1,000-dog demo snapshot (SQLite DB + processed photos) into the image's data dir, then
52
+ # drop the source copy. DEMO_MODE blocks all writes, so the DB never changes; a redeploy just
53
+ # reloads this same read-only snapshot.
 
54
  RUN mkdir -p /app/data \
55
  && cp /app/demo_data/app.db /app/data/app.db \
56
  && cp -r /app/demo_data/media /app/data/media \
57
  && rm -rf /app/demo_data
58
  ENV DATABASE_URL=sqlite:////app/data/app.db \
59
  MEDIA_DIR=/app/data/media \
60
+ EMBEDDER=reid \
61
+ REID_MODEL_PATH=/app/best.pt \
62
+ REID_MODEL_VERSION=v4 \
63
  BREED_CLASSIFIER=hf \
64
+ BREED_TOP_K=10 \
65
+ DEMO_MODE=true \
66
  HF_HUB_OFFLINE=1 \
67
  TRANSFORMERS_OFFLINE=1
68
  # HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE: use the model baked into HF_HOME above; never call
69
  # huggingface.co at runtime (faster cold start, no external dependency during a demo).
70
 
71
+ # HF Spaces routes to the port declared as `app_port` in README.md (7860). Bind there; ${PORT} keeps
72
+ # it portable to hosts that inject a port (Render, etc.).
73
+ EXPOSE 7860
74
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  # PawTrace — Lost-Dog Reunification Platform
2
 
3
  A responsive web app that helps reunite lost dogs with their owners by matching photos with
 
1
+ ---
2
+ title: PawTrace
3
+ emoji: 🐕
4
+ colorFrom: yellow
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ short_description: Find a lost dog by its image — an AI dog re-identification demo.
10
+ ---
11
+
12
  # PawTrace — Lost-Dog Reunification Platform
13
 
14
  A responsive web app that helps reunite lost dogs with their owners by matching photos with
backend/app/api/geo.py CHANGED
@@ -1,7 +1,13 @@
1
  from __future__ import annotations
2
 
3
- from fastapi import APIRouter
 
 
4
 
 
 
 
 
5
  from .shelters import nearby_shelters
6
 
7
  router = APIRouter(tags=["geo"])
@@ -15,3 +21,13 @@ def get_nearby_shelters(zip: str | None = None) -> dict:
15
  @router.get("/healthz")
16
  def healthz() -> dict:
17
  return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ from fastapi import APIRouter, Depends
4
+ from sqlalchemy import func, select
5
+ from sqlalchemy.orm import Session
6
 
7
+ from ..config import settings
8
+ from ..db import get_db
9
+ from ..models import UnknownDog
10
+ from ..models.base import UnknownDogStatus
11
  from .shelters import nearby_shelters
12
 
13
  router = APIRouter(tags=["geo"])
 
21
  @router.get("/healthz")
22
  def healthz() -> dict:
23
  return {"status": "ok"}
24
+
25
+
26
+ @router.get("/config")
27
+ def public_config(db: Session = Depends(get_db)) -> dict:
28
+ """Public runtime config the frontend reads on load: which UI to render, and how many
29
+ found/unclaimed dogs a photo search is compared against (the searchable haystack size)."""
30
+ haystack_size = db.execute(
31
+ select(func.count(UnknownDog.id)).where(UnknownDog.status == UnknownDogStatus.pending)
32
+ ).scalar()
33
+ return {"demo_mode": settings.demo_mode, "haystack_size": haystack_size}
backend/app/config.py CHANGED
@@ -25,17 +25,32 @@ class Settings(BaseSettings):
25
  media_dir: str = "./data/media"
26
  storage_backend: str = "local" # local | s3
27
 
28
- # Embedder / ML
29
- embedder: str = "mock" # mock | cnn | hf
30
- embedder_model: str = "resnet50" # torchvision backbone when EMBEDDER=cnn
 
 
 
 
 
 
31
  # HF re-ID embedder: uses the penultimate (pre-classifier) pooled features as the vector.
32
  embedder_hf_model: str = "jhoppanne/Dogs-Breed-Image-Classification-V1"
33
- detector_enabled: bool = False
 
 
 
 
34
 
35
  # Populate a small demo dataset on startup when the DB is empty (SEED_DEMO=1). For free/mock
36
  # deploys so the app isn't blank; safe to leave on (only ever seeds an empty database).
37
  seed_demo: bool = False
38
 
 
 
 
 
 
39
  # Breed classifier — cheap estimated-breed candidate gate (spec §9.3, extends metadata gate).
40
  # Predicts breed *labels only*; never used for similarity. mock = deterministic, no downloads.
41
  breed_classifier: str = "mock" # mock | hf
 
25
  media_dir: str = "./data/media"
26
  storage_backend: str = "local" # local | s3
27
 
28
+ # Embedder / ML. THE SINGLE SWITCH between one-model and two-model modes:
29
+ # EMBEDDER=hf -> SINGLE model: the breed model runs ONCE per image and yields BOTH the
30
+ # matching vector (penultimate features) and the top-K breed labels.
31
+ # EMBEDDER=reid -> TWO models: the fine-tuned re-ID model (reid_model_path) produces the
32
+ # matching vector, and the separate breed classifier produces the top-K
33
+ # breeds. Better matching, at the cost of a second forward pass + model.
34
+ # (Routing is automatic: with EMBEDDER=reid the embedder != breed model, so images.py takes its
35
+ # existing two-model path. Set BREED_CLASSIFIER=hf in reid mode to keep real breed labels.)
36
+ embedder: str = "mock" # mock | hf | reid
37
  # HF re-ID embedder: uses the penultimate (pre-classifier) pooled features as the vector.
38
  embedder_hf_model: str = "jhoppanne/Dogs-Breed-Image-Classification-V1"
39
+ # Fine-tuned re-ID checkpoint (EMBEDDER=reid): a train_reid.py best.pt loaded into the breed
40
+ # backbone. reid_model_version tags stored vectors so matching only compares same-model vectors
41
+ # (switching modes needs a re-embed — the tag keeps old/new vectors from being mixed).
42
+ reid_model_path: str = "./best.pt"
43
+ reid_model_version: str = "v4"
44
 
45
  # Populate a small demo dataset on startup when the DB is empty (SEED_DEMO=1). For free/mock
46
  # deploys so the app isn't blank; safe to leave on (only ever seeds an empty database).
47
  seed_demo: bool = False
48
 
49
+ # Public read-only showcase (DEMO_MODE=1): the frontend renders the 3-page demo shell, and the
50
+ # backend HARD-BLOCKS every write — only the two transient, no-persist photo-search endpoints are
51
+ # allowed. Nothing can modify the database (not the demo UI, a direct API call, or curl).
52
+ demo_mode: bool = False
53
+
54
  # Breed classifier — cheap estimated-breed candidate gate (spec §9.3, extends metadata gate).
55
  # Predicts breed *labels only*; never used for similarity. mock = deterministic, no downloads.
56
  breed_classifier: str = "mock" # mock | hf
backend/app/main.py CHANGED
@@ -74,6 +74,26 @@ async def _validation_exc_handler(_: Request, exc: RequestValidationError) -> JS
74
  )
75
 
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  # ---- Routers ----
78
  app.include_router(auth.router)
79
  app.include_router(dogs.router)
 
74
  )
75
 
76
 
77
+ # ---- Demo mode: hard, server-side read-only guard ----
78
+ # When DEMO_MODE=1 the app is a public showcase. The ONLY writes allowed are the two transient
79
+ # photo-search endpoints (which persist nothing). Every other mutating request is refused here — so
80
+ # the database can never be modified from the demo UI, a direct API call, or curl. Safety is enforced
81
+ # on the server, not by hiding buttons in the frontend.
82
+ if settings.demo_mode:
83
+ _DEMO_WRITE_ALLOWLIST = {"/search/by-photo", "/search/breed"}
84
+ _MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
85
+
86
+ @app.middleware("http")
87
+ async def _demo_readonly_guard(request: Request, call_next):
88
+ if request.method in _MUTATING_METHODS and request.url.path not in _DEMO_WRITE_ALLOWLIST:
89
+ return JSONResponse(
90
+ status_code=403,
91
+ content={"error": {"code": 403,
92
+ "message": "This is a read-only demo — changes are disabled."}},
93
+ )
94
+ return await call_next(request)
95
+
96
+
97
  # ---- Routers ----
98
  app.include_router(auth.router)
99
  app.include_router(dogs.router)
backend/app/ml/breed.py CHANGED
@@ -7,7 +7,7 @@ similarity; that matcher is selected separately.
7
 
8
  Mock-first: the default ``MockBreedClassifier`` is deterministic and weight-free, so tests and the
9
  seed demo run offline. The real ``HFBreedClassifier`` is opt-in via config and lazily imports
10
- torch/transformers, exactly like ``CNNEmbedder``.
11
  """
12
  from __future__ import annotations
13
 
 
7
 
8
  Mock-first: the default ``MockBreedClassifier`` is deterministic and weight-free, so tests and the
9
  seed demo run offline. The real ``HFBreedClassifier`` is opt-in via config and lazily imports
10
+ torch/transformers, exactly like ``HFEmbedder``.
11
  """
12
  from __future__ import annotations
13
 
backend/app/ml/detector.py DELETED
@@ -1,55 +0,0 @@
1
- """Optional dog-detector crop (spec §8 step 3, milestone 8).
2
-
3
- Disabled by default (DETECTOR_ENABLED=false) and never used on the mock path. When enabled with
4
- torch/torchvision installed, crops the most confident dog box (with padding) before embedding.
5
- """
6
- from __future__ import annotations
7
-
8
- from PIL import Image
9
-
10
-
11
- class DogDetector: # pragma: no cover - only used when DETECTOR_ENABLED + torch installed
12
- # COCO class id 18 == "dog".
13
- DOG_CLASS_ID = 18
14
-
15
- def __init__(self, score_threshold: float = 0.5, padding: float = 0.1):
16
- import torchvision
17
- from torchvision.models.detection import (
18
- FasterRCNN_ResNet50_FPN_Weights as W,
19
- )
20
-
21
- self.score_threshold = score_threshold
22
- self.padding = padding
23
- weights = W.DEFAULT
24
- self._model = torchvision.models.detection.fasterrcnn_resnet50_fpn(
25
- weights=weights
26
- ).eval()
27
- self._preprocess = weights.transforms()
28
-
29
- def detect_box(self, img: Image.Image) -> tuple[int, int, int, int] | None:
30
- import torch
31
-
32
- with torch.no_grad():
33
- tensor = self._preprocess(img)
34
- pred = self._model([tensor])[0]
35
- best = None
36
- best_score = self.score_threshold
37
- for box, label, score in zip(pred["boxes"], pred["labels"], pred["scores"]):
38
- if int(label) == self.DOG_CLASS_ID and float(score) >= best_score:
39
- best_score = float(score)
40
- best = box.tolist()
41
- if best is None:
42
- return None
43
- x1, y1, x2, y2 = best
44
- w, h = img.size
45
- px, py = (x2 - x1) * self.padding, (y2 - y1) * self.padding
46
- return (
47
- max(0, int(x1 - px)),
48
- max(0, int(y1 - py)),
49
- min(w, int(x2 + px)),
50
- min(h, int(y2 + py)),
51
- )
52
-
53
- def crop(self, img: Image.Image) -> Image.Image:
54
- box = self.detect_box(img)
55
- return img.crop(box) if box else img
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/embedder.py CHANGED
@@ -1,4 +1,4 @@
1
- """Embedder interface + Mock (default) and CNN (real, optional) implementations (spec §9.1).
2
 
3
  The Embedder is one of the four swap points. The matcher only depends on this Protocol, so a
4
  better model can be dropped in without schema or API changes (A6).
@@ -70,48 +70,6 @@ class MockEmbedder:
70
  return [self._vector_for(p) for p in image_paths]
71
 
72
 
73
- class CNNEmbedder: # pragma: no cover - exercised only when torch is installed (milestone 8)
74
- """Pretrained CNN backbone, classifier head removed, global-pooled + L2-normalized (spec §9.1).
75
-
76
- Lazily imports torch/torchvision so the mock path never requires them. Optional dog-crop
77
- preprocess is gated behind DETECTOR_ENABLED (added in milestone 8).
78
- """
79
-
80
- def __init__(self, model_name: str | None = None):
81
- import torch # noqa: F401
82
- import torchvision
83
- from torchvision.models import ResNet50_Weights
84
-
85
- self.name = model_name or settings.embedder_model
86
- self.version = "imagenet-v1"
87
- weights = ResNet50_Weights.DEFAULT
88
- backbone = torchvision.models.resnet50(weights=weights)
89
- import torch.nn as nn
90
-
91
- self._model = nn.Sequential(*list(backbone.children())[:-1]).eval()
92
- self._preprocess = weights.transforms()
93
- self.dim = 2048
94
- self._detector = None
95
- if settings.detector_enabled:
96
- from .detector import DogDetector
97
-
98
- self._detector = DogDetector()
99
-
100
- def embed(self, image_paths: list[str]) -> list[np.ndarray]:
101
- import torch
102
-
103
- out: list[np.ndarray] = []
104
- with torch.no_grad():
105
- for p in image_paths:
106
- img = Image.open(p).convert("RGB")
107
- if self._detector is not None:
108
- img = self._detector.crop(img)
109
- tensor = self._preprocess(img).unsqueeze(0)
110
- feat = self._model(tensor).flatten().cpu().numpy()
111
- out.append(_l2_normalize(feat))
112
- return out
113
-
114
-
115
  class HFEmbedder: # pragma: no cover - exercised only when transformers is installed
116
  """HuggingFace image model used as a re-ID embedder via its penultimate pooled features.
117
 
@@ -119,7 +77,7 @@ class HFEmbedder: # pragma: no cover - exercised only when transformers is inst
119
  individual-dog re-identification (per the project owner). We run the image through the model,
120
  take the last hidden state, pool it (global average for CNN feature maps, mean-over-tokens for
121
  transformer sequences), and L2-normalize. Breed *labels* are NOT used here — that is the
122
- separate BreedClassifier swap point. Lazily imports torch/transformers like CNNEmbedder.
123
  """
124
 
125
  def __init__(self, model_id: str | None = None):
@@ -195,16 +153,64 @@ class HFEmbedder: # pragma: no cover - exercised only when transformers is inst
195
  return results
196
 
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  _embedder: Embedder | None = None
199
 
200
 
201
  def get_embedder() -> Embedder:
202
  global _embedder
203
  if _embedder is None:
204
- if settings.embedder == "cnn":
205
- _embedder = CNNEmbedder()
206
- elif settings.embedder == "hf":
207
  _embedder = HFEmbedder()
 
 
208
  else:
209
  _embedder = MockEmbedder()
210
  return _embedder
 
1
+ """Embedder interface + Mock (default), HF, and fine-tuned re-ID implementations (spec §9.1).
2
 
3
  The Embedder is one of the four swap points. The matcher only depends on this Protocol, so a
4
  better model can be dropped in without schema or API changes (A6).
 
70
  return [self._vector_for(p) for p in image_paths]
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  class HFEmbedder: # pragma: no cover - exercised only when transformers is installed
74
  """HuggingFace image model used as a re-ID embedder via its penultimate pooled features.
75
 
 
77
  individual-dog re-identification (per the project owner). We run the image through the model,
78
  take the last hidden state, pool it (global average for CNN feature maps, mean-over-tokens for
79
  transformer sequences), and L2-normalize. Breed *labels* are NOT used here — that is the
80
+ separate BreedClassifier swap point. Lazily imports torch/transformers only when selected.
81
  """
82
 
83
  def __init__(self, model_id: str | None = None):
 
153
  return results
154
 
155
 
156
+ _MEAN, _STD = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] # ImageNet stats (re-ID preprocessing)
157
+
158
+
159
+ class ReIDEmbedder: # pragma: no cover - exercised only when torch is installed
160
+ """Fine-tuned re-ID embedder (EMBEDDER=reid): the breed backbone further trained with triplet
161
+ loss for individual-dog re-identification.
162
+
163
+ Loads a local checkpoint (a state_dict saved by scripts/train_reid.py's ``ReIDModel`` — keys are
164
+ prefixed ``backbone.``) into the same ResNet base as the breed model, and emits the L2-normalized
165
+ penultimate pooled features. Preprocessing matches TRAINING (Resize 224 + ImageNet norm), NOT the
166
+ HF image processor. This model produces NO breed labels — when it is the active embedder, breed
167
+ labels come from the separate breed classifier via images.py's two-model path.
168
+ """
169
+
170
+ def __init__(self, ckpt_path: str | None = None, base_model: str | None = None):
171
+ import torch # noqa: F401
172
+ import torchvision.transforms as T
173
+ from transformers import AutoModel
174
+
175
+ self.name = "reid"
176
+ self.version = settings.reid_model_version
177
+ base = base_model or settings.embedder_hf_model
178
+ self.model = AutoModel.from_pretrained(base)
179
+ raw = torch.load(ckpt_path or settings.reid_model_path, map_location="cpu")
180
+ # accept the ReIDModel wrapper's 'backbone.'-prefixed keys OR a bare backbone state_dict.
181
+ state = {k.removeprefix("backbone."): v for k, v in raw.items()}
182
+ self.model.load_state_dict(state)
183
+ self.model.eval()
184
+ self._device = "cuda" if torch.cuda.is_available() else "cpu"
185
+ self.model.to(self._device)
186
+ self._prep = T.Compose(
187
+ [T.Resize((224, 224)), T.ToTensor(), T.Normalize(_MEAN, _STD)]
188
+ )
189
+ self.dim = 2048
190
+
191
+ def embed(self, image_paths: list[str]) -> list[np.ndarray]:
192
+ import torch
193
+
194
+ out: list[np.ndarray] = []
195
+ with torch.no_grad():
196
+ for p in image_paths:
197
+ with Image.open(p) as img:
198
+ x = self._prep(img.convert("RGB")).unsqueeze(0).to(self._device)
199
+ feat = self.model(x).pooler_output.flatten(1)[0]
200
+ out.append(_l2_normalize(feat.cpu().numpy()))
201
+ return out
202
+
203
+
204
  _embedder: Embedder | None = None
205
 
206
 
207
  def get_embedder() -> Embedder:
208
  global _embedder
209
  if _embedder is None:
210
+ if settings.embedder == "hf":
 
 
211
  _embedder = HFEmbedder()
212
+ elif settings.embedder == "reid":
213
+ _embedder = ReIDEmbedder()
214
  else:
215
  _embedder = MockEmbedder()
216
  return _embedder
backend/demo_data/app.db CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:e17ecfb47a92704d6a687c4e0ff1b7815447dbddeea14e61927d0cd99f11f4af
3
- size 14491648
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3708ba8aa5761b5f6afe855f2c29b55b959eaf987d32018fad4dcb4a41ad4e5c
3
+ size 39477248
backend/demo_data/media/known/1/1e6abffb49aa43f1822fb9ed29356967.jpg DELETED

Git LFS Details

  • SHA256: ad757239a9aa12a640b2d45d49d50c2272b293d43344767f3dab5d8e76f005c5
  • Pointer size: 130 Bytes
  • Size of remote file: 10.9 kB
backend/demo_data/media/known/1/41bc76f5b36340b8b59e95f25a406113.jpg DELETED

Git LFS Details

  • SHA256: ff6381d0602874420d2590ccbda35b1762cd3469038b586cfcb36a89cd01387b
  • Pointer size: 130 Bytes
  • Size of remote file: 11.5 kB
backend/demo_data/media/known/1/ca4f3fabe3034731bfb7e47e98244bc4.jpg DELETED

Git LFS Details

  • SHA256: 0c7496b6a47fae90fcabda7061a2801ae44b889b080182f8d55e483ddcf1cd16
  • Pointer size: 130 Bytes
  • Size of remote file: 11.4 kB
backend/demo_data/media/known/11/4b97be269fac4e63953eba8b0e41c43e.jpg DELETED

Git LFS Details

  • SHA256: 8617ecda4e9cc409b124d17ea266b255b89216b9c5dbc06423bc0c400d74f289
  • Pointer size: 130 Bytes
  • Size of remote file: 27 kB
backend/demo_data/media/known/12/7f83295941134d89b033d8fec8dffd2f.jpg DELETED

Git LFS Details

  • SHA256: 2a5248e75e97a26e461767cc74cf79d594fc36bba56a58b708b0d5fc71073816
  • Pointer size: 130 Bytes
  • Size of remote file: 24 kB
backend/demo_data/media/known/12/a64dc67d5085476f881f971e73f52b2e.jpg DELETED

Git LFS Details

  • SHA256: f02c64b5b31fcacaa6ee30d0eee1e157c490903e234367353dddca0d30e97d47
  • Pointer size: 130 Bytes
  • Size of remote file: 19.9 kB
backend/demo_data/media/known/12/f93f2f92e16e43a79c0e1497ded8410a.jpg DELETED

Git LFS Details

  • SHA256: 2525cc6d2b531ab4a23d421046995ea62c458b4a4b6a2bf5c9c029b5c8d4654d
  • Pointer size: 130 Bytes
  • Size of remote file: 35.7 kB
backend/demo_data/media/known/13/450ea8a48cad492a9c49a11393eb8495.jpg DELETED

Git LFS Details

  • SHA256: 56351414e95391f682637d470c739dac5afc10fc521e14d11b4a9d356d263bb5
  • Pointer size: 130 Bytes
  • Size of remote file: 15.3 kB
backend/demo_data/media/known/13/b1d596d553fe4e3cae4c67fe46b379c3.jpg DELETED

Git LFS Details

  • SHA256: ba9b09bef5b46c73b6034eb679eafbc4455fb28aadf87cf1f05d9bbfaec86e58
  • Pointer size: 130 Bytes
  • Size of remote file: 12.9 kB
backend/demo_data/media/known/13/ee2e8e1152d740e6abfb0af8ef3c3f19.jpg DELETED

Git LFS Details

  • SHA256: 1c149cb854ef266029ceacc07e254c45c369fcbcf7b605d81c5cf01439f6cea2
  • Pointer size: 129 Bytes
  • Size of remote file: 4.45 kB
backend/demo_data/media/known/14/42294b0c875541fa878d6ab8225fb4de.jpg DELETED

Git LFS Details

  • SHA256: a90b0c3768c39cf6e3917d51fde98cd48c323d98af15c05917c4f3a9c85c3697
  • Pointer size: 129 Bytes
  • Size of remote file: 1.64 kB
backend/demo_data/media/known/14/c6f1598561454e65a522127bd8f01c6f.jpg DELETED

Git LFS Details

  • SHA256: 25d9bb875a62ebaa299878963acfe5108f30a18e00dafa5cd84a231c38232f30
  • Pointer size: 129 Bytes
  • Size of remote file: 2.11 kB
backend/demo_data/media/known/14/d86a962b033f466295244ea36f1789a9.jpg DELETED

Git LFS Details

  • SHA256: d7e8bc2799cbc8c111bc6c76b0245848db7f2ec7a8b319090303cef3d94aec7f
  • Pointer size: 129 Bytes
  • Size of remote file: 1.89 kB
backend/demo_data/media/known/15/101251303df44d1bac54e58b7b5e10d0.jpg DELETED

Git LFS Details

  • SHA256: 04060fc2a2de64efafa6f74b6eb9986d5dc5b7cd2a0fe3cc6859d9e5cce628fe
  • Pointer size: 129 Bytes
  • Size of remote file: 5.12 kB
backend/demo_data/media/known/15/3e1114b46fa64b98bb50d1f14bc38bda.jpg DELETED

Git LFS Details

  • SHA256: 1a1b9ca88eea8ec6091c6497f645e34109ca9e8e1e4b2811912e625c729f4324
  • Pointer size: 130 Bytes
  • Size of remote file: 10.5 kB
backend/demo_data/media/known/15/596faa34b27d411c9d7928bf41606f7b.jpg DELETED

Git LFS Details

  • SHA256: 5135316a2564d68238b465f3d06b251e74e1301060c72ba2912446f617acb6a4
  • Pointer size: 129 Bytes
  • Size of remote file: 5 kB
backend/demo_data/media/known/16/16023f39dfdf40ff8b10f46f9718ab2b.jpg DELETED

Git LFS Details

  • SHA256: fe2fb5223e4c4da734ee5251920c603bef959dabeb45d6c7427d837084379caa
  • Pointer size: 130 Bytes
  • Size of remote file: 38.9 kB
backend/demo_data/media/known/16/2f7b765e96bf4a9b834574b55403c196.jpg DELETED

Git LFS Details

  • SHA256: 8964f4aea8056ce9b3773cc0f3566daf28a89849fbbf8810f49d0731ea75e742
  • Pointer size: 130 Bytes
  • Size of remote file: 36 kB
backend/demo_data/media/known/16/955a2d571eed41909ecf81bbbf34fd13.jpg DELETED

Git LFS Details

  • SHA256: e6ccd04e0c67ebf7491c8ee8be53ef443c7ee659380720577bf780a69c1ad769
  • Pointer size: 130 Bytes
  • Size of remote file: 25.2 kB
backend/demo_data/media/known/18/630f9c5a28604f14a5c50d2b1fca5996.jpg DELETED

Git LFS Details

  • SHA256: 68b284ce1887a9c16fe6dfb07e65fcddbcc10ede365fcf60bd5ac5d40ab8186a
  • Pointer size: 129 Bytes
  • Size of remote file: 2.84 kB
backend/demo_data/media/known/18/a3694f54285e4aedb13b1502c5fd41b7.jpg DELETED

Git LFS Details

  • SHA256: 2b8faf0669710fa897e6ea6c26b55d1cac8a35c05dbce549ec69d05de08ffc6e
  • Pointer size: 129 Bytes
  • Size of remote file: 4.18 kB
backend/demo_data/media/known/18/fef796259d264e43a3efa784d59b789f.jpg DELETED

Git LFS Details

  • SHA256: a222a30aee78f538f31a9344c34b0bab6bd3fbbc899f1529c205da2c3686a48d
  • Pointer size: 129 Bytes
  • Size of remote file: 2.86 kB
backend/demo_data/media/known/19/2668ad9747c64280b49442b408892a00.jpg DELETED

Git LFS Details

  • SHA256: 6ca43f0e7704d2722537feca3867cddd48c7c62cb1da0e7562c59d7d5f50b4db
  • Pointer size: 130 Bytes
  • Size of remote file: 12.5 kB
backend/demo_data/media/known/19/3a8fcb16eea8449191a72b1e1f66b418.jpg DELETED

Git LFS Details

  • SHA256: c7004844f1cd4ea77b468acd1ec461ac23edf3d490cbe93e56757091ba8f16b7
  • Pointer size: 130 Bytes
  • Size of remote file: 27.8 kB
backend/demo_data/media/known/19/e3e4286484ff4295a3c2151d25ff8c1d.jpg DELETED

Git LFS Details

  • SHA256: 4d26224e916307749f14217924a0ebdc9968e737cfb50d66a6ce4c79ee3b5ab6
  • Pointer size: 129 Bytes
  • Size of remote file: 6.14 kB
backend/demo_data/media/known/2/229eef530f064b99b72a4cf1a04f3eab.jpg DELETED

Git LFS Details

  • SHA256: 0ff4c6f19df61f7f1ca1dbbcab242dc5058885b87f8dca84dad3d46eb98108bb
  • Pointer size: 129 Bytes
  • Size of remote file: 4.91 kB
backend/demo_data/media/known/2/97bfa63313074cd888736d131f82bc6c.jpg DELETED

Git LFS Details

  • SHA256: 6086805c271f8d3bbbcc0b8ff12e2b5e84cfab09756249400ea4491fb9b7f4d0
  • Pointer size: 129 Bytes
  • Size of remote file: 5.36 kB
backend/demo_data/media/known/2/d745cafed42f43478a4f37fec4981352.jpg DELETED

Git LFS Details

  • SHA256: b38ed539043b9c48bfb8d6eb7d0888e036b4f757facf82db988d3929f4ca88f4
  • Pointer size: 129 Bytes
  • Size of remote file: 3.96 kB
backend/demo_data/media/known/2/dd63a482808b4ba1884d42c7e99b116c.jpg DELETED

Git LFS Details

  • SHA256: 562f3831b3631aab94ab3b016c0eb9d2886f7cc9951eea4f82aa95360c4a0fcf
  • Pointer size: 129 Bytes
  • Size of remote file: 5.03 kB
backend/demo_data/media/known/21/0620a5b6c9794547925baeb807b1ecef.jpg DELETED

Git LFS Details

  • SHA256: 3fa210d97e7aa49083d4b6249e409bc907652a36ff11ae500b5c8b37e0290aa3
  • Pointer size: 130 Bytes
  • Size of remote file: 15.4 kB
backend/demo_data/media/known/21/5c0acc4e4c964f6d813093cf62d22ded.jpg DELETED

Git LFS Details

  • SHA256: c95591458c02b040e69bb210a58d1e07f3533ad908c309888d5b6e126547d5f8
  • Pointer size: 129 Bytes
  • Size of remote file: 4.57 kB
backend/demo_data/media/known/21/6d9c1ee992d44a26a6350f6eeb1b3de7.jpg DELETED

Git LFS Details

  • SHA256: 42bfc62ea02b6ec4ebcc8095a8eeeaca7c5d288f32a1063d40705d08967bcb38
  • Pointer size: 129 Bytes
  • Size of remote file: 4.17 kB
backend/demo_data/media/known/22/68e56438501c460abc3aaf05e91b8346.jpg DELETED

Git LFS Details

  • SHA256: 4a02e85bf67979eec5b5303004608b640c5ef7e74bbcb82fa9e4e198d4ac7fd5
  • Pointer size: 129 Bytes
  • Size of remote file: 3.06 kB
backend/demo_data/media/known/22/a1a82403e1d0442e96ac9be580db1f35.jpg DELETED

Git LFS Details

  • SHA256: bf0115346a88f1015d4b5d30d6b4e6df676c2234cf1e4e18c135fb8f3e8e0e69
  • Pointer size: 129 Bytes
  • Size of remote file: 4.58 kB
backend/demo_data/media/known/22/d7101488750c4478802eb72c0d6fa8ce.jpg DELETED

Git LFS Details

  • SHA256: 5c27566ebbe94974903d4f8b5b33247da06ed9cb6f287ffa1f49d44859956f7b
  • Pointer size: 130 Bytes
  • Size of remote file: 16.3 kB
backend/demo_data/media/known/23/091b968a546e47278d5d22054e73ab33.jpg DELETED

Git LFS Details

  • SHA256: d066e842c96da33f81cc0b849e3b4e70856c27a8ce8854c5a5887236d24f532d
  • Pointer size: 130 Bytes
  • Size of remote file: 33 kB
backend/demo_data/media/known/23/954a1c7a56ea46e5ae34b8b1fa2b5d24.jpg DELETED

Git LFS Details

  • SHA256: f5cc0ee6eff20b787686881ca3fa351dcef65dbc102d69d7600b529c9f7d8baf
  • Pointer size: 130 Bytes
  • Size of remote file: 61.2 kB
backend/demo_data/media/known/23/a42d8b7dec254893a8b4fbaf8ef34979.jpg DELETED

Git LFS Details

  • SHA256: 9b19e05115e74fc54a3f995da816f3c8ff89cfbb4447df0935b4eaec6aa6c2b4
  • Pointer size: 130 Bytes
  • Size of remote file: 27.8 kB