diff --git a/README.md b/README.md index f19c71d..34c96c8 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,7 @@ streamlined and repeatable process to monitor signs and signals along any roadwa * **ssoss_gui.py** - optional graphical front end built with Gooey that exposes the same features through a GUI. ## Requirements - Python 3.9 -- Required libraries: pandas, numpy, opencv-python, geopy, gpxpy, imageio, tqdm, lxml, pillow, piexif, timezonefinder, fastapi, uvicorn, jinja2 -HTMX is loaded from its CDN when using the photo review web app. +- Required libraries: pandas, numpy, opencv-python, geopy, gpxpy, imageio, tqdm, lxml, pillow, piexif, timezonefinder ## Installation Windows OS users can use the [Releases](https://github.com/redmond2742/ssoss/releases) to download an .exe of SSOSS for simple graphical usage. For Mac and Linux users, the command line option is described below. diff --git a/dev-requirements.in b/dev-requirements.in index 499761f..d895d1d 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -15,9 +15,3 @@ icecream click geopandas folium -httpx -fastapi -uvicorn[standard] -python-multipart -jinja2 -htmx diff --git a/dev-requirements.txt b/dev-requirements.txt index ba078b1..35a4e54 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -60,9 +60,3 @@ geopandas==1.1.0 # via -r dev-requirements.in folium==0.19.7 # via -r dev-requirements.in -httpx==0.26.0 -fastapi==0.97.0 -uvicorn[standard]==0.27.0.post1 -python-multipart==0.0.20 -jinja2==3.1.6 -htmx==* diff --git a/requirements.in b/requirements.in index e5fe3b1..83c1ab2 100644 --- a/requirements.in +++ b/requirements.in @@ -14,10 +14,4 @@ timezonefinder click geopandas folium -httpx -fastapi -uvicorn[standard] -python-multipart -jinja2 -htmx diff --git a/requirements.txt b/requirements.txt index d202963..42dc5c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,9 +47,3 @@ click==8.2.1 geopandas==1.1.0 folium==0.19.7 # via -r requirements.in -httpx==0.26.0 -fastapi==0.97.0 -uvicorn[standard]==0.27.0.post1 -python-multipart==0.0.20 -jinja2==3.1.6 -htmx==* diff --git a/src/ssoss/cli.py b/src/ssoss/cli.py index 60d93bc..8e31c56 100644 --- a/src/ssoss/cli.py +++ b/src/ssoss/cli.py @@ -3,7 +3,6 @@ from . import ssoss_cli from .signal_layer import build_signal_layer -from .cli import review_photos @click.group(invoke_without_command=True, add_help_option=False) @@ -28,7 +27,6 @@ def cli(ctx, show_help): cli.add_command(build_signal_layer) -cli.add_command(review_photos) if __name__ == "__main__": cli() diff --git a/src/ssoss/cli/__init__.py b/src/ssoss/cli/__init__.py deleted file mode 100644 index 8eea58d..0000000 --- a/src/ssoss/cli/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .review import review_photos - -__all__ = ["review_photos"] diff --git a/src/ssoss/cli/review.py b/src/ssoss/cli/review.py deleted file mode 100644 index 4cbd252..0000000 --- a/src/ssoss/cli/review.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path -import click -import uvicorn - -@click.command("review-photos") -@click.option("--src", type=click.Path(exists=True, file_okay=False), required=True) -@click.option("--port", type=int, default=8000, show_default=True) -def review_photos(src: str, port: int) -> None: - """Launch photo review server.""" - os.environ["SSOSS_PHOTO_SRC"] = str(Path(src)) - uvicorn.run("ssoss.web.reviewer:app", host="0.0.0.0", port=port) diff --git a/src/ssoss/web/reviewer.py b/src/ssoss/web/reviewer.py deleted file mode 100644 index 4d4fb30..0000000 --- a/src/ssoss/web/reviewer.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -import csv -from datetime import datetime, timezone -from pathlib import Path -from typing import List, Optional - -from fastapi import FastAPI, Request, Form, HTTPException -from fastapi.responses import FileResponse, HTMLResponse -from fastapi.templating import Jinja2Templates -from fastapi.staticfiles import StaticFiles -import asyncio - -IMAGE_EXTS = {".jpg", ".jpeg", ".png"} - -BASE_DIR = Path(__file__).resolve().parent -templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) - - -def _scan_unlabelled(src: Path, labels_csv: Path) -> List[Path]: - labelled = set() - if labels_csv.exists(): - with labels_csv.open(newline="") as f: - reader = csv.reader(f) - next(reader, None) - for row in reader: - if row: - labelled.add(row[0]) - files = [] - for ext in IMAGE_EXTS: - for p in src.rglob(f"*{ext}"): - if p.name not in labelled: - files.append(p) - files.sort() - return files - - -def create_app(src: Path) -> FastAPI: - app = FastAPI() - app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") - - @app.on_event("startup") - async def startup() -> None: - app.state.src = src - app.state.blocked_dir = src.parent / "blocked_signals" - app.state.clear_dir = src.parent / "clear_signals" - app.state.labels_csv = src.parent / "labels.csv" - app.state.blocked_dir.mkdir(parents=True, exist_ok=True) - app.state.clear_dir.mkdir(parents=True, exist_ok=True) - app.state.lock = asyncio.Lock() - app.state.queue = _scan_unlabelled(src, app.state.labels_csv) - - @app.get("/", response_class=HTMLResponse) - async def index(request: Request) -> HTMLResponse: - async with app.state.lock: - path = app.state.queue[0] if app.state.queue else None - img_id = path.relative_to(app.state.src).as_posix() if path else None - return templates.TemplateResponse("index.html", {"request": request, "id": img_id}) - - @app.get("/image/{img_id:path}") - async def image(img_id: str) -> FileResponse: - path = app.state.src / img_id - if not path.exists(): - raise HTTPException(status_code=404) - return FileResponse(str(path), media_type="image/jpeg") - - def _move(src_path: Path, dest_dir: Path) -> Path: - dest = dest_dir / src_path.name - n = 1 - while dest.exists(): - dest = dest_dir / f"{src_path.stem}_{n}{src_path.suffix}" - n += 1 - src_path.replace(dest) - return dest - - @app.post("/label", response_class=HTMLResponse) - async def label(request: Request, id: str = Form(...), label: str = Form(...)) -> HTMLResponse: - async with app.state.lock: - match = None - for i, p in enumerate(app.state.queue): - if p.relative_to(app.state.src).as_posix() == id: - match = app.state.queue.pop(i) - break - if match is None: - raise HTTPException(status_code=404, detail="Image not queued") - dest_dir = app.state.blocked_dir if label == "blocked" else app.state.clear_dir - dest = _move(match, dest_dir) - write_header = not app.state.labels_csv.exists() - with app.state.labels_csv.open("a", newline="") as f: - writer = csv.writer(f) - if write_header: - writer.writerow(["photo_name", "label", "timestamp_utc"]) - writer.writerow([dest.name, label, datetime.utcnow().replace(tzinfo=timezone.utc).isoformat()]) - async with app.state.lock: - next_path = app.state.queue[0] if app.state.queue else None - next_id = next_path.relative_to(app.state.src).as_posix() if next_path else None - return templates.TemplateResponse("fragment.html", {"request": request, "id": next_id}) - - return app - - -# For uvicorn 'ssoss.web.reviewer:app' -_app_src = os.environ.get("SSOSS_PHOTO_SRC") -if _app_src: - app = create_app(Path(_app_src)) -else: - app = FastAPI() diff --git a/src/ssoss/web/static/tailwind.css b/src/ssoss/web/static/tailwind.css deleted file mode 100644 index b634b91..0000000 --- a/src/ssoss/web/static/tailwind.css +++ /dev/null @@ -1 +0,0 @@ -@import url("https://cdn.tailwindcss.com"); diff --git a/src/ssoss/web/templates/fragment.html b/src/ssoss/web/templates/fragment.html deleted file mode 100644 index 01158b5..0000000 --- a/src/ssoss/web/templates/fragment.html +++ /dev/null @@ -1,9 +0,0 @@ -{% if id %} - -
- - -
-{% else %} -
All done!
-{% endif %} diff --git a/src/ssoss/web/templates/index.html b/src/ssoss/web/templates/index.html deleted file mode 100644 index dce8e6f..0000000 --- a/src/ssoss/web/templates/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - Photo Review - - -
- {% include 'fragment.html' %} -
- - - diff --git a/tests/test_reviewer.py b/tests/test_reviewer.py deleted file mode 100644 index 5e46a36..0000000 --- a/tests/test_reviewer.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -import csv -from pathlib import Path -from fastapi.testclient import TestClient -from PIL import Image - -root = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(root / "src")) -from ssoss.web import reviewer - - -def create_image(path: Path) -> None: - img = Image.new("RGB", (10, 10), color="white") - img.save(path) - - -def test_reviewer_flow(tmp_path): - src = tmp_path / "unclassified" - src.mkdir() - create_image(src / "a.jpg") - create_image(src / "b.jpg") - - app = reviewer.create_app(src) - with TestClient(app) as client: - client.get("/") - resp = client.post("/label", data={"id": "a.jpg", "label": "blocked"}) - assert resp.status_code == 200 - resp = client.post("/label", data={"id": "b.jpg", "label": "clear"}) - assert resp.status_code == 200 - - assert (tmp_path / "blocked_signals" / "a.jpg").exists() - assert (tmp_path / "clear_signals" / "b.jpg").exists() - - csv_path = tmp_path / "labels.csv" - with csv_path.open() as f: - rows = list(csv.reader(f)) - assert len(rows) == 3