@nodaro/prompts 1.21.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/index.cjs +191 -45
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +362 -207
  4. package/dist/index.d.ts +362 -207
  5. package/dist/index.js +183 -46
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/assemble-video-input.test.ts +20 -12
  9. package/src/__tests__/camera-motion-approved.test.ts +46 -0
  10. package/src/__tests__/camera-motions-from-connections.test.ts +3 -2
  11. package/src/__tests__/catalog-packs.test.ts +13 -0
  12. package/src/__tests__/character-fx-timing-catalogs.test.ts +3 -2
  13. package/src/__tests__/factory-presets.test.ts +28 -3
  14. package/src/__tests__/fixtures/camera-motion-approved.json +330 -0
  15. package/src/__tests__/fixtures/parameter-hint-golden.json +32 -16
  16. package/src/__tests__/hint-join.test.ts +3 -2
  17. package/src/__tests__/node-prompt-fields.test.ts +2 -2
  18. package/src/__tests__/parameter-hint-mode.test.ts +17 -5
  19. package/src/__tests__/prompt-style-section.test.ts +2 -2
  20. package/src/__tests__/transitions-instant.test.ts +242 -0
  21. package/src/__tests__/transitions-scope.test.ts +160 -0
  22. package/src/__tests__/transitions.test.ts +9 -9
  23. package/src/ad-creative-analysis.ts +99 -0
  24. package/src/camera-motions.ts +21 -21
  25. package/src/catalog-packs.ts +13 -2
  26. package/src/direction-registry.ts +2 -2
  27. package/src/factory-presets/generate-video.ts +22 -0
  28. package/src/index.ts +3 -0
  29. package/src/node-prompt-fields.ts +4 -0
  30. package/src/picker-catalogs.ts +13 -0
  31. package/src/ref-binding.ts +19 -0
  32. package/src/transitions.ts +204 -27
  33. package/src/video-reference-resolver.ts +1 -1
@@ -53,7 +53,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
53
53
  label: "Static",
54
54
  category: "default",
55
55
  description: "Fixed camera, no movement",
56
- promptHint: "locked off static camera, no camera movement",
56
+ promptHint: "The camera is locked on a solid tripod and does not move at all for the entire shot: it never travels, never rotates, never tilts, never changes focal length. The viewpoint is completely fixed and the framing stays exactly the same from the first frame to the last.\nLife within the scene carries on naturally, but the camera holds perfectly still throughout.\nNo camera movement of any kind, no drift, no sway, no zoom.",
57
57
  term: "locked-off static camera",
58
58
  },
59
59
  {
@@ -61,7 +61,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
61
61
  label: "Handheld",
62
62
  category: "default",
63
63
  description: "Natural handheld shake",
64
- promptHint: "handheld camera with subtle natural shake and micro movements",
64
+ promptHint: "The camera is HANDHELD, held in an operator's hands rather than on any support, so it carries the constant small live motion of a real person holding it: a continuous gentle shake and wobble, tiny irregular drifts and corrections in every direction, a slight bob in time with breathing, never settling completely still even for a moment.\nThe motion is small and organic, never a large or deliberate move: the camera stays roughly where it is and keeps roughly the same framing, it simply never locks off. It does not travel toward anything, away from anything, or off to any side; it holds its position and only jitters around it. The unsteadiness is human and slightly random, not a smooth mechanical sway and not a rhythmic loop.\nThe lens does not change and the camera does not travel anywhere, orbit, or turn to a new direction; it just floats and jitters in place as if held.\nThe framing stays broadly the same across the shot, loosely on the subject, wandering by only small amounts.\nNo zoom, no deliberate camera move, no forward or backward travel, no rotation to a new angle, no smooth glide.",
65
65
  term: "handheld camera",
66
66
  },
