Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ dependencies = [
"av>=13",
"opencv-python-headless>=4.13",
"natsort>=8.4.0",
# The interactive web viewer/corrector (`deeperfly gui`): FastAPI + uvicorn
# serve a browser front-end (the compiled assets ship in the wheel under
# deeperfly/gui/web). Kept in the core deps -- they're tiny next to torch/jax
# and `deeperfly gui` is a first-class subcommand, so a plain install must be
# able to run it. The `[standard]` uvicorn extra pulls the WebSocket +
# http-tools speedups; both are still imported lazily (only `gui` needs them).
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
]

[project.optional-dependencies]
# The interactive viewer/corrector (`deeperfly gui`). Optional: the core package
# and CLI import fine without it. PySide6 is the official Qt for Python binding
# (LGPLv3). Install with `pip install deeperfly[gui]` / `uv sync --extra gui`.
gui = ["PySide6>=6.6"]

[project.urls]
Homepage = "https://github.com/NeLy-EPFL/deeperfly"
Documentation = "https://nely-epfl.github.io/deeperfly/"
Expand All @@ -82,7 +84,10 @@ dev = [
test = [
"pytest>=9.0.3",
"pytest-cov>=6.0",
"PySide6>=6.6", # exercise the optional GUI tests headlessly (QT_QPA_PLATFORM=offscreen)
# Exercise the web GUI: FastAPI's TestClient (httpx-backed) drives the API +
# WebSocket in-process, no real server or browser needed. FastAPI/uvicorn
# themselves are core deps; only the httpx test client is test-only.
"httpx>=0.27",
]
docs = [
# The documentation site (MkDocs + Material). The library API reference is
Expand Down
50 changes: 43 additions & 7 deletions src/deeperfly/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,53 @@ def gui(
"results.h5 no longer resolve",
),
] = None,
host: Annotated[
str,
typer.Option(
"--host",
help="address to bind the server to; the loopback default keeps the "
"editor private (bind a routable address only behind a trusted "
"network -- it is unauthenticated; prefer an 'ssh -L' tunnel)",
),
] = "127.0.0.1",
port: Annotated[
int,
typer.Option("--port", help="TCP port to serve on (0 picks a free one)"),
] = 8000,
no_browser: Annotated[
bool,
typer.Option("--no-browser", help="do not open a browser on startup"),
] = False,
keep_alive: Annotated[
bool,
typer.Option(
"--keep-alive",
help="keep the server running after the browser is closed (by default "
"it stops a few seconds after the last tab closes; a refresh reconnects)",
),
] = False,
log_level: LogLevelOption = LogLevel.info,
) -> None:
"""Open the interactive viewer/corrector on a result (needs the 'gui' extra).

View every camera with its 2D skeleton overlay, drag keypoints to correct
the 2D pose, or switch to 3D mode to drag a reprojected 3D point (the other
views update live). Corrections are written to a corrections.h5 sidecar and
never modify results.h5. Install the viewer with 'pip install deeperfly[gui]'.
"""Serve the interactive web viewer/corrector for a result.

Starts a local server and opens a browser editor. View every camera with its
2D skeleton overlay, drag keypoints to correct the 2D pose, or switch to 3D
mode to drag a reprojected 3D point (the other views update live).
Corrections are written to a corrections.h5 sidecar and never modify
results.h5. It runs headless and can be reached from another machine's
browser (default-bound to localhost; tunnel with 'ssh -L' for remote use).
"""
_configure_logging(log_level.value)
_cmd_gui(argparse.Namespace(path=path, footage_dir=footage_dir))
_cmd_gui(
argparse.Namespace(
path=path,
footage_dir=footage_dir,
host=host,
port=port,
no_browser=no_browser,
keep_alive=keep_alive,
)
)


def _normalize_overwrite_argv(argv: list[str]) -> list[str]:
Expand Down
39 changes: 30 additions & 9 deletions src/deeperfly/cli/gui.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"""The ``gui`` command worker: launch the interactive viewer/corrector.
"""The ``gui`` command worker: launch the interactive web viewer/corrector.

Kept thin and free of any Qt import at module load -- the heavy PySide6 import
happens inside :func:`deeperfly.gui.launch`, so ``deeperfly`` (and this module)
import fine without the optional ``gui`` extra installed.
Kept thin and free of any web import at module load -- the FastAPI/uvicorn
import happens inside :func:`deeperfly.gui.serve`, so importing ``deeperfly``
(and this module) stays cheap for every command other than ``gui``.
"""

from __future__ import annotations

import argparse
import logging
from pathlib import Path

log = logging.getLogger("deeperfly")

#: Bind addresses that keep the editor on the local machine (no warning).
_LOOPBACK = ("127.0.0.1", "localhost", "::1")


def _find_results(path: Path) -> Path:
"""Resolve ``path`` to a ``results.h5`` file.
Expand Down Expand Up @@ -48,24 +54,39 @@ def _find_results(path: Path) -> Path:


def _cmd_gui(args: argparse.Namespace) -> None:
"""Launch the GUI on the result resolved from ``args.path``.
"""Serve the web GUI on the result resolved from ``args.path``.

Parameters
----------
args
The ``gui`` namespace (``path``, ``footage_dir``).
The ``gui`` namespace (``path``, ``footage_dir``, ``host``, ``port``,
``no_browser``, ``keep_alive``).

Raises
------
SystemExit
If no result is found, or the optional ``gui`` extra is not installed.
If no result is found, or the web stack fails to import (an incomplete
install -- FastAPI + uvicorn are core dependencies).
"""
results_path = _find_results(Path(args.path))
if args.host not in _LOOPBACK:
log.warning(
"binding %s exposes the editor on the network without authentication; "
"prefer the default localhost and an `ssh -L` tunnel for remote use",
args.host,
)
try:
from ..gui import launch
from ..gui import serve
except ImportError as exc: # pragma: no cover -- exercised manually
raise SystemExit(str(exc)) from exc
try:
launch(results_path, footage_dir=args.footage_dir)
serve(
results_path,
footage_dir=args.footage_dir,
host=args.host,
port=args.port,
open_browser=not args.no_browser,
exit_on_close=not args.keep_alive,
)
except ImportError as exc:
raise SystemExit(str(exc)) from exc
11 changes: 7 additions & 4 deletions src/deeperfly/cli/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,15 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
_doctor_row("image read", "opencv" if have_cv2 else "opencv not installed")

_doctor_header("gui")
have_qt = importlib.util.find_spec("PySide6") is not None
have_web = (
importlib.util.find_spec("fastapi") is not None
and importlib.util.find_spec("uvicorn") is not None
)
_doctor_row(
"deeperfly gui",
"PySide6 available"
if have_qt
else "not installed -- run 'pip install deeperfly[gui]'",
"FastAPI + uvicorn available"
if have_web
else "missing -- core deps absent, reinstall deeperfly",
)

_doctor_header("weights")
Expand Down
Loading
Loading