diff --git a/dev-requirements.in b/dev-requirements.in index d895d1d..98e68ac 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -15,3 +15,6 @@ icecream click geopandas folium + +fastapi +uvicorn diff --git a/dev-requirements.txt b/dev-requirements.txt index 35a4e54..ae5f6d0 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,61 +2,140 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile dev-requirements.in +# pip-compile --output-file=dev-requirements.txt dev-requirements.in # +annotated-types==0.7.0 + # via pydantic +anyio==4.9.0 + # via starlette asttokens==2.4.0 # via icecream +branca==0.8.1 + # via folium +certifi==2025.4.26 + # via + # pyogrio + # pyproj + # requests +cffi==1.17.1 + # via timezonefinder +charset-normalizer==3.4.2 + # via requests +click==8.2.1 + # via + # -r dev-requirements.in + # uvicorn colorama==0.4.6 # via icecream executing==2.0.0 # via icecream +fastapi==0.115.12 + # via -r dev-requirements.in +folium==0.19.7 + # via -r dev-requirements.in geographiclib==2.0 # via geopy +geopandas==1.1.0 + # via -r dev-requirements.in geopy==2.4.0 # via -r dev-requirements.in gpxpy==1.5.0 # via -r dev-requirements.in +h11==0.16.0 + # via uvicorn +h3==4.2.2 + # via timezonefinder icecream==2.1.3 # via -r dev-requirements.in +idna==3.10 + # via + # anyio + # requests imageio==2.31.5 # via -r dev-requirements.in +jinja2==3.1.6 + # via + # branca + # folium lxml==4.9.3 # via -r dev-requirements.in +markupsafe==3.0.2 + # via jinja2 numpy==1.26.0 # via # -r dev-requirements.in + # folium + # geopandas # imageio # opencv-python # pandas + # pyogrio + # shapely + # timezonefinder opencv-python==4.8.1.78 # via -r dev-requirements.in +packaging==25.0 + # via + # geopandas + # pyogrio pandas==2.1.1 + # via + # -r dev-requirements.in + # geopandas +piexif==1.1.3 # via -r dev-requirements.in pillow==10.0.1 # via # -r dev-requirements.in # imageio +pycparser==2.22 + # via cffi +pydantic==2.11.7 + # via fastapi +pydantic-core==2.33.2 + # via pydantic pygments==2.16.1 # via icecream +pyogrio==0.11.0 + # via geopandas +pyproj==3.7.1 + # via geopandas python-dateutil==2.8.2 # via # -r dev-requirements.in # pandas pytz==2023.3.post1 # via pandas +requests==2.32.4 + # via folium +shapely==2.1.1 + # via geopandas six==1.16.0 # via # asttokens # python-dateutil +sniffio==1.3.1 + # via anyio +starlette==0.46.2 + # via fastapi +timezonefinder==6.5.9 + # via -r dev-requirements.in tqdm==4.66.1 # via -r dev-requirements.in +typing-extensions==4.14.0 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.1 + # via pydantic tzdata==2023.3 # via pandas -timezonefinder==6.5.9 - # via -r dev-requirements.in -click==8.2.1 - # via -r dev-requirements.in -geopandas==1.1.0 - # via -r dev-requirements.in -folium==0.19.7 +urllib3==2.4.0 + # via requests +uvicorn==0.34.3 # via -r dev-requirements.in +xyzservices==2025.4.0 + # via folium diff --git a/pyproject.toml b/pyproject.toml index baedf24..29fe1d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,4 +28,5 @@ files = ["requirements.txt"] [project.scripts] ssoss = "ssoss.cli:cli" +ssoss-web = "ssoss.web_api:main" diff --git a/requirements.in b/requirements.in index 83c1ab2..e467150 100644 --- a/requirements.in +++ b/requirements.in @@ -15,3 +15,6 @@ click geopandas folium + +fastapi +uvicorn diff --git a/requirements.txt b/requirements.txt index c359b01..d7d8990 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,48 +2,128 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile +# pip-compile --output-file=requirements.txt requirements.in # +annotated-types==0.7.0 + # via pydantic +anyio==4.9.0 + # via starlette +branca==0.8.1 + # via folium +certifi==2025.4.26 + # via + # pyogrio + # pyproj + # requests +cffi==1.17.1 + # via timezonefinder +charset-normalizer==3.4.2 + # via requests +click==8.2.1 + # via + # -r requirements.in + # uvicorn +fastapi==0.115.12 + # via -r requirements.in +folium==0.19.7 + # via -r requirements.in geographiclib==2.0 # via geopy +geopandas==1.1.0 + # via -r requirements.in geopy==2.4.0 # via -r requirements.in gpxpy==1.5.0 # via -r requirements.in +h11==0.16.0 + # via uvicorn +h3==4.2.2 + # via timezonefinder +idna==3.10 + # via + # anyio + # requests imageio==2.31.5 # via -r requirements.in +jinja2==3.1.6 + # via + # branca + # folium lxml==4.9.3 # via -r requirements.in -numpy>=1.26,<2.0 +markupsafe==3.0.2 + # via jinja2 +numpy==2.3.0 # via # -r requirements.in + # folium + # geopandas # imageio # opencv-python # pandas + # pyogrio + # shapely + # timezonefinder opencv-python==4.8.1.78 # via -r requirements.in +packaging==25.0 + # via + # geopandas + # pyogrio pandas==2.1.1 + # via + # -r requirements.in + # geopandas +piexif==1.1.3 # via -r requirements.in pillow==10.0.1 # via # -r requirements.in # imageio -piexif==1.1.3 - # via -r requirements.in +pycparser==2.22 + # via cffi +pydantic==2.11.7 + # via fastapi +pydantic-core==2.33.2 + # via pydantic +pyogrio==0.11.0 + # via geopandas +pyproj==3.7.1 + # via geopandas python-dateutil==2.8.2 # via # -r requirements.in # pandas pytz==2023.3.post1 # via pandas +requests==2.32.4 + # via folium +shapely==2.1.1 + # via geopandas six==1.16.0 # via python-dateutil +sniffio==1.3.1 + # via anyio +starlette==0.46.2 + # via fastapi +timezonefinder==6.5.9 + # via -r requirements.in tqdm==4.66.1 # via -r requirements.in +typing-extensions==4.14.0 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.1 + # via pydantic tzdata==2023.3 # via pandas -timezonefinder==6.5.9 -click==8.2.1 -geopandas==1.1.0 -folium==0.19.7 +urllib3==2.4.0 + # via requests +uvicorn==0.34.3 # via -r requirements.in +xyzservices==2025.4.0 + # via folium diff --git a/src/ssoss/web_api.py b/src/ssoss/web_api.py new file mode 100644 index 0000000..bc2e04b --- /dev/null +++ b/src/ssoss/web_api.py @@ -0,0 +1,103 @@ +from fastapi import FastAPI, UploadFile, File, Form +from fastapi.responses import HTMLResponse, JSONResponse +from pathlib import Path +import tempfile +import shutil +from typing import Optional + +from . import ssoss_cli + +app = FastAPI(title="SSOSS Web API") + +@app.get("/", response_class=HTMLResponse) +def index(): + return """ + + +