67
67
  {
@@ -69,7 +69,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
69
69
  label: "Steadicam",
70
70
  category: "default",
71
71
  description: "Smooth stabilized walking shot",
72
- promptHint: "smooth steadicam shot, gliding stabilized movement through the scene",
72
+ promptHint: "The camera moves with the perfectly smooth, floating quality of a Steadicam or gimbal: it glides gently and continuously, with none of the shake, jitter or bob of a handheld rig. Every bit of movement is buttery, damped and stabilised, as if the camera were floating on air.\nThe motion is a slow, graceful drift through the scene — easing a little forward and around with fluid, gliding ease — always even and controlled, never abrupt, never trembling, never correcting.\nThe lens does not change. The framing stays loosely on the subject throughout, the glide being gentle enough that the composition holds.\nThe single defining quality is SMOOTHNESS: this is the opposite of handheld — no shake whatsoever, only silky stabilised floating motion.\nNo zoom, no jitter, no handheld shake, no abrupt moves.",
73
73
  term: "smooth steadicam shot",
74
74
  },
75
75
 
@@ -271,14 +271,14 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
271
271
  label: "Dutch Angle",
272
272
  category: "roll",
273
273
  description: "Static tilted frame for tension",
274
- promptHint: "dutch angle, canted tilted frame for tension",
274
+ promptHint: "The shot begins level, then the camera rolls a little way around its own lens axis and STOPS at a fixed cant, holding that tilted Dutch-angle framing for the rest of the shot. It is a single small settling roll into a tilt, not a continuous spin: the frame tips perhaps fifteen or twenty degrees off level and then stays there, canted and still, for an uneasy, tense feeling.\nOnly the roll axis is involved: the camera stays in the same position, does not travel, does not pan or tilt its aim up or down, and the lens does not change. The horizon starts level and ends clearly canted, then holds.\nBecause it is a pure roll, there is no parallax: the whole frame turns as one flat picture, near and far together.\nLife in the scene carries on gently while the framing holds its tilt.\nNo zoom, no travel, no continuous spinning, no returning to level once canted.",
275
275
  },
276
276
  {
277
277
  id: "spin-360",
278
278
  label: "Full 360 Spin",
279
279
  category: "roll",
280
280
  description: "Camera rotates a full 360° on its axis",
281
- promptHint: "full 360 degree spin, the camera rotates a complete revolution on its own lens axis",
281
+ promptHint: "The camera rotates a FULL 360 degrees around its own lens axis in one continuous even spin, and keeps turning the same way without pausing until the picture has come all the way back to level exactly as it started. The horizon sweeps from level, round through fully sideways on one side, through completely upside-down, through fully sideways on the other side, and back to level — one complete unbroken revolution.\nOnly the roll axis is involved: the camera stays in exactly the same position, does not travel, does not pan or tilt its aim, and the lens does not change. It spins about the axis running straight out through the lens.\nBecause it is a pure roll, there is no parallax: the whole frame turns as one flat picture, near and far rotating together by the same amount.\nThe spin is smooth and continuous at an even speed, going round only once and ending back at level.\nNo zoom, no travel, no pan, no tilt of the aim, no reversal of the spin direction.",
282
282
  term: "full 360 degree camera roll",
283
283
  },
284
284
 
@@ -403,7 +403,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
403
403
  label: "Ronin Glide",
404
404
  category: "tracking",
405
405
  description: "Slow gliding move on a Ronin/Movi gimbal",
406
- promptHint: "ronin glide, slow gliding move on a Ronin or Movi gimbal, cinematic float without any shake",
406
+ promptHint: "ronin gimbal glide, ultra-smooth floating tracking movement following the subject",
407
407
  term: "slow gliding gimbal move",
408
408
  },
409
409
  {
@@ -411,7 +411,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
411
411
  label: "Serpentine Track",
412
412
  category: "tracking",
413
413
  description: "Camera weaves through obstacles in S-curves",
414
- promptHint: "serpentine track, the camera weaves through obstacles in S-curves, snaking forward along a winding path",
414
+ promptHint: "serpentine tracking shot, camera weaves smoothly side to side while following the subject",
415
415
  term: "serpentine tracking shot",
416
416
  },
417
417
 
@@ -421,7 +421,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
421
421
  label: "POV",
