From e22c0059f6f324aecf425384309bdcb5f371d1f0 Mon Sep 17 00:00:00 2001 From: jonathan Date: Tue, 2 Jun 2026 22:30:12 +0200 Subject: [PATCH] feat: cache analysis results alongside audio files After the first analysis of a WAV/FLAC file, the result is written to .analysis.json next to the audio. Subsequent requests with the same threshold and min_gap parameters return the cached result immediately without re-reading the audio data. The cache is invalidated automatically if either parameter changes. Written via temp-then-replace for thread safety. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 +- web.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a582520..6feb676 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Shows recordings grouped by day with collapsible sections. Features: - **Day groups** — recordings are grouped under a collapsible day heading showing date, file count, total duration, and total size. The most recent day is expanded by default; older days start collapsed. Expanded state is preserved across filter changes. - **Day highlights** — click **★ Highlights** on any day heading to run loudness analysis across all WAV/FLAC files in that day and display a combined activity timeline SVG. Orange segments show when loud sections occurred relative to the day's time span; blue shows the file extents. Labels show the start, midpoint, and end times. - **Inline playback** — collapsible `▶ Play` button per row; audio loads lazily via a seekable `/stream/` endpoint with HTTP Range support. Metadata is fetched immediately so the duration is visible without pressing play. -- **Waveform analysis** — on demand per file; computes RMS per 100 ms window and highlights loud sections. Supported for WAV and FLAC (FLAC requires `numpy` + `soundfile`). Pure-Python fallback for WAV when numpy is absent. +- **Waveform analysis** — on demand per file; computes RMS per 100 ms window and highlights loud sections. Supported for WAV and FLAC (FLAC requires `numpy` + `soundfile`). Pure-Python fallback for WAV when numpy is absent. Results are cached alongside the audio file as `.analysis.json`; subsequent requests at the same threshold and min-gap settings return instantly without re-reading the audio. - **Grace period** — configurable in the controls bar (default 2 s). Loud sections separated by less than this gap are merged into one. Raise this (e.g. to 15–30 s) when a single event generates many timestamps due to brief quiet gaps within it. - **Timestamp jump** — after analysis, click any loud-section chip to seek the player to that position and pre-fill the cut panel. Use **J** / **K** keyboard shortcuts to jump to the previous / next section while audio is playing. - **Cut & download** — `✂ Cut` button opens the player row and reveals a cut panel. Enter start and end times in `m:ss` or `h:mm:ss` format and click **↓ Download cut** to receive an ffmpeg-trimmed copy without re-encoding. Requires ffmpeg (included in the Docker image). diff --git a/web.py b/web.py index 9fb54a0..3758914 100644 --- a/web.py +++ b/web.py @@ -331,6 +331,15 @@ class _Handler(BaseHTTPRequestHandler): except Exception: pass + cache_path = path.parent / (path.name + '.analysis.json') + try: + cached = json.loads(cache_path.read_text('utf-8')) + if cached.get('threshold') == threshold and cached.get('min_gap') == min_gap: + self._send(200, json.dumps(cached['result']).encode('utf-8'), 'application/json') + return + except Exception: + pass + ext = path.suffix.lower() if ext == '.wav': result = analyze_wav(path, threshold=threshold, min_gap=min_gap) @@ -343,6 +352,13 @@ class _Handler(BaseHTTPRequestHandler): self._json_err(400, f'Loudness analysis is not available for {ext} files') return + try: + tmp = cache_path.with_suffix('.tmp') + tmp.write_text(json.dumps({'threshold': threshold, 'min_gap': min_gap, 'result': result}), 'utf-8') + os.replace(tmp, cache_path) + except Exception: + pass + self._send(200, json.dumps(result).encode('utf-8'), 'application/json') def _api_status(self):