Files
docker-infrastructure/.claude/skills/subwave/SKILL.md
T
poprhythm 39fdc4276f Add subwave skill for SUB/WAVE admin-API interactions
Wraps show/persona/schedule/trigger calls that were previously done by
hand with raw docker exec curl, and documents the gotchas hit tonight
(whole-object PUT/POST semantics, the frequency enum, the silent
default-persona trap, and the port-not-published-to-host setup).

Claude-Session: https://claude.ai/code/session_01HZQK6jHmdTpFjFZM8FUnqA
2026-09-19 23:40:00 +00:00

101 lines
5.2 KiB
Markdown

---
name: subwave
description: Manage SUB/WAVE (self-hosted AI radio station) shows, DJ personas, the weekly schedule grid, and live overrides. Use for any SUB/WAVE admin-API work — creating a new anchor show, minting a DJ persona, scheduling a slot, or triggering a show live.
---
# SUB/WAVE admin API
A CLI wrapper (`subwave_api.py`, in this skill's directory) around the
`sub-wave-controller` admin REST API, distilled from a long session of
building ~30 anchor-playlist shows and DJ personas by hand with raw
`docker exec curl` calls. Use it instead of re-deriving the request shapes
each time.
## Setup
Auth comes from `subwave/.env` (`ADMIN_USER`/`ADMIN_PASS`) at the repo root —
no flags needed. **The admin API (port 7701) is not published to the host** —
`docker ps` shows `sub-wave-controller` with only `7701/tcp` (internal), not
`0.0.0.0:7701->7701`. Every call must go through
`docker exec sub-wave-controller curl ...`, which is exactly what this script
does. Don't waste a round trip re-checking this — it's a deliberate
container-internal-only setup, not a misconfiguration.
Run it as: `python3 .claude/skills/subwave/subwave_api.py <command> ...`
## Commands
```
persona list
persona create --name N --voice V --tagline T --soul S
[--frequency silent|quiet|moderate|chatty|aggressive]
[--humour 0-10] [--warmth 0-10] [--local-colour 0-10]
show list
show get --id ID
show create --name N --topic T --playlist-id ID
[--genres a,b,c] [--moods a,b] [--energies a,b]
[--eras 1990-2000,2005-2010] [--persona-id ID]
show set-persona --id ID --persona-id ID
show set-name --id ID --name N
show set-topic --id ID --topic T
schedule grid # full 7x24 weekly grid, show names
schedule open-slots # every null (day, hour) pair
schedule set --show-id ID --day 0-6 --hour 0-23 # day 0 = Sunday
schedule set-random --show-id ID # pick a random open slot
trigger --show-id ID [--minutes 60] # live override, starts immediately
```
Playlists themselves (search/curate/create anchor playlists in Navidrome) are
a separate concern — use the **navidrome** skill for that, then pass the
resulting playlist id to `show create --playlist-id`.
## Gotchas learned the hard way
- **`POST /shows` needs the show wrapped in `{"show": {...}}`** — posting the
show object bare fails with a schema error (`expected object, received
undefined`). The CLI handles this.
- **New shows need a full show object, not a partial patch** — there's no
PATCH endpoint. To change one field (persona, name, topic), read the full
current show from `schedule.json`, mutate the one field, and re-POST the
whole thing. `show set-persona`/`set-name`/`set-topic` do exactly this.
- **`PUT /schedule` takes the *entire* weekly grid**, not a single slot —
read the current grid, mutate one `[day][hour]` cell, PUT the whole thing
back. `schedule set`/`set-random` do this for you. The response's
`dropped` count should always be `0`; a nonzero value means a show id in
the grid doesn't exist anymore (a bug elsewhere, not addressed here).
- **`POST /settings` for a new persona is also whole-array append**, not a
single-persona-create endpoint — there is no such endpoint. Read
`personas`, append one object, POST `{"personas": [...]}`.
- **Persona `frequency` is a strict enum**: `silent | quiet | moderate |
chatty | aggressive`. Passing `"normal"` (an easy guess) fails with `must
be one of: silent, quiet, moderate, chatty, aggressive` — the CLI defaults
to `moderate` to sidestep this.
- **A show with no explicit `personaId` inherits whatever `p_default0` is**
(one of the original 3 built-in personas) — and if that persona has
`djMode: false`, the show runs with **zero DJ talk breaks** and sounds
silent/DJ-less. Always set a real persona explicitly once you know what
voice/character you want; don't assume a blank field means "no host," it
means "the accidental default host."
- **`/schedule/override` (the live-trigger endpoint) starts immediately** —
`startedAt: Date.now()`. There's no way to schedule a *future* start time
through this endpoint; "play this at 9pm" only works if it's already ~9pm,
or by placing the show in the recurring weekly grid at that hour instead.
- **Two shows can't cleanly share one weekly slot** — if you want two moods
to alternate in the same hour across the week (e.g. show A on
Sun/Tue/Thu/Sat, show B on Mon/Wed/Fri), that's just setting the same
`[hour]` column to different show ids on different `day` rows via repeated
`schedule set` calls — there's no "alternate" primitive, just per-day-row
assignment.
- **Kokoro voice pool has ~28 usable English voices** (`af_*`/`am_*`
American, `bf_*`/`bm_*` British) sharing one 27MB `voices-v1.0.bin` file —
installing/using more costs nothing. `PERSONA_LIMIT = 48` is the only hard
ceiling (`/app/src/schemas/persona.ts` inside the controller container).
- **A library rescan can surface artists that "don't exist" on a first
check** — Navidrome's index lags real disk state. If a user swears an
artist should be there, `navidrome_api.py`-equivalent rescan
(`startScan`/`getScanStatus` via the Subsonic API) before concluding it's
actually missing.