422
422
  category: "special",
423
423
  description: "First person point of view",
424
- promptHint: "POV shot, first person perspective as seen through the subject's eyes",
424
+ promptHint: "POV shot, first person perspective as seen through the subject's eyes. The viewpoint moves forward at a calm, unhurried walking pace — steady and gentle, never a fast rush or a vehicle-like travel speed. No body parts, hands or feet ever enter the frame; only the point of view itself drifts smoothly forward.",
425
425
  term: "first person pov shot",
426
426
  },
427
427
  {
@@ -429,7 +429,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
429
429
  label: "Over The Shoulder",
430
430
  category: "special",
431
431
  description: "Frame past a character's shoulder",
432
- promptHint: "over the shoulder shot, framing past one character's shoulder onto another",
432
+ promptHint: "Over the shoulder shot: the camera is already framed past the near character's shoulder onto the second character from the very first frame, with no push-in or approach - the framing and distance stay constant throughout, at most a very small handheld-style settle. No camera travel toward or away from either character. No dialogue, no spoken words, no lip movement suggesting speech - completely silent, ambient sound only if any.",
433
433
  term: "over-the-shoulder shot",
434
434
  },
435
435
  {
@@ -453,7 +453,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
453
453
  label: "Aerial",
454
454
  category: "special",
455
455
  description: "High altitude drone-style shot",
456
- promptHint: "aerial drone shot, high altitude slow forward movement over the landscape",
456
+ promptHint: "The camera is airborne and sinks straight downward through the air, losing altitude like an aircraft lowering itself vertically. Its aim is completely fixed: the horizon line stays at exactly the same height across the frame from the first moment to the last, and the camera never dips or tips its view downward at all — what it points at does not change, only where it is in space.\nAs the camera drops in height, everything in view slides UPWARD through the frame together because the viewpoint is lower: whatever stands tall rises up the frame and grows, and more of what lies far below climbs into view from the bottom. This vertical sliding of the whole scene is the ONLY change; the angle of view is frozen.\nNo tilting, no dipping the aim down, no looking further down, no rotation, no forward travel — the horizon must remain pinned at its starting height while the camera simply loses altitude.",
457
457
  term: "high altitude aerial drone shot",
458
458
  },
459
459
  {
@@ -461,7 +461,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
461
461
  label: "Helicopter",
462
462
  category: "special",
463
463
  description: "Wide high-altitude sweeping aerial",
464
- promptHint: "helicopter shot, high altitude wide sweeping aerial pass with strong lateral movement",
464
+ promptHint: "helicopter shot, sweeping aerial movement banking around the scene",
465
465
  term: "sweeping helicopter aerial shot",
466
466
  },
467
467
  {
@@ -469,7 +469,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
469
469
  label: "Fly Over",
470
470
  category: "special",
471
471
  description: "Low fast aerial pass over the scene",
472
- promptHint: "fly over shot, low altitude drone passing quickly over the scene with strong forward motion",
472
+ promptHint: "The camera flies steadily FORWARD through the air at a constant height, cruising ahead toward what lies in front of it without ever changing altitude. Its height stays exactly the same from start to finish — it does not descend, does not climb, does not dip its aim; it simply travels forward at cruising speed.\nBecause it moves forward, whatever lies ahead grows closer and sweeps toward the camera and past it on both sides, near things rushing by faster than far things. The horizon holds at exactly the same height in the frame the whole time.\nNo descending, no climbing, no tilting the aim, no rotation — only steady level forward flight at a fixed altitude.",
473
473
  term: "low aerial fly-over pass",
474
474
  },
