@nodaro/prompts 1.14.0 → 1.15.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 +633 -248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +379 -1
- package/dist/index.d.ts +379 -1
- package/dist/index.js +603 -250
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adult-only-ratchet.test.ts +110 -0
- package/src/__tests__/age-floor.test.ts +347 -0
- package/src/__tests__/catalog-id-guard.test.ts +146 -0
- package/src/__tests__/catalog-overlay.test.ts +163 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +17 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2 -2
- package/src/__tests__/minor-age-floor.test.ts +136 -0
- package/src/__tests__/person-pack-adult-only.test.ts +45 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +14 -0
- package/src/action-fx.ts +2 -1
- package/src/aesthetic.ts +2 -1
- package/src/age-floor.ts +296 -0
- package/src/age-signal.ts +36 -0
- package/src/atmosphere.ts +2 -1
- package/src/backdrop.ts +2 -1
- package/src/camera-format.ts +2 -1
- package/src/camera-motions.ts +2 -1
- package/src/catalog-id-guard.ts +189 -0
- package/src/catalog-overlay.ts +148 -0
- package/src/catalog-packs.ts +27 -2
- package/src/character-fx.ts +2 -1
- package/src/color-look.ts +2 -1
- package/src/composition-effects.ts +2 -1
- package/src/era.ts +2 -1
- package/src/exposure-settings.ts +2 -1
- package/src/framing.ts +2 -1
- package/src/held-prop.ts +2 -1
- package/src/index.ts +4 -0
- package/src/instrumentation.ts +5 -4
- package/src/lens.ts +2 -1
- package/src/lighting.ts +2 -1
- package/src/loop-subject.ts +3 -1
- package/src/materials.ts +2 -1
- package/src/mood.ts +16 -5
- package/src/music-genre.ts +5 -4
- package/src/music-mood.ts +4 -3
- package/src/parameter-prompt-hint.ts +13 -57
- package/src/person-packs.ts +14 -2
- package/src/person.ts +71 -38
- package/src/photo-genre.ts +14 -3
- package/src/photographer.ts +2 -1
- package/src/picker-analyzer-registry.ts +9 -0
- package/src/picker-catalogs.ts +11 -0
- package/src/pose.ts +17 -6
- package/src/post-process-effects.ts +2 -1
- package/src/prompt-builder.ts +3 -3
- package/src/render-quality.ts +2 -1
- package/src/setting.ts +2 -1
- package/src/shared-catalog-overlay.ts +83 -0
- package/src/style.ts +16 -1
- package/src/styling.ts +57 -29
- package/src/subject-registry.ts +2 -2
- package/src/temporal.ts +2 -1
- package/src/transitions.ts +2 -1
- package/src/voice-character.ts +6 -5
- package/src/voice-delivery.ts +4 -3
|
@@ -38,10 +38,7 @@ import { composeCameraMotionHintFromConnections } from "./camera-motions.js"
|
|
|
38
38
|
import { composeTransitionHintFromConnections, type TransitionDuration, type TransitionIntensity, type TransitionPosition, type TransitionTiming } from "./transitions.js"
|
|
39
39
|
import { composeCharacterFxHintFromConnections, type CharacterFxDuration, type CharacterFxIntensity, type CharacterFxPosition, type CharacterFxTiming } from "./character-fx.js"
|
|
40
40
|
import { buildMaterialHints } from "./materials.js"
|
|
41
|
-
import {
|
|
42
|
-
import { getVehicle } from "@nodaro/shared"
|
|
43
|
-
import { getWeapon } from "@nodaro/shared"
|
|
44
|
-
import { getFurniture } from "@nodaro/shared"
|
|
41
|
+
import { curatedAnimalPromptHint, curatedAnimalTerm, curatedVehicleText, curatedWeaponText, curatedFurnitureText } from "./shared-catalog-overlay.js"
|
|
45
42
|
import { getPhotoGenrePromptHint, getPhotoGenreTerm } from "./photo-genre.js"
|
|
46
43
|
import { getBackdropPromptHint, getBackdropTerm } from "./backdrop.js"
|
|
47
44
|
import { buildHeldPropHints } from "./held-prop.js"
|
|
@@ -58,7 +55,7 @@ import { buildInstrumentationHints } from "./instrumentation.js"
|
|
|
58
55
|
import { buildVoiceCharacterHints } from "./voice-character.js"
|
|
59
56
|
import { buildVoiceDeliveryHints } from "./voice-delivery.js"
|
|
60
57
|
import { getPickerCatalog } from "./picker-catalogs.js"
|
|
61
|
-
import {
|
|
58
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
62
59
|
|
|
63
60
|
|
|
64
61
|
function asStr(v: unknown): string {
|
|
@@ -109,28 +106,6 @@ function byMode<T>(mode: PickerHintMode, full: T, compact: T): T {
|
|
|
109
106
|
return mode === "compact" ? compact : full
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
/**
|
|
113
|
-
* The compact fragment for an OBJECT-entity entry (animal / vehicle / weapon /
|
|
114
|
-
* furniture). Those catalogs carry no `promptHint` of their own — the full
|
|
115
|
-
* fragment is synthesized as "featuring a {label}, {description}" — so the
|
|
116
|
-
* compact form is the authored `term` when there is one and the derived label
|
|
117
|
-
* otherwise (a concrete object's label IS its trade term). The framing verb
|
|
118
|
-
* ("featuring a", "with a") belongs to the HINT; a term drops bare into
|
|
119
|
-
* whatever sentence the consumer is building, and "the object is in the scene"
|
|
120
|
-
* is precisely what these four nodes mean.
|
|
121
|
-
*
|
|
122
|
-
* The fallback MUST be `deriveTerm` and not a bare `toLowerCase()`: it is the
|
|
123
|
-
* same fallback `objectOptions` uses to build the `/v1/catalogs` projection,
|
|
124
|
-
* so a parenthetical label ("Rifle (bolt-action)") must strip identically here
|
|
125
|
-
* or the injected fragment and the projected term would disagree.
|
|
126
|
-
*
|
|
127
|
-
* `term` is read structurally because it is being added to the shared entity
|
|
128
|
-
* interfaces separately; this stays correct before and after that lands.
|
|
129
|
-
*/
|
|
130
|
-
function objectEntityTerm(entry: { readonly label: string }): string {
|
|
131
|
-
return (entry as { term?: string }).term ?? deriveTerm(entry.label)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
109
|
/**
|
|
135
110
|
* Dispatch by parameter-node type to its prompt-hint string. For camera-motion,
|
|
136
111
|
* pass `ctx` to include the composed start/end clauses; otherwise only the
|
|
@@ -322,42 +297,23 @@ function resolveBaseHint(
|
|
|
322
297
|
return withCustomText(data, byMode(mode, getLoopSubjectPromptHint, getLoopSubjectTerm)(asStr(data.loopSubject)))
|
|
323
298
|
case "material":
|
|
324
299
|
return withCustomText(data, buildMaterialHints(data.material, mode))
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
// the
|
|
328
|
-
// miss, so the entry lookup
|
|
329
|
-
// getters' job now, not this switch's.
|
|
300
|
+
// The four object-entity catalogs are owned by @nodaro/shared and overlaid
|
|
301
|
+
// in shared-catalog-overlay.ts — the ONE module every prompt-text read of
|
|
302
|
+
// them goes through (the subject fold uses it too). Each returns "" on a
|
|
303
|
+
// miss, so the entry lookup is the module's job, not this switch's.
|
|
330
304
|
case "animal":
|
|
331
|
-
return withCustomText(
|
|
332
|
-
data,
|
|
333
|
-
byMode(mode, getAnimalPromptHint, getAnimalTerm)(asStr(data.animal)),
|
|
334
|
-
)
|
|
305
|
+
return withCustomText(data, byMode(mode, curatedAnimalPromptHint, curatedAnimalTerm)(asStr(data.animal)))
|
|
335
306
|
case "vehicle": {
|
|
336
|
-
const
|
|
337
|
-
return withCustomText(
|
|
338
|
-
data,
|
|
339
|
-
vehicle
|
|
340
|
-
? byMode(mode, `featuring a ${vehicle.label.toLowerCase()}, ${vehicle.description}`, objectEntityTerm(vehicle))
|
|
341
|
-
: "",
|
|
342
|
-
)
|
|
307
|
+
const t = curatedVehicleText(asStr(data.vehicle))
|
|
308
|
+
return withCustomText(data, byMode(mode, t.hint, t.term))
|
|
343
309
|
}
|
|
344
310
|
case "weapon": {
|
|
345
|
-
const
|
|
346
|
-
return withCustomText(
|
|
347
|
-
data,
|
|
348
|
-
weapon
|
|
349
|
-
? byMode(mode, `with a ${weapon.label.toLowerCase()}, ${weapon.description}`, objectEntityTerm(weapon))
|
|
350
|
-
: "",
|
|
351
|
-
)
|
|
311
|
+
const t = curatedWeaponText(asStr(data.weapon))
|
|
312
|
+
return withCustomText(data, byMode(mode, t.hint, t.term))
|
|
352
313
|
}
|
|
353
314
|
case "furniture": {
|
|
354
|
-
const
|
|
355
|
-
return withCustomText(
|
|
356
|
-
data,
|
|
357
|
-
furniture
|
|
358
|
-
? byMode(mode, `including a ${furniture.label.toLowerCase()}, ${furniture.description}`, objectEntityTerm(furniture))
|
|
359
|
-
: "",
|
|
360
|
-
)
|
|
315
|
+
const t = curatedFurnitureText(asStr(data.furniture))
|
|
316
|
+
return withCustomText(data, byMode(mode, t.hint, t.term))
|
|
361
317
|
}
|
|
362
318
|
case "photo-genre":
|
|
363
319
|
return withCustomText(data, byMode(mode, getPhotoGenrePromptHint, getPhotoGenreTerm)(asStr(data.photoGenre)))
|
package/src/person-packs.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
PERSON_FIELD_BY_DIMENSION,
|
|
6
6
|
type Person,
|
|
7
7
|
} from "./person.js"
|
|
8
|
-
import { registerCatalogPack, getRegisteredCatalogPacks } from "./catalog-packs.js"
|
|
8
|
+
import { registerCatalogPack, getRegisteredCatalogPacks, catalogPacksVersion } from "./catalog-packs.js"
|
|
9
9
|
import type { PickerDimension, PickerOption } from "./picker-catalogs.js"
|
|
10
10
|
import { resolveTerm } from "./term.js"
|
|
11
11
|
import { setRegisteredPersonPackFields } from "@nodaro/shared"
|
|
@@ -64,6 +64,7 @@ export function registerPersonPack(pack: PersonPack): void {
|
|
|
64
64
|
category: e.group,
|
|
65
65
|
promptHint: e.promptHint,
|
|
66
66
|
term: resolveTerm(e),
|
|
67
|
+
adultOnly: e.adultOnly,
|
|
67
68
|
}
|
|
68
69
|
const existing = byField.get(dim.field)
|
|
69
70
|
if (existing) existing.options.push(opt)
|
|
@@ -85,8 +86,17 @@ export function resetPersonPacks(): void {
|
|
|
85
86
|
version++
|
|
86
87
|
syncPersonPackFields()
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The version every person memo keys on. It moves when a PERSON pack registers
|
|
91
|
+
* AND when a CATALOG pack for `person` does: `getRegisteredPeople()` folds
|
|
92
|
+
* both, so a memo keyed on the person-pack counter alone kept serving a
|
|
93
|
+
* denied id after a `deny` catalog pack for person landed — correct on the
|
|
94
|
+
* server only because the Haredi person pack happened to register afterwards
|
|
95
|
+
* and bump this, and wrong in the browser, where only catalog packs arrive.
|
|
96
|
+
* Folding the catalog version in makes the memo honest regardless of order.
|
|
97
|
+
*/
|
|
88
98
|
export function personPacksVersion(): number {
|
|
89
|
-
return version
|
|
99
|
+
return version * 1_000_003 + catalogPacksVersion()
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
/** Reverse of the registered dimension→field map, for reconstructing entries
|
|
@@ -110,6 +120,7 @@ function personEntriesFromDims(
|
|
|
110
120
|
description?: string
|
|
111
121
|
category?: string
|
|
112
122
|
promptHint: string
|
|
123
|
+
adultOnly?: true
|
|
113
124
|
}>
|
|
114
125
|
}>,
|
|
115
126
|
): RegisteredPersonEntry[] {
|
|
@@ -125,6 +136,7 @@ function personEntriesFromDims(
|
|
|
125
136
|
group: o.category,
|
|
126
137
|
promptHint: o.promptHint,
|
|
127
138
|
dimension,
|
|
139
|
+
adultOnly: o.adultOnly,
|
|
128
140
|
} as RegisteredPersonEntry)
|
|
129
141
|
}
|
|
130
142
|
}
|
package/src/person.ts
CHANGED
|
@@ -37,6 +37,12 @@ import {
|
|
|
37
37
|
personPacksVersion,
|
|
38
38
|
} from "./person-packs.js"
|
|
39
39
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
40
|
+
// W1-a minor-age floor: `age-signal.ts` is an import-free leaf (no runtime
|
|
41
|
+
// deps at all), so this import is cycle-safe unconditionally — unlike
|
|
42
|
+
// `age-floor.js`, which pulls in the catalog funnel (`person-packs.js` /
|
|
43
|
+
// `picker-catalogs.js`) and would reopen the person → age-floor →
|
|
44
|
+
// picker-catalogs load-time cycle if imported from here.
|
|
45
|
+
import { isMinorAge } from "./age-signal.js"
|
|
40
46
|
|
|
41
47
|
export type PersonDimension =
|
|
42
48
|
| "type"
|
|
@@ -79,6 +85,16 @@ export interface Person {
|
|
|
79
85
|
readonly dimension: PersonDimension
|
|
80
86
|
readonly description: string
|
|
81
87
|
readonly promptHint: string
|
|
88
|
+
/**
|
|
89
|
+
* W1-a minor-age floor (spec 2026-09-01 §3.3). `true` marks an entry whose
|
|
90
|
+
* hint describes body exposure, sheer/wet clothing, swimwear/lingerie, a
|
|
91
|
+
* seductive expression or gaze, or a body-placed tattoo. The fragment
|
|
92
|
+
* collectors DROP flagged entries for a minor subject (`isMinorAge`), the
|
|
93
|
+
* picker hides their tiles, the analyzer never emits them, and the backend
|
|
94
|
+
* policy strips their wording from free text. Hand-curated; the
|
|
95
|
+
* `adult-only-ratchet` test only ratchets. Never set on neutral defaults.
|
|
96
|
+
*/
|
|
97
|
+
readonly adultOnly?: true
|
|
82
98
|
/** Optional sub-grouping within a dimension. Used by the picker to render
|
|
83
99
|
* two-level selection (e.g. ethnicity grouped by region → specific). */
|
|
84
100
|
readonly group?: string
|
|
@@ -138,7 +154,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
138
154
|
{ 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" },
|
|
139
155
|
{ 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" },
|
|
140
156
|
{ 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" },
|
|
141
|
-
{ 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" },
|
|
157
|
+
{ 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" , adultOnly: true },
|
|
142
158
|
{ id: "tough-guy", label: "Tough Guy", group: "Realistic — Style", dimension: "type", description: "Hardened, tough man", promptHint: "a tough, hardened man" },
|
|
143
159
|
// Aesthetic archetypes that traditionally read older — hints kept gender +
|
|
144
160
|
// look-focused so the Age dimension stays in control of the literal age.
|
|
@@ -149,7 +165,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
149
165
|
{ 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" },
|
|
150
166
|
{ 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" },
|
|
151
167
|
{ 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" },
|
|
152
|
-
{ 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", term: "thick voluptuous figure" },
|
|
168
|
+
{ 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", term: "thick voluptuous figure" , adultOnly: true },
|
|
153
169
|
{ 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", term: "androgynous figure" },
|
|
154
170
|
{ 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" },
|
|
155
171
|
{ 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", term: "big bearded hairy man" },
|
|
@@ -158,7 +174,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
158
174
|
{ 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" },
|
|
159
175
|
{ 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" },
|
|
160
176
|
{ 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" },
|
|
161
|
-
{ 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" },
|
|
177
|
+
{ 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" , adultOnly: true },
|
|
162
178
|
{ 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" },
|
|
163
179
|
|
|
164
180
|
// ----- Fantasy (humanoid) -----
|
|
@@ -173,7 +189,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
173
189
|
{ 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", term: "faun" },
|
|
174
190
|
{ 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" },
|
|
175
191
|
{ 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", term: "male vampire" },
|
|
176
|
-
{ 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", term: "female vampire" },
|
|
192
|
+
{ 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", term: "female vampire" , adultOnly: true },
|
|
177
193
|
{ 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" },
|
|
178
194
|
{ 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" },
|
|
179
195
|
{ 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" },
|
|
@@ -418,25 +434,25 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
418
434
|
{ id: "mass-heavy", label: "Heavy", dimension: "body-mass", description: "Heavy-set, large build", promptHint: "heavy-set build", term: "heavy-set build" },
|
|
419
435
|
|
|
420
436
|
// ----- Bust (applicable subjects; ungated like Facial Hair, neutral = no-op) -----
|
|
421
|
-
{ id: "bust-small", label: "Small", dimension: "bust", description: "Small bust", promptHint: "small bust", term: "small bust" },
|
|
437
|
+
{ id: "bust-small", label: "Small", dimension: "bust", description: "Small bust", promptHint: "small bust", term: "small bust" , adultOnly: true },
|
|
422
438
|
{ id: "bust-average", label: "Average", dimension: "bust", description: "Average bust", promptHint: "" },
|
|
423
|
-
{ id: "bust-full", label: "Full", dimension: "bust", description: "Full bust", promptHint: "full bust", term: "full bust" },
|
|
424
|
-
{ id: "bust-very-full", label: "Very Full", dimension: "bust", description: "Very full bust", promptHint: "very full bust", term: "very full bust" },
|
|
439
|
+
{ id: "bust-full", label: "Full", dimension: "bust", description: "Full bust", promptHint: "full bust", term: "full bust" , adultOnly: true },
|
|
440
|
+
{ id: "bust-very-full", label: "Very Full", dimension: "bust", description: "Very full bust", promptHint: "very full bust", term: "very full bust" , adultOnly: true },
|
|
425
441
|
|
|
426
442
|
// ----- Waist -----
|
|
427
|
-
{ id: "waist-defined", label: "Defined", dimension: "waist", description: "Defined, cinched waist", promptHint: "defined waist", term: "defined waist" },
|
|
443
|
+
{ id: "waist-defined", label: "Defined", dimension: "waist", description: "Defined, cinched waist", promptHint: "defined waist", term: "defined waist" , adultOnly: true },
|
|
428
444
|
{ id: "waist-average", label: "Average", dimension: "waist", description: "Average waist", promptHint: "" },
|
|
429
|
-
{ id: "waist-straight", label: "Straight", dimension: "waist", description: "Straight, undefined waist", promptHint: "straight waistline", term: "straight waistline" },
|
|
445
|
+
{ id: "waist-straight", label: "Straight", dimension: "waist", description: "Straight, undefined waist", promptHint: "straight waistline", term: "straight waistline" , adultOnly: true },
|
|
430
446
|
|
|
431
447
|
// ----- Hips -----
|
|
432
|
-
{ id: "hips-narrow", label: "Narrow", dimension: "hips", description: "Narrow hips", promptHint: "narrow hips", term: "narrow hips" },
|
|
448
|
+
{ id: "hips-narrow", label: "Narrow", dimension: "hips", description: "Narrow hips", promptHint: "narrow hips", term: "narrow hips" , adultOnly: true },
|
|
433
449
|
{ id: "hips-balanced", label: "Balanced", dimension: "hips", description: "Balanced hips", promptHint: "" },
|
|
434
|
-
{ id: "hips-wide", label: "Wide", dimension: "hips", description: "Wide hips", promptHint: "wide hips", term: "wide hips" },
|
|
450
|
+
{ id: "hips-wide", label: "Wide", dimension: "hips", description: "Wide hips", promptHint: "wide hips", term: "wide hips" , adultOnly: true },
|
|
435
451
|
|
|
436
452
|
// ----- Silhouette (overall body shape; optional, no neutral) -----
|
|
437
|
-
{ id: "silhouette-hourglass", label: "Hourglass", dimension: "silhouette", description: "Balanced bust + hips, defined waist", promptHint: "hourglass silhouette", term: "hourglass silhouette" },
|
|
453
|
+
{ id: "silhouette-hourglass", label: "Hourglass", dimension: "silhouette", description: "Balanced bust + hips, defined waist", promptHint: "hourglass silhouette", term: "hourglass silhouette" , adultOnly: true },
|
|
438
454
|
{ id: "silhouette-rectangular", label: "Rectangular", dimension: "silhouette", description: "Straight up-and-down silhouette", promptHint: "rectangular silhouette", term: "rectangular silhouette" },
|
|
439
|
-
{ id: "silhouette-pear", label: "Pear", dimension: "silhouette", description: "Hips wider than shoulders", promptHint: "pear-shaped silhouette", term: "pear-shaped silhouette" },
|
|
455
|
+
{ id: "silhouette-pear", label: "Pear", dimension: "silhouette", description: "Hips wider than shoulders", promptHint: "pear-shaped silhouette", term: "pear-shaped silhouette" , adultOnly: true },
|
|
440
456
|
{ id: "silhouette-inverted", label: "Inverted Triangle", dimension: "silhouette", description: "Broad shoulders, narrow hips", promptHint: "inverted-triangle silhouette", term: "inverted-triangle silhouette" },
|
|
441
457
|
{ id: "silhouette-athletic", label: "Athletic", dimension: "silhouette", description: "Lean, toned, V-taper", promptHint: "athletic silhouette", term: "athletic silhouette" },
|
|
442
458
|
|
|
@@ -537,17 +553,17 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
537
553
|
|
|
538
554
|
// -------------------- Lip State (what the lips are doing / wearing) --------------------
|
|
539
555
|
{ id: "lip-state-chapped", label: "Chapped", dimension: "lip-state", description: "Cracked, dry, weather-worn lips", promptHint: "with chapped, cracked, weather-worn dry lips", term: "chapped dry lips" },
|
|
540
|
-
{ id: "lip-state-glossy", label: "Glossy", dimension: "lip-state", description: "High-shine, wet-look lips", promptHint: "with high-shine glossy wet-look lips", term: "glossy wet-look lips" },
|
|
556
|
+
{ id: "lip-state-glossy", label: "Glossy", dimension: "lip-state", description: "High-shine, wet-look lips", promptHint: "with high-shine glossy wet-look lips", term: "glossy wet-look lips" , adultOnly: true },
|
|
541
557
|
{ id: "lip-state-bare", label: "Bare", dimension: "lip-state", description: "Natural, untreated, no makeup", promptHint: "with bare, natural, untreated lips", term: "bare natural lips" },
|
|
542
558
|
{ id: "lip-state-bold-red", label: "Bold Red", dimension: "lip-state", description: "Saturated red lipstick statement", promptHint: "with a bold, saturated red lipstick statement", term: "bold red lipstick" },
|
|
543
|
-
{ id: "lip-state-bitten", label: "Bitten", dimension: "lip-state", description: "Slight playful lip-bite, lower lip caught", promptHint: "playfully biting the lower lip", term: "biting the lower lip" },
|
|
544
|
-
{ 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", term: "lips slightly parted" },
|
|
559
|
+
{ id: "lip-state-bitten", label: "Bitten", dimension: "lip-state", description: "Slight playful lip-bite, lower lip caught", promptHint: "playfully biting the lower lip", term: "biting the lower lip" , adultOnly: true },
|
|
560
|
+
{ 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", term: "lips slightly parted" , adultOnly: true },
|
|
545
561
|
{ id: "lip-state-pursed", label: "Pursed", dimension: "lip-state", description: "Lips pressed and pushed forward", promptHint: "with lips pursed, pressed and pushed forward", term: "pursed lips" },
|
|
546
562
|
{ 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", term: "bold black lipstick" },
|
|
547
563
|
{ id: "lip-state-burgundy", label: "Burgundy", dimension: "lip-state", description: "Deep wine-red lipstick", promptHint: "with deep wine-red burgundy lipstick", term: "burgundy lipstick" },
|
|
548
564
|
{ id: "lip-state-plum", label: "Plum", dimension: "lip-state", description: "Dark purple-toned lipstick", promptHint: "with dark purple-toned plum lipstick", term: "plum lipstick" },
|
|
549
565
|
{ id: "lip-state-mauve", label: "Mauve", dimension: "lip-state", description: "Muted mauve / dusty-pink lipstick", promptHint: "with muted mauve, dusty-pink lipstick", term: "mauve lipstick" },
|
|
550
|
-
{ id: "lip-state-pouting", label: "Pouting", dimension: "lip-state", description: "Full pout", promptHint: "with a full pouting expression", term: "full pouting expression" },
|
|
566
|
+
{ id: "lip-state-pouting", label: "Pouting", dimension: "lip-state", description: "Full pout", promptHint: "with a full pouting expression", term: "full pouting expression" , adultOnly: true },
|
|
551
567
|
|
|
552
568
|
// -------------------- Hair Color --------------------
|
|
553
569
|
// Blonde family (light → warm → cool)
|
|
@@ -660,7 +676,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
660
676
|
|
|
661
677
|
// -------------------- Eye State (what the eyes are doing / where they look) --------------------
|
|
662
678
|
{ id: "eye-state-closed", label: "Closed", dimension: "eye-state", description: "Eyes fully closed, peaceful", promptHint: "with eyes fully closed in a peaceful expression", term: "eyes fully closed" },
|
|
663
|
-
{ id: "eye-state-half-lidded", label: "Half-lidded", dimension: "eye-state", description: "Heavy-lidded sleepy gaze", promptHint: "with heavy half-lidded sleepy eyes", term: "half-lidded sleepy eyes" },
|
|
679
|
+
{ id: "eye-state-half-lidded", label: "Half-lidded", dimension: "eye-state", description: "Heavy-lidded sleepy gaze", promptHint: "with heavy half-lidded sleepy eyes", term: "half-lidded sleepy eyes" , adultOnly: true },
|
|
664
680
|
{ 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" },
|
|
665
681
|
{ 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" },
|
|
666
682
|
{ id: "eye-state-gazing-away", label: "Gazing Away", dimension: "eye-state", description: "Looking off-camera, contemplative", promptHint: "gazing off-camera with a contemplative expression", term: "gazing off-camera" },
|
|
@@ -682,20 +698,20 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
682
698
|
{ id: "texture-wrinkled", label: "Wrinkled", dimension: "skin-texture", description: "Aged, deeply lined skin", promptHint: "with deep wrinkles and aged skin texture", term: "deeply wrinkled aged skin" },
|
|
683
699
|
{ id: "texture-goosebumps", label: "Goosebumps", dimension: "skin-texture", description: "Raised goosebumps on skin", promptHint: "with goosebumps raised on the skin" },
|
|
684
700
|
{ id: "texture-dewy", label: "Dewy", dimension: "skin-texture", description: "Glowing, dewy fresh skin", promptHint: "with dewy, glowing skin and a fresh sheen", term: "dewy glowing skin" },
|
|
685
|
-
{ id: "texture-glistening", label: "Glistening", dimension: "skin-texture", description: "Sweat or oil sheen", promptHint: "with glistening skin, sweat or oil catching the light", term: "glistening skin" },
|
|
701
|
+
{ id: "texture-glistening", label: "Glistening", dimension: "skin-texture", description: "Sweat or oil sheen", promptHint: "with glistening skin, sweat or oil catching the light", term: "glistening skin" , adultOnly: true },
|
|
686
702
|
{ id: "texture-weathered", label: "Weathered", dimension: "skin-texture", description: "Sun-aged rough skin", promptHint: "with weathered, sun-worn rough skin", term: "weathered sun-worn skin" },
|
|
687
703
|
{ id: "texture-porcelain", label: "Porcelain", dimension: "skin-texture", description: "Flawless near-translucent porcelain", promptHint: "with flawless, near-translucent porcelain skin", term: "porcelain skin" },
|
|
688
704
|
{ 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", term: "sun-kissed skin" },
|
|
689
705
|
{ id: "texture-tanned", label: "Tanned", dimension: "skin-texture", description: "Deeply, evenly tanned skin", promptHint: "with deeply, evenly bronzed tanned skin", term: "deeply tanned skin" },
|
|
690
|
-
{ 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", term: "visible swimwear tan lines" },
|
|
706
|
+
{ 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", term: "visible swimwear tan lines" , adultOnly: true },
|
|
691
707
|
{ 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", term: "freckled skin" },
|
|
692
708
|
{ id: "texture-ruddy", label: "Ruddy", dimension: "skin-texture", description: "Pink-flushed cheeks and nose", promptHint: "with ruddy, warm pink-flushed cheeks and nose", term: "ruddy flushed complexion" },
|
|
693
709
|
{ id: "texture-ashen", label: "Ashen / Pale", dimension: "skin-texture", description: "Cool pale, near-bloodless undertone", promptHint: "with ashen, cool pale, near-bloodless skin", term: "ashen pale skin" },
|
|
694
|
-
{ 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", term: "oily shiny skin" },
|
|
710
|
+
{ 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", term: "oily shiny skin" , adultOnly: true },
|
|
695
711
|
{ id: "texture-matte", label: "Matte", dimension: "skin-texture", description: "Poreless matte finish", promptHint: "with a poreless, matte skin finish", term: "matte skin finish" },
|
|
696
712
|
{ id: "texture-blemished", label: "Blemished", dimension: "skin-texture", description: "Visible blemishes, real-skin imperfections", promptHint: "with visible blemishes and natural real-skin imperfections", term: "blemished skin" },
|
|
697
713
|
{ 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", term: "baby-soft skin" },
|
|
698
|
-
{ 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", term: "shower-fresh wet skin" },
|
|
714
|
+
{ 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", term: "shower-fresh wet skin" , adultOnly: true },
|
|
699
715
|
{ 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", term: "acne-scarred skin" },
|
|
700
716
|
|
|
701
717
|
// -------------------- Distinctive Features --------------------
|
|
@@ -717,18 +733,18 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
717
733
|
{ 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" },
|
|
718
734
|
{ 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" },
|
|
719
735
|
{ id: "feature-hand-tattoos", label: "Hand Tattoos", dimension: "distinctive-features", description: "Knuckle / finger tattoos", promptHint: "with hand tattoos across knuckles and fingers" },
|
|
720
|
-
{ 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" },
|
|
721
|
-
{ 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" },
|
|
722
|
-
{ 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" },
|
|
736
|
+
{ 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" , adultOnly: true },
|
|
737
|
+
{ 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" , adultOnly: true },
|
|
738
|
+
{ 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" , adultOnly: true },
|
|
723
739
|
{ 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" },
|
|
724
740
|
{ 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", term: "multiple visible piercings" },
|
|
725
741
|
{ 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", term: "multiple stacked ear piercings" },
|
|
726
742
|
{ 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" },
|
|
727
743
|
{ 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" },
|
|
728
|
-
{ 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" },
|
|
729
|
-
{ 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", term: "visible collarbone" },
|
|
730
|
-
{ id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "wearing a cropped style with the midriff visible", term: "visible midriff" },
|
|
731
|
-
{ id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with the navel visible", term: "visible navel" },
|
|
744
|
+
{ 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" , adultOnly: true },
|
|
745
|
+
{ 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", term: "visible collarbone" , adultOnly: true },
|
|
746
|
+
{ id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "wearing a cropped style with the midriff visible", term: "visible midriff" , adultOnly: true },
|
|
747
|
+
{ id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with the navel visible", term: "visible navel" , adultOnly: true },
|
|
732
748
|
{ 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", term: "elongated swan-like neck" },
|
|
733
749
|
{ 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", term: "dark under-eye circles" },
|
|
734
750
|
{ id: "feature-fangs", label: "Fangs", dimension: "distinctive-features", description: "Visible fangs (vampire / character archetype)", promptHint: "with visible fangs at the canines, character / vampire archetype", term: "visible fangs at the canines" },
|
|
@@ -816,7 +832,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
816
832
|
// ----- Latin America -----
|
|
817
833
|
{ 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", term: "carioca aesthetic" },
|
|
818
834
|
{ 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", term: "paulista aesthetic" },
|
|
819
|
-
{ 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", term: "buenos aires tango aesthetic" },
|
|
835
|
+
{ 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", term: "buenos aires tango aesthetic" , adultOnly: true },
|
|
820
836
|
{ 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", term: "porteño aesthetic" },
|
|
821
837
|
{ 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", term: "mexico city chic aesthetic" },
|
|
822
838
|
{ 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", term: "chilango traditional aesthetic" },
|
|
@@ -1322,7 +1338,11 @@ function emitIndependentFragments(value: unknown, fragmentFor: PersonFragmentFor
|
|
|
1322
1338
|
const navel = ids.includes("feature-navel-visible")
|
|
1323
1339
|
for (const id of ids) {
|
|
1324
1340
|
if (midriff && navel && (id === "feature-midriff-visible" || id === "feature-navel-visible")) {
|
|
1325
|
-
|
|
1341
|
+
// The fold pushes a literal string without consulting `fragmentFor` — a
|
|
1342
|
+
// floored `fragmentFor` (see `emit` in collectPersonFragments) returns
|
|
1343
|
+
// "" for a flagged id, so gate the fold on that instead of hardcoding
|
|
1344
|
+
// the minor check here.
|
|
1345
|
+
if (id === "feature-midriff-visible" && fragmentFor(id) !== "") out.push("wearing a cropped style, midriff and navel visible")
|
|
1326
1346
|
continue
|
|
1327
1347
|
}
|
|
1328
1348
|
const fragment = fragmentFor(id)
|
|
@@ -1348,6 +1368,19 @@ function collectPersonFragments(
|
|
|
1348
1368
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
1349
1369
|
if (pre) hints.push(pre)
|
|
1350
1370
|
|
|
1371
|
+
// W1-a minor-age floor (Layer 1): a minor subject never emits an adultOnly
|
|
1372
|
+
// entry — drop, never soften. Adults are untouched (the golden fixture pins
|
|
1373
|
+
// that). The drop is checked on the ENTRY the collector itself resolves for
|
|
1374
|
+
// that id (`getPerson`, the same pack-composed lookup `fragmentFor` already
|
|
1375
|
+
// reads through) — not a separately-sourced id set — so pack-registered and
|
|
1376
|
+
// built-in entries are both covered by construction. `fragmentFor` is
|
|
1377
|
+
// wrapped so every branch below (independent fragments, the midriff+navel
|
|
1378
|
+
// fold, custom text) sees the same rule.
|
|
1379
|
+
const floored = isMinorAge(data)
|
|
1380
|
+
const emit: PersonFragmentFor = floored
|
|
1381
|
+
? (id) => (typeof id === "string" && getPerson(id)?.adultOnly === true ? "" : fragmentFor(id))
|
|
1382
|
+
: fragmentFor
|
|
1383
|
+
|
|
1351
1384
|
const fieldByDimension = getRegisteredPersonFieldByDimension()
|
|
1352
1385
|
for (const dimension of getRegisteredPersonDimensionOrder()) {
|
|
1353
1386
|
const field = fieldByDimension[dimension]
|
|
@@ -1356,22 +1389,22 @@ function collectPersonFragments(
|
|
|
1356
1389
|
if (dimension === "age") {
|
|
1357
1390
|
const ageId = typeof raw === "string" ? raw : undefined
|
|
1358
1391
|
const customAge = typeof data.customAge === "number" ? data.customAge : undefined
|
|
1359
|
-
const h = buildAgeFragment(ageId, customAge,
|
|
1392
|
+
const h = buildAgeFragment(ageId, customAge, emit)
|
|
1360
1393
|
if (h) hints.push(h)
|
|
1361
1394
|
continue
|
|
1362
1395
|
}
|
|
1363
1396
|
if (dimension === "ethnicity") {
|
|
1364
|
-
const h = buildEthnicityFragment(raw,
|
|
1397
|
+
const h = buildEthnicityFragment(raw, emit)
|
|
1365
1398
|
if (h) hints.push(h)
|
|
1366
1399
|
continue
|
|
1367
1400
|
}
|
|
1368
1401
|
if (dimension === "hair-color") {
|
|
1369
|
-
const h = buildHairColorFragment(raw,
|
|
1402
|
+
const h = buildHairColorFragment(raw, emit)
|
|
1370
1403
|
if (h) hints.push(h)
|
|
1371
1404
|
continue
|
|
1372
1405
|
}
|
|
1373
1406
|
if (dimension === "eye-color") {
|
|
1374
|
-
for (const h of buildEyeColorFragments(raw,
|
|
1407
|
+
for (const h of buildEyeColorFragments(raw, emit)) hints.push(h)
|
|
1375
1408
|
continue
|
|
1376
1409
|
}
|
|
1377
1410
|
if (
|
|
@@ -1381,11 +1414,11 @@ function collectPersonFragments(
|
|
|
1381
1414
|
dimension === "skin-texture" ||
|
|
1382
1415
|
dimension === "regional-aesthetic"
|
|
1383
1416
|
) {
|
|
1384
|
-
for (const h of emitIndependentFragments(raw,
|
|
1417
|
+
for (const h of emitIndependentFragments(raw, emit)) hints.push(h)
|
|
1385
1418
|
continue
|
|
1386
1419
|
}
|
|
1387
1420
|
if (typeof raw !== "string" || raw.length === 0) continue
|
|
1388
|
-
const hint =
|
|
1421
|
+
const hint = emit(raw)
|
|
1389
1422
|
if (hint) hints.push(hint)
|
|
1390
1423
|
}
|
|
1391
1424
|
|
|
@@ -1400,7 +1433,7 @@ function collectPersonFragments(
|
|
|
1400
1433
|
!data.lipFullness &&
|
|
1401
1434
|
!data.lipShape
|
|
1402
1435
|
) {
|
|
1403
|
-
const legacyLips =
|
|
1436
|
+
const legacyLips = emit(data.lips)
|
|
1404
1437
|
if (legacyLips) hints.push(legacyLips)
|
|
1405
1438
|
}
|
|
1406
1439
|
|
package/src/photo-genre.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { resolveTerm } from "./term.js"
|
|
31
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
31
32
|
|
|
32
33
|
export type PhotoGenreCategory =
|
|
33
34
|
| "editorial"
|
|
@@ -44,6 +45,16 @@ export interface PhotoGenre {
|
|
|
44
45
|
readonly category: PhotoGenreCategory
|
|
45
46
|
readonly description: string
|
|
46
47
|
readonly promptHint: string
|
|
48
|
+
/**
|
|
49
|
+
* W1-a minor-age floor (spec 2026-09-01 §3.3). `true` marks an entry whose
|
|
50
|
+
* hint describes body exposure, sheer/wet clothing, swimwear/lingerie, a
|
|
51
|
+
* seductive expression or gaze, or a body-placed tattoo. The fragment
|
|
52
|
+
* collectors DROP flagged entries for a minor subject (`isMinorAge`), the
|
|
53
|
+
* picker hides their tiles, the analyzer never emits them, and the backend
|
|
54
|
+
* policy strips their wording from free text. Hand-curated; the
|
|
55
|
+
* `adult-only-ratchet` test only ratchets. Never set on neutral defaults.
|
|
56
|
+
*/
|
|
57
|
+
readonly adultOnly?: true
|
|
47
58
|
/**
|
|
48
59
|
* Compact professional term (see `term.ts`). Authored only where the
|
|
49
60
|
* lowercased label is not what a photographer would write in a prompt —
|
|
@@ -70,7 +81,7 @@ export const PHOTO_GENRES: ReadonlyArray<PhotoGenre> = [
|
|
|
70
81
|
{ id: "brand-jil-sander", label: "Jil Sander Minimalism", category: "editorial", description: "Jil Sander — minimalist architectural muted", promptHint: "shot in the language of a Jil Sander editorial, minimalist composition, muted ecru and stone palette, architectural wardrobe with clean unbroken lines and the brand's quiet austere mood" },
|
|
71
82
|
{ id: "brand-vivienne-tam", label: "Vivienne Tam Style", category: "editorial", description: "Vivienne Tam — orientalist ornate fashion", promptHint: "shot in the language of a Vivienne Tam editorial, orientalist motifs and dragon embroidery, ornate East-Asian fashion detailing and a saturated jewel-toned palette" },
|
|
72
83
|
{ id: "brand-jacquemus", label: "Jacquemus Style", category: "editorial", description: "Jacquemus — sun-soaked surrealist playful", promptHint: "shot in the language of a Jacquemus campaign, sun-soaked Mediterranean light, surrealist scale play with oversized hats and miniature bags, terracotta and ochre palette and a playful French Riviera mood" },
|
|
73
|
-
{ id: "brand-helmut-newton", label: "Helmut Newton Style", category: "editorial", description: "Helmut Newton — high-contrast B&W provocation", promptHint: "shot in the signature language of Helmut Newton, high-contrast black-and-white, hard direct flash carving every line, statuesque pose, glamorous wardrobe and the cool confrontational eroticism of his 1970s editorial work" },
|
|
84
|
+
{ id: "brand-helmut-newton", label: "Helmut Newton Style", category: "editorial", description: "Helmut Newton — high-contrast B&W provocation", promptHint: "shot in the signature language of Helmut Newton, high-contrast black-and-white, hard direct flash carving every line, statuesque pose, glamorous wardrobe and the cool confrontational eroticism of his 1970s editorial work" , adultOnly: true },
|
|
74
85
|
{ id: "brand-harpers-bazaar", label: "Harper's Bazaar Style", category: "editorial", description: "Harper's Bazaar — high-fashion glossy", promptHint: "shot in the signature language of a Harper's Bazaar editorial, high-fashion glossy production, bold graphic backdrops, dramatic single-source key light and the brand's distinctively confident composition" },
|
|
75
86
|
|
|
76
87
|
// -------------------- Documentary / Candid --------------------
|
|
@@ -89,7 +100,7 @@ export const PHOTO_GENRES: ReadonlyArray<PhotoGenre> = [
|
|
|
89
100
|
{ id: "mugshot", label: "Mugshot", category: "studio-formal", description: "Police booking-style portrait", promptHint: "shot as a police booking mugshot, harsh fluorescent overhead lighting, height chart on the wall behind, dead-square frontal framing and a flat institutional color cast" },
|
|
90
101
|
{ id: "wedding-portrait", label: "Wedding Portrait", category: "studio-formal", description: "Romantic bridal-style portrait", promptHint: "shot as a wedding portrait, soft golden-hour backlight, romantic shallow depth of field, dreamy pastel color grade and an intimate posed embrace or solo bridal stance" },
|
|
91
102
|
{ id: "family-portrait", label: "Family Portrait", category: "studio-formal", description: "Posed family group shot", promptHint: "shot as a posed family portrait, warm even studio lighting, neutral backdrop, coordinated wardrobe and the group arranged in a balanced pyramid composition" },
|
|
92
|
-
{ id: "glamour-portrait", label: "Glamour Portrait", category: "studio-formal", description: "Soft-focus glamour portrait", promptHint: "shot as a glamour portrait with soft diffused beauty lighting, a high-key warm tone, gentle skin retouch, voluminous hair and a sultry over-the-shoulder pose" },
|
|
103
|
+
{ id: "glamour-portrait", label: "Glamour Portrait", category: "studio-formal", description: "Soft-focus glamour portrait", promptHint: "shot as a glamour portrait with soft diffused beauty lighting, a high-key warm tone, gentle skin retouch, voluminous hair and a sultry over-the-shoulder pose" , adultOnly: true },
|
|
93
104
|
{ id: "film-noir", label: "Film Noir", category: "studio-formal", description: "Hard-shadow noir portrait", promptHint: "shot in a film-noir portrait idiom, hard single-source key light casting deep shadows, venetian-blind shadow patterns, monochrome high-contrast palette and a brooding closed-off pose" },
|
|
94
105
|
|
|
95
106
|
// -------------------- Selfie sub-types --------------------
|
|
@@ -124,7 +135,7 @@ const photoGenreById = new Map<string, PhotoGenre>(
|
|
|
124
135
|
|
|
125
136
|
export function getPhotoGenre(id: string | undefined | null): PhotoGenre | undefined {
|
|
126
137
|
if (!id) return undefined
|
|
127
|
-
return photoGenreById.get(id)
|
|
138
|
+
return overlayEntry("photo-genre", id, photoGenreById.get(id))
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
export function getPhotoGenreLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/photographer.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
23
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
23
24
|
|
|
24
25
|
export type PhotographerCategory =
|
|
25
26
|
| "editorial"
|
|
@@ -642,7 +643,7 @@ const photographerById = new Map<string, Photographer>(
|
|
|
642
643
|
|
|
643
644
|
export function getPhotographer(id: string | undefined | null): Photographer | undefined {
|
|
644
645
|
if (!id) return undefined
|
|
645
|
-
return photographerById.get(id)
|
|
646
|
+
return overlayEntry("photographer", id, photographerById.get(id))
|
|
646
647
|
}
|
|
647
648
|
|
|
648
649
|
export function getPhotographerLabel(id: string | undefined | null, fallback?: string): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
|
+
import { isMinorAge, getAdultOnlyIds } from "./age-floor.js"
|
|
2
3
|
import {
|
|
3
4
|
PEOPLE,
|
|
4
5
|
PERSON_DIMENSION_ORDER,
|
|
@@ -104,6 +105,14 @@ const personCleanup: ApplyCleanup = (patch, mode) => {
|
|
|
104
105
|
} else if ("age" in patch && patch.age !== "age-custom") {
|
|
105
106
|
patch.customAge = undefined
|
|
106
107
|
}
|
|
108
|
+
// W1-a: an analysis that returned a minor age never carries a flagged id.
|
|
109
|
+
if (isMinorAge(patch as { age?: string; customAge?: number; type?: string })) {
|
|
110
|
+
const drop = getAdultOnlyIds()
|
|
111
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
112
|
+
if (typeof v === "string" && drop.has(v)) (patch as Record<string, unknown>)[k] = undefined
|
|
113
|
+
else if (Array.isArray(v)) (patch as Record<string, unknown>)[k] = v.filter((x) => !(typeof x === "string" && drop.has(x)))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
export const PICKER_ANALYZER_REGISTRY = {
|
package/src/picker-catalogs.ts
CHANGED
|
@@ -87,6 +87,7 @@ import { INSTRUMENTS, PRODUCTION_STYLES, VOCAL_PRESENCE, SINGING_STYLES } from "
|
|
|
87
87
|
import { VOICE_AGES, VOICE_GENDERS, VOICE_LANGUAGES, VOICE_ACCENTS, VOICE_TIMBRES } from "./voice-character.js"
|
|
88
88
|
import { VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES } from "./voice-delivery.js"
|
|
89
89
|
import { composePickerCatalogs, getRegisteredCatalogPacks, catalogPacksVersion } from "./catalog-packs.js"
|
|
90
|
+
import { setComposedCatalogResolver } from "./catalog-overlay.js"
|
|
90
91
|
import { deriveTerm, resolveTerm } from "./term.js"
|
|
91
92
|
|
|
92
93
|
export interface PickerOption {
|
|
@@ -96,6 +97,8 @@ export interface PickerOption {
|
|
|
96
97
|
/** The group id (matches `categoryOrder` / `categoryLabels`). */
|
|
97
98
|
readonly category?: string
|
|
98
99
|
readonly promptHint: string
|
|
100
|
+
/** W1-a: see Person.adultOnly. Carried verbatim through packs. */
|
|
101
|
+
readonly adultOnly?: true
|
|
99
102
|
/**
|
|
100
103
|
* The short professional term this id injects in COMPACT hint mode ("whip
|
|
101
104
|
* pan left" where `promptHint` is the full mechanism sentence). Always
|
|
@@ -156,6 +159,8 @@ interface BaseCatalogEntry {
|
|
|
156
159
|
readonly promptHint: string
|
|
157
160
|
/** Optional authored compact term (see `term.ts` for the convention). */
|
|
158
161
|
readonly term?: string
|
|
162
|
+
/** W1-a: see Person.adultOnly. Propagated verbatim into the flattened option. */
|
|
163
|
+
readonly adultOnly?: true
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
/**
|
|
@@ -183,6 +188,7 @@ function toOptions<T extends BaseCatalogEntry>(
|
|
|
183
188
|
}
|
|
184
189
|
if (e.description) opt.description = e.description
|
|
185
190
|
if (categoryField) opt.category = e[categoryField] as unknown as string
|
|
191
|
+
if (e.adultOnly) opt.adultOnly = true
|
|
186
192
|
return opt as PickerOption
|
|
187
193
|
})
|
|
188
194
|
}
|
|
@@ -836,6 +842,11 @@ export function getPickerCatalog(nodeTypeOrCatalogId: string): PickerCatalog | u
|
|
|
836
842
|
)
|
|
837
843
|
}
|
|
838
844
|
|
|
845
|
+
// Install the composed view into the per-catalog getters (catalog-overlay.ts).
|
|
846
|
+
// Done here, at the one module that already imports every catalog, so the
|
|
847
|
+
// catalog modules themselves never import upward.
|
|
848
|
+
setComposedCatalogResolver(getPickerCatalog)
|
|
849
|
+
|
|
839
850
|
export function listPickerCatalogs(): readonly PickerCatalog[] {
|
|
840
851
|
return getRegisteredPickerCatalogs()
|
|
841
852
|
}
|