Spaces:
Sleeping
Sleeping
Change to API default directory
Browse files- Dockerfile +1 -1
- Makefile +43 -6
- README.md +37 -27
- app.py +3 -1
- pyproject.toml +1 -0
- src/{app β api}/__init__.py +0 -0
- src/{app β api}/app_factory.py +1 -1
- src/{app β api}/banks.py +0 -0
- src/{app β api}/clip_service.py +3 -3
- src/{app β api}/clip_store.py +4 -4
- src/{app β api}/deps.py +2 -2
- src/{app β api}/image_io.py +0 -0
- src/{app β api}/label_hash.py +0 -0
- src/{app β api}/logging_utils.py +0 -0
- src/{app β api}/main.py +101 -11
- src/{app β api}/middleware.py +0 -0
- src/{app β api}/registry.py +1 -1
- src/{app β api}/results.py +0 -0
- src/{app β api}/schemas.py +0 -0
- src/{app β api}/settings.py +0 -0
- src/api/splash.html +77 -0
- src/app/__pycache__/__init__.cpython-312.pyc +0 -0
- src/app/__pycache__/banks.cpython-312.pyc +0 -0
- src/app/__pycache__/clip_service.cpython-312.pyc +0 -0
- src/app/__pycache__/clip_store.cpython-312.pyc +0 -0
- src/app/__pycache__/deps.cpython-312.pyc +0 -0
- src/app/__pycache__/image_io.cpython-312.pyc +0 -0
- src/app/__pycache__/label_hash.cpython-312.pyc +0 -0
- src/app/__pycache__/logging_utils.cpython-312.pyc +0 -0
- src/app/__pycache__/main.cpython-312.pyc +0 -0
- src/app/__pycache__/middleware.cpython-312.pyc +0 -0
- src/app/__pycache__/registry.cpython-312.pyc +0 -0
- src/app/__pycache__/results.cpython-312.pyc +0 -0
- src/app/__pycache__/schemas.cpython-312.pyc +0 -0
- src/app/__pycache__/settings.cpython-312.pyc +0 -0
- src/eval/classify_dataset.py +6 -5
- src/eval/common.py +17 -0
- src/eval/eval_matrix.py +6 -5
- tests/__pycache__/conftest.cpython-312-pytest-8.3.2.pyc +0 -0
- tests/__pycache__/fakes.cpython-312.pyc +0 -0
- tests/__pycache__/test_integration_real_clip.cpython-312-pytest-8.3.2.pyc +0 -0
- tests/conftest.py +1 -1
- tests/fakes.py +4 -4
- tests/test_integration_real_clip.py +3 -3
- uv.lock +11 -0
Dockerfile
CHANGED
|
@@ -24,4 +24,4 @@ RUN uv sync --no-dev
|
|
| 24 |
|
| 25 |
ENV PATH="$HOME/app/.venv/bin:$PATH"
|
| 26 |
|
| 27 |
-
CMD ["uvicorn", "
|
|
|
|
| 24 |
|
| 25 |
ENV PATH="$HOME/app/.venv/bin:$PATH"
|
| 26 |
|
| 27 |
+
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
Makefile
CHANGED
|
@@ -1,18 +1,28 @@
|
|
| 1 |
-
.PHONY: help docker-build docker-run local-install local-run local-test local-test-integration eval-photo eval-dance eval-photo-matrix eval-dance-matrix data-photos data-dance
|
| 2 |
|
| 3 |
help:
|
| 4 |
@echo "---------------------------------------------------"
|
| 5 |
@echo "Targets:"
|
|
|
|
| 6 |
@echo " docker-build Build Docker image"
|
| 7 |
@echo " docker-run Run Docker image on :7860"
|
|
|
|
|
|
|
| 8 |
@echo " local-install Sync deps with uv"
|
| 9 |
@echo " local-run Run API locally on :7860"
|
| 10 |
@echo " local-test Run unit tests"
|
| 11 |
@echo " local-test-integration Run integration tests"
|
|
|
|
| 12 |
@echo " eval-photo Run eval on personal-photos-lite-v1"
|
| 13 |
@echo " eval-dance Run eval on scene-dance-formation-group-v1"
|
| 14 |
@echo " eval-photo-matrix Run eval for all personal-photos label sets"
|
| 15 |
@echo " eval-dance-matrix Run eval for all dance label sets"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
@echo " data-photos Download + normalize photo eval dataset"
|
| 17 |
@echo " data-dance Download + normalize dance eval dataset"
|
| 18 |
|
|
@@ -22,11 +32,14 @@ docker-build:
|
|
| 22 |
docker-run:
|
| 23 |
docker run --rm -p 7860:7860 photo-classification
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
local-install:
|
| 26 |
uv sync --extra dev --python 3.12
|
| 27 |
|
| 28 |
local-run:
|
| 29 |
-
uv run uvicorn
|
| 30 |
|
| 31 |
local-test:
|
| 32 |
uv run pytest -q
|
|
@@ -38,28 +51,52 @@ eval-photo:
|
|
| 38 |
uv run photo-eval single \
|
| 39 |
--label-set label-dataset/personal-photos-lite-v1.json \
|
| 40 |
--images data_eval/photos/normalized \
|
| 41 |
-
--out-dir data_results \
|
| 42 |
--summary
|
| 43 |
|
| 44 |
eval-dance:
|
| 45 |
uv run photo-eval single \
|
| 46 |
--label-set label-dataset/scene-dance-formation-group-v1.json \
|
| 47 |
--images data_eval/dance/normalized \
|
| 48 |
-
--out-dir data_results \
|
| 49 |
--summary
|
| 50 |
|
| 51 |
eval-photo-matrix:
|
| 52 |
uv run photo-eval matrix \
|
| 53 |
--label-sets "label-dataset/personal-photos-*.json" \
|
| 54 |
--images data_eval/photos/normalized \
|
| 55 |
-
--out-dir data_results \
|
| 56 |
--summary
|
| 57 |
|
| 58 |
eval-dance-matrix:
|
| 59 |
uv run photo-eval matrix \
|
| 60 |
--label-sets "label-dataset/scene-dance-*.json" \
|
| 61 |
--images data_eval/dance/normalized \
|
| 62 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
--summary
|
| 64 |
|
| 65 |
data-photos:
|
|
|
|
| 1 |
+
.PHONY: help docker-build docker-run local-install local-run local-test local-test-integration eval-photo eval-dance eval-photo-matrix eval-dance-matrix eval-photo-hf eval-dance-hf eval-photo-matrix-hf eval-dance-matrix-hf data-photos data-dance
|
| 2 |
|
| 3 |
help:
|
| 4 |
@echo "---------------------------------------------------"
|
| 5 |
@echo "Targets:"
|
| 6 |
+
@echo " "
|
| 7 |
@echo " docker-build Build Docker image"
|
| 8 |
@echo " docker-run Run Docker image on :7860"
|
| 9 |
+
@echo " docker-stop Stop Docker image"
|
| 10 |
+
@echo " "
|
| 11 |
@echo " local-install Sync deps with uv"
|
| 12 |
@echo " local-run Run API locally on :7860"
|
| 13 |
@echo " local-test Run unit tests"
|
| 14 |
@echo " local-test-integration Run integration tests"
|
| 15 |
+
@echo " "
|
| 16 |
@echo " eval-photo Run eval on personal-photos-lite-v1"
|
| 17 |
@echo " eval-dance Run eval on scene-dance-formation-group-v1"
|
| 18 |
@echo " eval-photo-matrix Run eval for all personal-photos label sets"
|
| 19 |
@echo " eval-dance-matrix Run eval for all dance label sets"
|
| 20 |
+
@echo " "
|
| 21 |
+
@echo " eval-photo-hf Run eval on HF Space (photo)"
|
| 22 |
+
@echo " eval-dance-hf Run eval on HF Space (dance)"
|
| 23 |
+
@echo " eval-photo-matrix-hf Run matrix eval on HF Space (photo)"
|
| 24 |
+
@echo " eval-dance-matrix-hf Run matrix eval on HF Space (dance)"
|
| 25 |
+
@echo " "
|
| 26 |
@echo " data-photos Download + normalize photo eval dataset"
|
| 27 |
@echo " data-dance Download + normalize dance eval dataset"
|
| 28 |
|
|
|
|
| 32 |
docker-run:
|
| 33 |
docker run --rm -p 7860:7860 photo-classification
|
| 34 |
|
| 35 |
+
docker-stop:
|
| 36 |
+
docker stop photo-classification
|
| 37 |
+
|
| 38 |
local-install:
|
| 39 |
uv sync --extra dev --python 3.12
|
| 40 |
|
| 41 |
local-run:
|
| 42 |
+
uv run uvicorn api.main:app --host 0.0.0.0 --port 7860 --reload
|
| 43 |
|
| 44 |
local-test:
|
| 45 |
uv run pytest -q
|
|
|
|
| 51 |
uv run photo-eval single \
|
| 52 |
--label-set label-dataset/personal-photos-lite-v1.json \
|
| 53 |
--images data_eval/photos/normalized \
|
|
|
|
| 54 |
--summary
|
| 55 |
|
| 56 |
eval-dance:
|
| 57 |
uv run photo-eval single \
|
| 58 |
--label-set label-dataset/scene-dance-formation-group-v1.json \
|
| 59 |
--images data_eval/dance/normalized \
|
|
|
|
| 60 |
--summary
|
| 61 |
|
| 62 |
eval-photo-matrix:
|
| 63 |
uv run photo-eval matrix \
|
| 64 |
--label-sets "label-dataset/personal-photos-*.json" \
|
| 65 |
--images data_eval/photos/normalized \
|
|
|
|
| 66 |
--summary
|
| 67 |
|
| 68 |
eval-dance-matrix:
|
| 69 |
uv run photo-eval matrix \
|
| 70 |
--label-sets "label-dataset/scene-dance-*.json" \
|
| 71 |
--images data_eval/dance/normalized \
|
| 72 |
+
--summary
|
| 73 |
+
|
| 74 |
+
eval-hf-photo:
|
| 75 |
+
uv run photo-eval single \
|
| 76 |
+
--api https://esandorfi-photo-classification.hf.space \
|
| 77 |
+
--label-set label-dataset/personal-photos-lite-v1.json \
|
| 78 |
+
--images data_eval/photos/normalized \
|
| 79 |
+
--summary
|
| 80 |
+
|
| 81 |
+
eval-hf-dance:
|
| 82 |
+
uv run photo-eval single \
|
| 83 |
+
--api https://esandorfi-photo-classification.hf.space \
|
| 84 |
+
--label-set label-dataset/scene-dance-formation-group-v1.json \
|
| 85 |
+
--images data_eval/dance/normalized \
|
| 86 |
+
--summary
|
| 87 |
+
|
| 88 |
+
eval-hf-photo-matrix:
|
| 89 |
+
uv run photo-eval matrix \
|
| 90 |
+
--api https://esandorfi-photo-classification.hf.space \
|
| 91 |
+
--label-sets "label-dataset/personal-photos-*.json" \
|
| 92 |
+
--images data_eval/photos/normalized \
|
| 93 |
+
--summary
|
| 94 |
+
|
| 95 |
+
eval-hf-dance-matrix:
|
| 96 |
+
uv run photo-eval matrix \
|
| 97 |
+
--api https://esandorfi-photo-classification.hf.space \
|
| 98 |
+
--label-sets "label-dataset/scene-dance-*.json" \
|
| 99 |
+
--images data_eval/dance/normalized \
|
| 100 |
--summary
|
| 101 |
|
| 102 |
data-photos:
|
README.md
CHANGED
|
@@ -9,13 +9,6 @@ app_port: 7860
|
|
| 9 |
|
| 10 |
# Photo Classification API
|
| 11 |
|
| 12 |
-
```
|
| 13 |
-
____ _ _ ____ _
|
| 14 |
-
| _ \| |__ ___ | |_ ___ / ___| | __ _ ___ ___
|
| 15 |
-
| |_) | '_ \ / _ \| __/ _ \ | | | |/ _` / __/ __|
|
| 16 |
-
| __/| | | | (_) | || (_) | | |___| | (_| \__ \__ \
|
| 17 |
-
|_| |_| |_|\___/ \__\___/ \____|_|\__,_|___/___/
|
| 18 |
-
```
|
| 19 |
|
| 20 |
A small, prompt-driven photo classification API built on CLIP. You upload a label set
|
| 21 |
(domains + labels with prompts), then classify images against that taxonomy without
|
|
@@ -51,7 +44,7 @@ Use these as starting points or create your own taxonomy.
|
|
| 51 |
|
| 52 |
## API quickstart
|
| 53 |
|
| 54 |
-
1) Start the service (Docker or `uvicorn app
|
| 55 |
2) Upload a label set.
|
| 56 |
3) Optionally activate a label set.
|
| 57 |
4) Classify images.
|
|
@@ -94,11 +87,11 @@ Guard policy example:
|
|
| 94 |
|
| 95 |
## Architecture
|
| 96 |
|
| 97 |
-
- API layer: FastAPI endpoints and request/response schemas (`src/
|
| 98 |
-
- Use-case layer: two-stage classification (domain -> labels) (`src/
|
| 99 |
-
- Model layer: CLIP model + processor + embedding banks (`src/
|
| 100 |
-
- Runtime support: registry, settings, logging, middleware (`src/
|
| 101 |
-
`src/
|
| 102 |
|
| 103 |
## Coding rules (deeper)
|
| 104 |
|
|
@@ -167,6 +160,16 @@ uv run photo-eval single \
|
|
| 167 |
|
| 168 |
Output CSV files are timestamped (UTC) in `data_results/`.
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
Makefile shortcuts:
|
| 171 |
|
| 172 |
- `make eval-photo`
|
|
@@ -226,18 +229,25 @@ uv run photo-eval prep --normalize-only --in-dir /path/to/images --out data_eval
|
|
| 226 |
βββ Dockerfile
|
| 227 |
βββ requirements.txt
|
| 228 |
βββ src
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
```
|
|
|
|
| 9 |
|
| 10 |
# Photo Classification API
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
A small, prompt-driven photo classification API built on CLIP. You upload a label set
|
| 14 |
(domains + labels with prompts), then classify images against that taxonomy without
|
|
|
|
| 44 |
|
| 45 |
## API quickstart
|
| 46 |
|
| 47 |
+
1) Start the service (Docker or `uvicorn app:app`).
|
| 48 |
2) Upload a label set.
|
| 49 |
3) Optionally activate a label set.
|
| 50 |
4) Classify images.
|
|
|
|
| 87 |
|
| 88 |
## Architecture
|
| 89 |
|
| 90 |
+
- API layer: FastAPI endpoints and request/response schemas (`src/api/main.py`, `src/api/schemas.py`).
|
| 91 |
+
- Use-case layer: two-stage classification (domain -> labels) (`src/api/clip_service.py`).
|
| 92 |
+
- Model layer: CLIP model + processor + embedding banks (`src/api/clip_store.py`, `src/api/banks.py`).
|
| 93 |
+
- Runtime support: registry, settings, logging, middleware (`src/api/registry.py`, `src/api/settings.py`,
|
| 94 |
+
`src/api/logging_utils.py`, `src/api/middleware.py`).
|
| 95 |
|
| 96 |
## Coding rules (deeper)
|
| 97 |
|
|
|
|
| 160 |
|
| 161 |
Output CSV files are timestamped (UTC) in `data_results/`.
|
| 162 |
|
| 163 |
+
Run evals against a remote Space by setting `--api`:
|
| 164 |
+
|
| 165 |
+
```bash
|
| 166 |
+
uv run photo-eval single \
|
| 167 |
+
--api https://esandorfi-photoclassification.hf.space \
|
| 168 |
+
--label-set label-dataset/personal-photos-lite-v1.json \
|
| 169 |
+
--images /path/to/images \
|
| 170 |
+
--summary
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
Makefile shortcuts:
|
| 174 |
|
| 175 |
- `make eval-photo`
|
|
|
|
| 229 |
βββ Dockerfile
|
| 230 |
βββ requirements.txt
|
| 231 |
βββ src
|
| 232 |
+
βββ api
|
| 233 |
+
β βββ banks.py
|
| 234 |
+
β βββ clip_service.py
|
| 235 |
+
β βββ clip_store.py
|
| 236 |
+
β βββ deps.py
|
| 237 |
+
β βββ image_io.py
|
| 238 |
+
β βββ label_hash.py
|
| 239 |
+
β βββ logging_utils.py
|
| 240 |
+
β βββ main.py
|
| 241 |
+
β βββ middleware.py
|
| 242 |
+
β βββ registry.py
|
| 243 |
+
β βββ results.py
|
| 244 |
+
β βββ schemas.py
|
| 245 |
+
β βββ settings.py
|
| 246 |
+
βββ eval
|
| 247 |
+
βββ README.md
|
| 248 |
+
βββ cli.py
|
| 249 |
+
βββ classify_dataset.py
|
| 250 |
+
βββ common.py
|
| 251 |
+
βββ dataset_prep.py
|
| 252 |
+
βββ eval_matrix.py
|
| 253 |
```
|
app.py
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
from api.main import app
|
| 2 |
+
|
| 3 |
+
__all__ = ["app"]
|
pyproject.toml
CHANGED
|
@@ -10,6 +10,7 @@ readme = "README.md"
|
|
| 10 |
requires-python = ">=3.12,<3.13"
|
| 11 |
dependencies = [
|
| 12 |
"fastapi==0.115.6",
|
|
|
|
| 13 |
"pillow==10.4.0",
|
| 14 |
"pydantic==2.8.2",
|
| 15 |
"torch==2.3.1",
|
|
|
|
| 10 |
requires-python = ">=3.12,<3.13"
|
| 11 |
dependencies = [
|
| 12 |
"fastapi==0.115.6",
|
| 13 |
+
"markdown==3.7",
|
| 14 |
"pillow==10.4.0",
|
| 15 |
"pydantic==2.8.2",
|
| 16 |
"torch==2.3.1",
|
src/{app β api}/__init__.py
RENAMED
|
File without changes
|
src/{app β api}/app_factory.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from fastapi import FastAPI
|
| 4 |
-
from
|
| 5 |
|
| 6 |
def create_app() -> FastAPI:
|
| 7 |
return build_app()
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from fastapi import FastAPI
|
| 4 |
+
from api.main import build_app # we'll define build_app in main.py
|
| 5 |
|
| 6 |
def create_app() -> FastAPI:
|
| 7 |
return build_app()
|
src/{app β api}/banks.py
RENAMED
|
File without changes
|
src/{app β api}/clip_service.py
RENAMED
|
@@ -5,9 +5,9 @@ from dataclasses import dataclass
|
|
| 5 |
|
| 6 |
import torch
|
| 7 |
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass(slots=True)
|
|
|
|
| 5 |
|
| 6 |
import torch
|
| 7 |
|
| 8 |
+
from api.banks import EmbeddingBank, LabelSetBank
|
| 9 |
+
from api.clip_store import ClipStore
|
| 10 |
+
from api.results import ClassificationResult, StageTimings
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass(slots=True)
|
src/{app β api}/clip_store.py
RENAMED
|
@@ -5,10 +5,10 @@ import warnings
|
|
| 5 |
import torch
|
| 6 |
from transformers import CLIPModel, CLIPProcessor
|
| 7 |
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
-
from
|
| 12 |
|
| 13 |
|
| 14 |
class ClipStore:
|
|
|
|
| 5 |
import torch
|
| 6 |
from transformers import CLIPModel, CLIPProcessor
|
| 7 |
|
| 8 |
+
from api.banks import EmbeddingBank, LabelSetBank
|
| 9 |
+
from api.label_hash import stable_hash
|
| 10 |
+
from api.schemas import LabelSet
|
| 11 |
+
from api.settings import settings
|
| 12 |
|
| 13 |
|
| 14 |
class ClipStore:
|
src/{app β api}/deps.py
RENAMED
|
@@ -4,8 +4,8 @@ from typing import Optional
|
|
| 4 |
|
| 5 |
from fastapi import Depends, HTTPException, Query, Request
|
| 6 |
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
|
| 10 |
|
| 11 |
def get_request_id(request: Request) -> str:
|
|
|
|
| 4 |
|
| 5 |
from fastapi import Depends, HTTPException, Query, Request
|
| 6 |
|
| 7 |
+
from api.banks import LabelSetBank
|
| 8 |
+
from api.registry import LabelSetRegistry
|
| 9 |
|
| 10 |
|
| 11 |
def get_request_id(request: Request) -> str:
|
src/{app β api}/image_io.py
RENAMED
|
File without changes
|
src/{app β api}/label_hash.py
RENAMED
|
File without changes
|
src/{app β api}/logging_utils.py
RENAMED
|
File without changes
|
src/{app β api}/main.py
RENAMED
|
@@ -5,16 +5,19 @@ from dataclasses import dataclass
|
|
| 5 |
from typing import Optional
|
| 6 |
|
| 7 |
from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response
|
| 8 |
-
from fastapi.responses import JSONResponse
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
from
|
| 14 |
-
from
|
| 15 |
-
from
|
| 16 |
-
from
|
| 17 |
-
from
|
|
|
|
|
|
|
|
|
|
| 18 |
ActivateResponse,
|
| 19 |
ClassifyRequest,
|
| 20 |
ClassifyResponse,
|
|
@@ -23,7 +26,7 @@ from app.schemas import (
|
|
| 23 |
LabelSetCreateResponse,
|
| 24 |
LabelSetInfo,
|
| 25 |
)
|
| 26 |
-
from
|
| 27 |
|
| 28 |
|
| 29 |
logger = setup_logging()
|
|
@@ -124,6 +127,93 @@ def create_app(*, resources: Resources | None = None) -> FastAPI:
|
|
| 124 |
)
|
| 125 |
return Response(content=svg, media_type="image/svg+xml")
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
@app.exception_handler(Exception)
|
| 128 |
async def unhandled_exception_handler(request: Request, exc: Exception):
|
| 129 |
rid = getattr(request.state, "request_id", None)
|
|
|
|
| 5 |
from typing import Optional
|
| 6 |
|
| 7 |
from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response
|
| 8 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import markdown
|
| 12 |
+
|
| 13 |
+
from api.clip_store import ClipStore
|
| 14 |
+
from api.clip_service import TwoStageClassifier
|
| 15 |
+
from api.deps import get_request_id, resolve_bank
|
| 16 |
+
from api.image_io import load_image_from_base64
|
| 17 |
+
from api.logging_utils import setup_logging, log_json
|
| 18 |
+
from api.middleware import RequestIdMiddleware
|
| 19 |
+
from api.registry import LabelSetRegistry
|
| 20 |
+
from api.schemas import (
|
| 21 |
ActivateResponse,
|
| 22 |
ClassifyRequest,
|
| 23 |
ClassifyResponse,
|
|
|
|
| 26 |
LabelSetCreateResponse,
|
| 27 |
LabelSetInfo,
|
| 28 |
)
|
| 29 |
+
from api.settings import settings
|
| 30 |
|
| 31 |
|
| 32 |
logger = setup_logging()
|
|
|
|
| 127 |
)
|
| 128 |
return Response(content=svg, media_type="image/svg+xml")
|
| 129 |
|
| 130 |
+
@app.get("/", include_in_schema=False)
|
| 131 |
+
def home() -> HTMLResponse:
|
| 132 |
+
splash_path = Path(__file__).with_name("splash.html")
|
| 133 |
+
try:
|
| 134 |
+
html = splash_path.read_text(encoding="utf-8")
|
| 135 |
+
except Exception:
|
| 136 |
+
html = "<h1>Photo Class</h1><p>Missing splash.html</p>"
|
| 137 |
+
return HTMLResponse(content=html)
|
| 138 |
+
|
| 139 |
+
@app.get("/readme", include_in_schema=False)
|
| 140 |
+
def readme() -> HTMLResponse:
|
| 141 |
+
readme_path = Path(__file__).resolve().parents[2] / "README.md"
|
| 142 |
+
try:
|
| 143 |
+
md_text = readme_path.read_text(encoding="utf-8")
|
| 144 |
+
except Exception:
|
| 145 |
+
md_text = "# Photo Classification API\n\nREADME.md not found."
|
| 146 |
+
|
| 147 |
+
if md_text.lstrip().startswith("---"):
|
| 148 |
+
parts = md_text.split("---", 2)
|
| 149 |
+
if len(parts) == 3:
|
| 150 |
+
md_text = parts[2].lstrip()
|
| 151 |
+
|
| 152 |
+
content_html = markdown.markdown(
|
| 153 |
+
md_text,
|
| 154 |
+
extensions=["fenced_code", "tables"],
|
| 155 |
+
output_format="html5",
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
html = f"""<!doctype html>
|
| 159 |
+
<html>
|
| 160 |
+
<head>
|
| 161 |
+
<meta charset="utf-8" />
|
| 162 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 163 |
+
<title>README</title>
|
| 164 |
+
<style>
|
| 165 |
+
:root {{
|
| 166 |
+
--bg: #f8fafc;
|
| 167 |
+
--card: #ffffff;
|
| 168 |
+
--ink: #0f172a;
|
| 169 |
+
--muted: #475569;
|
| 170 |
+
--line: #e2e8f0;
|
| 171 |
+
}}
|
| 172 |
+
* {{ box-sizing: border-box; }}
|
| 173 |
+
body {{
|
| 174 |
+
margin: 0;
|
| 175 |
+
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
| 176 |
+
color: var(--ink);
|
| 177 |
+
background: var(--bg);
|
| 178 |
+
}}
|
| 179 |
+
header {{
|
| 180 |
+
padding: 16px 20px;
|
| 181 |
+
border-bottom: 1px solid var(--line);
|
| 182 |
+
background: #fff;
|
| 183 |
+
}}
|
| 184 |
+
main {{
|
| 185 |
+
max-width: 980px;
|
| 186 |
+
margin: 0 auto;
|
| 187 |
+
padding: 24px 20px 40px;
|
| 188 |
+
}}
|
| 189 |
+
pre {{
|
| 190 |
+
background: #f1f5f9;
|
| 191 |
+
color: #0f172a;
|
| 192 |
+
padding: 12px;
|
| 193 |
+
border-radius: 10px;
|
| 194 |
+
overflow-x: auto;
|
| 195 |
+
}}
|
| 196 |
+
code {{
|
| 197 |
+
background: #e2e8f0;
|
| 198 |
+
padding: 2px 6px;
|
| 199 |
+
border-radius: 6px;
|
| 200 |
+
}}
|
| 201 |
+
a {{
|
| 202 |
+
color: #2563eb;
|
| 203 |
+
}}
|
| 204 |
+
</style>
|
| 205 |
+
</head>
|
| 206 |
+
<body>
|
| 207 |
+
<header>
|
| 208 |
+
<a href="/">Back</a>
|
| 209 |
+
</header>
|
| 210 |
+
<main>
|
| 211 |
+
{content_html}
|
| 212 |
+
</main>
|
| 213 |
+
</body>
|
| 214 |
+
</html>"""
|
| 215 |
+
return HTMLResponse(content=html)
|
| 216 |
+
|
| 217 |
@app.exception_handler(Exception)
|
| 218 |
async def unhandled_exception_handler(request: Request, exc: Exception):
|
| 219 |
rid = getattr(request.state, "request_id", None)
|
src/{app β api}/middleware.py
RENAMED
|
File without changes
|
src/{app β api}/registry.py
RENAMED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import Optional
|
| 5 |
-
from
|
| 6 |
|
| 7 |
|
| 8 |
@dataclass(slots=True)
|
|
|
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import Optional
|
| 5 |
+
from api.banks import LabelSetBank
|
| 6 |
|
| 7 |
|
| 8 |
@dataclass(slots=True)
|
src/{app β api}/results.py
RENAMED
|
File without changes
|
src/{app β api}/schemas.py
RENAMED
|
File without changes
|
src/{app β api}/settings.py
RENAMED
|
File without changes
|
src/api/splash.html
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Photo Class</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #f8fafc;
|
| 10 |
+
--card: #ffffff;
|
| 11 |
+
--ink: #0f172a;
|
| 12 |
+
--muted: #64748b;
|
| 13 |
+
--accent: #2563eb;
|
| 14 |
+
--line: #e2e8f0;
|
| 15 |
+
}
|
| 16 |
+
* { box-sizing: border-box; }
|
| 17 |
+
body {
|
| 18 |
+
margin: 0;
|
| 19 |
+
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
| 20 |
+
color: var(--ink);
|
| 21 |
+
background: radial-gradient(circle at 10% 0%, #dbeafe, transparent 35%),
|
| 22 |
+
radial-gradient(circle at 90% 20%, #fee2e2, transparent 40%),
|
| 23 |
+
var(--bg);
|
| 24 |
+
min-height: 100vh;
|
| 25 |
+
display: grid;
|
| 26 |
+
place-items: center;
|
| 27 |
+
padding: 24px;
|
| 28 |
+
}
|
| 29 |
+
.card {
|
| 30 |
+
width: min(820px, 100%);
|
| 31 |
+
background: var(--card);
|
| 32 |
+
border: 1px solid var(--line);
|
| 33 |
+
border-radius: 20px;
|
| 34 |
+
padding: 28px;
|
| 35 |
+
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
| 36 |
+
}
|
| 37 |
+
h1 {
|
| 38 |
+
margin: 0 0 8px;
|
| 39 |
+
font-size: clamp(24px, 3vw, 34px);
|
| 40 |
+
letter-spacing: 0.04em;
|
| 41 |
+
text-transform: uppercase;
|
| 42 |
+
}
|
| 43 |
+
p {
|
| 44 |
+
margin: 0 0 20px;
|
| 45 |
+
color: var(--muted);
|
| 46 |
+
}
|
| 47 |
+
.actions {
|
| 48 |
+
display: flex;
|
| 49 |
+
flex-wrap: wrap;
|
| 50 |
+
gap: 12px;
|
| 51 |
+
}
|
| 52 |
+
a.button {
|
| 53 |
+
text-decoration: none;
|
| 54 |
+
padding: 10px 16px;
|
| 55 |
+
border-radius: 999px;
|
| 56 |
+
border: 1px solid var(--line);
|
| 57 |
+
color: var(--ink);
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
background: #fff;
|
| 60 |
+
}
|
| 61 |
+
a.button.primary {
|
| 62 |
+
border-color: var(--accent);
|
| 63 |
+
color: var(--accent);
|
| 64 |
+
}
|
| 65 |
+
</style>
|
| 66 |
+
</head>
|
| 67 |
+
<body>
|
| 68 |
+
<div class="card">
|
| 69 |
+
<h1>Photo Class</h1>
|
| 70 |
+
<p>Prompt-driven photo classification with CLIP. Upload a label set, classify images, and inspect timings.</p>
|
| 71 |
+
<div class="actions">
|
| 72 |
+
<a class="button primary" href="/docs">API Docs</a>
|
| 73 |
+
<a class="button" href="/readme">README</a>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</body>
|
| 77 |
+
</html>
|
src/app/__pycache__/__init__.cpython-312.pyc
DELETED
|
Binary file (149 Bytes)
|
|
|
src/app/__pycache__/banks.cpython-312.pyc
DELETED
|
Binary file (964 Bytes)
|
|
|
src/app/__pycache__/clip_service.cpython-312.pyc
DELETED
|
Binary file (4.86 kB)
|
|
|
src/app/__pycache__/clip_store.cpython-312.pyc
DELETED
|
Binary file (5.74 kB)
|
|
|
src/app/__pycache__/deps.cpython-312.pyc
DELETED
|
Binary file (1.74 kB)
|
|
|
src/app/__pycache__/image_io.cpython-312.pyc
DELETED
|
Binary file (1.17 kB)
|
|
|
src/app/__pycache__/label_hash.cpython-312.pyc
DELETED
|
Binary file (737 Bytes)
|
|
|
src/app/__pycache__/logging_utils.cpython-312.pyc
DELETED
|
Binary file (1.57 kB)
|
|
|
src/app/__pycache__/main.cpython-312.pyc
DELETED
|
Binary file (14 kB)
|
|
|
src/app/__pycache__/middleware.cpython-312.pyc
DELETED
|
Binary file (1.09 kB)
|
|
|
src/app/__pycache__/registry.cpython-312.pyc
DELETED
|
Binary file (1.67 kB)
|
|
|
src/app/__pycache__/results.cpython-312.pyc
DELETED
|
Binary file (938 Bytes)
|
|
|
src/app/__pycache__/schemas.cpython-312.pyc
DELETED
|
Binary file (3.27 kB)
|
|
|
src/app/__pycache__/settings.cpython-312.pyc
DELETED
|
Binary file (705 Bytes)
|
|
|
src/eval/classify_dataset.py
CHANGED
|
@@ -20,7 +20,7 @@ class Config:
|
|
| 20 |
top_k: int
|
| 21 |
activate: bool
|
| 22 |
limit: int
|
| 23 |
-
out_dir: Path
|
| 24 |
csv_path: Path | None
|
| 25 |
summary: bool
|
| 26 |
select_domain_n: int | None
|
|
@@ -109,15 +109,16 @@ def run(cfg: Config) -> int:
|
|
| 109 |
"elapsed_labels_ms",
|
| 110 |
]
|
| 111 |
|
|
|
|
| 112 |
if cfg.csv_path and rows:
|
| 113 |
common.write_csv(cfg.csv_path, rows, fieldnames)
|
| 114 |
elif rows:
|
| 115 |
-
out_path =
|
| 116 |
common.write_csv(out_path, rows, fieldnames)
|
| 117 |
|
| 118 |
if cfg.summary:
|
| 119 |
summary = common.summarize_latency(rows)
|
| 120 |
-
summary_path =
|
| 121 |
common.write_csv(summary_path, [summary], list(summary.keys()))
|
| 122 |
|
| 123 |
return 0
|
|
@@ -131,7 +132,7 @@ def run(cfg: Config) -> int:
|
|
| 131 |
@click.option("--top-k", default=5, show_default=True, type=int)
|
| 132 |
@click.option("--activate", is_flag=True, default=False)
|
| 133 |
@click.option("--limit", default=0, show_default=True, type=int)
|
| 134 |
-
@click.option("--out-dir",
|
| 135 |
@click.option("--csv", "csv_path", type=click.Path(path_type=Path))
|
| 136 |
@click.option("--summary", is_flag=True, default=False)
|
| 137 |
@click.option("--select-domain-n", type=int, default=None)
|
|
@@ -146,7 +147,7 @@ def cli(
|
|
| 146 |
top_k: int,
|
| 147 |
activate: bool,
|
| 148 |
limit: int,
|
| 149 |
-
out_dir: Path,
|
| 150 |
csv_path: Path | None,
|
| 151 |
summary: bool,
|
| 152 |
select_domain_n: int | None,
|
|
|
|
| 20 |
top_k: int
|
| 21 |
activate: bool
|
| 22 |
limit: int
|
| 23 |
+
out_dir: Path | None
|
| 24 |
csv_path: Path | None
|
| 25 |
summary: bool
|
| 26 |
select_domain_n: int | None
|
|
|
|
| 109 |
"elapsed_labels_ms",
|
| 110 |
]
|
| 111 |
|
| 112 |
+
out_dir = common.resolve_out_dir(cfg.api, cfg.out_dir)
|
| 113 |
if cfg.csv_path and rows:
|
| 114 |
common.write_csv(cfg.csv_path, rows, fieldnames)
|
| 115 |
elif rows:
|
| 116 |
+
out_path = out_dir / f"{cfg.label_set.stem}_{common.timestamp()}.csv"
|
| 117 |
common.write_csv(out_path, rows, fieldnames)
|
| 118 |
|
| 119 |
if cfg.summary:
|
| 120 |
summary = common.summarize_latency(rows)
|
| 121 |
+
summary_path = out_dir / f"{cfg.label_set.stem}_summary_{common.timestamp()}.csv"
|
| 122 |
common.write_csv(summary_path, [summary], list(summary.keys()))
|
| 123 |
|
| 124 |
return 0
|
|
|
|
| 132 |
@click.option("--top-k", default=5, show_default=True, type=int)
|
| 133 |
@click.option("--activate", is_flag=True, default=False)
|
| 134 |
@click.option("--limit", default=0, show_default=True, type=int)
|
| 135 |
+
@click.option("--out-dir", type=click.Path(path_type=Path))
|
| 136 |
@click.option("--csv", "csv_path", type=click.Path(path_type=Path))
|
| 137 |
@click.option("--summary", is_flag=True, default=False)
|
| 138 |
@click.option("--select-domain-n", type=int, default=None)
|
|
|
|
| 147 |
top_k: int,
|
| 148 |
activate: bool,
|
| 149 |
limit: int,
|
| 150 |
+
out_dir: Path | None,
|
| 151 |
csv_path: Path | None,
|
| 152 |
summary: bool,
|
| 153 |
select_domain_n: int | None,
|
src/eval/common.py
CHANGED
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
| 7 |
from datetime import datetime, timezone
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Iterable
|
|
|
|
| 10 |
|
| 11 |
import httpx
|
| 12 |
|
|
@@ -101,6 +102,22 @@ def timestamp() -> str:
|
|
| 101 |
return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 102 |
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
def write_csv(path: Path, rows: list[dict[str, str]], fieldnames: list[str]) -> None:
|
| 105 |
path.parent.mkdir(parents=True, exist_ok=True)
|
| 106 |
with path.open("w", newline="", encoding="utf-8") as f:
|
|
|
|
| 7 |
from datetime import datetime, timezone
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Iterable
|
| 10 |
+
from urllib.parse import urlparse
|
| 11 |
|
| 12 |
import httpx
|
| 13 |
|
|
|
|
| 102 |
return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 103 |
|
| 104 |
|
| 105 |
+
def api_slug(api: str) -> str:
|
| 106 |
+
parsed = urlparse(api)
|
| 107 |
+
host = parsed.netloc or parsed.path
|
| 108 |
+
host = host.replace("http://", "").replace("https://", "")
|
| 109 |
+
host = host.strip("/")
|
| 110 |
+
if host in {"localhost:7860", "localhost", "127.0.0.1:7860", "127.0.0.1"}:
|
| 111 |
+
return "local"
|
| 112 |
+
return "".join(ch if ch.isalnum() or ch in {"-", "."} else "-" for ch in host)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def resolve_out_dir(api: str, out_dir: Path | None) -> Path:
|
| 116 |
+
if out_dir is not None:
|
| 117 |
+
return out_dir
|
| 118 |
+
return Path("data_results") / api_slug(api)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
def write_csv(path: Path, rows: list[dict[str, str]], fieldnames: list[str]) -> None:
|
| 122 |
path.parent.mkdir(parents=True, exist_ok=True)
|
| 123 |
with path.open("w", newline="", encoding="utf-8") as f:
|
src/eval/eval_matrix.py
CHANGED
|
@@ -19,7 +19,7 @@ class Config:
|
|
| 19 |
images: list[Path]
|
| 20 |
domain_top_n: int
|
| 21 |
top_k: int
|
| 22 |
-
out_dir: Path
|
| 23 |
summary: bool
|
| 24 |
select_domain_n: int | None
|
| 25 |
select_label_n: int | None
|
|
@@ -140,12 +140,13 @@ def run(cfg: Config) -> None:
|
|
| 140 |
"elapsed_labels_ms",
|
| 141 |
]
|
| 142 |
|
| 143 |
-
|
|
|
|
| 144 |
common.write_csv(out_path, rows, fieldnames)
|
| 145 |
|
| 146 |
if cfg.summary:
|
| 147 |
summary_rows = summarize_by_label_set(rows)
|
| 148 |
-
summary_path =
|
| 149 |
common.write_csv(summary_path, summary_rows, ["label_set", "count", "avg_elapsed_ms", "p50_elapsed_ms", "p95_elapsed_ms"])
|
| 150 |
|
| 151 |
|
|
@@ -155,7 +156,7 @@ def run(cfg: Config) -> None:
|
|
| 155 |
@click.option("--images", multiple=True, required=True, type=click.Path(path_type=Path))
|
| 156 |
@click.option("--domain-top-n", default=2, show_default=True, type=int)
|
| 157 |
@click.option("--top-k", default=5, show_default=True, type=int)
|
| 158 |
-
@click.option("--out-dir",
|
| 159 |
@click.option("--summary", is_flag=True, default=False)
|
| 160 |
@click.option("--select-domain-n", type=int, default=None)
|
| 161 |
@click.option("--select-label-n", type=int, default=None)
|
|
@@ -167,7 +168,7 @@ def cli(
|
|
| 167 |
images: tuple[Path, ...],
|
| 168 |
domain_top_n: int,
|
| 169 |
top_k: int,
|
| 170 |
-
out_dir: Path,
|
| 171 |
summary: bool,
|
| 172 |
select_domain_n: int | None,
|
| 173 |
select_label_n: int | None,
|
|
|
|
| 19 |
images: list[Path]
|
| 20 |
domain_top_n: int
|
| 21 |
top_k: int
|
| 22 |
+
out_dir: Path | None
|
| 23 |
summary: bool
|
| 24 |
select_domain_n: int | None
|
| 25 |
select_label_n: int | None
|
|
|
|
| 140 |
"elapsed_labels_ms",
|
| 141 |
]
|
| 142 |
|
| 143 |
+
out_dir = common.resolve_out_dir(cfg.api, cfg.out_dir)
|
| 144 |
+
out_path = out_dir / f"eval_matrix_{common.timestamp()}.csv"
|
| 145 |
common.write_csv(out_path, rows, fieldnames)
|
| 146 |
|
| 147 |
if cfg.summary:
|
| 148 |
summary_rows = summarize_by_label_set(rows)
|
| 149 |
+
summary_path = out_dir / f"eval_matrix_summary_{common.timestamp()}.csv"
|
| 150 |
common.write_csv(summary_path, summary_rows, ["label_set", "count", "avg_elapsed_ms", "p50_elapsed_ms", "p95_elapsed_ms"])
|
| 151 |
|
| 152 |
|
|
|
|
| 156 |
@click.option("--images", multiple=True, required=True, type=click.Path(path_type=Path))
|
| 157 |
@click.option("--domain-top-n", default=2, show_default=True, type=int)
|
| 158 |
@click.option("--top-k", default=5, show_default=True, type=int)
|
| 159 |
+
@click.option("--out-dir", type=click.Path(path_type=Path))
|
| 160 |
@click.option("--summary", is_flag=True, default=False)
|
| 161 |
@click.option("--select-domain-n", type=int, default=None)
|
| 162 |
@click.option("--select-label-n", type=int, default=None)
|
|
|
|
| 168 |
images: tuple[Path, ...],
|
| 169 |
domain_top_n: int,
|
| 170 |
top_k: int,
|
| 171 |
+
out_dir: Path | None,
|
| 172 |
summary: bool,
|
| 173 |
select_domain_n: int | None,
|
| 174 |
select_label_n: int | None,
|
tests/__pycache__/conftest.cpython-312-pytest-8.3.2.pyc
CHANGED
|
Binary files a/tests/__pycache__/conftest.cpython-312-pytest-8.3.2.pyc and b/tests/__pycache__/conftest.cpython-312-pytest-8.3.2.pyc differ
|
|
|
tests/__pycache__/fakes.cpython-312.pyc
CHANGED
|
Binary files a/tests/__pycache__/fakes.cpython-312.pyc and b/tests/__pycache__/fakes.cpython-312.pyc differ
|
|
|
tests/__pycache__/test_integration_real_clip.cpython-312-pytest-8.3.2.pyc
CHANGED
|
Binary files a/tests/__pycache__/test_integration_real_clip.cpython-312-pytest-8.3.2.pyc and b/tests/__pycache__/test_integration_real_clip.cpython-312-pytest-8.3.2.pyc differ
|
|
|
tests/conftest.py
CHANGED
|
@@ -6,7 +6,7 @@ import pytest
|
|
| 6 |
from PIL import Image
|
| 7 |
from fastapi.testclient import TestClient
|
| 8 |
|
| 9 |
-
from
|
| 10 |
from tests.fakes import FakeClipStore, FakeTwoStageClassifier
|
| 11 |
|
| 12 |
|
|
|
|
| 6 |
from PIL import Image
|
| 7 |
from fastapi.testclient import TestClient
|
| 8 |
|
| 9 |
+
from api.main import build_app
|
| 10 |
from tests.fakes import FakeClipStore, FakeTwoStageClassifier
|
| 11 |
|
| 12 |
|
tests/fakes.py
CHANGED
|
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
|
| 5 |
-
from
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
|
| 10 |
|
| 11 |
class FakeClipStore:
|
|
|
|
| 2 |
|
| 3 |
from dataclasses import dataclass
|
| 4 |
|
| 5 |
+
from api.banks import EmbeddingBank, LabelSetBank
|
| 6 |
+
from api.label_hash import stable_hash
|
| 7 |
+
from api.results import ClassificationResult, StageTimings
|
| 8 |
+
from api.schemas import LabelSet
|
| 9 |
|
| 10 |
|
| 11 |
class FakeClipStore:
|
tests/test_integration_real_clip.py
CHANGED
|
@@ -6,9 +6,9 @@ import pytest
|
|
| 6 |
from PIL import Image
|
| 7 |
from fastapi.testclient import TestClient
|
| 8 |
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
-
from
|
| 12 |
|
| 13 |
|
| 14 |
@pytest.mark.integration
|
|
|
|
| 6 |
from PIL import Image
|
| 7 |
from fastapi.testclient import TestClient
|
| 8 |
|
| 9 |
+
from api.main import build_app
|
| 10 |
+
from api.clip_store import ClipStore
|
| 11 |
+
from api.clip_service import TwoStageClassifier
|
| 12 |
|
| 13 |
|
| 14 |
@pytest.mark.integration
|
uv.lock
CHANGED
|
@@ -366,6 +366,15 @@ wheels = [
|
|
| 366 |
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
| 367 |
]
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
[[package]]
|
| 370 |
name = "markupsafe"
|
| 371 |
version = "3.0.3"
|
|
@@ -621,6 +630,7 @@ version = "0.1.0"
|
|
| 621 |
source = { editable = "." }
|
| 622 |
dependencies = [
|
| 623 |
{ name = "fastapi" },
|
|
|
|
| 624 |
{ name = "pillow" },
|
| 625 |
{ name = "pydantic" },
|
| 626 |
{ name = "torch" },
|
|
@@ -644,6 +654,7 @@ requires-dist = [
|
|
| 644 |
{ name = "datasets", marker = "extra == 'dev'", specifier = "==2.21.0" },
|
| 645 |
{ name = "fastapi", specifier = "==0.115.6" },
|
| 646 |
{ name = "httpx", marker = "extra == 'dev'", specifier = "==0.27.2" },
|
|
|
|
| 647 |
{ name = "pillow", specifier = "==10.4.0" },
|
| 648 |
{ name = "pydantic", specifier = "==2.8.2" },
|
| 649 |
{ name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.2" },
|
|
|
|
| 366 |
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
| 367 |
]
|
| 368 |
|
| 369 |
+
[[package]]
|
| 370 |
+
name = "markdown"
|
| 371 |
+
version = "3.7"
|
| 372 |
+
source = { registry = "https://pypi.org/simple" }
|
| 373 |
+
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" }
|
| 374 |
+
wheels = [
|
| 375 |
+
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" },
|
| 376 |
+
]
|
| 377 |
+
|
| 378 |
[[package]]
|
| 379 |
name = "markupsafe"
|
| 380 |
version = "3.0.3"
|
|
|
|
| 630 |
source = { editable = "." }
|
| 631 |
dependencies = [
|
| 632 |
{ name = "fastapi" },
|
| 633 |
+
{ name = "markdown" },
|
| 634 |
{ name = "pillow" },
|
| 635 |
{ name = "pydantic" },
|
| 636 |
{ name = "torch" },
|
|
|
|
| 654 |
{ name = "datasets", marker = "extra == 'dev'", specifier = "==2.21.0" },
|
| 655 |
{ name = "fastapi", specifier = "==0.115.6" },
|
| 656 |
{ name = "httpx", marker = "extra == 'dev'", specifier = "==0.27.2" },
|
| 657 |
+
{ name = "markdown", specifier = "==3.7" },
|
| 658 |
{ name = "pillow", specifier = "==10.4.0" },
|
| 659 |
{ name = "pydantic", specifier = "==2.8.2" },
|
| 660 |
{ name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.2" },
|