@nodaro/prompts 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +5453 -4271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1221 -63
- package/dist/index.d.ts +1221 -63
- package/dist/index.js +5366 -4273
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
- package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
- package/src/__tests__/catalog-packs.test.ts +130 -0
- package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
- package/src/__tests__/catalog-terms.test.ts +162 -0
- package/src/__tests__/character-default-role.test.ts +3 -2
- package/src/__tests__/content-free-contract.test.ts +45 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
- package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
- package/src/__tests__/parameter-hint-mode.test.ts +385 -0
- package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
- package/src/__tests__/person-packs.test.ts +166 -0
- package/src/__tests__/project-all-catalogs.test.ts +26 -0
- package/src/__tests__/prompt-builder.test.ts +53 -0
- package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
- package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
- package/src/__tests__/term.test.ts +104 -0
- package/src/__tests__/transitions.test.ts +8 -5
- package/src/__tests__/upstream-immutability.test.ts +23 -0
- package/src/action-fx.ts +67 -17
- package/src/aesthetic.ts +54 -6
- package/src/atmosphere.ts +64 -23
- package/src/backdrop.ts +44 -30
- package/src/camera-format.ts +33 -11
- package/src/camera-motions.ts +89 -0
- package/src/catalog-packs.ts +125 -0
- package/src/catalog-sidecar-coverage.ts +36 -0
- package/src/character-fx.ts +112 -39
- package/src/color-look.ts +44 -27
- package/src/composition-effects.ts +21 -7
- package/src/era.ts +24 -0
- package/src/exposure-settings.ts +76 -18
- package/src/framing.ts +100 -0
- package/src/held-prop.ts +125 -63
- package/src/identity-lock.ts +12 -5
- package/src/image-reference-doctrine.ts +55 -0
- package/src/index.ts +6 -0
- package/src/instrumentation.ts +148 -57
- package/src/lens.ts +31 -15
- package/src/lighting.ts +120 -59
- package/src/loop-subject.ts +27 -1
- package/src/materials.ts +123 -69
- package/src/mood.ts +123 -51
- package/src/music-genre.ts +171 -67
- package/src/music-mood.ts +85 -17
- package/src/parameter-prompt-hint.ts +168 -56
- package/src/person-packs.ts +182 -0
- package/src/person.ts +506 -408
- package/src/photo-genre.ts +37 -22
- package/src/photographer.ts +138 -1
- package/src/picker-catalogs.ts +86 -40
- package/src/picker-wiring.ts +1 -1
- package/src/pose.ts +105 -40
- package/src/post-process-effects.ts +51 -8
- package/src/prompt-builder.ts +16 -1
- package/src/provider-prompt-doctrine.ts +1 -2
- package/src/render-quality.ts +25 -7
- package/src/setting.ts +30 -14
- package/src/style-presets.ts +1 -1
- package/src/style.ts +33 -15
- package/src/styling.ts +155 -94
- package/src/surround-fill.ts +67 -0
- package/src/temporal.ts +80 -18
- package/src/term.ts +155 -0
- package/src/transitions.ts +73 -26
- package/src/voice-character.ts +190 -90
- package/src/voice-delivery.ts +83 -12
package/src/voice-character.ts
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { pickIds } from "@nodaro/shared"
|
|
11
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
11
12
|
|
|
12
13
|
export interface VoiceCharacterEntry {
|
|
13
14
|
readonly id: string
|
|
14
15
|
readonly label: string
|
|
15
16
|
readonly description: string
|
|
16
17
|
readonly promptHint: string
|
|
18
|
+
/** Optional authored compact term (see `term.ts` for the convention). */
|
|
19
|
+
readonly term?: string
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export const VOICE_AGES: ReadonlyArray<VoiceCharacterEntry> = [
|
|
@@ -71,83 +74,83 @@ export const VOICE_LANGUAGES: ReadonlyArray<VoiceCharacterEntry> = [
|
|
|
71
74
|
|
|
72
75
|
export const VOICE_ACCENTS: ReadonlyArray<VoiceCharacterEntry> = [
|
|
73
76
|
// 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" },
|
|
77
|
+
{ id: "general-american", label: "General American", description: "Neutral US accent", promptHint: "general American", term: "general american accent" },
|
|
78
|
+
{ id: "southern-us", label: "Southern US", description: "US Southern drawl", promptHint: "Southern US", term: "southern american accent" },
|
|
79
|
+
{ id: "new-york", label: "New York", description: "NY metro accent", promptHint: "New York", term: "new york accent" },
|
|
80
|
+
{ id: "boston", label: "Boston", description: "Boston / Mass accent", promptHint: "Boston", term: "boston accent" },
|
|
81
|
+
{ id: "midwestern-us", label: "Midwestern", description: "US heartland", promptHint: "Midwestern American", term: "midwestern american accent" },
|
|
82
|
+
{ id: "chicago", label: "Chicago", description: "Upper-Midwest urban", promptHint: "Chicago", term: "chicago accent" },
|
|
83
|
+
{ id: "appalachian", label: "Appalachian", description: "Mountain South", promptHint: "Appalachian", term: "appalachian accent" },
|
|
84
|
+
{ id: "canadian", label: "Canadian", description: "Canada English", promptHint: "Canadian", term: "canadian accent" },
|
|
82
85
|
// 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" },
|
|
86
|
+
{ id: "british-rp", label: "British RP", description: "Received Pronunciation", promptHint: "British RP", term: "received pronunciation accent" },
|
|
87
|
+
{ id: "cockney", label: "Cockney", description: "London working-class", promptHint: "Cockney", term: "cockney accent" },
|
|
88
|
+
{ id: "estuary-english", label: "Estuary", description: "South-East England", promptHint: "Estuary English", term: "estuary english accent" },
|
|
89
|
+
{ id: "northern-english", label: "Northern English", description: "Manchester / Yorkshire", promptHint: "Northern English", term: "northern english accent" },
|
|
90
|
+
{ id: "scouse", label: "Scouse", description: "Liverpool", promptHint: "Scouse", term: "scouse accent" },
|
|
91
|
+
{ id: "geordie", label: "Geordie", description: "Newcastle", promptHint: "Geordie", term: "geordie accent" },
|
|
92
|
+
{ id: "scottish", label: "Scottish", description: "Scotland", promptHint: "Scottish", term: "scottish accent" },
|
|
93
|
+
{ id: "irish", label: "Irish", description: "Ireland", promptHint: "Irish", term: "irish accent" },
|
|
94
|
+
{ id: "welsh", label: "Welsh", description: "Wales", promptHint: "Welsh", term: "welsh accent" },
|
|
92
95
|
// 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" },
|
|
96
|
+
{ id: "australian", label: "Australian", description: "Australia", promptHint: "Australian", term: "australian accent" },
|
|
97
|
+
{ id: "new-zealand", label: "New Zealand", description: "NZ Kiwi", promptHint: "New Zealand", term: "new zealand accent" },
|
|
98
|
+
{ id: "south-african", label: "South African", description: "South Africa", promptHint: "South African", term: "south african accent" },
|
|
99
|
+
{ id: "indian-english", label: "Indian English", description: "South Asian English", promptHint: "Indian English", term: "indian english accent" },
|
|
100
|
+
{ id: "caribbean", label: "Caribbean", description: "Caribbean English", promptHint: "Caribbean", term: "caribbean accent" },
|
|
101
|
+
{ id: "jamaican", label: "Jamaican", description: "Jamaican Patois", promptHint: "Jamaican", term: "jamaican accent" },
|
|
99
102
|
// 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" },
|
|
103
|
+
{ id: "french-accented", label: "French", description: "French-accented English", promptHint: "French-accented", term: "french accent" },
|
|
104
|
+
{ id: "italian-accented", label: "Italian", description: "Italian-accented English", promptHint: "Italian-accented", term: "italian accent" },
|
|
105
|
+
{ id: "german-accented", label: "German", description: "German-accented English", promptHint: "German-accented", term: "german accent" },
|
|
106
|
+
{ id: "dutch-accented", label: "Dutch", description: "Dutch-accented English", promptHint: "Dutch-accented", term: "dutch accent" },
|
|
107
|
+
{ id: "russian-accented", label: "Russian", description: "Russian-accented English", promptHint: "Russian-accented", term: "russian accent" },
|
|
108
|
+
{ id: "polish-accented", label: "Polish", description: "Polish-accented English", promptHint: "Polish-accented", term: "polish accent" },
|
|
109
|
+
{ id: "spanish-accented", label: "Spanish", description: "Spanish-accented English", promptHint: "Spanish-accented", term: "spanish accent" },
|
|
110
|
+
{ id: "portuguese-accented", label: "Portuguese", description: "Portuguese / Brazilian", promptHint: "Portuguese-accented", term: "portuguese accent" },
|
|
111
|
+
{ id: "scandinavian-accented", label: "Scandinavian", description: "Nordic-accented English", promptHint: "Scandinavian-accented", term: "scandinavian accent" },
|
|
109
112
|
// 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" },
|
|
113
|
+
{ id: "mexican-accented", label: "Mexican", description: "Mexican-accented English", promptHint: "Mexican-accented", term: "mexican accent" },
|
|
114
|
+
{ id: "argentinian-accented", label: "Argentinian", description: "Río de la Plata", promptHint: "Argentinian-accented", term: "argentinian accent" },
|
|
112
115
|
// 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" },
|
|
116
|
+
{ id: "japanese-accented", label: "Japanese", description: "Japanese-accented English", promptHint: "Japanese-accented", term: "japanese accent" },
|
|
117
|
+
{ id: "korean-accented", label: "Korean", description: "Korean-accented English", promptHint: "Korean-accented", term: "korean accent" },
|
|
118
|
+
{ id: "chinese-accented", label: "Chinese", description: "Mandarin-accented", promptHint: "Chinese-accented", term: "chinese accent" },
|
|
119
|
+
{ id: "filipino-accented", label: "Filipino", description: "Filipino-accented English", promptHint: "Filipino-accented", term: "filipino accent" },
|
|
120
|
+
{ id: "arabic-accented", label: "Arabic", description: "Arabic-accented English", promptHint: "Arabic-accented", term: "arabic accent" },
|
|
121
|
+
{ id: "hebrew-accented", label: "Hebrew", description: "Israeli-accented English", promptHint: "Hebrew-accented", term: "hebrew accent" },
|
|
122
|
+
{ id: "turkish-accented", label: "Turkish", description: "Turkish-accented English", promptHint: "Turkish-accented", term: "turkish accent" },
|
|
123
|
+
{ id: "persian-accented", label: "Persian", description: "Persian-accented English", promptHint: "Persian-accented", term: "persian accent" },
|
|
121
124
|
// Africa
|
|
122
|
-
{ id: "nigerian-accented", label: "Nigerian", description: "Nigerian English", promptHint: "Nigerian-accented" },
|
|
125
|
+
{ id: "nigerian-accented", label: "Nigerian", description: "Nigerian English", promptHint: "Nigerian-accented", term: "nigerian accent" },
|
|
123
126
|
// 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" },
|
|
127
|
+
{ id: "neutral-international", label: "Neutral International", description: "Region-agnostic", promptHint: "neutral international", term: "neutral international accent" },
|
|
128
|
+
{ id: "transatlantic", label: "Transatlantic", description: "Mid-Atlantic theatrical", promptHint: "transatlantic", term: "transatlantic accent" },
|
|
126
129
|
] as const
|
|
127
130
|
|
|
128
131
|
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" },
|
|
132
|
+
{ id: "warm", label: "Warm", description: "Rich, inviting", promptHint: "warm", term: "warm timbre" },
|
|
133
|
+
{ id: "smooth", label: "Smooth", description: "Clean, even", promptHint: "smooth", term: "smooth timbre" },
|
|
134
|
+
{ id: "silky", label: "Silky", description: "Soft, refined", promptHint: "silky", term: "silky timbre" },
|
|
135
|
+
{ id: "velvety", label: "Velvety", description: "Lush, plush", promptHint: "velvety", term: "velvety timbre" },
|
|
136
|
+
{ id: "raspy", label: "Raspy", description: "Rough, textured", promptHint: "raspy", term: "raspy timbre" },
|
|
137
|
+
{ id: "gravelly", label: "Gravelly", description: "Deeply textured", promptHint: "gravelly", term: "gravelly timbre" },
|
|
138
|
+
{ id: "rough", label: "Rough", description: "Coarse, weathered", promptHint: "rough", term: "rough timbre" },
|
|
139
|
+
{ id: "husky", label: "Husky", description: "Throaty, hoarse", promptHint: "husky", term: "husky timbre" },
|
|
140
|
+
{ id: "breathy", label: "Breathy", description: "Airy, intimate", promptHint: "breathy", term: "breathy timbre" },
|
|
141
|
+
{ id: "whispered", label: "Whispered", description: "Hushed, intimate", promptHint: "whispered", term: "whispered timbre" },
|
|
142
|
+
{ id: "nasal", label: "Nasal", description: "Resonates in nose", promptHint: "nasal", term: "nasal timbre" },
|
|
143
|
+
{ id: "twangy", label: "Twangy", description: "Sharp, regional", promptHint: "twangy", term: "twangy timbre" },
|
|
144
|
+
{ id: "deep", label: "Deep", description: "Low pitch range", promptHint: "deep", term: "deep timbre" },
|
|
145
|
+
{ id: "booming", label: "Booming", description: "Resonant, large", promptHint: "booming", term: "booming timbre" },
|
|
146
|
+
{ id: "high-pitched", label: "High-pitched", description: "Upper register", promptHint: "high-pitched", term: "high-pitched timbre" },
|
|
147
|
+
{ id: "squeaky", label: "Squeaky", description: "Thin, piercing", promptHint: "squeaky", term: "squeaky timbre" },
|
|
148
|
+
{ id: "bright", label: "Bright", description: "Crisp, forward", promptHint: "bright", term: "bright timbre" },
|
|
149
|
+
{ id: "dark", label: "Dark", description: "Heavy, somber", promptHint: "dark", term: "dark timbre" },
|
|
150
|
+
{ id: "youthful", label: "Youthful", description: "Light, fresh", promptHint: "youthful", term: "youthful timbre" },
|
|
151
|
+
{ id: "authoritative",label: "Authoritative",description: "Commanding", promptHint: "authoritative", term: "authoritative timbre" },
|
|
152
|
+
{ id: "sultry", label: "Sultry", description: "Sensual, smoky", promptHint: "sultry", term: "sultry timbre" },
|
|
153
|
+
{ id: "polished", label: "Polished", description: "Practiced, broadcast-ready", promptHint: "polished", term: "polished timbre" },
|
|
151
154
|
] as const
|
|
152
155
|
|
|
153
156
|
const AGE_BY_ID = new Map(VOICE_AGES.map((x) => [x.id, x]))
|
|
@@ -163,19 +166,42 @@ export function getVoiceAccent(id: string | undefined) { return id ? ACCENT_BY_I
|
|
|
163
166
|
export function getVoiceTimbre(id: string | undefined) { return id ? TIMBRE_BY_ID.get(id) : undefined }
|
|
164
167
|
|
|
165
168
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* { timbre } → "warm timbre"
|
|
170
|
-
* { accent } → "British RP accent"
|
|
171
|
-
* { age, gender } → "middle-aged male voice"
|
|
172
|
-
* { language: ["english","spanish"] } → "English / Spanish voice"
|
|
173
|
-
* { } → ""
|
|
169
|
+
* The COMPACT counterparts of the five sub-field lookups: the short
|
|
170
|
+
* professional term a consumer injects instead of the entry's hint fragment.
|
|
171
|
+
* Same lookup, same empty-string-on-miss behavior.
|
|
174
172
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
173
|
+
* Accent and timbre terms CARRY their dimension noun — "boston accent",
|
|
174
|
+
* "french accent", "warm timbre" — where the `promptHint` fragments are bare
|
|
175
|
+
* ("Boston", "French-accented", "warm") and only read as an accent or a
|
|
176
|
+
* timbre once `buildVoiceCharacterHints` appends the noun. The term has to
|
|
177
|
+
* stand on its own because a thin client injects one by itself (a picker
|
|
178
|
+
* chip), with no composer to append anything: a bare "french" there is
|
|
179
|
+
* indistinguishable from the LANGUAGE French sitting in the neighbouring
|
|
180
|
+
* dimension.
|
|
181
|
+
*
|
|
182
|
+
* Age, gender and language terms deliberately stay BARE — "male", "mature",
|
|
183
|
+
* "english". Do NOT "fix" them by authoring the noun into the data:
|
|
184
|
+
* `composeVoiceCharacter` appends " voice" to the age+gender group itself, so
|
|
185
|
+
* a "male voice" term would emit "...male voice voice".
|
|
177
186
|
*/
|
|
178
|
-
export function
|
|
187
|
+
export function getVoiceAgeTerm(id: string | undefined | null): string {
|
|
188
|
+
return resolveTerm(getVoiceAge(id ?? undefined))
|
|
189
|
+
}
|
|
190
|
+
export function getVoiceGenderTerm(id: string | undefined | null): string {
|
|
191
|
+
return resolveTerm(getVoiceGender(id ?? undefined))
|
|
192
|
+
}
|
|
193
|
+
export function getVoiceLanguageTerm(id: string | undefined | null): string {
|
|
194
|
+
return resolveTerm(getVoiceLanguage(id ?? undefined))
|
|
195
|
+
}
|
|
196
|
+
export function getVoiceAccentTerm(id: string | undefined | null): string {
|
|
197
|
+
return resolveTerm(getVoiceAccent(id ?? undefined))
|
|
198
|
+
}
|
|
199
|
+
export function getVoiceTimbreTerm(id: string | undefined | null): string {
|
|
200
|
+
return resolveTerm(getVoiceTimbre(id ?? undefined))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The consumer fields a voice-character clause is composed from. */
|
|
204
|
+
type VoiceCharacterFieldSource = {
|
|
179
205
|
readonly preText?: string
|
|
180
206
|
readonly postText?: string
|
|
181
207
|
readonly age?: string
|
|
@@ -183,26 +209,47 @@ export function buildVoiceCharacterHints(data: {
|
|
|
183
209
|
readonly language?: string | ReadonlyArray<string>
|
|
184
210
|
readonly accent?: string
|
|
185
211
|
readonly timbre?: string
|
|
186
|
-
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* How one composition mode turns catalog entries into fragments.
|
|
216
|
+
*
|
|
217
|
+
* `of` picks the fragment (the long `promptHint`, or the compact `term`), and
|
|
218
|
+
* `trait` attaches the dimension noun: the bare hint fragments always need it
|
|
219
|
+
* ("warm" → "warm timbre"), while the compact mode appends it only when the
|
|
220
|
+
* fragment does not already end in it ("warm timbre" is passed through as
|
|
221
|
+
* authored, a term-less "bavarian" still becomes "bavarian accent").
|
|
222
|
+
*/
|
|
223
|
+
interface VoiceCharacterMode {
|
|
224
|
+
readonly of: (entry: VoiceCharacterEntry | undefined) => string
|
|
225
|
+
readonly trait: (fragment: string, noun: "timbre" | "accent") => string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The shared composition both modes walk, so the verbose and compact clauses
|
|
230
|
+
* can never drift in shape — same field order, same "voice with X and Y"
|
|
231
|
+
* skeleton, same language / preText / postText handling.
|
|
232
|
+
*/
|
|
233
|
+
function composeVoiceCharacter(data: VoiceCharacterFieldSource, mode: VoiceCharacterMode): string {
|
|
187
234
|
const fragments: string[] = []
|
|
188
235
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
189
236
|
if (pre) fragments.push(pre)
|
|
190
237
|
|
|
191
|
-
const age = getVoiceAge(data.age)
|
|
192
|
-
const gender = getVoiceGender(data.gender)
|
|
193
|
-
const accent = getVoiceAccent(data.accent)
|
|
194
|
-
const timbre = getVoiceTimbre(data.timbre)
|
|
238
|
+
const age = mode.of(getVoiceAge(data.age))
|
|
239
|
+
const gender = mode.of(getVoiceGender(data.gender))
|
|
240
|
+
const accent = mode.of(getVoiceAccent(data.accent))
|
|
241
|
+
const timbre = mode.of(getVoiceTimbre(data.timbre))
|
|
195
242
|
|
|
196
243
|
const langIds = pickIds(data.language)
|
|
197
|
-
const
|
|
198
|
-
.map((id) => getVoiceLanguage(id)
|
|
199
|
-
.filter((h)
|
|
200
|
-
const langClause =
|
|
244
|
+
const langFragments = langIds
|
|
245
|
+
.map((id) => mode.of(getVoiceLanguage(id)))
|
|
246
|
+
.filter((h) => !!h)
|
|
247
|
+
const langClause = langFragments.join(" / ")
|
|
201
248
|
|
|
202
|
-
const ageGender = [age
|
|
249
|
+
const ageGender = [age, gender].filter(Boolean).join(" ")
|
|
203
250
|
const traits: string[] = []
|
|
204
|
-
if (timbre) traits.push(
|
|
205
|
-
if (accent) traits.push(
|
|
251
|
+
if (timbre) traits.push(mode.trait(timbre, "timbre"))
|
|
252
|
+
if (accent) traits.push(mode.trait(accent, "accent"))
|
|
206
253
|
|
|
207
254
|
let core = ""
|
|
208
255
|
if (ageGender && traits.length > 0) {
|
|
@@ -226,6 +273,59 @@ export function buildVoiceCharacterHints(data: {
|
|
|
226
273
|
return fragments.join(", ")
|
|
227
274
|
}
|
|
228
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Compose a natural-language voice character clause.
|
|
278
|
+
* Examples (depending on which sub-fields are set):
|
|
279
|
+
* { age, gender, timbre, accent } → "middle-aged male voice with warm timbre and British RP accent"
|
|
280
|
+
* { timbre } → "warm timbre"
|
|
281
|
+
* { accent } → "British RP accent"
|
|
282
|
+
* { age, gender } → "middle-aged male voice"
|
|
283
|
+
* { language: ["english","spanish"] } → "English / Spanish voice"
|
|
284
|
+
* { } → ""
|
|
285
|
+
*
|
|
286
|
+
* `language` is multi-pick — multiple languages emit "English / Spanish"
|
|
287
|
+
* for codeswitching / multilingual voices.
|
|
288
|
+
*/
|
|
289
|
+
export function buildVoiceCharacterHints(data: VoiceCharacterFieldSource, mode: PickerHintMode = "full"): string {
|
|
290
|
+
if (mode === "compact") return buildVoiceCharacterTerms(data)
|
|
291
|
+
return composeVoiceCharacter(data, {
|
|
292
|
+
of: (entry) => entry?.promptHint ?? "",
|
|
293
|
+
trait: (fragment, noun) => `${fragment} ${noun}`,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Attach the dimension noun unless the fragment already ends with it.
|
|
299
|
+
*
|
|
300
|
+
* Every accent and timbre entry authors a noun-bearing term today ("warm
|
|
301
|
+
* timbre", "boston accent"), and this keeps that from being an ASSUMPTION the
|
|
302
|
+
* compact clause silently depends on: an entry added later with no `term`
|
|
303
|
+
* derives a bare "bavarian" from its label, and the noun is attached here
|
|
304
|
+
* rather than shipping a lone modifier no model can read as an accent.
|
|
305
|
+
*/
|
|
306
|
+
function withDimensionNoun(fragment: string, noun: "timbre" | "accent"): string {
|
|
307
|
+
return fragment === noun || fragment.endsWith(` ${noun}`) ? fragment : `${fragment} ${noun}`
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The COMPACT counterpart of `buildVoiceCharacterHints`: the same clause
|
|
312
|
+
* skeleton, built from each selection's short professional term instead of its
|
|
313
|
+
* hint fragment. The dimension noun is attached idempotently — an authored
|
|
314
|
+
* term already carries it ("warm timbre", "received pronunciation accent") and
|
|
315
|
+
* passes through untouched.
|
|
316
|
+
* Examples:
|
|
317
|
+
* { age, gender, timbre, accent } → "middle-aged male voice with warm timbre and received pronunciation accent"
|
|
318
|
+
* { timbre } → "warm timbre"
|
|
319
|
+
* { language: ["english","spanish"] } → "english / spanish voice"
|
|
320
|
+
* { } → ""
|
|
321
|
+
*/
|
|
322
|
+
export function buildVoiceCharacterTerms(data: VoiceCharacterFieldSource): string {
|
|
323
|
+
return composeVoiceCharacter(data, {
|
|
324
|
+
of: (entry) => resolveTerm(entry),
|
|
325
|
+
trait: withDimensionNoun,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
229
329
|
export const VOICE_CHARACTER_DEFAULT_DATA: {
|
|
230
330
|
preText?: string
|
|
231
331
|
postText?: string
|
package/src/voice-delivery.ts
CHANGED
|
@@ -3,17 +3,27 @@
|
|
|
3
3
|
* Voice Design's voiceDescription via the Sound aggregator.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
7
|
+
|
|
6
8
|
export interface VoiceDeliveryEntry {
|
|
7
9
|
readonly id: string
|
|
8
10
|
readonly label: string
|
|
9
11
|
readonly description: string
|
|
10
12
|
readonly promptHint: string
|
|
13
|
+
/**
|
|
14
|
+
* The COMPACT professional term this entry injects (see `./term.js`).
|
|
15
|
+
* Authored only where the lowercased label is not what a voice director
|
|
16
|
+
* would actually write — "drawled" not "drawl", "robotic" not "robot",
|
|
17
|
+
* "nature documentary narrator" not "nature narrator". Everywhere else the
|
|
18
|
+
* derived label already IS the term and nothing is stored.
|
|
19
|
+
*/
|
|
20
|
+
readonly term?: string
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
export const VOICE_PACES: ReadonlyArray<VoiceDeliveryEntry> = [
|
|
14
24
|
{ id: "very-slow", label: "Very Slow", description: "Deliberate, ponderous", promptHint: "very slow" },
|
|
15
25
|
{ id: "slow", label: "Slow", description: "Measured", promptHint: "slow" },
|
|
16
|
-
{ id: "drawl", label: "Drawl", description: "Drawn-out, lazy", promptHint: "drawled" },
|
|
26
|
+
{ id: "drawl", label: "Drawl", description: "Drawn-out, lazy", promptHint: "drawled", term: "drawled" },
|
|
17
27
|
{ id: "measured", label: "Measured", description: "Steady, intentional", promptHint: "measured" },
|
|
18
28
|
{ id: "moderate", label: "Moderate", description: "Conversational pace", promptHint: "moderate" },
|
|
19
29
|
{ id: "punchy", label: "Punchy", description: "Clipped, emphatic", promptHint: "punchy" },
|
|
@@ -55,7 +65,7 @@ export const VOICE_ARCHETYPES: ReadonlyArray<VoiceDeliveryEntry> = [
|
|
|
55
65
|
{ id: "newscaster", label: "Newscaster", description: "Authoritative news delivery", promptHint: "newscaster" },
|
|
56
66
|
{ id: "anchor", label: "TV Anchor", description: "Polished broadcast", promptHint: "TV anchor" },
|
|
57
67
|
{ 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" },
|
|
68
|
+
{ id: "nature-narrator", label: "Nature Narrator", description: "Soft-spoken Attenborough-style", promptHint: "nature documentary narrator", term: "nature documentary narrator" },
|
|
59
69
|
{ id: "fairy-tale-narrator", label: "Fairy-Tale Narrator", description: "Storyteller for children", promptHint: "fairy-tale narrator" },
|
|
60
70
|
{ id: "audiobook-narrator", label: "Audiobook Narrator", description: "Long-form fiction reading", promptHint: "audiobook narrator" },
|
|
61
71
|
{ id: "podcaster", label: "Podcaster", description: "Conversational", promptHint: "podcaster" },
|
|
@@ -74,16 +84,16 @@ export const VOICE_ARCHETYPES: ReadonlyArray<VoiceDeliveryEntry> = [
|
|
|
74
84
|
{ id: "drag-queen", label: "Drag Queen", description: "Theatrical, fierce", promptHint: "drag queen" },
|
|
75
85
|
// Character archetypes
|
|
76
86
|
{ id: "villain", label: "Villain", description: "Sinister, scheming", promptHint: "villain" },
|
|
77
|
-
{ id: "hero", label: "Hero", description: "Earnest, courageous", promptHint: "heroic" },
|
|
87
|
+
{ id: "hero", label: "Hero", description: "Earnest, courageous", promptHint: "heroic", term: "heroic" },
|
|
78
88
|
{ id: "mentor", label: "Mentor", description: "Wise, guiding", promptHint: "mentor" },
|
|
79
|
-
{ id: "comedian", label: "Comedian", description: "Comedic timing", promptHint: "comedic" },
|
|
89
|
+
{ id: "comedian", label: "Comedian", description: "Comedic timing", promptHint: "comedic", term: "comedic" },
|
|
80
90
|
{ id: "stand-up-comic", label: "Stand-up Comic", description: "Punchline-driven", promptHint: "stand-up comic" },
|
|
81
91
|
{ id: "drill-sergeant", label: "Drill Sergeant", description: "Barking commands", promptHint: "drill sergeant" },
|
|
82
92
|
{ id: "meditation-guide", label: "Meditation Guide", description: "Soft, slow guidance", promptHint: "meditation guide" },
|
|
83
93
|
{ id: "yoga-instructor", label: "Yoga Instructor", description: "Calm, grounding cues", promptHint: "yoga instructor" },
|
|
84
94
|
{ id: "noir-detective", label: "Noir Detective", description: "Hard-boiled, brooding", promptHint: "noir detective" },
|
|
85
95
|
{ id: "wizard", label: "Wizard", description: "Mystic, ancient", promptHint: "wizard" },
|
|
86
|
-
{ id: "robot", label: "Robot", description: "Synthetic, mechanical", promptHint: "robotic" },
|
|
96
|
+
{ id: "robot", label: "Robot", description: "Synthetic, mechanical", promptHint: "robotic", term: "robotic" },
|
|
87
97
|
{ id: "ai-assistant", label: "AI Assistant", description: "Polished synthetic helper", promptHint: "AI assistant" },
|
|
88
98
|
{ id: "siri-style", label: "Virtual Assistant", description: "Siri/Alexa-style helper", promptHint: "virtual assistant" },
|
|
89
99
|
] as const
|
|
@@ -96,6 +106,23 @@ export function getVoicePace(id: string | undefined) { return id ? PACE_BY_ID.ge
|
|
|
96
106
|
export function getVoiceEmotion(id: string | undefined) { return id ? EMOTION_BY_ID.get(id) : undefined }
|
|
97
107
|
export function getVoiceArchetype(id: string | undefined) { return id ? ARCHETYPE_BY_ID.get(id) : undefined }
|
|
98
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The COMPACT counterparts of the three lookups above: the short professional
|
|
111
|
+
* term a consumer injects instead of the full prompt fragment ("drawled",
|
|
112
|
+
* "reassuring", "nature documentary narrator"). Same lookup, same
|
|
113
|
+
* empty-string-on-miss behavior as the hint side, so the two can never
|
|
114
|
+
* disagree about which entry they describe.
|
|
115
|
+
*/
|
|
116
|
+
export function getVoicePaceTerm(id: string | undefined | null): string {
|
|
117
|
+
return resolveTerm(getVoicePace(id ?? undefined))
|
|
118
|
+
}
|
|
119
|
+
export function getVoiceEmotionTerm(id: string | undefined | null): string {
|
|
120
|
+
return resolveTerm(getVoiceEmotion(id ?? undefined))
|
|
121
|
+
}
|
|
122
|
+
export function getVoiceArchetypeTerm(id: string | undefined | null): string {
|
|
123
|
+
return resolveTerm(getVoiceArchetype(id ?? undefined))
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
/**
|
|
100
127
|
* Compose a delivery clause.
|
|
101
128
|
* Examples:
|
|
@@ -104,13 +131,17 @@ export function getVoiceArchetype(id: string | undefined) { return id ? ARCHETYP
|
|
|
104
131
|
* { emotion } → "reassuring tone"
|
|
105
132
|
* { pace } → "measured pace"
|
|
106
133
|
*/
|
|
107
|
-
export function buildVoiceDeliveryHints(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
export function buildVoiceDeliveryHints(
|
|
135
|
+
data: {
|
|
136
|
+
readonly preText?: string
|
|
137
|
+
readonly postText?: string
|
|
138
|
+
readonly pace?: string
|
|
139
|
+
readonly emotion?: string
|
|
140
|
+
readonly archetype?: string
|
|
141
|
+
},
|
|
142
|
+
mode: PickerHintMode = "full",
|
|
143
|
+
): string {
|
|
144
|
+
if (mode === "compact") return buildVoiceDeliveryTerms(data)
|
|
114
145
|
const fragments: string[] = []
|
|
115
146
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
116
147
|
if (pre) fragments.push(pre)
|
|
@@ -137,6 +168,46 @@ export function buildVoiceDeliveryHints(data: {
|
|
|
137
168
|
return fragments.join(", ")
|
|
138
169
|
}
|
|
139
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Compact-mode mirror of `buildVoiceDeliveryHints`: the same preText /
|
|
173
|
+
* [pace] [archetype]-style delivery / [emotion] tone / postText structure,
|
|
174
|
+
* built from each selection's short `term` instead of its `promptHint`
|
|
175
|
+
* ("drawled noir-detective-style delivery, menacing tone"). preText and
|
|
176
|
+
* postText — user free text, not catalog copy — pass through untouched.
|
|
177
|
+
*/
|
|
178
|
+
export function buildVoiceDeliveryTerms(data: {
|
|
179
|
+
readonly preText?: string
|
|
180
|
+
readonly postText?: string
|
|
181
|
+
readonly pace?: string
|
|
182
|
+
readonly emotion?: string
|
|
183
|
+
readonly archetype?: string
|
|
184
|
+
}): string {
|
|
185
|
+
const fragments: string[] = []
|
|
186
|
+
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
187
|
+
if (pre) fragments.push(pre)
|
|
188
|
+
|
|
189
|
+
const pace = getVoicePaceTerm(data.pace)
|
|
190
|
+
const emotion = getVoiceEmotionTerm(data.emotion)
|
|
191
|
+
const archetype = getVoiceArchetypeTerm(data.archetype)
|
|
192
|
+
|
|
193
|
+
let deliveryClause = ""
|
|
194
|
+
if (pace && archetype) deliveryClause = `${pace} ${archetype}-style delivery`
|
|
195
|
+
else if (archetype) deliveryClause = `${archetype}-style delivery`
|
|
196
|
+
else if (pace) deliveryClause = `${pace} pace`
|
|
197
|
+
|
|
198
|
+
const emotionClause = emotion ? `${emotion} tone` : ""
|
|
199
|
+
|
|
200
|
+
let main = ""
|
|
201
|
+
if (deliveryClause && emotionClause) main = `${deliveryClause}, ${emotionClause}`
|
|
202
|
+
else main = deliveryClause || emotionClause
|
|
203
|
+
if (main) fragments.push(main)
|
|
204
|
+
|
|
205
|
+
const post = typeof data.postText === "string" ? data.postText.trim() : ""
|
|
206
|
+
if (post) fragments.push(post)
|
|
207
|
+
|
|
208
|
+
return fragments.join(", ")
|
|
209
|
+
}
|
|
210
|
+
|
|
140
211
|
export const VOICE_DELIVERY_DEFAULT_DATA: {
|
|
141
212
|
preText?: string; postText?: string; pace?: string; emotion?: string; archetype?: string
|
|
142
213
|
} = {}
|