Pin each MusicMouse host to a deploy branch, and freeze musicmouse

musicmouse_version pointed both hosts at rearchitect-backend, a branch that only
ever existed on a laptop - so the previous commit's rework could never actually
have run. Point each host at a deploy/<host> branch on the remote instead. Those
branches are the deployment record: what is installed on that box right now,
moved with a force-push when you mean to deploy.

musicmouse (Kinderzimmer) is the reason this matters. It is in daily use and
still runs the pre-rearchitecture backend out of /opt/musicmouse_venv and
espmusicmouse/host_driver/main.py. That commit has no python-backend/
pyproject.toml, no python-backend/musicmouse.service and no web/ at all, so this
role cannot install it - and a run would have force-checked-out incompatible
code over a working device first and failed afterwards. Set
mediapi_install_kidsmusic: false there and leave it alone;
deploy/musicmouse records the commit that is installed, for rollback. Add a
stat + fail right after the checkout so the same mistake stops with an
explanation instead of failing three tasks deeper.

Also in the role:

- Install the typing game's curriculum next to config.yml. A config with a
  tippen: section refuses to start without it - curriculum_file is validated as
  must-exist - so musicdolphin's new config needs it present.

- Stop clobbering config.yml. The app writes to it at runtime: parent mode
  patches the volume keys and the remote-control page rewrites the remote:
  block. A plain copy: undid that on every run. Install-once by default, with
  -e pi_musicmouse_force_config=true to push a change deliberately.

- Fill out config-musicdolphin.yml against the current schema: library root and
  cache, an absolute static_dir (../web/dist would resolve to /media/web/dist
  from /media/musicmouse and silently serve nothing), tippen, and the volume
  range for a screen-driven instance. ha: is left commented out until the
  Wohnzimmer-oben entity ids are picked - the ones in the dev config are
  Arbeitszimmer. No mqtt: (it would publish a device of dead entities on a
  simulate-only host) and no lirc:/remote: (this host runs irserver, a serial
  daemon, not the lircd this speaks TCP to).

The curriculum is a verbatim copy of app content, so exclude it from
ansible-lint rather than reformat it away from its source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 19:04:57 +02:00
parent 9b1d01115b
commit 5763df90f8
9 changed files with 726 additions and 20 deletions

View File