475
475
  {
@@ -501,7 +501,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
501
501
  label: "Rack Focus",
502
502
  category: "special",
503
503
  description: "Pull focus between foreground and background",
504
- promptHint: "rack focus, lens focus shifts from a foreground subject to a background subject (or vice versa), the unfocused plane blurs",
504
+ promptHint: "The camera itself never moves, never zooms and never changes its framing — it is completely locked in place for the whole shot. Only the lens's focus plane shifts. It starts sharply focused on the nearest element in the frame, with everything further back a soft out-of-focus blur. Then the focus pulls smoothly across to the far background, which becomes crisp and sharp, while the near foreground that was sharp a moment ago now falls out of focus and blurs instead. The two states — near-sharp/far-blurred, then near-blurred/far-sharp — must read as clearly opposite. No camera travel, no zoom, no change in composition — only which plane is sharp changes.",
505
505
  },
506
506
 
507
507
  // Modern / social-video vocabulary
@@ -510,7 +510,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
510
510
  label: "Handheld Vlog",
511
511
  category: "default",
512
512
  description: "Casual vlog-style handheld",
513
- promptHint: "casual handheld vlog-style camera, slight wandering and natural shake, talking-to-camera framing",
513
+ promptHint: "The camera is a casual handheld vlog camera, held at arm's length by the subject or a companion, with the loose, informal quality of someone filming themselves. It has clear handheld shake and small bobbing motion, but looser and more wandering than a careful operator: it drifts around a bit, reframes casually, bobs as if the holder is shifting their weight or gesturing.\nThe framing sits fairly close and personal, roughly on the subject, wandering by moderate amounts as a vlogger's arm naturally would — informal and lively, not locked or composed.\nThe motion has personality and looseness: more movement than a careful handheld, unpolished and spontaneous, but it stays broadly in place rather than travelling off anywhere.\nThe lens does not change; the camera does not glide smoothly (that would be a steadicam) and does not travel deliberately in any direction.\nNo zoom, no smooth stabilised glide, no deliberate directional move, no tripod stillness.",
514
514
  term: "handheld vlog-style camera",
515
515
  },
516
516
  {
@@ -518,7 +518,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
518
518
  label: "POV Walk",
519
519
  category: "tracking",
520
520
  description: "First-person walking POV",
521
- promptHint: "first-person POV walking camera, GoPro-style head-mounted perspective with natural footstep movement",
521
+ promptHint: "first-person POV walking shot, camera as the subject's eyes with natural head-bob as they walk forward",
522
522
  term: "first person walking pov",
523
523
  },
524
524
  {
@@ -541,23 +541,23 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
541
541
  label: "Screen Tap",
542
542
  category: "special",
543
543
  description: "On-screen finger-tap transition",
544
- promptHint: "camera transition triggered by an on-screen finger tap, TikTok-native pacing with snap to the next subject",
545
- term: "on-screen finger-tap transition",
544
+ promptHint: "An abrupt hard snap-cut transition: the camera instantly jumps to a new framing of the scene with no gradual movement in between, a fast decisive jolt rather than any smooth pan, push or travel. No object, hand, finger, or UI element ever appears in the frame. TikTok-native snap pacing.",
545
+ term: "abrupt snap-cut transition",
546
546
  },
547
547
  {
548
548
  id: "phone-flip",
549
549
  label: "Phone Flip",
550
550
  category: "special",
551
551
  description: "Front/rear camera flip",
552
- promptHint: "camera-flip transition where the phone visibly rotates between the front and rear sensors, brief blur during the swap",
553
- term: "front-to-rear phone camera flip",
552
+ promptHint: "A rapid flip-transition, functioning as a hard cut rather than a smooth continuous rotation: the entire frame snaps through one quick full rotation in a fraction of a second, with strong motion blur through the spin, then lands upright again immediately on a new framing of the scene. This is abrupt and near-instant, never a slow, smooth, or continuously spinning carousel-like rotation. No object, hand, phone, or UI element ever appears in the frame.",
553
+ term: "fast flip-cut transition",
554
554
  },
