Add web UI: same backend, browser front end
Reuses the analysis backend unchanged. metrics.py already split compute() from build_spec(), and plotspec.PlotSpec is backend-agnostic, so the web UI reimplements only the renderer: uPlot for curves/bands, a canvas for the spectrogram heatmap. No PyQt5, pyqtgraph or matplotlib on the web path. Uploaded audio is never persisted on the host. The browser copies each file into Origin Private File System (sandboxed browser storage) and streams the bytes to the server per request; the server spools each body to a temp file, decodes, computes, and unlinks it in a finally before responding. There is no upload directory, no cache of audio, and no session state keyed on a path: file_id is the SHA-256 of the audio, so re-sending a master for another metric reuses the first decode. Verified: a spool is observable mid-request and gone after the response. Being precise: the bytes do have to reach the server, since librosa decodes from a path and the analysis is CPU-bound numpy/scipy. The claim is that running this does not accumulate masters on the host, not that audio is never on the machine mid-request. New: metrics_json.py PlotSpec/metric data -> JSON (the renderer seam) web_session.py headless analysis session, content-addressed tracks webapp.py FastAPI routes, raw-body analysis, ujm-web entry point static/ browser front end + test suite scripts/webapi_smoke.py 61 end-to-end assertions against a running server Also fixes a bug the direct-uvicorn dev loop had hidden: the CLI advertises logger_setup's WARN level but passed it straight to uvicorn, which only knows 'warning' -- `ujm-web --log-level WARN` died with KeyError: 'warn'. Removes an earlier /api/collect debug route that served every source module over HTTP. pyproject: 'web' extra (fastapi, uvicorn, python-multipart), package-data for static/, Apache-2.0 licence.
This commit is contained in:
@@ -109,6 +109,50 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
|
||||
gone with the matplotlib render path. If wanted back, it belongs in the
|
||||
renderer, applied uniformly to every metric — not per-metric
|
||||
|
||||
#### `web_session.py`
|
||||
- Server-side equivalent of `AnalysisResultsManager`, without Qt: owns the
|
||||
per-track metric cache and the overlay set, and mirrors the desktop's
|
||||
compute-off-the-request-thread behaviour
|
||||
- **No audio is stored.** Tracks are keyed by `file_id` = SHA-256 of the audio,
|
||||
and the session keeps only analysis results and metadata. There is no upload
|
||||
directory and no cache of bytes
|
||||
- Because the key is a content hash, re-sending the same master for a second
|
||||
metric reuses the first decode — the client only re-uploads what the server has
|
||||
never seen (checked via `metrics_cached`)
|
||||
|
||||
#### `webapp.py`
|
||||
- FastAPI routes + static mount + the `ujm-web` entry point (binds loopback by
|
||||
default; `--host`/`--port`/`--open` to override)
|
||||
- Analysis endpoints take the audio as a **raw request body**, not a multipart
|
||||
upload: spool to a temp file, `SESSION.observe(...)`, unlink in `finally`. The
|
||||
bytes exist on disk only for the duration of that one request
|
||||
- `POST /api/analyse` (analyse + plot data), `POST /api/file` (register without
|
||||
plotting), `DELETE /api/file`, `POST /api/overlay`, `GET /api/plot`,
|
||||
`GET /api/metadata`, `GET /api/metrics`, `GET /api/session`
|
||||
|
||||
#### `metrics_json.py`
|
||||
- The renderer seam: `PlotSpec`/metric data → JSON, so a non-pyqtgraph renderer
|
||||
consumes the identical spec (curves, bands, hlines, heatmap, axis specs) that
|
||||
the desktop widget draws. `hlines` carry the integrated LUFS reference line —
|
||||
it is not an annotation
|
||||
|
||||
#### `static/` (web front end)
|
||||
- `audio-store.js` — OPFS store. Copies each picked/dropped `File` into Origin
|
||||
Private File System (browser-sandboxed, not the host's filesystem), hashes it
|
||||
with WebCrypto in chunks while streaming (never loads a whole master into
|
||||
memory), and re-sends bytes to the server per metric
|
||||
- `app.js` — the web renderer, an ES module importing the store. Draws the same
|
||||
`PlotSpec` the desktop does: uPlot for curves/bands, a plain canvas for the
|
||||
spectrogram heatmap (a canvas can map rows through an arbitrary function, so it
|
||||
draws native linear rows and maps them, unlike pyqtgraph's affine-only
|
||||
`ImageItem`). Mirrors the desktop's per-metric colour cycle, lin/log toggles,
|
||||
absolute/relative time, and draggable reference lines
|
||||
- `vendor/` — uPlot 1.6.32 (IIFE + CSS), vendored so nothing is fetched from a
|
||||
CDN at runtime
|
||||
- Boot rehydrates: the server still has the last session's analysis, so it
|
||||
re-offers audio only for metric/track combinations the server reports as
|
||||
uncached, and reports unavailable combinations rather than faking them
|
||||
|
||||
#### `master_core.py`
|
||||
- Defines the `AudioFile` class: librosa loading, rolling RMS power. BPM detection
|
||||
was **removed** — `librosa.beat.beat_track` cost ~3.7 s on every load for a
|
||||
@@ -225,6 +269,7 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
|
||||
- matplotlib: Colormaps only (consumed by pyqtgraph) + librosa dependency
|
||||
- mutagen: Audio metadata extraction
|
||||
- PyQt5: GUI framework
|
||||
- fastapi / uvicorn: web UI (optional extra; the desktop app does not need them)
|
||||
|
||||
### Architecture considerations
|
||||
- Three-stage split: `metrics.compute` (heavy, worker thread, backend-neutral
|
||||
@@ -248,6 +293,21 @@ uv sync # one-time, after cloning
|
||||
uv run ujm # launch the GUI
|
||||
```
|
||||
|
||||
### Running the web UI
|
||||
Branch `feature/web-ui`. Same backend, browser front end.
|
||||
|
||||
```bash
|
||||
uv sync --extra web
|
||||
uv run ujm-web # http://127.0.0.1:8000
|
||||
uv run ujm-web --host 0.0.0.0 --port 8080 # reachable from another machine
|
||||
```
|
||||
|
||||
Uploaded audio is held in the browser's Origin Private File System and streamed
|
||||
to the server per request; the server spools each body to a temp file and unlinks
|
||||
it before responding. Nothing is written to the host beyond that transient spool.
|
||||
Verify with `python3 scripts/webapi_smoke.py` against a running server (needs
|
||||
`ffmpeg` for the fixtures).
|
||||
|
||||
Optional flags (handled by `logger_setup.parse_log_args`):
|
||||
```bash
|
||||
uv run ujm --log-level DEBUG # ERROR | WARN | INFO | DEBUG | TRACE
|
||||
|
||||
Reference in New Issue
Block a user