@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.
Files changed (74) hide show
  1. package/dist/index.cjs +5443 -4295
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1202 -61
  4. package/dist/index.d.ts +1202 -61
  5. package/dist/index.js +5357 -4297
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  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__/provider-prompt-doctrine.test.ts +18 -0
  25. package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
  26. package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
  27. package/src/__tests__/term.test.ts +104 -0
  28. package/src/__tests__/transitions.test.ts +8 -5
  29. package/src/__tests__/upstream-immutability.test.ts +23 -0
  30. package/src/action-fx.ts +67 -17
  31. package/src/aesthetic.ts +54 -6
  32. package/src/atmosphere.ts +64 -23
  33. package/src/backdrop.ts +44 -30
  34. package/src/camera-format.ts +33 -11
  35. package/src/camera-motions.ts +89 -0
  36. package/src/catalog-packs.ts +125 -0
  37. package/src/catalog-sidecar-coverage.ts +36 -0
  38. package/src/character-fx.ts +112 -39
  39. package/src/color-look.ts +44 -27
  40. package/src/composition-effects.ts +21 -7
  41. package/src/era.ts +24 -0
  42. package/src/exposure-settings.ts +76 -18
  43. package/src/framing.ts +102 -10
  44. package/src/held-prop.ts +125 -63
  45. package/src/identity-lock.ts +12 -5
  46. package/src/image-reference-doctrine.ts +55 -0
  47. package/src/index.ts +5 -0
  48. package/src/instrumentation.ts +148 -57
  49. package/src/lens.ts +31 -15
  50. package/src/lighting.ts +120 -59
  51. package/src/loop-subject.ts +27 -1
  52. package/src/materials.ts +123 -69
  53. package/src/mood.ts +123 -51
  54. package/src/music-genre.ts +171 -67
  55. package/src/music-mood.ts +85 -17
  56. package/src/parameter-prompt-hint.ts +168 -56
  57. package/src/person-packs.ts +182 -0
  58. package/src/person.ts +506 -408
  59. package/src/photo-genre.ts +37 -22
  60. package/src/photographer.ts +138 -1
  61. package/src/picker-catalogs.ts +86 -40
  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 +12 -5
  66. package/src/render-quality.ts +25 -7
  67. package/src/setting.ts +30 -14
  68. package/src/style.ts +33 -15
  69. package/src/styling.ts +155 -94
  70. package/src/temporal.ts +80 -18
  71. package/src/term.ts +155 -0
  72. package/src/transitions.ts +73 -26
  73. package/src/voice-character.ts +190 -90
  74. package/src/voice-delivery.ts +83 -12
@@ -19,6 +19,8 @@
19
19
  * the backend orchestrator.
20
20
  */
21
21
 
22
+ import { resolveTerm, type PickerHintMode } from "./term.js"
23
+
22
24
  export type ExposureCategory = "aperture" | "shutter-speed" | "iso"
23
25
 
