@nodaro/prompts 1.7.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +5422 -4269
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1198 -61
- package/dist/index.d.ts +1198 -61
- package/dist/index.js +5336 -4271
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
- package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
- package/src/__tests__/catalog-packs.test.ts +130 -0
- package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
- package/src/__tests__/catalog-terms.test.ts +162 -0
- package/src/__tests__/character-default-role.test.ts +3 -2
- package/src/__tests__/content-free-contract.test.ts +45 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
- package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
- package/src/__tests__/parameter-hint-mode.test.ts +385 -0
- package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
- package/src/__tests__/person-packs.test.ts +166 -0
- package/src/__tests__/project-all-catalogs.test.ts +26 -0
- package/src/__tests__/prompt-builder.test.ts +53 -0
- package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
- package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
- package/src/__tests__/term.test.ts +104 -0
- package/src/__tests__/transitions.test.ts +8 -5
- package/src/__tests__/upstream-immutability.test.ts +23 -0
- package/src/action-fx.ts +67 -17
- package/src/aesthetic.ts +54 -6
- package/src/atmosphere.ts +64 -23
- package/src/backdrop.ts +44 -30
- package/src/camera-format.ts +33 -11
- package/src/camera-motions.ts +89 -0
- package/src/catalog-packs.ts +125 -0
- package/src/catalog-sidecar-coverage.ts +36 -0
- package/src/character-fx.ts +112 -39
- package/src/color-look.ts +44 -27
- package/src/composition-effects.ts +21 -7
- package/src/era.ts +24 -0
- package/src/exposure-settings.ts +76 -18
- package/src/framing.ts +100 -0
- package/src/held-prop.ts +125 -63
- package/src/identity-lock.ts +12 -5
- package/src/image-reference-doctrine.ts +55 -0
- package/src/index.ts +5 -0
- package/src/instrumentation.ts +148 -57
- package/src/lens.ts +31 -15
- package/src/lighting.ts +120 -59
- package/src/loop-subject.ts +27 -1
- package/src/materials.ts +123 -69
- package/src/mood.ts +123 -51
- package/src/music-genre.ts +171 -67
- package/src/music-mood.ts +85 -17
- package/src/parameter-prompt-hint.ts +168 -56
- package/src/person-packs.ts +182 -0
- package/src/person.ts +506 -408
- package/src/photo-genre.ts +37 -22
- package/src/photographer.ts +138 -1
- package/src/picker-catalogs.ts +86 -40
- package/src/pose.ts +105 -40
- package/src/post-process-effects.ts +51 -8
- package/src/prompt-builder.ts +16 -1
- package/src/render-quality.ts +25 -7
- package/src/setting.ts +30 -14
- package/src/style.ts +33 -15
- package/src/styling.ts +155 -94
- package/src/temporal.ts +80 -18
- package/src/term.ts +155 -0
- package/src/transitions.ts +73 -26
- package/src/voice-character.ts +190 -90
- package/src/voice-delivery.ts +83 -12
package/src/music-genre.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { pickIds } from "@nodaro/shared"
|
|
16
16
|
|
|
17
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
18
|
+
|
|
17
19
|
export type MusicGenreCategory =
|
|
18
20
|
| "hip-hop-rnb"
|
|
19
21
|
| "electronic"
|
|
@@ -47,6 +49,16 @@ export interface MusicSubgenre {
|
|
|
47
49
|
readonly id: string
|
|
48
50
|
readonly label: string
|
|
49
51
|
readonly promptHint: string
|
|
52
|
+
/**
|
|
53
|
+
* Compact professional form injected in COMPACT hint mode (see `term.ts`).
|
|
54
|
+
* Authored where the lowercased label is not the genre name a producer would
|
|
55
|
+
* write — the picker drops the parent genre from the label to keep the tile
|
|
56
|
+
* short ("Conscious" → "conscious hip hop", "Beats" → "lo-fi beats"), the
|
|
57
|
+
* label is a UI compound the derivation mangles ("Drum & Bass",
|
|
58
|
+
* "Trap (EDM)"), or the bare word means something else entirely outside
|
|
59
|
+
* music ("Dub", "Swing", "Romantic"). Everywhere else the label IS the term.
|
|
60
|
+
*/
|
|
61
|
+
readonly term?: string
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
export interface MusicGenre {
|
|
@@ -54,6 +66,12 @@ export interface MusicGenre {
|
|
|
54
66
|
readonly label: string
|
|
55
67
|
readonly description: string
|
|
56
68
|
readonly promptHint: string
|
|
69
|
+
/**
|
|
70
|
+
* Compact professional form (see `term.ts`), authored where the lowercased
|
|
71
|
+
* label does not read as a music genre on its own — "World" → "world music",
|
|
72
|
+
* "R&B" → "rhythm and blues". Everywhere else the label IS the term.
|
|
73
|
+
*/
|
|
74
|
+
readonly term?: string
|
|
57
75
|
readonly category: MusicGenreCategory
|
|
58
76
|
readonly subgenres: ReadonlyArray<MusicSubgenre>
|
|
59
77
|
}
|
|
@@ -63,6 +81,12 @@ export interface MusicEra {
|
|
|
63
81
|
readonly label: string
|
|
64
82
|
readonly description: string
|
|
65
83
|
readonly promptHint: string
|
|
84
|
+
/**
|
|
85
|
+
* Compact professional form (see `term.ts`). The decade labels already ARE
|
|
86
|
+
* the term; authored only for the named eras whose label is not what a
|
|
87
|
+
* producer writes ("Futurist" → "futuristic").
|
|
88
|
+
*/
|
|
89
|
+
readonly term?: string
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
// Sourced from frontend/src/lib/suno-tags.ts SUNO_TAGS Genre + Subgenre buckets.
|
|
@@ -81,8 +105,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
81
105
|
{ id: "drill", label: "Drill", promptHint: "drill" },
|
|
82
106
|
{ id: "uk-drill", label: "UK Drill", promptHint: "UK drill" },
|
|
83
107
|
{ id: "cloud-rap", label: "Cloud Rap", promptHint: "cloud rap" },
|
|
84
|
-
{ id: "conscious-rap", label: "Conscious", promptHint: "conscious hip hop" },
|
|
85
|
-
{ id: "gangsta-rap", label: "Gangsta", promptHint: "gangsta rap" },
|
|
108
|
+
{ id: "conscious-rap", label: "Conscious", promptHint: "conscious hip hop", term: "conscious hip hop" },
|
|
109
|
+
{ id: "gangsta-rap", label: "Gangsta", promptHint: "gangsta rap", term: "gangsta rap" },
|
|
86
110
|
{ id: "jazz-rap", label: "Jazz Rap", promptHint: "jazz rap" },
|
|
87
111
|
{ id: "alt-hip-hop", label: "Alt Hip Hop", promptHint: "alternative hip hop" },
|
|
88
112
|
{ id: "phonk", label: "Phonk", promptHint: "phonk" },
|
|
@@ -90,7 +114,7 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
90
114
|
{ id: "uk-hip-hop", label: "UK Hip Hop", promptHint: "UK hip hop" },
|
|
91
115
|
{ id: "grime", label: "Grime", promptHint: "grime" },
|
|
92
116
|
{ id: "pluggnb", label: "PluggnB", promptHint: "pluggnb" },
|
|
93
|
-
{ id: "rage", label: "Rage", promptHint: "rage hip hop" },
|
|
117
|
+
{ id: "rage", label: "Rage", promptHint: "rage hip hop", term: "rage hip hop" },
|
|
94
118
|
{ id: "jersey-club", label: "Jersey Club", promptHint: "Jersey club" },
|
|
95
119
|
{ id: "brazilian-funk", label: "Brazilian Funk", promptHint: "Brazilian funk" },
|
|
96
120
|
{ id: "moombahton", label: "Moombahton", promptHint: "moombahton" },
|
|
@@ -102,12 +126,13 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
102
126
|
label: "R&B",
|
|
103
127
|
description: "Smooth vocals, soul-influenced",
|
|
104
128
|
promptHint: "R&B",
|
|
129
|
+
term: "rhythm and blues",
|
|
105
130
|
category: "hip-hop-rnb",
|
|
106
131
|
subgenres: [
|
|
107
132
|
{ id: "neo-soul", label: "Neo-Soul", promptHint: "neo-soul" },
|
|
108
|
-
{ id: "contemporary-rnb", label: "Contemporary R&B", promptHint: "contemporary R&B" },
|
|
109
|
-
{ id: "alt-rnb", label: "Alt R&B", promptHint: "alternative R&B" },
|
|
110
|
-
{ id: "neo-rnb", label: "Neo R&B", promptHint: "modern R&B" },
|
|
133
|
+
{ id: "contemporary-rnb", label: "Contemporary R&B", promptHint: "contemporary R&B", term: "contemporary rhythm and blues" },
|
|
134
|
+
{ id: "alt-rnb", label: "Alt R&B", promptHint: "alternative R&B", term: "alternative rhythm and blues" },
|
|
135
|
+
{ id: "neo-rnb", label: "Neo R&B", promptHint: "modern R&B", term: "modern rhythm and blues" },
|
|
111
136
|
{ id: "quiet-storm", label: "Quiet Storm", promptHint: "quiet storm" },
|
|
112
137
|
{ id: "new-jack-swing", label: "New Jack Swing", promptHint: "new jack swing" },
|
|
113
138
|
{ id: "future-soul", label: "Future Soul", promptHint: "future soul" },
|
|
@@ -146,8 +171,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
146
171
|
promptHint: "lo-fi",
|
|
147
172
|
category: "hip-hop-rnb",
|
|
148
173
|
subgenres: [
|
|
149
|
-
{ id: "lofi-beats", label: "Beats", promptHint: "lo-fi beats" },
|
|
150
|
-
{ id: "lofi-jazz", label: "Jazz", promptHint: "lo-fi jazz" },
|
|
174
|
+
{ id: "lofi-beats", label: "Beats", promptHint: "lo-fi beats", term: "lo-fi beats" },
|
|
175
|
+
{ id: "lofi-jazz", label: "Jazz", promptHint: "lo-fi jazz", term: "lo-fi jazz" },
|
|
151
176
|
{ id: "chillhop", label: "Chillhop", promptHint: "chillhop" },
|
|
152
177
|
],
|
|
153
178
|
},
|
|
@@ -168,18 +193,18 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
168
193
|
{ id: "techno", label: "Techno", promptHint: "techno" },
|
|
169
194
|
{ id: "minimal-techno", label: "Minimal Techno", promptHint: "minimal techno" },
|
|
170
195
|
{ id: "melodic-techno", label: "Melodic Techno", promptHint: "melodic techno" },
|
|
171
|
-
{ id: "drum-and-bass", label: "Drum & Bass", promptHint: "drum and bass" },
|
|
196
|
+
{ id: "drum-and-bass", label: "Drum & Bass", promptHint: "drum and bass", term: "drum and bass" },
|
|
172
197
|
{ id: "jungle", label: "Jungle", promptHint: "jungle" },
|
|
173
198
|
{ id: "dubstep", label: "Dubstep", promptHint: "dubstep" },
|
|
174
199
|
{ id: "future-bass", label: "Future Bass", promptHint: "future bass" },
|
|
175
|
-
{ id: "trap-edm", label: "Trap (EDM)", promptHint: "trap EDM" },
|
|
200
|
+
{ id: "trap-edm", label: "Trap (EDM)", promptHint: "trap EDM", term: "trap edm" },
|
|
176
201
|
{ id: "synthwave", label: "Synthwave", promptHint: "synthwave" },
|
|
177
|
-
{ id: "outrun", label: "Outrun", promptHint: "outrun synthwave" },
|
|
202
|
+
{ id: "outrun", label: "Outrun", promptHint: "outrun synthwave", term: "outrun synthwave" },
|
|
178
203
|
{ id: "vaporwave", label: "Vaporwave", promptHint: "vaporwave" },
|
|
179
204
|
{ id: "trance", label: "Trance", promptHint: "trance" },
|
|
180
205
|
{ id: "psytrance", label: "Psytrance", promptHint: "psytrance" },
|
|
181
|
-
{ id: "ambient", label: "Ambient", promptHint: "ambient electronic" },
|
|
182
|
-
{ id: "idm", label: "IDM", promptHint: "intelligent dance music" },
|
|
206
|
+
{ id: "ambient", label: "Ambient", promptHint: "ambient electronic", term: "ambient electronic" },
|
|
207
|
+
{ id: "idm", label: "IDM", promptHint: "intelligent dance music", term: "intelligent dance music" },
|
|
183
208
|
{ id: "breakbeat", label: "Breakbeat", promptHint: "breakbeat" },
|
|
184
209
|
{ id: "garage-uk", label: "UK Garage", promptHint: "UK garage" },
|
|
185
210
|
{ id: "hardstyle", label: "Hardstyle", promptHint: "hardstyle" },
|
|
@@ -194,9 +219,9 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
194
219
|
promptHint: "industrial",
|
|
195
220
|
category: "electronic",
|
|
196
221
|
subgenres: [
|
|
197
|
-
{ id: "ebm", label: "EBM", promptHint: "electronic body music" },
|
|
222
|
+
{ id: "ebm", label: "EBM", promptHint: "electronic body music", term: "electronic body music" },
|
|
198
223
|
{ id: "industrial-metal", label: "Industrial Metal", promptHint: "industrial metal" },
|
|
199
|
-
{ id: "noise", label: "Noise", promptHint: "noise industrial" },
|
|
224
|
+
{ id: "noise", label: "Noise", promptHint: "noise industrial", term: "industrial noise" },
|
|
200
225
|
],
|
|
201
226
|
},
|
|
202
227
|
{
|
|
@@ -208,8 +233,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
208
233
|
subgenres: [
|
|
209
234
|
{ id: "dark-ambient", label: "Dark Ambient", promptHint: "dark ambient" },
|
|
210
235
|
{ id: "drone", label: "Drone", promptHint: "drone" },
|
|
211
|
-
{ id: "new-age", label: "New Age", promptHint: "new age ambient" },
|
|
212
|
-
{ id: "space-music", label: "Space", promptHint: "space ambient" },
|
|
236
|
+
{ id: "new-age", label: "New Age", promptHint: "new age ambient", term: "new age ambient" },
|
|
237
|
+
{ id: "space-music", label: "Space", promptHint: "space ambient", term: "space ambient" },
|
|
213
238
|
],
|
|
214
239
|
},
|
|
215
240
|
{
|
|
@@ -220,8 +245,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
220
245
|
category: "electronic",
|
|
221
246
|
subgenres: [
|
|
222
247
|
{ id: "musique-concrete", label: "Musique Concrète", promptHint: "musique concrète" },
|
|
223
|
-
{ id: "noise-music", label: "Noise", promptHint: "noise music" },
|
|
224
|
-
{ id: "free-improv", label: "Free Improv", promptHint: "free improvisation" },
|
|
248
|
+
{ id: "noise-music", label: "Noise", promptHint: "noise music", term: "noise music" },
|
|
249
|
+
{ id: "free-improv", label: "Free Improv", promptHint: "free improvisation", term: "free improvisation" },
|
|
225
250
|
{ id: "sound-art", label: "Sound Art", promptHint: "sound art" },
|
|
226
251
|
],
|
|
227
252
|
},
|
|
@@ -265,8 +290,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
265
290
|
{ id: "k-pop", label: "K-Pop", promptHint: "k-pop" },
|
|
266
291
|
{ id: "j-pop", label: "J-Pop", promptHint: "j-pop" },
|
|
267
292
|
{ id: "art-pop", label: "Art Pop", promptHint: "art pop" },
|
|
268
|
-
{ id: "bubblegum-pop", label: "Bubblegum", promptHint: "bubblegum pop" },
|
|
269
|
-
{ id: "chamber-pop", label: "Chamber", promptHint: "chamber pop" },
|
|
293
|
+
{ id: "bubblegum-pop", label: "Bubblegum", promptHint: "bubblegum pop", term: "bubblegum pop" },
|
|
294
|
+
{ id: "chamber-pop", label: "Chamber", promptHint: "chamber pop", term: "chamber pop" },
|
|
270
295
|
{ id: "electro-pop", label: "Electro Pop", promptHint: "electro pop" },
|
|
271
296
|
{ id: "hyperpop", label: "Hyperpop", promptHint: "hyperpop" },
|
|
272
297
|
{ id: "power-pop", label: "Power Pop", promptHint: "power pop" },
|
|
@@ -278,9 +303,10 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
278
303
|
label: "Anime / J-Rock",
|
|
279
304
|
description: "Japanese animation soundtrack and J-Rock",
|
|
280
305
|
promptHint: "anime",
|
|
306
|
+
term: "anime music",
|
|
281
307
|
category: "pop",
|
|
282
308
|
subgenres: [
|
|
283
|
-
{ id: "anime-opening", label: "Opening", promptHint: "anime opening" },
|
|
309
|
+
{ id: "anime-opening", label: "Opening", promptHint: "anime opening", term: "anime opening" },
|
|
284
310
|
{ id: "j-rock", label: "J-Rock", promptHint: "J-rock" },
|
|
285
311
|
{ id: "city-pop", label: "City Pop", promptHint: "city pop" },
|
|
286
312
|
],
|
|
@@ -298,15 +324,15 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
298
324
|
{ id: "hard-rock", label: "Hard Rock", promptHint: "hard rock" },
|
|
299
325
|
{ id: "indie-rock", label: "Indie Rock", promptHint: "indie rock" },
|
|
300
326
|
{ id: "punk-rock", label: "Punk Rock", promptHint: "punk rock" },
|
|
301
|
-
{ id: "alt-rock", label: "Alternative", promptHint: "alternative rock" },
|
|
302
|
-
{ id: "psychedelic-rock", label: "Psychedelic", promptHint: "psychedelic rock" },
|
|
303
|
-
{ id: "garage-rock", label: "Garage", promptHint: "garage rock" },
|
|
304
|
-
{ id: "surf-rock", label: "Surf", promptHint: "surf rock" },
|
|
305
|
-
{ id: "prog-rock", label: "Progressive", promptHint: "progressive rock" },
|
|
327
|
+
{ id: "alt-rock", label: "Alternative", promptHint: "alternative rock", term: "alternative rock" },
|
|
328
|
+
{ id: "psychedelic-rock", label: "Psychedelic", promptHint: "psychedelic rock", term: "psychedelic rock" },
|
|
329
|
+
{ id: "garage-rock", label: "Garage", promptHint: "garage rock", term: "garage rock" },
|
|
330
|
+
{ id: "surf-rock", label: "Surf", promptHint: "surf rock", term: "surf rock" },
|
|
331
|
+
{ id: "prog-rock", label: "Progressive", promptHint: "progressive rock", term: "progressive rock" },
|
|
306
332
|
{ id: "post-rock", label: "Post-Rock", promptHint: "post-rock" },
|
|
307
333
|
{ id: "shoegaze", label: "Shoegaze", promptHint: "shoegaze" },
|
|
308
334
|
{ id: "math-rock", label: "Math Rock", promptHint: "math rock" },
|
|
309
|
-
{ id: "stoner-rock", label: "Stoner", promptHint: "stoner rock" },
|
|
335
|
+
{ id: "stoner-rock", label: "Stoner", promptHint: "stoner rock", term: "stoner rock" },
|
|
310
336
|
{ id: "emo", label: "Emo", promptHint: "emo" },
|
|
311
337
|
],
|
|
312
338
|
},
|
|
@@ -317,11 +343,11 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
317
343
|
promptHint: "punk",
|
|
318
344
|
category: "rock-metal",
|
|
319
345
|
subgenres: [
|
|
320
|
-
{ id: "hardcore-punk", label: "Hardcore", promptHint: "hardcore punk" },
|
|
346
|
+
{ id: "hardcore-punk", label: "Hardcore", promptHint: "hardcore punk", term: "hardcore punk" },
|
|
321
347
|
{ id: "post-punk", label: "Post-Punk", promptHint: "post-punk" },
|
|
322
348
|
{ id: "pop-punk", label: "Pop Punk", promptHint: "pop punk" },
|
|
323
349
|
{ id: "skate-punk", label: "Skate Punk", promptHint: "skate punk" },
|
|
324
|
-
{ id: "anarcho-punk", label: "Anarcho", promptHint: "anarcho-punk" },
|
|
350
|
+
{ id: "anarcho-punk", label: "Anarcho", promptHint: "anarcho-punk", term: "anarcho-punk" },
|
|
325
351
|
],
|
|
326
352
|
},
|
|
327
353
|
{
|
|
@@ -334,15 +360,15 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
334
360
|
{ id: "heavy-metal", label: "Heavy Metal", promptHint: "heavy metal" },
|
|
335
361
|
{ id: "death-metal", label: "Death Metal", promptHint: "death metal" },
|
|
336
362
|
{ id: "black-metal", label: "Black Metal", promptHint: "black metal" },
|
|
337
|
-
{ id: "thrash-metal", label: "Thrash", promptHint: "thrash metal" },
|
|
338
|
-
{ id: "doom-metal", label: "Doom", promptHint: "doom metal" },
|
|
339
|
-
{ id: "power-metal", label: "Power", promptHint: "power metal" },
|
|
340
|
-
{ id: "prog-metal", label: "Progressive", promptHint: "progressive metal" },
|
|
363
|
+
{ id: "thrash-metal", label: "Thrash", promptHint: "thrash metal", term: "thrash metal" },
|
|
364
|
+
{ id: "doom-metal", label: "Doom", promptHint: "doom metal", term: "doom metal" },
|
|
365
|
+
{ id: "power-metal", label: "Power", promptHint: "power metal", term: "power metal" },
|
|
366
|
+
{ id: "prog-metal", label: "Progressive", promptHint: "progressive metal", term: "progressive metal" },
|
|
341
367
|
{ id: "metalcore", label: "Metalcore", promptHint: "metalcore" },
|
|
342
368
|
{ id: "deathcore", label: "Deathcore", promptHint: "deathcore" },
|
|
343
369
|
{ id: "nu-metal", label: "Nu-Metal", promptHint: "nu-metal" },
|
|
344
|
-
{ id: "sludge-metal", label: "Sludge", promptHint: "sludge metal" },
|
|
345
|
-
{ id: "symphonic-metal", label: "Symphonic", promptHint: "symphonic metal" },
|
|
370
|
+
{ id: "sludge-metal", label: "Sludge", promptHint: "sludge metal", term: "sludge metal" },
|
|
371
|
+
{ id: "symphonic-metal", label: "Symphonic", promptHint: "symphonic metal", term: "symphonic metal" },
|
|
346
372
|
],
|
|
347
373
|
},
|
|
348
374
|
|
|
@@ -356,10 +382,10 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
356
382
|
subgenres: [
|
|
357
383
|
{ id: "bebop", label: "Bebop", promptHint: "bebop" },
|
|
358
384
|
{ id: "smooth-jazz", label: "Smooth Jazz", promptHint: "smooth jazz" },
|
|
359
|
-
{ id: "fusion", label: "Fusion", promptHint: "jazz fusion" },
|
|
385
|
+
{ id: "fusion", label: "Fusion", promptHint: "jazz fusion", term: "jazz fusion" },
|
|
360
386
|
{ id: "cool-jazz", label: "Cool Jazz", promptHint: "cool jazz" },
|
|
361
387
|
{ id: "free-jazz", label: "Free Jazz", promptHint: "free jazz" },
|
|
362
|
-
{ id: "swing", label: "Swing", promptHint: "swing jazz" },
|
|
388
|
+
{ id: "swing", label: "Swing", promptHint: "swing jazz", term: "swing jazz" },
|
|
363
389
|
{ id: "big-band", label: "Big Band", promptHint: "big band jazz" },
|
|
364
390
|
{ id: "dixieland", label: "Dixieland", promptHint: "dixieland jazz" },
|
|
365
391
|
{ id: "latin-jazz", label: "Latin Jazz", promptHint: "latin jazz" },
|
|
@@ -405,8 +431,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
405
431
|
category: "acoustic",
|
|
406
432
|
subgenres: [
|
|
407
433
|
{ id: "indie-folk", label: "Indie Folk", promptHint: "indie folk" },
|
|
408
|
-
{ id: "traditional-folk", label: "Traditional", promptHint: "traditional folk" },
|
|
409
|
-
{ id: "celtic-folk", label: "Celtic", promptHint: "Celtic folk" },
|
|
434
|
+
{ id: "traditional-folk", label: "Traditional", promptHint: "traditional folk", term: "traditional folk" },
|
|
435
|
+
{ id: "celtic-folk", label: "Celtic", promptHint: "Celtic folk", term: "celtic folk" },
|
|
410
436
|
{ id: "freak-folk", label: "Freak Folk", promptHint: "freak folk" },
|
|
411
437
|
{ id: "folk-rock", label: "Folk Rock", promptHint: "folk rock" },
|
|
412
438
|
{ id: "anti-folk", label: "Anti-Folk", promptHint: "anti-folk" },
|
|
@@ -419,9 +445,9 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
419
445
|
promptHint: "gospel",
|
|
420
446
|
category: "acoustic",
|
|
421
447
|
subgenres: [
|
|
422
|
-
{ id: "traditional-gospel", label: "Traditional", promptHint: "traditional gospel" },
|
|
423
|
-
{ id: "contemporary-gospel",label: "Contemporary", promptHint: "contemporary gospel" },
|
|
424
|
-
{ id: "urban-gospel", label: "Urban", promptHint: "urban gospel" },
|
|
448
|
+
{ id: "traditional-gospel", label: "Traditional", promptHint: "traditional gospel", term: "traditional gospel" },
|
|
449
|
+
{ id: "contemporary-gospel",label: "Contemporary", promptHint: "contemporary gospel", term: "contemporary gospel" },
|
|
450
|
+
{ id: "urban-gospel", label: "Urban", promptHint: "urban gospel", term: "urban gospel" },
|
|
425
451
|
],
|
|
426
452
|
},
|
|
427
453
|
|
|
@@ -433,8 +459,8 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
433
459
|
promptHint: "reggae",
|
|
434
460
|
category: "global",
|
|
435
461
|
subgenres: [
|
|
436
|
-
{ id: "roots-reggae", label: "Roots", promptHint: "roots reggae" },
|
|
437
|
-
{ id: "dub", label: "Dub", promptHint: "dub reggae" },
|
|
462
|
+
{ id: "roots-reggae", label: "Roots", promptHint: "roots reggae", term: "roots reggae" },
|
|
463
|
+
{ id: "dub", label: "Dub", promptHint: "dub reggae", term: "dub reggae" },
|
|
438
464
|
{ id: "dancehall", label: "Dancehall", promptHint: "dancehall" },
|
|
439
465
|
{ id: "ska", label: "Ska", promptHint: "ska" },
|
|
440
466
|
{ id: "rocksteady", label: "Rocksteady", promptHint: "rocksteady" },
|
|
@@ -466,6 +492,7 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
466
492
|
label: "World",
|
|
467
493
|
description: "Non-Western musical traditions",
|
|
468
494
|
promptHint: "world music",
|
|
495
|
+
term: "world music",
|
|
469
496
|
category: "global",
|
|
470
497
|
subgenres: [
|
|
471
498
|
{ id: "afrobeat", label: "Afrobeat", promptHint: "afrobeat" },
|
|
@@ -493,11 +520,11 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
493
520
|
category: "cinematic",
|
|
494
521
|
subgenres: [
|
|
495
522
|
{ id: "baroque", label: "Baroque", promptHint: "baroque classical" },
|
|
496
|
-
{ id: "romantic", label: "Romantic", promptHint: "romantic-era classical" },
|
|
523
|
+
{ id: "romantic", label: "Romantic", promptHint: "romantic-era classical", term: "romantic-era classical" },
|
|
497
524
|
{ id: "modern-classical", label: "Modern Classical", promptHint: "modern classical" },
|
|
498
|
-
{ id: "minimalist-classical",label: "Minimalist", promptHint: "minimalist classical" },
|
|
525
|
+
{ id: "minimalist-classical",label: "Minimalist", promptHint: "minimalist classical", term: "minimalist classical" },
|
|
499
526
|
{ id: "choral", label: "Choral", promptHint: "choral classical" },
|
|
500
|
-
{ id: "chamber-music", label: "Chamber", promptHint: "chamber music" },
|
|
527
|
+
{ id: "chamber-music", label: "Chamber", promptHint: "chamber music", term: "chamber music" },
|
|
501
528
|
{ id: "opera", label: "Opera", promptHint: "operatic" },
|
|
502
529
|
{ id: "early-music", label: "Early Music", promptHint: "early music" },
|
|
503
530
|
],
|
|
@@ -511,14 +538,14 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
511
538
|
subgenres: [
|
|
512
539
|
{ id: "epic-orchestral", label: "Epic Orchestral", promptHint: "epic orchestral cinematic" },
|
|
513
540
|
{ id: "minimalist-score", label: "Minimalist Score", promptHint: "minimalist cinematic score" },
|
|
514
|
-
{ id: "trailer-music", label: "Trailer", promptHint: "trailer cinematic music" },
|
|
515
|
-
{ id: "score-action", label: "Action", promptHint: "action film score" },
|
|
516
|
-
{ id: "score-horror", label: "Horror", promptHint: "horror film score" },
|
|
517
|
-
{ id: "score-romance", label: "Romance", promptHint: "romantic film score" },
|
|
518
|
-
{ id: "score-sci-fi", label: "Sci-Fi", promptHint: "sci-fi film score" },
|
|
519
|
-
{ id: "score-fantasy", label: "Fantasy", promptHint: "fantasy film score" },
|
|
520
|
-
{ id: "score-noir", label: "Noir", promptHint: "noir cinematic" },
|
|
521
|
-
{ id: "score-documentary", label: "Documentary", promptHint: "documentary score" },
|
|
541
|
+
{ id: "trailer-music", label: "Trailer", promptHint: "trailer cinematic music", term: "trailer music" },
|
|
542
|
+
{ id: "score-action", label: "Action", promptHint: "action film score", term: "action film score" },
|
|
543
|
+
{ id: "score-horror", label: "Horror", promptHint: "horror film score", term: "horror film score" },
|
|
544
|
+
{ id: "score-romance", label: "Romance", promptHint: "romantic film score", term: "romantic film score" },
|
|
545
|
+
{ id: "score-sci-fi", label: "Sci-Fi", promptHint: "sci-fi film score", term: "sci-fi film score" },
|
|
546
|
+
{ id: "score-fantasy", label: "Fantasy", promptHint: "fantasy film score", term: "fantasy film score" },
|
|
547
|
+
{ id: "score-noir", label: "Noir", promptHint: "noir cinematic", term: "noir film score" },
|
|
548
|
+
{ id: "score-documentary", label: "Documentary", promptHint: "documentary score", term: "documentary score" },
|
|
522
549
|
],
|
|
523
550
|
},
|
|
524
551
|
{
|
|
@@ -526,13 +553,14 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
526
553
|
label: "Video Game",
|
|
527
554
|
description: "Game-score-style — chiptune, orchestral, electronic",
|
|
528
555
|
promptHint: "video game music",
|
|
556
|
+
term: "video game music",
|
|
529
557
|
category: "cinematic",
|
|
530
558
|
subgenres: [
|
|
531
559
|
{ id: "chiptune", label: "Chiptune", promptHint: "chiptune" },
|
|
532
560
|
{ id: "8bit", label: "8-bit", promptHint: "8-bit" },
|
|
533
561
|
{ id: "16bit", label: "16-bit", promptHint: "16-bit" },
|
|
534
|
-
{ id: "vgm-orchestral", label: "Orchestral VGM", promptHint: "orchestral video game" },
|
|
535
|
-
{ id: "vgm-synth", label: "Synth VGM", promptHint: "synth video game" },
|
|
562
|
+
{ id: "vgm-orchestral", label: "Orchestral VGM", promptHint: "orchestral video game", term: "orchestral video game score" },
|
|
563
|
+
{ id: "vgm-synth", label: "Synth VGM", promptHint: "synth video game", term: "synth video game score" },
|
|
536
564
|
],
|
|
537
565
|
},
|
|
538
566
|
{
|
|
@@ -543,7 +571,7 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
543
571
|
category: "cinematic",
|
|
544
572
|
subgenres: [
|
|
545
573
|
{ id: "christmas", label: "Christmas", promptHint: "Christmas" },
|
|
546
|
-
{ id: "winter", label: "Winter", promptHint: "winter holiday" },
|
|
574
|
+
{ id: "winter", label: "Winter", promptHint: "winter holiday", term: "winter holiday" },
|
|
547
575
|
{ id: "festive", label: "Festive", promptHint: "festive" },
|
|
548
576
|
],
|
|
549
577
|
},
|
|
@@ -552,6 +580,7 @@ export const MUSIC_GENRES: ReadonlyArray<MusicGenre> = [
|
|
|
552
580
|
label: "Children's",
|
|
553
581
|
description: "Kids music — sing-alongs, lullabies, educational",
|
|
554
582
|
promptHint: "children's music",
|
|
583
|
+
term: "children's music",
|
|
555
584
|
category: "cinematic",
|
|
556
585
|
subgenres: [
|
|
557
586
|
{ id: "lullaby", label: "Lullaby", promptHint: "lullaby" },
|
|
@@ -573,7 +602,7 @@ export const MUSIC_ERAS: ReadonlyArray<MusicEra> = [
|
|
|
573
602
|
{ id: "2000s", label: "2000s", description: "Pop-punk, R&B revival", promptHint: "early 2000s" },
|
|
574
603
|
{ id: "2010s", label: "2010s", description: "EDM mainstream, streaming era", promptHint: "2010s" },
|
|
575
604
|
{ id: "modern", label: "Modern", description: "2020s+ contemporary", promptHint: "modern" },
|
|
576
|
-
{ id: "futurist", label: "Futurist", description: "Sci-fi, otherworldly", promptHint: "futuristic" },
|
|
605
|
+
{ id: "futurist", label: "Futurist", description: "Sci-fi, otherworldly", promptHint: "futuristic", term: "futuristic" },
|
|
577
606
|
] as const
|
|
578
607
|
|
|
579
608
|
const GENRE_BY_ID = new Map(MUSIC_GENRES.map((g) => [g.id, g]))
|
|
@@ -603,6 +632,33 @@ export function getMusicEra(id: string | undefined): MusicEra | undefined {
|
|
|
603
632
|
return ERA_BY_ID.get(id)
|
|
604
633
|
}
|
|
605
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Compact counterpart of a genre's `promptHint`: the short genre name a
|
|
637
|
+
* producer would write ("rhythm and blues", "world music"). Same lookup and
|
|
638
|
+
* same empty-string-on-miss behavior as `getMusicGenre`, so hint mode and
|
|
639
|
+
* compact mode can never disagree about which genre they describe.
|
|
640
|
+
*/
|
|
641
|
+
export function getMusicGenreTerm(id: string | undefined | null): string {
|
|
642
|
+
return resolveTerm(getMusicGenre(id ?? undefined))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Compact counterpart for a subgenre. Keeps `getMusicSubgenre`'s two-argument
|
|
647
|
+
* lookup — a subgenre id is unique only within its parent genre, so a
|
|
648
|
+
* single-id term getter could not resolve one.
|
|
649
|
+
*/
|
|
650
|
+
export function getMusicSubgenreTerm(
|
|
651
|
+
genreId: string | undefined | null,
|
|
652
|
+
subgenreId: string | undefined | null,
|
|
653
|
+
): string {
|
|
654
|
+
return resolveTerm(getMusicSubgenre(genreId ?? undefined, subgenreId ?? undefined))
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Compact counterpart of an era's `promptHint` — the short period name. */
|
|
658
|
+
export function getMusicEraTerm(id: string | undefined | null): string {
|
|
659
|
+
return resolveTerm(getMusicEra(id ?? undefined))
|
|
660
|
+
}
|
|
661
|
+
|
|
606
662
|
/**
|
|
607
663
|
* Compose hints from MusicGenreData: optional preText, structured
|
|
608
664
|
* [era] [subgenre|genre] (or " / "-joined genres for multi), optional
|
|
@@ -613,13 +669,17 @@ export function getMusicEra(id: string | undefined): MusicEra | undefined {
|
|
|
613
669
|
* — subgenre is ignored in multi-mode (subgenre is meaningful only against
|
|
614
670
|
* a single chosen genre).
|
|
615
671
|
*/
|
|
616
|
-
export function buildMusicGenreHints(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
672
|
+
export function buildMusicGenreHints(
|
|
673
|
+
data: {
|
|
674
|
+
readonly preText?: string
|
|
675
|
+
readonly postText?: string
|
|
676
|
+
readonly genre?: string | ReadonlyArray<string>
|
|
677
|
+
readonly subgenre?: string
|
|
678
|
+
readonly era?: string
|
|
679
|
+
},
|
|
680
|
+
mode: PickerHintMode = "full",
|
|
681
|
+
): string {
|
|
682
|
+
if (mode === "compact") return buildMusicGenreTerms(data)
|
|
623
683
|
const fragments: string[] = []
|
|
624
684
|
|
|
625
685
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
@@ -652,6 +712,50 @@ export function buildMusicGenreHints(data: {
|
|
|
652
712
|
return fragments.join(", ")
|
|
653
713
|
}
|
|
654
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Compact-mode mirror of `buildMusicGenreHints`: the same preText / [era]
|
|
717
|
+
* [subgenre|genre] / postText structure, built from each entry's short `term`
|
|
718
|
+
* instead of its `promptHint` ("1980s synth pop"). Multi-genre still joins
|
|
719
|
+
* with " / " and still ignores the subgenre, and preText / postText — user
|
|
720
|
+
* free text, not catalog copy — pass through untouched.
|
|
721
|
+
*/
|
|
722
|
+
export function buildMusicGenreTerms(data: {
|
|
723
|
+
readonly preText?: string
|
|
724
|
+
readonly postText?: string
|
|
725
|
+
readonly genre?: string | ReadonlyArray<string>
|
|
726
|
+
readonly subgenre?: string
|
|
727
|
+
readonly era?: string
|
|
728
|
+
}): string {
|
|
729
|
+
const fragments: string[] = []
|
|
730
|
+
|
|
731
|
+
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
732
|
+
if (pre) fragments.push(pre)
|
|
733
|
+
|
|
734
|
+
const parts: string[] = []
|
|
735
|
+
const era = getMusicEraTerm(data.era)
|
|
736
|
+
if (era) parts.push(era)
|
|
737
|
+
|
|
738
|
+
const genreIds = pickIds(data.genre)
|
|
739
|
+
if (genreIds.length > 1) {
|
|
740
|
+
const genreTerms = genreIds.map((id) => getMusicGenreTerm(id)).filter((t) => t.length > 0)
|
|
741
|
+
if (genreTerms.length > 0) parts.push(genreTerms.join(" / "))
|
|
742
|
+
} else if (genreIds.length === 1) {
|
|
743
|
+
const sub = getMusicSubgenreTerm(genreIds[0], data.subgenre)
|
|
744
|
+
if (sub) {
|
|
745
|
+
parts.push(sub)
|
|
746
|
+
} else {
|
|
747
|
+
const genre = getMusicGenreTerm(genreIds[0])
|
|
748
|
+
if (genre) parts.push(genre)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (parts.length > 0) fragments.push(parts.join(" "))
|
|
752
|
+
|
|
753
|
+
const post = typeof data.postText === "string" ? data.postText.trim() : ""
|
|
754
|
+
if (post) fragments.push(post)
|
|
755
|
+
|
|
756
|
+
return fragments.join(", ")
|
|
757
|
+
}
|
|
758
|
+
|
|
655
759
|
/** Default data when a music-genre node is dropped on canvas. Empty by design — forces a deliberate pick. */
|
|
656
760
|
export const MUSIC_GENRE_DEFAULT_DATA: {
|
|
657
761
|
preText?: string
|
package/src/music-mood.ts
CHANGED
|
@@ -5,22 +5,34 @@
|
|
|
5
5
|
|
|
6
6
|
import { pickIds } from "@nodaro/shared"
|
|
7
7
|
|
|
8
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
9
|
+
|
|
8
10
|
export interface MusicMoodEntry {
|
|
9
11
|
readonly id: string
|
|
10
12
|
readonly label: string
|
|
11
13
|
readonly description: string
|
|
12
14
|
readonly promptHint: string
|
|
15
|
+
/**
|
|
16
|
+
* Compact professional term injected in compact hint mode (see `term.ts`).
|
|
17
|
+
*
|
|
18
|
+
* Authored only where the lowercased label is NOT the phrase a music
|
|
19
|
+
* supervisor would write — the bare-degree energies ("Low"/"Moderate"/
|
|
20
|
+
* "High" mean nothing without "energy" attached) and the noun-shaped
|
|
21
|
+
* "Awe". Everywhere else the label already IS the trade descriptor and
|
|
22
|
+
* `resolveTerm` derives it.
|
|
23
|
+
*/
|
|
24
|
+
readonly term?: string
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
export const MUSIC_ENERGIES: ReadonlyArray<MusicMoodEntry> = [
|
|
16
|
-
{ id: "low", label: "Low", description: "Calm, slow", promptHint: "low-energy" },
|
|
28
|
+
{ id: "low", label: "Low", description: "Calm, slow", promptHint: "low-energy", term: "low-energy" },
|
|
17
29
|
{ id: "mellow", label: "Mellow", description: "Relaxed, laid-back", promptHint: "mellow" },
|
|
18
30
|
{ id: "gentle", label: "Gentle", description: "Soft, tender", promptHint: "gentle" },
|
|
19
|
-
{ id: "moderate", label: "Moderate", description: "Balanced, steady", promptHint: "moderate-energy" },
|
|
20
|
-
{ id: "building", label: "Building", description: "Slowly intensifying", promptHint: "building energy" },
|
|
31
|
+
{ id: "moderate", label: "Moderate", description: "Balanced, steady", promptHint: "moderate-energy", term: "moderate-energy" },
|
|
32
|
+
{ id: "building", label: "Building", description: "Slowly intensifying", promptHint: "building energy", term: "building energy" },
|
|
21
33
|
{ id: "upbeat", label: "Upbeat", description: "Lively, optimistic", promptHint: "upbeat" },
|
|
22
34
|
{ id: "driving", label: "Driving", description: "Forward-pushing momentum", promptHint: "driving" },
|
|
23
|
-
{ id: "high", label: "High", description: "Energetic, intense", promptHint: "high-energy" },
|
|
35
|
+
{ id: "high", label: "High", description: "Energetic, intense", promptHint: "high-energy", term: "high-energy" },
|
|
24
36
|
{ id: "explosive", label: "Explosive", description: "Bursting, wild", promptHint: "explosive" },
|
|
25
37
|
{ id: "frenetic", label: "Frenetic", description: "Intense, rapid", promptHint: "frenetic" },
|
|
26
38
|
{ id: "pulsing", label: "Pulsing", description: "Rhythmic, beat-driven", promptHint: "pulsing" },
|
|
@@ -57,7 +69,7 @@ export const MUSIC_EMOTIONS: ReadonlyArray<MusicMoodEntry> = [
|
|
|
57
69
|
{ id: "peaceful", label: "Peaceful", description: "Serene, restful", promptHint: "peaceful" },
|
|
58
70
|
{ id: "contemplative",label: "Contemplative",description: "Reflective, thoughtful", promptHint: "contemplative" },
|
|
59
71
|
{ id: "ethereal", label: "Ethereal", description: "Otherworldly, floating", promptHint: "ethereal" },
|
|
60
|
-
{ id: "awe", label: "Awe", description: "Wonder, vastness", promptHint: "awe-inspiring" },
|
|
72
|
+
{ id: "awe", label: "Awe", description: "Wonder, vastness", promptHint: "awe-inspiring", term: "awe-inspiring" },
|
|
61
73
|
] as const
|
|
62
74
|
|
|
63
75
|
export const MUSIC_VIBES: ReadonlyArray<MusicMoodEntry> = [
|
|
@@ -99,30 +111,72 @@ export function getMusicVibe(id: string | undefined): MusicMoodEntry | undefined
|
|
|
99
111
|
return id ? VIBE_BY_ID.get(id) : undefined
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The COMPACT counterparts of the `promptHint` lookups: the short professional
|
|
116
|
+
* term a consumer injects instead of the entry's full fragment ("low-energy",
|
|
117
|
+
* "building energy", "awe-inspiring"). Same lookup as the getters above, same
|
|
118
|
+
* empty-string-on-miss behavior, so hint mode and term mode can never disagree
|
|
119
|
+
* about WHICH entry they are describing.
|
|
120
|
+
*/
|
|
121
|
+
export function getMusicEnergyTerm(id: string | undefined | null): string {
|
|
122
|
+
return resolveTerm(id ? getMusicEnergy(id) : undefined)
|
|
123
|
+
}
|
|
124
|
+
export function getMusicEmotionTerm(id: string | undefined | null): string {
|
|
125
|
+
return resolveTerm(id ? getMusicEmotion(id) : undefined)
|
|
126
|
+
}
|
|
127
|
+
export function getMusicVibeTerm(id: string | undefined | null): string {
|
|
128
|
+
return resolveTerm(id ? getMusicVibe(id) : undefined)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MusicMoodData {
|
|
103
132
|
readonly preText?: string
|
|
104
133
|
readonly postText?: string
|
|
105
134
|
readonly energy?: string
|
|
106
135
|
readonly emotion?: string | ReadonlyArray<string>
|
|
107
136
|
readonly vibe?: string | ReadonlyArray<string>
|
|
108
|
-
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Resolve one multi-pick field's ids into non-empty fragments, in pick order. */
|
|
140
|
+
function musicMoodFragments(
|
|
141
|
+
value: string | ReadonlyArray<string> | undefined,
|
|
142
|
+
lookup: (id: string) => MusicMoodEntry | undefined,
|
|
143
|
+
fragmentFor: (entry: MusicMoodEntry) => string,
|
|
144
|
+
): string[] {
|
|
145
|
+
return pickIds(value)
|
|
146
|
+
.map((id) => {
|
|
147
|
+
const entry = lookup(id)
|
|
148
|
+
return entry ? fragmentFor(entry) : ""
|
|
149
|
+
})
|
|
150
|
+
.filter((f) => f.length > 0)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The one walk over the energy / emotion / vibe fields, parameterized by how a
|
|
155
|
+
* single selected entry turns into a fragment. `buildMusicMoodHints` (verbose)
|
|
156
|
+
* and `buildMusicMoodTerms` (compact) both delegate here, so the two can never
|
|
157
|
+
* disagree about WHICH ids contribute or in what order — only about how each
|
|
158
|
+
* one is phrased. Free-text pre/post is user wording and passes through both.
|
|
159
|
+
*/
|
|
160
|
+
function composeMusicMood(
|
|
161
|
+
data: MusicMoodData,
|
|
162
|
+
fragmentFor: (entry: MusicMoodEntry) => string,
|
|
163
|
+
): string {
|
|
109
164
|
const fragments: string[] = []
|
|
110
165
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
111
166
|
if (pre) fragments.push(pre)
|
|
112
167
|
|
|
113
168
|
const parts: string[] = []
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
169
|
+
const energy = getMusicEnergy(data.energy)
|
|
170
|
+
if (energy) {
|
|
171
|
+
const energyFragment = fragmentFor(energy)
|
|
172
|
+
if (energyFragment) parts.push(energyFragment)
|
|
173
|
+
}
|
|
116
174
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
.filter((h): h is string => !!h)
|
|
120
|
-
if (emotionHints.length > 0) parts.push(emotionHints.join(", "))
|
|
175
|
+
const emotionFragments = musicMoodFragments(data.emotion, getMusicEmotion, fragmentFor)
|
|
176
|
+
if (emotionFragments.length > 0) parts.push(emotionFragments.join(", "))
|
|
121
177
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
.filter((h): h is string => !!h)
|
|
125
|
-
if (vibeHints.length > 0) parts.push(vibeHints.join(", "))
|
|
178
|
+
const vibeFragments = musicMoodFragments(data.vibe, getMusicVibe, fragmentFor)
|
|
179
|
+
if (vibeFragments.length > 0) parts.push(vibeFragments.join(", "))
|
|
126
180
|
|
|
127
181
|
if (parts.length > 0) fragments.push(parts.join(" "))
|
|
128
182
|
|
|
@@ -132,6 +186,20 @@ export function buildMusicMoodHints(data: {
|
|
|
132
186
|
return fragments.join(", ")
|
|
133
187
|
}
|
|
134
188
|
|
|
189
|
+
export function buildMusicMoodHints(data: MusicMoodData, mode: PickerHintMode = "full"): string {
|
|
190
|
+
if (mode === "compact") return buildMusicMoodTerms(data)
|
|
191
|
+
return composeMusicMood(data, (entry) => entry.promptHint)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The COMPACT counterpart of `buildMusicMoodHints`: the same field walk in the
|
|
196
|
+
* same canonical order, emitting each selection's short professional term
|
|
197
|
+
* instead of its full prompt fragment.
|
|
198
|
+
*/
|
|
199
|
+
export function buildMusicMoodTerms(data: MusicMoodData): string {
|
|
200
|
+
return composeMusicMood(data, resolveTerm)
|
|
201
|
+
}
|
|
202
|
+
|
|
135
203
|
export const MUSIC_MOOD_DEFAULT_DATA: {
|
|
136
204
|
preText?: string; postText?: string; energy?: string; emotion?: string | ReadonlyArray<string>; vibe?: string | ReadonlyArray<string>
|
|
137
205
|
} = {}
|