@nodaro/prompts 1.26.0 → 1.27.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.
@@ -0,0 +1,19 @@
1
+ /** The dimension fields of each music / voice catalog that carries art. */
2
+ export interface SoundArtFields {
3
+ "music-genre": "genre" | "subgenre" | "era"
4
+ "music-mood": "energy" | "emotion" | "vibe"
5
+ instrumentation: "instruments" | "production" | "vocalPresence" | "singingStyle"
6
+ "voice-character": "age" | "gender" | "language" | "accent" | "timbre"
7
+ "voice-delivery": "pace" | "emotion" | "archetype"
8
+ }
9
+
10
+ export type SoundArtCatalogId = keyof SoundArtFields
11
+
12
+ /** `emoji/<slug>` or `flags/<code>` — a key of SOUND_ART_FILES. */
13
+ export type SoundArtKey = `emoji/${string}` | `flags/${string}`
14
+
15
+ export type SoundArtMap = {
16
+ readonly [C in SoundArtCatalogId]: {
17
+ readonly [F in SoundArtFields[C]]: Readonly<Record<string, SoundArtKey>>
18
+ }
19
+ }
@@ -81,7 +81,8 @@ import { ANIMALS, ANIMAL_SUBCATEGORY_LABELS, ANIMAL_SUBCATEGORY_ORDER, getAnimal
81
81
  import { VEHICLES, VEHICLE_SUBCATEGORY_LABELS, VEHICLE_SUBCATEGORY_ORDER } from "@nodaro/shared"
82
82
  import { WEAPONS, WEAPON_SUBCATEGORY_LABELS, WEAPON_SUBCATEGORY_ORDER } from "@nodaro/shared"
83
83
  import { FURNITURE, FURNITURE_SUBCATEGORY_LABELS, FURNITURE_SUBCATEGORY_ORDER } from "@nodaro/shared"
84
- import type { ProjectedCatalog, ProjectedCatalogOption, ProjectedCatalogDimension } from "@nodaro/shared"
84
+ import type { ProjectedCatalog, ProjectedCatalogOption, ProjectedCatalogDimension, ProjectedCatalogSection } from "@nodaro/shared"
85
+ import { pickerOptionImageUrl, pickerSectionImageUrl, type PickerImageOptions } from "./picker-art/images.js"
85
86
  import { HELD_PROPS, HELD_PROP_CATEGORY_LABELS, HELD_PROP_CATEGORY_ORDER } from "./held-prop.js"
86
87
  import { FRAMINGS, FRAMING_FIELD_BY_CATEGORY, FRAMING_CATEGORY_LABELS } from "./framing.js"
87
88
  import { LIGHTINGS, LIGHTING_FIELD_BY_CATEGORY, LIGHTING_CATEGORY_ORDER, LIGHTING_CATEGORY_LABELS } from "./lighting.js"
@@ -89,6 +90,7 @@ import { TEMPORALS, TEMPORAL_FIELD_BY_CATEGORY, TEMPORAL_CATEGORY_LABELS } from
89
90
  import { EXPOSURE_SETTINGS, EXPOSURE_FIELD_BY_CATEGORY, EXPOSURE_CATEGORY_LABELS } from "./exposure-settings.js"
90
91
  import { PEOPLE, PERSON_FIELD_BY_DIMENSION, PERSON_DIMENSION_ORDER, PERSON_DIMENSION_LABELS } from "./person.js"
91
92
  import { STYLINGS, STYLING_FIELD_BY_DIMENSION, STYLING_DIMENSION_ORDER, STYLING_DIMENSION_LABELS } from "./styling.js"
93
+ import { catalogTopics } from "./catalog-sections.js"
92
94
  import { MUSIC_GENRES, MUSIC_ERAS } from "./music-genre.js"
93
95
  import { MUSIC_ENERGIES, MUSIC_EMOTIONS, MUSIC_VIBES } from "./music-mood.js"
94
96
  import { INSTRUMENTS, PRODUCTION_STYLES, VOCAL_PRESENCE, SINGING_STYLES } from "./instrumentation.js"
@@ -908,23 +910,41 @@ export interface PickerCatalogSummary {
908
910
  readonly fields?: readonly string[]
909
911
  /** single: options.length; multi: sum of every dimension's options. */
910
912
  readonly optionCount: number
913
+ /** How many of those options carry an `imageUrl` (0 without picture options). */
914
+ readonly imageCount: number
911
915
  }
912
916
 
913
- /** Lightweight directory of every picker catalog — no option payloads. */
914
- export function summarizePickerCatalogs(): readonly PickerCatalogSummary[] {
915
- return getRegisteredPickerCatalogs().map((c) => ({
916
- nodeType: c.nodeType,
917
- label: c.label,
918
- catalogId: c.catalogId,
919
- kind: c.kind,
920
- valueField: c.valueField,
921
- fields: c.fields,
922
- // Every option the detail call will return, whichever kind — a single-dim
923
- // catalog with secondary dimensions carries both.
924
- optionCount:
925
- (c.options?.length ?? 0) +
926
- (c.dimensions?.reduce((n, d) => n + d.options.length, 0) ?? 0),
927
- }))
917
+ /** Every (field, option) pair a catalog's detail call returns, whichever kind. */
918
+ function everyOption(c: PickerCatalog): ReadonlyArray<{ readonly field: string | undefined; readonly option: PickerOption }> {
919
+ return [
920
+ ...(c.options ?? []).map((option) => ({ field: c.valueField, option })),
921
+ ...(c.dimensions ?? []).flatMap((d) => d.options.map((option) => ({ field: d.field, option }))),
922
+ ]
923
+ }
924
+
925
+ /**
926
+ * Lightweight directory of every picker catalog — no option payloads. With
927
+ * `images`, `imageCount` says how many options the detail call pictures.
928
+ */
929
+ export function summarizePickerCatalogs(opts: { readonly images?: PickerImageOptions } = {}): readonly PickerCatalogSummary[] {
930
+ return getRegisteredPickerCatalogs().map((c) => {
931
+ const all = everyOption(c)
932
+ const images = opts.images
933
+ return {
934
+ nodeType: c.nodeType,
935
+ label: c.label,
936
+ catalogId: c.catalogId,
937
+ kind: c.kind,
938
+ valueField: c.valueField,
939
+ fields: c.fields,
940
+ // Every option the detail call will return, whichever kind — a single-dim
941
+ // catalog with secondary dimensions carries both.
942
+ optionCount: all.length,
943
+ imageCount: images
944
+ ? all.filter(({ field, option }) => pickerOptionImageUrl(c, field, option.id, images) !== undefined).length
945
+ : 0,
946
+ }
947
+ })
928
948
  }
929
949
 
930
950
  export type PickerCatalogDetail = "compact" | "full"
@@ -936,6 +956,12 @@ export interface ProjectPickerCatalogOptions {
936
956
  readonly category?: string
937
957
  /** multi-dim: keep only this dimension field. */
938
958
  readonly field?: string
959
+ /**
960
+ * Attach each option's absolute `imageUrl` (and the Person / Styling topic
961
+ * pictures). Left out, no option carries one — the caller (the API) knows
962
+ * which host this installation is reached at, the package does not.
963
+ */
964
+ readonly images?: PickerImageOptions
939
965
  }
940
966
 
941
967
  // The projection shape now lives in `@nodaro/shared` (Apache) as the tag-free
@@ -946,10 +972,12 @@ export type ProjectedPickerOption = ProjectedCatalogOption
946
972
  export type ProjectedPickerDimension = ProjectedCatalogDimension
947
973
  export type ProjectedPickerCatalog = ProjectedCatalog
948
974
 
949
- function projectOption(o: PickerOption, detail: PickerCatalogDetail): ProjectedPickerOption {
975
+ function projectOption(o: PickerOption, detail: PickerCatalogDetail, imageUrl?: string): ProjectedPickerOption {
950
976
  // `term` rides at BOTH detail levels: a thin client renders `label` and
951
977
  // injects `term`, so compact must carry it — it is what makes compact hint
952
- // mode possible without a second round-trip for the full payload.
978
+ // mode possible without a second round-trip for the full payload. So does
979
+ // `imageUrl` (display data), present only when the option has a picture.
980
+ const extras = { ...(o.motion ? { motion: o.motion } : {}), ...(imageUrl ? { imageUrl } : {}) }
953
981
  return detail === "full"
954
982
  ? {
955
983
  id: o.id,
@@ -959,9 +987,25 @@ function projectOption(o: PickerOption, detail: PickerCatalogDetail): ProjectedP
959
987
  promptHint: o.promptHint,
960
988
  term: o.term,
961
989
  icon: o.icon,
962
- ...(o.motion ? { motion: o.motion } : {}),
990
+ ...extras,
963
991
  }
964
- : { id: o.id, label: o.label, category: o.category, term: o.term, icon: o.icon, ...(o.motion ? { motion: o.motion } : {}) }
992
+ : { id: o.id, label: o.label, category: o.category, term: o.term, icon: o.icon, ...extras }
993
+ }
994
+
995
+ /**
996
+ * The topics a catalog's settings are grouped under in the editor (Person,
997
+ * Styling), as node-data fields; only the fields the catalog has are listed.
998
+ */
999
+ function catalogSections(c: PickerCatalog, images: PickerImageOptions | undefined): ProjectedCatalogSection[] | undefined {
1000
+ const source = catalogTopics(c.catalogId)
1001
+ if (!source) return undefined
1002
+ const present = new Set((c.dimensions ?? []).map((d) => d.field))
1003
+ return source
1004
+ .map((s) => {
1005
+ const imageUrl = images ? pickerSectionImageUrl(s.label, images) : undefined
1006
+ return { label: s.label, fields: s.fields.filter((f) => present.has(f)), ...(imageUrl ? { imageUrl } : {}) }
1007
+ })
1008
+ .filter((s) => s.fields.length > 0)
965
1009
  }
966
1010
 
967
1011
  /** Project a catalog to the wire shape: compact by default, optional category/field filter. */
@@ -981,17 +1025,20 @@ export function projectPickerCatalog(
981
1025
  categoryLabels: c.categoryLabels,
982
1026
  detail,
983
1027
  }
1028
+ const images = opts.images
1029
+ const imageOf = (field: string | undefined, o: PickerOption) =>
1030
+ images ? pickerOptionImageUrl(c, field, o.id, images) : undefined
984
1031
  const projectDims = (dims: readonly PickerDimension[]) =>
985
1032
  dims.map((d) => ({
986
1033
  field: d.field,
987
1034
  label: d.label,
988
- options: d.options.map((o) => projectOption(o, detail)),
1035
+ options: d.options.map((o) => projectOption(o, detail, imageOf(d.field, o))),
989
1036
  }))
990
1037
 
991
1038
  if (c.kind === "single") {
992
1039
  let options = c.options ?? []
993
1040
  if (opts.category) options = options.filter((o) => o.category === opts.category)
994
- const projected = { ...base, options: options.map((o) => projectOption(o, detail)) }
1041
+ const projected = { ...base, options: options.map((o) => projectOption(o, detail, imageOf(c.valueField, o))) }
995
1042
  // A single-dim catalog may still carry secondary parameter dimensions
996
1043
  // (transition's position/duration/intensity). They are additive — a
997
1044
  // consumer reading only `options` is unaffected — but dropping them here
@@ -1003,7 +1050,8 @@ export function projectPickerCatalog(
1003
1050
  }
1004
1051
  let dims = c.dimensions ?? []
1005
1052
  if (opts.field) dims = dims.filter((d) => d.field === opts.field)
1006
- return { ...base, fields: c.fields, dimensions: projectDims(dims) }
1053
+ const sections = catalogSections(c, images)
1054
+ return { ...base, fields: c.fields, dimensions: projectDims(dims), ...(sections ? { sections } : {}) }
1007
1055
  }
1008
1056
 
1009
1057
  /**
@@ -1011,6 +1059,6 @@ export function projectPickerCatalog(
1011
1059
  * `/v1/catalogs` wire shape. This is what the `GET /v1/catalogs` route and its
1012
1060
  * SDK resource serve, so a deployment's vendored packs are reflected verbatim.
1013
1061
  */
1014
- export function projectAllCatalogs(opts: { detail?: PickerCatalogDetail } = {}): ProjectedCatalog[] {
1015
- return getRegisteredPickerCatalogs().map((c) => projectPickerCatalog(c, { detail: opts.detail }))
1062
+ export function projectAllCatalogs(opts: { detail?: PickerCatalogDetail; images?: PickerImageOptions } = {}): ProjectedCatalog[] {
1063
+ return getRegisteredPickerCatalogs().map((c) => projectPickerCatalog(c, { detail: opts.detail, images: opts.images }))
1016
1064
  }
package/src/styling.ts CHANGED
@@ -467,6 +467,24 @@ export const STYLING_DIMENSION_ORDER: ReadonlyArray<StylingDimension> = [
467
467
  "wardrobe-state",
468
468
  ]
469
469
 
470
+ /**
471
+ * The Styling picker's topics, in display order (mirrors PERSON_DIMENSION_SECTIONS):
472
+ * every dimension belongs to exactly one — styling-sections.test.ts fails the build
473
+ * when a new dimension is left out or listed twice. The editor lays the open
474
+ * picker out by these, and the API serves them as the catalog's `sections`.
475
+ */
476
+ export interface StylingDimensionSection {
477
+ readonly label: string
478
+ readonly dimensions: ReadonlyArray<StylingDimension>
479
+ }
480
+
481
+ export const STYLING_DIMENSION_SECTIONS: ReadonlyArray<StylingDimensionSection> = [
482
+ { label: "Beauty & Hair", dimensions: ["makeup", "hair-cut", "hair-treatment", "hair-state", "nails", "face-paint"] },
483
+ { label: "Accessories", dimensions: ["eyewear", "headwear", "jewelry"] },
484
+ { label: "Wardrobe", dimensions: ["outfit", "top", "bottom", "outerwear", "legwear", "footwear"] },
485
+ { label: "Fabric & Fit", dimensions: ["fabric", "wardrobe-state"] },
486
+ ]
487
+
470
488
  export const STYLING_DIMENSION_LABELS: Readonly<Record<StylingDimension, string>> = {
471
489
  makeup: "Makeup",
472
490
  eyewear: "Eyewear",
@@ -159,15 +159,15 @@ export const TRANSITIONS: ReadonlyArray<Transition> = [
159
159
  { id: "magic-sparkles", label: "Magic Sparkles", category: "element", description: "Particle dissolve à la Avengers / apparition",
160
160
  promptHint: "the subject disintegrates into a cloud of glowing golden sparkles that scatter outward, then the sparkles converge from across the frame and re-coalesce into the new subject" },
161
161
  { id: "lightning-flash", label: "Lightning Strike", category: "element", description: "Lightning strikes, scene changes in flash",
162
- promptHint: "a brilliant white lightning bolt cracks across the frame with a bright flash, and when the flash subsides the scene has changed to the new setting" },
162
+ promptHint: "a brilliant jagged bolt of lightning cracks across the frame and its flash turns the whole picture white. The camera stays where it is and the framing does not change. As the flash dies away, the second shot is revealed in its place. The shot ends on the second shot, fully resolved, with no bolt or flash left. The change happens inside the flash, and the second shot appears only as the white fades" },
163
163
  { id: "ink-splash", label: "Ink Splash", category: "element", description: "Ink splashes across, scene changes",
164
164
  promptHint: "black ink splashes across the frame in expanding tendrils that fully cover the image, then the ink retracts inward and pulls back to reveal the new scene" },
165
165
  { id: "sand-storm", label: "Sand Storm", category: "element", description: "Sand storm engulfs the frame, scene changes inside",
166
- promptHint: "a violent sand storm sweeps in from the side and engulfs the frame in opaque swirling ochre dust, and as the wind dies and the dust settles the second scene is revealed in the now-clear air" },
166
+ promptHint: "a wall of opaque, swirling ochre sand sweeps in from one side of the frame and surges across it until the whole picture is hidden in dust. The camera stays where it is and the framing does not change. The dust then thins and sinks away toward the lower edge of the frame, revealing the second shot behind it. The shot ends on the second shot, clear and fully resolved, with no dust left. The first shot stays as it is until the sand covers it completely, and the second shot appears only as the dust clears" },
167
167
  { id: "paint-splash", label: "Paint Splash", category: "element", description: "Vivid paint splash covers, retracts into new scene",
168
- promptHint: "a vivid splash of colored paint hurls across the frame in arcing tendrils until it fully covers the image, then the paint flows and retracts inward to pull back and reveal the new scene" },
168
+ promptHint: "vivid splashes of coloured paint fly across the frame in arcing streaks and pile over one another until the whole picture is covered in wet paint. The camera stays where it is and the framing does not change. The paint then gathers toward the centre of the frame and shrinks to nothing, uncovering the second shot from the edges inward. The shot ends on the second shot, clean and fully resolved, with no paint left. The paint lies on the surface of the picture itself, and the second shot appears only where the paint has gone" },
169
169
  { id: "aurora-sweep", label: "Aurora Sweep", category: "element", description: "Aurora curtain sweeps across, scene changes behind",
170
- promptHint: "a luminous green and violet aurora curtain ripples across the entire frame, the bright bands obscure the first scene, and as the aurora dissipates the second scene resolves in the clear sky" },
170
+ promptHint: "a luminous curtain of green and violet aurora light ripples across the whole frame, and its bright bands veil the first shot. The camera stays where it is and the framing does not change. As the bands fade, the second shot is revealed behind them. The shot ends on the second shot, clear and fully resolved, with no aurora light left. The aurora glows over the front of the picture, and the second shot appears only as it fades" },
171
171
  { id: "sakura-petals", label: "Sakura Storm", category: "element", description: "Cherry blossom petals storm across the frame",
172
172
  promptHint: "a dense storm of cherry blossom petals swirls in from one side and fills the frame in soft pink motion, the petals cluster to fully veil the image, then drift past to reveal the new scene", term: "cherry blossom petal storm" },
173
173
  { id: "garden-bloom", label: "Garden Bloom", category: "element", description: "Flowers bloom outward, parting to reveal new scene",
@@ -187,7 +187,7 @@ export const TRANSITIONS: ReadonlyArray<Transition> = [
187
187
  { id: "origami-fold", label: "Origami Fold", category: "morph", description: "Subject folds like paper into new subject",
188
188
  promptHint: "the first subject creases and folds like sheets of origami paper, the folds rotate and re-arrange in elegant geometric steps, and the final fold reveals the new subject" },
189
189
  { id: "vortex-swirl", label: "Vortex Swirl", category: "morph", description: "Subject swirls into vortex, unwinds as new",
190
- promptHint: "the first subject spirals inward into a tight vortex at the center of the frame, the vortex briefly compresses to a point, then unwinds outward into the new subject" },
190
+ promptHint: "only the first subject twists, winding around its own centre like wrung cloth into a tight narrow column at the same place in the frame, turning faster as it narrows, while the surroundings stay upright and still. the camera is locked on a tripod head planted in one spot, holding the frame level from start to finish. the column then unwinds in the same direction and opens into the shape of the second subject, while the second shot appears around it. the shot ends on the second subject, solid and fully resolved, with nothing left turning. the twist stays inside the outline of the subject, and the edges of the frame stay square and still" },
191
191
  { id: "dream-ripple", label: "Dream Ripple", category: "morph", description: "Surface ripple wave reveals new scene",
192
192
  promptHint: "a circular ripple radiates outward across the frame as if the image were the surface of water, and where the ripple passes the first scene is replaced by the second scene" },
193
193
  { id: "wireframe-morph", label: "Wireframe Morph", category: "morph", description: "Subject reduces to wireframe, reforms as new subject",
@@ -207,21 +207,21 @@ export const TRANSITIONS: ReadonlyArray<Transition> = [
207
207
  { id: "zoom-into-screen", label: "Zoom Into Screen", category: "portal", description: "Push into TV/phone screen",
208
208
  promptHint: "the camera pushes toward a screen visible in the scene, such as a TV, phone or monitor, the screen's image fills the frame, and the camera passes through into that image which becomes the new scene" },
209
209
  { id: "zoom-into-book", label: "Zoom Into Book", category: "portal", description: "Push into book page illustration",
210
- promptHint: "the camera pushes down into an illustrated page in a book, the illustration grows to fill the frame, and the illustration comes alive as the new scene" },
210
+ promptHint: "the camera pushes down toward the illustrated page of an open book in the frame until the illustration fills the whole picture. The camera travels in one smooth line, without turning or rolling. The drawn picture comes alive, gaining depth, light and movement, and becomes the second shot. The shot ends inside the second shot, real and fully resolved, with no paper, ink lines or page edges left. The camera heads for the page from the start and goes into its illustration" },
211
211
  { id: "walk-through-door", label: "Walk Through Doorway", category: "portal", description: "Through doorway into new scene",
212
- promptHint: "the camera follows the subject through a doorway, and the space on the other side is the new scene — different location entirely, lit differently" },
212
+ promptHint: "the camera follows the first subject through a doorway in the frame, moving forward at the subject's pace. The camera travels straight forward, without turning, tilting or rolling. On the far side of the doorway the space is the second shot, a different place in different light. The shot ends in the second shot, fully resolved, with the doorway behind the camera. The change of place happens at the doorway itself, the moment the camera passes through it" },
213
213
  { id: "fall-into-hole", label: "Fall Into Hole", category: "portal", description: "Camera falls through opening",
214
214
  promptHint: "the floor or ground opens beneath the camera and the camera falls downward through the opening, tumbling, and emerges into the new scene below" },
215
215
  { id: "pull-out-reveal", label: "Pull-Out Reveal", category: "portal", description: "Reveals scene was a picture in larger context",
216
- promptHint: "the camera pulls back rapidly and reveals that the entire first scene was actually contained within a picture, painting, screen, or window in a larger second scene", term: "pull-back reveal" },
216
+ promptHint: "the camera pulls straight back fast, and the whole first shot shrinks until its edges show as the border of a framed picture inside a larger space. The camera travels back in one smooth line, without turning, tilting or rolling. The larger space around the picture is the second shot, and the first shot stays inside the picture. The shot ends on the second shot, with the first shot visible as a picture within it. The first shot's image stays exactly the same as it shrinks, so it reads as the same picture all along", term: "pull-back reveal" },
217
217
  { id: "zoom-into-mouth", label: "Zoom Into Mouth", category: "portal", description: "Push into open mouth, emerges in new world inside",
218
- promptHint: "the camera pushes into the subject's open mouth, the dark interior fills the frame, and the camera passes through into the new scene which materialises as if emerging from inside the body" },
218
+ promptHint: "the camera pushes straight into the first subject's open mouth until the dark interior fills the whole picture. The camera travels forward in one smooth line, without turning, tilting or rolling. It passes through the darkness, and the second shot emerges out of it. The shot ends inside the second shot, still and fully resolved, with no mouth left in view. The dark interior stays a plain darkness the camera passes through" },
219
219
  { id: "push-through-glass", label: "Push Through Glass", category: "portal", description: "Camera pushes through pane of glass into new world",
220
220
  promptHint: "the camera pushes toward a pane of glass in the scene, the surface ripples like liquid as the camera passes through with a faint refraction, and the space on the other side resolves as the new scene" },
221
221
  { id: "soul-jump", label: "Soul Jump", category: "portal", description: "Translucent soul leaves body, enters new body",
222
222
  promptHint: "a translucent luminous form rises out of the first subject's body and shoots forward through the frame as a ghost-like soul, then dives into a new body in the new scene where the second subject animates to life", term: "soul leaves body and enters another" },
223
223
  { id: "mask-transition", label: "Mask Transition", category: "portal", description: "Foreground object blacks out the frame, camera pulls through",
224
- promptHint: "mask transition: a foreground object or a passer-by sweeps across the lens and fills the frame with darkness, the camera keeps travelling forward through the black, then pulls out of the darkness into the new scene", term: "mask transition" },
224
+ promptHint: "a dark foreground shape sweeps across the lens and fills the frame with black. The camera keeps travelling forward through the black in one smooth line. It emerges from the darkness into the second shot. The shot ends in the second shot, fully resolved, with no dark shape left. The dark shape passes close to the lens and is gone once the second shot appears", term: "mask transition" },
225
225
  { id: "zoom-through", label: "Zoom Through", category: "portal", description: "Camera magnifies one detail until the new scene unfolds inside it",
226
226
  promptHint: "zoom-through transition: the camera magnifies one small detail of the frame further and further until the detail loses its texture and fills the image entirely, and the new scene unfolds from within it", term: "zoom-through transition" },
