Bring the docs in line with the code

- python-backend/README: track analysis is implemented (2e0e6ad); the cache layout,
  the module table (lirc, screen, timers, podcasts, tippen) and the route table
  (track analysis, HA proxy, typing game) were missing pieces.
- docs/REPO_OVERVIEW: Mein Zimmer and the clock are implemented; Home Assistant is
  also reached over HTTP through the room page's proxy, not MQTT only; the IR remote,
  scripts/, the optional config sections and figure kind were not mentioned.
- web/README: the key table was the mockup's (Ctrl transport keys, TAB over 'all');
  ?pi=1 is now a preset with per-browser overrides, not a read-once flag; the room
  page and the typing game exist; eslint is part of the checks.
- Docstrings that described earlier designs: HaConfig (written as browser-direct in
  the same commit that built the proxy, a8ed350), services/__init__, events,
  playback, media, colors, tippen progress and AppHeader, lowPower.ts, and the
  'Hoerbuecher' spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 00:28:37 +02:00
parent a0ac48ccbf
commit a737ccf108
12 changed files with 105 additions and 77 deletions

View File

@@ -94,7 +94,10 @@ the prompt becomes a regression test by pasting it into a `.txt` file.
| `musicmouse/reactions/` | The policy. `@on(SomeEvent)` functions that get the app and act. |
| `musicmouse/library/` | The music collection: scanning, tags, cover art and its colours, the cache, and the seams for future track analysis. |
| `musicmouse/services/mqtt/` | Home Assistant entities: three lights, a player sensor, a volume number, transport buttons, device triggers, a tag scanner. |
| `musicmouse/services/web/` | The browser front-end's API: the library, a state websocket, command endpoints, and parent-mode settings. |
| `musicmouse/services/web/` | The browser front-end's API: the library, a state websocket, command endpoints, parent-mode settings, the typing game and the Home Assistant proxy. |
| `musicmouse/services/lirc/` | The IR remote, as a lircd TCP client. |
| `musicmouse/services/` (`screen.py`, `timers.py`, `podcasts.py`) | The monitor's on/off and idle auto-off; the parents' timer and school-day calendar; the podcast feed poller. |
| `musicmouse/tippen/` | The typing game's curriculum, progress and track unlocks. |
| `musicmouse/simulator/` | Fake transport and player, the driver vocabulary, the REPL and the script runner. |
| `musicmouse/config.py` | Pydantic schema, validation, and human-readable error formatting. |
@@ -186,8 +189,9 @@ different amounts to produce:
```
index.json cheap: tags and structure. Rebuilt freely.
covers/<album_id>.jpg medium: art extracted from an ID3 APIC frame
analysis/<track_key>.json expensive: reserved for offline audio analysis
covers/<album_id>.jpg medium: the album's art, downscaled to 384 px
covers/<album_id>.thumb.jpg medium: the same at 256 px, for cards and rows
analysis/<track_key>.json expensive: track analysis (plus .beats.json, .curve.json)
```
Deleting the whole directory is safe; deleting it throws away analysis that is minutes
@@ -199,13 +203,16 @@ change to how the scanner derives a title, artist or series is invisible until t
is invalidated: bump `_INDEX_VERSION` in `musicmouse/library/cache.py` when you touch
that logic.
**Track analysis is not implemented.** `musicmouse/library/analysis.py` fixes the shape
of the results - scalars (`tempo`, `energy`, `valence`, `brightness`) travel inline with
the index, and a beat grid lives in its own file and is fetched per track - so an
analyzer can be added later without touching the scanner, the API or the frontend. It
will go behind an optional dependency group, and because results are content-keyed
files, they can equally well be computed on a workstation and the `analysis/` folder
copied to the device.
**Track analysis** drives the web front-end's ambient background. It is optional: install
the `analysis` extra (`pip install -e '.[analysis]'`, librosa and numpy) and
`musicmouse/library/librosa_analyzer.py` works through the music shelf in the
background, in a pool of niced worker processes (`musicmouse/library/workers.py`), and
pauses while anything is playing. Without the extra, `build_analyzer()` returns a no-op
and the background stays static. Scalars (`tempo`, `energy`, `valence`, `brightness`,
`pulse`) travel inline with the index; the beat grid and per-second curves live in their
own files and are fetched only for the playing track. Results are content-keyed, so they
can be computed on a workstation and the `analysis/` folder copied to the device
(`scripts/sync-library.sh` does that).
## The web front-end
@@ -226,6 +233,9 @@ copied to the device.
| `POST /api/screen/toggle` `/screen/activity` | the kiosk's Del key; its key presses and taps, holding off the idle screen-off |
| `GET` `PUT /api/settings` | parent mode |
| `POST` `DELETE /api/timer` | parent mode's countdown: `{minutes}` or `{until: "HH:MM"}`; stop or silence it |
| `GET /api/tracks/{album_id}/{index}/analysis` | beat grid and mood curves of one track, or 404 |
| `GET /api/ha`, `GET /api/ha/states/{entity}`, `POST /api/ha/services/{domain}/{service}` | the room page's Home Assistant proxy; 404 without an `ha` section |
| `GET /api/tippen/curriculum` `/progress`, `PUT /api/tippen/settings`, `POST /api/tippen/runs` | the typing game; 404 without a `tippen` section |
Two decisions worth knowing:

View File

@@ -205,7 +205,7 @@ class ClockConfig(_Strict):
class LibraryConfig(_Strict):
"""Where the music lives.
One path. The shelves underneath it - ``Figuren``, ``Musik``, ``Hoerbuecher``,
One path. The shelves underneath it - ``Figuren``, ``Musik``, ``Hörbücher``,
``Kinderpodcasts`` - are fixed names, not settings; see
:mod:`musicmouse.library.sections`.
"""
@@ -259,10 +259,10 @@ class HaDeviceConfig(_Strict):
class HaConfig(_Strict):
"""Home Assistant integration for the room-control page ("Mein Zimmer").
The backend never calls Home Assistant itself - it only hands the browser the
server URL, the token, and these two ordered lists. Control happens directly from
the browser to Home Assistant's own REST API, so this token grants full HA control
to anything on the LAN that can reach musicmouse. See config.yml.example.
The browser only gets the two ordered lists. Every state read and service call goes
through the backend's ``/api/ha/...`` proxy, which attaches the token - so the token
never leaves the backend. The proxy itself is unauthenticated, though: anything on
the LAN that can reach musicmouse can use it. See config.yml.example.
"""
url: str

View File

@@ -7,8 +7,8 @@ Three flavours, distinguished by base class:
* :class:`StateEvent` - something changed.
The intent layer is what lets several front-ends drive the same behaviour: a button
press, an MQTT command and a future web request all emit ``NextTrackRequested`` and a
single reaction acts on it.
press, an MQTT command, a web request and the IR remote all emit ``NextTrackRequested``
and a single reaction acts on it.
"""
from __future__ import annotations

View File

@@ -2,7 +2,7 @@
The frontend paints an album card with these and the LED strips run an effect in them,
so a web-started album looks the same on the shelf as it does on the screen. Extraction
happens once per album ever - the result is cached next to the cover.
happens when an album is scanned; the result is stored in the cached index.
"""
from __future__ import annotations

View File

@@ -1,8 +1,8 @@
"""Playlist model.
Deliberately a real type rather than a bare ``list[str]``: it is what the browse API
serves, and it keeps track metadata in one place. Building one from a folder is
:mod:`musicmouse.library`'s job.
What the player plays: a name and the files, in order. The browse API serves
:class:`~musicmouse.library.models.Album` instead, which carries the tag metadata;
``Album.to_playlist()`` builds one of these from it.
"""
from __future__ import annotations

View File

@@ -1,8 +1,8 @@
"""What the mouse plays, and when.
Physical inputs are turned into *intents*, and the intents are what actually drive the
player. That indirection is the point: an MQTT command or a future web request emits
the same intent and lands in the same handler, so there is one place per behaviour.
player. That indirection is the point: an MQTT command, a web request or an IR remote key
emits the same intent and lands in the same handler, so there is one place per behaviour.
"""
from __future__ import annotations

View File

@@ -1,7 +1,8 @@
"""Front-ends: things that mirror state outwards and turn requests into intents.
``MqttService`` is the reference implementation. A web service would be another file
here plus one line in the app - devices and reactions would not change.
MQTT (Home Assistant), web (the browser), lirc (the IR remote) and the screen, timer and
podcast services each live here; adding another is a file here plus a line in
``__main__._build_services`` - devices and reactions do not change.
"""
from musicmouse.services.base import Service

View File

@@ -5,8 +5,8 @@ Same convention as everywhere else this codebase persists something outside
missing or corrupt file is just a fresh start, never a crash: nothing here is precious
enough to raise over.
``record_run`` is a near-verbatim port of tippen's own (client-side, until now)
``recordRun`` in ``tippen/src/lib/progress.ts`` - grading a keystroke-by-keystroke run
``record_run`` is a port of the ``recordRun`` the standalone typing app had client-side
before it was merged into the web front-end - grading a keystroke-by-keystroke run
into stars/points/an animal stays entirely client-side (see ``TippenRunIn`` in
``musicmouse.services.web.schemas``); this only owns what happens to progress once a
graded result arrives.