@nodaro/prompts 1.53.1 → 1.54.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 (52) hide show
  1. package/dist/{chunk-GRSKB3P7.cjs → chunk-56HWABF5.cjs} +52 -2
  2. package/dist/chunk-56HWABF5.cjs.map +1 -0
  3. package/dist/{chunk-EKRO42T3.cjs → chunk-CDNW7O7H.cjs} +77 -5
  4. package/dist/chunk-CDNW7O7H.cjs.map +1 -0
  5. package/dist/{chunk-WQ7FWGZ2.js → chunk-IVRSNUYX.js} +50 -3
  6. package/dist/chunk-IVRSNUYX.js.map +1 -0
  7. package/dist/{chunk-TYRIHUID.cjs → chunk-NUC5RV4P.cjs} +5 -2
  8. package/dist/chunk-NUC5RV4P.cjs.map +1 -0
  9. package/dist/{chunk-RBRCJTDJ.js → chunk-OMRFKG4M.js} +5 -2
  10. package/dist/chunk-OMRFKG4M.js.map +1 -0
  11. package/dist/{chunk-XRCK3BCF.cjs → chunk-XUMEDRML.cjs} +52 -10
  12. package/dist/chunk-XUMEDRML.cjs.map +1 -0
  13. package/dist/{chunk-PHCYQWJP.js → chunk-YDG3KMCN.js} +52 -10
  14. package/dist/chunk-YDG3KMCN.js.map +1 -0
  15. package/dist/{chunk-XR7RAHZI.js → chunk-Z4KWM6DV.js} +77 -5
  16. package/dist/chunk-Z4KWM6DV.js.map +1 -0
  17. package/dist/entries/assembly.cjs +24 -24
  18. package/dist/entries/assembly.d.cts +9 -0
  19. package/dist/entries/assembly.d.ts +9 -0
  20. package/dist/entries/assembly.js +1 -1
  21. package/dist/entries/doctrine.cjs +46 -46
  22. package/dist/entries/doctrine.js +1 -1
  23. package/dist/entries/picker-art.cjs +3 -3
  24. package/dist/entries/picker-art.js +1 -1
  25. package/dist/entries/presets.cjs +47 -35
  26. package/dist/entries/presets.d.cts +17 -1
  27. package/dist/entries/presets.d.ts +17 -1
  28. package/dist/entries/presets.js +1 -1
  29. package/dist/index.cjs +120 -108
  30. package/dist/index.d.cts +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +2 -2
  34. package/src/__tests__/factory-preset-shelves.test.ts +30 -0
  35. package/src/__tests__/seedance-extend.test.ts +24 -25
  36. package/src/__tests__/video-reference-features.test.ts +7 -5
  37. package/src/__tests__/video-reference-seatless-assets.test.ts +132 -0
  38. package/src/factory-presets/index.ts +2 -0
  39. package/src/factory-presets/shelves.ts +50 -0
  40. package/src/picker-art/restyle-previews.ts +3 -0
  41. package/src/prompt-wizard-categories.ts +12 -3
  42. package/src/provider-prompt-doctrine.ts +44 -5
  43. package/src/seatless-assets.ts +141 -0
  44. package/src/video-reference-resolver.ts +31 -4
  45. package/dist/chunk-EKRO42T3.cjs.map +0 -1
  46. package/dist/chunk-GRSKB3P7.cjs.map +0 -1
  47. package/dist/chunk-PHCYQWJP.js.map +0 -1
  48. package/dist/chunk-RBRCJTDJ.js.map +0 -1
  49. package/dist/chunk-TYRIHUID.cjs.map +0 -1
  50. package/dist/chunk-WQ7FWGZ2.js.map +0 -1
  51. package/dist/chunk-XR7RAHZI.js.map +0 -1
  52. package/dist/chunk-XRCK3BCF.cjs.map +0 -1