555
555
  {
556
556
  id: "gentle-drift",
557
557
  label: "Gentle Drift",
558
558
  category: "default",
559
559
  description: "Slow ambient floating motion",
560
- promptHint: "gentle camera drift, a slow ambient floating motion with no specific direction, the camera barely moves but never sits perfectly still, evocative of contemplative atmospheric shots",
560
+ promptHint: "The camera drifts with an extremely slow, gentle, ambient floating motion, as if suspended and barely stirring in still air. There is no shake and no deliberate direction: it simply floats, drifting a tiny amount one way and then easing another, aimless and dreamlike.\nThe movement is minimal and continuous — the camera never sits perfectly still, but it never travels anywhere either; it hovers almost in place, wandering by only the smallest amounts, smooth and weightless throughout.\nThe pace is contemplative and calm, the drift so slow it is only just perceptible. The picture stays sharp; there is no motion blur.\nThe lens does not change and the framing stays broadly the same, resting loosely on the subject.\nNo zoom, no shake, no jitter, no deliberate directional move, no travel across the scene.",
561
561
  term: "slow gentle camera drift",
562
562
  },
563
563
  {
@@ -565,7 +565,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
565
565
  label: "Parallax",
566
566
  category: "default",
567
567
  description: "Lateral motion with foreground/background depth separation",
568
- promptHint: "parallax camera motion, lateral movement that emphasizes the depth separation between foreground and background elements, foreground objects appearing to move faster than distant ones",
568
+ promptHint: "The camera makes a small, smooth sideways move that shows off the depth of the scene through parallax. It eases gently a short way across, staying at the same distance and keeping its aim forward.\nThe defining quality is a STRONG parallax gradient: the nearest foreground elements slide across noticeably and quickly, the mid-distance elements shift more gently, and the most distant elements barely move at all — one smooth continuous gradient of speed from near to far that makes the separation between the layers obvious. Nothing is frozen; everything shifts, just by very different amounts according to its distance.\nThe move is modest and ambient rather than a full travelling shot: a gentle reveal of depth, not a journey across the scene.\nThe lens does not change; the camera does not push in, pull back, rotate, or shake.\nNo zoom, no forward or backward travel, no rotation, no handheld shake.",
569
569
  term: "lateral parallax camera move",
570
570
  },
571
571
  ]
@@ -125,8 +125,13 @@ function applyExtend(c: PickerCatalog, pack: CatalogPack): PickerCatalog {
125
125
 
126
126
  function applyDeny(c: PickerCatalog, denyIds: readonly string[]): PickerCatalog {
127
127
  const deny = new Set(denyIds)
128
- if (c.kind === "single") return { ...c, options: (c.options ?? []).filter((o) => !deny.has(o.id)) }
129
- return { ...c, dimensions: (c.dimensions ?? []).map((d) => ({ ...d, options: d.options.filter((o) => !deny.has(o.id)) })) }
128
+ // Single-value catalogs can also declare auxiliary dimensions (e.g.
129
+ // transition duration or motion speed). Those are curated too.
130
+ return {
131
+ ...c,
132
+ options: c.options?.filter((o) => !deny.has(o.id)),
133
+ dimensions: c.dimensions?.map((d) => ({ ...d, options: d.options.filter((o) => !deny.has(o.id)) })),
134
+ }
130
135
  }
131
136
 
132
137
  export function composePickerCatalogs(
@@ -145,6 +150,12 @@ export function composePickerCatalogs(
145
150
  else if (pack.mode === "extend") out = applyExtend(out, pack)
146
151
  else if (pack.mode === "deny") out = applyDeny(out, pack.denyIds ?? [])
147
152
  }
153
+ if (activePacks.some((p) => p.catalogId === c.catalogId) && out.defaultValue) {
154
+ const options = [...(out.options ?? []), ...(out.dimensions ?? []).flatMap((d) => d.options)]
155
+ if (!options.some((o) => o.id === out.defaultValue)) {
156
+ out = { ...out, defaultValue: out.options?.[0]?.id }
157
+ }
158
+ }
148
159
  return out
149
160
  })
150
161
  }
@@ -67,7 +67,7 @@ import { getEraPromptHint, getEraTerm } from "./era.js"
67
67
  import { getBackdropPromptHint, getBackdropTerm } from "./backdrop.js"
68
68
  import { buildActionFxHints } from "./action-fx.js"
