@nodaro/prompts 1.0.1

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 (150) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +27 -0
  3. package/dist/index.cjs +21240 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +3599 -0
  6. package/dist/index.d.ts +3599 -0
  7. package/dist/index.js +20854 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +49 -0
  10. package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
  11. package/src/__tests__/action-fx.test.ts +155 -0
  12. package/src/__tests__/apply-picker-json.test.ts +50 -0
  13. package/src/__tests__/assemble-image-input.test.ts +248 -0
  14. package/src/__tests__/assemble-suno-input.test.ts +283 -0
  15. package/src/__tests__/brand-tokens.test.ts +81 -0
  16. package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
  17. package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
  18. package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
  19. package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
  20. package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
  21. package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
  22. package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
  23. package/src/__tests__/catalog-gapfill.test.ts +53 -0
  24. package/src/__tests__/character-convergence-image.test.ts +217 -0
  25. package/src/__tests__/character-default-role-image.test.ts +167 -0
  26. package/src/__tests__/character-default-role-video.test.ts +168 -0
  27. package/src/__tests__/character-default-role.test.ts +70 -0
  28. package/src/__tests__/character-fx.test.ts +200 -0
  29. package/src/__tests__/entity-prompts-location.test.ts +97 -0
  30. package/src/__tests__/entity-prompts.test.ts +240 -0
  31. package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
  32. package/src/__tests__/factory-presets.test.ts +1045 -0
  33. package/src/__tests__/factory-snippets.test.ts +68 -0
  34. package/src/__tests__/framing-multi.test.ts +71 -0
  35. package/src/__tests__/framing-vantage.test.ts +39 -0
  36. package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
  37. package/src/__tests__/identity-lock.test.ts +46 -0
  38. package/src/__tests__/instrumentation.test.ts +68 -0
  39. package/src/__tests__/lighting-multi.test.ts +68 -0
  40. package/src/__tests__/location-convergence-image.test.ts +124 -0
  41. package/src/__tests__/mention-lock-flag.test.ts +499 -0
  42. package/src/__tests__/multi-picker-spec.test.ts +71 -0
  43. package/src/__tests__/music-genre.test.ts +103 -0
  44. package/src/__tests__/music-mood.test.ts +87 -0
  45. package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
  46. package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
  47. package/src/__tests__/parameter-registry-sync.test.ts +243 -0
  48. package/src/__tests__/person-age.test.ts +80 -0
  49. package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
  50. package/src/__tests__/person-body-axes.test.ts +67 -0
  51. package/src/__tests__/person-facial-geometry.test.ts +156 -0
  52. package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
  53. package/src/__tests__/person-sections.test.ts +18 -0
  54. package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
  55. package/src/__tests__/picker-catalogs-project.test.ts +85 -0
  56. package/src/__tests__/picker-catalogs.test.ts +53 -0
  57. package/src/__tests__/picker-limits.test.ts +20 -0
  58. package/src/__tests__/prompt-builder-segments.test.ts +183 -0
  59. package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
  60. package/src/__tests__/prompt-builder.test.ts +1773 -0
  61. package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
  62. package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
  63. package/src/__tests__/resolve-prompt-append.test.ts +58 -0
  64. package/src/__tests__/resolve-prompt.test.ts +44 -0
  65. package/src/__tests__/role-picker-shared.test.ts +208 -0
  66. package/src/__tests__/seedance-2-inputs.test.ts +173 -0
  67. package/src/__tests__/seedance-extend.test.ts +44 -0
  68. package/src/__tests__/sound-aggregator.test.ts +350 -0
  69. package/src/__tests__/style-presets.test.ts +34 -0
  70. package/src/__tests__/temporal-multi.test.ts +74 -0
  71. package/src/__tests__/transitions.test.ts +213 -0
  72. package/src/__tests__/video-reference-features.test.ts +41 -0
  73. package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
  74. package/src/__tests__/video-reference-resolver.test.ts +233 -0
  75. package/src/__tests__/video-reference-roles.test.ts +96 -0
  76. package/src/__tests__/voice-character.test.ts +48 -0
  77. package/src/__tests__/voice-delivery.test.ts +41 -0
  78. package/src/__tests__/wardrobe.test.ts +25 -0
  79. package/src/action-fx.ts +255 -0
  80. package/src/aesthetic.ts +435 -0
  81. package/src/assemble-image-input.ts +236 -0
  82. package/src/assemble-suno-input.ts +147 -0
  83. package/src/atmosphere.ts +104 -0
  84. package/src/backdrop.ts +131 -0
  85. package/src/brand-tokens.ts +154 -0
  86. package/src/camera-format.ts +76 -0
  87. package/src/camera-motions.ts +615 -0
  88. package/src/character-fx.ts +274 -0
  89. package/src/color-look.ts +103 -0
  90. package/src/composition-effects.ts +67 -0
  91. package/src/entity-prompts.ts +231 -0
  92. package/src/era.ts +292 -0
  93. package/src/exposure-settings.ts +142 -0
  94. package/src/factory-presets/generate-image.ts +1644 -0
  95. package/src/factory-presets/generate-video.ts +1116 -0
  96. package/src/factory-presets/index.ts +46 -0
  97. package/src/factory-presets/lottie-overlay.ts +166 -0
  98. package/src/factory-presets/motion-graphics.ts +350 -0
  99. package/src/factory-presets/music.ts +734 -0
  100. package/src/factory-presets/sfx.ts +136 -0
  101. package/src/factory-presets/shared-image.ts +207 -0
  102. package/src/factory-presets/switchx.ts +65 -0
  103. package/src/factory-presets/text.ts +292 -0
  104. package/src/factory-presets/types.ts +51 -0
  105. package/src/factory-presets/video-edit.ts +136 -0
  106. package/src/factory-presets/voice.ts +172 -0
  107. package/src/factory-presets.ts +2 -0
  108. package/src/factory-snippets/catalog.ts +105 -0
  109. package/src/factory-snippets/index.ts +16 -0
  110. package/src/factory-snippets/types.ts +33 -0
  111. package/src/framing.ts +634 -0
  112. package/src/held-prop.ts +187 -0
  113. package/src/identity-lock.ts +213 -0
  114. package/src/index.ts +66 -0
  115. package/src/instrumentation.ts +337 -0
  116. package/src/lens.ts +59 -0
  117. package/src/lighting.ts +229 -0
  118. package/src/loop-subject.ts +276 -0
  119. package/src/materials.ts +184 -0
  120. package/src/mood.ts +186 -0
  121. package/src/music-genre.ts +662 -0
  122. package/src/music-mood.ts +137 -0
  123. package/src/object-asset-presets.ts +81 -0
  124. package/src/parameter-prompt-hint.ts +281 -0
  125. package/src/person.ts +1368 -0
  126. package/src/photo-genre.ts +151 -0
  127. package/src/photographer.ts +612 -0
  128. package/src/picker-analyzer-registry.ts +374 -0
  129. package/src/picker-catalogs.ts +858 -0
  130. package/src/pose.ts +246 -0
  131. package/src/post-process-effects.ts +94 -0
  132. package/src/prompt-builder-structured-fields.ts +116 -0
  133. package/src/prompt-builder.ts +2964 -0
  134. package/src/prompt-templates.ts +50 -0
  135. package/src/prompt-wizard-categories.ts +334 -0
  136. package/src/provider-prompt-doctrine.ts +85 -0
  137. package/src/render-quality.ts +89 -0
  138. package/src/resolve-prompt.ts +120 -0
  139. package/src/seedance-2-inputs.ts +100 -0
  140. package/src/setting.ts +130 -0
  141. package/src/sound-aggregator.ts +241 -0
  142. package/src/style-presets.ts +162 -0
  143. package/src/style.ts +99 -0
  144. package/src/styling.ts +585 -0
  145. package/src/temporal.ts +151 -0
  146. package/src/transitions.ts +333 -0
  147. package/src/video-reference-resolver.ts +823 -0
  148. package/src/voice-character.ts +237 -0
  149. package/src/voice-delivery.ts +142 -0
  150. package/src/wardrobe.ts +179 -0
