@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
@@ -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",
@@ -151,6 +174,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
151
174
  category: "shot-size",
152
175
  description: "From head down to the knees",
153
176
  promptHint: "head-to-knees framing, subject visible from the top of the head down to just above the knees",
177
+ term: "head-to-knees framing",
154
178
  },
155
179
  {
156
180
  id: "head-to-hip",
@@ -158,6 +182,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
158
182
  category: "shot-size",
159
183
  description: "From head down to the hips",
160
184
  promptHint: "head-to-hip framing, subject visible from the top of the head down to the hipline, slightly tighter than waist-up",
185
+ term: "head-to-hip framing",
161
186
  },
162
187
  {
163
188
  id: "half-body",
@@ -165,6 +190,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
165
190
  category: "shot-size",
166
191
  description: "Clean waist-up portrait",
167
192
  promptHint: "half body portrait, subject framed from the waist up with clean portrait composition",
193
+ term: "half-body portrait",
168
194
  },
169
195
 
170
196
  // Angle (camera height / orientation)
@@ -195,6 +221,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
195
221
  category: "angle",
196
222
  description: "Direct top-down god's eye view",
197
223
  promptHint: "overhead shot, direct top-down god's eye view looking straight down at the scene",
224
+ term: "overhead shot, looking straight down",
198
225
  },
199
226
  {
200
227
  id: "worms-eye-angle",
@@ -202,6 +229,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
202
229
  category: "angle",
203
230
  description: "Extreme low angle from the ground",
204
231
  promptHint: "worm's eye view, extreme low angle from ground level looking up",
232
+ term: "worm's eye view",
205
233
  },
206
234
  {
207
235
  id: "dutch-angle",
@@ -216,6 +244,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
216
244
  category: "angle",
217
245
  description: "High aerial overhead view",
218
246
  promptHint: "bird's eye view, high aerial perspective looking down on the scene from far above",
247
+ term: "bird's eye view",
219
248
  },
220
249
  {
221
250
  id: "slightly-downward",
@@ -223,6 +252,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
223
252
  category: "angle",
224
253
  description: "Gentle tilt from above, selfie-style",
225
254
  promptHint: "slightly downward angle, camera tilted gently down toward the subject from just above their eyeline, the natural angle of a held-out phone",
255
+ term: "slightly downward angle, just above the eyeline",
226
256
  },
227
257
 
228
258
  // Coverage (dialog / multi-subject framing)
@@ -232,6 +262,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
232
262
  category: "coverage",
233
263
  description: "Clean shot of one subject",
234
264
  promptHint: "single shot, clean framing of one subject with nothing else in frame",
265
+ term: "clean single, one subject in frame",
235
266
  },
236
267
  {
237
268
  id: "two-shot",
@@ -253,6 +284,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
253
284
  category: "coverage",
254
285
  description: "Past one subject's shoulder onto another",
255
286
  promptHint: "over the shoulder framing, camera looks past one character's shoulder onto the subject opposite them",
287
+ term: "over-the-shoulder shot",
256
288
  },
257
289
  {
258
290
  id: "reverse-shot",
@@ -267,6 +299,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
267
299
  category: "coverage",
268
300
  description: "Through subject's eyes",
269
301
  promptHint: "POV framing, scene viewed through the subject's own eyes, first person perspective",
302
+ term: "pov shot, first-person perspective",
270
303
  },
271
304
  {
272
305
  id: "selfie-framing",
@@ -274,6 +307,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
274
307
  category: "coverage",
275
308
  description: "Arm's-length self-portrait",
276
309
  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",
310
+ term: "arm's-length selfie framing",
277
311
  },
278
312
  {
279
313
  id: "mirror-selfie",
@@ -295,6 +329,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
295
329
  category: "coverage",
296
330
  description: "Framed through a foreground glass pane",
297
331
  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",
332
+ term: "shot through a pane of glass",
298
333
  },
