Commit Graph

13 Commits

Author SHA1 Message Date
mikkeli 6ae4d5e8e3 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
2026-09-14 10:16:52 +09:00
irene 5df802a574 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.
2026-09-14 01:44:48 +09:00
Mikkeli Matlock 507af2f676 Native integrated/LRA, background prefetch, timing-rich status
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>
2026-06-14 01:49:50 +09:00
Mikkeli Matlock d7782bb9d9 Speed up analysis: drop BPM, vectorise loudness/true-peak, share short-term
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>
2026-06-14 01:36:08 +09:00
Mikkeli Matlock 22ecb67296 Per-metric ref lines with Hz spectrogram units; relative-time toggle; fixed font
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>
2026-06-14 01:05:23 +09:00
Mikkeli Matlock a45bd4ba9b Reference-line manager, relative time axis, axis-aware ref-line UX
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>
2026-06-14 00:51:33 +09:00
Mikkeli Matlock b400551321 Move plotting to pyqtgraph: interactive, overlay-capable render layer
Replace the fire-and-forget matplotlib pipeline (render() -> throwaway Figure ->
canvas teardown) with a three-stage architecture that supports zoom/pan, lin/log
toggling, and multi-file overlay:

  compute(audio_file) -> data        # heavy, worker thread, backend-neutral
  build_spec(data, view) -> PlotSpec # cheap, GUI thread, view-aware
  show_specs([(label, spec, color)]) # pyqtgraph, persistent PlotItem, overlay

- plotspec.py: backend-agnostic descriptors (Curve, Band, HLine, Heatmap,
  AxisSpec, PlotSpec) + ViewState (recompute-free lin/log)
- audio_visualization_widget.py: persistent pyqtgraph plot, never torn down;
  per-dataset colours for overlay; spectrogram log-freq via row resample
  (ImageItem is affine-only); ColorBarItem at a fixed cell
- Compare/overlay driven by file-list checkboxes; stable per-song colour by row
- Custom draggable reference lines (add/clear), persist across redraws
- Axis-constrained scroll zoom: Ctrl=time, Shift=value (_AxisZoomViewBox)
- RMS render no longer per-segment fill_between (was the slow path)

Fixes found in review/testing:
- FillBetweenItem needs penned child curves or it fills nothing (RMS/Waveform
  were blank); band fill verified by pixel count
- band overlay alpha was a no-op (QBrush.color() returns a copy)
- colorbar could stack across renders; now added/removed at a fixed layout cell

Deferred (per scope): stereo retention, deep perf rewrites (eager beat_track,
true-peak/crest loops, shared LUFS), per-song colour picker UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:35:10 +09:00
Mikkeli Matlock a322f08d0c Add spectrogram, native-rate loading, and always-labelled axis extremes
- 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>
2026-06-07 00:06:50 +09:00
Mikkeli Matlock 7bdf465799 Add pluggable metrics architecture with LUFS
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>
2026-05-30 00:42:45 +09:00
Mikkeli Matlock cf901a5686 cleanup 2026-05-28 14:05:34 +09:00
Mikkeli Matlock fa844dfde1 Updating CLAUDE settings to match initial rules 2025-08-22 00:36:33 +09:00
Mikkeli Matlock 9e65e721d4 Update project roadmap and documentation
- 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>
2025-08-22 00:26:27 +09:00
Mikkeli Matlock a9a4b1725c Add comprehensive documentation and usage instructions
- 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>
2025-08-21 00:47:47 +09:00