@nodaro/prompts 1.26.1 → 1.28.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 (40) hide show
  1. package/dist/index.cjs +3127 -92
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1776 -10
  4. package/dist/index.d.ts +1776 -10
  5. package/dist/index.js +3094 -93
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/fixtures/parameter-hint-golden.json +33 -1
  9. package/src/__tests__/hint-join.test.ts +64 -1
  10. package/src/__tests__/motion-hint.test.ts +20 -0
  11. package/src/__tests__/parameter-registry-sync.test.ts +1 -0
  12. package/src/__tests__/person-mixed-heritage.test.ts +43 -0
  13. package/src/__tests__/picker-art-images.test.ts +191 -0
  14. package/src/__tests__/resolve-prompt.test.ts +13 -1
  15. package/src/__tests__/transition-options.test.ts +223 -0
  16. package/src/__tests__/transition-styles.test.ts +202 -0
  17. package/src/__tests__/transitions-instant.test.ts +9 -5
  18. package/src/__tests__/transitions-scope.test.ts +593 -8
  19. package/src/__tests__/video-own-motion.test.ts +26 -0
  20. package/src/catalog-sections.ts +24 -0
  21. package/src/hint-join.ts +33 -1
  22. package/src/index.ts +4 -0
  23. package/src/parameter-prompt-hint.ts +20 -3
  24. package/src/person.ts +24 -8
  25. package/src/picker-art/character-art-files.generated.ts +1118 -0
  26. package/src/picker-art/images.ts +83 -0
  27. package/src/picker-art/index.ts +13 -0
  28. package/src/picker-art/look-preview-sets.ts +489 -0
  29. package/src/picker-art/paths.ts +53 -0
  30. package/src/picker-art/sound-art-files.generated.ts +319 -0
  31. package/src/picker-art/sound-art-map.ts +722 -0
  32. package/src/picker-art/sound-art-types.ts +19 -0
  33. package/src/picker-catalogs.ts +125 -24
  34. package/src/prompt-builder.ts +2 -1
  35. package/src/prompt-hint-join.ts +8 -2
  36. package/src/prompt-style-section.ts +3 -5
  37. package/src/resolve-prompt.ts +22 -4
  38. package/src/styling.ts +18 -0
  39. package/src/transitions.ts +320 -51
  40. package/src/video-own-motion.ts +29 -0
@@ -0,0 +1,24 @@
1
+ import { PERSON_DIMENSION_SECTIONS, PERSON_FIELD_BY_DIMENSION } from "./person.js"
2
+ import { STYLING_DIMENSION_SECTIONS, STYLING_FIELD_BY_DIMENSION } from "./styling.js"
3
+
4
+ /** One of the topics the editor groups a catalog's settings under, as node-data fields. */
5
+ export interface CatalogTopic {
6
+ readonly label: string
7
+ readonly fields: ReadonlyArray<string>
8
+ }
9
+
10
+ /**
11
+ * The editor's topics for a catalog (Person, Styling), in order, with their
12
+ * settings as node-data fields — or undefined for every other catalog. Kept
13
+ * out of the picker-catalogs funnel on purpose: the topic lists are layout
14
+ * metadata, not catalog entries.
15
+ */
16
+ export function catalogTopics(catalogId: string): ReadonlyArray<CatalogTopic> | undefined {
17
+ if (catalogId === "person") {
18
+ return PERSON_DIMENSION_SECTIONS.map((s) => ({ label: s.label, fields: s.dimensions.map((d) => PERSON_FIELD_BY_DIMENSION[d] as string) }))
19
+ }
20
+ if (catalogId === "styling") {
21
+ return STYLING_DIMENSION_SECTIONS.map((s) => ({ label: s.label, fields: s.dimensions.map((d) => STYLING_FIELD_BY_DIMENSION[d] as string) }))
22
+ }
23
+ return undefined
24
+ }
package/src/hint-join.ts CHANGED
@@ -13,7 +13,9 @@
13
13
  * approved, and a user's preText/postText is never rewritten.
14
14
  */
15
15
 
16
- const ENDS_SENTENCE = /[.!?]$/
16
+ /** A piece that already closes a sentence: a Latin or CJK full stop, question
17
+ * or exclamation mark, or an ellipsis. */
18
+ const ENDS_SENTENCE = /[.!?…。!?]$/
17
19
 
18
20
  export function joinHintFragments(fragments: ReadonlyArray<string>): string {
19
21
  let out = ""
@@ -24,3 +26,33 @@ export function joinHintFragments(fragments: ReadonlyArray<string>): string {
24
26
  }
25
27
  return out
26
28
  }