@@ -2,8 +2,9 @@
Deploys the MusicMouse backend (`python-backend/`) and its built web frontend.
- Checks out `musicmouse_version` (a tag or branch - set per host in `host_vars`, no
default) from `musicmouse_repo` to `/opt/musicmouse`.
- Checks out `musicmouse_version` from `pi_musicmouse_repo` to `/opt/musicmouse`, then
**fails early** if that checkout has no `python-backend/pyproject.toml` - see
"Which version" below.
- Installs a pinned `uv` release and uses it to create `/opt/musicmouse/.venv` with
Python `pi_musicmouse_python_version` (the backend needs 3.12+ for PEP 695 syntax;
Raspbian Bookworm's apt python3 is only 3.11) and `pip install` the backend into it.
@@ -11,10 +12,55 @@ Deploys the MusicMouse backend (`python-backend/`) and its built web frontend.
the Pi - no Node/npm is installed on the Pi) and syncs `web/dist` over.
- Installs `/media/musicmouse/config.yml` from `files/config-<inventory_hostname>.yml`
(one file per host - the schema differs between a host with a real mouse attached and
a display-only one; see those files' own comments) and the systemd unit copied
straight from the checked-out repo's `python-backend/musicmouse.service`, so it can
never drift from what's actually in the app repo.
a display-only one; see those files' own comments), the typing game's curriculum
beside it, and the systemd unit copied straight from the checked-out repo's
`python-backend/musicmouse.service`, so it can never drift from what's actually in the
app repo.
- Sets up a Samba share of `/media/musicmouse` for copying music onto the device.
Manual steps after a first deploy: set the Samba password with `smbpasswd`, and copy
music into `/media/musicmouse` (or via the Samba share).
## Which version
`musicmouse_version` is set per host in `host_vars`, with no default, and points at a
`deploy/<host>` branch on the remote. Those branches are the deployment record: what is
installed on that box right now. Moving one is a force-push, and the next run follows it
(the `git:` task uses `force: true`):
git push -f origin rearchitect-backend:deploy/musicdolphin
just mediapis musicdolphin
This role only understands the post-rearchitecture layout - `python-backend/` as an
installable package plus a `web/` frontend. On older commits the host driver lives at
`espmusicmouse/host_driver/main.py`, there is no `pyproject.toml`, the unit file is
under `esp-firmware/`, there is no frontend at all, and the config is a different format
that this role's config files would fail to load. There is deliberately no legacy path:
the role stops at the checkout with an explanation instead.
`musicmouse` (Kinderzimmer) is on such an older commit and still working, so it has
`mediapi_install_kidsmusic: false` and this role skips it entirely; `deploy/musicmouse`
exists purely as the rollback marker for what is installed there. Migrating it means
moving `/media/musicmouse/<figure>/` into `Figuren/` and switching to the new config
schema - `files/config-musicmouse.yml` is already written for that.
## config.yml is install-once
The app writes to its own config at runtime: parent mode (`?parentMode=1`) patches the
volume keys, and the remote-control page rewrites the whole `remote:` block. A plain
`copy:` would throw those away on every ansible run, so the config task is
`force: "{{ pi_musicmouse_force_config }}"`, default `false`. Push a changed config when
you actually mean to:
just run mediapis.yml musicdolphin -e pi_musicmouse_force_config=true
The curriculum file is not written by the app, so it is installed unconditionally. A
config with a `tippen:` section will not start without it - `curriculum_file` is
validated as must-exist - so a host whose config has no `tippen:` section should set
`pi_musicmouse_curriculum_file: ""`.
## Manual steps after a first deploy
- Set the Samba password with `smbpasswd`.
- Get the music onto the device. The app repo's `scripts/sync-library.sh` is the way to
do it: as well as the library it carries over the cover and track-analysis cache,
which is the part that costs minutes of DSP per track to recompute, and it knows which
parts of that cache survive the move and which must not be copied. The Samba share
works too, and then the device does all that work itself.

View File

@@ -1,7 +1,9 @@
---
pi_musicmouse_repo: "ssh://git@git.bauer.tech:2222/martin/musicmouse.git"
# No default on purpose: which tag/branch to run is a per-host decision, set in host_vars.
# musicmouse_version: "rearchitect-backend"
# No default on purpose: which branch to run is a per-host decision, set in host_vars.
# The deploy/<host> branches on the remote are the intended values - move one with
# `git push -f origin <branch>:deploy/musicdolphin` and re-run this role.
# musicmouse_version: "deploy/musicdolphin"
pi_musicmouse_python_version: "3.13"
pi_musicmouse_uv_version: "0.12.10"
@@ -13,3 +15,16 @@ pi_musicmouse_build_cache: "/tmp/musicmouse-build-cache"
# Which files/config-*.yml to install as this host's /media/musicmouse/config.yml.
pi_musicmouse_config_file: "config-{{ inventory_hostname }}.yml"
# The app owns config.yml at runtime: parent mode writes the volume keys back to it, and
# the remote-control page rewrites its `remote:` block. So the config is installed once
# and then left alone, or every ansible run would silently undo those edits. Push a
# changed config deliberately:
#
# just run mediapis.yml musicdolphin -e pi_musicmouse_force_config=true
pi_musicmouse_force_config: false
# The typing game's lesson plan, installed next to config.yml. A config with a `tippen:`
# section will not start without it - curriculum_file is validated as must-exist. Set to
# "" for a host whose config has no tippen section.
pi_musicmouse_curriculum_file: "tippen-curriculum.yml"

View File

@@ -1,25 +1,89 @@
---
# musicdolphin: display-only instance. No physical mouse attached (for
# now), so serial and audio are both "simulate" - the web front-end (shown locally in
# the pi_kiosk role's Firefox kiosk window, and reachable on the LAN for a tablet) is a
# complete way to drive the player on its own. `figures` still needs at least one entry
# (the schema requires it) even though nothing will ever scan a tag here.
# musicdolphin: display-only instance. No physical mouse attached, so serial and audio
# are both "simulate" - the web front-end (shown locally in the pi_kiosk Firefox window,
# and reachable on the LAN for a tablet) is a complete way to drive the player on its
# own. `figures` still needs at least one entry (the schema requires it) even though
# nothing will ever scan a tag here.
#
# This Pi does have a HifiBerry amp (mediapi_has_hifiberry_amp, alsa_card_name
# sndrpihifiberry), so the `softvol_effects` slot from pi_alsasetup exists on it.
# Swapping alsa_device to that is the one-line change that gives this instance real
# sound - the amp is currently shared by squeezelite and shairport only.
#
# Unknown keys are rejected rather than ignored, so a typo fails at startup with the
# path to the offending line. Every key is documented in the app repo's
# python-backend/config.yml.example.
general:
library:
# The four shelves underneath are fixed names, not settings: Figuren/, Musik/,
# Hoerbuecher/ (with real umlauts) and Kinderpodcasts/.
root: /media/musicmouse
# Relative, so it resolves against *this file's* directory -
# /media/musicmouse/.musicmouse-cache. Seeded by the app repo's
# scripts/sync-library.sh; safe to delete, at the cost of recomputing analysis.
cache: .musicmouse-cache
# analysis_workers: 2
# ^ pointless until this role installs the backend's `analysis` extra
# (librosa + numpy, a heavy build on a Pi). Without it build_analyzer() falls
# back to NullAnalyzer and the reactive background stays static.
# Both required rather than optional on purpose: running blind or silent has to be
# asked for, so a config that lost a line fails loudly instead of booting into
# something that looks like it is working. Startup says so every boot.
serial_port: "simulate"
alsa_device: "simulate"
web:
host: "0.0.0.0"
port: 8080
# Absolute on purpose. The repo's example uses ../web/dist because its config sits
# in python-backend/; from /media/musicmouse that would resolve to /media/web/dist
# and silently serve nothing.
static_dir: /opt/musicmouse/web/dist
# The typing game. Both paths are relative, so both land in /media/musicmouse/.
# curriculum_file must exist at startup and is installed by this role;
# progress_file is created and written by the app itself.
tippen:
curriculum_file: tippen-curriculum.yml
progress_file: tippen-progress.json
# Home Assistant "Mein Zimmer" page. Left out until the Wohnzimmer-oben entity ids
# are picked - the dev config's entities are Arbeitszimmer, the wrong room for this
# Pi. An empty ha: block is invalid; the section needs at least one device or scene,
# so uncomment it only once it is filled in:
#
# ha:
# url: "https://ha.bauer.tech"
# token: "<long-lived access token>"
# devices:
# - entity_id: cover.wohnzimmer_oben_rollo
# name: "Rollo"
# - entity_id: light.wohnzimmer_oben_...
# name: "Licht"
# scenes:
# - entity_id: scene.wohnzimmer_oben_...
# name: "Lesen"
# No mqtt: section. The Home Assistant integration exposes the *mouse's* lights,
# buttons, touch areas and tag scanner, none of which exist on a simulate-only
# instance - it would publish a device of dead entities.
#
# No lirc: section either, and consequently no top-level `remote:` mapping. This host
# uses mediapi_ir_control: irserver, a custom serial daemon on /dev/ttyUSB0; the
# backend's LircConfig speaks lircd's TCP protocol (pi_lirc, port 2222) and cannot
# talk to it.
# Backlight of the prev/next buttons while a figure is playing, 0..1. Inert here
# (no mouse), kept so the file reads the same as the other hosts'.
button_leds_brightness: 0.5
volume_increment: 5
# Volume, 0..100. The full range: this one is driven from a screen, not from a knob a
# child can spin, so there is nothing to protect against.
min_volume: 0
max_volume: 100
initial_volume: 40
volume_increment: 5
figures:
# Placeholder - serial_port is "simulate" so no reader ever reports a tag id, but the

View File

@@ -1,4 +1,10 @@
---
# NOT IN USE. The Kinderzimmer device is frozen on the pre-rearchitecture backend
# (host_vars/musicmouse.yml sets mediapi_install_kidsmusic: false, and deploy/musicmouse
# records the commit that is installed), so this role never installs this file today.
# It is kept because it is already written against the new schema and is what the
# migration will use - which also means moving /media/musicmouse/<figure>/ into
# Figuren/, since the new scanner expects the figures under that shelf.
general:
library:
root: /media/musicmouse

View File

@@ -0,0 +1,527 @@
# The typing game's lesson plan, referenced from config.yml's general.tippen.curriculum_file.
#
# Every lesson: title, subtitle (read aloud). Then either:
# - keys: the letters-kind keys this round has on the table. At most two of them may
# be new; the rest are keys already taught, written out to say "these are in play
# too". The loader tracks which keys were already active and spotlights only what is
# new, so `keys: [f, j, a, d, k]` right after `[f, j, a, d]` is a K lesson with the
# other four as review ("isolated" - heavy weight on K). A round that introduces
# nothing new spotlights its whole list instead ("mixed" - lighter weight, blended
# with everything learned so far), so two lessons with the same `keys` back to back
# is exactly how you write "isolated, then mixed" - no separate flag needed.
# - drill: true - a pure review round, no new content, whatever is active so far.
# - kind + words - a fragments/words/sentences consolidation round (dive mode by
# default; `mode:` overrides it - see the backend's `ELIGIBLE_MODES` for which
# modes fit which kind).
#
# Letters rounds always play in bubbles mode - the only arcade game that fits a single
# key at a time.
#
# Letter order follows German letter frequency, adapted to a home-row-first, mirrored
# pace for a six-year-old.
#
# Optional reward key, on any lesson:
#
# unlocks: <path>
#
# A path into the music library, written relative to config.yml's
# general.library.root - "Musik/Klaviersammlung/Lied.mp3", not a path from "/" or "~".
# (An absolute path, or one starting with "~", still resolves, so an old file keeps
# working - but relative is the form to write, since it survives moving the library.)
# Passing this lesson (two stars, or five attempts
# regardless of score) unlocks that track and everything before it in the same album,
# inclusive - so pointing at an album's last track unlocks the whole album, and
# pointing at track 5 of an audiobook unlocks chapters 1 through 5. The web front-end
# shows a still-locked track as a question mark, with a note on which lesson unlocks it.
#
# For a podcast (one album per episode under Kinderpodcasts), the same key unlocks that
# episode and every earlier one of the same show, oldest first - so pointing at the
# third episode ever published unlocks the first three, regardless of upload order.
#
# A path that matches nothing in the library is logged as a warning at startup, not a
# fatal error - a moved or mistyped path only loses that one reward, never the app.
#
# Unlike config.yml, this file is never written back to by the app - only progress is.
worlds:
- number: 1
title: "Die Grundstellung"
emoji: "🏝️"
reward: clownfish
lessons:
- title: "F und J"
subtitle: "Die Zeigefinger - die Tasten mit den Punkten"
keys: [f, j]
- title: "Mit A"
subtitle: "Das A kommt dazu"
keys: [f, j, a]
- title: "Ja"
subtitle: "Jaja ;)"
kind: fragments
words: [ja, fa, aja, afa, faja]
- title: "Wörter"
subtitle: "Aja"
kind: fragments
mode: feed
words: [ja, fa, jafa, aja, ajaja]
unlocks: "Musik/Klaviersammlung/Coffin Dance - Astronomía 2K.mp3"
- title: "D"
subtitle: "Das D kommt dazu"
keys: [f, j, a, d]
- title: "Wörter"
subtitle: "ja da"
kind: fragments
words: [da ja]
- title: "Wörter"
subtitle: "ja da"
kind: fragments
mode: feed
words: [da ja ada adda fada aja jaja]
- title: "Wörter"
subtitle: "ja da"
kind: fragments
words: [da ja ada adda fada aja jaja ajaja afa fa dafa jafa jada jajaja]
- title: "K"
subtitle: "Der rechte Mittelfinger"
keys: [f, j, a, d, k]
- title: "Wörter"
subtitle: "kaka"
kind: fragments
words: [ka, ak, kak, akka, kaka, daka, kada, jaka, kaja, fakka, kakka]
- title: "Wörter"
subtitle: "kada jaka"
kind: fragments
mode: feed
words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada,
ka, ak, kak, akka, kaka, daka, kada, jaka, kaja, fakka,
ka, ak, kak, akka, kaka, daka, kada, jaka, kaja, fakka]
- title: "S"
subtitle: "Der linke Ringfinger"
keys: [f, j, a, d, k, s]
- title: "Wörter"
subtitle: "sassa"
kind: fragments
words: [sa, as, sas, assa, sass, das, dasa, sada, kasa, saka, fasa, jassa]
- title: "Wörter"
subtitle: "das sassa"
kind: fragments
mode: feed
words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada,
ka, kak, akka, kaka, daka, kada, jaka, kaja,
sa, as, sas, assa, sass, das, dasa, sada, kasa, saka, fasa, jassa,
sa, as, sas, assa, sass, das, dasa, sada, kasa, saka, fasa, jassa]
# Unlocks track 2 of Klaviersammlung, Coffin Dance already being track 1.
unlocks: "Musik/Klaviersammlung/Shakira - Dai Dai.mp3"
- title: "L"
subtitle: "Der rechte Ringfinger"
keys: [f, j, a, d, k, s, l]
- title: "Wörter"
subtitle: "lalla"
kind: fragments
words: [la, al, als, all, lala, alla, salla, lasa, lass, kalla, falla, jalla]
- title: "Wörter"
subtitle: "lass das"
kind: fragments
mode: feed
words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada,
ka, kak, akka, kaka, daka, kada, jaka, kaja,
sa, as, sas, assa, sass, das, dasa, saka, jassa,
la, al, als, all, lala, alla, salla, lasa, lass, kalla, falla, jalla,
la, al, als, all, lala, alla, salla, lasa, lass, kalla, falla, jalla]
# Unlocks track 3 of Klaviersammlung.
unlocks: "Musik/Klaviersammlung/Shakira - Waka Waka (This Time for Africa) (feat. Freshlyground).mp3"
- title: "Ö"
subtitle: "Der rechte kleine Finger"
keys: [f, j, a, d, k, s, l, ö]
- title: "Wörter"
subtitle: "öl"
kind: fragments
words: [öl, ölö, öla, ölda, ösa, öll, sölö, köla, dölö, jölö, fölö, lösa]
- title: "Wörter"
subtitle: "öl kalk salsa"
kind: fragments
words: [ja, da, ada, adda, fada, aja, jaja, afa, dafa, jafa, jada,
ka, kak, akka, kaka, daka, kada, jaka, kaja,
sa, as, sas, assa, sass, das, dasa, saka, jassa,
la, al, als, all, lala, alla, salla, lasa, lass, kalla,
öl, ölö, öla, ölda, ösa, öll, sölö, köla, dölö, jölö, fölö, lösa,
öl, ölö, öla, ölda, ösa, öll, sölö, köla, dölö, jölö, fölö, lösa]
- title: "Übung: die Grundstellung"
subtitle: "Alle acht Finger"
drill: true
- title: "Erste kleine Wörter"
subtitle: "Echte Wörter mit acht Tasten"
kind: fragments
words: [da, ja, das, dass, als, all, fall, falls, lass, saal, kalk, salsa, jass, öl]
- title: "Die Leertaste"
subtitle: "Der Daumen kommt dazu"
keys: [" "]
- title: "Übung: Grundstellung mit Leertaste"
subtitle: "Jetzt mit dem Daumen"
drill: true
- title: "Wörter mit Leertaste"
subtitle: "Kleine Wörter, kleine Sätze"
kind: fragments
mode: feed
words: ["lass das", "das da", "da ja", "fall da", "kalk da", "saal da", "ja lass das", "als da", "öl da"]
# Unlocks track 4 of Klaviersammlung - the reward for finishing die Grundstellung.
unlocks: "Musik/Klaviersammlung/Tones and I - Dance Monkey.mp3"
# to here
- number: 2
title: "Nach oben"
emoji: "🌊"
reward: octopus
lessons:
- title: "Das E"
subtitle: "Mittelfinger links nach oben"
keys: [e]
- title: "E üben"
subtitle: "Die neue Taste festigen"
keys: [e]
- title: "Das I"
subtitle: "Mittelfinger rechts nach oben"
keys: [i]
- title: "I üben"
subtitle: "Die neue Taste festigen"
keys: [i]
- title: "Übung: E und I"
subtitle: "Beide Mittelfinger nach oben"
drill: true
- title: "Das R"
subtitle: "Zeigefinger links nach oben"
keys: [r]
- title: "R üben"
subtitle: "Die neue Taste festigen"
keys: [r]
- title: "Das U"
subtitle: "Zeigefinger rechts nach oben"
keys: [u]
- title: "U üben"
subtitle: "Die neue Taste festigen"
keys: [u]
- title: "Übung: R und U"
subtitle: "Beide Zeigefinger nach oben"
drill: true
- title: "Kleine Wörter: E I R U"
subtitle: "Erste echte Wörter mit der oberen Reihe"
kind: fragments
words: [die, sie, elf, eis, esel, see, keks, fiel, lied, rad, reis, eier, rufe, feuer, sauer, lauf]
- title: "Das T"
subtitle: "Zeigefinger links weit nach oben"
keys: [t]
- title: "T üben"
subtitle: "Die neue Taste festigen"
keys: [t]
- title: "Das Z"
subtitle: "Zeigefinger rechts weit nach oben"
keys: [z]
- title: "Z üben"
subtitle: "Die neue Taste festigen"
keys: [z]
- title: "Übung: T und Z"
subtitle: "Weit nach oben greifen"
drill: true
- title: "Das O"
subtitle: "Ringfinger rechts nach oben"
keys: [o]
- title: "O üben"
subtitle: "Die neue Taste festigen"
keys: [o]
- title: "Das W"
subtitle: "Ringfinger links nach oben"
keys: [w]
- title: "W üben"
subtitle: "Die neue Taste festigen"
keys: [w]
- title: "Übung: O und W"
subtitle: "Beide Ringfinger nach oben"
drill: true
- title: "Kleine Wörter: T Z O W"
subtitle: "Noch mehr echte Wörter"
kind: fragments
mode: feed
words: [tier, tafel, kette, leiter, zeit, salz, zelt, katze, rot, tor, los, foto, wo, wald, zwei, wolke]
- title: "Das P"
subtitle: "Kleiner Finger rechts nach oben"
keys: [p]
- title: "P üben"
subtitle: "Die neue Taste festigen"
keys: [p]
- title: "Das Q"
subtitle: "Kleiner Finger links nach oben"
keys: [q]
- title: "Q üben"
subtitle: "Die neue Taste festigen"
keys: [q]
- title: "Das Ü"
subtitle: "Kleiner Finger rechts, ganz außen"
keys: [ü]
- title: "Ü üben"
subtitle: "Die neue Taste festigen"
keys: [ü]
- title: "Übung: P Q Ü"
subtitle: "Die kleinen Finger nach oben"
drill: true
- title: "Übung: die obere Reihe"
subtitle: "Die ganze Reihe zusammen"
kind: words
mode: feed
words: [wolke, zeit, pause, prüfe, qualle, torte, reiter, würfel]
- title: "Übung: Welt 2 komplett"
subtitle: "Alles aus der oberen Reihe"
drill: true
# Unlocks the whole audiobook (its last chapter, inclusive of every chapter
# before it).
unlocks: "Hörbücher/Grüffelo/Der Grüffelo/08 - Der Grüffelo - Outro.mp3"
- number: 3
title: "Nach unten"
emoji: "🪸"
reward: seahorse
lessons:
- title: "Das N"
subtitle: "Zeigefinger rechts nach unten"
keys: [n]
- title: "N üben"
subtitle: "Die neue Taste festigen"
keys: [n]
- title: "Das M"
subtitle: "Zeigefinger rechts, neben dem N"
keys: [m]
- title: "M üben"
subtitle: "Die neue Taste festigen"
keys: [m]
- title: "Übung: N und M"
subtitle: "Die neuen Tasten festigen"
drill: true
- title: "Das G"
subtitle: "Zeigefinger links, in der Mitte"
keys: [g]
- title: "G üben"
subtitle: "Die neue Taste festigen"
keys: [g]
- title: "Das H"
subtitle: "Zeigefinger rechts, in der Mitte"
keys: [h]
- title: "H üben"
subtitle: "Die neue Taste festigen"
keys: [h]
- title: "Übung: G und H"
subtitle: "Die Mitte der Grundreihe"
drill: true
- title: "Kleine Wörter: N M G H"
subtitle: "Erste echte Wörter nach unten"
kind: fragments
words: [nase, nein, kind, wind, sonne, mama, mond, meer, maus, gut, gans, regen, hase, haus, hund, hupe]
- title: "Das C"
subtitle: "Mittelfinger links nach unten"
keys: [c]
- title: "C üben"
subtitle: "Die neue Taste festigen"
keys: [c]
- title: "Das V"
subtitle: "Zeigefinger links nach unten"
keys: [v]
- title: "V üben"
subtitle: "Die neue Taste festigen"
keys: [v]
- title: "Übung: C und V"
subtitle: "Nach unten greifen"
drill: true
- title: "Das B"
subtitle: "Zeigefinger links, neben dem V"
keys: [b]
- title: "B üben"
subtitle: "Die neue Taste festigen"
keys: [b]
- title: "Das Y"
subtitle: "Kleiner Finger links nach unten"
keys: [y]
- title: "Y üben"
subtitle: "Die neue Taste festigen"
keys: [y]
- title: "Übung: B und Y"
subtitle: "Ganz unten links"
drill: true
- title: "Kleine Wörter: C V B Y"
subtitle: "Noch mehr echte Wörter"
kind: fragments
words: [koch, milch, schule, chaos, vier, vase, voll, vater, baum, boot, bunt, brot, yoga, baby, typ, pony]
- title: "Das X"
subtitle: "Ringfinger links nach unten"
keys: [x]
- title: "X üben"
subtitle: "Die neue Taste festigen"
keys: [x]
- title: "Das Ä"
subtitle: "Kleiner Finger rechts, ganz außen"
keys: [ä]
- title: "Ä üben"
subtitle: "Die neue Taste festigen"
keys: [ä]
- title: "Übung: X und Ä"
subtitle: "Die letzten beiden Tasten"
drill: true
- title: "Übung: alle Buchstaben"
subtitle: "Das ganze Alphabet"
kind: words
mode: race
words: [delfin, wasser, xylofon, bäume, vogel, qualle, muschel, tauchen]
- title: "Übung: Welt 3 komplett"
subtitle: "Alles aus der unteren Reihe"
drill: true
# Unlocks the first five episodes of GEOlino Spezial, oldest first.
unlocks: "Kinderpodcasts/GEOlino Spezial/20200323 - Das Immunsystem.mp3"
- number: 4
title: "Große Buchstaben"
emoji: "👑"
reward: turtle
lessons:
- title: "Umschalttaste rechts, Teil 1"
subtitle: "Große Buchstaben der linken Hand"
keys: ["⇧"]
kind: words
words: [Delfin, Wal, Fisch, Baum]
- title: "Umschalttaste rechts, Teil 2"
subtitle: "Noch mehr große Buchstaben"
kind: words
words: [Garten, Ente, Vogel, Riff]
- title: "Umschalttaste links, Teil 1"
subtitle: "Große Buchstaben der rechten Hand"
kind: words
words: [Haus, Kind, Mond, Nase]
- title: "Umschalttaste links, Teil 2"
subtitle: "Noch mehr große Buchstaben"
kind: words
words: [Lampe, Onkel, Uhr, Puppe]
- title: "Übung: Namen, Teil 1"
subtitle: "Namen fangen groß an"
kind: words
words: [Anna, Lena, Paul, Mia, Emil, Jonas, Tom, Lisa]
- title: "Übung: Namen, Teil 2"
subtitle: "Noch mehr Namen"
kind: words
mode: feed
words: [Ben, Nora, Finn, Ida, Max, Ella, Oskar, Greta]
- title: "Übung: große und kleine"
subtitle: "Beides gemischt"
kind: words
words: ["Das Meer", "Ein Delfin", "Die Sonne", "Mein Boot", "Der Wal", "Eine Muschel", "Ein Fisch", "Das Riff", "Mein Ball", "Die Welle", "Ein Stern", "Der Hai"]
- title: "Übung: Welt 4 komplett"
subtitle: "Groß und klein zusammen"
kind: words
drill: true
words: [Delfin, Haus, Anna, Ben, "Das Meer", "Der Wal", Mond, Riff]
- number: 5
title: "Ganze Sätze"
emoji: "📖"
reward: pearlmussel
lessons:
- title: "Der Punkt"
subtitle: "Ringfinger rechts nach unten"
keys: ["."]
- title: "Punkt üben"
subtitle: "Die neue Taste festigen"
keys: ["."]
- title: "Erste Sätze mit Punkt"
subtitle: "Ein Satz, ein Punkt"
kind: sentences
words:
- "Das Meer ist tief."
- "Der Hund bellt."
- "Ich mag Kekse."
- "Die Sonne scheint."
- "Der Wal ist riesig."
- "Wir gehen baden."
- "Mama liest ein Buch."
- "Der Fisch schwimmt."
- "Heute ist es warm."
- "Ich habe einen Ball."
- "Die Welle ist hoch."
- "Papa kocht Suppe."
- title: "Das Komma"
subtitle: "Mittelfinger rechts nach unten"
keys: [","]
- title: "Komma üben"
subtitle: "Die neue Taste festigen"
keys: [","]
- title: "Sätze mit Komma"
subtitle: "Zwei Gedanken, ein Satz"
kind: sentences
mode: race
words:
- "Ich mag Wale, Delfine und Fische."
- "Erst lesen, dann tippen."
- "Es ist warm, also baden wir."
- "Rot, gelb und blau sind Farben."
- "Wenn es regnet, bleiben wir drinnen."
- "Der Delfin springt, taucht und spielt."
- "Morgen, sagt Papa, fahren wir los."
- "Eins, zwei, drei, vier."
- "Oma, Opa und ich gehen schwimmen."
- "Die Sonne scheint, das Meer glitzert."
- "Muscheln, Steine und Sand liegen am Strand."
- title: "Der Bindestrich"
subtitle: "Kleiner Finger rechts, ganz außen"
keys: ["-"]
- title: "Bindestrich üben"
subtitle: "Die neue Taste festigen"
keys: ["-"]
- title: "Sätze mit Bindestrich"
subtitle: "Zwei Wörter, ein Strich"
kind: sentences
words:
- "Wir spielen mit dem Wasser-Ball."
- "Das ist ein Delfin-Baby."
- "Meine Ur-Oma kommt heute."
- "Wir bauen eine Sand-Burg."
- "Der Fisch-Schwarm ist riesig."
- "Ich trage mein T-Shirt."
- "Das Schwimm-Bad ist offen."
- "Die Bade-Hose ist nass."
- "Wir essen ein Eis-Hörnchen."
- "Das Segel-Boot ist blau."
- "Mein Lieblings-Tier ist der Delfin."
- title: "Fragezeichen und Ausrufezeichen"
subtitle: "Mit der Umschalttaste"
keys: ["ß", "1"]
- title: "Fragezeichen und Ausrufezeichen üben"
subtitle: "Die neuen Tasten festigen"
keys: ["ß", "1"]
- title: "Fragen und Rufe"
subtitle: "Wie klingt ein Satz?"
kind: sentences
words:
- "Wo ist der Delfin?"
- "Das war toll!"
- "Wie geht es dir?"
- "Pass auf!"
- "Kommst du mit?"
- "Der Wal ist so groß!"
- "Hast du Hunger?"
- "Hurra, Ferien!"
- "Was schwimmt da?"
- "Schau mal, ein Hai!"
- "Wie tief ist das Meer?"
- "Wir haben es geschafft!"
- title: "Übung: ganze Sätze"
subtitle: "Alles zusammen"
kind: sentences
drill: true
mode: race
words:
- "Der Delfin schwimmt sehr schnell."
- "Wo ist mein Boot?"
- "Ich tippe jetzt mit zehn Fingern!"
- "Das Meer ist blau, tief und kalt."
- "Kannst du das auch?"
- "Wir bauen eine Sand-Burg am Strand."
- "Die Möwe fliegt über das Wasser."
- "Oma, Opa und ich gehen schwimmen."
- "Das ist ja super!"
- "Wie heißt der große Wal?"
- "Im Riff wohnen bunte Fische."
- "Der Krake hat acht Arme."

