@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.
Files changed (72) hide show
  1. package/dist/index.cjs +5422 -4269
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1198 -61
  4. package/dist/index.d.ts +1198 -61
  5. package/dist/index.js +5336 -4271
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
  9. package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
  10. package/src/__tests__/catalog-packs.test.ts +130 -0
  11. package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
  12. package/src/__tests__/catalog-terms.test.ts +162 -0
  13. package/src/__tests__/character-default-role.test.ts +3 -2
  14. package/src/__tests__/content-free-contract.test.ts +45 -0
  15. package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
  16. package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
  17. package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
  18. package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
  19. package/src/__tests__/parameter-hint-mode.test.ts +385 -0
  20. package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
  21. package/src/__tests__/person-packs.test.ts +166 -0
  22. package/src/__tests__/project-all-catalogs.test.ts +26 -0
  23. package/src/__tests__/prompt-builder.test.ts +53 -0
  24. package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
  25. package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
  26. package/src/__tests__/term.test.ts +104 -0
  27. package/src/__tests__/transitions.test.ts +8 -5
  28. package/src/__tests__/upstream-immutability.test.ts +23 -0
  29. package/src/action-fx.ts +67 -17
  30. package/src/aesthetic.ts +54 -6
  31. package/src/atmosphere.ts +64 -23
  32. package/src/backdrop.ts +44 -30
  33. package/src/camera-format.ts +33 -11
  34. package/src/camera-motions.ts +89 -0
  35. package/src/catalog-packs.ts +125 -0
  36. package/src/catalog-sidecar-coverage.ts +36 -0
  37. package/src/character-fx.ts +112 -39
  38. package/src/color-look.ts +44 -27
  39. package/src/composition-effects.ts +21 -7
  40. package/src/era.ts +24 -0
  41. package/src/exposure-settings.ts +76 -18
  42. package/src/framing.ts +100 -0
  43. package/src/held-prop.ts +125 -63
  44. package/src/identity-lock.ts +12 -5
  45. package/src/image-reference-doctrine.ts +55 -0
  46. package/src/index.ts +5 -0
  47. package/src/instrumentation.ts +148 -57
  48. package/src/lens.ts +31 -15
  49. package/src/lighting.ts +120 -59
  50. package/src/loop-subject.ts +27 -1
  51. package/src/materials.ts +123 -69
  52. package/src/mood.ts +123 -51
  53. package/src/music-genre.ts +171 -67
  54. package/src/music-mood.ts +85 -17
  55. package/src/parameter-prompt-hint.ts +168 -56
  56. package/src/person-packs.ts +182 -0
  57. package/src/person.ts +506 -408
  58. package/src/photo-genre.ts +37 -22
  59. package/src/photographer.ts +138 -1
  60. package/src/picker-catalogs.ts +86 -40
  61. package/src/pose.ts +105 -40
  62. package/src/post-process-effects.ts +51 -8
  63. package/src/prompt-builder.ts +16 -1
  64. package/src/render-quality.ts +25 -7
  65. package/src/setting.ts +30 -14
  66. package/src/style.ts +33 -15
  67. package/src/styling.ts +155 -94
  68. package/src/temporal.ts +80 -18
  69. package/src/term.ts +155 -0
  70. package/src/transitions.ts +73 -26
  71. package/src/voice-character.ts +190 -90
  72. package/src/voice-delivery.ts +83 -12
@@ -11,6 +11,8 @@
11
11
  * Synth / Guitar / Bass / Brass / Woodwinds / Strings / World.
12
12
  */
13
13
 
14
+ import { resolveTerm, type PickerHintMode } from "./term.js"
15
+
14
16
  export type InstrumentCategory =
15
17
  | "drums"
16
18
  | "percussion"
@@ -48,8 +50,19 @@ export interface InstrumentationEntry {
48
50
  readonly label: string
49
51
  readonly description: string
50
52
  readonly promptHint: string
53
+ /**
54
+ * Optional authored compact term (see `term.ts` for the convention).
55
+ * Authored only where the lowercased label is NOT what a producer would
56
+ * write in a prompt — bare modifiers whose trade term carries a category
57
+ * noun ("Stab" → "synth stab", "Polished" → "polished production"), bare
58
+ * genre words in the singing-style slot ("Pop" → "pop singing"), and
59
+ * annotated labels ("Trap (Melodic)" → "melodic trap singing"). Everywhere
60
+ * else the lowercased label IS the term and nothing is authored.
61
+ */
62
+ readonly term?: string
51
63
  }
52
64
 
65
+ /** Inherits the optional `term` from `InstrumentationEntry`. */
53
66
  export interface CategorizedInstrument extends InstrumentationEntry {
54
67
  readonly category: InstrumentCategory
55
68
  }