299
334
  {
300
335
  id: "top-down-flat-lay",
@@ -316,6 +351,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
316
351
  category: "coverage",
317
352
  description: "Single with another character at the edge",
318
353
  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",
354
+ term: "dirty single, foreground shoulder in frame",
319
355
  },
320
356
 
321
357
  // Composition (where the subject sits in the frame)
@@ -332,6 +368,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
332
368
  category: "composition",
333
369
  description: "Subject dead center, symmetrical",
334
370
  promptHint: "centered composition, subject positioned exactly in the middle of the frame, symmetrical",
371
+ term: "centered composition",
335
372
  },
336
373
  {
337
374
  id: "headroom-tight",
@@ -339,6 +376,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
339
376
  category: "composition",
340
377
  description: "Subject's head near top of frame",
341
378
  promptHint: "tight headroom, subject's head positioned near the top of the frame with little space above",
379
+ term: "tight headroom",
342
380
  },
343
381
  {
344
382
  id: "negative-space",
@@ -360,6 +398,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
360
398
  category: "composition",
361
399
  description: "Subject in a 3×3 grid of variations",
362
400
  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",
401
+ term: "3x3 grid collage, nine equal panels",
363
402
  },
364
403
  {
365
404
  id: "diptych",
@@ -381,6 +420,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
381
420
  category: "composition",
382
421
  description: "Face built from a mosaic of small tiles",
383
422
  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",
423
+ term: "photo mosaic built from small image tiles",
384
424
  },
385
425
  {
386
426
  id: "contact-sheet",
@@ -402,6 +442,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
402
442
  category: "composition",
403
443
  description: "Architectural cross-section with walls peeled away",
404
444
  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",
445
+ term: "cutaway cross-section, near wall removed",
405
446
  },
406
447
  {
407
448
  id: "golden-spiral",
@@ -409,6 +450,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
409
450
  category: "composition",
410
451
  description: "Composition on the Fibonacci spiral",
411
452
  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",
453
+ term: "golden spiral composition",
412
454
  },
413
455
  {
414
456
  id: "frame-within-frame",
@@ -423,6 +465,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
423
465
  category: "composition",
424
466
  description: "Sinuous diagonal flow through the frame",
425
467
  promptHint: "S-curve serpentine composition, a sinuous diagonal flow winds through the frame, the eye guided along the curve from foreground to background",
468
+ term: "s-curve composition",
426
469
  },
427
470
  {
428
471
  id: "diagonal-composition",
@@ -444,6 +487,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
444
487
  category: "composition",
445
488
  description: "Exact left-right symmetry",
446
489
  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",
490
+ term: "symmetrical mirrored composition",
447
491
  },
448
492
  {
449
493
  id: "vignette-composition",
@@ -460,6 +504,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
460
504
  category: "vantage",
461
505
  description: "Subject facing camera",
462
506
  promptHint: "front-on shot, subject facing the camera directly",
507
+ term: "front-on view, subject facing camera",
463
508
  },
464
509
  {
465
510
  id: "three-quarter-front",
@@ -467,6 +512,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
467
512
  category: "vantage",
468
513
  description: "Slightly off-axis from front",
469
514
  promptHint: "three-quarter view of the subject, camera slightly off-axis from the front",
515
+ term: "three-quarter front view",
470
516
  },
471
517
  {
472
518
  id: "profile-left",
@@ -474,6 +520,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
474
520
  category: "vantage",
475
521
  description: "Side view, subject's left",
476
522
  promptHint: "profile shot from the subject's left side",
523
+ term: "left profile view",
477
524
  },
478
525
  {
479
526
  id: "profile-right",
@@ -481,6 +528,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
481
528
  category: "vantage",
482
529
  description: "Side view, subject's right",
483
530
  promptHint: "profile shot from the subject's right side",
531
+ term: "right profile view",
484
532
  },