package/src/person.ts ADDED
@@ -0,0 +1,1368 @@
1
+ /**
2
+ * Canonical catalog of Person / subject-appearance choices.
3
+ *
4
+ * Multi-dimension parameter node. Unlike Setting / Style / Camera-Motion
5
+ * (single pick) or Framing (5 dims, all shot composition), Person describes
6
+ * *who the subject is*. Each dimension is mutually exclusive within itself
7
+ * (pick one Type, one Age, one Cheekbones, etc.) and all are independently
8
+ * optional. Dimensions are grouped into six sections (PERSON_DIMENSION_SECTIONS):
9
+ * Identity, Body, Face, Hair, Skin & Eyes, Features.
10
+ *
11
+ * The Face section carries a dedicated facial-geometry layer — cheekbones,
12
+ * facial fullness, eyelid type, canthal tilt, eye spacing, eye-to-brow set,
13
+ * nose tip, and split lip fullness / lip shape — so the structured pickers
14
+ * (not a single "supermodel" Type token) drive the feature geometry and ratios
15
+ * that actually define a face's look. Neutral options (Average / Neutral /
16
+ * Standard / Natural) carry an empty promptHint, so enabling a dimension
17
+ * injects nothing until a real value is picked.
18
+ *
19
+ * Non-empty selections from each dimension are joined as a compound hint and
20
+ * injected as cinematography context into downstream image/video prompts:
21
+ * "a beautiful woman, in her 30s, East Asian, slim build, long wavy hair,
22
+ * brown hair, fair skin, green eyes"
23
+ *
24
+ * Applies to BOTH image and video consumers (a person description is not
25
+ * video-specific, unlike camera-motion / temporal). Not in
26
+ * STILL_IMAGE_EXCLUDE_TYPES.
27
+ *
28
+ * Shared between the picker UI, the standalone Person parameter node, and
29
+ * the prompt-hint injection on both the frontend DAG executor and the
30
+ * backend orchestrator.
31
+ */
32
+
33
+ export type PersonDimension =
34
+ | "type"
35
+ | "age"
36
+ | "ethnicity"
37
+ | "regional-aesthetic"
38
+ | "frame"
39
+ | "body-mass"
40
+ | "bust"
41
+ | "waist"
42
+ | "hips"
43
+ | "silhouette"
44
+ | "face-shape"
45
+ | "jawline"
46
+ | "cheekbones"
47
+ | "facial-fullness"
48
+ | "eye-shape"
49
+ | "eyelid-type"
50
+ | "canthal-tilt"
51
+ | "eye-spacing"
52
+ | "eye-set-brow"
53
+ | "nose"
54
+ | "nose-tip"
55
+ | "lip-fullness"
56
+ | "lip-shape"
57
+ | "lip-state"
58
+ | "hair-color"
59
+ | "hair-base"
60
+ | "eyebrows"
61
+ | "skin-tone"
62
+ | "skin-texture"
63
+ | "eye-color"
64
+ | "eye-state"
65
+ | "facial-hair"
66
+ | "distinctive-features"
67
+
68
+ export interface Person {
69
+ readonly id: string
70
+ readonly label: string
71
+ readonly dimension: PersonDimension
72
+ readonly description: string
73
+ readonly promptHint: string
74
+ /** Optional sub-grouping within a dimension. Used by the picker to render
75
+ * two-level selection (e.g. ethnicity grouped by region → specific). */
76
+ readonly group?: string
77
+ /** Optional shorter label for the picker chip. When a dimension renders
78
+ * with group headers (e.g. ethnicity), the group name already supplies
79
+ * context — so a chip under the "Asian" header shows "East (any)"
80
+ * instead of the redundant "East Asian (any)". Node cards + tooltips
81
+ * keep the full `label` / `description`. */
82
+ readonly shortLabel?: string
83
+ }
84
+
85
+ export const PEOPLE: ReadonlyArray<Person> = [
86
+ // -------------------- Type (primary subject descriptor) --------------------
87
+ // Type captures identity / archetype / species. Pure-age subjects (baby,
88
+ // child, teenager) belong in the Age dimension. Entries are tagged with a
89
+ // `group` so the picker renders Type as collapsible sub-sections — the
90
+ // catalog is large enough that flat scrolling isn't usable.
91
+ //
92
+ // Gender policy:
93
+ // - For species/creatures whose silhouette doesn't depend strongly on
94
+ // gender, entries are gender-neutral. The user can pair Build / Hair /
95
+ // Lips for masculine/feminine cues.
96
+ // - For archetypes with canonical gendered forms (wizard/witch,
97
+ // sorcerer/sorceress, god/goddess, prince/princess, mer-man/mermaid),
98
+ // we ship paired entries.
99
+
100
+ // ----- Realistic — Plain -----
101
+ { id: "man", label: "Man", group: "Realistic", dimension: "type", description: "Adult man", promptHint: "a man" },
102
+ { id: "woman", label: "Woman", group: "Realistic", dimension: "type", description: "Adult woman", promptHint: "a woman" },
103
+ // "Not defined" is the open / unspecified slot — distinct from non-binary.
104
+ // Use this when gender shouldn't be pinned in the prompt; the model gets
105
+ // "a person" and ethnicity / build / hair drive the rest.
106
+ { id: "not-defined", label: "Not Defined", group: "Realistic", dimension: "type", description: "Gender not specified — left open", promptHint: "a person" },
107
+
108
+ // ----- Realistic — Aesthetic / archetype -----
109
+ { id: "beautiful-woman", label: "Beautiful Woman", group: "Realistic — Style", dimension: "type", description: "Classically beautiful woman", promptHint: "a beautiful woman" },
110
+ { id: "girl-next-door", label: "Girl Next Door", group: "Realistic — Style", dimension: "type", description: "Friendly, approachable, wholesome", promptHint: "a girl-next-door type — friendly, approachable, natural beauty with wholesome charm" },
111
+ { id: "handsome-man", label: "Handsome Man", group: "Realistic — Style", dimension: "type", description: "Classically handsome man", promptHint: "a handsome man" },
112
+ { id: "boy-next-door", label: "Boy Next Door", group: "Realistic — Style", dimension: "type", description: "Friendly, approachable, wholesome", promptHint: "a boy-next-door type — friendly, approachable, with wholesome charm" },
113
+ { id: "gentleman", label: "Gentleman", group: "Realistic — Style", dimension: "type", description: "Refined, classic, polished man", promptHint: "a refined gentleman with classic elegance and polished presence" },
114
+ { id: "elegant-woman", label: "Elegant Woman", group: "Realistic — Style", dimension: "type", description: "Poised, refined woman", promptHint: "an elegant, poised woman" },
115
+ { id: "rugged-man", label: "Rugged Man", group: "Realistic — Style", dimension: "type", description: "Weathered, rugged man", promptHint: "a rugged, weathered man" },
116
+ { id: "bad-boy", label: "Bad Boy", group: "Realistic — Style", dimension: "type", description: "Rebellious, edgy masculine", promptHint: "a bad boy with rebellious, edgy attitude and a dangerous charm" },
117
+ { id: "graceful-woman", label: "Graceful Woman", group: "Realistic — Style", dimension: "type", description: "Gentle, graceful woman", promptHint: "a graceful woman" },
118
+ { id: "baddie", label: "Baddie", group: "Realistic — Style", dimension: "type", description: "Confident, trendy, styled woman", promptHint: "a baddie — a confident, trendy woman with styled makeup and a fashion-forward look" },
119
+ { id: "stylish-influencer", label: "Stylish Influencer", group: "Realistic — Style", dimension: "type", description: "Polished social-media tastemaker", promptHint: "a stylish social-media influencer — polished personal-brand styling, trend-forward outfit, expressive natural posing for the camera, the curated put-together look of a content creator" },
120
+ { id: "stunning-model", label: "Stunning Model", group: "Realistic — Style", dimension: "type", description: "Fashion-model aesthetic", promptHint: "a stunning fashion model with editorial poise, refined features, and high-fashion presence" },
121
+ { id: "supermodel", label: "Supermodel", group: "Realistic — Style", dimension: "type", description: "Iconic, top-tier runway and cover star", promptHint: "a supermodel — iconic top-tier presence with magnetic statement features, a striking silhouette and the unmistakable runway-and-cover aura that commands attention" },
122
+ { id: "femme-fatale", label: "Femme Fatale", group: "Realistic — Style", dimension: "type", description: "Alluring, dangerous noir seductress", promptHint: "a femme fatale — alluring, mysterious, and dangerous with classic noir seduction" },
123
+ { id: "tough-guy", label: "Tough Guy", group: "Realistic — Style", dimension: "type", description: "Hardened, tough man", promptHint: "a tough, hardened man" },
124
+ // Aesthetic archetypes that traditionally read older — hints kept gender +
125
+ // look-focused so the Age dimension stays in control of the literal age.
126
+ { id: "wise-elder", label: "Wise Elder", group: "Realistic — Style", dimension: "type", description: "Knowing, sage presence", promptHint: "a wise, sage figure with a knowing, contemplative presence" },
127
+ { id: "silver-fox", label: "Silver Fox", group: "Realistic — Style", dimension: "type", description: "Distinguished man with silver-gray hair", promptHint: "a silver fox — a distinguished man with silver-gray hair and a confident, magnetic presence" },
128
+ { id: "mysterious-figure", label: "Mysterious Figure", group: "Realistic — Style", dimension: "type", description: "Enigmatic figure", promptHint: "a mysterious figure" },
129
+ { id: "femboy", label: "Femboy", group: "Realistic — Style", dimension: "type", description: "Soft-feminine masculine type, gender-fluid styling", promptHint: "a femboy — soft-feminine masculine presentation with gender-fluid styling" },
130
+ { id: "twink", label: "Twink", group: "Realistic — Style", dimension: "type", description: "Slim, smooth, soft gay-male type", promptHint: "a twink — slim, smooth, soft masculine presence with a delicate, gender-fluid vibe" },
131
+ { id: "soft-butch", label: "Soft Butch", group: "Realistic — Style", dimension: "type", description: "Gentle masculine-of-center woman", promptHint: "a soft-butch woman — gentle masculine-of-center styling and energy" },
132
+ { id: "tomboy", label: "Tomboy", group: "Realistic — Style", dimension: "type", description: "Boyish styling and energy on a feminine body", promptHint: "a tomboy — boyish styling and energy on a feminine body" },
133
+ { id: "thick", label: "Thick", group: "Realistic — Style", dimension: "type", description: "Voluptuous body-positive figure with curves", promptHint: "a thick, voluptuous body-positive figure with full curves" },
134
+ { id: "androgynous", label: "Androgynous", group: "Realistic — Style", dimension: "type", description: "Deliberately blended masculine + feminine cues", promptHint: "an androgynous figure with deliberately blended masculine and feminine cues" },
135
+ { id: "goth", label: "Goth", group: "Realistic — Style", dimension: "type", description: "Goth subculture aesthetic — dark, romantic, dramatic", promptHint: "a goth figure with a dark romantic dramatic subculture aesthetic" },
136
+ { id: "bear", label: "Bear", group: "Realistic — Style", dimension: "type", description: "Big, bearded, hairy masculine type", promptHint: "a bear — a big, bearded, hairy man with a warm masculine presence" },
137
+
138
+ // ----- Primitive / Wild -----
139
+ { id: "caveman", label: "Caveman", group: "Primitive / Wild", dimension: "type", description: "Stone Age primitive man", promptHint: "a caveman — Stone Age primitive in animal furs, with shaggy hair, a heavy brow and a sturdy frame" },
140
+ { id: "cavewoman", label: "Cavewoman", group: "Primitive / Wild", dimension: "type", description: "Stone Age primitive woman", promptHint: "a cavewoman — Stone Age primitive in animal furs, with shaggy hair and a sturdy frame" },
141
+ { id: "apeman", label: "Apeman", group: "Primitive / Wild", dimension: "type", description: "Half-human half-ape hybrid", promptHint: "an apeman — half-human half-ape hybrid with simian features, broad shoulders and a heavy stooped build" },
142
+ { id: "feral-wildman", label: "Feral Wild-Human", group: "Primitive / Wild", dimension: "type", description: "Overgrown, primal feral human", promptHint: "a feral wild-human — overgrown matted hair, primal eyes and a near-naked, bestial bearing" },
143
+ { id: "neanderthal", label: "Neanderthal", group: "Primitive / Wild", dimension: "type", description: "Pre-modern human species", promptHint: "a neanderthal — heavy brow, broad nose, stocky pre-modern human build with weather-toughened skin" },
144
+
145
+ // ----- Fantasy (humanoid) -----
146
+ { id: "elf-man", label: "Elf (Male)", group: "Fantasy", dimension: "type", description: "Masculine pointed-ear humanoid", promptHint: "a male elf — slender masculine humanoid with delicately pointed ears, fine sharp features and an ethereal poise" },
147
+ { id: "elf-woman", label: "Elf (Female)", group: "Fantasy", dimension: "type", description: "Feminine pointed-ear humanoid", promptHint: "a female elf — slender feminine humanoid with delicately pointed ears, fine elegant features and an ethereal poise" },
148
+ { id: "elf", label: "Elf", group: "Fantasy", dimension: "type", description: "Slender humanoid with pointed ears", promptHint: "an elf — slender humanoid with delicately pointed ears, fine features and an ethereal poise" },
149
+ { id: "half-elf", label: "Half-Elf", group: "Fantasy", dimension: "type", description: "Mixed elven-human heritage", promptHint: "a half-elf with subtly pointed ears and a graceful blend of human and elven features" },
150
+ { id: "dwarf", label: "Dwarf", group: "Fantasy", dimension: "type", description: "Stocky mountain-folk humanoid", promptHint: "a dwarf — stocky, broad-shouldered humanoid with a thick beard and a hardy mountain-folk look" },
151
+ { id: "orc", label: "Orc", group: "Fantasy", dimension: "type", description: "Heavyset tusked humanoid", promptHint: "an orc — heavyset humanoid with greenish skin, jutting tusks and a brutish, battle-scarred build" },
152
+ { id: "goblin", label: "Goblin", group: "Fantasy", dimension: "type", description: "Small, wiry humanoid", promptHint: "a goblin — small, wiry humanoid with sharp features, oversized ears and a mischievous look" },
153
+ { id: "tiefling", label: "Tiefling", group: "Fantasy", dimension: "type", description: "Horned humanoid with infernal heritage", promptHint: "a tiefling — humanoid with curling horns, a tail and faintly otherworldly skin tones" },
154
+ { id: "faun", label: "Faun / Satyr", group: "Fantasy", dimension: "type", description: "Goat-legged horned humanoid", promptHint: "a faun — humanoid with goat legs, small curling horns and a woodland presence" },
155
+ { id: "centaur", label: "Centaur", group: "Fantasy", dimension: "type", description: "Human torso on a horse body", promptHint: "a centaur — human torso fused to the body of a horse" },
156
+ { id: "vampire-man", label: "Vampire (Male)", group: "Fantasy", dimension: "type", description: "Masculine fanged immortal", promptHint: "a male vampire — pale, fanged immortal man in formal evening wear with gothic elegance and a quiet predatory poise" },
157
+ { id: "vampire-woman", label: "Vampire (Female)", group: "Fantasy", dimension: "type", description: "Feminine fanged immortal", promptHint: "a female vampire — pale, fanged immortal woman in flowing gothic attire with seductive elegance and a quiet predatory poise" },
158
+ { id: "vampire", label: "Vampire", group: "Fantasy", dimension: "type", description: "Pale, fanged immortal", promptHint: "a vampire — pale, fanged immortal with a pristine gothic elegance and a quiet predatory poise" },
159
+ { id: "werewolf", label: "Werewolf", group: "Fantasy", dimension: "type", description: "Partially transformed lycanthrope", promptHint: "a werewolf — partially transformed humanoid with wolfish snout, fur, claws and a feral musculature" },
160
+ { id: "mermaid", label: "Mermaid", group: "Fantasy", dimension: "type", description: "Female humanoid with a fish tail", promptHint: "a mermaid — humanoid woman from the waist up with a long iridescent fish tail below" },
161
+ { id: "merman", label: "Merman", group: "Fantasy", dimension: "type", description: "Male humanoid with a fish tail", promptHint: "a merman — humanoid man from the waist up with a long iridescent fish tail below" },
162
+ { id: "dragonkin", label: "Dragonkin", group: "Fantasy", dimension: "type", description: "Scaled draconic humanoid", promptHint: "dragonkin — humanoid with scaled skin, ridged horns and reptilian draconic features" },
163
+ { id: "wizard", label: "Wizard", group: "Fantasy", dimension: "type", description: "Robed male spellcaster", promptHint: "a wizard — robed male spellcaster with arcane symbols, a long staff and a contemplative bearing" },
164
+ { id: "witch", label: "Witch", group: "Fantasy", dimension: "type", description: "Robed female spellcaster", promptHint: "a witch — robed female spellcaster with arcane sigils, a knowing gaze and a quietly powerful air" },
165
+ { id: "sorcerer", label: "Sorcerer", group: "Fantasy", dimension: "type", description: "Magical man in flowing robes", promptHint: "a sorcerer — magical man in flowing robes wreathed in arcane glow" },
166
+ { id: "sorceress", label: "Sorceress", group: "Fantasy", dimension: "type", description: "Magical woman in flowing robes", promptHint: "a sorceress — magical woman in flowing robes wreathed in arcane glow" },
167
+ { id: "knight-man", label: "Knight (Male)", group: "Fantasy", dimension: "type", description: "Plate-armoured male warrior", promptHint: "a male knight in full plate armour with a heraldic surcoat, broad-shouldered build and a steady warrior's stance" },
168
+ { id: "knight-woman", label: "Knight (Female)", group: "Fantasy", dimension: "type", description: "Plate-armoured female warrior", promptHint: "a female knight — warrior maiden in full plate armour with a heraldic surcoat, a poised stance and steady warrior's bearing" },
169
+ { id: "knight", label: "Knight", group: "Fantasy", dimension: "type", description: "Plate-armoured warrior", promptHint: "a knight in full plate armour with a heraldic surcoat and a steady warrior's stance" },
170
+ { id: "barbarian-man", label: "Barbarian (Male)", group: "Fantasy", dimension: "type", description: "Brawny male fur-clad warrior", promptHint: "a male barbarian — brawny masculine warrior in furs and tribal markings, weathered and battle-ready" },
171
+ { id: "barbarian-woman", label: "Barbarian (Female)", group: "Fantasy", dimension: "type", description: "Strong female fur-clad warrior", promptHint: "a female barbarian — strong woman warrior in furs and tribal markings, weathered and battle-ready" },
172
+ { id: "barbarian", label: "Barbarian", group: "Fantasy", dimension: "type", description: "Brawny fur-clad warrior", promptHint: "a barbarian — brawny warrior in furs and tribal markings, weathered and battle-ready" },
173
+ { id: "druid", label: "Druid", group: "Fantasy", dimension: "type", description: "Nature-bound robed mystic", promptHint: "a druid — robed nature-binder wreathed in foliage, antler accents and earthy ornaments" },
174
+ { id: "ranger", label: "Ranger", group: "Fantasy", dimension: "type", description: "Cloaked archer / wilderness scout", promptHint: "a ranger — cloaked archer in muted leather and forest greens, alert wilderness bearing" },
175
+ { id: "necromancer", label: "Necromancer", group: "Fantasy", dimension: "type", description: "Pale dark-arts spellcaster", promptHint: "a necromancer — pale robed figure with dark sigils and a corpse-cold aura" },
176
+ { id: "prince", label: "Prince", group: "Fantasy", dimension: "type", description: "Royal young man", promptHint: "a prince — royal young man in tailored regalia with a poised, courtly bearing" },
177
+ { id: "princess", label: "Princess", group: "Fantasy", dimension: "type", description: "Royal young woman", promptHint: "a princess — royal young woman in flowing regalia with a poised, courtly bearing" },
178
+
179
+ // ----- Warriors / Martial -----
180
+ { id: "ninja", label: "Ninja", group: "Warriors / Martial", dimension: "type", description: "Stealth-clad shinobi assassin", promptHint: "a ninja — masked shinobi in a black hooded shozoku with face wrap, lean athletic build, twin sword on the back and a stealthy combat-ready stance" },
181
+ { id: "kunoichi", label: "Kunoichi", group: "Warriors / Martial", dimension: "type", description: "Female ninja", promptHint: "a kunoichi — female ninja in a fitted black hooded shozoku with face wrap, lean athletic build and a stealthy combat-ready stance" },
182
+ { id: "samurai-man", label: "Samurai (Male)", group: "Warriors / Martial", dimension: "type", description: "Male Japanese armoured warrior", promptHint: "a male samurai — Japanese warrior in lacquered lamellar yoroi armour with shoulder guards, kabuto helmet, top-knot under the helmet, katana at the hip and a disciplined battle-ready stance" },
183
+ { id: "samurai-woman", label: "Samurai (Female)", group: "Warriors / Martial", dimension: "type", description: "Female samurai (onna-musha)", promptHint: "a female samurai — onna-musha warrior in lacquered lamellar yoroi armour with shoulder guards, hair tied up, naginata or katana in hand and a disciplined battle-ready stance" },
184
+ { id: "samurai", label: "Samurai", group: "Warriors / Martial", dimension: "type", description: "Japanese armoured warrior", promptHint: "a samurai — Japanese warrior in lacquered lamellar yoroi armour with shoulder guards, kabuto helmet, katana at the hip and a disciplined battle-ready stance" },
185
+ { id: "karate-fighter", label: "Karate Fighter", group: "Warriors / Martial", dimension: "type", description: "Karate practitioner in gi", promptHint: "a karate practitioner in a crisp white karate gi tied with a black belt, barefoot, in a focused fighting stance" },
186
+ { id: "kung-fu-master", label: "Kung Fu Master", group: "Warriors / Martial", dimension: "type", description: "Chinese martial-arts adept", promptHint: "a kung fu master in a flowing silk changshan tunic with frog-button closures, slippers and a poised martial stance, decades-of-training discipline" },
187
+ { id: "shaolin-monk", label: "Shaolin Monk", group: "Warriors / Martial", dimension: "type", description: "Buddhist warrior monk", promptHint: "a Shaolin warrior monk with a shaved head, ochre and saffron robes wrapped over the shoulder, prayer beads, calloused hands and a centred meditative martial bearing" },
188
+ { id: "sumo-wrestler", label: "Sumo Wrestler", group: "Warriors / Martial", dimension: "type", description: "Japanese sumo competitor", promptHint: "a sumo wrestler — heavy-set Japanese fighter in a thick mawashi loincloth, top-knot chonmage hairstyle, broad imposing build planted in a low ready stance" },
189
+ { id: "gladiator", label: "Gladiator", group: "Warriors / Martial", dimension: "type", description: "Roman arena fighter", promptHint: "a gladiator — Roman arena fighter in a leather subligaculum, manica arm guard, greaves and a galea helmet, gladius and scutum shield, scarred and battle-hardened" },
190
+ { id: "spartan-warrior", label: "Spartan Warrior", group: "Warriors / Martial", dimension: "type", description: "Ancient Greek hoplite", promptHint: "a Spartan hoplite — bronze Corinthian helmet with horsehair crest, crimson cloak, round aspis shield, dory spear, leather kilt and greaves, disciplined warrior bearing" },
191
+ { id: "viking-warrior", label: "Viking Warrior", group: "Warriors / Martial", dimension: "type", description: "Norse raider", promptHint: "a Viking warrior — bearded Norse raider in a chainmail hauberk over a leather tunic, fur cloak, round wooden shield with iron boss, axe in hand and a horned-or-conical iron helm" },
192
+ { id: "mongol-warrior", label: "Mongol Warrior", group: "Warriors / Martial", dimension: "type", description: "Steppe horse archer", promptHint: "a Mongol warrior — steppe horse archer in lamellar leather armour over a quilted coat, fur-trimmed conical helmet, recurve bow and quiver, weathered nomadic bearing" },
193
+ { id: "centurion", label: "Centurion", group: "Warriors / Martial", dimension: "type", description: "Roman legionary officer", promptHint: "a Roman centurion in segmented lorica armour, transverse-crested galea helmet, red tunic and cape, gladius at the hip, vine staff in hand and a commanding bearing" },
194
+ { id: "crusader", label: "Crusader Knight", group: "Warriors / Martial", dimension: "type", description: "Medieval crusader", promptHint: "a Crusader knight — chainmail hauberk under a white surcoat with a red cross emblem, conical helm, longsword and kite shield, devout warrior bearing" },
195
+ { id: "pirate", label: "Pirate", group: "Warriors / Martial", dimension: "type", description: "Golden-age pirate / buccaneer", promptHint: "a pirate — Golden-Age buccaneer in a tricorn hat, weathered long coat, cutlass at the hip, flintlock pistol tucked in the belt, eyepatch or beaded beard, salty seafarer's swagger" },
196
+ { id: "musketeer", label: "Musketeer", group: "Warriors / Martial", dimension: "type", description: "17th-century French swordsman", promptHint: "a 17th-century French musketeer in a wide-brimmed feathered hat, blue tabard with white cross, leather doublet, rapier at the hip, dashing swordsman's bearing" },
197
+
198
+ // ----- Mythic / Divine -----
199
+ { id: "angel", label: "Angel", group: "Mythic / Divine", dimension: "type", description: "Winged divine humanoid", promptHint: "an angel — humanoid with luminous feathered wings, a soft halo of light and a serene divine presence" },
200
+ { id: "demon", label: "Demon", group: "Mythic / Divine", dimension: "type", description: "Horned infernal humanoid", promptHint: "a demon — horned humanoid with red or charcoal skin, infernal eyes and a smouldering aura" },
201
+ { id: "ghost", label: "Ghost", group: "Mythic / Divine", dimension: "type", description: "Translucent spectral figure", promptHint: "a ghost — translucent humanoid figure with a faint pale glow and a haunting drifting silhouette" },
202
+ { id: "valkyrie", label: "Valkyrie", group: "Mythic / Divine", dimension: "type", description: "Norse warrior-spirit woman", promptHint: "a valkyrie — armoured warrior woman with metal-feathered wings and a shield-maiden's bearing" },
203
+ { id: "god", label: "God", group: "Mythic / Divine", dimension: "type", description: "Male deity", promptHint: "a god — male deity with a radiant divine aura, idealised features and an aura of immense power" },
204
+ { id: "goddess", label: "Goddess", group: "Mythic / Divine", dimension: "type", description: "Female deity", promptHint: "a goddess — female deity with a radiant divine aura, idealised features and an aura of immense power" },
205
+
206
+ // ----- Sci-Fi -----
207
+ { id: "android-man", label: "Android (Male)", group: "Sci-Fi", dimension: "type", description: "Masculine humanoid robot", promptHint: "an android — humanoid robot with a masculine sculpt, smooth synthetic skin and visible mechanical seams" },
208
+ { id: "android-woman", label: "Android (Female)", group: "Sci-Fi", dimension: "type", description: "Feminine humanoid robot", promptHint: "an android — humanoid robot with a feminine sculpt, smooth synthetic skin and visible mechanical seams" },
209
+ { id: "android-neutral", label: "Android", group: "Sci-Fi", dimension: "type", description: "Humanoid robot, gender unspecified", promptHint: "an androgynous android — humanoid robot with a neutral sculpt and exposed precision servos" },
210
+ { id: "cyborg-man", label: "Cyborg (Male)", group: "Sci-Fi", dimension: "type", description: "Masculine human-machine hybrid", promptHint: "a male cyborg — masculine human body fused with mechanical augmentations, cybernetic limbs and partial exposed circuitry" },
211
+ { id: "cyborg-woman", label: "Cyborg (Female)", group: "Sci-Fi", dimension: "type", description: "Feminine human-machine hybrid", promptHint: "a female cyborg — feminine human body fused with mechanical augmentations, cybernetic limbs and partial exposed circuitry" },
212
+ { id: "cyborg", label: "Cyborg", group: "Sci-Fi", dimension: "type", description: "Human-machine hybrid", promptHint: "a cyborg — human body fused with mechanical augmentations, cybernetic limbs and partial exposed circuitry" },
213
+ { id: "robot", label: "Robot", group: "Sci-Fi", dimension: "type", description: "Fully mechanical humanoid", promptHint: "a robot — fully mechanical humanoid with metal plating, articulated joints and glowing optical sensors" },
214
+ { id: "alien", label: "Alien", group: "Sci-Fi", dimension: "type", description: "Non-human humanoid", promptHint: "an alien — non-human humanoid with otherworldly facial features, unusual skin tone and unfamiliar proportions" },
215
+ { id: "astronaut", label: "Astronaut", group: "Sci-Fi", dimension: "type", description: "Suited space explorer", promptHint: "an astronaut in a sealed pressure suit and reflective helmet visor with mission patches across the chest" },
216
+ { id: "space-marine", label: "Space Marine", group: "Sci-Fi", dimension: "type", description: "Power-armoured soldier", promptHint: "a space marine in heavy power armour with a sealed helmet, reinforced shoulder pauldrons and a slung weapon harness" },
217
+ { id: "ai-hologram", label: "AI Hologram", group: "Sci-Fi", dimension: "type", description: "Volumetric light figure", promptHint: "an AI hologram — humanoid figure rendered as translucent volumetric light with subtle scanline shimmer" },
218
+
219
+ // ----- Heroes & Villains -----
220
+ { id: "superhero", label: "Superhero", group: "Heroes & Villains", dimension: "type", description: "Masked caped male hero", promptHint: "a superhero — masked, caped, in a fitted spandex suit with a chest emblem and a heroic ready stance" },
221
+ { id: "superheroine", label: "Superheroine", group: "Heroes & Villains", dimension: "type", description: "Masked caped female hero", promptHint: "a superheroine — masked, caped, in a fitted spandex suit with a chest emblem and a heroic ready stance" },
222
+ { id: "masked-vigilante", label: "Masked Vigilante", group: "Heroes & Villains", dimension: "type", description: "Dark-suited masked hero", promptHint: "a masked vigilante — dark tactical bodysuit with a half-mask covering the eyes, urban gritty aesthetic" },
223
+ { id: "supervillain", label: "Supervillain", group: "Heroes & Villains", dimension: "type", description: "Theatrical evil archetype", promptHint: "a supervillain — theatrical menacing costume with a sinister emblem, dramatic cape and a commanding villain bearing" },
224
+ { id: "caped-crusader-man", label: "Caped Crusader (Male)", group: "Heroes & Villains", dimension: "type", description: "Armored cowled male dark hero", promptHint: "a male caped crusader — armoured cowled masculine hero in dark colors with reinforced gauntlets, utility belt and a heavy cape" },
225
+ { id: "caped-crusader-woman", label: "Caped Crusader (Female)", group: "Heroes & Villains", dimension: "type", description: "Armored cowled female dark hero", promptHint: "a female caped crusader — armoured cowled feminine hero in dark colors with reinforced gauntlets, utility belt and a heavy cape" },
226
+ { id: "caped-crusader", label: "Caped Crusader", group: "Heroes & Villains", dimension: "type", description: "Armored cowled dark hero", promptHint: "a caped crusader — armoured cowled hero in dark colors with reinforced gauntlets, utility belt and a heavy cape" },
227
+ { id: "super-soldier", label: "Super-Soldier", group: "Heroes & Villains", dimension: "type", description: "Patriotic exo-armoured warrior", promptHint: "a super-soldier — military-grade exo-armour with patriotic markings, reinforced helm and a star-spangled emblem" },
228
+ { id: "mutant", label: "Mutant", group: "Heroes & Villains", dimension: "type", description: "Human with one or two visible mutations", promptHint: "a mutant — human-baseline body with one or two visible mutations such as unusual skin pigmentation, glowing eyes, claws or gills" },
229
+ { id: "magical-girl", label: "Magical Girl", group: "Heroes & Villains", dimension: "type", description: "Transforming-heroine archetype", promptHint: "a magical girl — transforming heroine in a frilled costume with sparkles, ribbons, ornamented accessories and a transformation aura" },
230
+
231
+ // ----- Professions / Roles -----
232
+ { id: "police-officer", label: "Police Officer", group: "Professions / Roles", dimension: "type", description: "Uniformed officer of the law", promptHint: "a police officer in a navy-blue uniform with badge, duty belt with holstered sidearm and radio, peaked cap or service hat, professional law-enforcement bearing" },
233
+ { id: "policeman", label: "Policeman", group: "Professions / Roles", dimension: "type", description: "Male police officer", promptHint: "a male police officer in a navy-blue uniform with badge, duty belt with holstered sidearm, peaked cap, professional law-enforcement bearing" },
234
+ { id: "policewoman", label: "Policewoman", group: "Professions / Roles", dimension: "type", description: "Female police officer", promptHint: "a female police officer in a navy-blue uniform with badge, duty belt with holstered sidearm, hair tucked under a service cap, professional law-enforcement bearing" },
235
+ { id: "firefighter", label: "Firefighter", group: "Professions / Roles", dimension: "type", description: "Bunker-gear emergency responder", promptHint: "a firefighter in heavy yellow-and-tan turnout gear with reflective stripes, helmet with visor, suspenders and rubber boots, soot-streaked and ready for action" },
236
+ { id: "doctor", label: "Doctor", group: "Professions / Roles", dimension: "type", description: "Physician in white coat", promptHint: "a doctor in a white medical coat over scrubs with a stethoscope around the neck, professional clinical bearing" },
237
+ { id: "surgeon", label: "Surgeon", group: "Professions / Roles", dimension: "type", description: "Operating-room surgeon", promptHint: "a surgeon in teal or blue surgical scrubs with a surgical mask pulled down to the neck, scrub cap and gloved hands, focused operating-room bearing" },
238
+ { id: "nurse", label: "Nurse", group: "Professions / Roles", dimension: "type", description: "Hospital nurse in scrubs", promptHint: "a nurse in coloured medical scrubs with an ID badge, stethoscope around the neck and a warm professional caregiver bearing" },
239
+ { id: "chef", label: "Chef", group: "Professions / Roles", dimension: "type", description: "Kitchen-uniformed cook", promptHint: "a chef in a crisp white double-breasted chef's coat with a tall white toque hat, knotted neckerchief and an apron, focused culinary bearing" },
240
+ { id: "teacher", label: "Teacher", group: "Professions / Roles", dimension: "type", description: "Classroom educator", promptHint: "a teacher in smart-casual professional attire — a button-down or blouse with a cardigan or blazer, holding a book or marker, warm patient educator bearing" },
241
+ { id: "priest", label: "Priest", group: "Professions / Roles", dimension: "type", description: "Catholic clergyman", promptHint: "a priest in a black cassock with a white Roman collar, hands folded, calm devout clerical bearing" },
242
+ { id: "nun", label: "Nun", group: "Professions / Roles", dimension: "type", description: "Catholic woman religious", promptHint: "a nun in a black habit and white wimple framing the face, scapular and rosary at the waist, hands folded in serene devout bearing" },
243
+ { id: "soldier", label: "Soldier", group: "Professions / Roles", dimension: "type", description: "Modern military service member", promptHint: "a soldier in modern camouflage combat fatigues with body armour, helmet, slung rifle and tactical gear, disciplined military bearing" },
244
+ { id: "lawyer", label: "Lawyer", group: "Professions / Roles", dimension: "type", description: "Suited attorney", promptHint: "a lawyer in a sharp tailored business suit with a dress shirt and tie or blouse, holding a leather portfolio, polished professional courtroom bearing" },
245
+ { id: "judge", label: "Judge", group: "Professions / Roles", dimension: "type", description: "Robed judicial officer", promptHint: "a judge in a long black judicial robe over a shirt and collar, gavel in hand, an authoritative impartial courtroom bearing" },
246
+ { id: "scientist", label: "Scientist", group: "Professions / Roles", dimension: "type", description: "Lab-coated researcher", promptHint: "a scientist in a white lab coat with safety goggles around the neck, holding a clipboard or beaker, focused inquisitive research bearing" },
247
+ { id: "pilot", label: "Pilot", group: "Professions / Roles", dimension: "type", description: "Commercial-airline pilot", promptHint: "a commercial airline pilot in a navy-blue uniform with gold stripes on the sleeves, peaked cap with airline insignia, white shirt and black tie, confident captain's bearing" },
248
+ { id: "flight-attendant", label: "Flight Attendant", group: "Professions / Roles", dimension: "type", description: "Cabin-crew member", promptHint: "a flight attendant in a tailored airline uniform with neck scarf or tie, name tag and wings pin, polished hospitable cabin-crew bearing" },
249
+ { id: "construction-worker", label: "Construction Worker", group: "Professions / Roles", dimension: "type", description: "Hard-hatted builder", promptHint: "a construction worker in a high-visibility safety vest over a work shirt, hard hat, work jeans and steel-toed boots, tool belt at the waist, hands-on builder bearing" },
250
+ { id: "businessman", label: "Businessman", group: "Professions / Roles", dimension: "type", description: "Suited corporate man", promptHint: "a businessman in a sharp tailored two-piece suit with a crisp dress shirt and tie, polished leather shoes, briefcase in hand, confident corporate bearing" },
251
+ { id: "businesswoman", label: "Businesswoman", group: "Professions / Roles", dimension: "type", description: "Suited corporate woman", promptHint: "a businesswoman in a tailored skirt or trouser suit with a blouse, heels or smart flats, holding a portfolio, confident corporate bearing" },
252
+ { id: "farmer", label: "Farmer", group: "Professions / Roles", dimension: "type", description: "Agricultural worker", promptHint: "a farmer in worn denim overalls over a flannel shirt, a wide-brimmed straw or trucker hat, work boots and weathered hands, rustic agricultural bearing" },
253
+ { id: "mechanic", label: "Mechanic", group: "Professions / Roles", dimension: "type", description: "Auto / industrial mechanic", promptHint: "a mechanic in oil-stained navy coveralls or work shirt with rolled sleeves, grease-streaked hands, a wrench in hand and a name patch on the chest" },
254
+ { id: "artist", label: "Artist", group: "Professions / Roles", dimension: "type", description: "Painter / visual artist", promptHint: "an artist in paint-spattered casual clothing with rolled-up sleeves, brush or palette in hand, an inquisitive creative bearing" },
255
+ { id: "musician", label: "Musician", group: "Professions / Roles", dimension: "type", description: "Performer with instrument", promptHint: "a musician holding their instrument in a stylish performance outfit, expressive performer's bearing" },
256
+ { id: "athlete", label: "Athlete", group: "Professions / Roles", dimension: "type", description: "Pro-sport competitor", promptHint: "an athlete in technical performance sportswear with a defined athletic build, focused competitive bearing" },
257
+ { id: "detective", label: "Detective", group: "Professions / Roles", dimension: "type", description: "Plain-clothes investigator", promptHint: "a plain-clothes detective in a rumpled trench coat or sport jacket over a dress shirt, badge clipped to the belt, sharp investigative bearing" },
258
+ { id: "journalist", label: "Journalist", group: "Professions / Roles", dimension: "type", description: "Reporter with notebook / camera", promptHint: "a journalist with a press lanyard, notebook and pen or microphone in hand, smart-casual professional attire, alert reporter's bearing" },
259
+ { id: "barista", label: "Barista", group: "Professions / Roles", dimension: "type", description: "Coffee-shop server", promptHint: "a barista in a dark apron over a casual t-shirt, name tag, behind-the-counter bearing with a friendly café-staff vibe" },
260
+
261
+ // ----- Hybrid / Anthro -----
262
+ // Person archetypes — visual style (anime, realistic, painterly…) is
263
+ // chosen separately via the Style node, so prompts here describe the
264
+ // creature without baking in any specific art style.
265
+ { id: "catgirl", label: "Catgirl", group: "Hybrid / Anthro", dimension: "type", description: "Woman with cat ears and tail", promptHint: "a catgirl — humanoid woman with cat ears on her head and a soft cat tail" },
266
+ { id: "catboy", label: "Catboy", group: "Hybrid / Anthro", dimension: "type", description: "Man with cat ears and tail", promptHint: "a catboy — humanoid man with cat ears on his head and a soft cat tail" },
267
+ { id: "anthro", label: "Anthropomorphic", group: "Hybrid / Anthro", dimension: "type", description: "Bipedal animal-headed humanoid", promptHint: "an anthropomorphic animal humanoid — bipedal body in human stance with an animal head, fur and animal tail" },
268
+
269
+ // ----- Iconic / Public Domain -----
270
+ // Only public-domain figures — modern trademarked characters (Superman,
271
+ // Spider-Man, Yoda, Mickey, etc.) are deliberately NOT shipped as named
272
+ // entries. Users wanting them can type the name into the prompt; we don't
273
+ // ship a button.
274
+ { id: "sherlock-holmes", label: "Sherlock Holmes", group: "Iconic / Public Domain", dimension: "type", description: "Victorian consulting detective", promptHint: "Sherlock Holmes — Victorian consulting detective in a tweed deerstalker hat and Inverness cape, pipe in hand, sharp analytical gaze" },
275
+ { id: "dracula", label: "Count Dracula", group: "Iconic / Public Domain", dimension: "type", description: "Stoker's gothic vampire count", promptHint: "Count Dracula — gothic vampire count in formal black evening wear with a long opera cape, pale aristocratic features and pronounced fangs" },
276
+ { id: "frankenstein-monster", label: "Frankenstein's Monster", group: "Iconic / Public Domain", dimension: "type", description: "Shelley's reanimated creature", promptHint: "Frankenstein's Monster — tall reanimated humanoid with a flat-topped head, bolts in the neck, sutured greenish skin and a heavy stooped frame" },
277
+ { id: "phantom-opera", label: "Phantom of the Opera", group: "Iconic / Public Domain", dimension: "type", description: "Leroux's masked operatic recluse", promptHint: "the Phantom of the Opera — masked operatic figure in formal evening wear with a stark white half-mask covering one side of the face" },
278
+ { id: "mr-hyde", label: "Mr Hyde", group: "Iconic / Public Domain", dimension: "type", description: "Stevenson's monstrous alter ego", promptHint: "Mr Hyde — bestial transformed alter ego with a hunched ape-like frame, brutish features and a sneer of malevolent glee" },
279
+ { id: "king-arthur", label: "King Arthur", group: "Iconic / Public Domain", dimension: "type", description: "Legendary medieval king", promptHint: "King Arthur — armoured medieval king in plate and chainmail with a regal crown, the legendary sword Excalibur at his side" },
280
+ { id: "merlin", label: "Merlin", group: "Iconic / Public Domain", dimension: "type", description: "Arthurian wizard advisor", promptHint: "Merlin — Arthurian wizard with a long flowing white beard, deep blue robes embroidered with stars, a tall pointed hat and a gnarled wooden staff" },
281
+ { id: "robin-hood", label: "Robin Hood", group: "Iconic / Public Domain", dimension: "type", description: "Forest outlaw archer", promptHint: "Robin Hood — forest outlaw archer in green tunic and hood with a longbow and quiver of arrows, leather boots and a roguish grin" },
282
+ { id: "joan-of-arc", label: "Joan of Arc", group: "Iconic / Public Domain", dimension: "type", description: "Medieval warrior-maid in armour", promptHint: "Joan of Arc — medieval warrior-maid in shining plate armour over a white surcoat, holding a banner, short cropped hair and a determined holy expression" },
283
+ { id: "cleopatra", label: "Cleopatra", group: "Iconic / Public Domain", dimension: "type", description: "Last pharaoh of Egypt", promptHint: "Cleopatra — Egyptian pharaoh-queen in pleated white linen and a broad gold collar, kohl-lined almond eyes, a black bobbed wig and a golden uraeus diadem" },
284
+ { id: "captain-nemo", label: "Captain Nemo", group: "Iconic / Public Domain", dimension: "type", description: "Verne's submarine captain", promptHint: "Captain Nemo — 19th-century submarine captain in a dark double-breasted naval coat, peaked cap and a stern mariner's bearing" },
285
+ { id: "alice-wonderland", label: "Alice (Wonderland)", group: "Iconic / Public Domain", dimension: "type", description: "Carroll's Wonderland girl", promptHint: "Alice from Wonderland — young girl in a blue dress with a white pinafore apron, long blonde hair tied back with a black bow and white knee-high stockings" },
286
+ { id: "peter-pan", label: "Peter Pan", group: "Iconic / Public Domain", dimension: "type", description: "Boy who never grew up", promptHint: "Peter Pan — boyish figure in a green tunic and tights with a pointed feathered cap, mischievous grin and a youthful adventurous bearing" },
287
+ { id: "dorothy-oz", label: "Dorothy (Oz)", group: "Iconic / Public Domain", dimension: "type", description: "Oz girl in blue gingham", promptHint: "Dorothy from Oz — young girl in a blue and white gingham dress with brown pigtail braids tied with ribbons and ruby slippers" },
288
+ { id: "tin-man", label: "Tin Man", group: "Iconic / Public Domain", dimension: "type", description: "Oz tin woodsman", promptHint: "the Tin Man from Oz — humanoid figure made of riveted tin plates with a funnel-shaped hat, an axe in hand and an oil-can at the belt" },
289
+ { id: "scarecrow-oz", label: "Scarecrow (Oz)", group: "Iconic / Public Domain", dimension: "type", description: "Oz straw scarecrow", promptHint: "the Scarecrow from Oz — straw-stuffed humanoid in a ragged farmer's outfit and floppy pointed hat with a painted burlap face and patched clothes" },
290
+ { id: "cowardly-lion", label: "Cowardly Lion", group: "Iconic / Public Domain", dimension: "type", description: "Oz anthropomorphic lion", promptHint: "the Cowardly Lion from Oz — anthropomorphic bipedal lion with a thick mane, expressive humanlike face and a fretful, timid posture" },
291
+ { id: "santa-claus", label: "Santa Claus", group: "Iconic / Public Domain", dimension: "type", description: "Christmas gift-giver", promptHint: "Santa Claus — jolly bearded old man in a red coat trimmed with white fur, a wide black belt, red trousers and tall black boots, carrying a sack of toys" },
292
+ { id: "grim-reaper", label: "Grim Reaper", group: "Iconic / Public Domain", dimension: "type", description: "Personification of Death", promptHint: "the Grim Reaper — tall hooded skeletal figure in a tattered black robe, bony hands gripping a long wooden scythe, face hidden in shadow" },
293
+
294
+ // -------------------- Age --------------------
295
+ // Young ages get finer splits — a 2yo, 5yo, and 8yo look very different;
296
+ // 14yo vs 18yo vs 22yo vs 28yo all read distinct on screen. From 30 onward
297
+ // a single decade bucket reads close enough. Custom age lets the user
298
+ // dial in any specific number for full control.
299
+ { id: "age-baby", label: "Baby", dimension: "age", description: "Newborn / infant under 1", promptHint: "a baby under 1 year old" },
300
+ { id: "age-toddler", label: "Toddler", dimension: "age", description: "Around 2-3 years old", promptHint: "a toddler around 2-3 years old" },
301
+ { id: "age-young-child", label: "Young Child", dimension: "age", description: "Around 4-6 years old", promptHint: "a young child around 5 years old" },
302
+ { id: "age-child", label: "Child", dimension: "age", description: "Around 7-9 years old", promptHint: "a child around 8 years old" },
303
+ { id: "age-pre-teen", label: "Pre-Teen", dimension: "age", description: "Around 10-12 years old", promptHint: "a pre-teen around 11 years old" },
304
+ { id: "age-early-teen", label: "Early Teen", dimension: "age", description: "Around 13-15 years old", promptHint: "in their early teens, around 14 years old" },
305
+ { id: "age-late-teen", label: "Late Teen", dimension: "age", description: "Around 16-19 years old", promptHint: "in their late teens, around 17 years old" },
306
+ { id: "age-teen", label: "Teenager (any)", dimension: "age", description: "Anywhere in 13-19", promptHint: "a teenager, somewhere in their teens" },
307
+ { id: "age-early-20s", label: "Early 20s", dimension: "age", description: "Around 20-23 years old", promptHint: "in their early 20s, around 21 years old" },
308
+ { id: "age-late-20s", label: "Late 20s", dimension: "age", description: "Around 24-29 years old", promptHint: "in their late 20s, around 27 years old" },
309
+ { id: "age-20s", label: "20s (any)", dimension: "age", description: "Anywhere in their twenties", promptHint: "in their 20s" },
310
+ { id: "age-30s", label: "30s", dimension: "age", description: "Thirties", promptHint: "in their 30s" },
311
+ { id: "age-40s", label: "40s", dimension: "age", description: "Forties", promptHint: "in their 40s" },
312
+ { id: "age-50s", label: "50s", dimension: "age", description: "Fifties", promptHint: "in their 50s" },
313
+ { id: "age-60s", label: "60s", dimension: "age", description: "Sixties", promptHint: "in their 60s" },
314
+ { id: "age-elderly", label: "Elderly", dimension: "age", description: "70 and older", promptHint: "elderly, in their 70s or older" },
315
+ { id: "age-custom", label: "Custom age...", dimension: "age", description: "Specify an exact age in years", promptHint: "" },
316
+
317
+ // -------------------- Ethnicity --------------------
318
+ // Asian (generic + East / South / Southeast breakouts + national-level splits)
319
+ { id: "asian-any", label: "Asian (any)", shortLabel: "any", group: "Asian", dimension: "ethnicity", description: "Any Asian — unspecified region", promptHint: "of Asian descent" },
320
+ { id: "east-asian", label: "East Asian (any)", shortLabel: "East (any)", group: "Asian", dimension: "ethnicity", description: "East Asian features (broad)", promptHint: "East Asian" },
321
+ { id: "chinese", label: "Chinese", group: "Asian", dimension: "ethnicity", description: "Han Chinese features", promptHint: "of Chinese descent" },
322
+ { id: "japanese", label: "Japanese", group: "Asian", dimension: "ethnicity", description: "Japanese features", promptHint: "of Japanese descent" },
323
+ { id: "korean", label: "Korean", group: "Asian", dimension: "ethnicity", description: "Korean features", promptHint: "of Korean descent" },
324
+ { id: "mongolian-tibetan", label: "Mongolian / Tibetan", shortLabel: "Mongol / Tibetan", group: "Asian", dimension: "ethnicity", description: "Mongolian / Tibetan features", promptHint: "of Mongolian or Tibetan descent" },
325
+ { id: "south-asian", label: "South Asian / Indian", shortLabel: "South / Indian", group: "Asian", dimension: "ethnicity", description: "Indian / Pakistani / Bangladeshi / Sri Lankan", promptHint: "South Asian" },
326
+ { id: "southeast-asian", label: "Southeast Asian (any)", shortLabel: "Southeast (any)", group: "Asian", dimension: "ethnicity", description: "Southeast Asian (broad)", promptHint: "Southeast Asian" },
327
+ { id: "thai", label: "Thai", group: "Asian", dimension: "ethnicity", description: "Thai features", promptHint: "of Thai descent" },
328
+ { id: "vietnamese", label: "Vietnamese", group: "Asian", dimension: "ethnicity", description: "Vietnamese features", promptHint: "of Vietnamese descent" },
329
+ { id: "indonesian-malay", label: "Indonesian / Malay", shortLabel: "Indo / Malay", group: "Asian", dimension: "ethnicity", description: "Indonesian / Malay features", promptHint: "of Indonesian or Malay descent" },
330
+ { id: "khmer", label: "Khmer / Cambodian", shortLabel: "Khmer", group: "Asian", dimension: "ethnicity", description: "Khmer / Cambodian features", promptHint: "of Khmer Cambodian descent" },
331
+ { id: "filipino", label: "Filipino", group: "Asian", dimension: "ethnicity", description: "Filipino features (Asian + Spanish heritage)", promptHint: "Filipino" },
332
+ // Middle East & North Africa
333
+ { id: "mena-any", label: "Middle Eastern / N. African (any)", shortLabel: "any", group: "Middle East & N. Africa", dimension: "ethnicity", description: "Any MENA — unspecified", promptHint: "of Middle Eastern or North African descent" },
334
+ { id: "middle-eastern", label: "Middle Eastern (Arab)", shortLabel: "Arab", group: "Middle East & N. Africa", dimension: "ethnicity", description: "Arab / Levantine / Gulf features", promptHint: "Middle Eastern" },
335
+ { id: "turkish", label: "Turkish", group: "Middle East & N. Africa", dimension: "ethnicity", description: "Anatolian Turkish features", promptHint: "of Turkish descent" },
336
+ { id: "persian", label: "Persian / Iranian", shortLabel: "Persian", group: "Middle East & N. Africa", dimension: "ethnicity", description: "Indo-Iranian features", promptHint: "of Persian Iranian descent" },
337
+ { id: "israeli", label: "Israeli", group: "Middle East & N. Africa", dimension: "ethnicity", description: "Israeli features (Mediterranean Levantine, broad)", promptHint: "of Israeli descent" },
338
+ // African
339
+ { id: "african", label: "African (any)", shortLabel: "any", group: "African", dimension: "ethnicity", description: "Any Sub-Saharan African — unspecified region", promptHint: "of African descent" },
340
+ { id: "west-african", label: "West African", shortLabel: "West", group: "African", dimension: "ethnicity", description: "Yoruba / Igbo / Akan / Wolof", promptHint: "of West African descent" },
341
+ { id: "east-african", label: "East African", shortLabel: "East", group: "African", dimension: "ethnicity", description: "Somali / Ethiopian / Swahili", promptHint: "of East African descent" },
342
+ { id: "central-african", label: "Central African", shortLabel: "Central", group: "African", dimension: "ethnicity", description: "Bantu / Congolese / Angolan", promptHint: "of Central African descent" },
343
+ { id: "southern-african", label: "Southern African", shortLabel: "Southern", group: "African", dimension: "ethnicity", description: "Zulu / Xhosa / Sotho", promptHint: "of Southern African descent" },
344
+ { id: "afro-caribbean", label: "Afro-Caribbean", group: "African", dimension: "ethnicity", description: "Anglophone Caribbean (Jamaica / Trinidad / Barbados)", promptHint: "of Afro-Caribbean descent" },
345
+ // European
346
+ { id: "european-any", label: "European (any)", shortLabel: "any", group: "European", dimension: "ethnicity", description: "Any European — unspecified region", promptHint: "of European descent" },
347
+ { id: "nordic", label: "Nordic (any)", shortLabel: "Nordic", group: "European", dimension: "ethnicity", description: "Any Scandinavian / Northern European — unspecified region", promptHint: "of Nordic Scandinavian descent" },
348
+ { id: "swedish", label: "Swedish", group: "European", dimension: "ethnicity", description: "Sweden — tall, often blonde, fine features", promptHint: "of Swedish descent" },
349
+ { id: "norwegian", label: "Norwegian", group: "European", dimension: "ethnicity", description: "Norway — fair-skinned, often light-eyed", promptHint: "of Norwegian descent" },
350
+ { id: "danish", label: "Danish", group: "European", dimension: "ethnicity", description: "Denmark — soft features, ash-blonde common", promptHint: "of Danish descent" },
351
+ { id: "finnish", label: "Finnish", group: "European", dimension: "ethnicity", description: "Finland — Uralic features, high cheekbones", promptHint: "of Finnish descent" },
352
+ { id: "icelandic", label: "Icelandic", group: "European", dimension: "ethnicity", description: "Iceland — Norse + Celtic blend, sharp angular features", promptHint: "of Icelandic descent" },
353
+ { id: "celtic", label: "Celtic", group: "European", dimension: "ethnicity", description: "British / Irish features", promptHint: "of Celtic British descent" },
354
+ { id: "mediterranean", label: "Mediterranean", shortLabel: "Mediter.", group: "European", dimension: "ethnicity", description: "Southern European (broad)", promptHint: "of Mediterranean descent" },
355
+ { id: "slavic", label: "Slavic", group: "European", dimension: "ethnicity", description: "Eastern European features", promptHint: "of Slavic Eastern European descent" },
356
+ { id: "czech", label: "Czech", group: "European", dimension: "ethnicity", description: "Czech features (West Slavic, Central European)", promptHint: "of Czech descent" },
357
+ { id: "russian", label: "Russian", group: "European", dimension: "ethnicity", description: "Russian features (East Slavic)", promptHint: "of Russian descent" },
358
+ { id: "ukrainian", label: "Ukrainian", group: "European", dimension: "ethnicity", description: "Ukrainian features (East Slavic)", promptHint: "of Ukrainian descent" },
359
+ { id: "italian", label: "Italian", group: "European", dimension: "ethnicity", description: "Italian features", promptHint: "of Italian descent" },
360
+ { id: "iberian", label: "Iberian (Spanish / Portuguese)", shortLabel: "Iberian", group: "European", dimension: "ethnicity", description: "Spanish / Portuguese with Celtiberian + Moorish heritage", promptHint: "of Iberian Spanish or Portuguese descent" },
361
+ { id: "french", label: "French", group: "European", dimension: "ethnicity", description: "French features (varies north to south)", promptHint: "of French descent" },
362
+ { id: "germanic", label: "Germanic", group: "European", dimension: "ethnicity", description: "German / Austrian / Dutch / Swiss-German", promptHint: "of Germanic descent" },
363
+ { id: "greek", label: "Greek", group: "European", dimension: "ethnicity", description: "Greek features", promptHint: "of Greek descent" },
364
+ { id: "baltic", label: "Baltic", group: "European", dimension: "ethnicity", description: "Lithuanian / Latvian / Estonian", promptHint: "of Baltic descent" },
365
+ // Latin American
366
+ { id: "latin-american", label: "Latin American (any)", shortLabel: "any", group: "Latin American", dimension: "ethnicity", description: "Any Latin American — unspecified region", promptHint: "Latin American" },
367
+ { id: "mexican-mesoamerican", label: "Mexican / Mesoamerican", shortLabel: "Mexican", group: "Latin American", dimension: "ethnicity", description: "Mexican / Guatemalan / Central American Mestizo", promptHint: "of Mexican or Mesoamerican heritage" },
368
+ { id: "caribbean-latino", label: "Caribbean Latin", shortLabel: "Caribbean", group: "Latin American", dimension: "ethnicity", description: "Cuban / Dominican / Puerto Rican", promptHint: "of Caribbean Latin heritage" },
369
+ { id: "brazilian", label: "Brazilian", group: "Latin American", dimension: "ethnicity", description: "Brazilian (Portuguese + African + Indigenous)", promptHint: "of Brazilian heritage" },
370
+ { id: "andean-indigenous", label: "Andean / Indigenous Latin", shortLabel: "Andean", group: "Latin American", dimension: "ethnicity", description: "Quechua / Aymara / Maya / Nahua", promptHint: "of Andean or Indigenous Latin American heritage" },
371
+ { id: "southern-cone", label: "Southern Cone European", shortLabel: "Southern Cone", group: "Latin American", dimension: "ethnicity", description: "Argentinian / Uruguayan / Chilean (largely European descent)", promptHint: "of Southern Cone European descent" },
372
+ // Indigenous
373
+ { id: "indigenous-any", label: "Indigenous (any)", shortLabel: "any", group: "Indigenous", dimension: "ethnicity", description: "Any Indigenous — unspecified group", promptHint: "of Indigenous descent" },
374
+ { id: "native-american", label: "Native American", shortLabel: "Native American", group: "Indigenous", dimension: "ethnicity", description: "Native American / First Nations", promptHint: "Native American" },
375
+ { id: "aboriginal-australian", label: "Aboriginal Australian", shortLabel: "Aboriginal", group: "Indigenous", dimension: "ethnicity", description: "Aboriginal Australian / Torres Strait", promptHint: "Aboriginal Australian" },
376
+ { id: "pacific-islander", label: "Pacific Islander", shortLabel: "Pacific Isl.", group: "Indigenous", dimension: "ethnicity", description: "Polynesian / Maori / Samoan / Hawaiian", promptHint: "Pacific Islander" },
377
+ { id: "inuit", label: "Inuit / Arctic", shortLabel: "Inuit", group: "Indigenous", dimension: "ethnicity", description: "Inuit / Yupik / Aleut", promptHint: "of Inuit Arctic descent" },
378
+ // Other
379
+ { id: "mixed", label: "Mixed", group: "Other", dimension: "ethnicity", description: "Mixed heritage", promptHint: "of mixed heritage" },
380
+
381
+ // -------------------- Body — six independent proportion axes --------------------
382
+ // Frame / Body Mass / Bust / Waist / Hips / Silhouette replace the old single
383
+ // Build + Body Proportions pickers, so otherwise-inexpressible bodies (e.g.
384
+ // "slim frame + full bust") become representable. Neutral values (Average /
385
+ // Balanced) carry an empty promptHint — selecting them injects nothing.
386
+ // ORDER MATTERS: hints compose by comma-join in PERSON_DIMENSION_ORDER, so
387
+ // Frame:Slim ("slim") + Body Mass:Lean ("lean build") → "slim, lean build".
388
+
389
+ // ----- Frame (skeletal width / height) -----
390
+ { id: "frame-petite", label: "Petite", dimension: "frame", description: "Small, short-statured frame", promptHint: "petite" },
391
+ { id: "frame-slim", label: "Slim", dimension: "frame", description: "Narrow, slender frame", promptHint: "slim" },
392
+ { id: "frame-average", label: "Average", dimension: "frame", description: "Average frame", promptHint: "" },
393
+ { id: "frame-broad", label: "Broad", dimension: "frame", description: "Wide, broad-shouldered frame", promptHint: "broad-framed" },
394
+
395
+ // ----- Body Mass (leanness / weight, independent of Frame) -----
396
+ { id: "mass-lean", label: "Lean", dimension: "body-mass", description: "Lean, low body mass", promptHint: "lean build" },
397
+ { id: "mass-average", label: "Average", dimension: "body-mass", description: "Average body mass", promptHint: "" },
398
+ { id: "mass-full", label: "Full", dimension: "body-mass", description: "Full-figured", promptHint: "full-figured" },
399
+ { id: "mass-heavy", label: "Heavy", dimension: "body-mass", description: "Heavy-set, large build", promptHint: "heavy-set build" },
400
+
401
+ // ----- Bust (applicable subjects; ungated like Facial Hair, neutral = no-op) -----
402
+ { id: "bust-small", label: "Small", dimension: "bust", description: "Small bust", promptHint: "small bust" },
403
+ { id: "bust-average", label: "Average", dimension: "bust", description: "Average bust", promptHint: "" },
404
+ { id: "bust-full", label: "Full", dimension: "bust", description: "Full bust", promptHint: "full bust" },
405
+ { id: "bust-very-full", label: "Very Full", dimension: "bust", description: "Very full bust", promptHint: "very full bust" },
406
+
407
+ // ----- Waist -----
408
+ { id: "waist-defined", label: "Defined", dimension: "waist", description: "Defined, cinched waist", promptHint: "defined waist" },
409
+ { id: "waist-average", label: "Average", dimension: "waist", description: "Average waist", promptHint: "" },
410
+ { id: "waist-straight", label: "Straight", dimension: "waist", description: "Straight, undefined waist", promptHint: "straight waistline" },
411
+
412
+ // ----- Hips -----
413
+ { id: "hips-narrow", label: "Narrow", dimension: "hips", description: "Narrow hips", promptHint: "narrow hips" },
414
+ { id: "hips-balanced", label: "Balanced", dimension: "hips", description: "Balanced hips", promptHint: "" },
415
+ { id: "hips-wide", label: "Wide", dimension: "hips", description: "Wide hips", promptHint: "wide hips" },
416
+
417
+ // ----- Silhouette (overall body shape; optional, no neutral) -----
418
+ { id: "silhouette-hourglass", label: "Hourglass", dimension: "silhouette", description: "Balanced bust + hips, defined waist", promptHint: "hourglass silhouette" },
419
+ { id: "silhouette-rectangular", label: "Rectangular", dimension: "silhouette", description: "Straight up-and-down silhouette", promptHint: "rectangular silhouette" },
420
+ { id: "silhouette-pear", label: "Pear", dimension: "silhouette", description: "Hips wider than shoulders", promptHint: "pear-shaped silhouette" },
421
+ { id: "silhouette-inverted", label: "Inverted Triangle", dimension: "silhouette", description: "Broad shoulders, narrow hips", promptHint: "inverted-triangle silhouette" },
422
+ { id: "silhouette-athletic", label: "Athletic", dimension: "silhouette", description: "Lean, toned, V-taper", promptHint: "athletic silhouette" },
423
+
424
+ // -------------------- Face Shape --------------------
425
+ { id: "face-oval", label: "Oval", dimension: "face-shape", description: "Slightly longer than wide, soft curves", promptHint: "an oval face shape" },
426
+ { id: "face-round", label: "Round", dimension: "face-shape", description: "Soft circular face", promptHint: "a round face shape" },
427
+ { id: "face-square", label: "Square", dimension: "face-shape", description: "Strong jaw, equal width forehead and jaw", promptHint: "a square face shape with a strong jaw" },
428
+ { id: "face-heart", label: "Heart", dimension: "face-shape", description: "Wider forehead, pointed chin", promptHint: "a heart-shaped face with a wide forehead and pointed chin" },
429
+ { id: "face-diamond", label: "Diamond", dimension: "face-shape", description: "Narrow forehead and chin, wide cheekbones", promptHint: "a diamond face shape with a narrow forehead and chin and wide cheekbones" },
430
+ { id: "face-oblong", label: "Oblong", dimension: "face-shape", description: "Long and narrow", promptHint: "an oblong face shape, long and narrow" },
431
+ { id: "face-triangular", label: "Triangular", dimension: "face-shape", description: "Narrow forehead, wide jaw", promptHint: "a triangular face shape with a narrow forehead and wide jaw" },
432
+
433
+ // -------------------- Jawline --------------------
434
+ { id: "jaw-strong", label: "Strong", dimension: "jawline", description: "Sharp, defined jawline", promptHint: "a strong, sharply defined jawline" },
435
+ { id: "jaw-soft", label: "Soft", dimension: "jawline", description: "Soft, rounded jaw", promptHint: "a soft rounded jawline" },
436
+ { id: "jaw-pointed", label: "Pointed", dimension: "jawline", description: "Pointed chin, narrow jaw", promptHint: "a pointed chin with a narrow jawline" },
437
+ { id: "jaw-wide", label: "Wide", dimension: "jawline", description: "Wide, broad jaw", promptHint: "a wide broad jaw" },
438
+ { id: "jaw-double", label: "Double Chin", dimension: "jawline", description: "Visible double chin", promptHint: "a visible double chin" },
439
+
440
+ // -------------------- Cheekbones --------------------
441
+ // Neutral-first: "Average" carries an empty promptHint so enabling the
442
+ // dimension injects nothing until a non-neutral value is picked.
443
+ { id: "cheekbones-average", label: "Average", dimension: "cheekbones", description: "Average cheekbone projection (no emphasis)", promptHint: "" },
444
+ { id: "cheekbones-low", label: "Low", dimension: "cheekbones", description: "Low, flat cheekbones", promptHint: "low, flat cheekbones" },
445
+ { id: "cheekbones-high-defined", label: "High & Defined", dimension: "cheekbones", description: "High, clearly defined cheekbones", promptHint: "high, defined cheekbones" },
446
+ { id: "cheekbones-sculpted", label: "Sculpted-High", dimension: "cheekbones", description: "Sharply sculpted high cheekbones", promptHint: "sharply sculpted high cheekbones with strong cheekbone projection" },
447
+ { id: "cheekbones-wide", label: "Wide", dimension: "cheekbones", description: "Wide, prominent cheekbones", promptHint: "wide, prominent cheekbones" },
448
+
449
+ // -------------------- Facial Fullness (face leanness, independent of body Build) --------------------
450
+ { id: "facial-fullness-average", label: "Average", dimension: "facial-fullness", description: "Average facial fullness", promptHint: "" },
451
+ { id: "facial-fullness-gaunt", label: "Gaunt", dimension: "facial-fullness", description: "Hollow cheeks, very low facial fat", promptHint: "a gaunt face with hollow cheeks and very low facial fat" },
452
+ { id: "facial-fullness-lean", label: "Lean", dimension: "facial-fullness", description: "Low facial fat, subtle hollowing under cheekbones", promptHint: "a lean face with low facial fat and subtle hollowing beneath the cheekbones" },
453
+ { id: "facial-fullness-full", label: "Full", dimension: "facial-fullness", description: "Soft, well-padded cheeks", promptHint: "a full face with soft, well-padded cheeks" },
454
+ { id: "facial-fullness-round", label: "Round", dimension: "facial-fullness", description: "Round face, soft rounded cheeks", promptHint: "a round face with soft, rounded cheeks" },
455
+
456
+ // -------------------- Eye Shape (anatomical shape only — lid/tilt/spacing/set split out below) --------------------
457
+ { id: "eye-almond", label: "Almond", dimension: "eye-shape", description: "Almond-shaped eyes", promptHint: "almond-shaped eyes" },
458
+ { id: "eye-round", label: "Round", dimension: "eye-shape", description: "Wide, round eyes", promptHint: "wide round eyes" },
459
+ { id: "eye-monolid", label: "Monolid", dimension: "eye-shape", description: "No visible crease, smooth lid", promptHint: "monolid eyes with no visible crease" },
460
+ { id: "eye-double-eyelid", label: "Double Eyelid", dimension: "eye-shape", description: "Defined upper-eyelid crease", promptHint: "double-eyelid eyes with a clearly defined upper crease" },
461
+ { id: "eye-wide", label: "Wide", dimension: "eye-shape", description: "Wide, large eyes", promptHint: "wide, large eyes" },
462
+ { id: "eye-narrow", label: "Narrow", dimension: "eye-shape", description: "Narrow, elongated eyes", promptHint: "narrow, elongated eyes" },
463
+
464
+ // -------------------- Eyelid Type (upper-lid trait) --------------------
465
+ // `eye-hooded` / `eye-droopy` / `eye-deep-set` keep their original ids
466
+ // (moved out of eye-shape) so legacy data still resolves the same hint.
467
+ { id: "eyelid-standard", label: "Standard", dimension: "eyelid-type", description: "Standard upper eyelid", promptHint: "" },
468
+ { id: "eye-hooded", label: "Hooded", dimension: "eyelid-type", description: "Upper lid partially covers the crease", promptHint: "hooded eyes with the upper lid partially covering the crease" },
469
+ { id: "eye-droopy", label: "Droopy", dimension: "eyelid-type", description: "Upper lids sit low — soft, sleepy gaze", promptHint: "droopy eyes with the upper lids sitting low for a soft, sleepy gaze" },
470
+ { id: "eye-deep-set", label: "Deep-set", dimension: "eyelid-type", description: "Set deeper into the socket", promptHint: "deep-set eyes" },
471
+
472
+ // -------------------- Canthal Tilt (outer-corner angle) --------------------
473
+ { id: "canthal-neutral", label: "Neutral", dimension: "canthal-tilt", description: "Neutral canthal tilt", promptHint: "" },
474
+ { id: "eye-upturned", label: "Upturned", dimension: "canthal-tilt", description: "Outer corners lift up (positive tilt)", promptHint: "upturned eyes with the outer corners lifted, positive canthal tilt" },
475
+ { id: "eye-downturned", label: "Downturned", dimension: "canthal-tilt", description: "Outer corners angle down (negative tilt)", promptHint: "downturned eyes with the outer corners angled down, negative canthal tilt" },
476
+
477
+ // -------------------- Eye Spacing (horizontal distance between eyes) --------------------
478
+ { id: "eye-spacing-average", label: "Average", dimension: "eye-spacing", description: "Average eye spacing", promptHint: "" },
479
+ { id: "eye-close-set", label: "Close-set", dimension: "eye-spacing", description: "Eyes set close together", promptHint: "close-set eyes set close together" },
480
+ { id: "eye-wide-set", label: "Wide-set", dimension: "eye-spacing", description: "Eyes spaced widely apart", promptHint: "wide-set eyes spaced widely apart" },
481
+
482
+ // -------------------- Eye Set / Brow Distance (vertical brow-to-eye gap) --------------------
483
+ { id: "eyeset-average", label: "Average", dimension: "eye-set-brow", description: "Average brow-to-eye distance", promptHint: "" },
484
+ { id: "eyeset-low", label: "Low-set", dimension: "eye-set-brow", description: "Little brow-to-eye distance (hunter eyes)", promptHint: "eyes set close beneath the brow with little brow-to-eye distance, hunter eyes" },
485
+ { id: "eyeset-high", label: "High-set", dimension: "eye-set-brow", description: "Generous brow-to-eye distance (doe eyes)", promptHint: "eyes set well below the brow with generous brow-to-eye distance, doe eyes" },
486
+
487
+ // -------------------- Nose (bridge) --------------------
488
+ { id: "nose-straight", label: "Straight", dimension: "nose", description: "Straight bridge", promptHint: "a straight nose" },
489
+ { id: "nose-aquiline", label: "Aquiline", dimension: "nose", description: "Curved hooked bridge", promptHint: "an aquiline nose with a curved bridge" },
490
+ { id: "nose-roman", label: "Roman", dimension: "nose", description: "Prominent bridge with slight bump", promptHint: "a Roman nose with a prominent bridge" },
491
+ { id: "nose-snub", label: "Snub", dimension: "nose", description: "Short, slightly upturned", promptHint: "a snub nose, short and slightly upturned" },
492
+ { id: "nose-button", label: "Button", dimension: "nose", description: "Small, rounded tip", promptHint: "a small button nose with a rounded tip" },
493
+ { id: "nose-broad", label: "Broad", dimension: "nose", description: "Wide nostrils, broad bridge", promptHint: "a broad nose with wide nostrils" },
494
+ { id: "nose-narrow", label: "Narrow", dimension: "nose", description: "Thin, narrow", promptHint: "a narrow thin nose" },
495
+ { id: "nose-hooked", label: "Hooked", dimension: "nose", description: "Strongly curved hooked tip", promptHint: "a hooked nose with a strongly curved tip" },
496
+
497
+ // -------------------- Nose Tip (rotation / shape of the tip) --------------------
498
+ { id: "nose-tip-natural", label: "Natural", dimension: "nose-tip", description: "Natural nose tip (no emphasis)", promptHint: "" },
499
+ { id: "nose-tip-refined", label: "Refined", dimension: "nose-tip", description: "Refined, narrow tip", promptHint: "a refined, narrow nose tip" },
500
+ { id: "nose-tip-upturned", label: "Upturned", dimension: "nose-tip", description: "Slightly upturned tip", promptHint: "a slightly upturned nose tip" },
501
+ { id: "nose-tip-rounded", label: "Rounded", dimension: "nose-tip", description: "Soft, rounded tip", promptHint: "a soft, rounded nose tip" },
502
+ { id: "nose-tip-drooping", label: "Drooping", dimension: "nose-tip", description: "Slightly drooping, downward tip", promptHint: "a slightly drooping, downward-pointing nose tip" },
503
+
504
+ // -------------------- Lip Fullness --------------------
505
+ // `lips-thin` / `lips-medium` / `lips-full` keep their original ids (moved
506
+ // out of the old combined `lips` dimension) so legacy data resolves the same.
507
+ { id: "lips-thin", label: "Thin", dimension: "lip-fullness", description: "Thin lips", promptHint: "thin lips" },
508
+ { id: "lips-medium", label: "Medium", dimension: "lip-fullness", description: "Average fullness", promptHint: "medium lips of average fullness" },
509
+ { id: "lips-full", label: "Full", dimension: "lip-fullness", description: "Full plump lips", promptHint: "full plump lips" },
510
+ { id: "lips-full-lower", label: "Full Lower Lip", dimension: "lip-fullness", description: "Full lips with a fuller lower lip", promptHint: "full lips with a noticeably fuller lower lip" },
511
+
512
+ // -------------------- Lip Shape --------------------
513
+ { id: "lips-natural", label: "Natural", dimension: "lip-shape", description: "Natural lip shape", promptHint: "" },
514
+ { id: "lips-cupids-bow", label: "Cupid's Bow", dimension: "lip-shape", description: "Pronounced cupid's bow on the upper lip", promptHint: "a pronounced cupid's bow on the upper lip" },
515
+ { id: "lips-wide", label: "Wide", dimension: "lip-shape", description: "Wide mouth shape", promptHint: "a wide mouth" },
516
+ { id: "lips-heart", label: "Heart", dimension: "lip-shape", description: "Heart-shaped lips with a defined cupid's bow", promptHint: "heart-shaped lips with a defined cupid's bow" },
517
+ { id: "lips-small", label: "Small", dimension: "lip-shape", description: "Petite mouth", promptHint: "a small petite mouth" },
518
+
519
+ // -------------------- Lip State (what the lips are doing / wearing) --------------------
520
+ { id: "lip-state-chapped", label: "Chapped", dimension: "lip-state", description: "Cracked, dry, weather-worn lips", promptHint: "with chapped, cracked, weather-worn dry lips" },
521
+ { id: "lip-state-glossy", label: "Glossy", dimension: "lip-state", description: "High-shine, wet-look lips", promptHint: "with high-shine glossy wet-look lips" },
522
+ { id: "lip-state-bare", label: "Bare", dimension: "lip-state", description: "Natural, untreated, no makeup", promptHint: "with bare, natural, untreated lips" },
523
+ { id: "lip-state-bold-red", label: "Bold Red", dimension: "lip-state", description: "Saturated red lipstick statement", promptHint: "with a bold, saturated red lipstick statement" },
524
+ { id: "lip-state-bitten", label: "Bitten", dimension: "lip-state", description: "Slight playful lip-bite, lower lip caught", promptHint: "playfully biting the lower lip" },
525
+ { id: "lip-state-parted", label: "Parted", dimension: "lip-state", description: "Lips slightly parted, breath of air", promptHint: "with lips slightly parted, taking a soft breath" },
526
+ { id: "lip-state-pursed", label: "Pursed", dimension: "lip-state", description: "Lips pressed and pushed forward", promptHint: "with lips pursed, pressed and pushed forward" },
527
+ { id: "lip-state-bold-black", label: "Bold Black", dimension: "lip-state", description: "Saturated black lipstick statement (goth / avant-garde)", promptHint: "with a bold, saturated black lipstick statement, goth / avant-garde" },
528
+ { id: "lip-state-burgundy", label: "Burgundy", dimension: "lip-state", description: "Deep wine-red lipstick", promptHint: "with deep wine-red burgundy lipstick" },
529
+ { id: "lip-state-plum", label: "Plum", dimension: "lip-state", description: "Dark purple-toned lipstick", promptHint: "with dark purple-toned plum lipstick" },
530
+ { id: "lip-state-mauve", label: "Mauve", dimension: "lip-state", description: "Muted mauve / dusty-pink lipstick", promptHint: "with muted mauve, dusty-pink lipstick" },
531
+ { id: "lip-state-pouting", label: "Pouting", dimension: "lip-state", description: "Full pout", promptHint: "with a full pouting expression" },
532
+
533
+ // -------------------- Hair Color --------------------
534
+ // Blonde family (light → warm → cool)
535
+ { id: "hair-platinum", label: "Platinum", dimension: "hair-color", description: "Very pale, icy platinum", promptHint: "platinum blonde hair, very pale and icy" },
536
+ { id: "hair-creamy", label: "Creamy", dimension: "hair-color", description: "Warm creamy / vanilla blonde", promptHint: "creamy vanilla blonde hair" },
537
+ { id: "hair-blonde", label: "Blonde", dimension: "hair-color", description: "Classic blonde hair", promptHint: "blonde hair" },
538
+ { id: "hair-honey", label: "Honey", dimension: "hair-color", description: "Warm honey-gold blonde", promptHint: "honey-gold blonde hair with warm golden tones" },
539
+ { id: "hair-strawberry", label: "Strawberry Blonde", dimension: "hair-color", description: "Blonde with pinkish-red tone", promptHint: "strawberry blonde hair with a pinkish-red tint" },
540
+ { id: "hair-ash-blonde", label: "Ash Blonde", dimension: "hair-color", description: "Cool, ashy blonde", promptHint: "ash blonde hair with cool, muted tones" },
541
+ // Red / warm family
542
+ { id: "hair-ginger", label: "Ginger", dimension: "hair-color", description: "Bright ginger (reddish-orange)", promptHint: "bright ginger hair, reddish-orange" },
543
+ { id: "hair-copper", label: "Copper", dimension: "hair-color", description: "Vibrant copper orange-red", promptHint: "vibrant copper hair with orange-red metallic shine" },
544
+ { id: "hair-red", label: "Red", dimension: "hair-color", description: "Classic red hair", promptHint: "red hair" },
545
+ { id: "hair-auburn", label: "Auburn", dimension: "hair-color", description: "Rich reddish-brown", promptHint: "auburn hair, rich reddish-brown" },
546
+ { id: "hair-burgundy", label: "Burgundy", dimension: "hair-color", description: "Dark wine-red", promptHint: "burgundy hair, deep wine-red" },
547
+ // Brown family (light → dark)
548
+ { id: "hair-light-brown", label: "Light Brown", dimension: "hair-color", description: "Light brown hair", promptHint: "light brown hair" },
549
+ { id: "hair-caramel", label: "Caramel", dimension: "hair-color", description: "Warm caramel brown", promptHint: "caramel brown hair with warm golden undertones" },
550
+ { id: "hair-brown", label: "Brown", dimension: "hair-color", description: "Medium brown hair", promptHint: "brown hair" },
551
+ { id: "hair-chestnut", label: "Chestnut", dimension: "hair-color", description: "Warm reddish-brown chestnut", promptHint: "chestnut brown hair with warm reddish undertones" },
552
+ { id: "hair-chocolate", label: "Chocolate", dimension: "hair-color", description: "Rich dark chocolate brown", promptHint: "rich chocolate brown hair" },
553
+ { id: "hair-dark-brown", label: "Dark Brown", dimension: "hair-color", description: "Dark brown hair", promptHint: "dark brown hair" },
554
+ // Black family
555
+ { id: "hair-black", label: "Black", dimension: "hair-color", description: "Classic black hair", promptHint: "black hair" },
556
+ { id: "hair-jet-black", label: "Jet Black", dimension: "hair-color", description: "Deep black with blue undertones", promptHint: "jet black hair with deep blue-black undertones and glossy shine" },
557
+ // Gray / neutral
558
+ { id: "hair-gray", label: "Gray", dimension: "hair-color", description: "Gray hair", promptHint: "gray hair" },
559
+ { id: "hair-salt-pepper", label: "Salt & Pepper", dimension: "hair-color", description: "Mixed black and gray hair", promptHint: "salt-and-pepper hair" },
560
+ { id: "hair-white", label: "White", dimension: "hair-color", description: "White hair", promptHint: "white hair" },
561
+ // Silver / Metallic
562
+ { id: "hair-silver", label: "Silver", dimension: "hair-color", description: "Bright metallic silver", promptHint: "bright metallic silver hair with a clean reflective sheen" },
563
+ { id: "hair-rose-gold", label: "Rose Gold", dimension: "hair-color", description: "Soft pink-gold metallic", promptHint: "rose gold hair, soft pink-gold metallic warmth" },
564
+ // Dyed (catch-all + specific fantasy / cosplay colors)
565
+ { id: "hair-dyed", label: "Colorful (any)", dimension: "hair-color", description: "Dyed vibrant colors — unspecified", promptHint: "vibrantly dyed colorful hair" },
566
+ { id: "hair-blue", label: "Blue", dimension: "hair-color", description: "Bold blue dyed hair", promptHint: "bold blue dyed hair" },
567
+ { id: "hair-pastel-blue", label: "Pastel Blue", dimension: "hair-color", description: "Soft pastel sky-blue", promptHint: "soft pastel sky-blue hair" },
568
+ { id: "hair-teal", label: "Teal", dimension: "hair-color", description: "Saturated teal blue-green", promptHint: "saturated teal blue-green hair" },
569
+ { id: "hair-mint", label: "Mint", dimension: "hair-color", description: "Pale mint green", promptHint: "pale mint-green hair" },
570
+ { id: "hair-green", label: "Green", dimension: "hair-color", description: "Bold green dyed hair", promptHint: "bold green dyed hair" },
571
+ { id: "hair-lavender", label: "Lavender", dimension: "hair-color", description: "Soft lavender violet", promptHint: "soft lavender violet hair" },
572
+ { id: "hair-purple", label: "Purple", dimension: "hair-color", description: "Bold deep purple", promptHint: "bold deep purple dyed hair" },
573
+ { id: "hair-magenta", label: "Magenta", dimension: "hair-color", description: "Vivid magenta pink-purple", promptHint: "vivid magenta pink-purple hair" },
574
+ { id: "hair-pink", label: "Pink", dimension: "hair-color", description: "Bold pink dyed hair", promptHint: "bold pink dyed hair" },
575
+ { id: "hair-pastel-pink", label: "Pastel Pink", dimension: "hair-color", description: "Soft pastel cotton-candy pink", promptHint: "soft pastel cotton-candy pink hair" },
576
+ { id: "hair-peach", label: "Peach", dimension: "hair-color", description: "Warm peach blonde-pink", promptHint: "warm peach hair, blonde-pink with soft warmth" },
577
+ { id: "hair-mermaid", label: "Mermaid", dimension: "hair-color", description: "Blue-green-purple ocean blend", promptHint: "mermaid hair blending blue, green, and purple in flowing waves" },
578
+ { id: "hair-rainbow", label: "Rainbow", dimension: "hair-color", description: "Multi-color rainbow streaks", promptHint: "rainbow hair with streaks of multiple bright dyed colors" },
579
+
580
+ // -------------------- Hair Style (length + texture) --------------------
581
+ // -------------------- Hair Base (natural texture + length) --------------------
582
+ // Cut/styling choices live in Styling.hair-cut (separate dimension); this is
583
+ // about what hair the character has, not what was done to it today.
584
+ { id: "base-bald", label: "Bald / Shaved", dimension: "hair-base", description: "Bald or fully shaved head", promptHint: "a shaved bald head" },
585
+ { id: "base-buzz", label: "Buzz", dimension: "hair-base", description: "Very short cropped hair", promptHint: "very short buzzed hair" },
586
+ { id: "base-short-straight", label: "Short Straight", dimension: "hair-base", description: "Short, straight texture", promptHint: "short straight hair" },
587
+ { id: "base-short-wavy", label: "Short Wavy", dimension: "hair-base", description: "Short, wavy texture", promptHint: "short wavy hair" },
588
+ { id: "base-short-curly", label: "Short Curly", dimension: "hair-base", description: "Short, curly texture", promptHint: "short curly hair" },
589
+ { id: "base-short-coily", label: "Short Coily", dimension: "hair-base", description: "Short, tightly coiled texture", promptHint: "short tightly coiled hair" },
590
+ { id: "base-medium-straight", label: "Medium Straight", dimension: "hair-base", description: "Shoulder-length, straight", promptHint: "medium-length straight hair" },
591
+ { id: "base-medium-wavy", label: "Medium Wavy", dimension: "hair-base", description: "Shoulder-length, wavy", promptHint: "medium-length wavy hair" },
592
+ { id: "base-medium-curly", label: "Medium Curly", dimension: "hair-base", description: "Shoulder-length, curly", promptHint: "medium-length curly hair" },
593
+ { id: "base-medium-coily", label: "Medium Coily", dimension: "hair-base", description: "Shoulder-length, tightly coiled", promptHint: "medium-length tightly coiled hair" },
594
+ { id: "base-long-straight", label: "Long Straight", dimension: "hair-base", description: "Mid-back, straight", promptHint: "long straight hair" },
595
+ { id: "base-long-wavy", label: "Long Wavy", dimension: "hair-base", description: "Mid-back, wavy", promptHint: "long wavy hair" },
596
+ { id: "base-long-curly", label: "Long Curly", dimension: "hair-base", description: "Mid-back, curly", promptHint: "long curly hair" },
597
+ { id: "base-long-coily", label: "Long Coily", dimension: "hair-base", description: "Mid-back, tightly coiled", promptHint: "long tightly coiled hair" },
598
+ { id: "base-very-long-straight", label: "Very Long Straight", dimension: "hair-base", description: "Waist-length, straight", promptHint: "very long straight hair down to the waist" },
599
+ { id: "base-very-long-wavy", label: "Very Long Wavy", dimension: "hair-base", description: "Waist-length, wavy", promptHint: "very long wavy hair down to the waist" },
600
+ { id: "base-very-long-curly", label: "Very Long Curly", dimension: "hair-base", description: "Waist-length, curly", promptHint: "very long curly hair down to the waist" },
601
+ { id: "base-afro", label: "Afro", dimension: "hair-base", description: "Voluminous rounded afro", promptHint: "voluminous afro hair" },
602
+ { id: "base-thinning", label: "Thinning / Receding", dimension: "hair-base", description: "Visibly thinning or receding hairline", promptHint: "thinning hair with a receding hairline" },
603
+ { id: "base-wet", label: "Wet", dimension: "hair-base", description: "Wet, slick hair", promptHint: "wet, slick hair" },
604
+
605
+ // -------------------- Eyebrows --------------------
606
+ { id: "brows-natural", label: "Natural", dimension: "eyebrows", description: "Natural, untouched brows", promptHint: "natural eyebrows" },
607
+ { id: "brows-thick", label: "Thick / Bushy", dimension: "eyebrows", description: "Thick, bushy brows", promptHint: "thick bushy eyebrows" },
608
+ { id: "brows-thin", label: "Thin / Pencil", dimension: "eyebrows", description: "Thin, pencil-shaped brows", promptHint: "thin pencil-shaped eyebrows" },
609
+ { id: "brows-arched", label: "Arched", dimension: "eyebrows", description: "Strongly arched, dramatic curve", promptHint: "strongly arched eyebrows with a dramatic curve" },
610
+ { id: "brows-straight", label: "Straight", dimension: "eyebrows", description: "Flat, horizontal brows", promptHint: "flat straight eyebrows" },
611
+ { id: "brows-bold", label: "Bold", dimension: "eyebrows", description: "Defined, dark, full brows", promptHint: "bold defined dark full eyebrows" },
612
+ { id: "brows-soft", label: "Soft / Faded", dimension: "eyebrows", description: "Soft, light, faded brows", promptHint: "soft, light, faded eyebrows" },
613
+ { id: "brows-microbladed", label: "Microbladed", dimension: "eyebrows", description: "Drawn / microbladed crisp shape", promptHint: "microbladed crisply drawn eyebrows" },
614
+ { id: "brows-tinted", label: "Tinted", dimension: "eyebrows", description: "Tinted a different color than hair", promptHint: "eyebrows tinted a different color from the hair" },
615
+ { id: "brows-unibrow", label: "Unibrow", dimension: "eyebrows", description: "Connected unibrow", promptHint: "a connected unibrow" },
616
+
617
+ // -------------------- Skin Tone --------------------
618
+ { id: "skin-very-fair", label: "Very Fair", dimension: "skin-tone", description: "Very fair / pale skin", promptHint: "very fair pale skin" },
619
+ { id: "skin-fair", label: "Fair", dimension: "skin-tone", description: "Fair skin", promptHint: "fair skin" },
620
+ { id: "skin-medium", label: "Medium", dimension: "skin-tone", description: "Medium skin tone", promptHint: "medium skin tone" },
621
+ { id: "skin-olive", label: "Olive", dimension: "skin-tone", description: "Olive skin", promptHint: "olive skin" },
622
+ { id: "skin-tan", label: "Tan", dimension: "skin-tone", description: "Tan skin", promptHint: "tan skin" },
623
+ { id: "skin-brown", label: "Brown", dimension: "skin-tone", description: "Brown skin", promptHint: "brown skin" },
624
+ { id: "skin-dark", label: "Dark", dimension: "skin-tone", description: "Dark skin", promptHint: "dark skin" },
625
+ { id: "skin-very-dark", label: "Very Dark / Ebony", dimension: "skin-tone", description: "Very dark, ebony skin tone (deepest end of the scale)", promptHint: "very dark ebony skin, the deepest skin tone" },
626
+
627
+ // -------------------- Eye Color --------------------
628
+ { id: "eyes-brown", label: "Brown", dimension: "eye-color", description: "Brown eyes", promptHint: "brown eyes" },
629
+ { id: "eyes-blue", label: "Blue", dimension: "eye-color", description: "Blue eyes", promptHint: "blue eyes" },
630
+ { id: "eyes-green", label: "Green", dimension: "eye-color", description: "Green eyes", promptHint: "green eyes" },
631
+ { id: "eyes-hazel", label: "Hazel", dimension: "eye-color", description: "Hazel eyes", promptHint: "hazel eyes" },
632
+ { id: "eyes-gray", label: "Gray", dimension: "eye-color", description: "Gray eyes", promptHint: "gray eyes" },
633
+ { id: "eyes-amber", label: "Amber", dimension: "eye-color", description: "Amber eyes", promptHint: "amber eyes" },
634
+ { id: "eyes-violet", label: "Violet", dimension: "eye-color", description: "Rare violet eyes (Liz Taylor)", promptHint: "rare violet eyes with a deep purple iris" },
635
+ { id: "eyes-gold", label: "Gold", dimension: "eye-color", description: "Bright gold iris", promptHint: "bright gold eyes with luminous metallic warmth" },
636
+ { id: "eyes-silver", label: "Silver", dimension: "eye-color", description: "Pale silver-gray iris", promptHint: "pale silver eyes with a luminous reflective sheen" },
637
+ { id: "eyes-red", label: "Red", dimension: "eye-color", description: "Red iris (albinism / fantasy)", promptHint: "red eyes, albinism or fantasy red iris" },
638
+ { id: "eyes-pink", label: "Pink", dimension: "eye-color", description: "Soft pink iris (albino / fantasy)", promptHint: "soft pink eyes, albino or fantasy pink iris" },
639
+ { id: "eyes-turquoise", label: "Turquoise", dimension: "eye-color", description: "Vivid turquoise blue-green iris", promptHint: "vivid turquoise blue-green eyes" },
640
+ { id: "eyes-black", label: "Black", dimension: "eye-color", description: "Truly black iris (deeper than brown)", promptHint: "jet-black eyes with a truly black iris, deeper than brown" },
641
+
642
+ // -------------------- Eye State (what the eyes are doing / where they look) --------------------
643
+ { id: "eye-state-closed", label: "Closed", dimension: "eye-state", description: "Eyes fully closed, peaceful", promptHint: "with eyes fully closed in a peaceful expression" },
644
+ { id: "eye-state-half-lidded", label: "Half-lidded", dimension: "eye-state", description: "Heavy-lidded sleepy gaze", promptHint: "with heavy half-lidded sleepy eyes" },
645
+ { id: "eye-state-wide-eyed", label: "Wide-eyed", dimension: "eye-state", description: "Eyes wide open, alert / surprised", promptHint: "with eyes wide open, alert and surprised" },
646
+ { id: "eye-state-staring-camera", label: "Staring at Camera", dimension: "eye-state", description: "Direct unbroken eye contact with the lens", promptHint: "staring directly at the camera with unbroken eye contact" },
647
+ { id: "eye-state-gazing-away", label: "Gazing Away", dimension: "eye-state", description: "Looking off-camera, contemplative", promptHint: "gazing off-camera with a contemplative expression" },
648
+ { id: "eye-state-gazing-up", label: "Gazing Up", dimension: "eye-state", description: "Eyes raised toward something above", promptHint: "with eyes gazing upward toward something above" },
649
+ { id: "eye-state-gazing-down", label: "Gazing Down", dimension: "eye-state", description: "Eyes downcast", promptHint: "with eyes downcast, gazing softly downward" },
650
+ { id: "eye-state-glassy", label: "Glassy", dimension: "eye-state", description: "Eyes wet / tear-glazed", promptHint: "with glassy, tear-glazed wet eyes" },
651
+
652
+ // -------------------- Facial Hair --------------------
653
+ { id: "face-clean-shaven", label: "Clean-shaven", dimension: "facial-hair", description: "Clean-shaven face", promptHint: "clean-shaven" },
654
+ { id: "face-stubble", label: "Stubble", dimension: "facial-hair", description: "Light stubble", promptHint: "light stubble" },
655
+ { id: "face-mustache", label: "Mustache", dimension: "facial-hair", description: "Mustache", promptHint: "a mustache" },
656
+ { id: "face-goatee", label: "Goatee", dimension: "facial-hair", description: "Goatee", promptHint: "a goatee" },
657
+ { id: "face-short-beard", label: "Short Beard", dimension: "facial-hair", description: "Short trimmed beard", promptHint: "a short trimmed beard" },
658
+ { id: "face-full-beard", label: "Full Beard", dimension: "facial-hair", description: "Thick full beard", promptHint: "a full beard" },
659
+ { id: "face-five-oclock-shadow", label: "Five-O'Clock Shadow", dimension: "facial-hair", description: "Heavier daily growth — denser than light stubble", promptHint: "a five-o'clock shadow, daily growth heavier than light stubble" },
660
+
661
+ // -------------------- Skin Texture --------------------
662
+ { id: "texture-smooth", label: "Smooth", dimension: "skin-texture", description: "Flawless, silky smooth skin", promptHint: "with flawless, silky smooth skin" },
663
+ { id: "texture-wrinkled", label: "Wrinkled", dimension: "skin-texture", description: "Aged, deeply lined skin", promptHint: "with deep wrinkles and aged skin texture" },
664
+ { id: "texture-goosebumps", label: "Goosebumps", dimension: "skin-texture", description: "Raised goosebumps on skin", promptHint: "with goosebumps raised on the skin" },
665
+ { id: "texture-dewy", label: "Dewy", dimension: "skin-texture", description: "Glowing, dewy fresh skin", promptHint: "with dewy, glowing skin and a fresh sheen" },
666
+ { id: "texture-glistening", label: "Glistening", dimension: "skin-texture", description: "Sweat or oil sheen", promptHint: "with glistening skin, sweat or oil catching the light" },
667
+ { id: "texture-weathered", label: "Weathered", dimension: "skin-texture", description: "Sun-aged rough skin", promptHint: "with weathered, sun-worn rough skin" },
668
+ { id: "texture-porcelain", label: "Porcelain", dimension: "skin-texture", description: "Flawless near-translucent porcelain", promptHint: "with flawless, near-translucent porcelain skin" },
669
+ { id: "texture-sun-kissed", label: "Sun-kissed", dimension: "skin-texture", description: "Warm tan with healthy glow", promptHint: "with sun-kissed skin, warmly tanned with a healthy glow" },
670
+ { id: "texture-tanned", label: "Tanned", dimension: "skin-texture", description: "Deeply, evenly tanned skin", promptHint: "with deeply, evenly bronzed tanned skin" },
671
+ { id: "texture-tan-lines", label: "Tan Lines", dimension: "skin-texture", description: "Visible tan lines from swimwear", promptHint: "with visible swimwear tan lines on the skin" },
672
+ { id: "texture-freckled", label: "Freckled", dimension: "skin-texture", description: "Freckles across cheeks and nose", promptHint: "with visible freckles scattered across cheeks and the bridge of the nose" },
673
+ { id: "texture-ruddy", label: "Ruddy", dimension: "skin-texture", description: "Pink-flushed cheeks and nose", promptHint: "with ruddy, warm pink-flushed cheeks and nose" },
674
+ { id: "texture-ashen", label: "Ashen / Pale", dimension: "skin-texture", description: "Cool pale, near-bloodless undertone", promptHint: "with ashen, cool pale, near-bloodless skin" },
675
+ { id: "texture-oily", label: "Oily / Shiny", dimension: "skin-texture", description: "Slight oil sheen on T-zone", promptHint: "with a slight oily sheen across the T-zone" },
676
+ { id: "texture-matte", label: "Matte", dimension: "skin-texture", description: "Poreless matte finish", promptHint: "with a poreless, matte skin finish" },
677
+ { id: "texture-blemished", label: "Blemished", dimension: "skin-texture", description: "Visible blemishes, real-skin imperfections", promptHint: "with visible blemishes and natural real-skin imperfections" },
678
+ { id: "texture-baby-soft", label: "Baby-soft", dimension: "skin-texture", description: "Smooth, fine-pored youthful skin", promptHint: "with baby-soft, fine-pored, youthful smooth skin" },
679
+ { id: "texture-shower-fresh-wet", label: "Shower-Fresh Wet", dimension: "skin-texture", description: "Just-out-of-shower wet skin with water beads", promptHint: "with just-out-of-the-shower wet skin, water beading on the surface and rolling in slow droplets down the curves of the body" },
680
+ { id: "texture-acne-scarred", label: "Acne-scarred", dimension: "skin-texture", description: "Visible acne scarring (distinct from blemished — healed scar pattern)", promptHint: "with visible acne scarring, healed pitted-skin texture and uneven surface from past breakouts" },
681
+
682
+ // -------------------- Distinctive Features --------------------
683
+ { id: "feature-glasses", label: "Glasses", dimension: "distinctive-features", description: "Wears glasses", promptHint: "wearing glasses" },
684
+ { id: "feature-freckles", label: "Freckles", dimension: "distinctive-features", description: "Visible freckles", promptHint: "with freckles" },
685
+ { id: "feature-tattoos", label: "Tattoos", dimension: "distinctive-features", description: "Visible tattoos", promptHint: "with visible tattoos" },
686
+ { id: "feature-scar", label: "Scar", dimension: "distinctive-features", description: "Facial scar", promptHint: "with a facial scar" },
687
+ { id: "feature-dimples", label: "Dimples", dimension: "distinctive-features", description: "Dimpled cheeks", promptHint: "with dimples" },
688
+ { id: "feature-piercing", label: "Piercing", dimension: "distinctive-features", description: "Visible piercing", promptHint: "with a visible piercing" },
689
+ { id: "feature-birthmark", label: "Birthmark", dimension: "distinctive-features", description: "Visible birthmark on face or shoulder", promptHint: "with a visible birthmark on the face or shoulder" },
690
+ { id: "feature-mole", label: "Mole / Beauty Mark", dimension: "distinctive-features", description: "One prominent mole, often near upper lip", promptHint: "with a prominent beauty mark mole near the upper lip" },
691
+ { id: "feature-gap-teeth", label: "Gap Teeth", dimension: "distinctive-features", description: "Gap between front teeth", promptHint: "with a gap between the front teeth" },
692
+ { id: "feature-aegyo-sal", label: "Aegyo Sal", dimension: "distinctive-features", description: "Puffy under-eye fold prized in K-beauty", promptHint: "with aegyo sal, the soft puffy under-eye fold" },
693
+ { id: "feature-eye-bags", label: "Eye Bags", dimension: "distinctive-features", description: "Mild dark under-eye circles", promptHint: "with mild dark circles and under-eye bags" },
694
+ { id: "feature-dimpled-chin", label: "Dimpled Chin", dimension: "distinctive-features", description: "Soft dimple in the chin", promptHint: "with a soft dimple in the chin" },
695
+ { id: "feature-cleft-chin", label: "Cleft Chin", dimension: "distinctive-features", description: "Defined vertical chin cleft", promptHint: "with a defined vertical cleft chin" },
696
+ { id: "feature-heterochromia", label: "Heterochromia", dimension: "distinctive-features", description: "Each iris a different color", promptHint: "with heterochromia, each iris a different color" },
697
+ { id: "feature-vitiligo", label: "Vitiligo", dimension: "distinctive-features", description: "Patches of depigmented skin", promptHint: "with vitiligo, visible patches of depigmented skin" },
698
+ { id: "feature-sleeve-tattoo", label: "Sleeve Tattoo", dimension: "distinctive-features", description: "Full one-arm tattoo sleeve", promptHint: "with a full tattoo sleeve covering one arm in intricate ink" },
699
+ { id: "feature-face-tattoo", label: "Face Tattoo", dimension: "distinctive-features", description: "Small face tattoo near eye or cheekbone", promptHint: "with a small face tattoo near the eye or cheekbone" },
700
+ { id: "feature-hand-tattoos", label: "Hand Tattoos", dimension: "distinctive-features", description: "Knuckle / finger tattoos", promptHint: "with hand tattoos across knuckles and fingers" },
701
+ { id: "feature-back-tattoo", label: "Back Tattoo", dimension: "distinctive-features", description: "Large piece across the back", promptHint: "with a large back-piece tattoo across the upper back" },
702
+ { id: "feature-chest-tattoo", label: "Chest Tattoo", dimension: "distinctive-features", description: "Tattoo across chest / décolletage", promptHint: "with a tattoo across the chest and décolletage" },
703
+ { id: "feature-leg-tattoo", label: "Leg Tattoo", dimension: "distinctive-features", description: "Visible tattoo on leg / calf / thigh", promptHint: "with a visible tattoo on the leg, running across the calf or thigh" },
704
+ { id: "feature-forearm-tattoo", label: "Forearm Tattoo", dimension: "distinctive-features", description: "Tattoo on inner or outer forearm", promptHint: "with a tattoo on the forearm, visible on the inner or outer side" },
705
+ { id: "feature-visible-piercings", label: "Visible Piercings", dimension: "distinctive-features", description: "Multiple visible piercings (septum, nose, lip, multi-ear)", promptHint: "with multiple visible piercings — septum, nose, lip, and multi-ear" },
706
+ { id: "feature-ear-piercings", label: "Ear Piercings", dimension: "distinctive-features", description: "Multiple stacked ear piercings (cartilage, helix, conch)", promptHint: "with multiple stacked ear piercings — cartilage, helix, and conch" },
707
+ { id: "feature-lip-piercing", label: "Lip Piercing", dimension: "distinctive-features", description: "Lip ring / labret stud", promptHint: "with a lip piercing, a small ring or labret stud at the lip" },
708
+ { id: "feature-nostril-piercing", label: "Nostril Piercing", dimension: "distinctive-features", description: "Single nostril stud or ring", promptHint: "with a single nostril piercing, a small stud or ring at the nostril" },
709
+ { id: "feature-bare-shoulders", label: "Bare Shoulders", dimension: "distinctive-features", description: "Bare shoulders exposed", promptHint: "with bare shoulders exposed, the line of the collarbone and shoulder muscles uncovered" },
710
+ { id: "feature-collarbone-visible", label: "Collarbone Visible", dimension: "distinctive-features", description: "Prominent collarbone catching light", promptHint: "with a prominent collarbone clearly defined and catching the light" },
711
+ { id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "with the midriff exposed, a strip of bare stomach visible between the top and the bottom" },
712
+ { id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with a visible navel on a bare stomach" },
713
+ { id: "feature-elongated-neck", label: "Elongated Neck", dimension: "distinctive-features", description: "Long swan-like neck", promptHint: "with an elongated, swan-like neck, long and gracefully extended" },
714
+ { id: "feature-under-eye-circles", label: "Under-Eye Circles", dimension: "distinctive-features", description: "Subtle dark circles under the eyes (distinct from puffy eye-bags)", promptHint: "with subtle dark circles under the eyes" },
715
+ { id: "feature-fangs", label: "Fangs", dimension: "distinctive-features", description: "Visible fangs (vampire / character archetype)", promptHint: "with visible fangs at the canines, character / vampire archetype" },
716
+
717
+ // -------------------- Regional Aesthetic --------------------
718
+ // Vibe layer (composes with Ethnicity, Skin Tone, Hair, Styling — never
719
+ // hard-codes them). Sub-grouped via `group` so the picker can render
720
+ // collapsible sections by region. Multi-pick supported (up to 2).
721
+ //
722
+ // promptHint invariant: VIBE-ONLY. Do not mention skin tone, hair color,
723
+ // hair style, or specific wardrobe — those compete with the user's
724
+ // explicit picks in other dimensions.
725
+ //
726
+ // Sub-Saharan African entries use 🇬🇧 / 🇫🇷 / 🇵🇹 in the description to
727
+ // call out the dominant colonial-cultural overlay (Anglophone /
728
+ // Francophone / Lusophone) — this drives sartorial + media-diet
729
+ // differences. Ethiopia is intentionally untagged (never colonized).
730
+
731
+ // ----- USA — Mainstream -----
732
+ { id: "cali-beach", label: "California Beach", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Laid-back beach-town California aesthetic", promptHint: "a California beach aesthetic — laid-back beach-town vibe, golden-hour sun-warmed energy, easy carefree mood" },
733
+ { id: "valley-girl", label: "Valley Girl", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Southern California valley aesthetic (feminine)", promptHint: "a Valley Girl aesthetic — Southern California suburban-chic vibe, polished and sun-kissed" },
734
+ { id: "norcal-hippie", label: "NorCal Hippie", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Northern California Berkeley / Bay-Area hippie aesthetic", promptHint: "a Northern California hippie aesthetic — laid-back Berkeley / Bay-Area mood, gentle counter-culture vibe" },
735
+ { id: "boho-la", label: "Boho LA", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Los Angeles canyon-bohemian aesthetic (feminine)", promptHint: "a Boho LA aesthetic — Los Angeles canyon-bohemian vibe, free-spirited Laurel Canyon mood" },
736
+ { id: "texas-cowgirl", label: "Texas Cowgirl", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Texas Western ranch-country aesthetic (feminine)", promptHint: "a Texas cowgirl aesthetic — Western ranch-country vibe, warm Lone-Star confidence" },
737
+ { id: "texas-cowboy", label: "Texas Cowboy", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Texas Western ranch-country aesthetic (masculine)", promptHint: "a Texas cowboy aesthetic — Western ranch-country vibe, weathered Lone-Star presence" },
738
+ { id: "southern-belle", label: "Southern Belle", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Charleston / Savannah antebellum-gentility aesthetic (feminine)", promptHint: "a Southern Belle aesthetic — Charleston / Savannah gentility, gracious antebellum charm" },
739
+ { id: "southern-gentleman", label: "Southern Gentleman", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Old-South gentility aesthetic (masculine)", promptHint: "a Southern Gentleman aesthetic — old-South gentility, gracious Charleston / Savannah charm" },
740
+ { id: "miami-latina", label: "Miami Latina", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "South Beach Caribbean-Latin aesthetic (feminine)", promptHint: "a Miami Latina aesthetic — South Beach Caribbean-Latin energy, sun-and-neon vibrance" },
741
+ { id: "miami-latino", label: "Miami Latino", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "South Beach Caribbean-Latin aesthetic (masculine)", promptHint: "a Miami Latino aesthetic — South Beach Caribbean-Latin energy, sun-and-neon confidence" },
742
+ { id: "hawaii-island", label: "Hawaii Island", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Polynesian-Pacific Hawaiian aesthetic", promptHint: "a Hawaiian island aesthetic — Polynesian-Pacific vibe, gentle ocean-warmed presence" },
743
+ { id: "nyc-fashion", label: "NYC Fashion", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Manhattan editorial-fashion aesthetic (feminine)", promptHint: "a New York fashion-girl aesthetic — Manhattan editorial vibe, urban-sharp confident energy" },
744
+ { id: "nyc-sharp", label: "NYC Sharp", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Manhattan power-tailored aesthetic (masculine)", promptHint: "a New York sharp-dressed aesthetic — Manhattan power-and-pace vibe, urban-tailored presence" },
745
+ { id: "brooklyn-hipster", label: "Brooklyn Hipster", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Williamsburg creative-class aesthetic", promptHint: "a Brooklyn hipster aesthetic — Williamsburg creative-class vibe, considered bohemian-urban energy" },
746
+ { id: "new-england-prep", label: "New England Prep", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Cape Cod / Nantucket old-money preppy aesthetic", promptHint: "a New England prep aesthetic — Cape Cod / Nantucket old-money vibe, classic Ivy-coast polish" },
747
+ { id: "pnw-granola", label: "PNW Granola", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Portland / Seattle outdoorsy aesthetic", promptHint: "a Pacific Northwest granola aesthetic — Portland / Seattle outdoorsy vibe, mossy mountain-coffee mood" },
748
+ { id: "nashville-country", label: "Nashville Country", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Music-City modern country-pop aesthetic (feminine)", promptHint: "a Nashville country-pop aesthetic — Music-City warmth, Southern modern-country charm" },
749
+ { id: "nashville-cowboy", label: "Nashville Cowboy", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Music-City modern-cowboy aesthetic (masculine)", promptHint: "a Nashville modern-cowboy aesthetic — Music-City warmth, modern-Western charm" },
750
+ { id: "vegas-glam", label: "Vegas Glam", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Las Vegas Strip-glamour aesthetic (feminine)", promptHint: "a Las Vegas glam aesthetic — Strip-glamour vibe, high-shine showroom presence" },
751
+ { id: "vegas-suit", label: "Vegas Suit", group: "USA — Mainstream", dimension: "regional-aesthetic", description: "Las Vegas Strip suited aesthetic (masculine)", promptHint: "a Las Vegas suit-and-shine aesthetic — Strip-glamour vibe, high-roller charisma" },
752
+
753
+ // ----- USA — African-American -----
754
+ { id: "harlem-classic", label: "Harlem Classic", group: "USA — African-American", dimension: "regional-aesthetic", description: "Black-New-York heritage aesthetic, jazz-age elegance", promptHint: "a Harlem classic aesthetic — Black-New-York heritage vibe, jazz-age elegance and modern-uptown polish" },
755
+ { id: "atl-southern", label: "ATL Southern", group: "USA — African-American", dimension: "regional-aesthetic", description: "Atlanta Black-South hip-hop / R&B aesthetic", promptHint: "an Atlanta Black-South aesthetic — Southern hip-hop / R&B capital vibe, peach-tree confident-and-warm mood" },
756
+ { id: "nola-creole", label: "NOLA Creole", group: "USA — African-American", dimension: "regional-aesthetic", description: "New Orleans Louisiana French-Caribbean-African aesthetic", promptHint: "a New Orleans Creole aesthetic — Louisiana French-Caribbean-African vibe, jazz-and-second-line warmth" },
757
+ { id: "dmv-go-go", label: "DMV Go-Go", group: "USA — African-American", dimension: "regional-aesthetic", description: "DC / Maryland / Virginia Black-DMV go-go aesthetic", promptHint: "a DC / Maryland / Virginia Black-DMV aesthetic — go-go-music capital vibe, polished East-Coast confidence" },
758
+ { id: "west-coast-hip-hop", label: "West Coast", group: "USA — African-American", dimension: "regional-aesthetic", description: "South Central / Compton West Coast hip-hop aesthetic", promptHint: "a West Coast Black-LA aesthetic — South Central / Compton hip-hop vibe, sun-warmed laid-back swagger" },
759
+ { id: "chicago-south-side", label: "Chicago South Side", group: "USA — African-American", dimension: "regional-aesthetic", description: "Midwest Black-urban Chicago aesthetic", promptHint: "a Chicago South Side aesthetic — Midwest Black-urban vibe, drill-era street-sharp presence" },
760
+
761
+ // ----- Europe -----
762
+ { id: "parisienne", label: "Parisienne", group: "Europe", dimension: "regional-aesthetic", description: "Left-Bank Paris French-chic aesthetic (feminine)", promptHint: "a Parisienne aesthetic — Left-Bank Paris vibe, effortless French-chic mood, je-ne-sais-quoi confidence" },
763
+ { id: "parisian-flaneur", label: "Parisian Flâneur", group: "Europe", dimension: "regional-aesthetic", description: "Left-Bank Paris French-chic aesthetic (masculine)", promptHint: "a Parisian flâneur aesthetic — Left-Bank Paris vibe, contemplative French-chic mood, refined city-walker air" },
764
+ { id: "milanese-fashion", label: "Milanese Fashion", group: "Europe", dimension: "regional-aesthetic", description: "Italian Milanese high-fashion aesthetic", promptHint: "a Milanese fashion aesthetic — Italian high-fashion polish, Quadrilatero confident sleekness" },
765
+ { id: "sicilian-beach", label: "Sicilian Beach", group: "Europe", dimension: "regional-aesthetic", description: "Mediterranean southern-Italian beach aesthetic", promptHint: "a Sicilian beach aesthetic — Mediterranean sun-and-salt vibe, warm southern-Italian ease" },
766
+ { id: "madrileña", label: "Madrileña", group: "Europe", dimension: "regional-aesthetic", description: "Madrid Spanish aesthetic (feminine)", promptHint: "a Madrileña aesthetic — Madrid Spanish vibe, sun-and-confidence Iberian energy" },
767
+ { id: "madrileño", label: "Madrileño", group: "Europe", dimension: "regional-aesthetic", description: "Madrid Spanish aesthetic (masculine)", promptHint: "a Madrileño aesthetic — Madrid Spanish vibe, sun-and-confidence Iberian presence" },
768
+ { id: "catalan-modern", label: "Catalan Modern", group: "Europe", dimension: "regional-aesthetic", description: "Barcelona Mediterranean-design aesthetic", promptHint: "a Catalan modern aesthetic — Barcelona Mediterranean-design vibe, easygoing creative coast" },
769
+ { id: "greek-mediterranean", label: "Greek Mediterranean", group: "Europe", dimension: "regional-aesthetic", description: "Aegean island Greek aesthetic", promptHint: "a Greek Mediterranean aesthetic — Aegean island vibe, sun-bleached limestone-and-sea mood" },
770
+ { id: "scandi-minimal", label: "Scandi Minimal", group: "Europe", dimension: "regional-aesthetic", description: "Nordic clean-line minimal aesthetic", promptHint: "a Scandinavian minimal aesthetic — Nordic clean-line vibe, hygge-and-quiet-confidence mood" },
771
+ { id: "english-rose", label: "English Rose", group: "Europe", dimension: "regional-aesthetic", description: "Countryside English aesthetic (feminine)", promptHint: "an English Rose aesthetic — countryside English vibe, gentle rose-garden refinement" },
772
+ { id: "london-prep", label: "London Prep", group: "Europe", dimension: "regional-aesthetic", description: "Mayfair / Chelsea refined British aesthetic (masculine)", promptHint: "a London prep aesthetic — Mayfair / Chelsea vibe, refined British polish" },
773
+ { id: "manchester-indie", label: "Manchester Indie", group: "Europe", dimension: "regional-aesthetic", description: "North-England gig-and-rain working-class-cool aesthetic", promptHint: "a Manchester indie aesthetic — North-England gig-and-rain vibe, working-class cool" },
774
+ { id: "berlin-alt", label: "Berlin Alt", group: "Europe", dimension: "regional-aesthetic", description: "Kreuzberg techno-and-art counterculture aesthetic", promptHint: "a Berlin alt aesthetic — Kreuzberg techno-and-art vibe, edgy considered counterculture" },
775
+ { id: "munich-traditional", label: "Munich Traditional", group: "Europe", dimension: "regional-aesthetic", description: "Bavarian Oktoberfest-traditional aesthetic", promptHint: "a Munich Bavarian-traditional aesthetic — Oktoberfest warmth, alpine-folk pride" },
776
+ { id: "eastern-european-glam", label: "Eastern European Glam", group: "Europe", dimension: "regional-aesthetic", description: "Moscow / Kyiv winter-glamour aesthetic (feminine)", promptHint: "an Eastern European glam aesthetic — Moscow / Kyiv vibe, statement-confident winter-glamour" },
777
+ { id: "eastern-european-stoic", label: "Eastern European Stoic", group: "Europe", dimension: "regional-aesthetic", description: "Moscow / Kyiv weathered-confidence aesthetic (masculine)", promptHint: "an Eastern European stoic aesthetic — Moscow / Kyiv vibe, weathered confidence, winter-hardened presence" },
778
+
779
+ // ----- Asia -----
780
+ { id: "tokyo-harajuku", label: "Tokyo Harajuku", group: "Asia", dimension: "regional-aesthetic", description: "Tokyo playful expressive Japanese street aesthetic", promptHint: "a Tokyo Harajuku aesthetic — playful expressive Japanese street vibe, color-and-character mood" },
781
+ { id: "ginza-office", label: "Ginza Office", group: "Asia", dimension: "regional-aesthetic", description: "Tokyo Ginza Japanese corporate-elegant aesthetic", promptHint: "a Ginza Tokyo office aesthetic — buttoned-up Japanese corporate-elegant vibe" },
782
+ { id: "shibuya-streetwear", label: "Shibuya Streetwear", group: "Asia", dimension: "regional-aesthetic", description: "Tokyo Shibuya street-fashion aesthetic", promptHint: "a Shibuya streetwear aesthetic — Tokyo street-fashion vibe, layered post-modern mood" },
783
+ { id: "osaka-everyday", label: "Osaka Everyday", group: "Asia", dimension: "regional-aesthetic", description: "Osaka warm casual Kansai aesthetic", promptHint: "an Osaka everyday aesthetic — warm casual Kansai vibe, easygoing humor-and-warmth" },
784
+ { id: "seoul-k-pop", label: "Seoul K-Pop", group: "Asia", dimension: "regional-aesthetic", description: "Seoul Korean-pop polished aesthetic", promptHint: "a Seoul K-pop aesthetic — Korean-pop polish vibe, idol-grade brightness" },
785
+ { id: "gangnam-glam", label: "Gangnam Glam", group: "Asia", dimension: "regional-aesthetic", description: "Seoul Gangnam Korean luxury-district aesthetic", promptHint: "a Gangnam Seoul glam aesthetic — Korean luxury-district vibe, polished beauty-statement" },
786
+ { id: "shanghai-modern", label: "Shanghai Modern", group: "Asia", dimension: "regional-aesthetic", description: "Shanghai Chinese metropolitan aesthetic", promptHint: "a Shanghai modern aesthetic — Chinese metropolitan vibe, Bund-skyline cosmopolitan polish" },
787
+ { id: "beijing-hutong", label: "Beijing Hutong", group: "Asia", dimension: "regional-aesthetic", description: "Beijing old-courtyard traditional-meets-modern aesthetic", promptHint: "a Beijing hutong aesthetic — old-Beijing courtyard vibe, traditional-meets-modern mood" },
788
+ { id: "hong-kong-cinematic", label: "Hong Kong Cinematic", group: "Asia", dimension: "regional-aesthetic", description: "Hong Kong neon-noir Wong-Kar-Wai aesthetic", promptHint: "a Hong Kong cinematic aesthetic — neon-mid-century vibe, Wong-Kar-Wai mood" },
789
+ { id: "mumbai-bollywood", label: "Mumbai Bollywood", group: "Asia", dimension: "regional-aesthetic", description: "Mumbai Indian film-industry aesthetic", promptHint: "a Mumbai Bollywood aesthetic — Indian film-industry vibe, vibrant statement-glamour" },
790
+ { id: "south-india-traditional", label: "South India Traditional", group: "Asia", dimension: "regional-aesthetic", description: "Tamil / Kerala temple-town classical aesthetic", promptHint: "a South Indian traditional aesthetic — Tamil / Kerala temple-town vibe, classical refinement" },
791
+ { id: "bangkok-street", label: "Bangkok Street", group: "Asia", dimension: "regional-aesthetic", description: "Bangkok Thai night-market neon-urban aesthetic", promptHint: "a Bangkok street aesthetic — Thai night-market energy, neon-and-warmth urban vibe" },
792
+
793
+ // ----- Latin America -----
794
+ { id: "carioca-rio", label: "Carioca (Rio)", group: "Latin America", dimension: "regional-aesthetic", description: "Rio de Janeiro beach-and-favela-music Brazilian aesthetic", promptHint: "a Carioca aesthetic — Rio de Janeiro beach-and-favela-music vibe, sun-warmed Brazilian energy" },
795
+ { id: "paulista", label: "Paulista (São Paulo)", group: "Latin America", dimension: "regional-aesthetic", description: "São Paulo metropolitan Brazilian creative-class aesthetic", promptHint: "a Paulista aesthetic — São Paulo metropolitan vibe, urban-Brazilian creative-class polish" },
796
+ { id: "buenos-aires-tango", label: "Buenos Aires Tango", group: "Latin America", dimension: "regional-aesthetic", description: "Buenos Aires Recoleta / San Telmo tango aesthetic (feminine)", promptHint: "a Buenos Aires tango aesthetic — Recoleta / San Telmo vibe, sultry porteña confidence" },
797
+ { id: "porteño", label: "Porteño", group: "Latin America", dimension: "regional-aesthetic", description: "Buenos Aires refined milonguero aesthetic (masculine)", promptHint: "a Porteño aesthetic — Buenos Aires vibe, refined milonguero presence" },
798
+ { id: "mexico-city-chic", label: "Mexico City Chic", group: "Latin America", dimension: "regional-aesthetic", description: "CDMX Roma / Polanco modern-Mexican aesthetic", promptHint: "a Mexico City chic aesthetic — CDMX Roma / Polanco vibe, modern-Mexican polish" },
799
+ { id: "chilango-traditional", label: "Chilango Traditional", group: "Latin America", dimension: "regional-aesthetic", description: "Mexico City warm folk-and-feast aesthetic", promptHint: "a Chilango traditional aesthetic — Mexico City vibe, warm folk-and-feast mood" },
800
+ { id: "bogota-cosmopolitan", label: "Bogotá Cosmopolitan", group: "Latin America", dimension: "regional-aesthetic", description: "Bogotá Colombian capital refined Andean aesthetic", promptHint: "a Bogotá cosmopolitan aesthetic — Colombian capital vibe, refined Andean polish" },
801
+
802
+ // ----- Middle East -----
803
+ { id: "beirut-riviera", label: "Beirut Riviera", group: "Middle East", dimension: "regional-aesthetic", description: "Lebanese Mediterranean cosmopolitan aesthetic", promptHint: "a Beirut Riviera aesthetic — Lebanese Mediterranean vibe, cosmopolitan-glamour mood" },
804
+ { id: "tel-aviv-beach", label: "Tel Aviv Beach", group: "Middle East", dimension: "regional-aesthetic", description: "Tel Aviv Mediterranean coastal-city startup-energy aesthetic", promptHint: "a Tel Aviv beach aesthetic — Mediterranean coastal-city vibe, easygoing sun-and-sea startup-energy mood" },
805
+ { id: "jerusalem-stone", label: "Jerusalem Stone", group: "Middle East", dimension: "regional-aesthetic", description: "Jerusalem old-city limestone-heritage aesthetic", promptHint: "a Jerusalem old-city aesthetic — limestone-warmth heritage vibe, layered ancient-and-modern presence" },
806
+ { id: "sabra", label: "Sabra", group: "Middle East", dimension: "regional-aesthetic", description: "Native-Israeli direct-and-warm Sabra aesthetic", promptHint: "a Sabra aesthetic — native-Israeli vibe, sun-warmed direct-and-warm energy, prickly-outside-sweet-inside mood" },
807
+ { id: "dubai-modern", label: "Dubai Modern", group: "Middle East", dimension: "regional-aesthetic", description: "Gulf-luxury Dubai cosmopolitan-emirate aesthetic", promptHint: "a Dubai modern aesthetic — Gulf-luxury vibe, polished cosmopolitan-emirate presence" },
808
+ { id: "persian-glam", label: "Persian Glam", group: "Middle East", dimension: "regional-aesthetic", description: "Iranian heritage-and-glamour Persian aesthetic", promptHint: "a Persian glam aesthetic — Iranian heritage-and-glamour vibe, refined statement mood" },
809
+ { id: "istanbul-bosphorus", label: "Istanbul Bosphorus", group: "Middle East", dimension: "regional-aesthetic", description: "Istanbul east-meets-west Bosphorus Ottoman-and-modern aesthetic", promptHint: "an Istanbul Bosphorus aesthetic — east-meets-west Turkish vibe, layered Ottoman-and-modern strait-city presence" },
810
+
811
+ // ----- North Africa -----
812
+ { id: "cairene-cosmopolitan", label: "Cairene Cosmopolitan", group: "North Africa", dimension: "regional-aesthetic", description: "Cairo Egyptian-Arab metropolitan aesthetic", promptHint: "a Cairene cosmopolitan aesthetic — Cairo metropolitan vibe, layered Egyptian-Arab modern-and-historical mood" },
813
+ { id: "maghreb-coastal", label: "Maghreb Coastal", group: "North Africa", dimension: "regional-aesthetic", description: "Tunis / Algiers French-Mediterranean-North-African colonial-port aesthetic", promptHint: "a Maghreb coastal aesthetic — Tunis / Algiers French-Mediterranean-North-African vibe, sun-bleached colonial-port mood" },
814
+ { id: "marrakech-bohemian", label: "Marrakech Bohemian", group: "North Africa", dimension: "regional-aesthetic", description: "Marrakech Moroccan medina warm-spice aesthetic", promptHint: "a Marrakech bohemian aesthetic — Moroccan medina vibe, warm-spice and -textile mood" },
815
+
816
+ // ----- Sub-Saharan Africa -----
817
+ // 🇬🇧 Anglophone, 🇫🇷 Francophone, 🇵🇹 Lusophone — colonial-cultural overlay
818
+ // drives sartorial / media-diet differences. Ethiopia untagged (never colonized).
819
+ { id: "lagos-afro-glam", label: "Lagos Afro-Glam", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇬🇧 Lagos Nigerian Anglophone-West-African metropolitan aesthetic", promptHint: "a Lagos Afro-glam aesthetic — Nigerian Anglophone-West-African metropolitan vibe, vibrant statement-fashion mood" },
820
+ { id: "accra-afro-fashion", label: "Accra Afro-Fashion", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇬🇧 Accra Ghanaian Anglophone-West-African kente-meets-modern aesthetic", promptHint: "an Accra Afro-fashion aesthetic — Ghanaian Anglophone-West-African vibe, kente-meets-modern statement" },
821
+ { id: "dakar-francophone", label: "Dakar Francophone", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇫🇷 Dakar Senegalese Francophone-West-African coastal-cosmopolitan aesthetic", promptHint: "a Dakar aesthetic — Senegalese Francophone-West-African coastal-cosmopolitan vibe, refined boubou-and-Paris-pull mood" },
822
+ { id: "abidjan-cosmopolitan", label: "Abidjan Cosmopolitan", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇫🇷 Abidjan Ivoirian Francophone-West-African urban aesthetic", promptHint: "an Abidjan aesthetic — Ivoirian Francophone-West-African urban vibe, lagoon-city polish" },
823
+ { id: "kinshasa-sape", label: "Kinshasa SAPE", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇫🇷 Kinshasa Congolese Francophone-Central-African SAPE dandy aesthetic", promptHint: "a Kinshasa SAPE aesthetic — Congolese Francophone-Central-African dandy vibe, Society-of-Ambianceurs hyper-tailored statement" },
824
+ { id: "nairobi-cosmopolitan", label: "Nairobi Cosmopolitan", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇬🇧 Nairobi Kenyan Anglophone-East-African modern-urban aesthetic", promptHint: "a Nairobi cosmopolitan aesthetic — Kenyan Anglophone-East-African modern-urban vibe, considered creative-class polish" },
825
+ { id: "addis-habesha", label: "Addis Habesha", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "Addis Ababa Ethiopian Habesha highland-heritage aesthetic (Ethiopia was never colonized)", promptHint: "an Addis Ababa Habesha aesthetic — Ethiopian highland heritage vibe, refined coffee-ceremony-and-modern-Addis polish" },
826
+ { id: "swahili-coast", label: "Swahili Coast", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇬🇧 Zanzibar / Mombasa Anglophone-East-African coastal-Swahili aesthetic", promptHint: "a Swahili-Coast aesthetic — Zanzibar / Mombasa Anglophone-East-African coastal vibe, Indian-Ocean-trade-route warmth" },
827
+ { id: "johannesburg-urban", label: "Johannesburg Urban", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇬🇧 Johannesburg South-African Anglophone-Southern-African Black-metropolitan aesthetic", promptHint: "a Johannesburg urban aesthetic — South-African Anglophone-Southern-African Black-metropolitan vibe, post-Soweto urban-creative confidence" },
828
+ { id: "cape-town-cosmopolitan", label: "Cape Town Cosmopolitan", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇬🇧 Cape Town South-African Anglophone-Southern-African coastal-city aesthetic", promptHint: "a Cape Town cosmopolitan aesthetic — South-African Anglophone-Southern-African coastal-city vibe, easygoing creative polish" },
829
+ { id: "luanda-lusophone", label: "Luanda Lusophone", group: "Sub-Saharan Africa", dimension: "regional-aesthetic", description: "🇵🇹 Luanda Angolan Lusophone-Southern-African kizomba-and-Atlantic-coast aesthetic", promptHint: "a Luanda aesthetic — Angolan Lusophone-Southern-African vibe, kizomba-and-Atlantic-coast statement-presence" },
830
+
831
+ // ----- Oceania -----
832
+ { id: "bondi-beach", label: "Bondi Beach", group: "Oceania", dimension: "regional-aesthetic", description: "Bondi Australian coastal salt-and-sun aesthetic", promptHint: "a Bondi Beach aesthetic — Australian coastal vibe, salt-and-sun easygoing presence" },
833
+ { id: "australian-outback", label: "Outback", group: "Oceania", dimension: "regional-aesthetic", description: "Rural Australian Outback aesthetic (feminine)", promptHint: "an Australian Outback aesthetic — rural Australian vibe, weathered sun-burnt confidence" },
834
+ { id: "outback-stockman", label: "Outback Stockman", group: "Oceania", dimension: "regional-aesthetic", description: "Rural Australian Outback aesthetic (masculine)", promptHint: "an Outback Stockman aesthetic — rural Australian vibe, weathered station-hand presence" },
835
+ { id: "auckland-coastal", label: "Auckland Coastal", group: "Oceania", dimension: "regional-aesthetic", description: "Auckland New Zealand harbor-city Pacific aesthetic", promptHint: "an Auckland coastal aesthetic — New Zealand harbor-city vibe, considered Pacific polish" },
836
+ ] as const
837
+
838
+ export const PERSON_DIMENSION_ORDER: ReadonlyArray<PersonDimension> = [
839
+ // Identity
840
+ "type",
841
+ "age",
842
+ "ethnicity",
843
+ "regional-aesthetic",
844
+ // Body
845
+ "frame",
846
+ "body-mass",
847
+ "bust",
848
+ "waist",
849
+ "hips",
850
+ "silhouette",
851
+ // Face structure
852
+ "face-shape",
853
+ "jawline",
854
+ "cheekbones",
855
+ "facial-fullness",
856
+ "eye-shape",
857
+ "eyelid-type",
858
+ "canthal-tilt",
859
+ "eye-spacing",
860
+ "eye-set-brow",
861
+ "nose",
862
+ "nose-tip",
863
+ "lip-fullness",
864
+ "lip-shape",
865
+ "lip-state",
866
+ // Hair
867
+ "hair-base",
868
+ "hair-color",
869
+ "eyebrows",
870
+ // Skin & eyes
871
+ "skin-tone",
872
+ "skin-texture",
873
+ "eye-color",
874
+ "eye-state",
875
+ "facial-hair",
876
+ "distinctive-features",
877
+ ]
878
+
879
+ export const PERSON_DIMENSION_LABELS: Readonly<Record<PersonDimension, string>> = {
880
+ type: "Type",
881
+ age: "Age",
882
+ ethnicity: "Ethnicity",
883
+ "regional-aesthetic": "Regional Aesthetic",
884
+ frame: "Frame",
885
+ "body-mass": "Body Mass",
886
+ bust: "Bust",
887
+ waist: "Waist",
888
+ hips: "Hips",
889
+ silhouette: "Silhouette",
890
+ "face-shape": "Face Shape",
891
+ jawline: "Jawline",
892
+ cheekbones: "Cheekbones",
893
+ "facial-fullness": "Facial Fullness",
894
+ "eye-shape": "Eye Shape",
895
+ "eyelid-type": "Eyelid Type",
896
+ "canthal-tilt": "Canthal Tilt",
897
+ "eye-spacing": "Eye Spacing",
898
+ "eye-set-brow": "Eye Set / Brow Distance",
899
+ nose: "Nose",
900
+ "nose-tip": "Nose Tip",
901
+ "lip-fullness": "Lip Fullness",
902
+ "lip-shape": "Lip Shape",
903
+ "lip-state": "Lip State",
904
+ "hair-color": "Hair Color",
905
+ "hair-base": "Hair (Texture & Length)",
906
+ eyebrows: "Eyebrows",
907
+ "skin-tone": "Skin Tone",
908
+ "skin-texture": "Skin Texture",
909
+ "eye-color": "Eye Color",
910
+ "eye-state": "Eye State",
911
+ "facial-hair": "Facial Hair",
912
+ "distinctive-features": "Distinctive Features",
913
+ }
914
+
915
+ /**
916
+ * Cross-dimension grouping for the compact Person picker view. The detailed
917
+ * view renders one row per dimension (PERSON_DIMENSION_ORDER); the compact
918
+ * view collapses those rows into these six labelled sections. This is the
919
+ * machine-readable form of the section comments in PERSON_DIMENSION_ORDER —
920
+ * the guard test (person-sections.test.ts) asserts it partitions every
921
+ * PersonDimension exactly once, so a newly added dimension can't silently
922
+ * drop out of the compact grouping.
923
+ */
924
+ export interface PersonDimensionSection {
925
+ readonly label: string
926
+ readonly dimensions: ReadonlyArray<PersonDimension>
927
+ }
928
+
929
+ export const PERSON_DIMENSION_SECTIONS: ReadonlyArray<PersonDimensionSection> = [
930
+ { label: "Identity", dimensions: ["type", "age", "ethnicity", "regional-aesthetic"] },
931
+ { label: "Body", dimensions: ["frame", "body-mass", "bust", "waist", "hips", "silhouette"] },
932
+ { label: "Face", dimensions: ["face-shape", "jawline", "cheekbones", "facial-fullness", "eye-shape", "eyelid-type", "canthal-tilt", "eye-spacing", "eye-set-brow", "nose", "nose-tip", "lip-fullness", "lip-shape", "lip-state"] },
933
+ { label: "Hair", dimensions: ["hair-base", "hair-color", "eyebrows"] },
934
+ { label: "Skin & Eyes", dimensions: ["skin-tone", "skin-texture", "eye-color", "eye-state"] },
935
+ { label: "Features", dimensions: ["facial-hair", "distinctive-features"] },
936
+ ] as const
937
+
938
+ /**
939
+ * Maps each PersonDimension to the consumer data field name holding the
940
+ * selected entry id. Multi-dimension model: a consumer (PersonData) may
941
+ * independently set a value in each of the dimensions.
942
+ */
943
+ export const PERSON_FIELD_BY_DIMENSION: Record<
944
+ PersonDimension,
945
+ | "type" | "age" | "ethnicity" | "regionalAesthetic"
946
+ | "frame" | "bodyMass" | "bust" | "waist" | "hips" | "silhouette"
947
+ | "faceShape" | "jawline" | "cheekbones" | "facialFullness"
948
+ | "eyeShape" | "eyelidType" | "canthalTilt" | "eyeSpacing" | "eyeSetBrow"
949
+ | "nose" | "noseTip" | "lipFullness" | "lipShape" | "lipState"
950
+ | "hairColor" | "hairBase" | "eyebrows"
951
+ | "skinTone" | "skinTexture" | "eyeColor" | "eyeState" | "facialHair" | "distinctiveFeature"
952
+ > = {
953
+ type: "type",
954
+ age: "age",
955
+ ethnicity: "ethnicity",
956
+ "regional-aesthetic": "regionalAesthetic",
957
+ frame: "frame",
958
+ "body-mass": "bodyMass",
959
+ bust: "bust",
960
+ waist: "waist",
961
+ hips: "hips",
962
+ silhouette: "silhouette",
963
+ "face-shape": "faceShape",
964
+ jawline: "jawline",
965
+ cheekbones: "cheekbones",
966
+ "facial-fullness": "facialFullness",
967
+ "eye-shape": "eyeShape",
968
+ "eyelid-type": "eyelidType",
969
+ "canthal-tilt": "canthalTilt",
970
+ "eye-spacing": "eyeSpacing",
971
+ "eye-set-brow": "eyeSetBrow",
972
+ nose: "nose",
973
+ "nose-tip": "noseTip",
974
+ "lip-fullness": "lipFullness",
975
+ "lip-shape": "lipShape",
976
+ "lip-state": "lipState",
977
+ "hair-color": "hairColor",
978
+ "hair-base": "hairBase",
979
+ eyebrows: "eyebrows",
980
+ "skin-tone": "skinTone",
981
+ "skin-texture": "skinTexture",
982
+ "eye-color": "eyeColor",
983
+ "eye-state": "eyeState",
984
+ "facial-hair": "facialHair",
985
+ "distinctive-features": "distinctiveFeature",
986
+ }
987
+
988
+ /** Per-dimension multi-select cap (single source of truth for the picker UI,
989
+ * the image-analyzer tool schema, and the Zod validator).
990
+ * - ethnicity / regional-aesthetic / hair-color / eye-color / lip-state /
991
+ * eye-state / skin-texture: 2
992
+ * - distinctive-features: 3
993
+ * All other dims are single-select (absent → treated as 1). */
994
+ export const MAX_SELECTED_BY_DIMENSION: Partial<Record<PersonDimension, number>> = {
995
+ ethnicity: 2,
996
+ "regional-aesthetic": 2,
997
+ "hair-color": 2,
998
+ "eye-color": 2,
999
+ "distinctive-features": 3,
1000
+ "lip-state": 2,
1001
+ "eye-state": 2,
1002
+ "skin-texture": 2,
1003
+ }
1004
+
1005
+ /** Selection limit for a dimension (1 when not multi-select). */
1006
+ export function getPersonDimensionLimit(dimension: PersonDimension): number {
1007
+ return MAX_SELECTED_BY_DIMENSION[dimension] ?? 1
1008
+ }
1009
+
1010
+ /**
1011
+ * Shape of the per-dimension person fields. All optional — user may set
1012
+ * zero, one, or all ten dimensions.
1013
+ */
1014
+ export interface PersonValue {
1015
+ type?: string
1016
+ /** Selected age preset id (e.g. `"age-30s"`). When set to the special
1017
+ * `"age-custom"` sentinel, the literal age in years is read from
1018
+ * `customAge` and the hint is generated at build time. */
1019
+ age?: string
1020
+ /** Specific age in years. Only consulted when `age === "age-custom"`. */
1021
+ customAge?: number
1022
+ /** Single id, or an array of up to 2 ids for mixed heritage (e.g.
1023
+ * ["slavic","mediterranean"] → "of mixed Slavic and Mediterranean heritage"). */
1024
+ ethnicity?: string | ReadonlyArray<string>
1025
+ /** Regional / cultural aesthetic vibe (e.g. `"cali-beach"`, `"parisienne"`,
1026
+ * `"kinshasa-sape"`). Composes with ethnicity, skin tone, hair, and
1027
+ * styling — the dimension's promptHints are vibe-only and never hard-code
1028
+ * the visuals those other dimensions own. Single id or up to 2 ids for
1029
+ * hybrid looks (e.g. ["nyc-fashion","parisienne"]). */
1030
+ regionalAesthetic?: string | ReadonlyArray<string>
1031
+ /** Skeletal frame width / height (petite, slim, average, broad). Independent
1032
+ * of body mass — "slim frame + full bust" is expressible. */
1033
+ frame?: string
1034
+ /** Body mass / leanness (lean, average, full, heavy). Independent of frame. */
1035
+ bodyMass?: string
1036
+ /** Bust size (small, average, full, very full) — applicable subjects. */
1037
+ bust?: string
1038
+ /** Waist definition (defined, average, straight). */
1039
+ waist?: string
1040
+ /** Hip width (narrow, balanced, wide). */
1041
+ hips?: string
1042
+ /** Overall body silhouette (hourglass, rectangular, pear, inverted-triangle,
1043
+ * athletic). Optional — only emits when picked. */
1044
+ silhouette?: string
1045
+ /** Face silhouette (oval, round, square, heart…). */
1046
+ faceShape?: string
1047
+ /** Jaw shape (strong, soft, pointed, wide). */
1048
+ jawline?: string
1049
+ /** Cheekbone projection / height (low, average, high-defined, sculpted-high, wide). */
1050
+ cheekbones?: string
1051
+ /** Face-specific leanness, independent of body mass (gaunt, lean, average, full, round). */
1052
+ facialFullness?: string
1053
+ /** Eye shape — anatomical shape only (almond, round, monolid, double-eyelid,
1054
+ * wide, narrow). Lid type, canthal tilt, spacing and brow-set are separate. */
1055
+ eyeShape?: string
1056
+ /** Upper-eyelid trait (standard, hooded, droopy, deep-set). */
1057
+ eyelidType?: string
1058
+ /** Canthal tilt — outer-corner angle (neutral, upturned, downturned). */
1059
+ canthalTilt?: string
1060
+ /** Horizontal eye spacing (close-set, average, wide-set). */
1061
+ eyeSpacing?: string
1062
+ /** Brow-to-eye distance / eye set (low-set "hunter", average, high-set "doe"). */
1063
+ eyeSetBrow?: string
1064
+ /** Nose bridge shape (straight, aquiline, snub, broad…). */
1065
+ nose?: string
1066
+ /** Nose tip rotation / shape (natural, refined, upturned, rounded, drooping). */
1067
+ noseTip?: string
1068
+ /** Lip fullness (thin, medium, full, full-lower). Replaces the legacy
1069
+ * combined `lips` field together with `lipShape`. */
1070
+ lipFullness?: string
1071
+ /** Lip shape (natural, cupid's bow, wide, heart, small). Replaces the legacy
1072
+ * combined `lips` field together with `lipFullness`. */
1073
+ lipShape?: string
1074
+ /** @deprecated Split into `lipFullness` + `lipShape`. Retained so legacy
1075
+ * workflow data keeps resolving — `buildPersonHints` reads it as a fallback
1076
+ * and `migratePersonValue` relocates it onto the new fields. */
1077
+ lips?: string
1078
+ /** Lip state — what the lips are doing right now (chapped, glossy,
1079
+ * parted, biting, pursed, bold-red…). Distinct from `lips` which is
1080
+ * anatomical shape. Single id or up to 2 (e.g. glossy + parted). */
1081
+ lipState?: string | ReadonlyArray<string>
1082
+ /** Single id or up to 2 ids for two-tone / ombre / highlighted hair
1083
+ * (e.g. ["hair-black","hair-platinum"]). */
1084
+ hairColor?: string | ReadonlyArray<string>
1085
+ /** Natural hair texture + length (texture×length combos). The actual cut
1086
+ * / styling choice (bob, wolf cut, braids…) lives in Styling.hair-cut. */
1087
+ hairBase?: string
1088
+ eyebrows?: string
1089
+ skinTone?: string
1090
+ /** Skin texture (smooth, porcelain, freckled, …). Single id or up to 2
1091
+ * combined (e.g. porcelain + freckled, sun-kissed + dewy). */
1092
+ skinTexture?: string | ReadonlyArray<string>
1093
+ /** Single id or up to 2 ids for heterochromia (e.g.
1094
+ * ["eyes-blue","eyes-green"]). */
1095
+ eyeColor?: string | ReadonlyArray<string>
1096
+ /** Eye state — what the eyes are doing (closed, half-lidded, wide-eyed,
1097
+ * staring at camera, gazing away/up/down, glassy). Distinct from
1098
+ * `eyeShape` (anatomy) and `eyeColor`. Single id or up to 2 (e.g.
1099
+ * half-lidded + glassy). */
1100
+ eyeState?: string | ReadonlyArray<string>
1101
+ facialHair?: string
1102
+ /** Single id or up to 3 ids for combined features (e.g. freckles +
1103
+ * glasses + sleeve tattoo). */
1104
+ distinctiveFeature?: string | ReadonlyArray<string>
1105
+ /** Free-text appended BEFORE the dimension compound. Use when you want
1106
+ * context/framing to come first (e.g. "wet-haired" or "covered in paint"). */
1107
+ preText?: string
1108
+ /** Free-text appended AFTER the dimension compound. Use for extra
1109
+ * specifics that dimensions can't capture (e.g. "wearing a leather
1110
+ * jacket", "with a silver necklace"). */
1111
+ postText?: string
1112
+ }
1113
+
1114
+ const personById = new Map<string, Person>(PEOPLE.map((p) => [p.id, p]))
1115
+
1116
+ export function getPerson(id: string | undefined | null): Person | undefined {
1117
+ if (!id) return undefined
1118
+ return personById.get(id)
1119
+ }
1120
+
1121
+ export function getPersonLabel(id: string | undefined | null, fallback?: string): string {
1122
+ const p = getPerson(id)
1123
+ if (p) return p.label
1124
+ if (fallback !== undefined) return fallback
1125
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
1126
+ }
1127
+
1128
+ export function getPersonPromptHint(id: string | undefined | null): string {
1129
+ return getPerson(id)?.promptHint ?? ""
1130
+ }
1131
+
1132
+ export const PERSON_IDS: ReadonlyArray<string> = PEOPLE.map((p) => p.id)
1133
+
1134
+ /**
1135
+ * Aggregate all enabled per-dimension person prompt hints from a consumer's
1136
+ * data, in canonical dimension order (type, age, ethnicity, build, hair
1137
+ * style, hair color, skin tone, eye color, facial hair, distinctive
1138
+ * features).
1139
+ *
1140
+ * Accepts a loosely typed record (the helper is shared between strongly
1141
+ * typed frontend node data and the backend's `Record<string, unknown>`
1142
+ * workflow data). Non-string values are ignored.
1143
+ *
1144
+ * Returns array of fragment strings — caller joins on ", " for the
1145
+ * compound description ("a beautiful woman, in their 30s, East Asian, slim
1146
+ * build, long wavy hair, brown hair, fair skin, green eyes, wearing
1147
+ * glasses").
1148
+ */
1149
+ /**
1150
+ * Build the ethnicity hint clause. Single pick → "of {Group} descent"
1151
+ * (the entry's own promptHint). Two picks → mixed-heritage clause that
1152
+ * names both backgrounds. We use the entry label rather than re-running
1153
+ * promptHint, since "of mixed X and Y heritage" is a different grammar
1154
+ * from concatenating two "of X descent" / "of Y descent" sentences.
1155
+ */
1156
+ function normalizePickIds(value: unknown): string[] {
1157
+ if (typeof value === "string") return value ? [value] : []
1158
+ if (!Array.isArray(value)) return []
1159
+ const out: string[] = []
1160
+ for (const v of value) {
1161
+ if (typeof v === "string" && v && !out.includes(v)) out.push(v)
1162
+ }
1163
+ return out
1164
+ }
1165
+
1166
+ function buildEthnicityHint(value: unknown): string {
1167
+ const ids = normalizePickIds(value)
1168
+ if (ids.length === 0) return ""
1169
+ if (ids.length === 1) return getPersonPromptHint(ids[0])
1170
+ const labels = ids
1171
+ .slice(0, 2)
1172
+ .map((id) => getPerson(id)?.shortLabel ?? getPerson(id)?.label ?? "")
1173
+ .filter((s): s is string => Boolean(s))
1174
+ if (labels.length < 2) return getPersonPromptHint(ids[0])
1175
+ return `of mixed ${labels[0]} and ${labels[1]} heritage`
1176
+ }
1177
+
1178
+ /**
1179
+ * Hair color: single → entry's hint ("blonde hair", "auburn hair"). Two →
1180
+ * "two-tone X and Y hair" using lowercased entry labels (covers ombre,
1181
+ * balayage, highlights, dyed two-tone styles).
1182
+ */
1183
+ function buildHairColorHint(value: unknown): string {
1184
+ const ids = normalizePickIds(value)
1185
+ if (ids.length === 0) return ""
1186
+ if (ids.length === 1) return getPersonPromptHint(ids[0])
1187
+ const labels = ids
1188
+ .slice(0, 2)
1189
+ .map((id) => getPerson(id)?.label?.toLowerCase() ?? "")
1190
+ .filter((s): s is string => Boolean(s))
1191
+ if (labels.length < 2) return getPersonPromptHint(ids[0])
1192
+ return `two-tone ${labels[0]} and ${labels[1]} hair`
1193
+ }
1194
+
1195
+ /**
1196
+ * Eye color: single → entry's hint ("blue eyes"). Two → independent emit
1197
+ * like distinctive-features. We deliberately avoid hard-coding a
1198
+ * heterochromia interpretation because users also pick two colors to
1199
+ * mean "natural eye color + tinted contacts" or "sectoral / mixed iris".
1200
+ * Letting both hints stand side-by-side lets the diffusion model
1201
+ * resolve from the rest of the prompt context. Users wanting an explicit
1202
+ * heterochromia look should add the `feature-heterochromia` distinctive
1203
+ * feature alongside the two color picks.
1204
+ */
1205
+ function buildEyeColorHints(value: unknown): string[] {
1206
+ return emitIndependentHints(value)
1207
+ }
1208
+
1209
+ /**
1210
+ * Age hint with optional custom-numeric override. When the user picks the
1211
+ * "age-custom" sentinel and provides a specific number, generate a phrase
1212
+ * tuned to the life stage (toddler / child / teen wording for the lower
1213
+ * ranges, plain "{N} years old" for adults). Otherwise return the catalog
1214
+ * entry's static promptHint.
1215
+ *
1216
+ * Custom number is clamped to a sane range and rejected if non-finite.
1217
+ */
1218
+ export function buildAgeHint(
1219
+ ageId: string | undefined | null,
1220
+ customAge: number | undefined | null,
1221
+ ): string {
1222
+ if (ageId !== "age-custom") return getPersonPromptHint(ageId)
1223
+ if (typeof customAge !== "number" || !Number.isFinite(customAge)) return ""
1224
+ const n = Math.max(0, Math.min(120, Math.round(customAge)))
1225
+ if (n === 0) return "a newborn under 1 year old"
1226
+ if (n === 1) return "around 1 year old"
1227
+ if (n < 4) return `a toddler around ${n} years old`
1228
+ if (n < 7) return `a young child around ${n} years old`
1229
+ if (n < 10) return `a child around ${n} years old`
1230
+ if (n < 13) return `a pre-teen around ${n} years old`
1231
+ if (n < 20) return `${n} years old, in their teens`
1232
+ return `${n} years old`
1233
+ }
1234
+
1235
+ /**
1236
+ * Multi-pick "state" dims (distinctive-features, lip-state, eye-state,
1237
+ * skin-texture) — independent flags rather than mutually exclusive. Each
1238
+ * entry's promptHint is a standalone "with X" / "wearing X" / "staring X"
1239
+ * clause, so we emit them all separately and let buildPersonHints'
1240
+ * comma-join read naturally.
1241
+ */
1242
+ function emitIndependentHints(value: unknown): string[] {
1243
+ const ids = normalizePickIds(value)
1244
+ const out: string[] = []
1245
+ for (const id of ids) {
1246
+ const hint = getPersonPromptHint(id)
1247
+ if (hint) out.push(hint)
1248
+ }
1249
+ return out
1250
+ }
1251
+
1252
+ export function buildPersonHints(
1253
+ data: Record<string, unknown> & PersonValue,
1254
+ ): string[] {
1255
+ const hints: string[] = []
1256
+
1257
+ const pre = typeof data.preText === "string" ? data.preText.trim() : ""
1258
+ if (pre) hints.push(pre)
1259
+
1260
+ for (const dimension of PERSON_DIMENSION_ORDER) {
1261
+ const field = PERSON_FIELD_BY_DIMENSION[dimension]
1262
+ const raw = data[field]
1263
+ if (dimension === "age") {
1264
+ const ageId = typeof raw === "string" ? raw : undefined
1265
+ const customAge = typeof data.customAge === "number" ? data.customAge : undefined
1266
+ const h = buildAgeHint(ageId, customAge)
1267
+ if (h) hints.push(h)
1268
+ continue
1269
+ }
1270
+ if (dimension === "ethnicity") {
1271
+ const h = buildEthnicityHint(raw)
1272
+ if (h) hints.push(h)
1273
+ continue
1274
+ }
1275
+ if (dimension === "hair-color") {
1276
+ const h = buildHairColorHint(raw)
1277
+ if (h) hints.push(h)
1278
+ continue
1279
+ }
1280
+ if (dimension === "eye-color") {
1281
+ for (const h of buildEyeColorHints(raw)) hints.push(h)
1282
+ continue
1283
+ }
1284
+ if (
1285
+ dimension === "distinctive-features" ||
1286
+ dimension === "lip-state" ||
1287
+ dimension === "eye-state" ||
1288
+ dimension === "skin-texture" ||
1289
+ dimension === "regional-aesthetic"
1290
+ ) {
1291
+ for (const h of emitIndependentHints(raw)) hints.push(h)
1292
+ continue
1293
+ }
1294
+ if (typeof raw !== "string" || raw.length === 0) continue
1295
+ const hint = getPersonPromptHint(raw)
1296
+ if (hint) hints.push(hint)
1297
+ }
1298
+
1299
+ // Legacy fallback: the old combined `lips` dimension was split into
1300
+ // `lip-fullness` + `lip-shape`. Saved workflow data may still carry a single
1301
+ // `data.lips` id; emit its hint, but ONLY when neither new field is set so we
1302
+ // never double-count after a migration. (The relocated lip ids remain in the
1303
+ // catalog, so `getPersonPromptHint` resolves them identically.)
1304
+ if (
1305
+ typeof data.lips === "string" &&
1306
+ data.lips.length > 0 &&
1307
+ !data.lipFullness &&
1308
+ !data.lipShape
1309
+ ) {
1310
+ const legacyLips = getPersonPromptHint(data.lips)
1311
+ if (legacyLips) hints.push(legacyLips)
1312
+ }
1313
+
1314
+ const post = typeof data.postText === "string" ? data.postText.trim() : ""
1315
+ if (post) hints.push(post)
1316
+
1317
+ return hints
1318
+ }
1319
+
1320
+ // Legacy → new field relocation tables for `migratePersonValue`. The old
1321
+ // single `eye-shape` dimension crammed shape + lid + tilt + spacing into one
1322
+ // pick, and `lips` crammed fullness + shape; both were split. Ids were kept
1323
+ // stable so prompt output is identical either way — this only moves a stored
1324
+ // value onto the field the picker now renders it under.
1325
+ const PERSON_EYELID_RELOCATE: ReadonlySet<string> = new Set(["eye-hooded", "eye-droopy", "eye-deep-set"])
1326
+ const PERSON_CANTHAL_RELOCATE: ReadonlySet<string> = new Set(["eye-upturned", "eye-downturned"])
1327
+ const PERSON_SPACING_RELOCATE: ReadonlySet<string> = new Set(["eye-close-set", "eye-wide-set"])
1328
+ const PERSON_LIP_FULLNESS_RELOCATE: ReadonlySet<string> = new Set(["lips-thin", "lips-medium", "lips-full"])
1329
+ const PERSON_LIP_SHAPE_RELOCATE: ReadonlySet<string> = new Set(["lips-wide", "lips-cupids-bow", "lips-small"])
1330
+
1331
+ /**
1332
+ * Relocate legacy single-field Person values onto the post-split fields so the
1333
+ * picker UI shows them in their new home:
1334
+ *
1335
+ * - `eyeShape` ∈ {hooded, droopy, deep-set} → `eyelidType`
1336
+ * - `eyeShape` ∈ {upturned, downturned} → `canthalTilt`
1337
+ * - `eyeShape` ∈ {wide-set, close-set} → `eyeSpacing`
1338
+ * - `lips` ∈ {thin, medium, full} → `lipFullness`
1339
+ * - `lips` ∈ {wide, cupids-bow, small} → `lipShape`
1340
+ *
1341
+ * PROMPT OUTPUT IS UNAFFECTED either way — `getPersonPromptHint` resolves the
1342
+ * ids regardless of dimension and `buildPersonHints` has a `lips` fallback;
1343
+ * this is UI hygiene + clean re-saves only. Immutable + idempotent: returns the
1344
+ * same reference when nothing moves, never overwrites an already-set target,
1345
+ * clears the legacy source once relocated, and preserves every other key (node
1346
+ * data carries non-Person fields too).
1347
+ */
1348
+ export function migratePersonValue<T extends Record<string, unknown>>(data: T): T {
1349
+ let next: Record<string, unknown> | undefined
1350
+ const relocate = (from: "eyeShape" | "lips", to: string, value: string) => {
1351
+ next = { ...(next ?? data), [to]: value, [from]: undefined }
1352
+ }
1353
+
1354
+ const eyeShape = data.eyeShape
1355
+ if (typeof eyeShape === "string" && eyeShape.length > 0) {
1356
+ if (PERSON_EYELID_RELOCATE.has(eyeShape) && !data.eyelidType) relocate("eyeShape", "eyelidType", eyeShape)
1357
+ else if (PERSON_CANTHAL_RELOCATE.has(eyeShape) && !data.canthalTilt) relocate("eyeShape", "canthalTilt", eyeShape)
1358
+ else if (PERSON_SPACING_RELOCATE.has(eyeShape) && !data.eyeSpacing) relocate("eyeShape", "eyeSpacing", eyeShape)
1359
+ }
1360
+
1361
+ const lips = data.lips
1362
+ if (typeof lips === "string" && lips.length > 0) {
1363
+ if (PERSON_LIP_FULLNESS_RELOCATE.has(lips) && !data.lipFullness) relocate("lips", "lipFullness", lips)
1364
+ else if (PERSON_LIP_SHAPE_RELOCATE.has(lips) && !data.lipShape) relocate("lips", "lipShape", lips)
1365
+ }
1366
+
1367
+ return (next ?? data) as T
1368
+ }