@@ -302,7 +302,7 @@ const GROK_IMAGINE_DOCTRINE: ProviderPromptDoctrine = {
302
302
  tips: [
303
303
  "Keep prompts simple and direct — Subject + Action + Setting + Camera + Mood. Grok expands the prompt itself; over-specification fights the expander.",
304
304
  "Image-to-video: the input image IS the first frame (composition, identity, and style are preserved) — describe the MOTION, don't re-describe the still.",
305
- "Video 1.5: 1-15s (default 8), 480p/720p/1080p, up to 7 input images (1080p allows only one). Native audio incl. music, SFX, and lip-synced dialogue — quote the line to have it spoken.",
305
+ "Video 1.5: 1-15s (default 8), 480p/720p/1080p; exact start and end frame (one image as both = seamless loop), up to 4 in-between frames. Native audio incl. dialogue — quote the line to have it spoken.",
306
306
  "Aspect ratio applies to text runs (1:1/16:9/9:16/3:2/2:3/auto); a single input image locks the output to the image's own aspect.",
307
307
  ],
308
308
  doctrine: `Prompt structure (xAI's guidance is minimal by design — the model auto-expands prompts):
@@ -311,18 +311,18 @@ descriptions of static/unchanged parts — spend the words on what MOVES.
311
311
 
312
312
  **Image-to-video (the primary mode)**
313
313
  - The input image is the FIRST FRAME, not a loose reference: composition, subject identity, and visual style carry over. Describe motion and camera only ("she turns toward the window as the camera slowly pushes in"); re-describing the still wastes adherence.
314
- - grok-imagine-video-1.5 accepts up to 7 images (identity/scene references beyond the first frame); at 1080p only ONE image is allowed.
314
+ - grok-imagine-video-1.5 takes frames, not loose references: the start frame is the exact FIRST frame, an end frame the exact LAST frame, and the same image wired as both makes a seamless loop. Its Image References are up to 4 IN-BETWEEN frames, in order, placed evenly over the clip at whole seconds (one per second inside it — a 3s clip takes 2); extra frames are left out. Describe the motion that carries the shot from one frame to the next; the frames cannot be named in the prompt, so refer to them in words ("by the midpoint she has reached the door").
315
315
  - grok-i2v with reference images: images are addressed \`@image(n)\` in array order. Your references come first (\`@image(1)\` is your first reference); a wired start frame is appended after them and the platform names it as the opening frame.
316
316
 
317
317
  **Audio (video-1.5)**
318
- - Native audio generates with the clip — background music, SFX, and lip-synced dialogue. Quote the spoken line; describe the music/SFX plainly. There is no audio toggle on the KIE contract — cue (or exclude) sound in the prompt text.
318
+ - Native audio generates with the clip — background music, SFX, and lip-synced dialogue. Quote the spoken line; describe the music/SFX plainly. The platform offers no audio toggle for it — cue (or exclude) sound in the prompt text.
319
319
 
320
320
  **Durations / tiers**
321
- - grok-i2v: 6 or 10 seconds. grok-imagine-video-1.5: 1-15 seconds in 1s steps (default 8), 480p (default) / 720p / 1080p. Prompt cap 4096 chars — but shorter is better here.
321
+ - grok-i2v: 6 or 10 seconds. grok-imagine-video-1.5: 1-15 seconds in 1s steps (default 8), 480p (default) / 720p / 1080p. At 1080p it takes the start frame ALONE — a run with an end frame, a loop or in-between frames is refused before any credits are held, so use 720p for those. Prompt cap 4096 chars — but shorter is better here.
322
322
 
323
323
  Sources: KIE Grok Imagine contracts (docs.kie.ai/market/grok-imagine/image-to-video,
324
324
  docs.kie.ai/market/grok-imagine/1-5-preview), xAI Grok Imagine 1.5 release notes
325
- (x.ai/news/grok-imagine-1-5). Captured 2026-08-09.`,
325
+ (x.ai/news/grok-imagine-1-5). Captured 2026-08-09; frames and in-between frames 2026-11-11.`,
326
326
  }
327
327
 
328
328
  const WAN_DOCTRINE: ProviderPromptDoctrine = {
@@ -494,6 +494,44 @@ Source: KIE Kling 4.0 market contracts (docs.kie.ai/market/kling/v4-reference-to
494
494
  docs.kie.ai/market/kling/v4-keyframe-to-video), live-probed 2026-10-23.`,
495
495
  }
496
496
 