227
227
 
@@ -231,7 +231,7 @@ export const TRANSITIONS: ReadonlyArray<Transition> = [
231
231
  { id: "explosion-blast", label: "Explosion Blast", category: "physics", description: "Explosion wipes frame, new scene emerges",
232
232
  promptHint: "an explosion erupts from the center of the frame with a bright fireball that expands to fill the frame, and as the fireball dissipates the new scene is revealed" },
233
233
  { id: "shockwave", label: "Shockwave", category: "physics", description: "Shockwave ripples across, scene changes",
234
- promptHint: "a visible shockwave ripples outward across the frame distorting the image, and as the shockwave passes the scene behind it has changed to the new setting" },
234
+ promptHint: "a flash at the exact centre of the frame bursts into a sharp, bright ring that races past every edge in a moment, trailing a smear of motion blur behind its rim and warping the picture as it goes. The camera stays where it is and the picture stays level. The second shot shows only inside the ring and the first only outside it, with the bright ring as the one hard border and no blending anywhere" },
235
235
  { id: "punch-into-camera", label: "Punch Into Camera", category: "physics", description: "Fist strikes camera, scene changes",
236
236
  promptHint: "a fist or object swings rapidly toward the camera and strikes the lens with motion blur and impact frames, and the moment of impact reveals the new scene" },
