@nodaro/prompts 1.7.3 → 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.
- package/dist/index.cjs +5422 -4269
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1198 -61
- package/dist/index.d.ts +1198 -61
- package/dist/index.js +5336 -4271
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
- package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
- package/src/__tests__/catalog-packs.test.ts +130 -0
- package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
- package/src/__tests__/catalog-terms.test.ts +162 -0
- package/src/__tests__/character-default-role.test.ts +3 -2
- package/src/__tests__/content-free-contract.test.ts +45 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
- package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
- package/src/__tests__/parameter-hint-mode.test.ts +385 -0
- package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
- package/src/__tests__/person-packs.test.ts +166 -0
- package/src/__tests__/project-all-catalogs.test.ts +26 -0
- package/src/__tests__/prompt-builder.test.ts +53 -0
- package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
- package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
- package/src/__tests__/term.test.ts +104 -0
- package/src/__tests__/transitions.test.ts +8 -5
- package/src/__tests__/upstream-immutability.test.ts +23 -0
- package/src/action-fx.ts +67 -17
- package/src/aesthetic.ts +54 -6
- package/src/atmosphere.ts +64 -23
- package/src/backdrop.ts +44 -30
- package/src/camera-format.ts +33 -11
- package/src/camera-motions.ts +89 -0
- package/src/catalog-packs.ts +125 -0
- package/src/catalog-sidecar-coverage.ts +36 -0
- package/src/character-fx.ts +112 -39
- package/src/color-look.ts +44 -27
- package/src/composition-effects.ts +21 -7
- package/src/era.ts +24 -0
- package/src/exposure-settings.ts +76 -18
- package/src/framing.ts +100 -0
- package/src/held-prop.ts +125 -63
- package/src/identity-lock.ts +12 -5
- package/src/image-reference-doctrine.ts +55 -0
- package/src/index.ts +5 -0
- package/src/instrumentation.ts +148 -57
- package/src/lens.ts +31 -15
- package/src/lighting.ts +120 -59
- package/src/loop-subject.ts +27 -1
- package/src/materials.ts +123 -69
- package/src/mood.ts +123 -51
- package/src/music-genre.ts +171 -67
- package/src/music-mood.ts +85 -17
- package/src/parameter-prompt-hint.ts +168 -56
- package/src/person-packs.ts +182 -0
- package/src/person.ts +506 -408
- package/src/photo-genre.ts +37 -22
- package/src/photographer.ts +138 -1
- package/src/picker-catalogs.ts +86 -40
- package/src/pose.ts +105 -40
- package/src/post-process-effects.ts +51 -8
- package/src/prompt-builder.ts +16 -1
- package/src/render-quality.ts +25 -7
- package/src/setting.ts +30 -14
- package/src/style.ts +33 -15
- package/src/styling.ts +155 -94
- package/src/temporal.ts +80 -18
- package/src/term.ts +155 -0
- package/src/transitions.ts +73 -26
- package/src/voice-character.ts +190 -90
- package/src/voice-delivery.ts +83 -12
package/src/exposure-settings.ts
CHANGED
|
@@ -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
|
+
}
|