Genre & Mood Rules
How a song gets its genres and moods in Music Studio (Create, Edit, Upload, draft "+Genre"), how those tags reach the AI, the catalog and its admin page, and how HOOP Music Browse uses them. For Haikal (QC) and Leong. Written 2026-10-08 from the code on master (v3.213). The source of truth stays the code and docs/music.md §3.6 "Genre workflow / rules"; file references are in grey under each section.

① Summary

Genres and moods come only from the Browse catalog (the music_tags table, kind genre / mood). Every song has 1 main genre (required to Compose) + up to 2 more genres + up to 3 moods, 6 tags at most. They are picked in one place, the "Where listeners find this song" card on Create / Edit / Upload, which opens a search sheet with the same coloured tiles as Browse. The picked names are written in front of the style text the AI receives. Magic Pen fills empty groups but never replaces what the creator picked. Admins manage the catalog in Developer Centre → Genres & moods, and listeners only see a genre or mood tile in Browse once it has at least 1 live song (changed from 3 on 10-08 15:30).

WhenWhat
10-08 10:44 · QC askedHaikal: redesign the Compose genre module (Spotify-like), align it with Browse, revisit the limits; cover Create, Edit and the draft "+Genre" redirect. Discuss first.
v3.202 server + web, 10-08Developer Centre "Genres & moods" admin page (add / rename / colour / aliases / reorder / hide, suggestions), 12 new genres + 4 new moods (migration 000535), Browse hides tiles with no live songs.
v3.213 server + web, 10-08Compose genre / mood redesign (1 + 2 + 3, search sheet, Magic Pen four cases, clear with Undo, tags sent to the AI, genre_required, Sound tab, draft "+Genre", Upload) and the new "Browse all" layout with tile covers. No migration.
PhonesiOS / Android get both with the next TestFlight build. Until then, test on hoop-web.pages.dev. Old phone builds keep working (see Rules).
New song, empty
New song: the card, Compose grey until a main genre is picked
Genre sheet
The sheet: search, Browse tiles, count, Suggested

② Rules

