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
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user