485
533
  {
486
534
  id: "three-quarter-back",
@@ -488,6 +536,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
488
536
  category: "vantage",
489
537
  description: "Off-axis from behind",
490
538
  promptHint: "three-quarter view from behind, camera angled off-axis from the rear",
539
+ term: "three-quarter rear view",
491
540
  },
492
541
  {
493
542
  id: "behind",
@@ -495,6 +544,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
495
544
  category: "vantage",
496
545
  description: "Direct rear view",
497
546
  promptHint: "shot from directly behind the subject, looking at their back",
547
+ term: "rear view from behind the subject",
498
548
  },
499
549
  {
500
550
  id: "side-back-angle",
@@ -502,6 +552,7 @@ export const FRAMINGS: ReadonlyArray<Framing> = [
502
552
  category: "vantage",
503
553
  description: "3/4 view from behind one shoulder",
504
554
  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",
555
+ term: "three-quarter view from behind one shoulder",
505
556
  },
506
557
  ]
507
558
 
@@ -539,6 +590,16 @@ export function getFramingPromptHint(id: string | undefined | null): string {
539
590
  return getFraming(id)?.promptHint ?? ""
540
591
  }
541
592
 
593
+ /**
594
+ * The compact professional term for a framing id (see `term.ts`): the short
595
+ * phrase this framing injects in compact hint mode ("tight headroom" where the
596
+ * hint is the full clause). Empty string for an unknown id and for any entry
597
+ * that injects no hint at all.
598
+ */
599
+ export function getFramingTerm(id: string | undefined | null): string {
600
+ return resolveTerm(getFraming(id))
601
+ }
602
+
542
603
  export const FRAMING_IDS: ReadonlyArray<string> = FRAMINGS.map((f) => f.id)
543
604
 
544
605
  export function isVantageFraming(id: string | undefined | null): boolean {
@@ -611,7 +672,9 @@ export function buildFramingHints(
611
672
  vantage?: unknown
612
673
  },
613
674
  skipVantage = false,
675
+ mode: PickerHintMode = "full",
614
676
  ): string[] {
677
+ if (mode === "compact") return buildFramingTerms(data, skipVantage)
615
678
  const hints: string[] = []
616
679
  for (const category of FRAMING_CATEGORY_ORDER) {
617
680
  if (category === "vantage" && skipVantage) continue
@@ -632,3 +695,40 @@ export function buildFramingHints(
632
695
  }
633
696
  return hints
634
697
  }
698
+
699
+ /**
700
+ * Compact counterpart of `buildFramingHints`: the same per-category walk in the
701
+ * same canonical order and with the same `skipVantage` gate, emitted as short
702
+ * professional terms instead of full mechanism clauses ("medium close-up",
703
+ * "tight headroom"). Entries that inject no hint contribute no term either.
704
+ */
705
+ export function buildFramingTerms(
706
+ data: Record<string, unknown> & {
707
+ shotSize?: unknown
708
+ angle?: unknown
709
+ coverage?: unknown
710
+ composition?: unknown
711
+ vantage?: unknown
712
+ },
713
+ skipVantage = false,
714
+ ): string[] {
715
+ const terms: string[] = []
716
+ for (const category of FRAMING_CATEGORY_ORDER) {
717
+ if (category === "vantage" && skipVantage) continue
718
+ const field = FRAMING_FIELD_BY_CATEGORY[category]
719
+ const raw = data[field]
720
+ // composition accepts string | string[] (multi-pick max 2). Other
721
+ // categories are single-pick; arrays are tolerated defensively.
722
+ if (typeof raw === "string" && raw.length > 0) {
723
+ const term = getFramingTerm(raw)
724
+ if (term) terms.push(term)
725
+ } else if (Array.isArray(raw)) {
726
+ for (const item of raw) {
727
+ if (typeof item !== "string" || item.length === 0) continue
728
+ const term = getFramingTerm(item)
729
+ if (term) terms.push(term)
730
+ }
731
+ }
732
+ }
733
+ return terms
734
+ }