webui.html becomes webui/ (index.html, style.css, ES modules in js/, no
build step); web.py serves it under / and /ui/<path> with the same
containment check as recordings.
How it feels with a screen reader:
- Days read "Today, Saturday 17 October"; recordings are named by their
wall-clock span ("22:00 to 23:00"), which is the row header and what
every button says. The filename moves to the last column.
- Focus is never dropped: busy buttons stay focused (aria-disabled),
finished analyses focus their first section, delete moves to the next
row, closing the clip bar (x or Escape) returns to where you were.
- Reloads rebuild only the days whose content changed; DOM ids are stable
per filename, so a rollover no longer resets the reading position.
- Only user-initiated actions are announced; highlights completion is
spoken (the progress area was stuck aria-busy before).
- Media Session previous/next track step sections like J/K (AirPods,
lock screen, media keys).
- Row layout: Time | Length | Loudness | Player | Options | File. The
player opens inside its cell; Download/Cut/Delete are always there,
and Cut toggles its start/end fields in place.
- Settings tucked behind a <details>; pre-roll and auto-advance are
remembered per browser; no CSS uppercase on headers or badges.
Fixes the REC badge showing on every row: its inline-flex display beat
the UA [hidden] rule. One global [hidden]{display:none !important} now
covers every case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pure move, no behaviour change. web.py 1221 -> 807 lines.
The reason is not file size but isolation of change. CLAUDE.md records
that the next step for detection, should flagging ever need improving, is
a spectral filter or an optional Silero VAD over candidate sections —
that work now lands entirely in isrweb/loudness.py and never has to open
the HTTP layer. The byte-level format code (FLAC frame parsing, CRC-8,
live header patching) is a second self-contained cluster, touched only
when a container format misbehaves.
isrweb/loudness.py 229 detector + its tuning constants
isrweb/audiofile.py 250 filename clock, WAV/FLAC headers, MIME types
web.py 807 cache, listing, handler, main()
Import direction is strictly web.py -> {loudness, audiofile}; the two
leaves import neither each other nor web.py, which is what lets the
detector be imported and tested without an HTTP server.
A package rather than flat modules because `audiofile` is a real PyPI
name and `detect` is generic enough to collide; a local file silently
shadows an installed one, which fails confusingly rather than loudly.
web.py stays at the repo root so `python web.py`, the docker-compose
command and the webui.html lookup are all unchanged.
Each module keeps its own optional numpy/soundfile try-import rather than
sharing one. That block cannot meaningfully diverge — if numpy is
installed every copy says True — and sharing it would stop isr.py being a
standalone file you can drop on a Pi and run.
Tests mirror the modules: test_loudness.py, test_audiofile.py, test_web.py
(+ unchanged test_isr.py). New tests/conftest.py owns the sys.path setup
that was previously an inline hack in test_isr.py — which only worked for
the other modules because that file happened to import first — and
provides the write_wav fixture the split would otherwise have duplicated.
Verified:
* 78 passed, same count as before the split; each test file also passes
standalone.
* All 21 moved functions diffed against HEAD:web.py — byte-identical.
* Import graph asserted acyclic; both leaves import with `web` absent
from sys.modules.
* An analysis cache written *before* the split still hits afterwards,
with identical sections — the one way a "pure" move could quietly
invalidate every user's cache.
* Endpoint smoke (analyze bounds, cache hit, cached_analysis in the
listing, clip Content-Length, 8-bit rejection) and the byte-serving
harness (ranges, download Range, HEAD, 405, live-header patching whole
and straddling) both re-run green.
* Dockerfile COPY set verified by building the image's file tree in a
temp dir and importing web.py with nothing else present.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
web.py was 1700 lines, more than half of it a single embedded HTML
string. The page now lives in webui.html (loaded once at startup),
so the frontend gets real syntax highlighting and web.py is pure
Python. Dockerfile copies the new file alongside web.py.
Also: ASCII startup banner (the arrow glyph crashed web.py on Windows
when stdout was redirected to a cp1252 file), and README fixes —
document the ALSA PCM-name device fallback and drop the monitor
device row, which the ALSA backend never supported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Cut panel inside every player row (✂ Cut button in the actions
column opens the row and focuses the Start field). Users type start and
end times in m:ss or h:mm:ss format; Download cut triggers
GET /api/cut which runs ffmpeg -c copy (no re-encode) and streams the
result as an attachment.
Clicking an analysis chip now also pre-fills the cut panel start/end
with the loud-section boundaries.
Server switched to ThreadingHTTPServer so ffmpeg runs don't block
other requests. ffmpeg added to Dockerfile apt install.