24
26
  export interface ExposureSettings {
@@ -27,35 +29,54 @@ export interface ExposureSettings {
27
29
  readonly category: ExposureCategory
28
30
  readonly description: string
29
31
  readonly promptHint: string
32
+ /**
33
+ * Optional authored compact term (see `term.ts`). Authored on nearly every
34
+ * dial value, because the mechanical derivation mangles all three
35
+ * categories — the f-stop slash reads as a UI compound ("f/1.2"), the
36
+ * shutter fraction loses its unit and its annotation ("1/1000 (action
37
+ * freeze)" → "1/1000"), and the ISO annotation is stripped ("ISO 3200
38
+ * (heavy grain)" → "iso 3200").
39
+ *
40
+ * Each term is the terse trade phrase a photographer writes — the dial
41
+ * value plus its category noun ("f/2.8 aperture", "1/500s shutter speed",
42
+ * "iso 1600 film speed") — and NOT the visual consequence: that is what
43
+ * `promptHint` is for, and a comma-bearing term would split into unrelated
44
+ * items once compact terms are joined into a comma-delimited list.
45
+ *
46
+ * ISO 400 / ISO 800 need nothing: their labels already read as the term.
47
+ * The three annotated ISO labels carry the category noun only because the
48
+ * guard requires an authored term to differ from the derivation.
49
+ */
50
+ readonly term?: string
30
51
  }
31
52
 
32
53
  export const EXPOSURE_SETTINGS: ReadonlyArray<ExposureSettings> = [
33
54
  // ---------------------------- Aperture ----------------------------
34
55
  // Wide-open primes through medium tele, then deep landscape stops.
35
- { id: "aperture-f1-2", label: "f/1.2", category: "aperture", description: "Razor-thin DOF, dreamy bokeh", promptHint: "shot wide open at f/1.2 — paper-thin depth of field with the subject's eyes in razor focus and everything else dissolving into creamy bokeh" },
36
- { id: "aperture-f1-4", label: "f/1.4", category: "aperture", description: "Aggressive subject isolation", promptHint: "shot at f/1.4 — extremely shallow depth of field, aggressive subject isolation against a smoothly melted background" },
37
- { id: "aperture-f1-8", label: "f/1.8", category: "aperture", description: "Classic portrait separation", promptHint: "shot at f/1.8 — shallow depth of field with the classic portrait separation between subject and softly defocused background" },
38
- { id: "aperture-f2-8", label: "f/2.8", category: "aperture", description: "Subject sharp, BG soft", promptHint: "shot at f/2.8 — subject crisply sharp with a gently defocused background, working aperture for low-light portraiture" },
39
- { id: "aperture-f4", label: "f/4", category: "aperture", description: "Balanced everyday DOF", promptHint: "shot at f/4 — balanced depth of field where the subject is fully in focus and the background is suggested rather than detailed" },
40
- { id: "aperture-f5-6", label: "f/5.6", category: "aperture", description: "Sharp across the subject", promptHint: "shot at f/5.6 — comfortable working aperture with edge-to-edge subject sharpness and a moderately rendered background" },
41
- { id: "aperture-f8", label: "f/8", category: "aperture", description: "Sweet-spot sharpness", promptHint: "shot at f/8 — peak optical sharpness across the frame, the photographer's sweet spot for storytelling and editorial work" },
42
- { id: "aperture-f11", label: "f/11", category: "aperture", description: "Deep landscape DOF", promptHint: "shot at f/11 — deep depth of field with foreground and distant elements both rendered crisply, classic landscape stop" },
43
- { id: "aperture-f16", label: "f/16", category: "aperture", description: "Hyperfocal, sun-stars", promptHint: "shot stopped down to f/16 — hyperfocal depth of field with everything sharp, sunlight rendered as crisp diffraction stars on bright highlights" },
56
+ { id: "aperture-f1-2", label: "f/1.2", category: "aperture", description: "Razor-thin DOF, dreamy bokeh", promptHint: "shot wide open at f/1.2 — paper-thin depth of field with the subject's eyes in razor focus and everything else dissolving into creamy bokeh", term: "f/1.2 aperture" },
57
+ { id: "aperture-f1-4", label: "f/1.4", category: "aperture", description: "Aggressive subject isolation", promptHint: "shot at f/1.4 — extremely shallow depth of field, aggressive subject isolation against a smoothly melted background", term: "f/1.4 aperture" },
58
+ { id: "aperture-f1-8", label: "f/1.8", category: "aperture", description: "Classic portrait separation", promptHint: "shot at f/1.8 — shallow depth of field with the classic portrait separation between subject and softly defocused background", term: "f/1.8 aperture" },
59
+ { id: "aperture-f2-8", label: "f/2.8", category: "aperture", description: "Subject sharp, BG soft", promptHint: "shot at f/2.8 — subject crisply sharp with a gently defocused background, working aperture for low-light portraiture", term: "f/2.8 aperture" },
60
+ { id: "aperture-f4", label: "f/4", category: "aperture", description: "Balanced everyday DOF", promptHint: "shot at f/4 — balanced depth of field where the subject is fully in focus and the background is suggested rather than detailed", term: "f/4 aperture" },
61
+ { id: "aperture-f5-6", label: "f/5.6", category: "aperture", description: "Sharp across the subject", promptHint: "shot at f/5.6 — comfortable working aperture with edge-to-edge subject sharpness and a moderately rendered background", term: "f/5.6 aperture" },
62
+ { id: "aperture-f8", label: "f/8", category: "aperture", description: "Sweet-spot sharpness", promptHint: "shot at f/8 — peak optical sharpness across the frame, the photographer's sweet spot for storytelling and editorial work", term: "f/8 aperture" },
63
+ { id: "aperture-f11", label: "f/11", category: "aperture", description: "Deep landscape DOF", promptHint: "shot at f/11 — deep depth of field with foreground and distant elements both rendered crisply, classic landscape stop", term: "f/11 aperture" },
64
+ { id: "aperture-f16", label: "f/16", category: "aperture", description: "Hyperfocal, sun-stars", promptHint: "shot stopped down to f/16 — hyperfocal depth of field with everything sharp, sunlight rendered as crisp diffraction stars on bright highlights", term: "f/16 aperture" },
44
65
 
45
66
  // ---------------------------- Shutter Speed ----------------------------
46
- { id: "shutter-1-30", label: "1/30 (handheld blur)", category: "shutter-speed", description: "Hint of handheld motion", promptHint: "captured at 1/30s — a hint of handheld motion blur on moving subjects, slight camera shake suggesting an in-the-moment documentary feel" },
47
- { id: "shutter-1-60", label: "1/60", category: "shutter-speed", description: "Standard everyday shutter", promptHint: "captured at 1/60s — standard everyday shutter speed, sharp on still subjects with subtle blur on fast motion" },
48
- { id: "shutter-1-200", label: "1/200", category: "shutter-speed", description: "Crisp on most subjects", promptHint: "captured at 1/200s — crisp on most subjects, the working shutter speed for portraits and general photography" },
49
- { id: "shutter-1-500", label: "1/500", category: "shutter-speed", description: "Sharp on quick action", promptHint: "captured at 1/500s — sharp rendering of quick human action, hair and fabric mid-motion frozen with clean edges" },
50
- { id: "shutter-1-1000", label: "1/1000 (action freeze)", category: "shutter-speed", description: "Frozen sports/wildlife", promptHint: "captured at 1/1000s — frozen mid-air action, water droplets suspended in space, every fast motion crystallized with clinical sharpness" },
51
- { id: "shutter-long-1s", label: "Long exposure (1s)", category: "shutter-speed", description: "Streaks and motion trails", promptHint: "captured with a one-second long exposure — flowing motion rendered as smooth light trails and streaks, static subjects sharp while moving elements paint across the frame" },
67
+ { id: "shutter-1-30", label: "1/30 (handheld blur)", category: "shutter-speed", description: "Hint of handheld motion", promptHint: "captured at 1/30s — a hint of handheld motion blur on moving subjects, slight camera shake suggesting an in-the-moment documentary feel", term: "1/30s shutter speed" },
68
+ { id: "shutter-1-60", label: "1/60", category: "shutter-speed", description: "Standard everyday shutter", promptHint: "captured at 1/60s — standard everyday shutter speed, sharp on still subjects with subtle blur on fast motion", term: "1/60s shutter speed" },
69
+ { id: "shutter-1-200", label: "1/200", category: "shutter-speed", description: "Crisp on most subjects", promptHint: "captured at 1/200s — crisp on most subjects, the working shutter speed for portraits and general photography", term: "1/200s shutter speed" },
70
+ { id: "shutter-1-500", label: "1/500", category: "shutter-speed", description: "Sharp on quick action", promptHint: "captured at 1/500s — sharp rendering of quick human action, hair and fabric mid-motion frozen with clean edges", term: "1/500s shutter speed" },
71
+ { id: "shutter-1-1000", label: "1/1000 (action freeze)", category: "shutter-speed", description: "Frozen sports/wildlife", promptHint: "captured at 1/1000s — frozen mid-air action, water droplets suspended in space, every fast motion crystallized with clinical sharpness", term: "1/1000s shutter speed" },
72
+ { id: "shutter-long-1s", label: "Long exposure (1s)", category: "shutter-speed", description: "Streaks and motion trails", promptHint: "captured with a one-second long exposure — flowing motion rendered as smooth light trails and streaks, static subjects sharp while moving elements paint across the frame", term: "one-second long exposure" },
52
73
 
53
74
  // ---------------------------- ISO ----------------------------
54
- { id: "iso-100", label: "ISO 100 (clean)", category: "iso", description: "Minimal noise, fine grain", promptHint: "ISO 100 — pristinely clean image with virtually no noise, ultra-fine grain structure and rich tonal latitude" },
75
+ { id: "iso-100", label: "ISO 100 (clean)", category: "iso", description: "Minimal noise, fine grain", promptHint: "ISO 100 — pristinely clean image with virtually no noise, ultra-fine grain structure and rich tonal latitude", term: "iso 100 film speed" },
55
76
  { id: "iso-400", label: "ISO 400", category: "iso", description: "Slight texture, daily-driver ISO", promptHint: "ISO 400 — subtle film-like grain texture, the daily-driver sensitivity that retains detail with a hint of organic noise" },
56
77
  { id: "iso-800", label: "ISO 800", category: "iso", description: "Visible but pleasant grain", promptHint: "ISO 800 — visible but pleasant grain pattern, evening-indoor sensitivity with a touch of analog character" },
57
- { id: "iso-1600", label: "ISO 1600 (visible grain)", category: "iso", description: "Editorial low-light texture", promptHint: "ISO 1600 — clearly visible grain, editorial low-light feel with rich texture and slightly muted shadows" },
58
- { id: "iso-3200", label: "ISO 3200 (heavy grain)", category: "iso", description: "Pushed, gritty documentary feel", promptHint: "ISO 3200 — heavy push-processed grain, gritty documentary character with elevated shadow noise and a raw, journalistic texture" },
78
+ { id: "iso-1600", label: "ISO 1600 (visible grain)", category: "iso", description: "Editorial low-light texture", promptHint: "ISO 1600 — clearly visible grain, editorial low-light feel with rich texture and slightly muted shadows", term: "iso 1600 film speed" },
79
+ { id: "iso-3200", label: "ISO 3200 (heavy grain)", category: "iso", description: "Pushed, gritty documentary feel", promptHint: "ISO 3200 — heavy push-processed grain, gritty documentary character with elevated shadow noise and a raw, journalistic texture", term: "iso 3200 film speed" },
59
80
  ] as const
60
81
 
61
82
  export const EXPOSURE_CATEGORY_ORDER: ReadonlyArray<ExposureCategory> = [
@@ -90,6 +111,19 @@ export function getExposurePromptHint(id: string | undefined | null): string {
90
111
  return getExposure(id)?.promptHint ?? ""
91
112
  }
92
113
 
114
+ /**
115
+ * Compact professional TERM for an exposure id — the short phrase a
116
+ * photographer would write in a prompt ("f/1.4 aperture"), as opposed to the
117
+ * sentence-long `promptHint`.
118
+ *
119
+ * Same lookup and same empty-string-on-miss behavior as
120
+ * `getExposurePromptHint`, so the two can never disagree about which entry
121
+ * they describe.
122
+ */
123
+ export function getExposureTerm(id: string | undefined | null): string {
124
+ return resolveTerm(getExposure(id))
125
+ }
126
+
93
127
  export const EXPOSURE_IDS: ReadonlyArray<string> = EXPOSURE_SETTINGS.map((e) => e.id)
94
128
 
95
129
  /**
@@ -130,7 +164,9 @@ export function buildExposureHints(
130
164
  shutterSpeed?: unknown
131
165
  isoValue?: unknown
132
166
  },
167
+ mode: PickerHintMode = "full",
133
168
  ): string[] {
169
+ if (mode === "compact") return buildExposureTerms(data)
134
170
  const hints: string[] = []
135
171
  for (const [field] of EXPOSURE_FIELDS_IN_ORDER) {
136
172
  const id = data[field]
@@ -140,3 +176,25 @@ export function buildExposureHints(
140
176
  }
141
177
  return hints
142
178
  }
179
+
180
+ /**
181
+ * Compact-mode mirror of `buildExposureHints`: the same per-category fields in
182
+ * the same canonical order (aperture, shutter-speed, iso), resolved to their
183
+ * short professional terms instead of their sentence-long hints.
184
+ */
185
+ export function buildExposureTerms(
186
+ data: Record<string, unknown> & {
187
+ aperture?: unknown
188
+ shutterSpeed?: unknown
189
+ isoValue?: unknown
190
+ },
191
+ ): string[] {
192
+ const terms: string[] = []
193
+ for (const [field] of EXPOSURE_FIELDS_IN_ORDER) {
194
+ const id = data[field]
195
+ if (typeof id !== "string" || id.length === 0) continue
196
+ const term = getExposureTerm(id)
197
+ if (term) terms.push(term)
198
+ }
199
+ return terms
200
+ }
package/src/framing.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  * frontend DAG executor and the backend orchestrator.
10
10
  */
11
11
 
12
+ import { resolveTerm, type PickerHintMode } from "./term.js"
13
+
12
14
  export type FramingCategory =
13
15
  | "shot-size"
14
16
  | "angle"
@@ -22,6 +24,18 @@ export interface Framing {
22
24
  readonly category: FramingCategory
23
25
  readonly description: string
24
26
  readonly promptHint: string
27
+ /**
28
+ * Optional authored compact term (see `term.ts`). Authored only where the
29
+ * lowercased label is not the phrase a cinematographer would write in a
30
+ * prompt — a UI compound ("Golden Spiral / Fibonacci"), an annotation
31
+ * ("ECU: Eye"), a reversed pair ("Headroom Tight" → "tight headroom"), a
32
+ * bare word that needs its shot noun ("Insert" → "insert shot"), or a trade
33
+ * phrase whose bare form reads as something else entirely once the trade
34
+ * context is stripped ("Choker" the neckwear, "Dirty Single" the grime,
35
+ * "Single" the continuous take). Everywhere else the label IS the term and
36
+ * nothing is authored.
37
+ */
38
+ readonly term?: string
25
39
  }
26
40
 
27
41
  export const FRAMINGS: ReadonlyArray<Framing> = [
@@ -46,6 +60,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
46
60
  category: "shot-size",
47
61
  description: "Subject from the knees up",
48
62
  promptHint: "medium wide shot, subject framed from the knees up",
63
+ term: "medium wide shot",
49
64
  },
50
65
  {
51
66
  id: "medium-shot",
@@ -81,6 +96,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
81
96
  category: "shot-size",
82
97
  description: "Sergio Leone-style ECU on a single eye",
83
98
  promptHint: "Sergio Leone-style extreme close-up tight on a single eye, iris filling the frame, every eyelash and reflection visible",
99
+ term: "extreme close-up on a single eye",
84
100
  },
85
101
  {
86
102
  id: "ecu-mouth",
@@ -88,6 +104,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
88
104
  category: "shot-size",
89
105
  description: "ECU tight on the mouth",
90
106
  promptHint: "extreme close-up tight on the mouth, lips and teeth filling the frame, breath visible",
107
+ term: "extreme close-up on the lips and mouth",
91
108
  },
92
109
  {
93
110
  id: "ecu-hands",
@@ -95,6 +112,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
95
112
  category: "shot-size",
96
113
  description: "ECU tight on the hands",
97
114
  promptHint: "extreme close-up tight on the hands, every finger and crease visible",
115
+ term: "extreme close-up on the hands",
98
116
  },
99
117
  {
100
118
  id: "big-close-up",
@@ -102,6 +120,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
102
120
  category: "shot-size",
103
121
  description: "Tighter than CU: chin to forehead, no headroom",
104
122
  promptHint: "big close-up, tighter than a standard close-up with just the face from chin to forehead, no headroom",
123
+ term: "big close-up, chin to forehead",
105
124
  },
106
125
  {
107
126
  id: "choker",
@@ -109,6 +128,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
109
128
  category: "shot-size",
110
129
  description: "Head and neck only, intimate intensity",
111
130
  promptHint: "choker shot, framed at the throat with head and neck only, intimate intensity",
131
+ term: "choker shot, head and neck only",
112
132
  },
113
133
  {
114
134
  id: "italian-shot",
@@ -116,6 +136,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
116
136
  category: "shot-size",
117
137
  description: "Sergio Leone Western: ECU at the eyes alone",
118
138
  promptHint: "Italian shot, Sergio Leone Western trope, extreme close-up cropping at the eyes alone leaving the rest of the face out of frame",
139
+ term: "extreme close-up cropped at the eyes",
119
140
  },
120
141
  {
121
142
  id: "insert",
@@ -123,6 +144,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
123
144
  category: "shot-size",
124
145
  description: "Detail shot of an object",
125
146
  promptHint: "insert shot, tight on a specific object or detail relevant to the scene",
147
+ term: "insert shot",
126
148
  },
127
149
  {
128
150
  id: "macro",
@@ -130,6 +152,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
130
152
  category: "shot-size",
131
153
  description: "Extreme close detail of a small subject",
132
154
  promptHint: "macro shot, extreme close-up of fine detail filling the frame, magnifying small subject features",
155
+ term: "macro shot",
133
156
  },
134
157
  {
135
158
  id: "full-shot",
@@ -143,14 +166,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
143
166
  label: "Cowboy Shot",
144
167
  category: "shot-size",
145
168
  description: "Mid-thigh up, classic Western framing",
146
- promptHint: "cowboy shot, subject framed from mid-thigh up, classic Western framing that leaves the holster visible",
147
- },
148
- {
149
- id: "head-to-knees",
150
- label: "Head to Knees",
151
- category: "shot-size",
152
- description: "From head down to the knees",
153
- promptHint: "head-to-knees framing, subject visible from the top of the head down to just above the knees",
169
+ promptHint: "cowboy shot, subject framed from mid-thigh up, classic Western framing",
154
170
  },
155
171
  {
156
172
  id: "head-to-hip",
@@ -158,6 +174,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
158
174
  category: "shot-size",
159
175
  description: "From head down to the hips",
160
176
  promptHint: "head-to-hip framing, subject visible from the top of the head down to the hipline, slightly tighter than waist-up",
177
+ term: "head-to-hip framing",
161
178
  },
162
179
  {
163
180
  id: "half-body",
@@ -165,6 +182,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
165
182
  category: "shot-size",
166
183
  description: "Clean waist-up portrait",
167
184
  promptHint: "half body portrait, subject framed from the waist up with clean portrait composition",
185
+ term: "half-body portrait",
168
186
  },
169
187
 
170
188
  // Angle (camera height / orientation)
@@ -195,6 +213,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
195
213
  category: "angle",
196
214
  description: "Direct top-down god's eye view",
197
215
  promptHint: "overhead shot, direct top-down god's eye view looking straight down at the scene",
216
+ term: "overhead shot, looking straight down",
198
217
  },
199
218
  {
200
219
  id: "worms-eye-angle",
@@ -202,6 +221,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
202
221
  category: "angle",
203
222
  description: "Extreme low angle from the ground",
204
223
  promptHint: "worm's eye view, extreme low angle from ground level looking up",
224
+ term: "worm's eye view",
205
225
  },
206
226
  {
207
227
  id: "dutch-angle",
@@ -216,6 +236,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
216
236
  category: "angle",
217
237
  description: "High aerial overhead view",
218
238
  promptHint: "bird's eye view, high aerial perspective looking down on the scene from far above",
239
+ term: "bird's eye view",
219
240
  },
220
241
  {
221
242
  id: "slightly-downward",
@@ -223,6 +244,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
223
244
  category: "angle",
224
245
  description: "Gentle tilt from above, selfie-style",
225
246
  promptHint: "slightly downward angle, camera tilted gently down toward the subject from just above their eyeline, the natural angle of a held-out phone",
247
+ term: "slightly downward angle, just above the eyeline",
226
248
  },
227
249
 
228
250
  // Coverage (dialog / multi-subject framing)
@@ -232,6 +254,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
232
254
  category: "coverage",
233
255
  description: "Clean shot of one subject",
234
256
  promptHint: "single shot, clean framing of one subject with nothing else in frame",
257
+ term: "clean single, one subject in frame",
235
258
  },