237
237
  { id: "debris-shower", label: "Debris Shower", category: "physics", description: "Debris flies past, scene changes behind",
@@ -239,9 +239,9 @@ export const TRANSITIONS: ReadonlyArray<Transition> = [
239
239
  { id: "gravity-flip", label: "Gravity Flip", category: "physics", description: "Gravity inverts, camera rotates 180",
240
240
  promptHint: "gravity inverts and the camera rotates a full 180 degrees as objects and the subject reorient to the new down, settling into the new scene oriented correctly" },
241
241
  { id: "building-explosion", label: "Building Explosion", category: "physics", description: "Structure detonates, scene shifts through smoke",
242
- promptHint: "a large structure in the frame detonates with a massive fireball, debris and dust pluming outward to fill the image, and as the smoke clears the new scene is revealed where the structure used to stand" },
242
+ promptHint: "the largest structure in the frame detonates in a massive fireball, and debris and dust plume outward until they fill the whole picture. The camera stays where it is and the framing does not change. As the dust cloud clears, the second shot is revealed in its place. The shot ends on the second shot, clear and fully resolved, with no dust or debris left. The blast comes from that structure itself, and the second shot appears only as the dust clears" },
243
243
  { id: "vehicle-explosion", label: "Vehicle Explosion", category: "physics", description: "Vehicle detonates in foreground, scene changes behind",
244
- promptHint: "a vehicle in the foreground erupts in a violent explosion of fire and twisted metal, the fireball expands toward the camera and washes the frame in orange flame, and as the smoke parts the second scene resolves" },
244
+ promptHint: "a vehicle in the frame bursts into a violent explosion of fire and twisted metal, and the fireball billows toward the lens until orange flame fills the whole picture. The camera stays where it is and the framing does not change. The flame gives way to thick smoke, and as the smoke parts the second shot is revealed. The shot ends on the second shot, clear and fully resolved, with no fire or smoke left. The explosion comes from that vehicle itself, and the second shot appears only as the smoke parts" },
245
245
  { id: "jump-match", label: "Jump Match", category: "physics", description: "Subject jumps, landing matches into new scene",
246
246
  promptHint: "the subject jumps upward and out of frame at the end of the first shot, with matched velocity the camera follows the arc, and on landing the subject is in a new location seamlessly continuing the same jump", term: "match cut on a jump" , instant: true },