@@ -58,7 +71,7 @@ export const INSTRUMENTS: ReadonlyArray<CategorizedInstrument> = [
58
71
  // -------- Drums --------
59
72
  { id: "drums", label: "Drums", description: "Acoustic drum kit", promptHint: "drums", category: "drums" },
60
73
  { id: "drum-machine", label: "Drum Machine", description: "Programmed beats", promptHint: "drum machine", category: "drums" },
61
- { id: "808", label: "808", description: "Roland TR-808 bass + kit", promptHint: "808 drums", category: "drums" },
74
+ { id: "808", label: "808", description: "Roland TR-808 bass + kit", promptHint: "808 drums", category: "drums", term: "808 drums" },
62
75
  { id: "live-drums", label: "Live Drums", description: "Recorded acoustic kit", promptHint: "live drums", category: "drums" },
63
76
  { id: "breakbeats", label: "Breakbeats", description: "Sampled drum loops", promptHint: "breakbeats", category: "drums" },
64
77
  { id: "trap-drums", label: "Trap Drums", description: "Hi-hat rolls + 808", promptHint: "trap drums", category: "drums" },
@@ -86,11 +99,11 @@ export const INSTRUMENTS: ReadonlyArray<CategorizedInstrument> = [
86
99
  // -------- Keys --------
87
100
  { id: "piano", label: "Piano", description: "Acoustic grand or upright", promptHint: "piano", category: "keys" },
88
101
  { id: "electric-piano", label: "Electric Piano", description: "Rhodes / Wurlitzer-style", promptHint: "electric piano", category: "keys" },
89
- { id: "wurlitzer", label: "Wurlitzer", description: "Vintage Wurlitzer EP", promptHint: "Wurlitzer electric piano", category: "keys" },
90
- { id: "rhodes", label: "Rhodes", description: "Vintage Rhodes EP", promptHint: "Rhodes electric piano",category: "keys" },
102
+ { id: "wurlitzer", label: "Wurlitzer", description: "Vintage Wurlitzer EP", promptHint: "Wurlitzer electric piano", category: "keys", term: "wurlitzer electric piano" },
103
+ { id: "rhodes", label: "Rhodes", description: "Vintage Rhodes EP", promptHint: "Rhodes electric piano",category: "keys", term: "rhodes electric piano" },
91
104
  { id: "clavinet", label: "Clavinet", description: "Funky Hohner D6", promptHint: "clavinet", category: "keys" },
92
105
  { id: "organ", label: "Organ", description: "Hammond or pipe organ", promptHint: "organ", category: "keys" },
93
- { id: "hammond-organ", label: "Hammond B3", description: "Soul / rock B3 organ", promptHint: "Hammond B3 organ", category: "keys" },
106
+ { id: "hammond-organ", label: "Hammond B3", description: "Soul / rock B3 organ", promptHint: "Hammond B3 organ", category: "keys", term: "hammond b3 organ" },
94
107
  { id: "pipe-organ", label: "Pipe Organ", description: "Cathedral / church", promptHint: "pipe organ", category: "keys" },
95
108
  { id: "accordion", label: "Accordion", description: "Bellows-driven reed", promptHint: "accordion", category: "keys" },
96
109
  { id: "mellotron", label: "Mellotron", description: "Tape-loop keyboard", promptHint: "mellotron", category: "keys" },
@@ -102,19 +115,19 @@ export const INSTRUMENTS: ReadonlyArray<CategorizedInstrument> = [
102
115
  { id: "lead-synth", label: "Lead Synth", description: "Cutting melodic synth", promptHint: "lead synth", category: "synth" },
103
116
  { id: "pluck-synth", label: "Pluck Synth", description: "Short, plucked timbre", promptHint: "pluck synth", category: "synth" },
104
117
  { id: "pads", label: "Synth Pads", description: "Sustained synth textures", promptHint: "synth pads", category: "synth" },
105
- { id: "arpeggiator", label: "Arpeggiator", description: "Sequenced arp patterns", promptHint: "arpeggiated synth", category: "synth" },
118
+ { id: "arpeggiator", label: "Arpeggiator", description: "Sequenced arp patterns", promptHint: "arpeggiated synth", category: "synth", term: "arpeggiated synth" },
106
119
  { id: "vocoder", label: "Vocoder", description: "Voice-modulated synth", promptHint: "vocoder", category: "synth" },
107
120
  { id: "talkbox", label: "Talkbox", description: "Mouth-shaped synth", promptHint: "talkbox", category: "synth" },
108
- { id: "modular-synth", label: "Modular", description: "Patchable analog synth", promptHint: "modular synth", category: "synth" },
121
+ { id: "modular-synth", label: "Modular", description: "Patchable analog synth", promptHint: "modular synth", category: "synth", term: "modular synth" },
109
122
  { id: "fm-synth", label: "FM Synth", description: "Frequency-modulation synth",promptHint: "FM synth", category: "synth" },
110
- { id: "wavetable-synth", label: "Wavetable", description: "Wavetable-scanning synth", promptHint: "wavetable synth", category: "synth" },
111
- { id: "stab-synth", label: "Stab", description: "Short, percussive chord", promptHint: "synth stab", category: "synth" },
123
+ { id: "wavetable-synth", label: "Wavetable", description: "Wavetable-scanning synth", promptHint: "wavetable synth", category: "synth", term: "wavetable synth" },
124
+ { id: "stab-synth", label: "Stab", description: "Short, percussive chord", promptHint: "synth stab", category: "synth", term: "synth stab" },
112
125
 
113
126
  // -------- Guitar --------
114
127
  { id: "acoustic-guitar", label: "Acoustic Guitar", description: "Steel- or nylon-strung", promptHint: "acoustic guitar", category: "guitar" },
115
128
  { id: "electric-guitar", label: "Electric Guitar", description: "Solid-body, often distorted", promptHint: "electric guitar", category: "guitar" },
116
129
  { id: "classical-guitar",label: "Classical Guitar",description: "Nylon-string, fingerstyle", promptHint: "classical guitar", category: "guitar" },
117
- { id: "12-string-guitar",label: "12-String", description: "Doubled-course strum", promptHint: "12-string guitar", category: "guitar" },
130
+ { id: "12-string-guitar",label: "12-String", description: "Doubled-course strum", promptHint: "12-string guitar", category: "guitar", term: "12-string guitar" },
118
131
  { id: "pedal-steel", label: "Pedal Steel", description: "Country sliding steel", promptHint: "pedal steel", category: "guitar" },
119
132
  { id: "lap-steel", label: "Lap Steel", description: "Hawaiian lap-played steel", promptHint: "lap steel", category: "guitar" },
120
133
  { id: "dobro", label: "Dobro", description: "Resonator guitar", promptHint: "dobro resonator", category: "guitar" },
@@ -162,8 +175,8 @@ export const INSTRUMENTS: ReadonlyArray<CategorizedInstrument> = [
162
175
  { id: "cello", label: "Cello", description: "Solo cello", promptHint: "cello", category: "strings" },
163
176
  { id: "double-bass", label: "Double Bass", description: "Orchestral upright bass", promptHint: "double bass", category: "strings" },
164
177
  { id: "harp", label: "Harp", description: "Concert or folk harp", promptHint: "harp", category: "strings" },
165
- { id: "pizzicato-strings",label: "Pizzicato", description: "Plucked strings", promptHint: "pizzicato strings", category: "strings" },
166
- { id: "staccato-strings",label: "Staccato", description: "Short, punchy strings", promptHint: "staccato strings", category: "strings" },
178
+ { id: "pizzicato-strings",label: "Pizzicato", description: "Plucked strings", promptHint: "pizzicato strings", category: "strings", term: "pizzicato strings" },
179
+ { id: "staccato-strings",label: "Staccato", description: "Short, punchy strings", promptHint: "staccato strings", category: "strings", term: "staccato strings" },
167
180
  { id: "fiddle", label: "Fiddle", description: "Folk-style violin", promptHint: "fiddle", category: "strings" },
168
181
 
169
182
  // -------- World --------
@@ -186,40 +199,40 @@ export const INSTRUMENTS: ReadonlyArray<CategorizedInstrument> = [
186
199
 
187
200
  // -------- Middle Eastern --------
188
201
  { id: "kamancheh", label: "Kamancheh", description: "Persian bowed spike fiddle", promptHint: "kamancheh", category: "middle-eastern" },
189
- { id: "ney", label: "Ney", description: "Persian/Arabic end-blown reed flute", promptHint: "ney flute", category: "middle-eastern" },
202
+ { id: "ney", label: "Ney", description: "Persian/Arabic end-blown reed flute", promptHint: "ney flute", category: "middle-eastern", term: "ney flute" },
190
203
  { id: "santur", label: "Santur", description: "Persian hammered dulcimer", promptHint: "santur", category: "middle-eastern" },
191
204
  ] as const
192
205
 
193
206
  export const PRODUCTION_STYLES: ReadonlyArray<InstrumentationEntry> = [
194
- { id: "polished", label: "Polished", description: "Pristine, mainstream production", promptHint: "polished production" },
195
- { id: "lo-fi", label: "Lo-fi", description: "Warm tape hiss, imperfect", promptHint: "lo-fi production" },
196
- { id: "raw", label: "Raw", description: "Unfiltered, live feel", promptHint: "raw production" },
197
- { id: "vintage", label: "Vintage", description: "Analog warmth", promptHint: "vintage production" },
198
- { id: "modern", label: "Modern", description: "Contemporary digital", promptHint: "modern production" },
199
- { id: "minimalist", label: "Minimalist", description: "Sparse, restrained", promptHint: "minimalist production" },
200
- { id: "wall-of-sound", label: "Wall of Sound", description: "Dense, layered", promptHint: "wall-of-sound production" },
201
- { id: "ambient", label: "Ambient", description: "Atmospheric, reverbed", promptHint: "ambient production" },
202
- { id: "garage-band", label: "Garage Band", description: "DIY, basement-tracked", promptHint: "garage band production" },
203
- { id: "live-recording", label: "Live", description: "Live concert / room sound", promptHint: "live recording" },
204
- { id: "demo-quality", label: "Demo", description: "Rough demo aesthetic", promptHint: "demo quality" },
205
- { id: "studio-pristine", label: "Studio Pristine", description: "Hi-fi reference quality", promptHint: "pristine studio production" },
206
- { id: "tape-saturated", label: "Tape Saturated", description: "Analog tape warmth + drive", promptHint: "tape-saturated" },
207
- { id: "8-bit-retro", label: "8-bit Retro", description: "Lo-bitrate retro game audio", promptHint: "8-bit retro production" },
207
+ { id: "polished", label: "Polished", description: "Pristine, mainstream production", promptHint: "polished production", term: "polished production" },
208
+ { id: "lo-fi", label: "Lo-fi", description: "Warm tape hiss, imperfect", promptHint: "lo-fi production", term: "lo-fi production" },
209
+ { id: "raw", label: "Raw", description: "Unfiltered, live feel", promptHint: "raw production", term: "raw production" },
210
+ { id: "vintage", label: "Vintage", description: "Analog warmth", promptHint: "vintage production", term: "vintage production" },
211
+ { id: "modern", label: "Modern", description: "Contemporary digital", promptHint: "modern production", term: "modern production" },
212
+ { id: "minimalist", label: "Minimalist", description: "Sparse, restrained", promptHint: "minimalist production", term: "minimalist production" },
213
+ { id: "wall-of-sound", label: "Wall of Sound", description: "Dense, layered", promptHint: "wall-of-sound production", term: "wall-of-sound production" },
214
+ { id: "ambient", label: "Ambient", description: "Atmospheric, reverbed", promptHint: "ambient production", term: "ambient production" },
215
+ { id: "garage-band", label: "Garage Band", description: "DIY, basement-tracked", promptHint: "garage band production", term: "garage band production" },
216
+ { id: "live-recording", label: "Live", description: "Live concert / room sound", promptHint: "live recording", term: "live recording" },
217
+ { id: "demo-quality", label: "Demo", description: "Rough demo aesthetic", promptHint: "demo quality", term: "demo quality" },
218
+ { id: "studio-pristine", label: "Studio Pristine", description: "Hi-fi reference quality", promptHint: "pristine studio production", term: "pristine studio production" },
219
+ { id: "tape-saturated", label: "Tape Saturated", description: "Analog tape warmth + drive", promptHint: "tape-saturated", term: "tape-saturated production" },
220
+ { id: "8-bit-retro", label: "8-bit Retro", description: "Lo-bitrate retro game audio", promptHint: "8-bit retro production", term: "8-bit retro production" },
208
221
  ] as const
209
222
 
210
223
  export const VOCAL_PRESENCE: ReadonlyArray<InstrumentationEntry> = [
211
- { id: "instrumental", label: "Instrumental", description: "No vocals (exclusive)", promptHint: "instrumental, no vocals" },
212
- { id: "male-lead", label: "Male Lead", description: "Male lead vocal", promptHint: "male lead vocals" },
213
- { id: "female-lead", label: "Female Lead", description: "Female lead vocal", promptHint: "female lead vocals" },
214
- { id: "androgynous-lead",label: "Androgynous", description: "Gender-neutral lead", promptHint: "androgynous lead vocals" },
215
- { id: "duet", label: "Duet", description: "Two lead vocalists", promptHint: "duet lead vocals" },
224
+ { id: "instrumental", label: "Instrumental", description: "No vocals (exclusive)", promptHint: "instrumental, no vocals", term: "instrumental, no vocals" },
225
+ { id: "male-lead", label: "Male Lead", description: "Male lead vocal", promptHint: "male lead vocals", term: "male lead vocals" },
226
+ { id: "female-lead", label: "Female Lead", description: "Female lead vocal", promptHint: "female lead vocals", term: "female lead vocals" },
227
+ { id: "androgynous-lead",label: "Androgynous", description: "Gender-neutral lead", promptHint: "androgynous lead vocals", term: "androgynous lead vocals" },
228
+ { id: "duet", label: "Duet", description: "Two lead vocalists", promptHint: "duet lead vocals", term: "duet lead vocals" },
216
229
  { id: "choir", label: "Choir", description: "Choral / ensemble vocals", promptHint: "choir vocals" },
217
230
  { id: "gospel-choir", label: "Gospel Choir", description: "Gospel-style choir backing", promptHint: "gospel choir" },
218
- { id: "harmony-stack", label: "Harmony Stack", description: "Stacked vocal harmonies", promptHint: "stacked vocal harmonies" },
219
- { id: "rapper-lead", label: "Rapper", description: "Rap lead", promptHint: "rap lead vocals" },
231
+ { id: "harmony-stack", label: "Harmony Stack", description: "Stacked vocal harmonies", promptHint: "stacked vocal harmonies", term: "stacked vocal harmonies" },
232
+ { id: "rapper-lead", label: "Rapper", description: "Rap lead", promptHint: "rap lead vocals", term: "rap lead vocals" },
220
233
  { id: "spoken-word", label: "Spoken Word", description: "Spoken / poetry over music", promptHint: "spoken word" },
221
234
  { id: "vocal-chops", label: "Vocal Chops", description: "Sliced sample vocals", promptHint: "vocal chops" },
222
- { id: "mixed", label: "Mixed", description: "Multiple lead vocalists", promptHint: "mixed lead vocals" },
235
+ { id: "mixed", label: "Mixed", description: "Multiple lead vocalists", promptHint: "mixed lead vocals", term: "mixed lead vocals" },
223
236
  ] as const
224
237
 
225
238
  /** "instrumental" is mutually exclusive with any other vocal presence —
@@ -228,30 +241,30 @@ export const VOCAL_PRESENCE: ReadonlyArray<InstrumentationEntry> = [
228
241
  export const VOCAL_PRESENCE_INSTRUMENTAL_ID = "instrumental"
229
242
 
230
243
  export const SINGING_STYLES: ReadonlyArray<InstrumentationEntry> = [
231
- { id: "pop-singing", label: "Pop", description: "Mainstream lead style", promptHint: "pop singing" },
232
- { id: "rock-singing", label: "Rock", description: "Rock-style vocal delivery", promptHint: "rock singing" },
233
- { id: "operatic", label: "Operatic", description: "Classical opera technique", promptHint: "operatic singing" },
234
- { id: "classical-vocals", label: "Classical", description: "Trained classical voice", promptHint: "classical vocals" },
244
+ { id: "pop-singing", label: "Pop", description: "Mainstream lead style", promptHint: "pop singing", term: "pop singing" },
245
+ { id: "rock-singing", label: "Rock", description: "Rock-style vocal delivery", promptHint: "rock singing", term: "rock singing" },
246
+ { id: "operatic", label: "Operatic", description: "Classical opera technique", promptHint: "operatic singing", term: "operatic singing" },
247
+ { id: "classical-vocals", label: "Classical", description: "Trained classical voice", promptHint: "classical vocals", term: "classical vocals" },
235
248
  { id: "rap", label: "Rap", description: "Rhythmic spoken delivery", promptHint: "rap" },
236
- { id: "trap-melodic", label: "Trap (Melodic)", description: "Melodic trap, autotuned", promptHint: "melodic trap singing" },
237
- { id: "growl", label: "Growl", description: "Death-metal-style growl", promptHint: "growled vocals" },
238
- { id: "scream", label: "Scream", description: "Hardcore / screamed", promptHint: "screamed vocals" },
239
- { id: "shout", label: "Shout", description: "Punk / chanted shout", promptHint: "shouted vocals" },
249
+ { id: "trap-melodic", label: "Trap (Melodic)", description: "Melodic trap, autotuned", promptHint: "melodic trap singing", term: "melodic trap singing" },
250
+ { id: "growl", label: "Growl", description: "Death-metal-style growl", promptHint: "growled vocals", term: "growled vocals" },
251
+ { id: "scream", label: "Scream", description: "Hardcore / screamed", promptHint: "screamed vocals", term: "screamed vocals" },
252
+ { id: "shout", label: "Shout", description: "Punk / chanted shout", promptHint: "shouted vocals", term: "shouted vocals" },
240
253
  { id: "falsetto", label: "Falsetto", description: "High register, light", promptHint: "falsetto" },
241
254
  { id: "head-voice", label: "Head Voice", description: "Resonant high register", promptHint: "head voice" },
242
255
  { id: "chest-voice", label: "Chest Voice", description: "Strong, low register", promptHint: "chest voice" },
243
256
  { id: "belting", label: "Belting", description: "Powerful sustained high", promptHint: "belting" },
244
257
  { id: "crooning", label: "Crooning", description: "Smooth vintage romantic", promptHint: "crooning" },
245
- { id: "scat", label: "Scat", description: "Improvised jazz syllables", promptHint: "scat singing" },
258
+ { id: "scat", label: "Scat", description: "Improvised jazz syllables", promptHint: "scat singing", term: "scat singing" },
246
259
  { id: "yodeling", label: "Yodeling", description: "Alpine register-flip", promptHint: "yodeling" },
247
260
  { id: "throat-singing", label: "Throat Singing", description: "Mongolian / Tuvan overtone", promptHint: "throat singing" },
248
261
  { id: "melismatic", label: "Melismatic", description: "Multi-note runs per syllable", promptHint: "melismatic" },
249
- { id: "autotuned", label: "Autotuned", description: "Pitch-corrected, processed", promptHint: "autotuned vocals" },
250
- { id: "vocoded", label: "Vocoded", description: "Vocoder-processed vocals", promptHint: "vocoded vocals" },
251
- { id: "whisper-singing", label: "Whisper Sing", description: "Hushed, intimate singing", promptHint: "whisper-singing" },
252
- { id: "breathy-singing", label: "Breathy", description: "Airy, intimate singing", promptHint: "breathy singing" },
253
- { id: "harmonized", label: "Harmonized", description: "Multi-voice harmonies", promptHint: "harmonized vocals" },
254
- { id: "chant", label: "Chant", description: "Liturgical / monk chanting", promptHint: "chanted vocals" },
262
+ { id: "autotuned", label: "Autotuned", description: "Pitch-corrected, processed", promptHint: "autotuned vocals", term: "autotuned vocals" },
263
+ { id: "vocoded", label: "Vocoded", description: "Vocoder-processed vocals", promptHint: "vocoded vocals", term: "vocoded vocals" },
264
+ { id: "whisper-singing", label: "Whisper Sing", description: "Hushed, intimate singing", promptHint: "whisper-singing", term: "whisper-singing" },
265
+ { id: "breathy-singing", label: "Breathy", description: "Airy, intimate singing", promptHint: "breathy singing", term: "breathy singing" },
266
+ { id: "harmonized", label: "Harmonized", description: "Multi-voice harmonies", promptHint: "harmonized vocals", term: "harmonized vocals" },
267
+ { id: "chant", label: "Chant", description: "Liturgical / monk chanting", promptHint: "chanted vocals", term: "chanted vocals" },
255
268
  ] as const
256
269
 
257
270
  const INSTRUMENT_BY_ID = new Map(INSTRUMENTS.map((x) => [x.id, x]))
@@ -272,16 +285,40 @@ export function getSingingStyle(id: string | undefined): InstrumentationEntry |
272
285
  return id ? SINGING_STYLE_BY_ID.get(id) : undefined
273
286
  }
274
287
 
288
+ /**
289
+ * The COMPACT counterparts of the `promptHint` lookups: the short professional
290
+ * term a consumer injects instead of the entry's full fragment ("synth stab",
291
+ * "pristine studio production", "melodic trap singing"). Same lookup as the
292
+ * getters above, same empty-string-on-miss behavior, so hint mode and term
293
+ * mode can never disagree about WHICH entry they are describing.
294
+ */
295
+ export function getInstrumentTerm(id: string | undefined | null): string {
296
+ return resolveTerm(id ? getInstrument(id) : undefined)
297
+ }
298
+ export function getProductionStyleTerm(id: string | undefined | null): string {
299
+ return resolveTerm(id ? getProductionStyle(id) : undefined)
300
+ }
301
+ export function getVocalPresenceTerm(id: string | undefined | null): string {
302
+ return resolveTerm(id ? getVocalPresence(id) : undefined)
303
+ }
304
+ export function getSingingStyleTerm(id: string | undefined | null): string {
305
+ return resolveTerm(id ? getSingingStyle(id) : undefined)
306
+ }
307
+
275
308
  import { pickIds } from "@nodaro/shared"
276
309
 
277
- export function buildInstrumentationHints(data: {
278
- readonly preText?: string
279
- readonly postText?: string
280
- readonly instruments?: ReadonlyArray<string>
281
- readonly production?: string
282
- readonly vocalPresence?: string | ReadonlyArray<string>
283
- readonly singingStyle?: string | ReadonlyArray<string>
284
- }): string {
310
+ export function buildInstrumentationHints(
311
+ data: {
312
+ readonly preText?: string
313
+ readonly postText?: string
314
+ readonly instruments?: ReadonlyArray<string>
315
+ readonly production?: string
316
+ readonly vocalPresence?: string | ReadonlyArray<string>
317
+ readonly singingStyle?: string | ReadonlyArray<string>
318
+ },
319
+ mode: PickerHintMode = "full",
320
+ ): string {
321
+ if (mode === "compact") return buildInstrumentationTerms(data)
285
322
  const fragments: string[] = []
286
323
  const pre = typeof data.preText === "string" ? data.preText.trim() : ""
287
324
  if (pre) fragments.push(pre)
@@ -323,6 +360,60 @@ export function buildInstrumentationHints(data: {
323
360
  return fragments.join(", ")
324
361
  }
325
362
 
363
+ /**
364
+ * Compact-mode mirror of `buildInstrumentationHints`: the same preText /
365
+ * [production] [instruments] / "with <vocals>" / "in <style> style" / postText
366
+ * structure, in the same canonical field order, built from each entry's short
367
+ * `term` instead of its full `promptHint`. preText / postText are user free
368
+ * text rather than catalog copy, so they pass through untouched in both modes.
369
+ */
370
+ export function buildInstrumentationTerms(data: {
371
+ readonly preText?: string
372
+ readonly postText?: string
373
+ readonly instruments?: ReadonlyArray<string>
374
+ readonly production?: string
375
+ readonly vocalPresence?: string | ReadonlyArray<string>
376
+ readonly singingStyle?: string | ReadonlyArray<string>
377
+ }): string {
378
+ const fragments: string[] = []
379
+ const pre = typeof data.preText === "string" ? data.preText.trim() : ""
380
+ if (pre) fragments.push(pre)
381
+
382
+ const segments: string[] = []
383
+ const prod = getProductionStyleTerm(data.production)
384
+ if (prod) segments.push(prod)
385
+
386
+ const instTerms = (data.instruments ?? [])
387
+ .map((id) => getInstrumentTerm(id))
388
+ .filter((t) => t.length > 0)
389
+ if (instTerms.length > 0) segments.push(instTerms.join(", "))
390
+
391
+ const vocalClause = pickIds(data.vocalPresence)
392
+ .map((id) => getVocalPresenceTerm(id))
393
+ .filter((t) => t.length > 0)
394
+ .join(", ")
395
+
396
+ const styleClause = pickIds(data.singingStyle)
397
+ .map((id) => getSingingStyleTerm(id))
398
+ .filter((t) => t.length > 0)
399
+ .join(", ")
400
+
401
+ const result: string[] = []
402
+ if (segments.length > 0) result.push(segments.join(" "))
403
+ if (vocalClause) {
404
+ result.push(result.length > 0 ? `with ${vocalClause}` : vocalClause)
405
+ }
406
+ if (styleClause) {
407
+ result.push(result.length > 0 ? `in ${styleClause} style` : `${styleClause} style`)
408
+ }
409
+ if (result.length > 0) fragments.push(result.join(" "))
410
+
411
+ const post = typeof data.postText === "string" ? data.postText.trim() : ""
412
+ if (post) fragments.push(post)
413
+
414
+ return fragments.join(", ")
415
+ }
416
+
326
417
  export function isInstrumentalVocal(value: unknown): boolean {
327
418
  return pickIds(value).includes(VOCAL_PRESENCE_INSTRUMENTAL_ID)
328
419
  }
package/src/lens.ts CHANGED
@@ -10,32 +10,43 @@
10
10
  * frontend DAG executor and the backend orchestrator.
11
11
  */
12
12
 
13
+ import { resolveTerm } from "./term.js"
14
+
13
15
  export interface Lens {
14
16
  readonly id: string
15
17
  readonly label: string
16
18
  readonly description: string
17
19
  readonly promptHint: string
20
+ /**
21
+ * Optional authored compact term (see `term.ts`). Authored where the
22
+ * lowercased label is not what a cinematographer would write in a prompt —
23
+ * the focal length lives in a parenthetical the derivation drops
24
+ * ("Wide (24mm)" → "wide"), or the bare optic name needs its category noun
25
+ * ("Macro" → "macro lens", "Shallow DOF" → "shallow depth of field").
26
+ * Everywhere else the label IS the term.
27
+ */
28
+ readonly term?: string
18
29
  }
19
30
 
20
31
  export const LENSES: ReadonlyArray<Lens> = [
21
- { id: "ultra-wide-14mm", label: "Ultra-wide (14mm)", description: "Extreme wide angle, exaggerated perspective", promptHint: "shot on ultra-wide 14mm lens, exaggerated perspective with strong barrel distortion" },
22
- { id: "wide-24mm", label: "Wide (24mm)", description: "Wide field of view, environmental", promptHint: "shot on wide 24mm lens, expansive environmental field of view" },
23
- { id: "standard-35mm", label: "Standard (35mm)", description: "Natural perspective, documentary feel", promptHint: "shot on standard 35mm lens, natural perspective with documentary feel" },
24
- { id: "normal-50mm", label: "Normal (50mm)", description: "Closest to human eye perception", promptHint: "shot on normal 50mm lens, perspective closest to natural human vision" },
25
- { id: "portrait-85mm", label: "Portrait (85mm)", description: "Flattering compression, creamy bokeh", promptHint: "shot on 85mm portrait lens, flattering facial compression with creamy background bokeh" },
26
- { id: "telephoto-135mm", label: "Telephoto (135mm)", description: "Compressed depth, isolated subject", promptHint: "shot on 135mm telephoto lens, compressed depth with subject isolated from background" },
27
- { id: "super-telephoto-400mm", label: "Super Telephoto (400mm)", description: "Extreme compression, distant subject", promptHint: "shot on super-telephoto 400mm lens, extreme depth compression with subject pulled forward" },
28
- { id: "fisheye", label: "Fisheye", description: "Hemispherical 180° distortion", promptHint: "shot on fisheye lens, extreme 180-degree hemispherical distortion with curved horizon" },
29
- { id: "anamorphic", label: "Anamorphic", description: "Cinematic widescreen, oval bokeh", promptHint: "anamorphic lens look, cinematic widescreen feel with characteristic oval bokeh and horizontal lens flares" },
30
- { id: "macro", label: "Macro", description: "Extreme close-up of small detail", promptHint: "macro lens, extreme close-up revealing fine detail with shallow depth of field" },
32
+ { id: "ultra-wide-14mm", label: "Ultra-wide (14mm)", description: "Extreme wide angle, exaggerated perspective", promptHint: "shot on ultra-wide 14mm lens, exaggerated perspective with strong barrel distortion", term: "14mm ultra-wide lens" },
33
+ { id: "wide-24mm", label: "Wide (24mm)", description: "Wide field of view, environmental", promptHint: "shot on wide 24mm lens, expansive environmental field of view", term: "24mm wide-angle lens" },
34
+ { id: "standard-35mm", label: "Standard (35mm)", description: "Natural perspective, documentary feel", promptHint: "shot on standard 35mm lens, natural perspective with documentary feel", term: "35mm standard lens" },
35
+ { id: "normal-50mm", label: "Normal (50mm)", description: "Closest to human eye perception", promptHint: "shot on normal 50mm lens, perspective closest to natural human vision", term: "50mm normal lens" },
36
+ { id: "portrait-85mm", label: "Portrait (85mm)", description: "Flattering compression, creamy bokeh", promptHint: "shot on 85mm portrait lens, flattering facial compression with creamy background bokeh", term: "85mm portrait lens" },
37
+ { id: "telephoto-135mm", label: "Telephoto (135mm)", description: "Compressed depth, isolated subject", promptHint: "shot on 135mm telephoto lens, compressed depth with subject isolated from background", term: "135mm telephoto lens" },
38
+ { id: "super-telephoto-400mm", label: "Super Telephoto (400mm)", description: "Extreme compression, distant subject", promptHint: "shot on super-telephoto 400mm lens, extreme depth compression with subject pulled forward", term: "400mm super-telephoto lens" },
39
+ { id: "fisheye", label: "Fisheye", description: "Hemispherical 180° distortion", promptHint: "shot on fisheye lens, extreme 180-degree hemispherical distortion with curved horizon", term: "fisheye lens" },
40
+ { id: "anamorphic", label: "Anamorphic", description: "Cinematic widescreen, oval bokeh", promptHint: "anamorphic lens look, cinematic widescreen feel with characteristic oval bokeh and horizontal lens flares", term: "anamorphic lens" },
41
+ { id: "macro", label: "Macro", description: "Extreme close-up of small detail", promptHint: "macro lens, extreme close-up revealing fine detail with shallow depth of field", term: "macro lens" },
31
42
  { id: "probe", label: "Probe Lens", description: "Tube macro — through holes and tight spaces", promptHint: "probe lens shot, tube-lens macro perspective gliding through impossibly tight spaces with extreme close-up depth" },
32
- { id: "cctv", label: "CCTV", description: "Security-camera surveillance look", promptHint: "CCTV security camera footage look, high-angle fixed surveillance framing, mild wide-angle distortion, washed contrast and visible video grain" },
33
- { id: "tilt-shift", label: "Tilt-shift", description: "Selective focus, miniature effect", promptHint: "tilt-shift lens, selective plane of focus producing a miniature-diorama effect" },
34
- { id: "shallow-dof", label: "Shallow DOF", description: "Razor-thin focus, dreamy bokeh", promptHint: "extremely shallow depth of field, razor-thin focal plane with dreamy out-of-focus bokeh" },
43
+ { id: "cctv", label: "CCTV", description: "Security-camera surveillance look", promptHint: "CCTV security camera footage look, high-angle fixed surveillance framing, mild wide-angle distortion, washed contrast and visible video grain", term: "cctv surveillance camera look" },
44
+ { id: "tilt-shift", label: "Tilt-shift", description: "Selective focus, miniature effect", promptHint: "tilt-shift lens, selective plane of focus producing a miniature-diorama effect", term: "tilt-shift lens" },
45
+ { id: "shallow-dof", label: "Shallow DOF", description: "Razor-thin focus, dreamy bokeh", promptHint: "extremely shallow depth of field, razor-thin focal plane with dreamy out-of-focus bokeh", term: "shallow depth of field" },
35
46
  { id: "canon-k35", label: "Canon K35 Prime", description: "Vintage cinematic, warm gentle skin", promptHint: "shot on a vintage Canon K35 cinema prime, warm gentle skin rendition, low-contrast highlight roll-off, soft dreamy falloff into the bokeh and the unmistakable seventies-era cinematic patina" },
36
47
  { id: "cooke-s4", label: "Cooke S4 Prime", description: "The Cooke look — creamy painterly skin", promptHint: "shot on a Cooke S4 cinema prime, the signature \"Cooke look\" with creamy painterly bokeh, gently rounded out-of-focus highlights and a flattering filmic skin rendition" },
37
- { id: "helios-44", label: "Helios 44", description: "Vintage Soviet swirly bokeh", promptHint: "shot on a vintage Soviet Helios 44 lens, the legendary swirly bokeh circling the subject, soft low contrast in the corners and a dreamlike vintage aberration character" },
38
- { id: "petzval", label: "Petzval Portrait", description: "Ultra-vintage swirl, dramatic falloff", promptHint: "shot on an ultra-vintage Petzval portrait lens, sharp central focus that falls off rapidly into a swirling, painterly bokeh, dramatic vignetting and a 19th-century daguerreotype-era softness at the edges" },
48
+ { id: "helios-44", label: "Helios 44", description: "Vintage Soviet swirly bokeh", promptHint: "shot on a vintage Soviet Helios 44 lens, the legendary swirly bokeh circling the subject, soft low contrast in the corners and a dreamlike vintage aberration character", term: "vintage helios 44 lens, swirly bokeh" },
49
+ { id: "petzval", label: "Petzval Portrait", description: "Ultra-vintage swirl, dramatic falloff", promptHint: "shot on an ultra-vintage Petzval portrait lens, sharp central focus that falls off rapidly into a swirling, painterly bokeh, dramatic vignetting and a 19th-century daguerreotype-era softness at the edges", term: "petzval portrait lens, swirling bokeh" },
39
50
  ] as const
40
51
 
41
52
  const lensById = new Map<string, Lens>(LENSES.map((l) => [l.id, l]))
@@ -56,4 +67,9 @@ export function getLensPromptHint(id: string | undefined | null): string {
56
67
  return getLens(id)?.promptHint ?? ""
57
68
  }
58
69
 
70
+ /** The compact professional term for a lens (see `term.ts`). */
71
+ export function getLensTerm(id: string | undefined | null): string {
72
+ return resolveTerm(getLens(id))
73
+ }
74
+
59
75
  export const LENS_IDS: ReadonlyArray<string> = LENSES.map((l) => l.id)