69
69
  import { getTemporalPromptHint, getTemporalTerm } from "./temporal.js"
70
- import { getTransitionPromptHint, getTransitionTerm } from "./transitions.js"
70
+ import { renderTransitionBases } from "./transitions.js"
71
71
  import { getLoopSubjectPromptHint, getLoopSubjectTerm } from "./loop-subject.js"
72
72
 
73
73
  /** Which generation stages fold a dimension. */
@@ -251,7 +251,7 @@ export const DIRECTION_FIELDS = [
251
251
  { key: "temporalFreeze", surface: "video", family: "motion", maxPicks: 1, render: temporal },
252
252
  { key: "temporalDirection", surface: "video", family: "motion", maxPicks: 1, render: temporal },
253
253
  { key: "temporalShutter", surface: "video", family: "motion", maxPicks: 1, render: temporal },
254
- { key: "transition", surface: "video", family: "motion", maxPicks: 2, render: perId(getTransitionPromptHint, getTransitionTerm) },
254
+ { key: "transition", surface: "video", family: "motion", maxPicks: 2, render: renderTransitionBases },
255
255
  { key: "loopSubject", surface: "video", family: "motion", maxPicks: 1, render: perId(getLoopSubjectPromptHint, getLoopSubjectTerm) },
256
256
 
257
257
  // ── LEGACY BLOCK — see the table doc above. Placed LAST, in today's exact
@@ -1113,4 +1113,26 @@ export const GENERATE_VIDEO_PRESETS: readonly FactoryPreset[] = [
1113
1113
  "{creature || A monstrous yeti with the physique and bone structure of a giant silverback gorilla}, {texture || thick matted white fur with snow crusted on the shoulders}, charges forward and smashes both fists into the ground. Low-angle ground shot looking up to emphasize its scale, heavy camera shake on impact, snow and debris bursting toward the lens. Overcast mountain light, volumetric breath mist around its muzzle. <deep guttural roar, the crunch of impact, debris scattering>(low ominous brass drone)Biologically accurate anatomy, epic cinematic scale. HD, sharp micro-details, stable picture, keep it subtitle-free, do not generate a watermark.",
1114
1114
  },
1115
1115
  },
1116
+ // ── Video Editing ─────────────────────────────────────────────────────────
1117
+ // Seedance 2.5 EDITS a wired reference clip when the prompt reads as an edit
1118
+ // instruction. Edit mode takes the source clip's own ratio and length, so the
1119
+ // preset ships the exact shape the provider requires — Adaptive + Auto
1120
+ // (duration -1) — and the run goes through on the first submit. The user's
1121
+ // own sentence is the whole prompt: the preset only supplies the pre & post
1122
+ // text, with the clip bound as {video:1} (wire it into Video Refs).
1123
+ {
1124
+ id: "generate-video/edit-video",
1125
+ name: "Edit Video",
1126
+ description: "Change a wired clip by instruction — keeps its length, ratio and everything you don't mention.",
1127
+ group: "Video Editing",
1128
+ data: {
1129
+ provider: "seedance-2-5",
1130
+ aspectRatio: "adaptive",
1131
+ duration: -1,
1132
+ generateAudio: true,
1133
+ promptPrefix: "edit {video:1} as follows:\n",
1134
+ promptSuffix:
1135
+ "Keep everything else in the clip unchanged: the same people, motion, framing and timing. Keep it subtitle-free, do not generate a watermark.",
1136
+ },
1137
+ },
1116
1138
  ]
package/src/index.ts CHANGED
@@ -96,3 +96,6 @@ export * from "./surround-fill.js"
96
96
  // --- Start/end frame delivery: one plan for the dispatch and the editor ---
97
97
  export * from "./frame-delivery.js"
98
98
  export * from "./character-motion-diagnostics.js"