497
+ // Kling 4.0 Flash — one id over KIE's two Flash endpoints, picked by what is
498
+ // wired (a start frame alone → keyframe endpoint; anything else → reference
499
+ // endpoint). KIE contract facts plus the platform-standard structure, as for
500
+ // Kling 4.0. The provider runner names subjects (`@Name`), binds each voice
501
+ // clip to a subject, and drops the `@` from any undeclared mention.
502
+ const KLING_4_FLASH_DOCTRINE: ProviderPromptDoctrine = {
503
+ providers: ["kling-4-flash"],
504
+ heading: "Kling 4.0 Flash (kling-4-flash)",
505
+ tips: [
506
+ "A start frame alone is the exact opening frame (@first_frame). Beside references it joins the reference images, like kling-4.",
507
+ "References: up to 10 images, 5 videos (2-20s together) and 7 characters (15 in all). Name each: @image_1, @video_1, a character's @Name.",
508
+ "No standalone voice: voice clip N is the voice of wired character N, if that character has 2-4 images (else the clip is dropped); write the character, never @audio_N.",
509
+ "3-20s (default 5), 720p only, native audio always on, no end frame. Prompt cap 8,000 chars. Aspect auto / 16:9 / 9:16 / 1:1 / 21:9 (a start frame alone sets its own shape).",
510
+ ],
511
+ doctrine: `Prompt structure (no public Kling 4.0 Flash prompt guide is captured — the KIE API contract is
512
+ the doctrine source; platform-standard structure applies):
513
+ subject → action → scene/environment → lighting → camera movement → style → constraints.
514
+
515
+ **One model, two endpoints (picked by what is connected)**
516
+ - A start frame with nothing else — no reference image, no reference video, no character — runs keyframe-to-video: the frame is the exact opening frame and the clip takes its shape. Name it @first_frame.
517
+ - Anything else — text only, reference images, reference videos, characters, or a start frame beside any of them — runs reference-to-video. A start frame there joins the reference images and is named as the opening frame, as on kling-4.
518
+
519
+ **Naming materials in the prompt**
520
+ - @image_1 … for reference images, @video_1 … for reference videos, a character by its own name (@Maya). Kling refuses a prompt that names a material the request does not carry; the platform removes the "@" from such a mention so the run still starts.
521
+ - There are no standalone voices. Reference audio clip N becomes the voice of character N, in the order the characters are wired. A character needs 2-4 images to carry a voice: a clip whose character has one image, and any clip past the last character, is dropped (the run still starts). Write who speaks by the character's name ("@Maya says…"), never @audio_1.
522
+
523
+ **Limits**
524
+ - Reference images 10, reference videos 5, characters 7 — 15 in all. Reference videos together must be 2-20 seconds; a longer set is refused before the run starts.
525
+ - No end frame and no in-between frames: describe how the shot ends in words.
526
+
527
+ **Duration, resolution, audio**
528
+ - 3-20 seconds (default 5); 720p only; aspect auto (follows the materials, 16:9 with none) / 16:9 / 9:16 / 1:1 / 21:9. Prompts up to 8,000 characters.
529
+ - Native audio is always generated; keep-original-sound keeps a reference video's own audio. Not yet verified for scripted lines here, so plan key dialogue as a TTS + lip-sync pass.
530
+
531
+ Source: KIE Kling 4.0 Flash market contracts (docs.kie.ai/market/kling/v4-flash-reference-to-video,
532
+ docs.kie.ai/market/kling/v4-flash-keyframe-to-video), read 2026-11-11.`,
533
+ }
534
+
497
535
  const HAPPYHORSE_DOCTRINE: ProviderPromptDoctrine = {
498
536
  providers: ["happyhorse", "happyhorse-i2v", "happyhorse-ref2v", "happyhorse-edit"],
499
537
  heading: "HappyHorse 1.1 (happyhorse, happyhorse-i2v, happyhorse-ref2v)",
@@ -555,6 +593,7 @@ export const PROVIDER_PROMPT_DOCTRINES: readonly ProviderPromptDoctrine[] = [
555
593
  WAN_3_DOCTRINE,
556
594
  VIDU_Q4_DOCTRINE,
557
595
  KLING_4_DOCTRINE,
596
+ KLING_4_FLASH_DOCTRINE,
558
597
  HAPPYHORSE_DOCTRINE,
559
598
  RUNWAY_KIE_DOCTRINE,
560
599
  ]
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Assets that take no image seat: the reference walk for a video model whose
3
+ * image references are its FRAMES (a `keyframes` model — Grok Imagine Video 1.5,
4
+ * Kling 4.0 Keyframes; decided 2026-11-11). Only what is wired into the node's
5
+ * Image References becomes a frame, so a wired character, a character extra or
6
+ * an entity attaches no picture and is numbered nowhere: no `@image_N` may point
7
+ * at a picture that is never sent.
8
+ *
9
+ * What a character still gives is its identity text — its name and description,
10
+ * the one line both reference formats render for a described reference — and a
11
+ * mention of it (`@kira:1`, `@kira:1:smile`) or a `{ref:<id>}` naming it reads as
12
+ * its name. A non-character extra or entity adds nothing.
13
+ *
14
+ * Pure: no I/O, no mutation of inputs. `resolveVideoReferenceCore` calls it when
15
+ * its caller passes `assetsTakeNoSeat`.
16
+ */
17
+
18
+ import { DEFAULT_USAGE_MODE, findCharacterMentionTokens, type UsageMode } from "@nodaro/shared/entities"
19
+ import type { ConnectedReference, DescribedReference } from "@nodaro/shared/types"
20
+
21
+ /** The fields of a video extra this walk reads (structural: `VideoExtraRef` fits). */
22
+ export interface SeatlessExtraRef {
23
+ id?: string
24
+ characterSlug?: string
25
+ description?: string
26
+ descriptionOverride?: string
27
+ usageMode?: UsageMode
28
+ elementInjection?: string | null
29
+ }
30
+
31
+ /** The fields of a character's metadata this walk reads (structural: `CharacterMeta` fits). */
32
+ export interface SeatlessCharacterMeta {
33
+ characterName?: string
34
+ defaultUsageMode?: UsageMode
35
+ canonicalDescription?: string
36
+ }
37
+
38
+ export interface SeatlessAssets {
39
+ /** The prompt with every wired character's mention replaced by its name. */
40
+ prompt: string | undefined
41
+ /** One name + description per character with an identity to give, in wiring order. */
42
+ identities: DescribedReference[]
43
+ /** `{ref:<id>}` names for the character extras, which the walk seats nowhere. */
44
+ extraNamesById: Map<string, string>
45
+ }
46
+
47
+ /**
48
+ * The identity text a seatless character gives. Mirrors what the legacy
49
+ * canonical line puts after the name — the per-use override in every mode that
50
+ * renders a line, else the stored description in the modes that carry it, then
51
+ * the wired element injection — minus the usage directive, which instructs how
52
+ * to use a picture the model does not receive.
53
+ */
54
+ function identityDescription(
55
+ mode: UsageMode,
56
+ override: string | null | undefined,
57
+ canonical: string | null | undefined,
58
+ injection: string | null | undefined,
59
+ ): string {
60
+ if (mode === "none" || mode === "name") return ""
61
+ const parts: string[] = []
62
+ const o = override?.trim()
63
+ const c = canonical?.trim()
64
+ if (o) parts.push(o)
65
+ else if ((mode === "identical" || mode === "face-pose") && c) parts.push(c)
66
+ const i = injection?.trim()
67
+ if (i) parts.push(i)
68
+ return parts.join(". ")
69
+ }
70
+
71
+ export function resolveSeatlessAssets(args: {
72
+ prompt: string | undefined
73
+ /** Every wired character reference, BEFORE canonical suppression. */
74
+ wiredCharRefs: readonly ConnectedReference[]
75
+ /** Character slugs whose canonical fallback the user hid. */
76
+ suppressedCanonicalSlugs: ReadonlySet<string>
77
+ extraRefs?: readonly SeatlessExtraRef[]
78
+ lookupCharacterBySlug?: (slug: string) => SeatlessCharacterMeta | undefined
79
+ }): SeatlessAssets {
80
+ // The reference each wired character is described from: its canonical entry,
81
+ // else the first entry wired for it.
82
+ const bySlug = new Map<string, ConnectedReference>()
83
+ for (const r of args.wiredCharRefs) {
84
+ if (r.source !== "wired-character" || !r.characterSlug) continue
85
+ const known = bySlug.get(r.characterSlug)
86
+ if (!known || (known.variantSlug && !r.variantSlug)) bySlug.set(r.characterSlug, r)
87
+ }
88
+ const nameOf = (slug: string): string => {
89
+ const r = bySlug.get(slug)
90
+ return r?.defaultName || r?.characterSlug || slug
91
+ }
92
+
93
+ // Mentions read as the character's name (right to left, so offsets hold).
94
+ let prompt = args.prompt
95
+ const mentioned = new Set<string>()
96
+ if (prompt && bySlug.size > 0) {
97
+ const tokens = findCharacterMentionTokens(prompt, [...bySlug.keys()])
98
+ for (const t of [...tokens].sort((a, b) => b.offset - a.offset)) {
99
+ mentioned.add(t.characterSlug)
100
+ prompt = prompt.slice(0, t.offset) + nameOf(t.characterSlug) + prompt.slice(t.offset + t.token.length)
101
+ }
102
+ }
103
+
104
+ const identities: DescribedReference[] = []
105
+ const described = new Set<string>()
106
+ const describe = (slug: string, name: string, description: string): void => {
107
+ if (described.has(slug)) return
108
+ described.add(slug)
109
+ if (description) identities.push({ name, description })
110
+ }
111
+
112
+ // A wired character gives its identity unless the user hid its canonical
113
+ // fallback and the prompt does not mention it either.
114
+ for (const [slug, r] of bySlug) {
115
+ const shown = !args.suppressedCanonicalSlugs.has(slug) || mentioned.has(slug)
116
+ if (!shown) continue
117
+ describe(
118
+ slug,
119
+ nameOf(slug),
120
+ identityDescription(r.defaultUsageMode ?? DEFAULT_USAGE_MODE, r.descriptionOverride, r.characterCanonicalDescription, r.elementInjection),
121
+ )
122
+ }
123
+
124
+ // A character extra (a variant picked on the node) speaks for its character
125
+ // once; a non-character extra has no frame and no text.
126
+ const extraNamesById = new Map<string, string>()
127
+ for (const ex of args.extraRefs ?? []) {
128
+ if (!ex.characterSlug) continue
129
+ const meta = args.lookupCharacterBySlug?.(ex.characterSlug)
130
+ const name = bySlug.has(ex.characterSlug) ? nameOf(ex.characterSlug) : meta?.characterName || ex.characterSlug
131
+ if (ex.id) extraNamesById.set(ex.id, name)
132
+ const own = (ex.descriptionOverride ?? "").trim() || (ex.description ?? "").trim()
133
+ describe(
134
+ ex.characterSlug,
135
+ name,
136
+ identityDescription(ex.usageMode ?? meta?.defaultUsageMode ?? DEFAULT_USAGE_MODE, own, meta?.canonicalDescription, ex.elementInjection),
137
+ )
138
+ }
139
+
140
+ return { prompt, identities, extraNamesById }
141
+ }
@@ -50,6 +50,7 @@ import { REF_BINDING } from "./ref-binding.js"
50
50
  import { resolveRefIdTokens } from "./ref-id-tokens.js"
51
51
  import { insertBeforeStyleSection } from "./prompt-style-section.js"
52
52
  import { tidyReferenceTokenGaps, unboundReferenceTokenText } from "./unbound-reference-tokens.js"
53
+ import { resolveSeatlessAssets } from "./seatless-assets.js"
53
54
 
54
55
  // The binding surface string and the id-addressed token resolver live in their
55
56
  // own modules (see them for the contracts); re-exported here so every existing
@@ -268,6 +269,15 @@ export interface ResolveVideoReferenceCoreArgs {
268
269
  */
269
270
  videoCaptions?: readonly string[]
270
271
  audioCaptions?: readonly string[]
272
+ /**
273
+ * No asset takes an image seat: set for a model whose image references are
274
+ * its FRAMES (the `keyframes` feature), where only what is wired into Image
275
+ * References is sent. Wired characters and extras attach no URL and are
276
+ * numbered nowhere; a character gives its name and description in both
277
+ * formats, and its mentions and `{ref:<id>}` read as its name. See
278
+ * `seatless-assets.ts`.
279
+ */
280
+ assetsTakeNoSeat?: boolean
271
281
  }
272
282
 
273
283
  /** Result of the HYBRID mention pass — inline role phrases + surfaced opt-in
@@ -477,8 +487,21 @@ export function resolveVideoReferenceCore(
477
487
  // below and the main assembly have to carry them. They attach no URL and claim
478
488
  // no `@image_N` seat, so they take no part in the numbering walk. Empty for
479
489
  // every caller that sends neither → every existing output is untouched.
490
+ // On a frames model (`assetsTakeNoSeat`) the wired characters join them as
491
+ // identity lines: no asset is seated, so the whole walk below is skipped.
492
+ const seatless = args.assetsTakeNoSeat === true
493
+ ? resolveSeatlessAssets({
494
+ prompt: args.prompt,
495
+ wiredCharRefs: args.wiredCharRefs,
496
+ suppressedCanonicalSlugs: new Set(args.suppressedCanonicalCharacterIds ?? []),
497
+ extraRefs: args.extraRefs,
498
+ lookupCharacterBySlug: args.lookupCharacterBySlug,
499
+ })
500
+ : undefined
480
501
  const describedAndCaptionLines = [
481
- ...renderDescribedReferenceLines(args.describedReferences),
502
+ ...renderDescribedReferenceLines(
503
+ seatless ? [...seatless.identities, ...(args.describedReferences ?? [])] : args.describedReferences,
504
+ ),
482
505
  ...renderReferenceCaptionLines(args.videoCaptions, args.audioCaptions, {
483
506
  video: args.videoRefCount ?? 0,
484
507
  audio: args.audioRefCount ?? 0,
@@ -510,6 +533,8 @@ export function resolveVideoReferenceCore(
510
533
  for (const ex of args.extraRefs ?? []) {
511
534
  if (ex.id && !nameByRefId.has(ex.id)) nameByRefId.set(ex.id, "")
512
535
  }
536
+ // Seated nowhere, a character extra's `{ref:<id>}` reads as its character.
537
+ for (const [id, name] of seatless?.extraNamesById ?? []) nameByRefId.set(id, name)
513
538
  for (const [id, name] of args.refNamesById ?? []) {
514
539
  if (id) nameByRefId.set(id, name)
515
540
  }
@@ -517,8 +542,10 @@ export function resolveVideoReferenceCore(
517
542
  // recovered from URLs afterwards (the walk counts a duplicate-URL extra that
518
543
  // `merged` dedups, so URL → index is ambiguous; id → position is not).
519
544
  const slotByRefId = new Map<string, number>()
520
- if (wiredCharRefs.length === 0 && !hasExtras) {
521
- // No wired chars / extras, but the node can still carry plain base reference
545
+ if (seatless || (wiredCharRefs.length === 0 && !hasExtras)) {
546
+ // No wired chars / extras — or none that may take a seat (`seatless`, whose
547
+ // characters already ride `describedAndCaptionLines` and whose mentions are
548
+ // already names) — but the node can still carry plain base reference
522
549
  // images (leadingRefUrls), so `{image:N}` body tokens MUST still resolve. The
523
550
  // count is the leading-ref count (or the legacy `imageRefCount` when no
524
551
  // leading refs were passed); the leading URLs are returned for the payload.
@@ -528,7 +555,7 @@ export function resolveVideoReferenceCore(
528
555
  // leadingRefUrls mode counts the leading refs; ordinalOffset mode counts the
529
556
  // caller-owned leading refs the offset stands in for.
530
557
  const resolved = resolveReferenceTokens(
531
- resolveRefIdTokens(args.prompt, { slotById: slotByRefId, nameById: nameByRefId, imageCount: counts.image }),
558
+ resolveRefIdTokens(seatless ? seatless.prompt : args.prompt, { slotById: slotByRefId, nameById: nameByRefId, imageCount: counts.image }),
532
559
  counts,
533
560
  )
534
561
  return {