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.
LUFS integrated + LRA:
- Reimplement BS.1770 two-stage gating (integrated) and EBU 3342 (LRA) directly
from the cached K-weighted signal, dropping pyloudnorm's two whole-signal
re-filters. Validated bit-equal to pyloudnorm across steady/dynamic/quiet
signals (0.0000 diff). LUFS compute ~1.9s -> ~0.6s (orig 3.8s). pyloudnorm now
only supplies the filter coefficients.
Prefetch on load:
- After a file loads, PrefetchWorker computes the remaining metrics in the
background (sequential, cooperatively cancellable, skips on-demand hits), so
the first switch to any metric is instant. Superseded when a new file loads.
Status slip:
- Workers measure compute time; metricTiming + phase/duration progress messages
drive the slip ("Loaded in Ns - computing X...", "X computed in Ys"). The old
fake percentage (10% then done) is logged but no longer shown.
- shutdown() stops background threads on window close.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Profiled hot spots on a 4-min track and cut the worst offenders:
- Remove BPM: librosa.beat.beat_track ran on every load (~3.7s) for a number no
better than tapping by hand. Dropped from AudioFile + the metadata panel.
- LUFS short-term: replace 474 per-window pyloudnorm.integrated_loudness calls
with one K-weighting pass (reusing pyloudnorm's own filter coefficients) + a
vectorised sliding mean-square. This is true *ungated* EBU R128 short-term
(the old loop wrongly gated each 3s window). Integrated + LRA still use
pyloudnorm's gated calls. ~3.8s -> ~1.9s.
- PSR: reuse LUFS's short-term series (memoised on the AudioFile) + vectorised
sample-peak. ~3.0s -> ~0.2s.
- True Peak: oversample the whole signal once, then an O(N) running max over
windows instead of per-window resample_poly. Bit-identical to the old loop
(max|diff| 0.0000 dB). ~2.1s -> ~1.1s.
- Crest Factor: peaks via the same O(N) running max (last per-window loop gone).
lufs+psr+true_peak: ~9.2s -> ~3.2s, plus ~3.7s of BPM removed from every load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference lines:
- Kept per metric (dict keyed by metric_id) so switching metrics and coming back
preserves them, instead of clearing on every switch
- Spectrogram lines read/edit/drag in Hz: the renderer installs Hz<->row-index
transforms (the heatmap y-axis is a row index), so value, label, the edit
dialog, and the new-line default all speak frequency. Curve metrics stay
identity. Round-trip verified (1000 Hz -> row -> 1000 Hz)
- Line label and drag-readback always in the metric's natural units
Controls:
- Relative-time abs/rel is now a checkbox toggle, not a dropdown
- Removed the font control panel; UI font is locked to M PLUS 1 Code @ 10pt via
font_manager.apply_fixed_font (system-default fallback). Deleted
font_control_widget.py
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference lines:
- Side-panel cluster (ref_line_widget.py): list of lines with Add/Edit/Remove/
Clear and a properties dialog (value, colour, line style, tag)
- Lines own their state as RefLineProps (held by MainWindow), drawn with a
triangle drag-handle; dragging writes the value back and refreshes the list
- Replaces the old centre-only add-and-drag-the-whole-line approach
Comparison:
- Time-axis mode selector: Absolute (seconds) vs Relative (% of each track's own
length), so a long track and a short one line up by song position. Pure view
transform (plotspec.apply_x_mode), no recompute
- Overlaid different-length tracks now autorange their x union in absolute mode
instead of clipping to the first track's span
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SpectrogramMetric: log-frequency STFT power heatmap over time, magma
colormap, -80 dB floor. Adaptive hop caps time bins at ~4000 so long
tracks stay responsive on redraw; N_FFT=4096 keeps low-freq resolution.
- master_core: load audio at native sample rate (librosa.load sr=None)
instead of librosa's 22050 Hz default, so the full band up to the
file's own nyquist (~22 kHz at 44.1 kHz) is analysed. ~2x heavier on
44.1/48 kHz files, by design.
- metrics: shared _show_axis_extents helper forces each axis's exact
min/max onto the tick list with compact labels (_fmt_tick), so the
true range is always readable -- notably the spectrogram's 22 kHz top,
which otherwise sits unlabelled between log-scale decade ticks. Applied
to all metric renders.
- Docs: README + CLAUDE updated for the new metric, native-rate loading,
and axis-readability behaviour.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce a Metric ABC (compute on worker thread, render on GUI thread)
with a METRICS registry, and refactor the analysis pipeline around it.
RMS power (ported), raw waveform, and BS.1770 LUFS (via pyloudnorm) ship
as the initial three; new metrics drop in by appending to METRICS.
- metrics.py: Metric ABC + RMSPowerMetric, WaveformMetric (locked to
+/-1.1 y-range for float headroom), LUFSMetric (short-term 3 s window
+ integrated value, with streaming-target reference line).
- plot_control_widget.py: metric selector dropdown + Refresh Plot button
(moved out of FontControlWidget).
- analysis_results_manager.py: AnalysisResult caches the AudioFile and a
per-metric data dict; new MetricComputeWorker runs metric switches off
the GUI thread via metricComputeStarted/metricReady/metricComputeError
signals, so LUFS on a 12-minute track no longer stalls the UI.
- main.py: all redraw paths funnel through one _render_or_request helper;
stale-result guards keep slow computes from overwriting fresh selections.
- plotting_engine.py removed (metadata text moved onto AnalysisResult;
figure construction lives in each Metric).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Reflect current implementation status with complete GUI and font system
- Update roadmap with prioritized development plan focusing on plot control widgets
- Reorganize future goals into urgent/short-term/mid-long term categories
- Add comprehensive feature overview and usage instructions
- Clarify next priority: plot control system clustering
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added CLAUDE.md with detailed project documentation and roadmap
- Enhanced README.md with usage section for command line and GUI modes
- Updated with Claude Code credit
- Improved master_core.py with better song name handling and BPM display
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>