@nodaro/prompts 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/index.cjs +5453 -4271
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1221 -63
  4. package/dist/index.d.ts +1221 -63
  5. package/dist/index.js +5366 -4273
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
  9. package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
  10. package/src/__tests__/catalog-packs.test.ts +130 -0
  11. package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
  12. package/src/__tests__/catalog-terms.test.ts +162 -0
  13. package/src/__tests__/character-default-role.test.ts +3 -2
  14. package/src/__tests__/content-free-contract.test.ts +45 -0
  15. package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
  16. package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
  17. package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
  18. package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
  19. package/src/__tests__/parameter-hint-mode.test.ts +385 -0
  20. package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
  21. package/src/__tests__/person-packs.test.ts +166 -0
  22. package/src/__tests__/project-all-catalogs.test.ts +26 -0
  23. package/src/__tests__/prompt-builder.test.ts +53 -0
  24. package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
  25. package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
  26. package/src/__tests__/term.test.ts +104 -0
  27. package/src/__tests__/transitions.test.ts +8 -5
  28. package/src/__tests__/upstream-immutability.test.ts +23 -0
  29. package/src/action-fx.ts +67 -17
  30. package/src/aesthetic.ts +54 -6
  31. package/src/atmosphere.ts +64 -23
  32. package/src/backdrop.ts +44 -30
  33. package/src/camera-format.ts +33 -11
  34. package/src/camera-motions.ts +89 -0
  35. package/src/catalog-packs.ts +125 -0
  36. package/src/catalog-sidecar-coverage.ts +36 -0
  37. package/src/character-fx.ts +112 -39
  38. package/src/color-look.ts +44 -27
  39. package/src/composition-effects.ts +21 -7
  40. package/src/era.ts +24 -0
  41. package/src/exposure-settings.ts +76 -18
  42. package/src/framing.ts +100 -0
  43. package/src/held-prop.ts +125 -63
  44. package/src/identity-lock.ts +12 -5
  45. package/src/image-reference-doctrine.ts +55 -0
  46. package/src/index.ts +6 -0
  47. package/src/instrumentation.ts +148 -57
  48. package/src/lens.ts +31 -15
  49. package/src/lighting.ts +120 -59
  50. package/src/loop-subject.ts +27 -1
  51. package/src/materials.ts +123 -69
  52. package/src/mood.ts +123 -51
  53. package/src/music-genre.ts +171 -67
  54. package/src/music-mood.ts +85 -17
  55. package/src/parameter-prompt-hint.ts +168 -56
  56. package/src/person-packs.ts +182 -0
  57. package/src/person.ts +506 -408
  58. package/src/photo-genre.ts +37 -22
  59. package/src/photographer.ts +138 -1
  60. package/src/picker-catalogs.ts +86 -40
  61. package/src/picker-wiring.ts +1 -1
  62. package/src/pose.ts +105 -40
  63. package/src/post-process-effects.ts +51 -8
  64. package/src/prompt-builder.ts +16 -1
  65. package/src/provider-prompt-doctrine.ts +1 -2
  66. package/src/render-quality.ts +25 -7
  67. package/src/setting.ts +30 -14
  68. package/src/style-presets.ts +1 -1
  69. package/src/style.ts +33 -15
  70. package/src/styling.ts +155 -94
  71. package/src/surround-fill.ts +67 -0
  72. package/src/temporal.ts +80 -18
  73. package/src/term.ts +155 -0
  74. package/src/transitions.ts +73 -26
  75. package/src/voice-character.ts +190 -90
  76. package/src/voice-delivery.ts +83 -12
@@ -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
- export const PICKER_CATALOGS: readonly PickerCatalog[] = [
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
- PICKER_CATALOGS.find((c) => c.nodeType === nodeTypeOrCatalogId) ??
731
- PICKER_CATALOGS.find((c) => c.catalogId === nodeTypeOrCatalogId)
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 PICKER_CATALOGS
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 PICKER_CATALOGS.map((c) => ({
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
- /** An option after projection — description/promptHint present only when detail="full". */
782
- export interface ProjectedPickerOption {
783
- readonly id: string
784
- readonly label: string
785
- readonly description?: string
786
- readonly category?: string
787
- readonly promptHint?: string
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
+ }
@@ -6,7 +6,7 @@
6
6
  * Pure data, no React. Extracted from the app's parameter-picker-registry so
7
7
  * THREE consumers share one definition and cannot drift:
8
8
  * 1. The app's community fallback registry (chip pickers, no rich previews).
9
- * 2. `@nodaroai/picker-ui`'s rich registry (attaches preview/Picker renderers).
9
+ * 2. `@nodaro/picker-ui`'s registry (attaches preview/Picker renderers).
10
10
  * 3. Nodaro Cine's builder panels.
11
11
  *
12
12
  * Renderers (preview components, multi-dim Picker components) deliberately do
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)