@nodaro/prompts 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +27 -0
  3. package/dist/index.cjs +21240 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +3599 -0
  6. package/dist/index.d.ts +3599 -0
  7. package/dist/index.js +20854 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +49 -0
  10. package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
  11. package/src/__tests__/action-fx.test.ts +155 -0
  12. package/src/__tests__/apply-picker-json.test.ts +50 -0
  13. package/src/__tests__/assemble-image-input.test.ts +248 -0
  14. package/src/__tests__/assemble-suno-input.test.ts +283 -0
  15. package/src/__tests__/brand-tokens.test.ts +81 -0
  16. package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
  17. package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
  18. package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
  19. package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
  20. package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
  21. package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
  22. package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
  23. package/src/__tests__/catalog-gapfill.test.ts +53 -0
  24. package/src/__tests__/character-convergence-image.test.ts +217 -0
  25. package/src/__tests__/character-default-role-image.test.ts +167 -0
  26. package/src/__tests__/character-default-role-video.test.ts +168 -0
  27. package/src/__tests__/character-default-role.test.ts +70 -0
  28. package/src/__tests__/character-fx.test.ts +200 -0
  29. package/src/__tests__/entity-prompts-location.test.ts +97 -0
  30. package/src/__tests__/entity-prompts.test.ts +240 -0
  31. package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
  32. package/src/__tests__/factory-presets.test.ts +1045 -0
  33. package/src/__tests__/factory-snippets.test.ts +68 -0
  34. package/src/__tests__/framing-multi.test.ts +71 -0
  35. package/src/__tests__/framing-vantage.test.ts +39 -0
  36. package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
  37. package/src/__tests__/identity-lock.test.ts +46 -0
  38. package/src/__tests__/instrumentation.test.ts +68 -0
  39. package/src/__tests__/lighting-multi.test.ts +68 -0
  40. package/src/__tests__/location-convergence-image.test.ts +124 -0
  41. package/src/__tests__/mention-lock-flag.test.ts +499 -0
  42. package/src/__tests__/multi-picker-spec.test.ts +71 -0
  43. package/src/__tests__/music-genre.test.ts +103 -0
  44. package/src/__tests__/music-mood.test.ts +87 -0
  45. package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
  46. package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
  47. package/src/__tests__/parameter-registry-sync.test.ts +243 -0
  48. package/src/__tests__/person-age.test.ts +80 -0
  49. package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
  50. package/src/__tests__/person-body-axes.test.ts +67 -0
  51. package/src/__tests__/person-facial-geometry.test.ts +156 -0
  52. package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
  53. package/src/__tests__/person-sections.test.ts +18 -0
  54. package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
  55. package/src/__tests__/picker-catalogs-project.test.ts +85 -0
  56. package/src/__tests__/picker-catalogs.test.ts +53 -0
  57. package/src/__tests__/picker-limits.test.ts +20 -0
  58. package/src/__tests__/prompt-builder-segments.test.ts +183 -0
  59. package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
  60. package/src/__tests__/prompt-builder.test.ts +1773 -0
  61. package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
  62. package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
  63. package/src/__tests__/resolve-prompt-append.test.ts +58 -0
  64. package/src/__tests__/resolve-prompt.test.ts +44 -0
  65. package/src/__tests__/role-picker-shared.test.ts +208 -0
  66. package/src/__tests__/seedance-2-inputs.test.ts +173 -0
  67. package/src/__tests__/seedance-extend.test.ts +44 -0
  68. package/src/__tests__/sound-aggregator.test.ts +350 -0
  69. package/src/__tests__/style-presets.test.ts +34 -0
  70. package/src/__tests__/temporal-multi.test.ts +74 -0
  71. package/src/__tests__/transitions.test.ts +213 -0
  72. package/src/__tests__/video-reference-features.test.ts +41 -0
  73. package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
  74. package/src/__tests__/video-reference-resolver.test.ts +233 -0
  75. package/src/__tests__/video-reference-roles.test.ts +96 -0
  76. package/src/__tests__/voice-character.test.ts +48 -0
  77. package/src/__tests__/voice-delivery.test.ts +41 -0
  78. package/src/__tests__/wardrobe.test.ts +25 -0
  79. package/src/action-fx.ts +255 -0
  80. package/src/aesthetic.ts +435 -0
  81. package/src/assemble-image-input.ts +236 -0
  82. package/src/assemble-suno-input.ts +147 -0
  83. package/src/atmosphere.ts +104 -0
  84. package/src/backdrop.ts +131 -0
  85. package/src/brand-tokens.ts +154 -0
  86. package/src/camera-format.ts +76 -0
  87. package/src/camera-motions.ts +615 -0
  88. package/src/character-fx.ts +274 -0
  89. package/src/color-look.ts +103 -0
  90. package/src/composition-effects.ts +67 -0
  91. package/src/entity-prompts.ts +231 -0
  92. package/src/era.ts +292 -0
  93. package/src/exposure-settings.ts +142 -0
  94. package/src/factory-presets/generate-image.ts +1644 -0
  95. package/src/factory-presets/generate-video.ts +1116 -0
  96. package/src/factory-presets/index.ts +46 -0
  97. package/src/factory-presets/lottie-overlay.ts +166 -0
  98. package/src/factory-presets/motion-graphics.ts +350 -0
  99. package/src/factory-presets/music.ts +734 -0
  100. package/src/factory-presets/sfx.ts +136 -0
  101. package/src/factory-presets/shared-image.ts +207 -0
  102. package/src/factory-presets/switchx.ts +65 -0
  103. package/src/factory-presets/text.ts +292 -0
  104. package/src/factory-presets/types.ts +51 -0
  105. package/src/factory-presets/video-edit.ts +136 -0
  106. package/src/factory-presets/voice.ts +172 -0
  107. package/src/factory-presets.ts +2 -0
  108. package/src/factory-snippets/catalog.ts +105 -0
  109. package/src/factory-snippets/index.ts +16 -0
  110. package/src/factory-snippets/types.ts +33 -0
  111. package/src/framing.ts +634 -0
  112. package/src/held-prop.ts +187 -0
  113. package/src/identity-lock.ts +213 -0
  114. package/src/index.ts +66 -0
  115. package/src/instrumentation.ts +337 -0
  116. package/src/lens.ts +59 -0
  117. package/src/lighting.ts +229 -0
  118. package/src/loop-subject.ts +276 -0
  119. package/src/materials.ts +184 -0
  120. package/src/mood.ts +186 -0
  121. package/src/music-genre.ts +662 -0
  122. package/src/music-mood.ts +137 -0
  123. package/src/object-asset-presets.ts +81 -0
  124. package/src/parameter-prompt-hint.ts +281 -0
  125. package/src/person.ts +1368 -0
  126. package/src/photo-genre.ts +151 -0
  127. package/src/photographer.ts +612 -0
  128. package/src/picker-analyzer-registry.ts +374 -0
  129. package/src/picker-catalogs.ts +858 -0
  130. package/src/pose.ts +246 -0
  131. package/src/post-process-effects.ts +94 -0
  132. package/src/prompt-builder-structured-fields.ts +116 -0
  133. package/src/prompt-builder.ts +2964 -0
  134. package/src/prompt-templates.ts +50 -0
  135. package/src/prompt-wizard-categories.ts +334 -0
  136. package/src/provider-prompt-doctrine.ts +85 -0
  137. package/src/render-quality.ts +89 -0
  138. package/src/resolve-prompt.ts +120 -0
  139. package/src/seedance-2-inputs.ts +100 -0
  140. package/src/setting.ts +130 -0
  141. package/src/sound-aggregator.ts +241 -0
  142. package/src/style-presets.ts +162 -0
  143. package/src/style.ts +99 -0
  144. package/src/styling.ts +585 -0
  145. package/src/temporal.ts +151 -0
  146. package/src/transitions.ts +333 -0
  147. package/src/video-reference-resolver.ts +823 -0
  148. package/src/voice-character.ts +237 -0
  149. package/src/voice-delivery.ts +142 -0
  150. package/src/wardrobe.ts +179 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Canonical catalog of temporal modifiers.
