@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { PickerCatalog, PickerOption } from "./picker-catalogs.js"
|
|
2
|
+
import { catalogPacksVersion, hasCatalogPacksFor } from "./catalog-packs.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* THE RESOLVER-SIDE HALF OF CATALOG CURATION.
|
|
6
|
+
*
|
|
7
|
+
* Every per-catalog module builds an index from its own base constant at load
|
|
8
|
+
* (`settingById = new Map(SETTINGS…)`) and its `getSetting(id)` reads that. A
|
|
9
|
+
* deployment's catalog packs compose a DIFFERENT catalog — entries removed,
|
|
10
|
+
* some rewritten at the same id — and until this module existed nothing on
|
|
11
|
+
* the resolve path read it. So on a curated deployment a denied id still
|
|
12
|
+
* turned into its full stock prompt text at run time, and a rewritten entry
|
|
13
|
+
* injected the stock wording instead of the curated one. The curation was
|
|
14
|
+
* real in `/v1/catalogs` and invisible in every generated prompt.
|
|
15
|
+
*
|
|
16
|
+
* `overlayEntry` sits inside each getter, between the base lookup and the
|
|
17
|
+
* return:
|
|
18
|
+
* - no pack targets the catalog → the base entry, untouched. This is the
|
|
19
|
+
* mainline path and it is byte-identical by construction; a deployment
|
|
20
|
+
* that curates nothing pays one Set probe per lookup.
|
|
21
|
+
* - a pack targets it, id absent → undefined. Denied means denied on the
|
|
22
|
+
* prompt path too, not only in the picker.
|
|
23
|
+
* - a pack targets it, id present → the base entry with the composed
|
|
24
|
+
* option's label / description / promptHint / term / category on top,
|
|
25
|
+
* so a rewrite reaches the prompt. Fields the composed option does not
|
|
26
|
+
* carry (typed extras a specific builder reads) survive from the base.
|
|
27
|
+
* - pack-ADDED id (no base entry) → the composed option itself.
|
|
28
|
+
*
|
|
29
|
+
* LATE-BOUND RESOLVER, ON PURPOSE. The catalog modules cannot import
|
|
30
|
+
* `getPickerCatalog`: picker-catalogs.ts imports every one of them to build
|
|
31
|
+
* `PICKER_CATALOGS` at module scope, and a module-scope cycle is a TDZ error
|
|
32
|
+
* under some load orders. picker-catalogs.ts instead installs its resolver
|
|
33
|
+
* here when IT loads (`setComposedCatalogResolver`), and this module imports
|
|
34
|
+
* only the pack registry, which imports nothing of the catalogs.
|
|
35
|
+
*
|
|
36
|
+
* FAIL CLOSED. If a pack targets a catalog and the resolver is somehow not
|
|
37
|
+
* installed, every lookup on that catalog throws rather than silently
|
|
38
|
+
* returning the base entry — a curated deployment answering with stock text
|
|
39
|
+
* is the failure this whole module exists to prevent.
|
|
40
|
+
*
|
|
41
|
+
* SCOPE BOUNDARY, on purpose: the transition / character-fx TIMING dimensions
|
|
42
|
+
* (position start/middle/end, duration, intensity) are structural knobs the
|
|
43
|
+
* catalogs expose for thin clients, not content — no curation removes "end".
|
|
44
|
+
* Their composers index total clause records by construction, and threading
|
|
45
|
+
* the overlay through that would trade a guaranteed-total prompt for a hole
|
|
46
|
+
* nothing needs. They stay uncurated; the wall still checks their ids.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
type Resolver = (catalogIdOrNodeType: string) => PickerCatalog | undefined
|
|
50
|
+
let resolver: Resolver | null = null
|
|
51
|
+
|
|
52
|
+
/** Installed by picker-catalogs.ts at load. */
|
|
53
|
+
export function setComposedCatalogResolver(fn: Resolver): void {
|
|
54
|
+
resolver = fn
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let memo: { v: number; byCatalog: Map<string, ReadonlyMap<string, PickerOption>> } | null = null
|
|
58
|
+
|
|
59
|
+
/** id → composed option for one catalog, memoized on the pack version. */
|
|
60
|
+
export function composedOptionIndex(catalogId: string): ReadonlyMap<string, PickerOption> {
|
|
61
|
+
const v = catalogPacksVersion()
|
|
62
|
+
if (!memo || memo.v !== v) memo = { v, byCatalog: new Map() }
|
|
63
|
+
const hit = memo.byCatalog.get(catalogId)
|
|
64
|
+
if (hit) return hit
|
|
65
|
+
if (!resolver) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`[catalog-overlay] catalog "${catalogId}" is curated by a pack but the composed-catalog resolver is not installed — refusing to resolve against the stock catalog`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
const cat = resolver(catalogId)
|
|
71
|
+
const index = new Map<string, PickerOption>()
|
|
72
|
+
if (cat) {
|
|
73
|
+
for (const o of cat.options ?? []) index.set(o.id, o)
|
|
74
|
+
for (const d of cat.dimensions ?? []) for (const o of d.options) if (!index.has(o.id)) index.set(o.id, o)
|
|
75
|
+
}
|
|
76
|
+
memo.byCatalog.set(catalogId, index)
|
|
77
|
+
return index
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Overlay the composed catalog on a base lookup. See the module doc for the
|
|
82
|
+
* four outcomes. `T` is the module's own entry type; the composed option's
|
|
83
|
+
* common fields are spread over it, so callers keep their typed result.
|
|
84
|
+
*/
|
|
85
|
+
export function overlayEntry<T extends { readonly id: string }>(
|
|
86
|
+
catalogId: string,
|
|
87
|
+
id: string,
|
|
88
|
+
base: T | undefined,
|
|
89
|
+
): T | undefined {
|
|
90
|
+
if (!hasCatalogPacksFor(catalogId)) return base
|
|
91
|
+
const opt = composedOptionIndex(catalogId).get(id)
|
|
92
|
+
if (!opt) return undefined
|
|
93
|
+
if (!base) return opt as unknown as T
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
label: opt.label,
|
|
97
|
+
promptHint: opt.promptHint,
|
|
98
|
+
term: opt.term,
|
|
99
|
+
...(opt.description !== undefined ? { description: opt.description } : {}),
|
|
100
|
+
...(opt.category !== undefined ? { category: opt.category } : {}),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Membership only — for id-carrying fields whose entries live outside this
|
|
106
|
+
* package (the animal / vehicle / weapon / furniture catalogs resolve through
|
|
107
|
+
* `@nodaro/shared`, which cannot depend on this package). The dispatcher asks
|
|
108
|
+
* this before calling those getters.
|
|
109
|
+
*/
|
|
110
|
+
export function composedHas(catalogId: string, id: string): boolean {
|
|
111
|
+
if (!hasCatalogPacksFor(catalogId)) return true
|
|
112
|
+
return composedOptionIndex(catalogId).has(id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The composed option, or undefined — for callers that render from it directly. */
|
|
116
|
+
export function composedOption(catalogId: string, id: string): PickerOption | undefined {
|
|
117
|
+
if (!hasCatalogPacksFor(catalogId)) return undefined
|
|
118
|
+
return composedOptionIndex(catalogId).get(id)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A base entry LIST filtered to ids the composed catalog offers and relabelled
|
|
123
|
+
* where a pack rewrote an entry — the list-shaped twin of `overlayEntry`, for
|
|
124
|
+
* anything that renders a whole catalog (pickers, dropdowns, studio browsers).
|
|
125
|
+
* Returns the base array BY IDENTITY when no pack targets the catalog, so
|
|
126
|
+
* memo deps and equality checks on mainline are unchanged. Pack-ADDED entries
|
|
127
|
+
* are not surfaced here: the caller's typed entry shape may carry fields a
|
|
128
|
+
* PickerOption cannot supply.
|
|
129
|
+
*/
|
|
130
|
+
export function curateEntries<T extends { readonly id: string; readonly label: string }>(
|
|
131
|
+
catalogId: string,
|
|
132
|
+
base: readonly T[],
|
|
133
|
+
): readonly T[] {
|
|
134
|
+
if (!hasCatalogPacksFor(catalogId)) return base
|
|
135
|
+
const index = composedOptionIndex(catalogId)
|
|
136
|
+
const out: T[] = []
|
|
137
|
+
for (const e of base) {
|
|
138
|
+
const opt = index.get(e.id)
|
|
139
|
+
if (!opt) continue
|
|
140
|
+
out.push({
|
|
141
|
+
...e,
|
|
142
|
+
label: opt.label,
|
|
143
|
+
...(opt.description !== undefined ? { description: opt.description } : {}),
|
|
144
|
+
...("promptHint" in e && opt.promptHint !== undefined ? { promptHint: opt.promptHint } : {}),
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
return out
|
|
148
|
+
}
|
package/src/catalog-packs.ts
CHANGED
|
@@ -59,20 +59,45 @@ export interface CatalogPack {
|
|
|
59
59
|
|
|
60
60
|
let packs: CatalogPack[] = []
|
|
61
61
|
let version = 0
|
|
62
|
+
// Registration can land AFTER a consumer first read (the browser fetches a
|
|
63
|
+
// deployment's composed catalogs at boot; a picker may already be mounted).
|
|
64
|
+
// A read-only memo keyed on `version` sees the change on its NEXT read, but
|
|
65
|
+
// nothing re-runs a React render for it — that gap is how a deployment's
|
|
66
|
+
// curation once rendered as the stock catalogs for a whole session. Listeners
|
|
67
|
+
// let a UI hook subscribe to the version instead of polling it.
|
|
68
|
+
const listeners = new Set<() => void>()
|
|
69
|
+
function bump(): void {
|
|
70
|
+
version++
|
|
71
|
+
for (const l of listeners) l()
|
|
72
|
+
}
|
|
62
73
|
|
|
63
74
|
export function registerCatalogPack(pack: CatalogPack): void {
|
|
64
75
|
if (packs.some((p) => p.id === pack.id)) throw new Error(`duplicate catalog pack id "${pack.id}"`)
|
|
65
76
|
packs = [...packs, pack]
|
|
66
|
-
version++
|
|
67
77
|
// Push this pack's localized sidecars into the shared app-UI localizer (G10)
|
|
68
78
|
// so pack-added entries resolve in `resolveLabel`/`resolveDescription`/search.
|
|
69
79
|
// One generic point covers every pack, incl. the person extend fan-out.
|
|
70
80
|
registerCatalogSidecars(pack.catalogId, pack.sidecars)
|
|
81
|
+
bump()
|
|
71
82
|
}
|
|
72
83
|
export function getRegisteredCatalogPacks(): readonly CatalogPack[] { return packs }
|
|
73
|
-
export function resetCatalogPacks(): void { packs = [];
|
|
84
|
+
export function resetCatalogPacks(): void { packs = []; resetCatalogSidecars(); bump() }
|
|
74
85
|
export function catalogPacksVersion(): number { return version }
|
|
75
86
|
|
|
87
|
+
/** True when at least one pack targets this catalog — i.e. this deployment
|
|
88
|
+
* CURATES it and the base constants are no longer the truth for it. */
|
|
89
|
+
export function hasCatalogPacksFor(catalogId: string): boolean {
|
|
90
|
+
return packs.some((p) => p.catalogId === catalogId)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Subscribe to registrations/resets. Returns the unsubscribe. */
|
|
94
|
+
export function subscribeCatalogPacks(listener: () => void): () => void {
|
|
95
|
+
listeners.add(listener)
|
|
96
|
+
return () => {
|
|
97
|
+
listeners.delete(listener)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
function cloneCatalog(c: PickerCatalogInput): PickerCatalog {
|
|
77
102
|
return {
|
|
78
103
|
...c,
|
package/src/character-fx.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
22
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
22
23
|
|
|
23
24
|
export type CharacterFxCategory =
|
|
24
25
|
| "transformation"
|
|
@@ -249,7 +250,7 @@ const characterFxById = new Map<string, CharacterFx>(
|
|
|
249
250
|
|
|
250
251
|
export function getCharacterFx(id: string | undefined | null): CharacterFx | undefined {
|
|
251
252
|
if (!id) return undefined
|
|
252
|
-
return characterFxById.get(id)
|
|
253
|
+
return overlayEntry("character-fx", id, characterFxById.get(id))
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
export function getCharacterFxLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/color-look.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { resolveTerm } from "./term.js"
|
|
15
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
15
16
|
|
|
16
17
|
export type ColorLookCategory = "palette" | "film-emulation" | "social-preset"
|
|
17
18
|
|
|
@@ -98,7 +99,7 @@ const colorLookById = new Map<string, ColorLook>(COLOR_LOOKS.map((c) => [c.id, c
|
|
|
98
99
|
|
|
99
100
|
export function getColorLook(id: string | undefined | null): ColorLook | undefined {
|
|
100
101
|
if (!id) return undefined
|
|
101
|
-
return colorLookById.get(id)
|
|
102
|
+
return overlayEntry("color-look", id, colorLookById.get(id))
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export function getColorLookLabel(id: string | undefined | null, fallback?: string): string {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { resolveTerm } from "./term.js"
|
|
19
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
19
20
|
|
|
20
21
|
export interface CompositionEffect {
|
|
21
22
|
readonly id: string
|
|
@@ -64,7 +65,7 @@ const compositionEffectById = new Map<string, CompositionEffect>(
|
|
|
64
65
|
|
|
65
66
|
export function getCompositionEffect(id: string | undefined | null): CompositionEffect | undefined {
|
|
66
67
|
if (!id) return undefined
|
|
67
|
-
return compositionEffectById.get(id)
|
|
68
|
+
return overlayEntry("composition-effects", id, compositionEffectById.get(id))
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
export function getCompositionEffectLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/era.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { resolveTerm } from "./term.js"
|
|
21
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
21
22
|
|
|
22
23
|
export type EraCategory = "decade-20c" | "pre-modern" | "speculative"
|
|
23
24
|
|
|
@@ -282,7 +283,7 @@ const eraById = new Map<string, Era>(ERAS.map((e) => [e.id, e]))
|
|
|
282
283
|
|
|
283
284
|
export function getEra(id: string | undefined | null): Era | undefined {
|
|
284
285
|
if (!id) return undefined
|
|
285
|
-
return eraById.get(id)
|
|
286
|
+
return overlayEntry("era", id, eraById.get(id))
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
export function getEraLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/exposure-settings.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 ExposureCategory = "aperture" | "shutter-speed" | "iso"
|
|
25
26
|
|
|
@@ -97,7 +98,7 @@ const exposureById = new Map<string, ExposureSettings>(
|
|
|
97
98
|
|
|
98
99
|
export function getExposure(id: string | undefined | null): ExposureSettings | undefined {
|
|
99
100
|
if (!id) return undefined
|
|
100
|
-
return exposureById.get(id)
|
|
101
|
+
return overlayEntry("exposure-settings", id, exposureById.get(id))
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
export function getExposureLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/framing.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
13
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
13
14
|
|
|
14
15
|
export type FramingCategory =
|
|
15
16
|
| "shot-size"
|
|
@@ -568,7 +569,7 @@ const framingById = new Map<string, Framing>(FRAMINGS.map((f) => [f.id, f]))
|
|
|
568
569
|
|
|
569
570
|
export function getFraming(id: string | undefined | null): Framing | undefined {
|
|
570
571
|
if (!id) return undefined
|
|
571
|
-
return framingById.get(id)
|
|
572
|
+
return overlayEntry("framing", id, framingById.get(id))
|
|
572
573
|
}
|
|
573
574
|
|
|
574
575
|
export function getFramingLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/held-prop.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
26
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
26
27
|
|
|
27
28
|
export type HeldPropCategory =
|
|
28
29
|
| "device"
|
|
@@ -142,7 +143,7 @@ const heldPropById = new Map<string, HeldProp>(HELD_PROPS.map((p) => [p.id, p]))
|
|
|
142
143
|
|
|
143
144
|
export function getHeldProp(id: string | undefined | null): HeldProp | undefined {
|
|
144
145
|
if (!id) return undefined
|
|
145
|
-
return heldPropById.get(id)
|
|
146
|
+
return overlayEntry("held-prop", id, heldPropById.get(id))
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
export function getHeldPropLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/index.ts
CHANGED
|
@@ -35,8 +35,12 @@ export * from "./veo-i2v-inputs.js"
|
|
|
35
35
|
export * from "./person.js"
|
|
36
36
|
export * from "./picker-catalogs.js"
|
|
37
37
|
export * from "./catalog-packs.js"
|
|
38
|
+
export * from "./catalog-overlay.js"
|
|
39
|
+
export * from "./shared-catalog-overlay.js"
|
|
40
|
+
export * from "./catalog-id-guard.js"
|
|
38
41
|
export * from "./catalog-sidecar-coverage.js"
|
|
39
42
|
export * from "./person-packs.js"
|
|
43
|
+
export * from "./age-floor.js"
|
|
40
44
|
export * from "./picker-analyzer-registry.js"
|
|
41
45
|
export * from "./action-fx.js"
|
|
42
46
|
export * from "./aesthetic.js"
|
package/src/instrumentation.ts
CHANGED
|
@@ -273,16 +273,16 @@ const VOCAL_BY_ID = new Map(VOCAL_PRESENCE.map((x) => [x.id, x]))
|
|
|
273
273
|
const SINGING_STYLE_BY_ID = new Map(SINGING_STYLES.map((x) => [x.id, x]))
|
|
274
274
|
|
|
275
275
|
export function getInstrument(id: string | undefined): CategorizedInstrument | undefined {
|
|
276
|
-
return id ? INSTRUMENT_BY_ID.get(id) : undefined
|
|
276
|
+
return id ? overlayEntry("instrumentation", id, INSTRUMENT_BY_ID.get(id)) : undefined
|
|
277
277
|
}
|
|
278
278
|
export function getProductionStyle(id: string | undefined): InstrumentationEntry | undefined {
|
|
279
|
-
return id ? PRODUCTION_BY_ID.get(id) : undefined
|
|
279
|
+
return id ? overlayEntry("instrumentation", id, PRODUCTION_BY_ID.get(id)) : undefined
|
|
280
280
|
}
|
|
281
281
|
export function getVocalPresence(id: string | undefined): InstrumentationEntry | undefined {
|
|
282
|
-
return id ? VOCAL_BY_ID.get(id) : undefined
|
|
282
|
+
return id ? overlayEntry("instrumentation", id, VOCAL_BY_ID.get(id)) : undefined
|
|
283
283
|
}
|
|
284
284
|
export function getSingingStyle(id: string | undefined): InstrumentationEntry | undefined {
|
|
285
|
-
return id ? SINGING_STYLE_BY_ID.get(id) : undefined
|
|
285
|
+
return id ? overlayEntry("instrumentation", id, SINGING_STYLE_BY_ID.get(id)) : undefined
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
/**
|
|
@@ -306,6 +306,7 @@ export function getSingingStyleTerm(id: string | undefined | null): string {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
import { pickIds } from "@nodaro/shared"
|
|
309
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
309
310
|
|
|
310
311
|
export function buildInstrumentationHints(
|
|
311
312
|
data: {
|
package/src/lens.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { resolveTerm } from "./term.js"
|
|
14
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
14
15
|
|
|
15
16
|
export interface Lens {
|
|
16
17
|
readonly id: string
|
|
@@ -53,7 +54,7 @@ const lensById = new Map<string, Lens>(LENSES.map((l) => [l.id, l]))
|
|
|
53
54
|
|
|
54
55
|
export function getLens(id: string | undefined | null): Lens | undefined {
|
|
55
56
|
if (!id) return undefined
|
|
56
|
-
return lensById.get(id)
|
|
57
|
+
return overlayEntry("lens", id, lensById.get(id))
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export function getLensLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/lighting.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
14
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
14
15
|
|
|
15
16
|
export type LightingCategory =
|
|
16
17
|
| "time-of-day"
|
|
@@ -143,7 +144,7 @@ const lightingById = new Map<string, Lighting>(LIGHTINGS.map((l) => [l.id, l]))
|
|
|
143
144
|
|
|
144
145
|
export function getLighting(id: string | undefined | null): Lighting | undefined {
|
|
145
146
|
if (!id) return undefined
|
|
146
|
-
return lightingById.get(id)
|
|
147
|
+
return overlayEntry("lighting", id, lightingById.get(id))
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
export function getLightingLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/loop-subject.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { resolveTerm } from "./term.js"
|
|
22
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
22
23
|
|
|
23
24
|
export type LoopSubjectCategory = "realistic" | "abstract"
|
|
24
25
|
|
|
@@ -280,7 +281,8 @@ export const LOOP_SUBJECTS: ReadonlyArray<LoopSubject> = [
|
|
|
280
281
|
]
|
|
281
282
|
|
|
282
283
|
export function getLoopSubject(id: string | undefined | null): LoopSubject | undefined {
|
|
283
|
-
|
|
284
|
+
if (!id) return undefined
|
|
285
|
+
return overlayEntry("loop-subject", id, LOOP_SUBJECTS.find((s) => s.id === id))
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
export function getLoopSubjectLabel(id: string): string {
|
package/src/materials.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
27
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
27
28
|
|
|
28
29
|
export type MaterialCategory =
|
|
29
30
|
| "fabric"
|
|
@@ -139,7 +140,7 @@ const materialById = new Map<string, Material>(MATERIALS.map((m) => [m.id, m]))
|
|
|
139
140
|
|
|
140
141
|
export function getMaterial(id: string | undefined | null): Material | undefined {
|
|
141
142
|
if (!id) return undefined
|
|
142
|
-
return materialById.get(id)
|
|
143
|
+
return overlayEntry("materials", id, materialById.get(id))
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
export function getMaterialLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/mood.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
27
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
27
28
|
|
|
28
29
|
export type MoodCategory = "positive" | "negative" | "neutral" | "intense"
|
|
29
30
|
|
|
@@ -33,6 +34,16 @@ export interface Mood {
|
|
|
33
34
|
readonly category: MoodCategory
|
|
34
35
|
readonly description: string
|
|
35
36
|
readonly promptHint: string
|
|
37
|
+
/**
|
|
38
|
+
* W1-a minor-age floor (spec 2026-09-01 §3.3). `true` marks an entry whose
|
|
39
|
+
* hint describes body exposure, sheer/wet clothing, swimwear/lingerie, a
|
|
40
|
+
* seductive expression or gaze, or a body-placed tattoo. The fragment
|
|
41
|
+
* collectors DROP flagged entries for a minor subject (`isMinorAge`), the
|
|
42
|
+
* picker hides their tiles, the analyzer never emits them, and the backend
|
|
43
|
+
* policy strips their wording from free text. Hand-curated; the
|
|
44
|
+
* `adult-only-ratchet` test only ratchets. Never set on neutral defaults.
|
|
45
|
+
*/
|
|
46
|
+
readonly adultOnly?: true
|
|
36
47
|
/**
|
|
37
48
|
* Compact professional term injected in compact hint mode (see `term.ts`).
|
|
38
49
|
*
|
|
@@ -91,10 +102,10 @@ export const MOODS: ReadonlyArray<Mood> = [
|
|
|
91
102
|
{ id: "determined", label: "Determined", category: "intense", description: "Resolute, focused will", promptHint: "with a determined, resolute expression and iron focus", term: "determined, resolute expression" },
|
|
92
103
|
{ id: "passionate", label: "Passionate", category: "intense", description: "Burning passion", promptHint: "with a passionate, burning expression full of conviction", term: "passionate expression" },
|
|
93
104
|
{ id: "brooding", label: "Brooding", category: "intense", description: "Dark, brooding melancholy", promptHint: "with a brooding, dark expression and moody introspection", term: "brooding expression" },
|
|
94
|
-
{ id: "seductive", label: "Seductive", category: "intense", description: "Alluring, seductive", promptHint: "with a seductive, alluring expression and lidded gaze", term: "seductive expression" },
|
|
105
|
+
{ id: "seductive", label: "Seductive", category: "intense", description: "Alluring, seductive", promptHint: "with a seductive, alluring expression and lidded gaze", term: "seductive expression" , adultOnly: true },
|
|
95
106
|
{ id: "defiant", label: "Defiant", category: "intense", description: "Defiant, unyielding", promptHint: "with a defiant, unyielding expression and challenging stance", term: "defiant expression" },
|
|
96
|
-
{ id: "sultry", label: "Sultry", category: "intense", description: "Smoldering, heavy-lidded", promptHint: "with a sultry, smoldering expression, heavy-lidded gaze and lips softly parted", term: "sultry expression" },
|
|
97
|
-
{ id: "smoldering", label: "Smoldering", category: "intense", description: "Coiled, slow-burning intensity", promptHint: "with a smoldering, coiled expression, slow-burning intensity behind half-closed eyes and a still, predatory calm", term: "smoldering intensity" },
|
|
107
|
+
{ id: "sultry", label: "Sultry", category: "intense", description: "Smoldering, heavy-lidded", promptHint: "with a sultry, smoldering expression, heavy-lidded gaze and lips softly parted", term: "sultry expression" , adultOnly: true },
|
|
108
|
+
{ id: "smoldering", label: "Smoldering", category: "intense", description: "Coiled, slow-burning intensity", promptHint: "with a smoldering, coiled expression, slow-burning intensity behind half-closed eyes and a still, predatory calm", term: "smoldering intensity" , adultOnly: true },
|
|
98
109
|
{ id: "sinister", label: "Sinister", category: "intense", description: "Dark, malicious, threatening", promptHint: "with a sinister expression, a slow crooked smile and eyes glinting with dark malicious intent", term: "sinister expression" },
|
|
99
110
|
{ id: "wiccan-mystical", label: "Wiccan / Mystical", category: "intense", description: "Quietly otherworldly, occult", promptHint: "with a quietly mystical, otherworldly expression, eyes distant and knowing as if reading something the camera cannot see", term: "occult, otherworldly mystical expression" },
|
|
100
111
|
{ id: "lazy-shy", label: "Lazy Shy", category: "neutral", description: "Drowsy, soft, half-shy", promptHint: "with a soft, drowsy half-shy expression, eyes lowered and barely-there smile, languid and unbothered", term: "drowsy, half-shy expression" },
|
|
@@ -102,7 +113,7 @@ export const MOODS: ReadonlyArray<Mood> = [
|
|
|
102
113
|
{ id: "shocked", label: "Shocked", category: "intense", description: "Surprised, mouth open", promptHint: "with a shocked, surprised expression, eyes wide, brows raised and mouth open", term: "shocked expression" },
|
|
103
114
|
|
|
104
115
|
// -------------------- Micro-emotions --------------------
|
|
105
|
-
{ id: "flirty", label: "Flirty", category: "positive", description: "Playful flirtation, lingering smile, sustained eye contact", promptHint: "with a flirty expression, a lingering playful smile and sustained, knowing eye contact with the camera", term: "flirty expression" },
|
|
116
|
+
{ id: "flirty", label: "Flirty", category: "positive", description: "Playful flirtation, lingering smile, sustained eye contact", promptHint: "with a flirty expression, a lingering playful smile and sustained, knowing eye contact with the camera", term: "flirty expression" , adultOnly: true },
|
|
106
117
|
{ id: "suspicious", label: "Suspicious", category: "negative", description: "Wary distrust, narrowed eyes, side-eye", promptHint: "with a suspicious, wary expression, eyes narrowed in distrust and a sidelong side-eye glance", term: "suspicious, wary expression" },
|
|
107
118
|
{ id: "resigned", label: "Resigned", category: "neutral", description: "Quiet acceptance of an unpleasant situation, sigh", promptHint: "with a resigned expression of quiet acceptance, shoulders softly dropped mid-sigh and a tired half-closed gaze", term: "resigned expression of quiet acceptance" },
|
|
108
119
|
{ id: "conflicted", label: "Conflicted", category: "neutral", description: "Visible internal struggle, brow furrowed, gaze unfocused", promptHint: "with a conflicted expression of visible internal struggle, brow furrowed and gaze unfocused, caught between two impulses", term: "conflicted, torn expression" },
|
|
@@ -112,7 +123,7 @@ const moodById = new Map<string, Mood>(MOODS.map((m) => [m.id, m]))
|
|
|
112
123
|
|
|
113
124
|
export function getMood(id: string | undefined | null): Mood | undefined {
|
|
114
125
|
if (!id) return undefined
|
|
115
|
-
return moodById.get(id)
|
|
126
|
+
return overlayEntry("mood", id, moodById.get(id))
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
export function getMoodLabel(id: string | undefined | null, fallback?: string): string {
|
package/src/music-genre.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { pickIds } from "@nodaro/shared"
|
|
16
16
|
|
|
17
17
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
18
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
18
19
|
|
|
19
20
|
export type MusicGenreCategory =
|
|
20
21
|
| "hip-hop-rnb"
|
|
@@ -610,12 +611,12 @@ const ERA_BY_ID = new Map(MUSIC_ERAS.map((e) => [e.id, e]))
|
|
|
610
611
|
|
|
611
612
|
export function getMusicGenre(id: string | undefined): MusicGenre | undefined {
|
|
612
613
|
if (!id) return undefined
|
|
613
|
-
return GENRE_BY_ID.get(id)
|
|
614
|
+
return overlayEntry("music-genre", id, GENRE_BY_ID.get(id))
|
|
614
615
|
}
|
|
615
616
|
|
|
616
617
|
export function getMusicGenreLabel(id: string | undefined): string {
|
|
617
618
|
if (!id) return ""
|
|
618
|
-
return
|
|
619
|
+
return getMusicGenre(id)?.label ?? id
|
|
619
620
|
}
|
|
620
621
|
|
|
621
622
|
export function getMusicSubgenre(
|
|
@@ -624,12 +625,12 @@ export function getMusicSubgenre(
|
|
|
624
625
|
): MusicSubgenre | undefined {
|
|
625
626
|
if (!genreId || !subgenreId) return undefined
|
|
626
627
|
const genre = GENRE_BY_ID.get(genreId)
|
|
627
|
-
return genre?.subgenres.find((s) => s.id === subgenreId)
|
|
628
|
+
return overlayEntry("music-genre", subgenreId, genre?.subgenres.find((s) => s.id === subgenreId))
|
|
628
629
|
}
|
|
629
630
|
|
|
630
631
|
export function getMusicEra(id: string | undefined): MusicEra | undefined {
|
|
631
632
|
if (!id) return undefined
|
|
632
|
-
return ERA_BY_ID.get(id)
|
|
633
|
+
return overlayEntry("music-genre", id, ERA_BY_ID.get(id))
|
|
633
634
|
}
|
|
634
635
|
|
|
635
636
|
/**
|
package/src/music-mood.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { pickIds } from "@nodaro/shared"
|
|
7
7
|
|
|
8
8
|
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
9
|
+
import { overlayEntry } from "./catalog-overlay.js"
|
|
9
10
|
|
|
10
11
|
export interface MusicMoodEntry {
|
|
11
12
|
readonly id: string
|
|
@@ -102,13 +103,13 @@ const EMOTION_BY_ID = new Map(MUSIC_EMOTIONS.map((x) => [x.id, x]))
|
|
|
102
103
|
const VIBE_BY_ID = new Map(MUSIC_VIBES.map((x) => [x.id, x]))
|
|
103
104
|
|
|
104
105
|
export function getMusicEnergy(id: string | undefined): MusicMoodEntry | undefined {
|
|
105
|
-
return id ? ENERGY_BY_ID.get(id) : undefined
|
|
106
|
+
return id ? overlayEntry("music-mood", id, ENERGY_BY_ID.get(id)) : undefined
|
|
106
107
|
}
|
|
107
108
|
export function getMusicEmotion(id: string | undefined): MusicMoodEntry | undefined {
|
|
108
|
-
return id ? EMOTION_BY_ID.get(id) : undefined
|
|
109
|
+
return id ? overlayEntry("music-mood", id, EMOTION_BY_ID.get(id)) : undefined
|
|
109
110
|
}
|
|
110
111
|
export function getMusicVibe(id: string | undefined): MusicMoodEntry | undefined {
|
|
111
|
-
return id ? VIBE_BY_ID.get(id) : undefined
|
|
112
|
+
return id ? overlayEntry("music-mood", id, VIBE_BY_ID.get(id)) : undefined
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|