236
259
  {
237
260
  id: "two-shot",
@@ -253,6 +276,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
253
276
  category: "coverage",
254
277
  description: "Past one subject's shoulder onto another",
255
278
  promptHint: "over the shoulder framing, camera looks past one character's shoulder onto the subject opposite them",
279
+ term: "over-the-shoulder shot",
256
280
  },
257
281
  {
258
282
  id: "reverse-shot",
@@ -267,6 +291,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
267
291
  category: "coverage",
268
292
  description: "Through subject's eyes",
269
293
  promptHint: "POV framing, scene viewed through the subject's own eyes, first person perspective",
294
+ term: "pov shot, first-person perspective",
270
295
  },
271
296
  {
272
297
  id: "selfie-framing",
@@ -274,6 +299,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
274
299
  category: "coverage",
275
300
  description: "Arm's-length self-portrait",
276
301
  promptHint: "selfie framing, subject holding the camera at arm's length to frame themselves, slightly high angle from above, phone-camera perspective with the subject's face and upper body dominant in frame, arm or phone edge sometimes visible",
302
+ term: "arm's-length selfie framing",
277
303
  },
278
304
  {
279
305
  id: "mirror-selfie",
@@ -295,6 +321,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
295
321
  category: "coverage",
296
322
  description: "Framed through a foreground glass pane",
297
323
  promptHint: "shot through a foreground pane of glass, subject seen with subtle optical refraction, reflections, and surface distortions across the image, glass texture frames the composition",
324
+ term: "shot through a pane of glass",
298
325
  },