SSOSS Web Interface

+
+ Static Object CSV:
+ GPX File:
+ Video File:
+ Sync Frame:
+ Sync Timestamp:
+ Autosync Filename Timestamp
+ Frame Extract Start:
+ Frame Extract End:
+ Label
+ GIF
+ Bounding Box
+ +
+ + + """ + + +def save_upload(upload: Optional[UploadFile], dst: Path) -> Optional[Path]: + if upload is None: + return None + path = dst / upload.filename + with path.open("wb") as f: + shutil.copyfileobj(upload.file, f) + return path + +@app.post("/process") +async def process( + static_object_file: Optional[UploadFile] = File(None), + gpx_file: Optional[UploadFile] = File(None), + video_file: Optional[UploadFile] = File(None), + sync_frame: Optional[int] = Form(None), + sync_timestamp: Optional[str] = Form(None), + autosync: bool = Form(False), + frame_extract_start: Optional[int] = Form(None), + frame_extract_end: Optional[int] = Form(None), + label: bool = Form(False), + gif: bool = Form(False), + bbox: bool = Form(False), +): + workdir = Path(tempfile.mkdtemp(prefix="ssoss_")) + + so_path = save_upload(static_object_file, workdir) + gpx_path = save_upload(gpx_file, workdir) + vid_path = save_upload(video_file, workdir) + + vid_sync = ("", "") + frame_extract = ("", "") + if autosync and vid_path is not None: + ts = ssoss_cli._timestamp_from_filename(vid_path.name) + vid_sync = (1, ts) + elif sync_frame is not None and sync_timestamp is not None: + vid_sync = (sync_frame, sync_timestamp) + if frame_extract_start is not None and frame_extract_end is not None: + frame_extract = (frame_extract_start, frame_extract_end) + + extra_out = (label, gif, bbox, False) + + so_f = open(so_path, "r") if so_path else None + gpx_f = open(gpx_path, "r") if gpx_path else None + vid_f = open(vid_path, "r") if vid_path else None + try: + ssoss_cli.args_static_obj_gpx_video( + generic_so_file=so_f, + gpx_file=gpx_f, + video_file=vid_f, + vid_sync=vid_sync, + frame_extract=frame_extract, + extra_out=extra_out, + autosync=autosync, + ) + finally: + for fh in (so_f, gpx_f, vid_f): + if fh: + fh.close() + + return JSONResponse({"output_dir": str(workdir)}) + + +def main(): + import uvicorn + uvicorn.run("ssoss.web_api:app", host="0.0.0.0", port=8000) + +if __name__ == "__main__": + main()