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 %}
-
-