299
326
  {
300
327
  id: "top-down-flat-lay",
@@ -316,6 +343,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
316
343
  category: "coverage",
317
344
  description: "Single with another character at the edge",
318
345
  promptHint: "dirty single framing, the shot is on one character but a slice of another character — a shoulder, ear, or back of head — intrudes into the foreground edge of frame",
346
+ term: "dirty single, foreground shoulder in frame",
319
347
  },
320
348
 
321
349
  // Composition (where the subject sits in the frame)
@@ -332,6 +360,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
332
360
  category: "composition",
333
361
  description: "Subject dead center, symmetrical",
334
362
  promptHint: "centered composition, subject positioned exactly in the middle of the frame, symmetrical",
363
+ term: "centered composition",
335
364
  },
336
365
  {
337
366
  id: "headroom-tight",
@@ -339,6 +368,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
339
368
  category: "composition",
340
369
  description: "Subject's head near top of frame",
341
370
  promptHint: "tight headroom, subject's head positioned near the top of the frame with little space above",
371
+ term: "tight headroom",
342
372
  },
343
373
  {
344
374
  id: "negative-space",
@@ -360,6 +390,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
360
390
  category: "composition",
361
391
  description: "Subject in a 3×3 grid of variations",
362
392
  promptHint: "3 by 3 grid collage layout, nine equal-sized panels arranged in a three-by-three grid, each cell shows the same subject in a different pose, expression, or outfit, clean white gutters between panels",
393
+ term: "3x3 grid collage, nine equal panels",
363
394
  },