247
247
  { id: "hand-swipe", label: "Hand Swipe", category: "physics", description: "Hand swipes across lens, scene changes during occlusion",
@@ -261,9 +261,9 @@ export const TRANSITIONS: ReadonlyArray<Transition> = [
261
261
  { id: "color-invert", label: "Color Invert Flash", category: "light", description: "Colors invert briefly",
262
262
  promptHint: "colors invert across the entire frame to their negative for a single beat, and when the colors snap back the scene has changed to the new setting" },
263
263
  { id: "sun-glare", label: "Sun Glare", category: "light", description: "Sun glare washes frame",
264
- promptHint: "intense sun glare overwhelms the lens with bright lens flares and bloom that wash out the frame, and as the glare dissipates the new scene resolves" },
264
+ promptHint: "intense warm glare floods the lens from one corner of the frame, blooming and scattering flares until the whole picture is washed out to a bright haze. The camera stays where it is and the framing does not change. As the glare fades, the second shot emerges through the haze. The shot ends on the second shot, clear and fully resolved, with no glare left. The glare comes from the lens itself, and the second shot appears only as the haze clears" },
265
265
  { id: "lens-crack", label: "Lens Crack", category: "light", description: "Lens cracks, scene through fractured glass",
266
- promptHint: "a hairline crack races diagonally across the camera lens with a sharp visual snap, multiple fracture lines branch outward, and the second scene resolves through the cracked glass before the fractures fade away" },
266
+ promptHint: "a hairline crack snaps diagonally across the lens and branches into a web of fracture lines that spreads over the whole picture. The camera stays where it is and the framing does not change. Seen through the cracks, the first shot gives way to the second shot, and then the fracture lines fade away. The shot ends on the second shot, clear and fully resolved, with no cracks left. The cracks form on the lens itself, while everything behind them stays whole" },
267
267
  { id: "dirty-lens-wipe", label: "Dirty Lens Wipe", category: "light", description: "Lens dust/grime wipes clean, scene changes",
268
268
  promptHint: "the camera lens is suddenly streaked with dust, water beads, and grime that swirl across the front element, a wiping motion sweeps the lens clean from one side to the other, revealing the new scene in crisp focus" },
269
269
  { id: "eye-light-burst", label: "Eye Light Burst", category: "light", description: "Bright beam from subject's eyes whites out frame",