Document the in-browser web UI and its parity tests
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015UXA39Sh9coAx4FhRkd8R2
This commit is contained in:
@@ -11,6 +11,8 @@ A custom mastering toolkit that provides metrics to evaluate audio masterings th
|
||||
- **Modular GUI Architecture**: Complete PyQt5 interface with drag-and-drop and file dialog support
|
||||
- **Font Management**: CJK-capable, fixed UI font (M PLUS 1 Code @ 10pt) with system fallback
|
||||
- **Threading & Logging**: Robust background processing with detailed logging system
|
||||
- **Web UI** (`web/`): the same metrics ported to in-browser JavaScript — a static
|
||||
site; audio never leaves the tab
|
||||
|
||||
### Technical stack
|
||||
- **Audio Processing**: librosa, numpy
|
||||
@@ -109,49 +111,41 @@ 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`)
|
||||
#### `web/` (browser version — no backend)
|
||||
- A static site: decoding and every metric run in the visitor's tab; nothing is
|
||||
uploaded or stored. Served with `Content-Security-Policy: connect-src 'none'`
|
||||
(`deploy/nginx.conf`), so the page and its worker cannot make network requests
|
||||
- `decode.js` — `AudioFile.__init__` counterpart. WAV/AIFF PCM & float are parsed
|
||||
directly with libsndfile scaling (bit-identical to `librosa.load`; Chromium's
|
||||
own WAV decoder scales positive int16 by 1/32767). Other formats go through
|
||||
`decodeAudioData` on an `OfflineAudioContext` at the header-sniffed native rate
|
||||
(no resampling). ID3 artist/title for MP3. Downmix = float32 channel mean
|
||||
- `dsp.js` — ports of every `Metric.compute`: pure functions over the mono
|
||||
Float32Array (run in the worker and under Node). Same numerics as the Python:
|
||||
sequential float64 prefix sums sampled only at window edges (no N-length
|
||||
arrays), pyloudnorm K-weighting via scipy `lfilter`'s DF2T recurrence (streamed),
|
||||
`resample_poly(y, 4, 1)`'s 81-tap Kaiser(5) filter + monotonic-deque window max,
|
||||
`librosa.stft` centred framing + `amplitude_to_db(ref=max, top_db=80)`.
|
||||
Loudness (short-term, integrated, LRA) is one memoised pass shared by LUFS and PSR
|
||||
- `metrics.js` — registry + `build_spec` ports producing the plot payload
|
||||
(`{axes, hlines, annotations, segments, series, heatmap}`), plus `applyRelativeX`
|
||||
- `analysis-worker.js` — Web Worker: holds each track's mono signal, computes one
|
||||
metric per request, transfers typed arrays back
|
||||
- `app.js` — `MainWindow` + `AnalysisResultsManager` + renderer: track list,
|
||||
overlay set, per-metric result cache, job queue (foreground jobs jump ahead of
|
||||
background prefetch), uPlot for curves/bands + canvas spectrogram (log axis
|
||||
drawn from linear rows), draggable reference lines per metric, metric hlines
|
||||
- `vendor/` — uPlot 1.6.32, vendored (the CSP only allows same-origin scripts)
|
||||
- Needs an HTTP server (module scripts and workers don't load from `file://`),
|
||||
but no secure context — plain `http://` on a LAN works
|
||||
|
||||
#### `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
|
||||
#### `tests/parity/`
|
||||
- `fixtures.py` synthesises edge-case audio (short / silent / below the relative
|
||||
gate, 96 kHz, every PCM layout, lossy formats via ffmpeg); `reference.py` runs
|
||||
`metrics.py` and saves the exact mono signal it analysed; `parity.mjs` runs
|
||||
`web/dsp.js` on that signal and compares every field (float64 paths ~1e-12,
|
||||
float32 paths ~1e-6)
|
||||
- Any change to `metrics.py` numerics must be mirrored in `web/dsp.js` and pass this
|
||||
|
||||
#### `master_core.py`
|
||||
- Defines the `AudioFile` class: librosa loading, rolling RMS power. BPM detection
|
||||
@@ -269,7 +263,8 @@ 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)
|
||||
- Web UI: no Python dependencies; uPlot is vendored in `web/vendor/`. The parity
|
||||
tests need Node 18+
|
||||
|
||||
### Architecture considerations
|
||||
- Three-stage split: `metrics.compute` (heavy, worker thread, backend-neutral
|
||||
@@ -294,19 +289,16 @@ uv run ujm # launch the GUI
|
||||
```
|
||||
|
||||
### Running the web UI
|
||||
Branch `feature/web-ui`. Same backend, browser front end.
|
||||
Branch `feature/web-js`. Static files in `web/`; everything runs in the browser.
|
||||
|
||||
```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
|
||||
docker compose up -d --build # nginx + CSP, http://localhost:8080
|
||||
python3 -m http.server -d web 8080 # quick local try (no CSP header)
|
||||
```
|
||||
|
||||
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).
|
||||
Audio is decoded and analysed in the tab and never leaves it (CSP
|
||||
`connect-src 'none'`). Check the port against `metrics.py` with the parity tests
|
||||
(`tests/parity`, see README).
|
||||
|
||||
Optional flags (handled by `logger_setup.parse_log_args`):
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user