364
395
  {
365
396
  id: "diptych",
@@ -381,6 +412,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
381
412
  category: "composition",
382
413
  description: "Face built from a mosaic of small tiles",
383
414
  promptHint: "multi-frame mosaic, the subject's face assembled from many small photographic tiles arranged in a grid, each tile a tiny image that combines from a distance into the larger portrait",
415
+ term: "photo mosaic built from small image tiles",
384
416
  },
385
417
  {
386
418
  id: "contact-sheet",
@@ -394,14 +426,15 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
394
426
  label: "Magazine Spread",
395
427
  category: "composition",
396
428
  description: "Two-page magazine layout with typography",
397
- promptHint: "magazine spread layout, two-page editorial composition with bold display typography overlaid on the image, headline and pull quotes integrated with the photograph, visible page gutter down the middle",
429
+ promptHint: "magazine spread layout, two-page editorial composition with a visible page gutter down the middle",
398
430
  },
399
431
  {
400
432
  id: "cutaway-cross-section",
401
433
  label: "Cutaway / Cross-Section",
402
434
  category: "composition",
403
435
  description: "Architectural cross-section with walls peeled away",
404
- promptHint: "cutaway cross-section composition, the building's near wall peeled away to reveal the interior in dollhouse fashion, multiple rooms visible at once with floors and furnishings exposed in clean architectural section, the subject inhabiting one of the rooms",
436
+ promptHint: "cutaway cross-section composition, the near surface peeled away to reveal the interior in clean cross-section, the inner layers and hidden structure exposed at once",
437
+ term: "cutaway cross-section, near surface removed",
405
438
  },
