@nodaro/prompts 1.7.3 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +5443 -4295
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1202 -61
- package/dist/index.d.ts +1202 -61
- package/dist/index.js +5357 -4297
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
- package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
- package/src/__tests__/catalog-packs.test.ts +130 -0
- package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
- package/src/__tests__/catalog-terms.test.ts +162 -0
- package/src/__tests__/character-default-role.test.ts +3 -2
- package/src/__tests__/content-free-contract.test.ts +45 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
- package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
- package/src/__tests__/parameter-hint-mode.test.ts +385 -0
- package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
- package/src/__tests__/person-packs.test.ts +166 -0
- package/src/__tests__/project-all-catalogs.test.ts +26 -0
- package/src/__tests__/prompt-builder.test.ts +53 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +18 -0
- package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
- package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
- package/src/__tests__/term.test.ts +104 -0
- package/src/__tests__/transitions.test.ts +8 -5
- package/src/__tests__/upstream-immutability.test.ts +23 -0
- package/src/action-fx.ts +67 -17
- package/src/aesthetic.ts +54 -6
- package/src/atmosphere.ts +64 -23
- package/src/backdrop.ts +44 -30
- package/src/camera-format.ts +33 -11
- package/src/camera-motions.ts +89 -0
- package/src/catalog-packs.ts +125 -0
- package/src/catalog-sidecar-coverage.ts +36 -0
- package/src/character-fx.ts +112 -39
- package/src/color-look.ts +44 -27
- package/src/composition-effects.ts +21 -7
- package/src/era.ts +24 -0
- package/src/exposure-settings.ts +76 -18
- package/src/framing.ts +102 -10
- package/src/held-prop.ts +125 -63
- package/src/identity-lock.ts +12 -5
- package/src/image-reference-doctrine.ts +55 -0
- package/src/index.ts +5 -0
- package/src/instrumentation.ts +148 -57
- package/src/lens.ts +31 -15
- package/src/lighting.ts +120 -59
- package/src/loop-subject.ts +27 -1
- package/src/materials.ts +123 -69
- package/src/mood.ts +123 -51
- package/src/music-genre.ts +171 -67
- package/src/music-mood.ts +85 -17
- package/src/parameter-prompt-hint.ts +168 -56
- package/src/person-packs.ts +182 -0
- package/src/person.ts +506 -408
- package/src/photo-genre.ts +37 -22
- package/src/photographer.ts +138 -1
- package/src/picker-catalogs.ts +86 -40
- package/src/pose.ts +105 -40
- package/src/post-process-effects.ts +51 -8
- package/src/prompt-builder.ts +16 -1
- package/src/provider-prompt-doctrine.ts +12 -5
- package/src/render-quality.ts +25 -7
- package/src/setting.ts +30 -14
- package/src/style.ts +33 -15
- package/src/styling.ts +155 -94
- package/src/temporal.ts +80 -18
- package/src/term.ts +155 -0
- package/src/transitions.ts +73 -26
- package/src/voice-character.ts +190 -90
- package/src/voice-delivery.ts +83 -12
package/src/picker-catalogs.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { ANIMALS, ANIMAL_SUBCATEGORY_LABELS, ANIMAL_SUBCATEGORY_ORDER } from "@n
|
|
|
59
59
|
import { VEHICLES, VEHICLE_SUBCATEGORY_LABELS, VEHICLE_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
60
60
|
import { WEAPONS, WEAPON_SUBCATEGORY_LABELS, WEAPON_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
61
61
|
import { FURNITURE, FURNITURE_SUBCATEGORY_LABELS, FURNITURE_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
62
|
+
import type { ProjectedCatalog, ProjectedCatalogOption, ProjectedCatalogDimension } from "@nodaro/shared"
|
|
62
63
|
import { HELD_PROPS, HELD_PROP_CATEGORY_LABELS, HELD_PROP_CATEGORY_ORDER } from "./held-prop.js"
|
|
63
64
|
import { FRAMINGS, FRAMING_FIELD_BY_CATEGORY, FRAMING_CATEGORY_LABELS } from "./framing.js"
|
|
64
65
|
import { LIGHTINGS, LIGHTING_FIELD_BY_CATEGORY, LIGHTING_CATEGORY_ORDER, LIGHTING_CATEGORY_LABELS } from "./lighting.js"
|
|
@@ -71,6 +72,8 @@ import { MUSIC_ENERGIES, MUSIC_EMOTIONS, MUSIC_VIBES } from "./music-mood.js"
|
|
|
71
72
|
import { INSTRUMENTS, PRODUCTION_STYLES, VOCAL_PRESENCE, SINGING_STYLES } from "./instrumentation.js"
|
|
72
73
|
import { VOICE_AGES, VOICE_GENDERS, VOICE_LANGUAGES, VOICE_ACCENTS, VOICE_TIMBRES } from "./voice-character.js"
|
|
73
74
|
import { VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES } from "./voice-delivery.js"
|
|
75
|
+
import { composePickerCatalogs, getRegisteredCatalogPacks, catalogPacksVersion } from "./catalog-packs.js"
|
|
76
|
+
import { deriveTerm, resolveTerm } from "./term.js"
|
|
74
77
|
|
|
75
78
|
export interface PickerOption {
|
|
76
79
|
readonly id: string
|
|
@@ -79,6 +82,15 @@ export interface PickerOption {
|
|
|
79
82
|
/** The group id (matches `categoryOrder` / `categoryLabels`). */
|
|
80
83
|
readonly category?: string
|
|
81
84
|
readonly promptHint: string
|
|
85
|
+
/**
|
|
86
|
+
* The short professional term this id injects in COMPACT hint mode ("whip
|
|
87
|
+
* pan left" where `promptHint` is the full mechanism sentence). Always
|
|
88
|
+
* present and already RESOLVED via `resolveTerm` — an authored `term` when
|
|
89
|
+
* the catalog entry carries one, the derived label otherwise, and `""` for
|
|
90
|
+
* a no-op ("auto"/"none") entry that injects nothing. Consumers render
|
|
91
|
+
* `label` and inject `term`; they never derive it themselves.
|
|
92
|
+
*/
|
|
93
|
+
readonly term: string
|
|
82
94
|
/** Only present if the source catalog entry already carries a data icon/emoji/thumbnail field. */
|
|
83
95
|
readonly icon?: string
|
|
84
96
|
}
|
|
@@ -125,6 +137,8 @@ interface BaseCatalogEntry {
|
|
|
125
137
|
readonly label: string
|
|
126
138
|
readonly description: string
|
|
127
139
|
readonly promptHint: string
|
|
140
|
+
/** Optional authored compact term (see `term.ts` for the convention). */
|
|
141
|
+
readonly term?: string
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
/**
|
|
@@ -145,6 +159,10 @@ function toOptions<T extends BaseCatalogEntry>(
|
|
|
145
159
|
id: e.id,
|
|
146
160
|
label: e.label,
|
|
147
161
|
promptHint: e.promptHint,
|
|
162
|
+
// Resolved here — never at the consumer — so an authored `term` on the
|
|
163
|
+
// catalog entry flows through automatically and a no-op entry resolves
|
|
164
|
+
// to "".
|
|
165
|
+
term: resolveTerm(e),
|
|
148
166
|
}
|
|
149
167
|
if (e.description) opt.description = e.description
|
|
150
168
|
if (categoryField) opt.category = e[categoryField] as unknown as string
|
|
@@ -164,6 +182,8 @@ interface ObjectCatalogEntry {
|
|
|
164
182
|
readonly label: string
|
|
165
183
|
readonly subcategory: string
|
|
166
184
|
readonly description: string
|
|
185
|
+
/** Optional authored compact term (see `term.ts` for the convention). */
|
|
186
|
+
readonly term?: string
|
|
167
187
|
}
|
|
168
188
|
function objectOptions(
|
|
169
189
|
arr: ReadonlyArray<ObjectCatalogEntry>,
|
|
@@ -175,6 +195,17 @@ function objectOptions(
|
|
|
175
195
|
description: e.description,
|
|
176
196
|
category: e.subcategory,
|
|
177
197
|
promptHint: phrase(e.label.toLowerCase(), e.description),
|
|
198
|
+
// Object entities have no `promptHint` field of their own (it is
|
|
199
|
+
// synthesized above), so the term cannot come from `resolveTerm`'s
|
|
200
|
+
// empty-hint rule: an authored `term` wins, and otherwise the label IS the
|
|
201
|
+
// professional term for a concrete object ("golden retriever", "katana").
|
|
202
|
+
// The fallback goes through `deriveTerm` — NOT a bare `toLowerCase()` — so
|
|
203
|
+
// a parenthetical label ("Rifle (bolt-action)") is stripped the same way
|
|
204
|
+
// every other catalog strips it. That also keeps the guard test honest:
|
|
205
|
+
// it treats `term !== deriveTerm(label)` as proof a human authored the
|
|
206
|
+
// term, so a fallback that diverges from `deriveTerm` would make a mangled
|
|
207
|
+
// value look authored and ship it.
|
|
208
|
+
term: e.term ?? deriveTerm(e.label),
|
|
178
209
|
}))
|
|
179
210
|
}
|
|
180
211
|
|
|
@@ -279,11 +310,11 @@ function perFieldDims(
|
|
|
279
310
|
*/
|
|
280
311
|
function musicGenreDims(fields: ReadonlyArray<string>): ReadonlyArray<PickerDimension> {
|
|
281
312
|
const optionsByField: Record<string, ReadonlyArray<PickerOption>> = {
|
|
282
|
-
genre: MUSIC_GENRES.map((g) => ({ id: g.id, label: g.label, description: g.description, category: g.category, promptHint: g.promptHint })),
|
|
313
|
+
genre: MUSIC_GENRES.map((g) => ({ id: g.id, label: g.label, description: g.description, category: g.category, promptHint: g.promptHint, term: resolveTerm(g) })),
|
|
283
314
|
subgenre: MUSIC_GENRES.flatMap((g) =>
|
|
284
|
-
g.subgenres.map((s) => ({ id: s.id, label: s.label, promptHint: s.promptHint })),
|
|
315
|
+
g.subgenres.map((s) => ({ id: s.id, label: s.label, promptHint: s.promptHint, term: resolveTerm(s) })),
|
|
285
316
|
),
|
|
286
|
-
era: MUSIC_ERAS.map((e) => ({ id: e.id, label: e.label, description: e.description, promptHint: e.promptHint })),
|
|
317
|
+
era: MUSIC_ERAS.map((e) => ({ id: e.id, label: e.label, description: e.description, promptHint: e.promptHint, term: resolveTerm(e) })),
|
|
287
318
|
}
|
|
288
319
|
return fields.map((field) => {
|
|
289
320
|
const options = optionsByField[field]
|
|
@@ -719,21 +750,46 @@ const MULTI_CATALOGS: readonly PickerCatalog[] = [
|
|
|
719
750
|
},
|
|
720
751
|
]
|
|
721
752
|
|
|
722
|
-
|
|
753
|
+
/**
|
|
754
|
+
* The frozen upstream base — the pure-data catalogs as authored, never mutated
|
|
755
|
+
* in place. Deployment curation is additive-by-registration (catalog packs);
|
|
756
|
+
* `getRegisteredPickerCatalogs()` is the pack-composed view every enumerating
|
|
757
|
+
* consumer reads.
|
|
758
|
+
*/
|
|
759
|
+
export const PICKER_CATALOGS: readonly PickerCatalog[] = Object.freeze([
|
|
723
760
|
...SINGLE_CATALOGS,
|
|
724
761
|
...MULTI_CATALOGS,
|
|
725
|
-
]
|
|
762
|
+
])
|
|
763
|
+
|
|
764
|
+
let _registeredMemo: { v: number; catalogs: readonly PickerCatalog[] } | null = null
|
|
765
|
+
/**
|
|
766
|
+
* The pack-composed picker catalogs — the single funnel every enumerating
|
|
767
|
+
* consumer (funnel getters, /v1/catalogs, MCP, completeness tests) reads.
|
|
768
|
+
* Memoized on the pack registry version so late registration is reflected.
|
|
769
|
+
*
|
|
770
|
+
* DEFERRED PLUG-IN POINT (do not build in Phase 0): a future `CatalogPolicy`
|
|
771
|
+
* filter (deny-by-tag / override, per read kind) applies HERE, after pack
|
|
772
|
+
* composition, e.g. `applyCatalogPolicy(composed, runtimePolicy())`.
|
|
773
|
+
*/
|
|
774
|
+
export function getRegisteredPickerCatalogs(): readonly PickerCatalog[] {
|
|
775
|
+
const v = catalogPacksVersion()
|
|
776
|
+
if (_registeredMemo && _registeredMemo.v === v) return _registeredMemo.catalogs
|
|
777
|
+
const catalogs = composePickerCatalogs(PICKER_CATALOGS, getRegisteredCatalogPacks())
|
|
778
|
+
_registeredMemo = { v, catalogs }
|
|
779
|
+
return catalogs
|
|
780
|
+
}
|
|
726
781
|
|
|
727
782
|
/** Resolve a catalog by `nodeType` first, then by `catalogId`. */
|
|
728
783
|
export function getPickerCatalog(nodeTypeOrCatalogId: string): PickerCatalog | undefined {
|
|
784
|
+
const all = getRegisteredPickerCatalogs()
|
|
729
785
|
return (
|
|
730
|
-
|
|
731
|
-
|
|
786
|
+
all.find((c) => c.nodeType === nodeTypeOrCatalogId) ??
|
|
787
|
+
all.find((c) => c.catalogId === nodeTypeOrCatalogId)
|
|
732
788
|
)
|
|
733
789
|
}
|
|
734
790
|
|
|
735
791
|
export function listPickerCatalogs(): readonly PickerCatalog[] {
|
|
736
|
-
return
|
|
792
|
+
return getRegisteredPickerCatalogs()
|
|
737
793
|
}
|
|
738
794
|
|
|
739
795
|
// ─── Summary + projection (for MCP / REST exposure) ──────────────────────────
|
|
@@ -753,7 +809,7 @@ export interface PickerCatalogSummary {
|
|
|
753
809
|
|
|
754
810
|
/** Lightweight directory of every picker catalog — no option payloads. */
|
|
755
811
|
export function summarizePickerCatalogs(): readonly PickerCatalogSummary[] {
|
|
756
|
-
return
|
|
812
|
+
return getRegisteredPickerCatalogs().map((c) => ({
|
|
757
813
|
nodeType: c.nodeType,
|
|
758
814
|
label: c.label,
|
|
759
815
|
catalogId: c.catalogId,
|
|
@@ -778,38 +834,18 @@ export interface ProjectPickerCatalogOptions {
|
|
|
778
834
|
readonly field?: string
|
|
779
835
|
}
|
|
780
836
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
readonly icon?: string
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
export interface ProjectedPickerDimension {
|
|
792
|
-
readonly field: string
|
|
793
|
-
readonly label: string
|
|
794
|
-
readonly options: readonly ProjectedPickerOption[]
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export interface ProjectedPickerCatalog {
|
|
798
|
-
readonly nodeType: string
|
|
799
|
-
readonly label: string
|
|
800
|
-
readonly catalogId: string
|
|
801
|
-
readonly kind: "single" | "multi"
|
|
802
|
-
readonly valueField?: string
|
|
803
|
-
readonly defaultValue?: string
|
|
804
|
-
readonly categoryOrder?: readonly string[]
|
|
805
|
-
readonly categoryLabels?: Readonly<Record<string, string>>
|
|
806
|
-
readonly options?: readonly ProjectedPickerOption[]
|
|
807
|
-
readonly fields?: readonly string[]
|
|
808
|
-
readonly dimensions?: readonly ProjectedPickerDimension[]
|
|
809
|
-
readonly detail: PickerCatalogDetail
|
|
810
|
-
}
|
|
837
|
+
// The projection shape now lives in `@nodaro/shared` (Apache) as the tag-free
|
|
838
|
+
// `/v1/catalogs` wire contract — the ONLY catalog type that crosses that
|
|
839
|
+
// boundary. These prompts-side names are kept as aliases so existing importers
|
|
840
|
+
// (backend route, MCP, SDK mirror) are unaffected.
|
|
841
|
+
export type ProjectedPickerOption = ProjectedCatalogOption
|
|
842
|
+
export type ProjectedPickerDimension = ProjectedCatalogDimension
|
|
843
|
+
export type ProjectedPickerCatalog = ProjectedCatalog
|
|
811
844
|
|
|
812
845
|
function projectOption(o: PickerOption, detail: PickerCatalogDetail): ProjectedPickerOption {
|
|
846
|
+
// `term` rides at BOTH detail levels: a thin client renders `label` and
|
|
847
|
+
// injects `term`, so compact must carry it — it is what makes compact hint
|
|
848
|
+
// mode possible without a second round-trip for the full payload.
|
|
813
849
|
return detail === "full"
|
|
814
850
|
? {
|
|
815
851
|
id: o.id,
|
|
@@ -817,9 +853,10 @@ function projectOption(o: PickerOption, detail: PickerCatalogDetail): ProjectedP
|
|
|
817
853
|
description: o.description,
|
|
818
854
|
category: o.category,
|
|
819
855
|
promptHint: o.promptHint,
|
|
856
|
+
term: o.term,
|
|
820
857
|
icon: o.icon,
|
|
821
858
|
}
|
|
822
|
-
: { id: o.id, label: o.label, category: o.category, icon: o.icon }
|
|
859
|
+
: { id: o.id, label: o.label, category: o.category, term: o.term, icon: o.icon }
|
|
823
860
|
}
|
|
824
861
|
|
|
825
862
|
/** Project a catalog to the wire shape: compact by default, optional category/field filter. */
|
|
@@ -856,3 +893,12 @@ export function projectPickerCatalog(
|
|
|
856
893
|
})),
|
|
857
894
|
}
|
|
858
895
|
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Project every REGISTERED (pack-composed) catalog to the tag-free
|
|
899
|
+
* `/v1/catalogs` wire shape. This is what the `GET /v1/catalogs` route and its
|
|
900
|
+
* SDK resource serve, so a deployment's vendored packs are reflected verbatim.
|
|
901
|
+
*/
|
|
902
|
+
export function projectAllCatalogs(opts: { detail?: PickerCatalogDetail } = {}): ProjectedCatalog[] {
|
|
903
|
+
return getRegisteredPickerCatalogs().map((c) => projectPickerCatalog(c, { detail: opts.detail }))
|
|
904
|
+
}
|
package/src/pose.ts
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* backend orchestrator.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
24
|
+
|
|
23
25
|
export type PoseCategory =
|
|
24
26
|
| "standing"
|
|
25
27
|
| "seated"
|
|
@@ -37,6 +39,15 @@ export interface Pose {
|
|
|
37
39
|
readonly category: PoseCategory
|
|
38
40
|
readonly description: string
|
|
39
41
|
readonly promptHint: string
|
|
42
|
+
/**
|
|
43
|
+
* Compact professional term injected instead of `promptHint` in compact hint
|
|
44
|
+
* mode. Authored only where the lowercased label is not what a photographer
|
|
45
|
+
* would write for the pose — bare sub-dimension modifiers ("Tilted Side"),
|
|
46
|
+
* labels that collide with a different meaning in a generation prompt
|
|
47
|
+
* ("Painting"), and telegraphic UI phrasings ("Head Over Shoulder").
|
|
48
|
+
* Everywhere else the derived label already IS the term.
|
|
49
|
+
*/
|
|
50
|
+
readonly term?: string
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export const POSES: ReadonlyArray<Pose> = [
|
|
@@ -45,23 +56,23 @@ export const POSES: ReadonlyArray<Pose> = [
|
|
|
45
56
|
{ id: "confident-stance", label: "Confident Stance", category: "standing", description: "Feet apart, shoulders back", promptHint: "standing in a confident stance with feet apart, shoulders back, and head up" },
|
|
46
57
|
{ id: "hands-on-hips", label: "Hands on Hips", category: "standing", description: "Hands on hips", promptHint: "standing with hands on hips" },
|
|
47
58
|
{ id: "arms-crossed", label: "Arms Crossed", category: "standing", description: "Arms folded across chest", promptHint: "standing with arms crossed over the chest" },
|
|
48
|
-
{ id: "leaning", label: "Leaning", category: "standing", description: "Leaning against something", promptHint: "leaning casually against a wall or surface" },
|
|
59
|
+
{ id: "leaning", label: "Leaning", category: "standing", description: "Leaning against something", promptHint: "leaning casually against a wall or surface", term: "leaning casually against a surface" },
|
|
49
60
|
{ id: "hero-pose", label: "Hero Pose", category: "standing", description: "Dramatic heroic stance", promptHint: "standing in a dramatic heroic pose, chest out, looking forward with purpose" },
|
|
50
61
|
{ id: "contrapposto", label: "Contrapposto", category: "standing", description: "Hip tilted, weight on one leg", promptHint: "standing in contrapposto, weight shifted to one leg with the opposite hip tilted out" },
|
|
51
|
-
{ id: "leaning-against-wall", label: "Leaning Against Wall", category: "standing", description: "Casually leaning on a wall", promptHint: "leaning casually against a wall, shoulder or back resting on the surface" },
|
|
52
|
-
{ id: "hands-behind-head", label: "Hands Behind Head", category: "standing", description: "Both hands clasped behind head", promptHint: "standing with both hands clasped behind the head, elbows out" },
|
|
53
|
-
{ id: "hands-behind-back", label: "Hands Behind Back", category: "standing", description: "Hands clasped behind back", promptHint: "standing with hands clasped behind the back, posture upright" },
|
|
62
|
+
{ id: "leaning-against-wall", label: "Leaning Against Wall", category: "standing", description: "Casually leaning on a wall", promptHint: "leaning casually against a wall, shoulder or back resting on the surface", term: "leaning against a wall" },
|
|
63
|
+
{ id: "hands-behind-head", label: "Hands Behind Head", category: "standing", description: "Both hands clasped behind head", promptHint: "standing with both hands clasped behind the head, elbows out", term: "hands clasped behind the head" },
|
|
64
|
+
{ id: "hands-behind-back", label: "Hands Behind Back", category: "standing", description: "Hands clasped behind back", promptHint: "standing with hands clasped behind the back, posture upright", term: "hands clasped behind the back" },
|
|
54
65
|
|
|
55
66
|
// -------------------- Seated --------------------
|
|
56
67
|
{ id: "sitting", label: "Sitting", category: "seated", description: "Sitting naturally", promptHint: "sitting naturally" },
|
|
57
|
-
{ id: "cross-legged", label: "Cross-legged", category: "seated", description: "Seated cross-legged on floor", promptHint: "sitting cross-legged on the ground" },
|
|
68
|
+
{ id: "cross-legged", label: "Cross-legged", category: "seated", description: "Seated cross-legged on floor", promptHint: "sitting cross-legged on the ground", term: "sitting cross-legged" },
|
|
58
69
|
{ id: "kneeling", label: "Kneeling", category: "seated", description: "Kneeling on the ground", promptHint: "kneeling on one or both knees" },
|
|
59
70
|
{ id: "crouching", label: "Crouching", category: "seated", description: "Crouched low", promptHint: "crouched low with knees bent" },
|
|
60
71
|
{ id: "lounging", label: "Lounging", category: "seated", description: "Reclined, relaxed sitting", promptHint: "lounging in a relaxed, reclined position" },
|
|
61
|
-
{ id: "sitting-edge-of-bed", label: "Sitting on Edge of Bed", category: "seated", description: "Perched on the edge of a bed", promptHint: "perched on the edge of a bed, hands resting on the mattress" },
|
|
62
|
-
{ id: "chair-arm-drape", label: "Legs Draped Over Chair", category: "seated", description: "Legs draped over chair arm", promptHint: "sitting sideways in a chair with legs draped casually over one arm" },
|
|
63
|
-
{ id: "elbow-propped", label: "Cheek on Propped Elbow", category: "seated", description: "Cheek resting on a propped elbow", promptHint: "seated with cheek resting against a propped-up elbow, contemplative" },
|
|
64
|
-
{ id: "lying-on-stomach-reading", label: "Lying Prone Reading", category: "seated", description: "Lying prone, propped on elbows reading", promptHint: "lying on the stomach, propped up on elbows while reading" },
|
|
72
|
+
{ id: "sitting-edge-of-bed", label: "Sitting on Edge of Bed", category: "seated", description: "Perched on the edge of a bed", promptHint: "perched on the edge of a bed, hands resting on the mattress", term: "sitting on the edge of a bed" },
|
|
73
|
+
{ id: "chair-arm-drape", label: "Legs Draped Over Chair", category: "seated", description: "Legs draped over chair arm", promptHint: "sitting sideways in a chair with legs draped casually over one arm", term: "legs draped over the chair arm" },
|
|
74
|
+
{ id: "elbow-propped", label: "Cheek on Propped Elbow", category: "seated", description: "Cheek resting on a propped elbow", promptHint: "seated with cheek resting against a propped-up elbow, contemplative", term: "cheek resting on a propped elbow" },
|
|
75
|
+
{ id: "lying-on-stomach-reading", label: "Lying Prone Reading", category: "seated", description: "Lying prone, propped on elbows reading", promptHint: "lying on the stomach, propped up on elbows while reading", term: "lying prone propped on elbows, reading" },
|
|
65
76
|
|
|
66
77
|
// -------------------- Movement --------------------
|
|
67
78
|
{ id: "walking", label: "Walking", category: "movement", description: "Mid-stride walking", promptHint: "walking, caught mid-stride" },
|
|
@@ -74,20 +85,20 @@ export const POSES: ReadonlyArray<Pose> = [
|
|
|
74
85
|
{ id: "stretching", label: "Stretching", category: "movement", description: "Full-body stretch, arms overhead", promptHint: "in a full-body stretch with both arms reaching overhead" },
|
|
75
86
|
{ id: "reaching-up", label: "Reaching Up", category: "movement", description: "Arms extended overhead", promptHint: "one or both arms extended overhead, reaching upward" },
|
|
76
87
|
{ id: "kissing", label: "Kissing", category: "movement", description: "Locked in a kiss", promptHint: "locked in a kiss, faces close and eyes closed" },
|
|
77
|
-
{ id: "riding", label: "Riding", category: "movement", description: "Riding a bike, horse, or motorcycle", promptHint: "riding astride a bike, horse, or motorcycle, hands gripping forward" },
|
|
78
|
-
{ id: "driving", label: "Driving", category: "movement", description: "Behind the wheel of a vehicle", promptHint: "behind the wheel of a vehicle, hands on the steering wheel, gaze forward" },
|
|
88
|
+
{ id: "riding", label: "Riding", category: "movement", description: "Riding a bike, horse, or motorcycle", promptHint: "riding astride a bike, horse, or motorcycle, hands gripping forward", term: "riding astride" },
|
|
89
|
+
{ id: "driving", label: "Driving", category: "movement", description: "Behind the wheel of a vehicle", promptHint: "behind the wheel of a vehicle, hands on the steering wheel, gaze forward", term: "driving, hands on the wheel" },
|
|
79
90
|
|
|
80
91
|
// -------------------- Action --------------------
|
|
81
92
|
{ id: "fighting-stance", label: "Fighting Stance", category: "action", description: "Combat-ready stance", promptHint: "in a combat-ready fighting stance, tensed and focused" },
|
|
82
|
-
{ id: "reaching", label: "Reaching", category: "action", description: "Reaching outward", promptHint: "reaching outward with one arm extended" },
|
|
83
|
-
{ id: "throwing", label: "Throwing", category: "action", description: "Mid-throw motion", promptHint: "caught mid-throw, body coiled and releasing" },
|
|
93
|
+
{ id: "reaching", label: "Reaching", category: "action", description: "Reaching outward", promptHint: "reaching outward with one arm extended", term: "reaching outward, arm extended" },
|
|
94
|
+
{ id: "throwing", label: "Throwing", category: "action", description: "Mid-throw motion", promptHint: "caught mid-throw, body coiled and releasing", term: "caught mid-throw" },
|
|
84
95
|
{ id: "leaping", label: "Leaping", category: "action", description: "Leaping forward dynamically", promptHint: "leaping forward dynamically with body extended" },
|
|
85
|
-
{ id: "dramatic-action", label: "Dramatic Action", category: "action", description: "Exaggerated action pose", promptHint: "in a dramatic, exaggerated action pose full of motion" },
|
|
86
|
-
{ id: "biting-lip", label: "Biting Lip", category: "action", description: "Slight playful lip-bite", promptHint: "biting the lower lip with a subtle playful expression" },
|
|
96
|
+
{ id: "dramatic-action", label: "Dramatic Action", category: "action", description: "Exaggerated action pose", promptHint: "in a dramatic, exaggerated action pose full of motion", term: "dramatic exaggerated action pose" },
|
|
97
|
+
{ id: "biting-lip", label: "Biting Lip", category: "action", description: "Slight playful lip-bite", promptHint: "biting the lower lip with a subtle playful expression", term: "biting the lower lip" },
|
|
87
98
|
{ id: "mid-laugh", label: "Mid-Laugh", category: "action", description: "Caught mid-laugh, head back", promptHint: "caught mid-laugh with head tipped back, eyes crinkled" },
|
|
88
|
-
{ id: "pointing-at-camera", label: "Pointing at Camera", category: "action", description: "Pointing directly at camera", promptHint: "pointing one finger directly at the camera, arm extended" },
|
|
99
|
+
{ id: "pointing-at-camera", label: "Pointing at Camera", category: "action", description: "Pointing directly at camera", promptHint: "pointing one finger directly at the camera, arm extended", term: "pointing directly at the camera" },
|
|
89
100
|
{ id: "tongue-out", label: "Sticking Tongue Out", category: "action", description: "Playful tongue-out expression", promptHint: "sticking the tongue out playfully" },
|
|
90
|
-
{ id: "thinking", label: "Thinking", category: "action", description: "Hand on chin, contemplative", promptHint: "in a thinking pose with hand on chin, gaze contemplative" },
|
|
101
|
+
{ id: "thinking", label: "Thinking", category: "action", description: "Hand on chin, contemplative", promptHint: "in a thinking pose with hand on chin, gaze contemplative", term: "thinking pose, hand on chin" },
|
|
91
102
|
|
|
92
103
|
// -------------------- Resting --------------------
|
|
93
104
|
{ id: "lying-down", label: "Lying Down", category: "resting", description: "Lying flat", promptHint: "lying down flat, relaxed" },
|
|
@@ -96,48 +107,48 @@ export const POSES: ReadonlyArray<Pose> = [
|
|
|
96
107
|
{ id: "looking-away", label: "Looking Away", category: "resting", description: "Head turned, looking away", promptHint: "head turned, looking off away from the camera" },
|
|
97
108
|
{ id: "looking-up", label: "Looking Up", category: "resting", description: "Gazing up at the sky", promptHint: "gazing upward at the sky or off-camera, chin slightly raised" },
|
|
98
109
|
{ id: "looking-down", label: "Looking Down", category: "resting", description: "Eyes downcast", promptHint: "eyes downcast, gaze turned softly to the ground" },
|
|
99
|
-
{ id: "head-over-shoulder", label: "Head Over Shoulder", category: "resting", description: "Looking back over shoulder", promptHint: "head turned to look back over one shoulder toward the camera" },
|
|
110
|
+
{ id: "head-over-shoulder", label: "Head Over Shoulder", category: "resting", description: "Looking back over shoulder", promptHint: "head turned to look back over one shoulder toward the camera", term: "looking back over the shoulder" },
|
|
100
111
|
{ id: "wading-in-water", label: "Wading in Water", category: "resting", description: "Wading mid-thigh deep in water", promptHint: "wading mid-thigh deep through water, ripples around the legs" },
|
|
101
112
|
|
|
102
113
|
// -------------------- Hand Position --------------------
|
|
103
114
|
{ id: "hands-in-pockets", label: "Hands in Pockets", category: "hand-position", description: "Both hands tucked in pockets", promptHint: "with both hands tucked into pockets" },
|
|
104
115
|
{ id: "hand-on-hip", label: "Hand on Hip", category: "hand-position", description: "One hand on hip", promptHint: "with one hand resting on the hip" },
|
|
105
|
-
{ id: "hand-position-hands-on-hips", label: "Hands on Hips", category: "hand-position", description: "Both hands on hips", promptHint: "with both hands planted on the hips" },
|
|
116
|
+
{ id: "hand-position-hands-on-hips", label: "Hands on Hips", category: "hand-position", description: "Both hands on hips", promptHint: "with both hands planted on the hips", term: "both hands planted on the hips" },
|
|
106
117
|
{ id: "hand-on-chin", label: "Hand on Chin", category: "hand-position", description: "Hand resting under the chin", promptHint: "with one hand resting under the chin, fingers framing the jaw" },
|
|
107
118
|
{ id: "hand-on-collarbone", label: "Hand on Collarbone", category: "hand-position", description: "Hand resting across collarbone", promptHint: "with one hand resting lightly across the collarbone" },
|
|
108
|
-
{ id: "hand-brushing-hair", label: "Hand Brushing Hair", category: "hand-position", description: "Hand running through the hair", promptHint: "with a hand running through the hair, fingers caught mid-strand" },
|
|
109
|
-
{ id: "finger-to-lip", label: "Finger to Lip", category: "hand-position", description: "Fingertip pressed against lower lip", promptHint: "with one fingertip pressed gently against the lower lip" },
|
|
110
|
-
{ id: "arms-wrapped-around-self", label: "Arms Wrapped Around Self", category: "hand-position", description: "Self-hug, arms around torso", promptHint: "arms wrapped around the torso in a self-hug" },
|
|
111
|
-
{ id: "hands-clasped", label: "Hands Clasped", category: "hand-position", description: "Both hands clasped at the front", promptHint: "with both hands clasped at the front" },
|
|
119
|
+
{ id: "hand-brushing-hair", label: "Hand Brushing Hair", category: "hand-position", description: "Hand running through the hair", promptHint: "with a hand running through the hair, fingers caught mid-strand", term: "hand running through the hair" },
|
|
120
|
+
{ id: "finger-to-lip", label: "Finger to Lip", category: "hand-position", description: "Fingertip pressed against lower lip", promptHint: "with one fingertip pressed gently against the lower lip", term: "fingertip pressed to the lower lip" },
|
|
121
|
+
{ id: "arms-wrapped-around-self", label: "Arms Wrapped Around Self", category: "hand-position", description: "Self-hug, arms around torso", promptHint: "arms wrapped around the torso in a self-hug", term: "arms wrapped around the torso" },
|
|
122
|
+
{ id: "hands-clasped", label: "Hands Clasped", category: "hand-position", description: "Both hands clasped at the front", promptHint: "with both hands clasped at the front", term: "hands clasped in front" },
|
|
112
123
|
|
|
113
124
|
// -------------------- Body Lean --------------------
|
|
114
|
-
{ id: "leaning-back", label: "Leaning Back", category: "body-lean", description: "Torso leaning back slightly", promptHint: "with the torso leaning back at a slight backward angle" },
|
|
115
|
-
{ id: "leaning-forward", label: "Leaning Forward", category: "body-lean", description: "Torso leaning toward camera", promptHint: "with the torso leaning forward toward the camera" },
|
|
116
|
-
{ id: "body-lean-contrapposto", label: "Contrapposto", category: "body-lean", description: "Weight on one leg, hip pushed out", promptHint: "with the weight on one leg and one hip pushed out, classical contrapposto" },
|
|
125
|
+
{ id: "leaning-back", label: "Leaning Back", category: "body-lean", description: "Torso leaning back slightly", promptHint: "with the torso leaning back at a slight backward angle", term: "torso leaning slightly back" },
|
|
126
|
+
{ id: "leaning-forward", label: "Leaning Forward", category: "body-lean", description: "Torso leaning toward camera", promptHint: "with the torso leaning forward toward the camera", term: "torso leaning forward" },
|
|
127
|
+
{ id: "body-lean-contrapposto", label: "Contrapposto", category: "body-lean", description: "Weight on one leg, hip pushed out", promptHint: "with the weight on one leg and one hip pushed out, classical contrapposto", term: "contrapposto, hip pushed out" },
|
|
117
128
|
{ id: "arched-back", label: "Arched Back", category: "body-lean", description: "Back gently arched, chest forward", promptHint: "with the back gently arched, chest forward" },
|
|
118
|
-
{ id: "shoulder-rolled-forward", label: "Shoulder Rolled Forward", category: "body-lean", description: "One shoulder rolled forward", promptHint: "with one shoulder rolled forward, asymmetric stance" },
|
|
129
|
+
{ id: "shoulder-rolled-forward", label: "Shoulder Rolled Forward", category: "body-lean", description: "One shoulder rolled forward", promptHint: "with one shoulder rolled forward, asymmetric stance", term: "one shoulder rolled forward" },
|
|
119
130
|
|
|
120
131
|
// -------------------- Head Tilt --------------------
|
|
121
|
-
{ id: "tilted-up", label: "Tilted Up", category: "head-tilt", description: "Head tipped slightly upward", promptHint: "with the head tipped slightly upward" },
|
|
122
|
-
{ id: "tilted-down", label: "Tilted Down", category: "head-tilt", description: "Head tipped slightly downward", promptHint: "with the head tipped slightly downward" },
|
|
123
|
-
{ id: "tilted-side", label: "Tilted Side", category: "head-tilt", description: "Head tilted toward shoulder", promptHint: "with the head tilted toward one shoulder" },
|
|
124
|
-
{ id: "tilted-back", label: "Tilted Back", category: "head-tilt", description: "Head fully back, throat exposed", promptHint: "with the head tipped fully back, exposing the throat" },
|
|
125
|
-
{ id: "chin-up", label: "Chin Up", category: "head-tilt", description: "Chin lifted, looking down nose", promptHint: "with the chin lifted, looking down the nose" },
|
|
132
|
+
{ id: "tilted-up", label: "Tilted Up", category: "head-tilt", description: "Head tipped slightly upward", promptHint: "with the head tipped slightly upward", term: "head tipped slightly up" },
|
|
133
|
+
{ id: "tilted-down", label: "Tilted Down", category: "head-tilt", description: "Head tipped slightly downward", promptHint: "with the head tipped slightly downward", term: "head tipped slightly down" },
|
|
134
|
+
{ id: "tilted-side", label: "Tilted Side", category: "head-tilt", description: "Head tilted toward shoulder", promptHint: "with the head tilted toward one shoulder", term: "head tilted toward one shoulder" },
|
|
135
|
+
{ id: "tilted-back", label: "Tilted Back", category: "head-tilt", description: "Head fully back, throat exposed", promptHint: "with the head tipped fully back, exposing the throat", term: "head tipped fully back" },
|
|
136
|
+
{ id: "chin-up", label: "Chin Up", category: "head-tilt", description: "Chin lifted, looking down nose", promptHint: "with the chin lifted, looking down the nose", term: "chin lifted" },
|
|
126
137
|
{ id: "chin-tucked", label: "Chin Tucked", category: "head-tilt", description: "Chin tucked toward chest", promptHint: "with the chin tucked toward the chest" },
|
|
127
138
|
|
|
128
139
|
// -------------------- Activity (what the subject is doing in the world) --------------------
|
|
129
|
-
{ id: "activity-smoking", label: "Smoking", category: "activity", description: "Holding and smoking a cigarette", promptHint: "holding and smoking a cigarette, smoke curling up from the lit tip" },
|
|
130
|
-
{ id: "activity-drinking", label: "Drinking", category: "activity", description: "Drinking from a glass or cup", promptHint: "raising a glass or cup to the lips and drinking from it" },
|
|
140
|
+
{ id: "activity-smoking", label: "Smoking", category: "activity", description: "Holding and smoking a cigarette", promptHint: "holding and smoking a cigarette, smoke curling up from the lit tip", term: "smoking a cigarette" },
|
|
141
|
+
{ id: "activity-drinking", label: "Drinking", category: "activity", description: "Drinking from a glass or cup", promptHint: "raising a glass or cup to the lips and drinking from it", term: "drinking from a glass" },
|
|
131
142
|
{ id: "activity-eating", label: "Eating", category: "activity", description: "Caught mid-bite", promptHint: "caught mid-bite, eating with a piece of food halfway to the mouth" },
|
|
132
|
-
{ id: "activity-talking-on-phone", label: "Talking on Phone", category: "activity", description: "Phone held to ear, speaking", promptHint: "holding a phone to one ear and speaking into it, eyes focused mid-conversation" },
|
|
143
|
+
{ id: "activity-talking-on-phone", label: "Talking on Phone", category: "activity", description: "Phone held to ear, speaking", promptHint: "holding a phone to one ear and speaking into it, eyes focused mid-conversation", term: "talking on the phone" },
|
|
133
144
|
{ id: "activity-texting", label: "Texting", category: "activity", description: "Looking down at phone, thumbs typing", promptHint: "looking down at a phone held in both hands, thumbs busy typing a message" },
|
|
134
|
-
{ id: "activity-typing-laptop", label: "Typing on Laptop", category: "activity", description: "Hands on keyboard, focused on screen", promptHint: "typing at a laptop with hands on the keyboard, focused on the glowing screen" },
|
|
145
|
+
{ id: "activity-typing-laptop", label: "Typing on Laptop", category: "activity", description: "Hands on keyboard, focused on screen", promptHint: "typing at a laptop with hands on the keyboard, focused on the glowing screen", term: "typing on a laptop" },
|
|
135
146
|
{ id: "activity-reading", label: "Reading", category: "activity", description: "Holding open a book or magazine", promptHint: "holding open a book or magazine in both hands, eyes following the page" },
|
|
136
|
-
{ id: "activity-writing", label: "Writing", category: "activity", description: "Writing in a notebook with a pen", promptHint: "writing in a notebook, pen in hand, head bent slightly over the page" },
|
|
137
|
-
{ id: "activity-painting", label: "Painting", category: "activity", description: "Painting on a canvas with a brush", promptHint: "painting on a canvas, brush in hand mid-stroke, palette held in the other" },
|
|
138
|
-
{ id: "activity-playing-instrument", label: "Playing Instrument", category: "activity", description: "Playing a musical instrument", promptHint: "playing a musical instrument, hands and fingers engaged with strings or keys, fully absorbed in the performance" },
|
|
147
|
+
{ id: "activity-writing", label: "Writing", category: "activity", description: "Writing in a notebook with a pen", promptHint: "writing in a notebook, pen in hand, head bent slightly over the page", term: "writing in a notebook" },
|
|
148
|
+
{ id: "activity-painting", label: "Painting", category: "activity", description: "Painting on a canvas with a brush", promptHint: "painting on a canvas, brush in hand mid-stroke, palette held in the other", term: "painting on a canvas" },
|
|
149
|
+
{ id: "activity-playing-instrument", label: "Playing Instrument", category: "activity", description: "Playing a musical instrument", promptHint: "playing a musical instrument, hands and fingers engaged with strings or keys, fully absorbed in the performance", term: "playing a musical instrument" },
|
|
139
150
|
{ id: "activity-cooking", label: "Cooking", category: "activity", description: "Cooking at a kitchen counter or stove", promptHint: "cooking at a kitchen counter or stove, hands actively working with food, steam rising from a pan" },
|
|
140
|
-
{ id: "activity-driving", label: "Driving", category: "activity", description: "Behind the wheel, hands gripping", promptHint: "behind the wheel of a vehicle, both hands on the steering wheel, gaze fixed on the road ahead" },
|
|
151
|
+
{ id: "activity-driving", label: "Driving", category: "activity", description: "Behind the wheel, hands gripping", promptHint: "behind the wheel of a vehicle, both hands on the steering wheel, gaze fixed on the road ahead", term: "behind the wheel of a vehicle" },
|
|
141
152
|
] as const
|
|
142
153
|
|
|
143
154
|
const poseById = new Map<string, Pose>(POSES.map((p) => [p.id, p]))
|
|
@@ -158,6 +169,16 @@ export function getPosePromptHint(id: string | undefined | null): string {
|
|
|
158
169
|
return getPose(id)?.promptHint ?? ""
|
|
159
170
|
}
|
|
160
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Compact counterpart of `getPosePromptHint`: the short professional term this
|
|
174
|
+
* pose injects in compact hint mode ("mid-throw" where the hint is the full
|
|
175
|
+
* body-mechanics sentence). Empty string for an unknown id and for any entry
|
|
176
|
+
* that injects no hint at all.
|
|
177
|
+
*/
|
|
178
|
+
export function getPoseTerm(id: string | undefined | null): string {
|
|
179
|
+
return resolveTerm(getPose(id))
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
export const POSE_IDS: ReadonlyArray<string> = POSES.map((p) => p.id)
|
|
162
183
|
|
|
163
184
|
export const POSE_CATEGORY_LABELS: Readonly<Record<PoseCategory, string>> = {
|
|
@@ -212,7 +233,9 @@ export interface PoseValue {
|
|
|
212
233
|
*/
|
|
213
234
|
export function buildPoseHints(
|
|
214
235
|
data: Record<string, unknown> & PoseValue,
|
|
236
|
+
mode: PickerHintMode = "full",
|
|
215
237
|
): string[] {
|
|
238
|
+
if (mode === "compact") return buildPoseTerms(data)
|
|
216
239
|
const hints: string[] = []
|
|
217
240
|
|
|
218
241
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
@@ -244,3 +267,45 @@ export function buildPoseHints(
|
|
|
244
267
|
|
|
245
268
|
return hints
|
|
246
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Compact counterpart of `buildPoseHints`: the same pre-text → pose →
|
|
273
|
+
* hand-position → body-lean → head-tilt → activity → post-text order, emitted
|
|
274
|
+
* as short professional terms instead of full mechanism sentences
|
|
275
|
+
* ("mid-throw, hands clasped behind the head"). Free-text pre/post fields are
|
|
276
|
+
* user prose and pass through unchanged in both modes.
|
|
277
|
+
*/
|
|
278
|
+
export function buildPoseTerms(
|
|
279
|
+
data: Record<string, unknown> & PoseValue,
|
|
280
|
+
): string[] {
|
|
281
|
+
const terms: string[] = []
|
|
282
|
+
|
|
283
|
+
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
284
|
+
if (pre) terms.push(pre)
|
|
285
|
+
|
|
286
|
+
const poseId = typeof data.pose === "string" ? data.pose : ""
|
|
287
|
+
const poseTerm = getPoseTerm(poseId)
|
|
288
|
+
if (poseTerm) terms.push(poseTerm)
|
|
289
|
+
|
|
290
|
+
const handPositionId =
|
|
291
|
+
typeof data.handPosition === "string" ? data.handPosition : ""
|
|
292
|
+
const handPositionTerm = getPoseTerm(handPositionId)
|
|
293
|
+
if (handPositionTerm) terms.push(handPositionTerm)
|
|
294
|
+
|
|
295
|
+
const bodyLeanId = typeof data.bodyLean === "string" ? data.bodyLean : ""
|
|
296
|
+
const bodyLeanTerm = getPoseTerm(bodyLeanId)
|
|
297
|
+
if (bodyLeanTerm) terms.push(bodyLeanTerm)
|
|
298
|
+
|
|
299
|
+
const headTiltId = typeof data.headTilt === "string" ? data.headTilt : ""
|
|
300
|
+
const headTiltTerm = getPoseTerm(headTiltId)
|
|
301
|
+
if (headTiltTerm) terms.push(headTiltTerm)
|
|
302
|
+
|
|
303
|
+
const activityId = typeof data.activity === "string" ? data.activity : ""
|
|
304
|
+
const activityTerm = getPoseTerm(activityId)
|
|
305
|
+
if (activityTerm) terms.push(activityTerm)
|
|
306
|
+
|
|
307
|
+
const post = typeof data.postText === "string" ? data.postText.trim() : ""
|
|
308
|
+
if (post) terms.push(post)
|
|
309
|
+
|
|
310
|
+
return terms
|
|
311
|
+
}
|
|
@@ -21,32 +21,42 @@
|
|
|
21
21
|
* executor and the backend orchestrator.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
25
|
+
|
|
24
26
|
export interface PostProcessEffect {
|
|
25
27
|
readonly id: string
|
|
26
28
|
readonly label: string
|
|
27
29
|
readonly description: string
|
|
28
30
|
readonly promptHint: string
|
|
31
|
+
/**
|
|
32
|
+
* Optional authored compact term (see `term.ts`). Authored only where the
|
|
33
|
+
* lowercased label is not the phrase a colorist / retoucher would write in a
|
|
34
|
+
* prompt — "Bloom Glow" and "Color Fringe" are UI compounds, the trade terms
|
|
35
|
+
* are "highlight bloom" and "color fringing". Everywhere else the label IS
|
|
36
|
+
* the term.
|
|
37
|
+
*/
|
|
38
|
+
readonly term?: string
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export const POST_PROCESS_EFFECTS: ReadonlyArray<PostProcessEffect> = [
|
|
32
42
|
{ id: "vignette-soft", label: "Soft Vignette", description: "Gentle corner darkening", promptHint: "soft dark vignette gently darkening the corners of the frame, drawing the eye toward the center without calling attention to itself" },
|
|
33
43
|
{ id: "vignette-heavy", label: "Heavy Vignette", description: "Dramatic black corners", promptHint: "heavy black vignette circumscribing the subject in a dramatic dark oval, deep falloff at the edges that nearly crushes to black" },
|
|
34
|
-
{ id: "dodge-and-burn", label: "Dodge & Burn", description: "Sculpted highlight/shadow", promptHint: "aggressive dodge-and-burn shaping with sculpted highlight and shadow chiaroscuro, hand-painted-style local contrast carving form into the subject" },
|
|
44
|
+
{ id: "dodge-and-burn", label: "Dodge & Burn", description: "Sculpted highlight/shadow", promptHint: "aggressive dodge-and-burn shaping with sculpted highlight and shadow chiaroscuro, hand-painted-style local contrast carving form into the subject", term: "dodge and burn" },
|
|
35
45
|
{ id: "film-grain-fine", label: "Fine Film Grain", description: "Subtle 35mm-style grain", promptHint: "fine film grain overlay across the entire image, classic 35mm character with delicate organic noise that softens digital edges" },
|
|
36
46
|
{ id: "film-grain-heavy", label: "Heavy Film Grain", description: "Coarse push-processed grain", promptHint: "heavy coarse grain like push-processed high-ISO film, gritty texture across midtones and dense noise in the shadows" },
|
|
37
|
-
{ id: "halation-glow", label: "Halation Glow", description: "Cinestill red-halo bloom", promptHint: "warm halation glow blooming around bright highlights, signature cinestill-style red halo bleeding from light sources and tungsten practicals" },
|
|
38
|
-
{ id: "bloom-glow", label: "Bloom Glow", description: "Romantic dreamy highlight bloom", promptHint: "dreamy soft-focus bloom on highlights, romantic glow that wraps around bright areas and lifts the overall mood with a hazy halo" },
|
|
47
|
+
{ id: "halation-glow", label: "Halation Glow", description: "Cinestill red-halo bloom", promptHint: "warm halation glow blooming around bright highlights, signature cinestill-style red halo bleeding from light sources and tungsten practicals", term: "warm halation glow" },
|
|
48
|
+
{ id: "bloom-glow", label: "Bloom Glow", description: "Romantic dreamy highlight bloom", promptHint: "dreamy soft-focus bloom on highlights, romantic glow that wraps around bright areas and lifts the overall mood with a hazy halo", term: "highlight bloom" },
|
|
39
49
|
{ id: "chromatic-aberration", label: "Chromatic Aberration", description: "Red/cyan fringe on edges", promptHint: "visible chromatic aberration with red and cyan fringing on contrast edges, lens-imperfection look that reads as authentic optical character" },
|
|
40
50
|
{ id: "light-leak", label: "Light Leak", description: "Warm streak across the frame", promptHint: "warm orange light leak streaking across one edge of the frame, fogged film aesthetic with a soft amber wash bleeding into the image" },
|
|
41
51
|
{ id: "film-burn", label: "Film Burn", description: "Vintage Super-8 corner flare", promptHint: "film burn flare in the corner with a vintage Super-8 aesthetic, charred edge bleeding orange and white light into the frame" },
|
|
42
|
-
{ id: "scratched-emulsion", label: "Scratched Emulsion", description: "Aged film scratches + dust", promptHint: "vertical scratches and dust marks like aged film emulsion, hairline streaks running through the frame and scattered dust specks across the image" },
|
|
43
|
-
{ id: "color-fringe", label: "Color Fringe", description: "Subtle high-contrast fringing", promptHint: "subtle color fringing on high-contrast edges, gentle prismatic shimmer that adds optical authenticity without overwhelming the image" },
|
|
52
|
+
{ id: "scratched-emulsion", label: "Scratched Emulsion", description: "Aged film scratches + dust", promptHint: "vertical scratches and dust marks like aged film emulsion, hairline streaks running through the frame and scattered dust specks across the image", term: "scratched, dusty film emulsion" },
|
|
53
|
+
{ id: "color-fringe", label: "Color Fringe", description: "Subtle high-contrast fringing", promptHint: "subtle color fringing on high-contrast edges, gentle prismatic shimmer that adds optical authenticity without overwhelming the image", term: "subtle color fringing" },
|
|
44
54
|
{ id: "soft-focus-diffusion", label: "Soft-Focus Diffusion", description: "Hazy dreamy highlight bloom", promptHint: "soft-focus diffusion filter — slightly hazy dreamy bloom on highlights, glowing skin tones and a romantic veil across the entire image" },
|
|
45
55
|
{ id: "contrast-boost", label: "Contrast Boost", description: "Crushed shadows + pushed highlights", promptHint: "aggressive contrast boost, deepened shadows crushing toward black and pushed highlights, punchy editorial-grade tonal separation" },
|
|
46
56
|
{ id: "sharpening", label: "Heavy Sharpening", description: "Aggressive edge-sharpening pass, crisp micro-detail", promptHint: "aggressive edge-sharpening pass with crisp micro-detail emphasized, hardened edges and pronounced texture acutance across the entire image" },
|
|
47
57
|
{ id: "clarity-boost", label: "Clarity Boost", description: "Mid-tone clarity enhancement, increased local contrast", promptHint: "mid-tone clarity enhancement with increased local contrast, deepened textural definition and punchy structural detail without crushing shadows or highlights" },
|
|
48
|
-
{ id: "dehaze", label: "Dehaze", description: "Atmospheric dehaze applied, removing softness and lifting contrast through fog", promptHint: "atmospheric dehaze applied across the frame, removing veiling softness and lifting contrast through haze and fog with restored color saturation in distant tones" },
|
|
49
|
-
{ id: "lift-gamma-gain", label: "Lift-Gamma-Gain Grade", description: "Three-way color grading wheels — shadow lift, midtone gamma, highlight gain", promptHint: "three-way color grading with lift-gamma-gain wheels shaping shadow lift, midtone gamma and highlight gain independently, professional colorist-grade tonal sculpting" },
|
|
58
|
+
{ id: "dehaze", label: "Dehaze", description: "Atmospheric dehaze applied, removing softness and lifting contrast through fog", promptHint: "atmospheric dehaze applied across the frame, removing veiling softness and lifting contrast through haze and fog with restored color saturation in distant tones", term: "atmospheric dehaze" },
|
|
59
|
+
{ id: "lift-gamma-gain", label: "Lift-Gamma-Gain Grade", description: "Three-way color grading wheels — shadow lift, midtone gamma, highlight gain", promptHint: "three-way color grading with lift-gamma-gain wheels shaping shadow lift, midtone gamma and highlight gain independently, professional colorist-grade tonal sculpting", term: "three-way lift gamma gain grade" },
|
|
50
60
|
] as const
|
|
51
61
|
|
|
52
62
|
const postProcessById = new Map<string, PostProcessEffect>(
|
|
@@ -69,13 +79,24 @@ export function getPostProcessEffectPromptHint(id: string | undefined | null): s
|
|
|
69
79
|
return getPostProcessEffect(id)?.promptHint ?? ""
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Compact counterpart of `getPostProcessEffectPromptHint`: the short
|
|
84
|
+
* professional term this pass injects in compact hint mode ("dodge and burn"
|
|
85
|
+
* where the hint is the full mechanism sentence). Empty string for an unknown
|
|
86
|
+
* id and for any entry that injects no hint at all.
|
|
87
|
+
*/
|
|
88
|
+
export function getPostProcessEffectTerm(id: string | undefined | null): string {
|
|
89
|
+
return resolveTerm(getPostProcessEffect(id))
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
/**
|
|
73
93
|
* Multi-pick: 1-2 post-process effect ids → composite grading clause.
|
|
74
94
|
* Common pairs: vignette + film-grain, halation + bloom, dodge-burn +
|
|
75
95
|
* chromatic-aberration. Each entry already describes a complete grading
|
|
76
96
|
* pass, so we emit independently and let the comma-join compose them.
|
|
77
97
|
*/
|
|
78
|
-
export function buildPostProcessHints(value: unknown): string[] {
|
|
98
|
+
export function buildPostProcessHints(value: unknown, mode: PickerHintMode = "full"): string[] {
|
|
99
|
+
if (mode === "compact") return buildPostProcessTerms(value)
|
|
79
100
|
const ids: string[] = []
|
|
80
101
|
if (typeof value === "string" && value) ids.push(value)
|
|
81
102
|
else if (Array.isArray(value)) {
|
|
@@ -91,4 +112,26 @@ export function buildPostProcessHints(value: unknown): string[] {
|
|
|
91
112
|
return out
|
|
92
113
|
}
|
|
93
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Compact counterpart of `buildPostProcessHints`: the same 1-2 id multi-pick,
|
|
117
|
+
* emitted as short professional terms instead of full mechanism sentences
|
|
118
|
+
* ("soft vignette", "fine film grain"). Same collection order, same
|
|
119
|
+
* skip-the-no-ops behavior.
|
|
120
|
+
*/
|
|
121
|
+
export function buildPostProcessTerms(value: unknown): string[] {
|
|
122
|
+
const ids: string[] = []
|
|
123
|
+
if (typeof value === "string" && value) ids.push(value)
|
|
124
|
+
else if (Array.isArray(value)) {
|
|
125
|
+
for (const v of value) {
|
|
126
|
+
if (typeof v === "string" && v && !ids.includes(v)) ids.push(v)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const out: string[] = []
|
|
130
|
+
for (const id of ids) {
|
|
131
|
+
const term = getPostProcessEffectTerm(id)
|
|
132
|
+
if (term) out.push(term)
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
export const POST_PROCESS_EFFECT_IDS: ReadonlyArray<string> = POST_PROCESS_EFFECTS.map((p) => p.id)
|
package/src/prompt-builder.ts
CHANGED
|
@@ -1655,7 +1655,22 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
1655
1655
|
if (refCap > 0) {
|
|
1656
1656
|
const structuredBudget = Math.max(0, refCap - referenceImageUrls.length)
|
|
1657
1657
|
if (connectedReferences.length > structuredBudget) {
|
|
1658
|
-
|
|
1658
|
+
// ATTACH-PRIORITY slice, not raw order. The raw list interleaves each
|
|
1659
|
+
// wired character's canonical with its whole variant array, and an
|
|
1660
|
+
// unmentioned VARIANT never attaches — yet a raw slice let one
|
|
1661
|
+
// variant-rich character fill the budget and evict every character
|
|
1662
|
+
// wired after it ENTIRELY, canonical included (the "two characters
|
|
1663
|
+
// wired, one sent" incident: 1 canonical + 7 variants consumed
|
|
1664
|
+
// nano-banana-pro's cap of 8, and the second character vanished
|
|
1665
|
+
// while the provider received TWO urls total). Non-variant entries
|
|
1666
|
+
// (canonicals, manual refs, extra-refs) take the budget first;
|
|
1667
|
+
// variants fill what remains as mention candidates. Survivors keep
|
|
1668
|
+
// their original relative order, so slot numbering is unchanged for
|
|
1669
|
+
// every graph that fit the budget before.
|
|
1670
|
+
const nonVariants = connectedReferences.filter((r) => !r.variantSlug || r.isExtraRef === true)
|
|
1671
|
+
const variants = connectedReferences.filter((r) => r.variantSlug && r.isExtraRef !== true)
|
|
1672
|
+
const kept = new Set([...nonVariants, ...variants].slice(0, structuredBudget))
|
|
1673
|
+
connectedReferences = connectedReferences.filter((r) => kept.has(r))
|
|
1659
1674
|
}
|
|
1660
1675
|
}
|
|
1661
1676
|
}
|