3
+ *
4
+ * Temporal is the speed / freeze / direction / shutter dimension of a video —
5
+ * how time flows through the shot. Distinct from the post-process Speed Ramp
6
+ * FFmpeg node (which operates on an existing rendered video); these are
7
+ * prompt-time guidance hints for the generator model so it produces footage
8
+ * that already embeds the temporal intent. Independent of lighting, color,
9
+ * lens, camera motion, etc.
10
+ *
11
+ * Video-only: stills can't have motion or a speed, so image-gen consumers
12
+ * don't read this field.
13
+ *
14
+ * Shared between the picker UI and the prompt-hint injection on both the
15
+ * frontend DAG executor and the backend orchestrator.
16
+ */
17
+
18
+ export type TemporalCategory = "speed" | "freeze" | "direction" | "shutter"
19
+
20
+ export interface Temporal {
21
+ readonly id: string
22
+ readonly label: string
23
+ readonly category: TemporalCategory
24
+ readonly description: string
25
+ readonly promptHint: string
26
+ }
27
+
28
+ export const TEMPORALS: ReadonlyArray<Temporal> = [
29
+ // Speed (6)
30
+ { id: "real-time", label: "Real-time", category: "speed", description: "Normal playback speed", promptHint: "real-time playback, normal speed" },
31
+ { id: "slow-motion", label: "Slow Motion", category: "speed", description: "Moderately slowed footage", promptHint: "slow motion, footage slowed down with smooth deliberate movement" },
32
+ { id: "super-slow-mo", label: "Super Slow-mo", category: "speed", description: "Extremely slow footage", promptHint: "super slow motion, extreme high-speed slow-mo capturing motion at a fraction of real time" },
33
+ { id: "time-lapse", label: "Time-lapse", category: "speed", description: "Compressed time, fast passage", promptHint: "time-lapse, highly compressed time showing hours or days passing in seconds" },
34
+ { id: "hyper-lapse", label: "Hyper-lapse", category: "speed", description: "Moving time-lapse", promptHint: "hyper-lapse, time-lapse combined with forward camera motion, accelerated movement through space" },
35
+ { id: "speed-ramp", label: "Speed Ramp", category: "speed", description: "Dynamic speed change mid-shot", promptHint: "speed ramp, dynamic speed change within the shot from slow motion to real-time or faster" },
36
+
37
+ // Freeze (4)
38
+ { id: "full-freeze", label: "Full Freeze-frame", category: "freeze", description: "All motion frozen", promptHint: "full freeze-frame, all motion in the scene completely frozen like a photograph" },
39
+ { id: "bullet-time", label: "Bullet Time", category: "freeze", description: "Subject frozen, camera orbits", promptHint: "bullet time effect, subject completely frozen mid-motion while camera orbits around them, Matrix-style" },
40
+ { id: "frozen-subject", label: "Frozen Subject", category: "freeze", description: "Subject frozen, world moves", promptHint: "frozen subject with moving world, subject remains completely still while the environment continues in motion" },
41
+ { id: "moving-subject", label: "Moving Subject", category: "freeze", description: "Subject moves, world frozen", promptHint: "moving subject with frozen world, subject continues in motion while everything else in the scene is completely frozen" },
42
+
43
+ // Direction (3)
44
+ { id: "forward", label: "Forward", category: "direction", description: "Normal forward playback", promptHint: "forward playback, time moving forward in the natural direction" },
45
+ { id: "reverse", label: "Reverse / Rewind", category: "direction", description: "Time plays backwards", promptHint: "reverse playback, time and motion running backwards" },
46
+ { id: "loop-boomerang", label: "Loop / Boomerang", category: "direction", description: "Forward then reverse", promptHint: "boomerang loop, playing forward then reversing back, creating a looping motion" },
47
+
48
+ // Shutter (5)
49
+ { id: "long-exposure", label: "Long Exposure", category: "shutter", description: "Motion trails and streaks", promptHint: "long exposure effect, motion trails and light streaks from slow shutter speed" },
50
+ { id: "crisp-shutter", label: "Crisp Shutter", category: "shutter", description: "Sharp motion, no blur", promptHint: "crisp shutter, sharp motion captured with fast shutter speed and no motion blur" },
51
+ { id: "motion-blur", label: "Motion Blur", category: "shutter", description: "Pronounced directional blur", promptHint: "pronounced motion blur, moving subjects blur directionally from slower-than-normal shutter speed" },
52
+ { id: "stutter-strobe", label: "Stutter / Strobe", category: "shutter", description: "Strobe-effect jerky motion", promptHint: "stutter or strobe effect, jerky discontinuous motion with visible frame steps" },
53
+ { id: "stop-motion", label: "Stop-motion", category: "shutter", description: "Stepped frame-by-frame motion", promptHint: "stop-motion animation style, stepped frame-by-frame motion with characteristic discrete movement" },
54
+ ] as const
55
+
56
+ export const TEMPORAL_CATEGORY_ORDER: ReadonlyArray<TemporalCategory> = [
57
+ "speed",
58
+ "freeze",
59
+ "direction",
60
+ "shutter",
61
+ ]
62
+
63
+ export const TEMPORAL_CATEGORY_LABELS: Record<TemporalCategory, string> = {
64
+ speed: "Speed",
65
+ freeze: "Freeze",
66
+ direction: "Direction",
67
+ shutter: "Shutter",
68
+ }
69
+
70
+ const temporalById = new Map<string, Temporal>(TEMPORALS.map((t) => [t.id, t]))
71
+
72
+ export function getTemporal(id: string | undefined | null): Temporal | undefined {
73
+ if (!id) return undefined
74
+ return temporalById.get(id)
75
+ }
76
+
77
+ export function getTemporalLabel(id: string | undefined | null, fallback?: string): string {
78
+ const t = getTemporal(id)
79
+ if (t) return t.label
80
+ if (fallback !== undefined) return fallback
81
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
82
+ }
83
+
84
+ export function getTemporalPromptHint(id: string | undefined | null): string {
85
+ return getTemporal(id)?.promptHint ?? ""
86
+ }
87
+
88
+ export const TEMPORAL_IDS: ReadonlyArray<string> = TEMPORALS.map((t) => t.id)
89
+
90
+ /**
91
+ * Maps each TemporalCategory to the consumer data field name that holds the
92
+ * selected entry id for that category. Multi-category temporal: a consumer
93
+ * (video-only) can independently set a value in each of the 4 dimensions.
94
+ *
95
+ * Field names use 'temporal' prefix throughout to avoid collisions: 'direction'
96
+ * collides with Lighting's 'direction' category; 'speed', 'shutter', and
97
+ * 'freeze' are generic vocabulary.
98
+ */
99
+ export const TEMPORAL_FIELD_BY_CATEGORY: Record<
100
+ TemporalCategory,
101
+ "temporalSpeed" | "temporalFreeze" | "temporalDirection" | "temporalShutter"
102
+ > = {
103
+ speed: "temporalSpeed",
104
+ freeze: "temporalFreeze",
105
+ direction: "temporalDirection",
106
+ shutter: "temporalShutter",
107
+ }
108
+
109
+ /**
110
+ * Shape of the per-category temporal fields on TemporalData and the 5 video
111
+ * consumer data types. All fields optional — user may set zero, one, or all
112
+ * categories.
113
+ */
114
+ export interface TemporalValue {
115
+ temporalSpeed?: string
116
+ temporalFreeze?: string
117
+ temporalDirection?: string
118
+ temporalShutter?: string
119
+ }
120
+
121
+ const TEMPORAL_FIELDS_IN_ORDER: ReadonlyArray<readonly [keyof TemporalValue, TemporalCategory]> =
122
+ TEMPORAL_CATEGORY_ORDER.map((cat) => [TEMPORAL_FIELD_BY_CATEGORY[cat], cat] as const)
123
+
124
+ /**
125
+ * Aggregate all enabled per-category temporal prompt hints from a consumer's
126
+ * data, in canonical category order (speed, freeze, direction, shutter).
127
+ *
128
+ * Accepts a loosely typed record (the helper is shared between strongly typed
129
+ * frontend node data and the backend's `Record<string, unknown>` workflow
130
+ * data). Non-string values are ignored.
131
+ *
132
+ * @param data the consumer data record (must include optional temporalSpeed /
133
+ * temporalFreeze / temporalDirection / temporalShutter fields)
134
+ */
135
+ export function buildTemporalHints(
136
+ data: Record<string, unknown> & {
137
+ temporalSpeed?: unknown
138
+ temporalFreeze?: unknown
139
+ temporalDirection?: unknown
140
+ temporalShutter?: unknown
141
+ },
142
+ ): string[] {
143
+ const hints: string[] = []
144
+ for (const [field] of TEMPORAL_FIELDS_IN_ORDER) {
145
+ const id = data[field]
146
+ if (typeof id !== "string" || id.length === 0) continue
147
+ const hint = getTemporalPromptHint(id)
148
+ if (hint) hints.push(hint)
149
+ }
150
+ return hints
151
+ }
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Canonical catalog of cinematic transitions for AI-video generation.
3
+ *
4
+ * Shared between frontend (picker UI, prompt hint injection) and backend
5
+ * (orchestrator payload builder). The `promptHint` is a natural-language
6
+ * cue that gets composed into the user prompt when a transition node is
7
+ * connected to a video consumer.
8
+ *
9
+ * Multi-pick supported: value field accepts `string | string[]` (cap 2).
10
+ * Graph-aware: `startState` / `endState` input handles accept upstream
11
+ * parameter nodes whose hints are folded into the composed clause as
12
+ * "starting from <X>, ending at <Y>".
13
+ */
14
+
15
+ export type TransitionCategory =
16
+ | "standard"
17
+ | "time"
18
+ | "element"
19
+ | "morph"
20
+ | "portal"
21
+ | "physics"
22
+ | "light"
23
+ | "glitch"
24
+
25
+ export interface Transition {
26
+ readonly id: string
27
+ readonly label: string
28
+ readonly category: TransitionCategory
29
+ readonly description: string
30
+ readonly promptHint: string
31
+ }
32
+
33
+ export type TransitionPosition = "auto" | "start" | "middle" | "end" | "full"
34
+ export type TransitionDuration = "auto" | "instant" | "short" | "medium" | "long"
35
+ export type TransitionIntensity = "auto" | "subtle" | "natural" | "dynamic" | "crazy"
36
+
37
+ export interface TransitionTiming {
38
+ position?: TransitionPosition
39
+ duration?: TransitionDuration
40
+ intensity?: TransitionIntensity
41
+ }
42
+
43
+ export const TRANSITIONS: ReadonlyArray<Transition> = [
44
+ // ============================================================================
45
+ // STANDARD — 11 entries — classical editing transitions
46
+ // ============================================================================
47
+ { id: "auto", label: "Auto", category: "standard", description: "Let the model choose", promptHint: "" },
48
+ { id: "none", label: "None / Hard Cut", category: "standard", description: "Instantaneous switch, no transition",
49
+ promptHint: "no transition, hard cut, instantaneous switch from first shot to second shot" },
50
+ { id: "cross-dissolve", label: "Cross-Dissolve", category: "standard", description: "Gradual blend between shots",
51
+ promptHint: "smooth cross-dissolve transition where the first shot gradually fades out as the second shot fades in" },
52
+ { id: "fade-to-black", label: "Fade to Black", category: "standard", description: "Darkens to black, second emerges",
53
+ promptHint: "fade to black: the first shot gradually darkens to full black, holds briefly, then the second shot fades up from black" },
54
+ { id: "fade-to-white", label: "Fade to White", category: "standard", description: "Blooms to white, second emerges",
55
+ promptHint: "fade to white: the first shot brightens until the frame is pure white, then the second shot resolves out of the white" },
56
+ { id: "match-cut", label: "Match Cut", category: "standard", description: "Shape or motion match across shots",
57
+ promptHint: "match cut: the final composition of the first shot matches the opening composition of the second shot in shape, color, and motion, so the cut feels like a visual rhyme" },
58
+ { id: "smash-cut", label: "Smash Cut", category: "standard", description: "Jarring abrupt cut between contrasting shots",
59
+ promptHint: "smash cut: an abrupt jarring transition between two visually or tonally contrasting shots with no fade, on a beat" },
60
+ { id: "iris", label: "Iris", category: "standard", description: "Circular iris closes, then opens on second",
61
+ promptHint: "iris transition: a circular vignette closes inward over the first shot until the frame is black, then opens outward to reveal the second shot" },
62
+ { id: "wipe", label: "Wipe", category: "standard", description: "Linear wipe replaces first shot",
63
+ promptHint: "linear wipe transition: a clean diagonal line sweeps across the frame, revealing the second shot behind it" },
64
+ { id: "roll-transition", label: "Roll", category: "standard", description: "Frame rolls 90-180°, second shot upright on landing",
65
+ promptHint: "the frame rolls along the camera axis with a smooth 90 to 180 degree rotation, motion-blurred during the roll, and as the rotation completes the new shot is upright and stable in frame" },
66
+ { id: "seamless-match", label: "Seamless Match", category: "standard", description: "Hidden cut disguised by matched motion and color",
67
+ promptHint: "hidden seamless transition: the camera motion, color palette, and on-screen motion at the end of the first shot continue exactly across the cut into the second shot, so the boundary is invisible and the two shots feel like one unbroken take" },
68
+
69
+ // ============================================================================
70
+ // TIME — 8 entries — temporal shifts (same or related scene, different time, or memory)
71
+ // ============================================================================
72
+ { id: "fast-forward-day-night", label: "Fast-Forward (Day → Night)", category: "time", description: "Time-lapse day to night same scene",
73
+ promptHint: "fast-forward time-lapse transition: the sun visibly arcs across the sky, shadows sweep, clouds streak, sky shifts from daylight blue through golden hour to deep night, stars emerge, all while framing and camera position remain locked on the same scene" },
74
+ { id: "fast-forward-night-day", label: "Fast-Forward (Night → Day)", category: "time", description: "Time-lapse night to dawn same scene",
75
+ promptHint: "fast-forward time-lapse transition: stars fade, the sky shifts from deep night through pre-dawn blue to golden sunrise, shadows sweep in reverse, all while framing and camera position remain locked on the same scene" },
76
+ { id: "seasonal-shift", label: "Seasonal Shift", category: "time", description: "Same scene through changing seasons",
77
+ promptHint: "accelerated seasonal time-lapse: foliage transitions from spring green to summer lushness to autumn red-gold to winter bare, leaves fall and regrow, snow accumulates and melts, all within the same locked framing" },
78
+ { id: "aging", label: "Aging", category: "time", description: "Subject visibly ages forward in time",
79
+ promptHint: "accelerated aging transition: the subject visibly ages forward — skin develops fine lines then deeper wrinkles, hair lightens to silver, posture shifts subtly, while the framing holds steady on the face" },
80
+ { id: "rewind", label: "Rewind", category: "time", description: "Time reverses, motion plays backward",
81
+ promptHint: "rewind transition: time reverses and all motion plays smoothly backward, water flows up, debris reassembles, the subject's recent actions undo, with a faint VHS-rewind tracking distortion at the edges" },
82
+ { id: "freeze-frame-jump", label: "Freeze-Frame Jump", category: "time", description: "Action freezes, jumps forward in time",
83
+ promptHint: "freeze-frame transition: motion arrests mid-action, the frame holds frozen for a beat, then snaps to a new moment hours or days later in the same scene with subjects in different positions" },
84
+ { id: "weather-shift", label: "Weather Shift", category: "time", description: "Same scene through changing weather",
85
+ promptHint: "accelerated weather transition: same scene, framing locked — clear sky darkens to storm clouds, rain begins and intensifies then clears, sun returns through breaking clouds" },
86
+ { id: "flashback", label: "Flashback", category: "time", description: "Memory-flashback into a past moment of the subject",
87
+ promptHint: "brief flashback transition: the frame washes with a soft warm or desaturated tint, faint ripple distortion crosses the image as the present scene fades, and a remembered earlier moment resolves into focus on the same subject" },
88
+
89
+ // ============================================================================
90
+ // ELEMENT — 14 entries — teleport via natural element
91
+ // ============================================================================
92
+ { id: "dissolve-to-mist", label: "Dissolve to Mist", category: "element", description: "Subject turns to mist, drifts, reforms",
93
+ promptHint: "the subject gradually dissolves into a soft cloud of mist that swirls and drifts across the frame, then the mist re-condenses into the new subject in the new setting" },
94
+ { id: "water-splash", label: "Water Splash", category: "element", description: "Subject becomes water, splashes, reforms",
95
+ promptHint: "the subject liquefies into a cascade of water that splashes and pools, the water then surges upward and re-forms into the new subject in a new setting" },
96
+ { id: "sand-scatter", label: "Sand Scatter", category: "element", description: "Subject becomes sand, blown away, reforms",
97
+ promptHint: "the subject crumbles into fine sand that is swept away by a gust of wind in a swirling vortex, then the sand particles converge and re-form into the new subject" },
98
+ { id: "fire-burnup", label: "Burn-Up", category: "element", description: "Subject burns to embers, embers reform",
99
+ promptHint: "the subject ignites and burns from edges inward into glowing embers and ash, the embers swirl through the frame and re-ignite into the new subject" },
100
+ { id: "smoke-puff", label: "Smoke Puff", category: "element", description: "Subject vanishes in smoke, reappears",
101
+ promptHint: "the subject vanishes in a soft puff of smoke that billows outward and fills the frame, the smoke then clears to reveal the new subject in the new scene" },
102
+ { id: "magic-sparkles", label: "Magic Sparkles", category: "element", description: "Particle dissolve à la Avengers / apparition",
103
+ 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" },
104
+ { id: "lightning-flash", label: "Lightning Strike", category: "element", description: "Lightning strikes, scene changes in flash",
105
+ 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" },
106
+ { id: "ink-splash", label: "Ink Splash", category: "element", description: "Ink splashes across, scene changes",
107
+ 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" },
108
+ { id: "sand-storm", label: "Sand Storm", category: "element", description: "Sand storm engulfs the frame, scene changes inside",
109
+ 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" },
110
+ { id: "paint-splash", label: "Paint Splash", category: "element", description: "Vivid paint splash covers, retracts into new scene",
111
+ 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" },
112
+ { id: "aurora-sweep", label: "Aurora Sweep", category: "element", description: "Aurora curtain sweeps across, scene changes behind",
113
+ 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" },
114
+ { id: "sakura-petals", label: "Sakura Storm", category: "element", description: "Cherry blossom petals storm across the frame",
115
+ 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" },
116
+ { id: "garden-bloom", label: "Garden Bloom", category: "element", description: "Flowers bloom outward, parting to reveal new scene",
117
+ promptHint: "lush flowers and vines rapidly grow and bloom outward from the edges of the frame, the foliage spreads to overtake the entire image, then parts open like curtains to reveal the new scene behind" },
118
+ { id: "powder-burst", label: "Powder Burst", category: "element", description: "Colored powder bursts across frame and clears",
119
+ promptHint: "a burst of vivid colored powder explodes from the center of the frame in slow motion, the cloud of pigment expands to fill the image, then drifts apart and settles to reveal the second scene" },
120
+
121
+ // ============================================================================
122
+ // MORPH — 9 entries — continuous shape-shift
123
+ // ============================================================================
124
+ { id: "liquid-morph", label: "Liquid Morph", category: "morph", description: "Subject melts and reforms as new subject",
125
+ promptHint: "smooth liquid morph: the first subject's surface becomes fluid and continuously deforms, flowing without breaks into the silhouette and details of the second subject" },
126
+ { id: "pixelate-reform", label: "Pixelate & Reform", category: "morph", description: "Pixelates, scatters, reforms as new",
127
+ promptHint: "the first subject pixelates into large mosaic blocks that scatter outward across the frame, then the blocks converge and resolve into the new subject" },
128
+ { id: "shatter-glass", label: "Shatter & Reform", category: "morph", description: "Subject shatters like glass, reforms",
129
+ promptHint: "the first subject shatters like glass into hundreds of shards that fly outward, then the shards reverse direction in reverse time and reassemble into the new subject" },
130
+ { id: "origami-fold", label: "Origami Fold", category: "morph", description: "Subject folds like paper into new subject",
131
+ 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" },
132
+ { id: "vortex-swirl", label: "Vortex Swirl", category: "morph", description: "Subject swirls into vortex, unwinds as new",
133
+ 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" },
134
+ { id: "dream-ripple", label: "Dream Ripple", category: "morph", description: "Surface ripple wave reveals new scene",
135
+ 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" },
136
+ { id: "wireframe-morph", label: "Wireframe Morph", category: "morph", description: "Subject reduces to wireframe, reforms as new subject",
137
+ promptHint: "the first subject's surface peels away to reveal a glowing geometric wireframe of polygons and edges, the wireframe flexes and re-tessellates into the topology of the new subject, then the new surface skins over the wireframe" },
138
+ { id: "polygon-shatter", label: "Polygon Shatter", category: "morph", description: "Subject fragments into low-poly chunks, reassembles",
139
+ promptHint: "the first subject fractures into low-polygon faceted chunks that explode outward in slow motion, the polygons then reverse course and re-assemble in clean geometric flight paths into the silhouette of the new subject" },
140
+ { id: "melt-down", label: "Melt Down", category: "morph", description: "Subject melts into puddle, reforms as new",
141
+ promptHint: "the first subject's form softens and melts downward like wax, collapsing into a glossy puddle on the ground, the puddle then surges upward and re-solidifies into the new subject standing in the new scene" },
142
+
143
+ // ============================================================================
144
+ // PORTAL — 10 entries — zoom-into-object world-jumps
145
+ // ============================================================================
146
+ { id: "zoom-into-eye", label: "Zoom Into Eye", category: "portal", description: "Push into pupil, new world inside",
147
+ promptHint: "the camera pushes into a tight macro of the subject's eye, the pupil dilates and fills the frame, and the new scene materialises from within the pupil as if the pupil itself were a portal" },
148
+ { id: "zoom-into-mirror", label: "Zoom Into Mirror", category: "portal", description: "Push into mirror, scene inside reflection",
149
+ promptHint: "the camera pushes toward a mirror in the scene, the mirror's reflection fills the frame, and the camera passes through the mirror surface into the reflected world which becomes the new scene" },
150
+ { id: "zoom-into-screen", label: "Zoom Into Screen", category: "portal", description: "Push into TV/phone screen",
151
+ promptHint: "the camera pushes toward a screen visible in the scene (TV, phone, monitor), the screen's image fills the frame, and the camera passes through into that image which becomes the new scene" },
152
+ { id: "zoom-into-book", label: "Zoom Into Book", category: "portal", description: "Push into book page illustration",
153
+ 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" },
154
+ { id: "walk-through-door", label: "Walk Through Doorway", category: "portal", description: "Through doorway into new scene",
155
+ 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" },
156
+ { id: "fall-into-hole", label: "Fall Into Hole", category: "portal", description: "Camera falls through opening",
157
+ 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" },
158
+ { id: "pull-out-reveal", label: "Pull-Out Reveal", category: "portal", description: "Reveals scene was a picture in larger context",
159
+ 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" },
160
+ { id: "zoom-into-mouth", label: "Zoom Into Mouth", category: "portal", description: "Push into open mouth, emerges in new world inside",
161
+ promptHint: "the camera pushes into the subject's open mouth, the dark interior fills the frame, and the camera passes through the throat into the new scene which materialises as if emerging from inside the body" },
162
+ { id: "push-through-glass", label: "Push Through Glass", category: "portal", description: "Camera pushes through pane of glass into new world",
163
+ 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" },
164
+ { id: "soul-jump", label: "Soul Jump", category: "portal", description: "Translucent soul leaves body, enters new body",
165
+ 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" },
166
+
167
+ // ============================================================================
168
+ // PHYSICS — 9 entries — force-driven transitions
169
+ // ============================================================================
170
+ { id: "explosion-blast", label: "Explosion Blast", category: "physics", description: "Explosion wipes frame, new scene emerges",
171
+ 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" },
172
+ { id: "shockwave", label: "Shockwave", category: "physics", description: "Shockwave ripples across, scene changes",
173
+ 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" },
174
+ { id: "punch-into-camera", label: "Punch Into Camera", category: "physics", description: "Fist strikes camera, scene changes",
175
+ 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" },
176
+ { id: "debris-shower", label: "Debris Shower", category: "physics", description: "Debris flies past, scene changes behind",
177
+ promptHint: "a shower of debris — leaves, papers, dust — sweeps across the frame in front of the camera, and once the debris clears the scene behind has changed" },
178
+ { id: "gravity-flip", label: "Gravity Flip", category: "physics", description: "Gravity inverts, camera rotates 180",
179
+ 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" },
180
+ { id: "building-explosion", label: "Building Explosion", category: "physics", description: "Structure detonates, scene shifts through smoke",
181
+ 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" },
182
+ { id: "vehicle-explosion", label: "Vehicle Explosion", category: "physics", description: "Vehicle detonates in foreground, scene changes behind",
183
+ 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" },
184
+ { id: "jump-match", label: "Jump Match", category: "physics", description: "Subject jumps, landing matches into new scene",
185
+ 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" },
186
+ { id: "hand-swipe", label: "Hand Swipe", category: "physics", description: "Hand swipes across lens, scene changes during occlusion",
187
+ promptHint: "a hand sweeps across the camera lens at close range, fully occluding the frame in motion blur for a single beat, and as the hand exits the opposite side the scene has changed to the new setting" },
188
+
189
+ // ============================================================================
190
+ // LIGHT — 8 entries — flash and lens FX
191
+ // ============================================================================
192
+ { id: "white-flash", label: "White Flash", category: "light", description: "Frame blooms to white",
193
+ promptHint: "a bright camera-flash bloom fills the frame with pure white, holds for a fraction of a second, then resolves into the new scene" },
194
+ { id: "lens-flare-swipe", label: "Lens Flare Swipe", category: "light", description: "Anamorphic lens flare swipes",
195
+ promptHint: "a horizontal anamorphic lens flare sweeps across the frame from one side to the other, and as it crosses the frame the scene behind it has changed to the new setting" },
196
+ { id: "light-streak", label: "Light Streak", category: "light", description: "Light streak wipes across",
197
+ promptHint: "a bright streak of light races across the frame leaving motion-blur trails, and as the streak exits the opposite side the scene is now the new setting" },
198
+ { id: "color-invert", label: "Color Invert Flash", category: "light", description: "Colors invert briefly",
199
+ 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" },
200
+ { id: "sun-glare", label: "Sun Glare", category: "light", description: "Sun glare washes frame",
201
+ 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" },
202
+ { id: "lens-crack", label: "Lens Crack", category: "light", description: "Lens cracks, scene through fractured glass",
203
+ 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" },
204
+ { id: "dirty-lens-wipe", label: "Dirty Lens Wipe", category: "light", description: "Lens dust/grime wipes clean, scene changes",
205
+ 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" },
206
+ { id: "eye-light-burst", label: "Eye Light Burst", category: "light", description: "Bright beam from subject's eyes whites out frame",
207
+ promptHint: "the subject's eyes ignite with a brilliant white beam of light that overpowers the frame in bloom and lens flares, the radiance fills the image entirely, and as the glow recedes the new scene is revealed" },
208
+
209
+ // ============================================================================
210
+ // GLITCH — 7 entries — digital corruption transitions
211
+ // ============================================================================
212
+ { id: "digital-glitch", label: "Digital Glitch", category: "glitch", description: "RGB-split + scanline + datamosh glitch",
213
+ promptHint: "a brief digital glitch corruption — RGB-split, scanline tearing, pixel-block displacement — overtakes the frame for a fraction of a second, and resolves into the new scene" },
214
+ { id: "vhs-rewind", label: "VHS Rewind", category: "glitch", description: "VHS tracking distortion",
215
+ promptHint: "VHS-style tracking distortion and tape-rewind artifacts sweep the frame with horizontal scanline noise, then resolve into the new scene as if rewinding to a different recording" },
216
+ { id: "datamosh", label: "Datamosh", category: "glitch", description: "Motion-vector smear bleeds scenes",
217
+ promptHint: "datamosh transition: the motion vectors of the first scene continue smearing into the pixels of the second scene, creating a fluid pixel-bleed handoff" },
218
+ { id: "channel-flip", label: "Channel Flip", category: "glitch", description: "TV channel flip with static",
219
+ promptHint: "a brief burst of TV static and channel-flip artifacts sweeps the frame, and the new scene resolves as if changing channels on an old television" },
220
+ { id: "hologram-flicker", label: "Hologram Flicker", category: "glitch", description: "Hologram-style flicker materialises new scene",
221
+ promptHint: "a hologram-style flicker with horizontal interference bands and chromatic aberration overtakes the frame for a beat, and resolves into the new scene as if it materialised from a projection" },
222
+ { id: "display-wipe", label: "Display Wipe", category: "glitch", description: "Scene compresses into display, expands to new scene",
223
+ promptHint: "the first scene compresses into a small floating display screen at the center of the frame with a CRT power-on/off animation and scanline flicker, the display then expands outward and unfolds into the new scene full-frame" },
224
+ { id: "double-exposure", label: "Double Exposure", category: "glitch", description: "Two scenes overlay translucent, first fades to second",
225
+ promptHint: "the first and second scenes blend as a translucent double exposure where both images coexist semi-transparently on the frame, the first image then gradually fades out leaving the second image fully resolved" },
226
+ ]
227
+
228
+ export const TRANSITION_CATEGORY_ORDER: ReadonlyArray<TransitionCategory> = [
229
+ "standard", "time", "element", "morph", "portal", "physics", "light", "glitch",
230
+ ]
231
+
232
+ export const TRANSITION_CATEGORY_LABELS: Readonly<Record<TransitionCategory, string>> = {
233
+ standard: "Standard",
234
+ time: "Time & Temporal",
235
+ element: "Element & Teleport",
236
+ morph: "Morph & Shape-shift",
237
+ portal: "Portal & Inside",
238
+ physics: "Physics & Force",
239
+ light: "Light & Flash",
240
+ glitch: "Glitch & Digital",
241
+ }
242
+
243
+ const transitionById = new Map<string, Transition>(
244
+ TRANSITIONS.map((t) => [t.id, t]),
245
+ )
246
+
247
+ export function getTransition(id: string | undefined | null): Transition | undefined {
248
+ if (!id) return undefined
249
+ return transitionById.get(id)
250
+ }
251
+
252
+ export function getTransitionLabel(id: string | undefined | null, fallback?: string): string {
253
+ const t = getTransition(id)
254
+ if (t) return t.label
255
+ if (fallback !== undefined) return fallback
256
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
257
+ }
258
+
259
+ export function getTransitionPromptHint(id: string | undefined | null): string {
260
+ return getTransition(id)?.promptHint ?? ""
261
+ }
262
+
263
+ export const TRANSITION_IDS: ReadonlyArray<string> = TRANSITIONS.map((t) => t.id)
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Graph-aware composer — start/end input handles + timing fields + multi-pick
267
+ // ---------------------------------------------------------------------------
268
+
269
+ const POSITION_CLAUSES: Record<Exclude<TransitionPosition, "auto">, string> = {
270
+ start: "the transition occurs at the opening of the clip",
271
+ middle: "the transition occurs in the middle of the clip",
272
+ end: "the transition occurs at the end of the clip",
273
+ full: "the transition spans the entire clip",
274
+ }
275
+
276
+ const DURATION_CLAUSES: Record<Exclude<TransitionDuration, "auto">, string> = {
277
+ instant: "occurring instantaneously",
278
+ short: "lasting approximately 1 second",
279
+ medium: "lasting approximately 2 seconds",
280
+ long: "lasting approximately 3 seconds",
281
+ }
282
+
283
+ const INTENSITY_CLAUSES: Record<Exclude<TransitionIntensity, "auto">, string> = {
284
+ subtle: "with subtle restrained energy and minimal flourish",
285
+ natural: "with natural unhurried timing",
286
+ dynamic: "with dynamic energy and assertive flourish",
287
+ crazy: "with extreme exaggerated energy, wild flourishes, and dramatic distortion",
288
+ }
289
+
290
+ /**
291
+ * Compose a structural prompt-hint sentence from a transition id (or array
292
+ * of 1-2 ids for multi-pick) plus optional start-state/end-state hints
293
+ * (collected by walking the source node's startState / endState input
294
+ * handle edges upstream) and optional timing fields.
295
+ *
296
+ * Behavior:
297
+ * - 0 hints (no transition, empty array, or all-empty hints) → ""
298
+ * - n base hints joined with ", and "
299
+ * - Timing/start/end clauses apply ONCE at the outer layer, not per-id
300
+ * - null input is treated like undefined (falsy short-circuit → returns "")
301
+ */
302
+ export function composeTransitionHintFromConnections(
303
+ transitionId: string | ReadonlyArray<string> | undefined,
304
+ startHints: ReadonlyArray<string>,
305
+ endHints: ReadonlyArray<string>,
306
+ timing?: TransitionTiming,
307
+ ): string {
308
+ const ids = Array.isArray(transitionId)
309
+ ? Array.from(new Set(transitionId)).slice(0, 2)
310
+ : transitionId ? [transitionId] : []
311
+ const baseHints = ids.map(getTransitionPromptHint).filter((h) => h.length > 0)
312
+ if (baseHints.length === 0) return ""
313
+
314
+ const combinedBase = baseHints.join(", and ")
315
+ const parts: string[] = [combinedBase]
316
+
317
+ if (timing?.position && timing.position !== "auto") {
318
+ parts.push(POSITION_CLAUSES[timing.position])
319
+ }
320
+ if (timing?.duration && timing.duration !== "auto") {
321
+ parts.push(DURATION_CLAUSES[timing.duration])
322
+ }
323
+ if (timing?.intensity && timing.intensity !== "auto") {
324
+ parts.push(INTENSITY_CLAUSES[timing.intensity])
325
+ }
326
+
327
+ const startClause = startHints.filter((h) => h && h.length > 0).join(" and ")
328
+ const endClause = endHints.filter((h) => h && h.length > 0).join(" and ")
329
+ if (startClause) parts.push(`starting from ${startClause}`)
330
+ if (endClause) parts.push(`ending at ${endClause}`)
331
+
332
+ return parts.join(", ")
333
+ }