406
439
  {
407
440
  id: "golden-spiral",
@@ -409,6 +442,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
409
442
  category: "composition",
410
443
  description: "Composition on the Fibonacci spiral",
411
444
  promptHint: "composed on the golden spiral / Fibonacci ratio, the eye guided through a nested logarithmic curve from the focal subject outward through progressively larger arcs",
445
+ term: "golden spiral composition",
412
446
  },
413
447
  {
414
448
  id: "frame-within-frame",
@@ -423,6 +457,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
423
457
  category: "composition",
424
458
  description: "Sinuous diagonal flow through the frame",
425
459
  promptHint: "S-curve serpentine composition, a sinuous diagonal flow winds through the frame, the eye guided along the curve from foreground to background",
460
+ term: "s-curve composition",
426
461
  },
427
462
  {
428
463
  id: "diagonal-composition",
@@ -444,6 +479,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
444
479
  category: "composition",
445
480
  description: "Exact left-right symmetry",
446
481
  promptHint: "symmetrical mirror composition, exact left-right symmetry across a central vertical axis, both halves of the frame mirror each other in shape, mass, and tone",
482
+ term: "symmetrical mirrored composition",
447
483
  },
448
484
  {
449
485
  id: "vignette-composition",
@@ -460,6 +496,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
460
496
  category: "vantage",
461
497
  description: "Subject facing camera",
462
498
  promptHint: "front-on shot, subject facing the camera directly",
499
+ term: "front-on view, subject facing camera",
463
500
  },