99
+
100
+ // --- Per-ad creative analysis for the social scraper nodes ---
101
+ export * from "./ad-creative-analysis.js"
@@ -128,6 +128,10 @@ export const NODE_PROMPT_FIELDS: Readonly<Record<string, PromptFieldSpec>> = {
128
128
  // Video Analysis — its focus hint is the editable prompt (renders a JSON scene
129
129
  // table, not a media preview → no inline editor).
130
130
  "video-analysis": { prompt: "analysisFocus", promptLabel: "Analysis focus", media: "video", inline: false },
131
+ // Edit Plan (podcast editing) — its free-text editing instructions are the
132
+ // editable prompt; the affixes wrap them. Emits an EDL (JSON), not a media
133
+ // preview, so no inline editor.
134
+ "edit-plan": { prompt: "instructions", promptLabel: "Editing instructions", media: "video", inline: false },
131
135
  }
132
136
 
133
137
  /** The prompt-field spec for a node type, or undefined if it has none. */
@@ -119,6 +119,16 @@ export interface PickerOption {
119
119
  * `label` and inject `term`; they never derive it themselves.
120
120
  */
121
121
  readonly term: string
122
+ /**
123
+ * Transitions only: present (`true`) on a row whose mechanism is a CUT, so
124
+ * it takes no duration and no intensity — see `Transition.instant` /
125
+ * `isInstantTransition`. A consumer that only reads this wire catalog
126
+ * (Studio builds the transition Duration lever from
127
+ * `getPickerCatalog("transition")`) hides those levers for such a row. Carried from the base catalog; a row a catalog pack ADDS has
128
+ * it only when the pack's own option says so, and absent means "has a
129
+ * duration" — the safe reading, since the composer then behaves as before.
130
+ */
131
+ readonly instant?: true
122
132
  /** Only present if the source catalog entry already carries a data icon/emoji/thumbnail field. */
123
133
  readonly icon?: string
124
134
  }
@@ -172,6 +182,8 @@ interface BaseCatalogEntry {
172
182
  readonly term?: string
173
183
  /** W1-a: see Person.adultOnly. Propagated verbatim into the flattened option. */
174
184
  readonly adultOnly?: true
185
+ /** Transitions: see `Transition.instant`. Propagated into the flattened option as `instant: true`. */
186
+ readonly instant?: boolean
175
187
  }
176
188
 
177
189
  /**
@@ -200,6 +212,7 @@ function toOptions<T extends BaseCatalogEntry>(
200
212
  if (e.description) opt.description = e.description
201
213
  if (categoryField) opt.category = e[categoryField] as unknown as string
202
214
  if (e.adultOnly) opt.adultOnly = true
215
+ if (e.instant) opt.instant = true
203
216
  return opt as PickerOption
204
217
  })
205
218
  }
@@ -43,3 +43,22 @@ export const REF_BINDING = {
43
43
  frame: (n: number, role: "opening" | "closing") =>
44
44
  `Use @image_${n} as the ${role} (${role === "opening" ? "first" : "last"}) frame of the video.`,
45
45
  } as const
46
+
47
+ /**
48
+ * The instruction that makes Seedance EDIT a wired clip rather than use it as a
49
+ * style reference — the Video to Video node's Seedance lane prepends it to the
50
+ * user's own sentence. Written with the EDITOR token (`{video:1}`) so it goes
51
+ * through the same reference resolver as any prompt: the source clip is the
52
+ * run's first reference video, so it resolves to `@video_1` on the wire. The
53
+ * instruction lands on its own line after it.
54
+ */
55
+ export const SEEDANCE_VIDEO_EDIT_PREFIX = "edit {video:1} as follows:\n"
56
+
57
+ /** The user's sentence, framed as an edit of the source clip. Idempotent: a
58
+ * prompt that already opens with the instruction (a preset's pre text, a
59
+ * hand-written one, either token spelling) is left alone. */
60
+ export function buildSeedanceVideoEditPrompt(prompt: string | undefined): string {
61
+ const body = (prompt ?? "").trim()
62
+ if (/^edit\s+(?:\{video:1(?::[^}]*)?\}|@video_1(?!\w))/i.test(body)) return body
63
+ return `${SEEDANCE_VIDEO_EDIT_PREFIX}${body}`
64
+ }