View File

@@ -42,6 +42,23 @@
force: true
notify: Reinstall musicmouse backend
- name: Check the checkout has the layout this role knows
ansible.builtin.stat:
path: /opt/musicmouse/python-backend/pyproject.toml
register: pi_musicmouse_pyproject
- name: Fail on a pre-rearchitecture checkout
ansible.builtin.fail:
msg: >-
{{ musicmouse_version }} has no python-backend/pyproject.toml, so it predates the
rearchitecture and this role cannot install it - the venv, the systemd unit and
the web/ build all live somewhere else on those commits. Point
musicmouse_version at a branch that has it (deploy/musicdolphin), or set
mediapi_install_kidsmusic to false for this host and leave whatever is installed
alone. Stopping here rather than failing three tasks deeper with the real reason
buried.
when: not pi_musicmouse_pyproject.stat.exists
- name: Create virtualenv with Python {{ pi_musicmouse_python_version }}
ansible.builtin.command:
cmd: "/usr/local/bin/uv venv --python {{ pi_musicmouse_python_version }} /opt/musicmouse/.venv"
@@ -58,6 +75,17 @@
src: "{{ pi_musicmouse_config_file }}"
dest: /media/musicmouse/config.yml
mode: "0644"
# Install-once: the app writes this file itself (parent mode's volume keys, the
# remote-control mapping). See pi_musicmouse_force_config in defaults.
force: "{{ pi_musicmouse_force_config }}"
notify: Restart musicmouse
- name: Install tippen curriculum
ansible.builtin.copy:
src: "{{ pi_musicmouse_curriculum_file }}"
dest: /media/musicmouse/tippen-curriculum.yml
mode: "0644"
when: pi_musicmouse_curriculum_file | length > 0
notify: Restart musicmouse
- name: Install systemd service file (from the checked-out repo, not a copy kept here)
@@ -108,5 +136,6 @@
daemon_reload: "yes"
# manual steps:
# - set samba passwords with smbpasswd
# - copy music into /media/musicmouse (or via samba share)
# - manual patching of hassclient necessary :( loop has to be passed in, to not use running_event_loop
# - copy music into /media/musicmouse: scripts/sync-library.sh in the app repo, which
# also carries the cover and analysis cache over (or use the samba share, and let
# the device spend the DSP time itself)