464
501
  {
465
502
  id: "three-quarter-front",
@@ -467,6 +504,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
467
504
  category: "vantage",
468
505
  description: "Slightly off-axis from front",
469
506
  promptHint: "three-quarter view of the subject, camera slightly off-axis from the front",
507
+ term: "three-quarter front view",
470
508
  },
471
509
  {
472
510
  id: "profile-left",
@@ -474,6 +512,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
474
512
  category: "vantage",
475
513
  description: "Side view, subject's left",
476
514
  promptHint: "profile shot from the subject's left side",
515
+ term: "left profile view",
477
516
  },
478
517
  {
479
518
  id: "profile-right",
@@ -481,6 +520,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
481
520
  category: "vantage",
482
521
  description: "Side view, subject's right",
483
522
  promptHint: "profile shot from the subject's right side",
523
+ term: "right profile view",
484
524
  },
485
525
  {
486
526
  id: "three-quarter-back",
@@ -488,6 +528,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
488
528
  category: "vantage",
489
529
  description: "Off-axis from behind",
490
530
  promptHint: "three-quarter view from behind, camera angled off-axis from the rear",
531
+ term: "three-quarter rear view",
491
532
  },
492
533
  {
493
534
  id: "behind",
@@ -495,6 +536,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
495
536
  category: "vantage",
496
537
  description: "Direct rear view",
497
538
  promptHint: "shot from directly behind the subject, looking at their back",
539
+ term: "rear view from behind the subject",
498
540
  },
