@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.
- package/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice-character catalog: age + gender + language + accent + timbre. Feeds
|
|
3
|
+
* Voice Design's voiceDescription field via the Sound aggregator.
|
|
4
|
+
*
|
|
5
|
+
* `language` is multi-pick (up to 3) for codeswitching / multilingual
|
|
6
|
+
* voice work. Distinct from `accent` — accent is HOW it sounds, language
|
|
7
|
+
* is WHAT'S being spoken.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { pickIds } from "@nodaro/shared"
|
|
11
|
+
|
|
12
|
+
export interface VoiceCharacterEntry {
|
|
13
|
+
readonly id: string
|
|
14
|
+
readonly label: string
|
|
15
|
+
readonly description: string
|
|
16
|
+
readonly promptHint: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const VOICE_AGES: ReadonlyArray<VoiceCharacterEntry> = [
|
|
20
|
+
{ id: "infant", label: "Infant", description: "0-2 years", promptHint: "infant" },
|
|
21
|
+
{ id: "child", label: "Child", description: "5-12 years", promptHint: "child" },
|
|
22
|
+
{ id: "preteen", label: "Preteen", description: "10-13 years", promptHint: "preteen" },
|
|
23
|
+
{ id: "teen", label: "Teen", description: "13-19 years", promptHint: "teen" },
|
|
24
|
+
{ id: "young-adult", label: "Young Adult", description: "20-35 years", promptHint: "young adult" },
|
|
25
|
+
{ id: "middle-aged", label: "Middle-aged", description: "36-55 years", promptHint: "middle-aged" },
|
|
26
|
+
{ id: "mature", label: "Mature", description: "55-70 years", promptHint: "mature" },
|
|
27
|
+
{ id: "elderly", label: "Elderly", description: "70+ years", promptHint: "elderly" },
|
|
28
|
+
] as const
|
|
29
|
+
|
|
30
|
+
export const VOICE_GENDERS: ReadonlyArray<VoiceCharacterEntry> = [
|
|
31
|
+
{ id: "male", label: "Male", description: "Male voice", promptHint: "male" },
|
|
32
|
+
{ id: "female", label: "Female", description: "Female voice", promptHint: "female" },
|
|
33
|
+
{ id: "androgynous", label: "Androgynous", description: "Gender-neutral", promptHint: "androgynous" },
|
|
34
|
+
] as const
|
|
35
|
+
|
|
36
|
+
export const VOICE_LANGUAGES: ReadonlyArray<VoiceCharacterEntry> = [
|
|
37
|
+
{ id: "english", label: "English", description: "English", promptHint: "English" },
|
|
38
|
+
{ id: "spanish", label: "Spanish", description: "Spanish (Castilian / LatAm)", promptHint: "Spanish" },
|
|
39
|
+
{ id: "french", label: "French", description: "French", promptHint: "French" },
|
|
40
|
+
{ id: "german", label: "German", description: "German", promptHint: "German" },
|
|
41
|
+
{ id: "italian", label: "Italian", description: "Italian", promptHint: "Italian" },
|
|
42
|
+
{ id: "portuguese", label: "Portuguese", description: "Portuguese / Brazilian", promptHint: "Portuguese" },
|
|
43
|
+
{ id: "dutch", label: "Dutch", description: "Dutch", promptHint: "Dutch" },
|
|
44
|
+
{ id: "russian", label: "Russian", description: "Russian", promptHint: "Russian" },
|
|
45
|
+
{ id: "polish", label: "Polish", description: "Polish", promptHint: "Polish" },
|
|
46
|
+
{ id: "ukrainian", label: "Ukrainian", description: "Ukrainian", promptHint: "Ukrainian" },
|
|
47
|
+
{ id: "swedish", label: "Swedish", description: "Swedish", promptHint: "Swedish" },
|
|
48
|
+
{ id: "norwegian", label: "Norwegian", description: "Norwegian", promptHint: "Norwegian" },
|
|
49
|
+
{ id: "danish", label: "Danish", description: "Danish", promptHint: "Danish" },
|
|
50
|
+
{ id: "finnish", label: "Finnish", description: "Finnish", promptHint: "Finnish" },
|
|
51
|
+
{ id: "greek", label: "Greek", description: "Greek", promptHint: "Greek" },
|
|
52
|
+
{ id: "turkish", label: "Turkish", description: "Turkish", promptHint: "Turkish" },
|
|
53
|
+
{ id: "arabic", label: "Arabic", description: "Modern Standard Arabic", promptHint: "Arabic" },
|
|
54
|
+
{ id: "hebrew", label: "Hebrew", description: "Hebrew", promptHint: "Hebrew" },
|
|
55
|
+
{ id: "persian", label: "Persian", description: "Persian / Farsi", promptHint: "Persian" },
|
|
56
|
+
{ id: "hindi", label: "Hindi", description: "Hindi", promptHint: "Hindi" },
|
|
57
|
+
{ id: "bengali", label: "Bengali", description: "Bengali / Bangla", promptHint: "Bengali" },
|
|
58
|
+
{ id: "tamil", label: "Tamil", description: "Tamil", promptHint: "Tamil" },
|
|
59
|
+
{ id: "urdu", label: "Urdu", description: "Urdu", promptHint: "Urdu" },
|
|
60
|
+
{ id: "tagalog", label: "Tagalog", description: "Tagalog / Filipino", promptHint: "Tagalog" },
|
|
61
|
+
{ id: "indonesian", label: "Indonesian", description: "Bahasa Indonesia", promptHint: "Indonesian" },
|
|
62
|
+
{ id: "thai", label: "Thai", description: "Thai", promptHint: "Thai" },
|
|
63
|
+
{ id: "vietnamese", label: "Vietnamese", description: "Vietnamese", promptHint: "Vietnamese" },
|
|
64
|
+
{ id: "mandarin", label: "Mandarin", description: "Mandarin Chinese", promptHint: "Mandarin Chinese" },
|
|
65
|
+
{ id: "cantonese", label: "Cantonese", description: "Cantonese Chinese", promptHint: "Cantonese" },
|
|
66
|
+
{ id: "japanese", label: "Japanese", description: "Japanese", promptHint: "Japanese" },
|
|
67
|
+
{ id: "korean", label: "Korean", description: "Korean", promptHint: "Korean" },
|
|
68
|
+
{ id: "swahili", label: "Swahili", description: "Swahili", promptHint: "Swahili" },
|
|
69
|
+
{ id: "yoruba", label: "Yoruba", description: "Yoruba", promptHint: "Yoruba" },
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
export const VOICE_ACCENTS: ReadonlyArray<VoiceCharacterEntry> = [
|
|
73
|
+
// North America
|
|
74
|
+
{ id: "general-american", label: "General American", description: "Neutral US accent", promptHint: "general American" },
|
|
75
|
+
{ id: "southern-us", label: "Southern US", description: "US Southern drawl", promptHint: "Southern US" },
|
|
76
|
+
{ id: "new-york", label: "New York", description: "NY metro accent", promptHint: "New York" },
|
|
77
|
+
{ id: "boston", label: "Boston", description: "Boston / Mass accent", promptHint: "Boston" },
|
|
78
|
+
{ id: "midwestern-us", label: "Midwestern", description: "US heartland", promptHint: "Midwestern American" },
|
|
79
|
+
{ id: "chicago", label: "Chicago", description: "Upper-Midwest urban", promptHint: "Chicago" },
|
|
80
|
+
{ id: "appalachian", label: "Appalachian", description: "Mountain South", promptHint: "Appalachian" },
|
|
81
|
+
{ id: "canadian", label: "Canadian", description: "Canada English", promptHint: "Canadian" },
|
|
82
|
+
// British Isles
|
|
83
|
+
{ id: "british-rp", label: "British RP", description: "Received Pronunciation", promptHint: "British RP" },
|
|
84
|
+
{ id: "cockney", label: "Cockney", description: "London working-class", promptHint: "Cockney" },
|
|
85
|
+
{ id: "estuary-english", label: "Estuary", description: "South-East England", promptHint: "Estuary English" },
|
|
86
|
+
{ id: "northern-english", label: "Northern English", description: "Manchester / Yorkshire", promptHint: "Northern English" },
|
|
87
|
+
{ id: "scouse", label: "Scouse", description: "Liverpool", promptHint: "Scouse" },
|
|
88
|
+
{ id: "geordie", label: "Geordie", description: "Newcastle", promptHint: "Geordie" },
|
|
89
|
+
{ id: "scottish", label: "Scottish", description: "Scotland", promptHint: "Scottish" },
|
|
90
|
+
{ id: "irish", label: "Irish", description: "Ireland", promptHint: "Irish" },
|
|
91
|
+
{ id: "welsh", label: "Welsh", description: "Wales", promptHint: "Welsh" },
|
|
92
|
+
// English-speaking world
|
|
93
|
+
{ id: "australian", label: "Australian", description: "Australia", promptHint: "Australian" },
|
|
94
|
+
{ id: "new-zealand", label: "New Zealand", description: "NZ Kiwi", promptHint: "New Zealand" },
|
|
95
|
+
{ id: "south-african", label: "South African", description: "South Africa", promptHint: "South African" },
|
|
96
|
+
{ id: "indian-english", label: "Indian English", description: "South Asian English", promptHint: "Indian English" },
|
|
97
|
+
{ id: "caribbean", label: "Caribbean", description: "Caribbean English", promptHint: "Caribbean" },
|
|
98
|
+
{ id: "jamaican", label: "Jamaican", description: "Jamaican Patois", promptHint: "Jamaican" },
|
|
99
|
+
// Continental European-accented English
|
|
100
|
+
{ id: "french-accented", label: "French", description: "French-accented English", promptHint: "French-accented" },
|
|
101
|
+
{ id: "italian-accented", label: "Italian", description: "Italian-accented English", promptHint: "Italian-accented" },
|
|
102
|
+
{ id: "german-accented", label: "German", description: "German-accented English", promptHint: "German-accented" },
|
|
103
|
+
{ id: "dutch-accented", label: "Dutch", description: "Dutch-accented English", promptHint: "Dutch-accented" },
|
|
104
|
+
{ id: "russian-accented", label: "Russian", description: "Russian-accented English", promptHint: "Russian-accented" },
|
|
105
|
+
{ id: "polish-accented", label: "Polish", description: "Polish-accented English", promptHint: "Polish-accented" },
|
|
106
|
+
{ id: "spanish-accented", label: "Spanish", description: "Spanish-accented English", promptHint: "Spanish-accented" },
|
|
107
|
+
{ id: "portuguese-accented", label: "Portuguese", description: "Portuguese / Brazilian", promptHint: "Portuguese-accented" },
|
|
108
|
+
{ id: "scandinavian-accented", label: "Scandinavian", description: "Nordic-accented English", promptHint: "Scandinavian-accented" },
|
|
109
|
+
// Latin America
|
|
110
|
+
{ id: "mexican-accented", label: "Mexican", description: "Mexican-accented English", promptHint: "Mexican-accented" },
|
|
111
|
+
{ id: "argentinian-accented", label: "Argentinian", description: "Río de la Plata", promptHint: "Argentinian-accented" },
|
|
112
|
+
// Asia / Middle East
|
|
113
|
+
{ id: "japanese-accented", label: "Japanese", description: "Japanese-accented English", promptHint: "Japanese-accented" },
|
|
114
|
+
{ id: "korean-accented", label: "Korean", description: "Korean-accented English", promptHint: "Korean-accented" },
|
|
115
|
+
{ id: "chinese-accented", label: "Chinese", description: "Mandarin-accented", promptHint: "Chinese-accented" },
|
|
116
|
+
{ id: "filipino-accented", label: "Filipino", description: "Filipino-accented English", promptHint: "Filipino-accented" },
|
|
117
|
+
{ id: "arabic-accented", label: "Arabic", description: "Arabic-accented English", promptHint: "Arabic-accented" },
|
|
118
|
+
{ id: "hebrew-accented", label: "Hebrew", description: "Israeli-accented English", promptHint: "Hebrew-accented" },
|
|
119
|
+
{ id: "turkish-accented", label: "Turkish", description: "Turkish-accented English", promptHint: "Turkish-accented" },
|
|
120
|
+
{ id: "persian-accented", label: "Persian", description: "Persian-accented English", promptHint: "Persian-accented" },
|
|
121
|
+
// Africa
|
|
122
|
+
{ id: "nigerian-accented", label: "Nigerian", description: "Nigerian English", promptHint: "Nigerian-accented" },
|
|
123
|
+
// General
|
|
124
|
+
{ id: "neutral-international", label: "Neutral International", description: "Region-agnostic", promptHint: "neutral international" },
|
|
125
|
+
{ id: "transatlantic", label: "Transatlantic", description: "Mid-Atlantic theatrical", promptHint: "transatlantic" },
|
|
126
|
+
] as const
|
|
127
|
+
|
|
128
|
+
export const VOICE_TIMBRES: ReadonlyArray<VoiceCharacterEntry> = [
|
|
129
|
+
{ id: "warm", label: "Warm", description: "Rich, inviting", promptHint: "warm" },
|
|
130
|
+
{ id: "smooth", label: "Smooth", description: "Clean, even", promptHint: "smooth" },
|
|
131
|
+
{ id: "silky", label: "Silky", description: "Soft, refined", promptHint: "silky" },
|
|
132
|
+
{ id: "velvety", label: "Velvety", description: "Lush, plush", promptHint: "velvety" },
|
|
133
|
+
{ id: "raspy", label: "Raspy", description: "Rough, textured", promptHint: "raspy" },
|
|
134
|
+
{ id: "gravelly", label: "Gravelly", description: "Deeply textured", promptHint: "gravelly" },
|
|
135
|
+
{ id: "rough", label: "Rough", description: "Coarse, weathered", promptHint: "rough" },
|
|
136
|
+
{ id: "husky", label: "Husky", description: "Throaty, hoarse", promptHint: "husky" },
|
|
137
|
+
{ id: "breathy", label: "Breathy", description: "Airy, intimate", promptHint: "breathy" },
|
|
138
|
+
{ id: "whispered", label: "Whispered", description: "Hushed, intimate", promptHint: "whispered" },
|
|
139
|
+
{ id: "nasal", label: "Nasal", description: "Resonates in nose", promptHint: "nasal" },
|
|
140
|
+
{ id: "twangy", label: "Twangy", description: "Sharp, regional", promptHint: "twangy" },
|
|
141
|
+
{ id: "deep", label: "Deep", description: "Low pitch range", promptHint: "deep" },
|
|
142
|
+
{ id: "booming", label: "Booming", description: "Resonant, large", promptHint: "booming" },
|
|
143
|
+
{ id: "high-pitched", label: "High-pitched", description: "Upper register", promptHint: "high-pitched" },
|
|
144
|
+
{ id: "squeaky", label: "Squeaky", description: "Thin, piercing", promptHint: "squeaky" },
|
|
145
|
+
{ id: "bright", label: "Bright", description: "Crisp, forward", promptHint: "bright" },
|
|
146
|
+
{ id: "dark", label: "Dark", description: "Heavy, somber", promptHint: "dark" },
|
|
147
|
+
{ id: "youthful", label: "Youthful", description: "Light, fresh", promptHint: "youthful" },
|
|
148
|
+
{ id: "authoritative",label: "Authoritative",description: "Commanding", promptHint: "authoritative" },
|
|
149
|
+
{ id: "sultry", label: "Sultry", description: "Sensual, smoky", promptHint: "sultry" },
|
|
150
|
+
{ id: "polished", label: "Polished", description: "Practiced, broadcast-ready", promptHint: "polished" },
|
|
151
|
+
] as const
|
|
152
|
+
|
|
153
|
+
const AGE_BY_ID = new Map(VOICE_AGES.map((x) => [x.id, x]))
|
|
154
|
+
const GENDER_BY_ID = new Map(VOICE_GENDERS.map((x) => [x.id, x]))
|
|
155
|
+
const LANGUAGE_BY_ID = new Map(VOICE_LANGUAGES.map((x) => [x.id, x]))
|
|
156
|
+
const ACCENT_BY_ID = new Map(VOICE_ACCENTS.map((x) => [x.id, x]))
|
|
157
|
+
const TIMBRE_BY_ID = new Map(VOICE_TIMBRES.map((x) => [x.id, x]))
|
|
158
|
+
|
|
159
|
+
export function getVoiceAge(id: string | undefined) { return id ? AGE_BY_ID.get(id) : undefined }
|
|
160
|
+
export function getVoiceGender(id: string | undefined) { return id ? GENDER_BY_ID.get(id) : undefined }
|
|
161
|
+
export function getVoiceLanguage(id: string | undefined) { return id ? LANGUAGE_BY_ID.get(id) : undefined }
|
|
162
|
+
export function getVoiceAccent(id: string | undefined) { return id ? ACCENT_BY_ID.get(id) : undefined }
|
|
163
|
+
export function getVoiceTimbre(id: string | undefined) { return id ? TIMBRE_BY_ID.get(id) : undefined }
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Compose a natural-language voice character clause.
|
|
167
|
+
* Examples (depending on which sub-fields are set):
|
|
168
|
+
* { age, gender, timbre, accent } → "middle-aged male voice with warm timbre and British RP accent"
|
|
169
|
+
* { timbre } → "warm timbre"
|
|
170
|
+
* { accent } → "British RP accent"
|
|
171
|
+
* { age, gender } → "middle-aged male voice"
|
|
172
|
+
* { language: ["english","spanish"] } → "English / Spanish voice"
|
|
173
|
+
* { } → ""
|
|
174
|
+
*
|
|
175
|
+
* `language` is multi-pick — multiple languages emit "English / Spanish"
|
|
176
|
+
* for codeswitching / multilingual voices.
|
|
177
|
+
*/
|
|
178
|
+
export function buildVoiceCharacterHints(data: {
|
|
179
|
+
readonly preText?: string
|
|
180
|
+
readonly postText?: string
|
|
181
|
+
readonly age?: string
|
|
182
|
+
readonly gender?: string
|
|
183
|
+
readonly language?: string | ReadonlyArray<string>
|
|
184
|
+
readonly accent?: string
|
|
185
|
+
readonly timbre?: string
|
|
186
|
+
}): string {
|
|
187
|
+
const fragments: string[] = []
|
|
188
|
+
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
189
|
+
if (pre) fragments.push(pre)
|
|
190
|
+
|
|
191
|
+
const age = getVoiceAge(data.age)
|
|
192
|
+
const gender = getVoiceGender(data.gender)
|
|
193
|
+
const accent = getVoiceAccent(data.accent)
|
|
194
|
+
const timbre = getVoiceTimbre(data.timbre)
|
|
195
|
+
|
|
196
|
+
const langIds = pickIds(data.language)
|
|
197
|
+
const langHints = langIds
|
|
198
|
+
.map((id) => getVoiceLanguage(id)?.promptHint)
|
|
199
|
+
.filter((h): h is string => !!h)
|
|
200
|
+
const langClause = langHints.join(" / ")
|
|
201
|
+
|
|
202
|
+
const ageGender = [age?.promptHint, gender?.promptHint].filter(Boolean).join(" ")
|
|
203
|
+
const traits: string[] = []
|
|
204
|
+
if (timbre) traits.push(`${timbre.promptHint} timbre`)
|
|
205
|
+
if (accent) traits.push(`${accent.promptHint} accent`)
|
|
206
|
+
|
|
207
|
+
let core = ""
|
|
208
|
+
if (ageGender && traits.length > 0) {
|
|
209
|
+
core = `${ageGender} voice with ${traits.join(" and ")}`
|
|
210
|
+
} else if (ageGender) {
|
|
211
|
+
core = `${ageGender} voice`
|
|
212
|
+
} else if (traits.length > 0) {
|
|
213
|
+
core = traits.join(" and ")
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let main = ""
|
|
217
|
+
if (langClause && core) main = `${langClause}-speaking ${core}`
|
|
218
|
+
else if (langClause) main = `${langClause} voice`
|
|
219
|
+
else main = core
|
|
220
|
+
|
|
221
|
+
if (main) fragments.push(main)
|
|
222
|
+
|
|
223
|
+
const post = typeof data.postText === "string" ? data.postText.trim() : ""
|
|
224
|
+
if (post) fragments.push(post)
|
|
225
|
+
|
|
226
|
+
return fragments.join(", ")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const VOICE_CHARACTER_DEFAULT_DATA: {
|
|
230
|
+
preText?: string
|
|
231
|
+
postText?: string
|
|
232
|
+
age?: string
|
|
233
|
+
gender?: string
|
|
234
|
+
language?: string | ReadonlyArray<string>
|
|
235
|
+
accent?: string
|
|
236
|
+
timbre?: string
|
|
237
|
+
} = {}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice-delivery catalog: pace + emotion + archetype. Feeds
|
|
3
|
+
* Voice Design's voiceDescription via the Sound aggregator.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface VoiceDeliveryEntry {
|
|
7
|
+
readonly id: string
|
|
8
|
+
readonly label: string
|
|
9
|
+
readonly description: string
|
|
10
|
+
readonly promptHint: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VOICE_PACES: ReadonlyArray<VoiceDeliveryEntry> = [
|
|
14
|
+
{ id: "very-slow", label: "Very Slow", description: "Deliberate, ponderous", promptHint: "very slow" },
|
|
15
|
+
{ id: "slow", label: "Slow", description: "Measured", promptHint: "slow" },
|
|
16
|
+
{ id: "drawl", label: "Drawl", description: "Drawn-out, lazy", promptHint: "drawled" },
|
|
17
|
+
{ id: "measured", label: "Measured", description: "Steady, intentional", promptHint: "measured" },
|
|
18
|
+
{ id: "moderate", label: "Moderate", description: "Conversational pace", promptHint: "moderate" },
|
|
19
|
+
{ id: "punchy", label: "Punchy", description: "Clipped, emphatic", promptHint: "punchy" },
|
|
20
|
+
{ id: "brisk", label: "Brisk", description: "Lively pace", promptHint: "brisk" },
|
|
21
|
+
{ id: "rapid", label: "Rapid", description: "Fast", promptHint: "rapid" },
|
|
22
|
+
{ id: "rapid-fire", label: "Rapid-fire", description: "Machine-gun delivery", promptHint: "rapid-fire" },
|
|
23
|
+
{ id: "frenetic", label: "Frenetic", description: "Breathless, urgent", promptHint: "frenetic" },
|
|
24
|
+
{ id: "halting", label: "Halting", description: "Pausing, uncertain", promptHint: "halting" },
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
export const VOICE_EMOTIONS: ReadonlyArray<VoiceDeliveryEntry> = [
|
|
28
|
+
{ id: "neutral", label: "Neutral", description: "Even, matter-of-fact", promptHint: "neutral" },
|
|
29
|
+
{ id: "cheerful", label: "Cheerful", description: "Upbeat, positive", promptHint: "cheerful" },
|
|
30
|
+
{ id: "warm", label: "Warm", description: "Friendly", promptHint: "warm" },
|
|
31
|
+
{ id: "reassuring", label: "Reassuring", description: "Calming, grounding", promptHint: "reassuring" },
|
|
32
|
+
{ id: "compassionate",label: "Compassionate",description: "Empathetic, caring", promptHint: "compassionate" },
|
|
33
|
+
{ id: "somber", label: "Somber", description: "Serious, grave", promptHint: "somber" },
|
|
34
|
+
{ id: "stoic", label: "Stoic", description: "Restrained, composed", promptHint: "stoic" },
|
|
35
|
+
{ id: "urgent", label: "Urgent", description: "Pressing", promptHint: "urgent" },
|
|
36
|
+
{ id: "frantic", label: "Frantic", description: "Panicked, scattered", promptHint: "frantic" },
|
|
37
|
+
{ id: "anxious", label: "Anxious", description: "Tense", promptHint: "anxious" },
|
|
38
|
+
{ id: "scared", label: "Scared", description: "Afraid, trembling", promptHint: "scared" },
|
|
39
|
+
{ id: "excited", label: "Excited", description: "Energetic, animated", promptHint: "excited" },
|
|
40
|
+
{ id: "enthusiastic",label: "Enthusiastic",description: "Hyped, eager", promptHint: "enthusiastic" },
|
|
41
|
+
{ id: "wistful", label: "Wistful", description: "Yearning, nostalgic", promptHint: "wistful" },
|
|
42
|
+
{ id: "melancholic", label: "Melancholic", description: "Sad, mourning", promptHint: "melancholic" },
|
|
43
|
+
{ id: "menacing", label: "Menacing", description: "Threatening", promptHint: "menacing" },
|
|
44
|
+
{ id: "angry", label: "Angry", description: "Hostile, hot", promptHint: "angry" },
|
|
45
|
+
{ id: "sarcastic", label: "Sarcastic", description: "Wry, dry", promptHint: "sarcastic" },
|
|
46
|
+
{ id: "smug", label: "Smug", description: "Self-satisfied", promptHint: "smug" },
|
|
47
|
+
{ id: "playful", label: "Playful", description: "Light, teasing", promptHint: "playful" },
|
|
48
|
+
{ id: "flirty", label: "Flirty", description: "Suggestive, teasing", promptHint: "flirty" },
|
|
49
|
+
{ id: "intimate", label: "Intimate", description: "Close, personal", promptHint: "intimate" },
|
|
50
|
+
{ id: "seductive", label: "Seductive", description: "Sensual, alluring", promptHint: "seductive" },
|
|
51
|
+
] as const
|
|
52
|
+
|
|
53
|
+
export const VOICE_ARCHETYPES: ReadonlyArray<VoiceDeliveryEntry> = [
|
|
54
|
+
// Broadcast / Narration
|
|
55
|
+
{ id: "newscaster", label: "Newscaster", description: "Authoritative news delivery", promptHint: "newscaster" },
|
|
56
|
+
{ id: "anchor", label: "TV Anchor", description: "Polished broadcast", promptHint: "TV anchor" },
|
|
57
|
+
{ id: "documentary-narrator", label: "Documentary Narrator", description: "Informative, measured", promptHint: "documentary narrator" },
|
|
58
|
+
{ id: "nature-narrator", label: "Nature Narrator", description: "Soft-spoken Attenborough-style", promptHint: "nature documentary narrator" },
|
|
59
|
+
{ id: "fairy-tale-narrator", label: "Fairy-Tale Narrator", description: "Storyteller for children", promptHint: "fairy-tale narrator" },
|
|
60
|
+
{ id: "audiobook-narrator", label: "Audiobook Narrator", description: "Long-form fiction reading", promptHint: "audiobook narrator" },
|
|
61
|
+
{ id: "podcaster", label: "Podcaster", description: "Conversational", promptHint: "podcaster" },
|
|
62
|
+
{ id: "talk-show-host", label: "Talk Show Host", description: "Personable interviewer", promptHint: "talk show host" },
|
|
63
|
+
{ id: "radio-dj", label: "Radio DJ", description: "Smooth FM voice", promptHint: "radio DJ" },
|
|
64
|
+
// Performance
|
|
65
|
+
{ id: "asmr", label: "ASMR", description: "Whispered, intimate", promptHint: "ASMR" },
|
|
66
|
+
{ id: "sportscaster", label: "Sportscaster", description: "Excitable play-by-play", promptHint: "sportscaster" },
|
|
67
|
+
{ id: "auctioneer", label: "Auctioneer", description: "Rapid bidding chant", promptHint: "auctioneer" },
|
|
68
|
+
{ id: "preacher", label: "Preacher", description: "Impassioned sermon", promptHint: "preacher" },
|
|
69
|
+
{ id: "politician", label: "Politician", description: "Stump-speech rhetoric", promptHint: "politician" },
|
|
70
|
+
{ id: "lawyer", label: "Lawyer", description: "Courtroom argument", promptHint: "lawyer" },
|
|
71
|
+
{ id: "professor", label: "Professor", description: "Lecturer, academic", promptHint: "professor" },
|
|
72
|
+
{ id: "infomercial", label: "Infomercial", description: "Selling, hyped", promptHint: "infomercial" },
|
|
73
|
+
{ id: "voice-actor", label: "Voice Actor", description: "Animated character work", promptHint: "voice actor" },
|
|
74
|
+
{ id: "drag-queen", label: "Drag Queen", description: "Theatrical, fierce", promptHint: "drag queen" },
|
|
75
|
+
// Character archetypes
|
|
76
|
+
{ id: "villain", label: "Villain", description: "Sinister, scheming", promptHint: "villain" },
|
|
77
|
+
{ id: "hero", label: "Hero", description: "Earnest, courageous", promptHint: "heroic" },
|
|
78
|
+
{ id: "mentor", label: "Mentor", description: "Wise, guiding", promptHint: "mentor" },
|
|
79
|
+
{ id: "comedian", label: "Comedian", description: "Comedic timing", promptHint: "comedic" },
|
|
80
|
+
{ id: "stand-up-comic", label: "Stand-up Comic", description: "Punchline-driven", promptHint: "stand-up comic" },
|
|
81
|
+
{ id: "drill-sergeant", label: "Drill Sergeant", description: "Barking commands", promptHint: "drill sergeant" },
|
|
82
|
+
{ id: "meditation-guide", label: "Meditation Guide", description: "Soft, slow guidance", promptHint: "meditation guide" },
|
|
83
|
+
{ id: "yoga-instructor", label: "Yoga Instructor", description: "Calm, grounding cues", promptHint: "yoga instructor" },
|
|
84
|
+
{ id: "noir-detective", label: "Noir Detective", description: "Hard-boiled, brooding", promptHint: "noir detective" },
|
|
85
|
+
{ id: "wizard", label: "Wizard", description: "Mystic, ancient", promptHint: "wizard" },
|
|
86
|
+
{ id: "robot", label: "Robot", description: "Synthetic, mechanical", promptHint: "robotic" },
|
|
87
|
+
{ id: "ai-assistant", label: "AI Assistant", description: "Polished synthetic helper", promptHint: "AI assistant" },
|
|
88
|
+
{ id: "siri-style", label: "Virtual Assistant", description: "Siri/Alexa-style helper", promptHint: "virtual assistant" },
|
|
89
|
+
] as const
|
|
90
|
+
|
|
91
|
+
const PACE_BY_ID = new Map(VOICE_PACES.map((x) => [x.id, x]))
|
|
92
|
+
const EMOTION_BY_ID = new Map(VOICE_EMOTIONS.map((x) => [x.id, x]))
|
|
93
|
+
const ARCHETYPE_BY_ID = new Map(VOICE_ARCHETYPES.map((x) => [x.id, x]))
|
|
94
|
+
|
|
95
|
+
export function getVoicePace(id: string | undefined) { return id ? PACE_BY_ID.get(id) : undefined }
|
|
96
|
+
export function getVoiceEmotion(id: string | undefined) { return id ? EMOTION_BY_ID.get(id) : undefined }
|
|
97
|
+
export function getVoiceArchetype(id: string | undefined) { return id ? ARCHETYPE_BY_ID.get(id) : undefined }
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compose a delivery clause.
|
|
101
|
+
* Examples:
|
|
102
|
+
* { pace, archetype, emotion } → "measured documentary-narrator-style delivery, reassuring tone"
|
|
103
|
+
* { archetype } → "documentary-narrator-style delivery"
|
|
104
|
+
* { emotion } → "reassuring tone"
|
|
105
|
+
* { pace } → "measured pace"
|
|
106
|
+
*/
|
|
107
|
+
export function buildVoiceDeliveryHints(data: {
|
|
108
|
+
readonly preText?: string
|
|
109
|
+
readonly postText?: string
|
|
110
|
+
readonly pace?: string
|
|
111
|
+
readonly emotion?: string
|
|
112
|
+
readonly archetype?: string
|
|
113
|
+
}): string {
|
|
114
|
+
const fragments: string[] = []
|
|
115
|
+
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
116
|
+
if (pre) fragments.push(pre)
|
|
117
|
+
|
|
118
|
+
const pace = getVoicePace(data.pace)
|
|
119
|
+
const emotion = getVoiceEmotion(data.emotion)
|
|
120
|
+
const archetype = getVoiceArchetype(data.archetype)
|
|
121
|
+
|
|
122
|
+
let deliveryClause = ""
|
|
123
|
+
if (pace && archetype) deliveryClause = `${pace.promptHint} ${archetype.promptHint}-style delivery`
|
|
124
|
+
else if (archetype) deliveryClause = `${archetype.promptHint}-style delivery`
|
|
125
|
+
else if (pace) deliveryClause = `${pace.promptHint} pace`
|
|
126
|
+
|
|
127
|
+
const emotionClause = emotion ? `${emotion.promptHint} tone` : ""
|
|
128
|
+
|
|
129
|
+
let main = ""
|
|
130
|
+
if (deliveryClause && emotionClause) main = `${deliveryClause}, ${emotionClause}`
|
|
131
|
+
else main = deliveryClause || emotionClause
|
|
132
|
+
if (main) fragments.push(main)
|
|
133
|
+
|
|
134
|
+
const post = typeof data.postText === "string" ? data.postText.trim() : ""
|
|
135
|
+
if (post) fragments.push(post)
|
|
136
|
+
|
|
137
|
+
return fragments.join(", ")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const VOICE_DELIVERY_DEFAULT_DATA: {
|
|
141
|
+
preText?: string; postText?: string; pace?: string; emotion?: string; archetype?: string
|
|
142
|
+
} = {}
|
package/src/wardrobe.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { pickIds } from "@nodaro/shared"
|
|
2
|
+
|
|
3
|
+
export type WardrobeDimension =
|
|
4
|
+
| "archetype" | "top" | "bottom" | "outerwear" | "footwear"
|
|
5
|
+
| "headwear" | "accessories" | "color-palette" | "material" | "era"
|
|
6
|
+
|
|
7
|
+
export interface WardrobeEntry {
|
|
8
|
+
readonly id: string
|
|
9
|
+
readonly label: string
|
|
10
|
+
readonly dimension: WardrobeDimension
|
|
11
|
+
readonly promptHint: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WardrobeValue {
|
|
15
|
+
archetype?: string
|
|
16
|
+
top?: string
|
|
17
|
+
bottom?: string
|
|
18
|
+
outerwear?: string
|
|
19
|
+
footwear?: string
|
|
20
|
+
headwear?: string | ReadonlyArray<string>
|
|
21
|
+
accessories?: string | ReadonlyArray<string>
|
|
22
|
+
colorPalette?: string
|
|
23
|
+
material?: string
|
|
24
|
+
era?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const WARDROBE_DIMENSION_ORDER: ReadonlyArray<WardrobeDimension> = [
|
|
28
|
+
"archetype", "top", "bottom", "outerwear", "footwear",
|
|
29
|
+
"headwear", "accessories", "color-palette", "material", "era",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export const WARDROBE_CATEGORY_LABELS: Readonly<Record<WardrobeDimension, string>> = {
|
|
33
|
+
archetype: "Archetype", top: "Top", bottom: "Bottom", outerwear: "Outerwear",
|
|
34
|
+
footwear: "Footwear", headwear: "Headwear", accessories: "Accessories",
|
|
35
|
+
"color-palette": "Color Palette", material: "Material", era: "Era",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const WARDROBE_FIELD_BY_DIMENSION: Record<WardrobeDimension, keyof WardrobeValue> = {
|
|
39
|
+
archetype: "archetype", top: "top", bottom: "bottom", outerwear: "outerwear",
|
|
40
|
+
footwear: "footwear", headwear: "headwear", accessories: "accessories",
|
|
41
|
+
"color-palette": "colorPalette", material: "material", era: "era",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Multi-pick dims emit each picked entry independently. */
|
|
45
|
+
const MULTI_PICK: ReadonlySet<WardrobeDimension> = new Set(["headwear", "accessories"])
|
|
46
|
+
/** Sentinel ids that mean "nothing here" and emit no hint. */
|
|
47
|
+
const NONE_IDS: ReadonlySet<string> = new Set(["wd-outer-none"])
|
|
48
|
+
|
|
49
|
+
export const WARDROBE: ReadonlyArray<WardrobeEntry> = [
|
|
50
|
+
// archetype
|
|
51
|
+
{ id: "wd-casual", label: "Casual", dimension: "archetype", promptHint: "in casual everyday clothing" },
|
|
52
|
+
{ id: "wd-business", label: "Business", dimension: "archetype", promptHint: "in smart business attire" },
|
|
53
|
+
{ id: "wd-formal", label: "Formal", dimension: "archetype", promptHint: "in elegant formal wear" },
|
|
54
|
+
{ id: "wd-streetwear", label: "Streetwear", dimension: "archetype", promptHint: "in contemporary streetwear" },
|
|
55
|
+
{ id: "wd-athletic", label: "Athletic", dimension: "archetype", promptHint: "in athletic sportswear" },
|
|
56
|
+
{ id: "wd-fantasy", label: "Fantasy", dimension: "archetype", promptHint: "in fantasy costume" },
|
|
57
|
+
{ id: "wd-scifi", label: "Sci-Fi", dimension: "archetype", promptHint: "in futuristic sci-fi attire" },
|
|
58
|
+
{ id: "wd-historical", label: "Historical", dimension: "archetype", promptHint: "in period historical dress" },
|
|
59
|
+
{ id: "wd-uniform", label: "Uniform", dimension: "archetype", promptHint: "in a uniform" },
|
|
60
|
+
{ id: "wd-loungewear", label: "Loungewear", dimension: "archetype", promptHint: "in relaxed loungewear" },
|
|
61
|
+
// top
|
|
62
|
+
{ id: "wd-tshirt", label: "T-Shirt", dimension: "top", promptHint: "wearing a t-shirt" },
|
|
63
|
+
{ id: "wd-blouse", label: "Blouse", dimension: "top", promptHint: "wearing a blouse" },
|
|
64
|
+
{ id: "wd-hoodie", label: "Hoodie", dimension: "top", promptHint: "wearing a hoodie" },
|
|
65
|
+
{ id: "wd-buttondown", label: "Button-down", dimension: "top", promptHint: "wearing a button-down shirt" },
|
|
66
|
+
{ id: "wd-tank", label: "Tank", dimension: "top", promptHint: "wearing a tank top" },
|
|
67
|
+
{ id: "wd-sweater", label: "Sweater", dimension: "top", promptHint: "wearing a sweater" },
|
|
68
|
+
{ id: "wd-turtleneck", label: "Turtleneck", dimension: "top", promptHint: "wearing a turtleneck" },
|
|
69
|
+
// bottom
|
|
70
|
+
{ id: "wd-jeans", label: "Jeans", dimension: "bottom", promptHint: "wearing jeans" },
|
|
71
|
+
{ id: "wd-trousers", label: "Trousers", dimension: "bottom", promptHint: "wearing tailored trousers" },
|
|
72
|
+
{ id: "wd-skirt", label: "Skirt", dimension: "bottom", promptHint: "wearing a skirt" },
|
|
73
|
+
{ id: "wd-shorts", label: "Shorts", dimension: "bottom", promptHint: "wearing shorts" },
|
|
74
|
+
{ id: "wd-leggings", label: "Leggings", dimension: "bottom", promptHint: "wearing leggings" },
|
|
75
|
+
{ id: "wd-cargo", label: "Cargo", dimension: "bottom", promptHint: "wearing cargo pants" },
|
|
76
|
+
// outerwear
|
|
77
|
+
{ id: "wd-outer-none", label: "None", dimension: "outerwear", promptHint: "" },
|
|
78
|
+
{ id: "wd-leatherjkt", label: "Leather Jacket", dimension: "outerwear", promptHint: "in a leather jacket" },
|
|
79
|
+
{ id: "wd-blazer", label: "Blazer", dimension: "outerwear", promptHint: "in a blazer" },
|
|
80
|
+
{ id: "wd-trench", label: "Trench Coat", dimension: "outerwear", promptHint: "in a trench coat" },
|
|
81
|
+
{ id: "wd-parka", label: "Parka", dimension: "outerwear", promptHint: "in a parka" },
|
|
82
|
+
{ id: "wd-cloak", label: "Cloak", dimension: "outerwear", promptHint: "in a flowing cloak" },
|
|
83
|
+
{ id: "wd-denimjkt", label: "Denim Jacket", dimension: "outerwear", promptHint: "in a denim jacket" },
|
|
84
|
+
{ id: "wd-cardigan", label: "Cardigan", dimension: "outerwear", promptHint: "in a cardigan" },
|
|
85
|
+
// footwear
|
|
86
|
+
{ id: "wd-sneakers", label: "Sneakers", dimension: "footwear", promptHint: "wearing sneakers" },
|
|
87
|
+
{ id: "wd-boots", label: "Boots", dimension: "footwear", promptHint: "wearing boots" },
|
|
88
|
+
{ id: "wd-heels", label: "Heels", dimension: "footwear", promptHint: "wearing heels" },
|
|
89
|
+
{ id: "wd-sandals", label: "Sandals", dimension: "footwear", promptHint: "wearing sandals" },
|
|
90
|
+
{ id: "wd-dressshoe", label: "Dress Shoes", dimension: "footwear", promptHint: "wearing dress shoes" },
|
|
91
|
+
{ id: "wd-barefoot", label: "Barefoot", dimension: "footwear", promptHint: "barefoot" },
|
|
92
|
+
// headwear (multi)
|
|
93
|
+
{ id: "wd-cap", label: "Cap", dimension: "headwear", promptHint: "wearing a cap" },
|
|
94
|
+
{ id: "wd-beanie", label: "Beanie", dimension: "headwear", promptHint: "wearing a beanie" },
|
|
95
|
+
{ id: "wd-widehat", label: "Wide-brim Hat", dimension: "headwear", promptHint: "wearing a wide-brim hat" },
|
|
96
|
+
{ id: "wd-hood", label: "Hood", dimension: "headwear", promptHint: "with the hood up" },
|
|
97
|
+
{ id: "wd-crown", label: "Crown", dimension: "headwear", promptHint: "wearing a crown" },
|
|
98
|
+
{ id: "wd-helmet", label: "Helmet", dimension: "headwear", promptHint: "wearing a helmet" },
|
|
99
|
+
// accessories (multi)
|
|
100
|
+
{ id: "wd-glasses", label: "Glasses", dimension: "accessories", promptHint: "wearing glasses" },
|
|
101
|
+
{ id: "wd-sunglasses", label: "Sunglasses", dimension: "accessories", promptHint: "wearing sunglasses" },
|
|
102
|
+
{ id: "wd-scarf", label: "Scarf", dimension: "accessories", promptHint: "wearing a scarf" },
|
|
103
|
+
{ id: "wd-gloves", label: "Gloves", dimension: "accessories", promptHint: "wearing gloves" },
|
|
104
|
+
{ id: "wd-jewelry", label: "Jewelry", dimension: "accessories", promptHint: "wearing statement jewelry" },
|
|
105
|
+
{ id: "wd-belt", label: "Belt", dimension: "accessories", promptHint: "with a belt" },
|
|
106
|
+
{ id: "wd-watch", label: "Watch", dimension: "accessories", promptHint: "wearing a watch" },
|
|
107
|
+
// color-palette
|
|
108
|
+
{ id: "wd-neutral", label: "Neutral", dimension: "color-palette", promptHint: "in a neutral color palette" },
|
|
109
|
+
{ id: "wd-all-black", label: "All Black", dimension: "color-palette", promptHint: "in an all-black palette" },
|
|
110
|
+
{ id: "wd-earth", label: "Earth Tones", dimension: "color-palette", promptHint: "in earth-tone colors" },
|
|
111
|
+
{ id: "wd-pastel", label: "Pastel", dimension: "color-palette", promptHint: "in pastel colors" },
|
|
112
|
+
{ id: "wd-jewel", label: "Jewel Tones", dimension: "color-palette", promptHint: "in rich jewel-tone colors" },
|
|
113
|
+
{ id: "wd-neon", label: "Neon", dimension: "color-palette", promptHint: "in vivid neon colors" },
|
|
114
|
+
{ id: "wd-all-white", label: "All White", dimension: "color-palette", promptHint: "in an all-white palette" },
|
|
115
|
+
// material
|
|
116
|
+
{ id: "wd-cotton", label: "Cotton", dimension: "material", promptHint: "in cotton fabric" },
|
|
117
|
+
{ id: "wd-denim", label: "Denim", dimension: "material", promptHint: "in denim" },
|
|
118
|
+
{ id: "wd-leather", label: "Leather", dimension: "material", promptHint: "in leather" },
|
|
119
|
+
{ id: "wd-silk", label: "Silk", dimension: "material", promptHint: "in silk" },
|
|
120
|
+
{ id: "wd-wool", label: "Wool", dimension: "material", promptHint: "in wool" },
|
|
121
|
+
{ id: "wd-latex", label: "Latex", dimension: "material", promptHint: "in latex" },
|
|
122
|
+
{ id: "wd-linen", label: "Linen", dimension: "material", promptHint: "in linen" },
|
|
123
|
+
// era
|
|
124
|
+
{ id: "wd-contemporary", label: "Contemporary", dimension: "era", promptHint: "in contemporary fashion" },
|
|
125
|
+
{ id: "wd-1920s", label: "1920s", dimension: "era", promptHint: "in 1920s fashion" },
|
|
126
|
+
{ id: "wd-1950s", label: "1950s", dimension: "era", promptHint: "in 1950s fashion" },
|
|
127
|
+
{ id: "wd-1980s", label: "1980s", dimension: "era", promptHint: "in 1980s fashion" },
|
|
128
|
+
{ id: "wd-victorian", label: "Victorian", dimension: "era", promptHint: "in Victorian-era dress" },
|
|
129
|
+
{ id: "wd-medieval", label: "Medieval", dimension: "era", promptHint: "in medieval dress" },
|
|
130
|
+
{ id: "wd-futuristic", label: "Futuristic", dimension: "era", promptHint: "in futuristic clothing" },
|
|
131
|
+
] as const
|
|
132
|
+
|
|
133
|
+
const BY_ID: Map<string, WardrobeEntry> = new Map(WARDROBE.map((e) => [e.id, e]))
|
|
134
|
+
|
|
135
|
+
/** Entries grouped by dimension — precomputed once so per-dimension lookups
|
|
136
|
+
* (the picker calls one per dimension per render) are O(1) with stable array
|
|
137
|
+
* refs, instead of a full WARDROBE scan each call. */
|
|
138
|
+
const BY_DIMENSION: Map<WardrobeDimension, WardrobeEntry[]> = (() => {
|
|
139
|
+
const map = new Map<WardrobeDimension, WardrobeEntry[]>()
|
|
140
|
+
for (const e of WARDROBE) {
|
|
141
|
+
const list = map.get(e.dimension)
|
|
142
|
+
if (list) list.push(e)
|
|
143
|
+
else map.set(e.dimension, [e])
|
|
144
|
+
}
|
|
145
|
+
return map
|
|
146
|
+
})()
|
|
147
|
+
|
|
148
|
+
export function getWardrobeEntry(id: string | undefined | null): WardrobeEntry | undefined {
|
|
149
|
+
return id ? BY_ID.get(id) : undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getWardrobePromptHint(id: string | undefined | null): string {
|
|
153
|
+
return getWardrobeEntry(id)?.promptHint ?? ""
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getWardrobeEntriesByDimension(dim: WardrobeDimension): WardrobeEntry[] {
|
|
157
|
+
return BY_DIMENSION.get(dim) ?? []
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildWardrobeHints(value: Record<string, unknown> & WardrobeValue): string[] {
|
|
161
|
+
const hints: string[] = []
|
|
162
|
+
for (const dimension of WARDROBE_DIMENSION_ORDER) {
|
|
163
|
+
const field = WARDROBE_FIELD_BY_DIMENSION[dimension]
|
|
164
|
+
const raw = value[field]
|
|
165
|
+
if (MULTI_PICK.has(dimension)) {
|
|
166
|
+
for (const id of pickIds(raw)) {
|
|
167
|
+
if (NONE_IDS.has(id)) continue
|
|
168
|
+
const h = getWardrobePromptHint(id)
|
|
169
|
+
if (h) hints.push(h)
|
|
170
|
+
}
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
const id = typeof raw === "string" ? raw : undefined
|
|
174
|
+
if (!id || NONE_IDS.has(id)) continue
|
|
175
|
+
const h = getWardrobePromptHint(id)
|
|
176
|
+
if (h) hints.push(h)
|
|
177
|
+
}
|
|
178
|
+
return hints
|
|
179
|
+
}
|