29
+
30
+ /**
31
+ * Join prompt pieces as sentences: `". "` between two pieces, or a single
32
+ * space when the piece before already ends a sentence. Without that check a
33
+ * prompt typed with a closing period came out as
34
+ * "no watermark.. butterfly portrait lighting". Blank pieces are dropped. The
35
+ * pieces are otherwise kept verbatim, except that trailing space before a join
36
+ * is not carried into it ("cat " + "hint" gives "cat. hint").
37
+ */
38
+ export function joinSentences(pieces: ReadonlyArray<string | undefined>): string {
39
+ let out = ""
40
+ for (const piece of pieces) {
41
+ if (!piece || !piece.trim()) continue
42
+ if (!out) { out = piece; continue }
43
+ const head = out.trimEnd()
44
+ out = ENDS_SENTENCE.test(head) ? `${head} ${piece}` : `${head}. ${piece}`
45
+ }
46
+ return out
47
+ }
48
+
49
+ /**
50
+ * Append wired picker hints to a prompt, the way both DAG engines fold them:
51
+ * the hints join as clauses (`joinHintFragments`), then follow the prompt as a
52
+ * new sentence (`joinSentences`). No hints (or only blank ones) gives the
53
+ * prompt back unchanged.
54
+ */
55
+ export function appendPromptHints(prompt: string | undefined, hints: ReadonlyArray<string>): string {
56
+ const joined = joinHintFragments(hints)
57
+ return joined.trim() ? joinSentences([prompt, joined]) : (prompt ?? "")
58
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./identity-lock.js"
14
14
  export * from "./reference-rules.js"
15
15
  export * from "./term.js"
16
16
  export * from "./parameter-prompt-hint.js"
17
+ export * from "./video-own-motion.js"
17
18
  export * from "./entity-prompts.js"
18
19
  export * from "./brand-tokens.js"
19
20
  export * from "./described-references.js"
@@ -99,3 +100,6 @@ export * from "./character-motion-diagnostics.js"
99
100
 
100
101
  // --- Per-ad creative analysis for the social scraper nodes ---
101
102
  export * from "./ad-creative-analysis.js"
103
+
104
+ // --- Picker art: option pictures + topic icons (editor and API) ---
105
+ export * from "./picker-art/index.js"
@@ -36,7 +36,7 @@ import { buildStylingHints } from "./styling.js"
36
36
  import { buildTemporalHints } from "./temporal.js"
37
37
  import { composeCameraMotionHintFromConnections } from "./camera-motions.js"
38
38
  import { joinHintFragments } from "./hint-join.js"
39
- import { composeTransitionHintFromConnections, type TransitionDuration, type TransitionIntensity, type TransitionPosition, type TransitionTiming } from "./transitions.js"
39
+ import { composeTransitionHintFromConnections, readTransitionOptionValues, type TransitionDuration, type TransitionIntensity, type TransitionPosition, type TransitionTiming } from "./transitions.js"
40
40
  import { composeCharacterFxHintFromConnections, type CharacterFxDuration, type CharacterFxIntensity, type CharacterFxPosition, type CharacterFxTiming } from "./character-fx.js"
41
41
  import { composeCharacterMotionHintFromConnections, type CharacterMotionPace, type CharacterMotionPosition, type CharacterMotionTiming } from "./character-motion.js"
42
42
  import { containsMinorAgeHint, isMinorAge } from "./age-floor.js"
@@ -158,6 +158,14 @@ function byMode<T>(mode: PickerHintMode, full: T, compact: T): T {
158
158
  return mode === "compact" ? compact : full
159
159
  }
160
160
 
161
+ /** How much a shot moves (the Motion node's three steps), as the clause it
162
+ * adds to a video prompt; the compact form is the bare term. */
163
+ const MOTION_INTENSITY_HINTS: Readonly<Record<string, { readonly full: string; readonly compact: string }>> = {
164
+ subtle: { full: "subtle, gentle motion with slow, minimal movement", compact: "subtle motion" },
165
+ moderate: { full: "moderate, natural motion at an even pace", compact: "moderate motion" },
166
+ dynamic: { full: "dynamic, energetic motion with fast, pronounced movement", compact: "dynamic motion" },
167
+ }
168
+
161
169
  /**
162
170
  * Dispatch by parameter-node type to its prompt-hint string. For camera-motion,
163
171
  * pass `ctx` to include the composed start/end clauses; otherwise only the
@@ -227,8 +235,11 @@ function resolveParameterHint(
227
235
  duration: asStr(data.duration) as TransitionDuration | undefined,
228
236
  intensity: asStr(data.intensity) as TransitionIntensity | undefined,
229
237
  }
238
+ // A picked row's own options (a wipe's `wipeDirection`), off the same node data.
239
+ const optionValues = readTransitionOptionValues(data)
240
+ const hintOptions = optionValues ? { optionValues } : undefined
230
241
  if (!ctx) {
231
- return withCustomText(data, composeTransitionHintFromConnections(transitionId, [], [], timing, mode))
242
+ return withCustomText(data, composeTransitionHintFromConnections(transitionId, [], [], timing, mode, hintOptions))
232
243
  }
233
244
  const startHints: string[] = []
234
245
  const endHints: string[] = []
@@ -241,7 +252,7 @@ function resolveParameterHint(
241
252
  if (edge.targetHandle === "startState") startHints.push(hint)
242
253
  else if (edge.targetHandle === "endState") endHints.push(hint)
243
254
  }
244
- return withCustomText(data, composeTransitionHintFromConnections(transitionId, startHints, endHints, timing, mode))
255
+ return withCustomText(data, composeTransitionHintFromConnections(transitionId, startHints, endHints, timing, mode, hintOptions))
245
256
  }
246
257
 
247
258
  if (node.type === "character-fx") {
@@ -409,6 +420,12 @@ function resolveBaseHint(
409
420
  return withCustomText(data, byMode(mode, getCompositionEffectPromptHint, getCompositionEffectTerm)(asStr(data.compositionEffect)))
410
421
  case "post-process-effects":
411
422
  return withCustomText(data, buildPostProcessHints(data.postProcess, mode).join(", "))
423
+ // Motion intensity: a Generation Settings node that a video node's
424
+ // Settings input takes as a prompt fragment (video only).
425
+ case "motion": {
426
+ const hint = MOTION_INTENSITY_HINTS[asStr(data.motion)]
427
+ return hint ? byMode(mode, hint.full, hint.compact) : ""
428
+ }
412
429
 
413
430
  // Free text authored by the user, not catalog copy — there is no shorter
414
431
  // professional form to swap in, so these are identical in both modes.
package/src/person.ts CHANGED
@@ -1060,7 +1060,8 @@ export interface PersonValue {
1060
1060
  /** Specific age in years. Only consulted when `age === "age-custom"`. */
1061
1061
  customAge?: number
1062
1062
  /** Single id, or an array of up to 2 ids for mixed heritage (e.g.
1063
- * ["slavic","mediterranean"] → "of mixed Slavic and Mediterranean heritage"). */
1063
+ * ["slavic","mediterranean"] → "of mixed Slavic Eastern European and
1064
+ * Mediterranean heritage"). */
1064
1065
  ethnicity?: string | ReadonlyArray<string>
1065
1066
  /** Regional / cultural aesthetic vibe (e.g. `"cali-beach"`, `"parisienne"`,
1066
1067
  * `"kinshasa-sape"`). Composes with ethnicity, skin tone, hair, and
@@ -1238,16 +1239,31 @@ function normalizePickIds(value: unknown): string[] {
1238
1239
  */
1239
1240
  type PersonFragmentFor = (id: string | undefined | null) => string
1240
1241
 
1242
+ /**
1243
+ * The heritage an ethnicity entry names, as a prompt-safe noun: its promptHint
1244
+ * without the "of … descent" / "… heritage" wrapper ("of Mediterranean descent"
1245
+ * → "Mediterranean"). NOT the short label — that is display copy for the
1246
+ * picker grid ("Mediter.", "Pacific Isl.", "any", a bare "East"), which is what
1247
+ * two picks used to put in the prompt ("of mixed Slavic and Mediter. heritage").
1248
+ */
1249
+ function heritageNoun(id: string): string | undefined {
1250
+ const noun = (getPerson(id)?.promptHint ?? "")
1251
+ .trim()
1252
+ .replace(/^of\s+/i, "")
1253
+ .replace(/\s+(descent|heritage)$/i, "")
1254
+ .trim()
1255
+ return noun.length > 0 ? noun : undefined
1256
+ }
1257
+
1241
1258
  function buildEthnicityFragment(value: unknown, fragmentFor: PersonFragmentFor): string {
1242
- const ids = normalizePickIds(value)
1259
+ // "mixed" is itself the mixed-heritage pick; paired with a specific one it
1260
+ // adds nothing, so the pair reads as that one.
1261
+ const ids = normalizePickIds(value).filter((id, _, all) => all.length < 2 || id !== "mixed")
1243
1262
  if (ids.length === 0) return ""
1244
1263
  if (ids.length === 1) return fragmentFor(ids[0])
1245
- const labels = ids
1246
- .slice(0, 2)
1247
- .map((id) => getPerson(id)?.shortLabel ?? getPerson(id)?.label ?? "")
1248
- .filter((s): s is string => Boolean(s))
1249
- if (labels.length < 2) return fragmentFor(ids[0])
1250
- return `of mixed ${labels[0]} and ${labels[1]} heritage`
1264
+ const nouns = ids.slice(0, 2).map(heritageNoun).filter((s): s is string => Boolean(s))
1265
+ if (nouns.length < 2) return fragmentFor(ids[0])
1266
+ return `of mixed ${nouns[0]} and ${nouns[1]} heritage`
1251
1267
  }
1252
1268
 
1253
1269
  /**