499
541
  {
500
542
  id: "side-back-angle",
@@ -502,6 +544,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
502
544
  category: "vantage",
503
545
  description: "3/4 view from behind one shoulder",
504
546
  promptHint: "side-back angle, three-quarter view from behind one shoulder of the subject, partial face visible in profile, the back and shoulder line dominate the framing",
547
+ term: "three-quarter view from behind one shoulder",
505
548
  },
506
549
  ]
507
550
 
@@ -539,6 +582,16 @@ export function getFramingPromptHint(id: string | undefined | null): string {
539
582
  return getFraming(id)?.promptHint ?? ""
540
583
  }
541
584
 
585
+ /**
586
+ * The compact professional term for a framing id (see `term.ts`): the short
587
+ * phrase this framing injects in compact hint mode ("tight headroom" where the
588
+ * hint is the full clause). Empty string for an unknown id and for any entry
589
+ * that injects no hint at all.
590
+ */
591
+ export function getFramingTerm(id: string | undefined | null): string {
592
+ return resolveTerm(getFraming(id))
593
+ }
594
+
542
595
  export const FRAMING_IDS: ReadonlyArray<string> = FRAMINGS.map((f) => f.id)
543
596
 
544
597
  export function isVantageFraming(id: string | undefined | null): boolean {
@@ -611,7 +664,9 @@ export function buildFramingHints(
611
664
  vantage?: unknown
612
665
  },
613
666
  skipVantage = false,
667
+ mode: PickerHintMode = "full",
614
668
  ): string[] {
669
+ if (mode === "compact") return buildFramingTerms(data, skipVantage)
615
670
  const hints: string[] = []
616
671
  for (const category of FRAMING_CATEGORY_ORDER) {
617
672
  if (category === "vantage" && skipVantage) continue
@@ -632,3 +687,40 @@ export function buildFramingHints(
632
687
  }
633
688
  return hints
634
689
  }
690
+
691
+ /**
692
+ * Compact counterpart of `buildFramingHints`: the same per-category walk in the
693
+ * same canonical order and with the same `skipVantage` gate, emitted as short
694
+ * professional terms instead of full mechanism clauses ("medium close-up",
695
+ * "tight headroom"). Entries that inject no hint contribute no term either.
696
+ */
697
+ export function buildFramingTerms(
698
+ data: Record<string, unknown> & {
699
+ shotSize?: unknown
700
+ angle?: unknown
701
+ coverage?: unknown
702
+ composition?: unknown
703
+ vantage?: unknown
704
+ },
705
+ skipVantage = false,
706
+ ): string[] {
707
+ const terms: string[] = []
708
+ for (const category of FRAMING_CATEGORY_ORDER) {
709
+ if (category === "vantage" && skipVantage) continue
710
+ const field = FRAMING_FIELD_BY_CATEGORY[category]
711
+ const raw = data[field]
712
+ // composition accepts string | string[] (multi-pick max 2). Other
713
+ // categories are single-pick; arrays are tolerated defensively.
714
+ if (typeof raw === "string" && raw.length > 0) {
715
+ const term = getFramingTerm(raw)
716
+ if (term) terms.push(term)
717
+ } else if (Array.isArray(raw)) {
718
+ for (const item of raw) {
719
+ if (typeof item !== "string" || item.length === 0) continue
720
+ const term = getFramingTerm(item)
721
+ if (term) terms.push(term)
722
+ }
723
+ }
724
+ }
725
+ return terms
726
+ }