RuleHow it works
Limits1 main genre (required to Compose) + up to 2 more genres + up to 3 moods = at most 6 tags per song. The server enforces the same caps (3 genres, 3 moods, 6 tags); extra tags are silently dropped, in the order sent, so the main genre (sent first) is never the one cut.
Where the main genre livesIn songs.genre, the column that already drives the genre pill on song rows. No new column. The app always sends the main genre first; changing only the main genre still saves it, and reopening Edit shows the same main genre.
Old songsSongs that only have tags get the first picked genre (catalog order) as main, once. A free-text genre from before the catalog shows in the main slot without using a place, and gives way as soon as a catalog genre is picked.
Required on Compose, not on Save draftApp: with no main genre the Compose button is grey and says "Pick a main genre to compose". Save draft is never blocked. Server: POST /v1/music/generate with a tags key but no recognisable genre → 400 genre_required (checked before polishing and before the quota is charged; the app shows the same line). Old phone builds don't send tags, so they are not checked and work as before.
Fixed orderPills on the card are always in catalog order (the admin's sort), not in tap order. Removing and re-adding a tag puts it back in the same place.
Limit messageTapping one more when a group is full changes nothing and shows "You can pick up to 2 more genres — remove one first" / "You can pick up to 3 moods — remove one first". In the "More genres" sheet the main genre tile is marked "Main" and can't be tapped.
The sheetSearch (matches name, Chinese name, slug or alias), Browse colours, a tick on picked tiles, a count next to the title. Main genre is single choice and closes itself; more genres and moods are multi-choice. Tiles that match the style text carry a "Suggested" badge.
Sound tabIn "+ Add a style" the old Genre tab is now Sound (same key, so old builds still find it): only instrument / sound words plus the Chinese-style group. Catalog genres are no longer there. Every tab in that panel only adds words to the style text; it never picks categories. Footer: "These only guide the AI…".
UploadSame card, same sheet, same 1 + 2 + 3. Upload is grey until a main genre is picked (when the catalog loaded).
Mixed versions are safe. New server + old phone: no tags → no genre check and no tag line; the old 3 + 2 caps fit inside the new ones; the old Genre tab just loses the catalog genres. New phone + old server: a 3rd mood is cut to 2, tags is ignored.

app/lib/screens/music_category_pick.dart (kMsMaxMoreGenres = 2, kMsMaxPickMoods = 3, MsCatSelection, showMsCategorySheet) · backend/internal/domain/music/browse.go (maxGenresPerSong, maxMoodsPerSong, maxTagsPerSong, setSongTagsSQL) · compose_tags.go · style_catalog.go (styleTabs). Tests: song_tags_caps_1002_db_test.go, compose_tags_1008_db_test.go, TestSoundTabHasNoCatalogGenres, music_browse_tags_0921_test, music_genre_rules_1008_test.

③ Magic Pen

The four cases Haikal approved on 10-08. Which tiles get picked comes from suggestedCategorySlugs: whole-word matches of the style text against each tile's slug, name and aliases (for both genres and moods). The 10-02 behaviour "re-pick every time the pen runs" is gone, because it replaced what the creator had chosen.

#Before pressing the penWhat the pen doesCategories afterwards
1Style empty, nothing pickedWrites a style from scratchPicks main genre + up to 2 more + up to 3 moods to fit it, marked with the pen; the card says "Picked by Magic Pen to fit your style. Remove any you don't want."
2Style empty, some pickedWrites a style using the picked English names as the seed ("Rock, Pop")Keeps every pick; fills only the groups that are still empty
3Creator wrote a styleRewrites (polishes) that textPicks only if nothing was picked; if anything was picked, touches nothing
4Clear button on the style boxClears the textClears all categories too; toast "Style and genres cleared" with Undo, which brings back both the text and the categories
Magic Pen filled
Case 1: pen wrote the style and picked genre + moods

MsCatSelection.penFill (music_category_pick.dart) · style card trash button (music_studio_screen.dart) · suggestedCategorySlugs (music_ui_api.dart). Tests: music_compose_retry_1001_test ⑥⑥b⑥c⑦⑦b⑦c, ⑪⑪b.

④ How the tags reach the AI

  1. On Compose the app sends tags (main genre first).
  2. The server looks up each tag's English name (music_tags.name_en) and builds one line: Genre: Rock, Pop. Mood: Chill. (no mood part if there are no moods).
  3. The line goes at the front of the style text, and it is added after polishing (polishing rewrites the whole text, so adding it earlier would be lost).
  4. The whole style is limited to 1000 characters (maxStyle). If it is too long, tags are trimmed first: moods from the last one back, then the extra genres from the last one back. The main genre and the creator's own text are never trimmed.
  5. Auto-tagging (autoTagSong) still reads only the creator's own text, not this line.

backend/internal/domain/music/compose_tags.go (composeTagLine, styleWithTags), handler.go generate. Tests: compose_tags_1008_db_test.go ③, TestStyleWithTagsTrimsTagsNotText, music_compose_retry_1001_test ⑦d.

⑤ Draft "+Genre"

Draft +Genre landing
After tapping "+Genre" on a draft: Edit at Style, sheet open, Suggested tiles

openSongEditor(null, 'cats'), _genreChip (music_studio_screen.dart). Tests: music_compose_cats_0923_test ①, music_genre_rules_1008_test ①①b.

⑥ The catalog

24 genres and 12 moods (12 + 8 from migration 000256, 12 + 4 added by 000535 on 10-08), in Browse order. The three special tiles (Music, New Releases, Mood) are not managed here.

Genres (24)

#Name中文Since
1Pop流行000256
2K-pop韩流000256
3Hip-Hop嘻哈000256
4Mandopop华语流行000256
5Rock摇滚000256
6Latin拉丁000256
7Dance / Electronic电子舞曲000256
8R&BR&B000256
9Jazz爵士000256
10Metal金属000256
11Classical古典000256
12Folk & Acoustic民谣000256
13Indie独立new 10-08
14Chinese Traditional国风new 10-08
15Malay Pop马来流行new 10-08
16Soul / Funk灵魂 / 放克new 10-08
17Country乡村new 10-08
18Lo-fiLo-finew 10-08
19Reggae雷鬼new 10-08
20Blues布鲁斯new 10-08
21Cantopop粤语流行new 10-08
22J-pop / Anime日系 / 动漫new 10-08
23Soundtrack原声配乐new 10-08
24Worship / Gospel敬拜 / 福音new 10-08

Moods (12)

#Name中文Since
1Chill放松000256
2Sleep助眠000256
3Party派对000256
4Focus专注000256
5Workout运动000256
6Romance浪漫000256
7Feel Good好心情000256
8Sad伤感000256
9Energetic活力new 10-08
10Nostalgic怀旧new 10-08
11Dreamy梦幻new 10-08
12Dark暗黑new 10-08

Aliases

Each tile can have up to 20 aliases: other words that mean the same thing (guofeng, guzheng, erhu → Chinese Traditional; lofi, chillhop → Lo-fi; ost, film score → Soundtrack). They are used by search in the sheet and by Magic Pen / "Suggested" to match style text. Aliases only have to be unique within the same kind, so the genre Lo-fi and the mood Chill can both know "lofi". 000535 also moved cantopop off Mandopop and soul off R&B onto their new tiles.

Admin page: Developer Centre → Genres & moods

Admin list
Genres & moods admin page

How to add a genre in future

  1. Developer Centre → Genres & moods → Genres → + (or Suggestions → Add as genre).
  2. Type the English and Chinese name, pick a colour, add aliases creators are likely to type in style text.
  3. Save. It is pickable in Compose straight away (0 songs is fine).
  4. Move it up / down to where it should sit in the sheet and in Browse.
  5. It appears in listener Browse by itself once 3 published songs carry it. No code change, no migration, no app release.

backend/migrations/000256_music_tags.up.sql, 000535_music_tags_admin.up.sql · backend/internal/domain/music/tags_admin.go (GET/POST /v1/music/admin/tags, PATCH /v1/music/admin/tags/{slug}, POST /v1/music/admin/tags/reorder) · app/lib/screens/music_tags_admin_screen.dart. Tests: tags_admin_1008_db_test.go, music_tags_admin_1008_test.dart.

⑦ Browse

Which tiles listeners see

CallUsed byGives
GET /v1/music/browse?listener=1Browse page, home category pillsNo hidden tiles, and no genre / mood with no published song (browseMinSongs = 1; was 3 until 10-08 15:30). Special tiles always.
GET /v1/music/browse (or ?all=1)Compose / Edit / Upload, diary music picker, old phone buildsEvery tile that isn't hidden, including new ones with 0 songs, plus aliases.

Why the default is the full catalog: old phone builds call Compose's catalog without any parameter. If the default were narrowed, they couldn't pick new genres and could get stuck unable to compose. And Compose must offer 0-song tiles, otherwise a new genre could never reach its 3rd song.

New "Browse all" layout (v3.213)

Browse all
New Browse all

backend/internal/domain/music/browse.go (browseMinSongs, browseCoverSQL) · app/lib/screens/music_browse_screen.dart (BrowseTile). Tests: tags_admin_1008_db_test.go, browse_cover_1008_db_test.go, browse_golden_test.dart.

⑧ Test checklist

On https://hoop-web.pages.dev (refresh first). Music Studio → New song unless stated.

#DoExpected
1Create: open New song, write nothing.Card "Where listeners find this song": Main genre Required, More genres 0/2, Mood 0/3. Compose is grey with "Pick a main genre to compose".
2Tap "Save draft" without a genre.The draft saves; no genre error.
3Tap "Choose a main genre", search "guofeng".Chinese Traditional is found (alias). Tap it: sheet closes, it shows as main, Compose turns active.
4More genres → Add: pick 2, then tap a 3rd.3rd refused with "You can pick up to 2 more genres — remove one first"; nothing changes. The main genre tile says "Main" and can't be tapped.
5Mood → Add: pick 3, then a 4th.4th refused with "You can pick up to 3 moods — remove one first".
6Remove a middle genre pill, then add it back.Pills always in catalog order; it returns to the same spot.
7Magic Pen case 1: new song, style empty, nothing picked, tap the pen.Style is written; main genre + up to 2 genres + up to 3 moods picked; "Picked by Magic Pen…" line on the card.
8Case 2: new song, pick Rock only, style empty, tap the pen.Style is about rock; Rock stays main; only empty groups (more genres / moods) are filled.
9Case 3: type your own style, pick Pop, tap the pen.Your text is polished; Pop and nothing else stays picked. Repeat with nothing picked: the pen now picks categories.
10Case 4: with style text and picks, tap the style box's clear (trash) button, then Undo.Text and all categories clear, toast "Style and genres cleared"; Undo restores both.
11"+ Add a style" → open each tab.The old Genre tab is called Sound and has only instrument / sound words; tapping words adds them to the style text but never picks a genre.
12Compose with main genre + moods.The song generates. (The AI receives "Genre: … Mood: …" in front of the style; not visible in the UI.)
13Edit an existing song: change only the main genre, save, reopen.The new main genre is shown; the genre pill in the song list updates.
14Open a draft with no genre → tap "+Genre".Edit opens at the Style card with the Main genre sheet open. With a main genre already: the More genres sheet opens.
15Two accounts: B is an accepted collaborator on A's draft; B opens it.B sees no "+Genre" chip.
16Upload a song: try without a main genre, then with.Upload grey until a main genre is picked; same sheet and limits.
17Admin account: Developer Centre → Genres & moods.Genres / Moods / Suggestions tabs; live song counts, aliases, "Not in Browse yet" on tiles with no live song. Non-admin: page not available.
18Admin: add a test genre with an alias; rename it; change colour; move it up; hide it; unhide it.Each change sticks after reload. While hidden it can't be picked in Compose. There is no Delete. Each action appears in the admin audit log.
19Admin: Suggestions → "Add as genre" on one, "Dismiss" on another.Add opens with the name filled; after saving, the suggestion leaves the list. Dismissed one leaves the list.
20Listener: HOOP Music → Browse all.Music + New Releases wide on top, then Genres, then Moods. No song counts. Covers tilted bottom-right (or a note icon).
211-song rule: compare the new genre from step 18 (0 songs) in Compose vs Browse.Pickable in Compose, absent from Browse. As soon as one published song has it, it shows in Browse.
22Cover: note the most-played published song in a genre with a cover.That song's cover is the one on the tile.

⑨ Decisions, 10-08

Time (MYT)HaikalResult
10:44Request: redesign the genre module (Spotify-like), align with Browse, revisit limits; Create, Edit and draft "+Genre". Discuss first.Audit + proposal sent: main genre + 2 more, up to 3 moods, search sheet with Browse tiles, tags sent to the AI, new genres for Haikal to choose.
12:02 · 12:24 · 12:36Answers to the proposal questions: the new genres / moods to add, admin page in Developer Centre, Browse hides tiles under 3 live songs; 12:24 "build now" for the admin page and new tags; Magic Pen still being settled (four cases proposed).Admin page + 000535 built and shipped in v3.202.
13:38Approved the Compose redesign (designs incl. Magic Pen four cases).Built and shipped in v3.213.
13:41Browse redesign in scope for this session too.New Browse all with cover_url, in v3.213.
14:08"Your top genres" later.Not built; listed under Future.
15:27 · 15:30Haikal on web live: only Pop showed, and the old "Mood" tile appeared. Cause: 64 of 68 live songs had no tags. Decision: show a tile from 1 live song (counts are hidden anyway); dry-run auto-tagging of the 64 untagged songs first.Threshold 1 shipped; backfill waits for the dry-run list OK.
16:01Haikal approved the backfill after reading the dry-run list.One transaction on prod: 226 tags on 59 of the 64 untagged live songs (main genre = the one mentioned first in the style text; songs.genre set where empty on 49). Mandopop aliases + mandarin, chinese song, 中文, 普通话. 5 songs got nothing (uploads / short Chinese text). Undo list: table _bak_20261008_tag_backfill; audit log area music, action backfill. Browse went from 6 tiles to 19 genres + 11 moods.

⑩ Open ideas