@nodaro/prompts 1.39.0 → 1.40.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 (66) hide show
  1. package/dist/{chunk-XIWWDGT5.cjs → chunk-2EW5B7ZV.cjs} +7 -7
  2. package/dist/{chunk-XIWWDGT5.cjs.map → chunk-2EW5B7ZV.cjs.map} +1 -1
  3. package/dist/{chunk-22KZOAGL.cjs → chunk-C3NHNECH.cjs} +86 -48
  4. package/dist/chunk-C3NHNECH.cjs.map +1 -0
  5. package/dist/{chunk-XRLOMTBZ.js → chunk-C46256GZ.js} +43 -5
  6. package/dist/chunk-C46256GZ.js.map +1 -0
  7. package/dist/{chunk-NMWKXAYA.cjs → chunk-DQFQJECU.cjs} +43 -5
  8. package/dist/chunk-DQFQJECU.cjs.map +1 -0
  9. package/dist/{chunk-XU54YJXQ.js → chunk-HQTHDTHN.js} +85 -49
  10. package/dist/chunk-HQTHDTHN.js.map +1 -0
  11. package/dist/{chunk-KUBZVWME.cjs → chunk-LXPKTI2K.cjs} +12 -5
  12. package/dist/chunk-LXPKTI2K.cjs.map +1 -0
  13. package/dist/{chunk-3INXNDV7.cjs → chunk-LZ53F5BN.cjs} +8 -8
  14. package/dist/chunk-LZ53F5BN.cjs.map +1 -0
  15. package/dist/{chunk-7XMTVYS5.js → chunk-N6VMPG5S.js} +12 -6
  16. package/dist/chunk-N6VMPG5S.js.map +1 -0
  17. package/dist/{chunk-6MPZHFIS.js → chunk-RB2F5JQF.js} +5 -5
  18. package/dist/chunk-RB2F5JQF.js.map +1 -0
  19. package/dist/{chunk-Z3SZXWRU.js → chunk-YS6HOWMW.js} +3 -3
  20. package/dist/{chunk-Z3SZXWRU.js.map → chunk-YS6HOWMW.js.map} +1 -1
  21. package/dist/entries/assembly.cjs +30 -22
  22. package/dist/entries/assembly.d.cts +90 -34
  23. package/dist/entries/assembly.d.ts +90 -34
  24. package/dist/entries/assembly.js +1 -1
  25. package/dist/entries/doctrine.cjs +46 -46
  26. package/dist/entries/doctrine.js +1 -1
  27. package/dist/entries/presets.cjs +32 -32
  28. package/dist/entries/presets.d.cts +2 -0
  29. package/dist/entries/presets.d.ts +2 -0
  30. package/dist/entries/presets.js +2 -2
  31. package/dist/entries/prompt-fields.cjs +26 -26
  32. package/dist/entries/prompt-fields.d.cts +2 -1
  33. package/dist/entries/prompt-fields.d.ts +2 -1
  34. package/dist/entries/prompt-fields.js +2 -2
  35. package/dist/index.cjs +133 -125
  36. package/dist/index.d.cts +1 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +5 -5
  39. package/package.json +2 -2
  40. package/src/__tests__/factory-presets-prototype-keys.test.ts +43 -0
  41. package/src/__tests__/frame-reference-slots.test.ts +43 -0
  42. package/src/__tests__/gemini-omni-inputs.test.ts +12 -10
  43. package/src/__tests__/provider-prompt-doctrine.test.ts +14 -0
  44. package/src/__tests__/veo-i2v-inputs.test.ts +16 -11
  45. package/src/entries/assembly.ts +1 -0
  46. package/src/factory-presets/index.ts +4 -1
  47. package/src/factory-presets/references.ts +2 -1
  48. package/src/frame-reference-slots.ts +92 -0
  49. package/src/gemini-omni-inputs.ts +23 -22
  50. package/src/node-prompt-fields.ts +5 -3
  51. package/src/own-entry.ts +11 -0
  52. package/src/prompt-wizard-categories.ts +2 -0
  53. package/src/provider-prompt-doctrine.ts +45 -3
  54. package/src/ref-binding.ts +28 -3
  55. package/src/resolve-prompt.ts +2 -1
  56. package/src/seedance-2-inputs.ts +15 -40
  57. package/src/veo-i2v-inputs.ts +24 -17
  58. package/src/video-reference-resolver.ts +1 -1
  59. package/dist/chunk-22KZOAGL.cjs.map +0 -1
  60. package/dist/chunk-3INXNDV7.cjs.map +0 -1
  61. package/dist/chunk-6MPZHFIS.js.map +0 -1
  62. package/dist/chunk-7XMTVYS5.js.map +0 -1
  63. package/dist/chunk-KUBZVWME.cjs.map +0 -1
  64. package/dist/chunk-NMWKXAYA.cjs.map +0 -1
  65. package/dist/chunk-XRLOMTBZ.js.map +0 -1
  66. package/dist/chunk-XU54YJXQ.js.map +0 -1
@@ -245,7 +245,7 @@ const GEMINI_OMNI_DOCTRINE: ProviderPromptDoctrine = {
245
245
  "Multimodal Google video with native audio: text-to-video, image-to-video, and video-edit through the same prompt surface. 4/6/8/10s; 720p/1080p or 4K tier.",
246
246
  "Structure like the platform default: subject → action → scene → lighting → camera → style. Quote dialogue lines to have them spoken; describe SFX/ambience plainly in the prompt.",
247
247
  "Text-to-video REQUIRES a concrete aspect ratio (the API hard-rejects a missing one); image runs infer aspect from the input.",
248
- "Reference images ride along as additional imageUrls — bind them in the prompt ('the woman from the first image'). Video-edit: wire a source clip and describe the change, not the whole scene.",
248
+ "Reference images ride in image_urls, your references first — bind them by ordinal ('the woman from @image_1'); a wired start frame is appended after them and bound as the opening frame. Video-edit: describe the change.",
249
249
  ],
250
250
  doctrine: `Prompt structure (no public Google prompt guide exists for the Omni video endpoint —
251
251
  the API contract is the doctrine source, like MiniMax H3; structure guidance mirrors the
@@ -254,7 +254,7 @@ subject → action → scene/environment → lighting → camera movement → st
254
254
 
255
255
  **Modes (picked from the wired inputs)**
256
256
  - Nothing visual → text-to-video. A concrete aspect ratio is REQUIRED — the API hard-rejects a missing one (Nodaro sends the node's ratio; there is no adaptive).
257
- - Image(s) wired → image-to-video: the first image anchors the scene; extra images are references — bind each in the prompt ("the woman from the first image", "the interior from the second image").
257
+ - Image(s) wired → image-to-video. Your reference images come FIRST, in your order (\`@image_1\`, \`@image_2\`, …) — bind each in the prompt ("the woman from @image_1", "the interior from @image_2"); a wired start frame is appended after them and the platform names it as the opening frame.
258
258
  - Source video wired → video-edit (served through the same handle): describe the CHANGE ("replace the daylight with dusk, keep the motion and framing"), not a full re-description.
259
259
 
260
260
  **Audio (native)**
@@ -284,6 +284,7 @@ descriptions of static/unchanged parts — spend the words on what MOVES.
284
284
  **Image-to-video (the primary mode)**
285
285
  - 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.
286
286
  - grok-imagine-video-1.5 accepts up to 7 images (identity/scene references beyond the first frame); at 1080p only ONE image is allowed.
287
+ - 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.
287
288
 
288
289
  **Audio (video-1.5)**
289
290
  - 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.
@@ -382,6 +383,46 @@ Source: KIE Wan 3.0 market contract (docs.kie.ai/market/wan/3-0-video,
382
383
  docs.kie.ai/market/wan/3-0-video-prime). Captured 2026-09-01.`,
383
384
  }
384
385
 
386
+ // Vidu Q4 — no public Q4 prompt guide is captured, so like Wan 3.0 / MiniMax H3
387
+ // the body is KIE contract facts plus the platform-standard structure. The
388
+ // contract names references "reference image 2" / "reference audio 1"; the
389
+ // editor's reference tokens emit the platform-wide `@image_N` / `@audio_N`
390
+ // binding instead (see ref-binding.ts), which is what this doctrine teaches.
391
+ const VIDU_Q4_DOCTRINE: ProviderPromptDoctrine = {
392
+ providers: ["vidu-q4"],
393
+ heading: "Vidu Q4 (vidu-q4)",
394
+ tips: [
395
+ "A start frame alone animates that frame (prompt optional). Any reference image or audio switches to reference mode: the prompt is REQUIRED and the start frame joins the reference images.",
396
+ "Reference mode: 1-15 images, up to 3 audio clips (MP3 only, 3-12s each). Name each by ordinal — \"the man from @image_2 says … in the voice of @audio_1\".",
397
+ "No text-only runs and no end frame: wire a start frame or at least one reference image.",
398
+ "3-16s (default 5); 540p / 720p (default) / 1080p / 2K / 4K; reference runs take 16:9 / 9:16 / 1:1 / 4:3 / 3:4. Prompt cap 20,000 chars.",
399
+ "`audio` is ON by default (synchronized sound). Cue the sound you want or state the exclusion; dialogue is not verified here, so plan key lines as a TTS + lip-sync pass.",
400
+ ],
401
+ doctrine: `Prompt structure (no public Vidu Q4 prompt guide is captured — the KIE API contract is the
402
+ doctrine source; platform-standard structure applies):
403
+ subject → action → scene/environment → lighting → camera movement → style → constraints.
404
+
405
+ **Modes (picked by what is wired)**
406
+ - Start frame only → image-to-video. The frame anchors the shot and its aspect ratio; describe MOTION and camera, not the still. The prompt is optional.
407
+ - Any reference image or reference audio → reference-to-video. The prompt is REQUIRED. A wired start frame cannot be sent as a frame here, so the platform adds it to the reference images (after the caller's own, ordinals unchanged) and names it in the prompt as the opening frame.
408
+ - There is no text-only mode and no first+last frame mode.
409
+
410
+ **Reference binding**
411
+ - Refer to inputs by ordinal in attachment order. In the editor the reference tokens emit "@image_N" / "@audio_N" — write the binding explicitly ("the woman from @image_1 turns to the man from @image_2 and says …, in the voice of @audio_1").
412
+ - Caps: 1-15 reference images (JPG / PNG / WEBP) and 0-3 reference audio clips. Audio must be MP3, each 3-12 seconds — the platform refuses any other format or length before the run starts.
413
+
414
+ **Duration, resolution, aspect**
415
+ - 3-16 seconds (default 5). 540p / 720p (default) / 1080p / 2K / 4K.
416
+ - Image-to-video has no aspect setting — the output inherits the start frame's shape. Reference runs take 16:9 (default) / 9:16 / 1:1 / 4:3 / 3:4.
417
+ - Prompts up to 20,000 characters.
418
+
419
+ **Audio**
420
+ - The "audio" boolean defaults ON and generates synchronized sound with the clip (the contract says "dialogue and sound effects"). Describe the soundscape plainly or state the exclusion, or turn the toggle off for a silent clip.
421
+
422
+ Source: KIE Vidu Q4 market contracts (docs.kie.ai/market/vidu-q4/image-to-video,
423
+ docs.kie.ai/market/vidu-q4/reference-to-video). Captured 2026-10-17.`,
424
+ }
425
+
385
426
  const HAPPYHORSE_DOCTRINE: ProviderPromptDoctrine = {
386
427
  providers: ["happyhorse", "happyhorse-i2v", "happyhorse-ref2v", "happyhorse-edit"],
387
428
  heading: "HappyHorse 1.1 (happyhorse, happyhorse-i2v, happyhorse-ref2v)",
@@ -398,7 +439,7 @@ subject → action → scene/environment → lighting → camera movement → st
398
439
  **Contract facts (KIE, per-mode pages)**
399
440
  - Prompts: any language, up to 5000 non-Chinese / 2500 Chinese characters — excess is TRUNCATED silently, so put the load-bearing content first.
400
441
  - Duration 3-15s (default 5), billed per second. Resolution 720p / 1080p (default). Aspect 16:9 (default) / 9:16 / 1:1 / 4:3 / 3:4.
401
- - Modes: text-to-video (happyhorse), image-to-video (happyhorse-i2v), reference-to-video (happyhorse-ref2v) — ref2v preserves wired identities; name each reference in the prompt so the binding is explicit.
442
+ - Modes: text-to-video (happyhorse), image-to-video (happyhorse-i2v), reference-to-video (happyhorse-ref2v) — ref2v preserves wired identities; name each reference in the prompt so the binding is explicit, as \`[Image N]\` in array order: your references come first (\`[Image 1]\` is your first), and a wired start frame is appended after them and named as the opening frame.
402
443
 
403
444
  **Style guidance (platform-standard, honestly generic)**
404
445
  - One camera movement per shot; physical, quantified action ("slowly raises a hand") over abstract emotion words; state exclusions ("no on-screen text, no watermark") in the prompt.
@@ -440,6 +481,7 @@ export const PROVIDER_PROMPT_DOCTRINES: readonly ProviderPromptDoctrine[] = [
440
481
  GROK_IMAGINE_DOCTRINE,
441
482
  WAN_DOCTRINE,
442
483
  WAN_3_DOCTRINE,
484
+ VIDU_Q4_DOCTRINE,
443
485
  HAPPYHORSE_DOCTRINE,
444
486
  RUNWAY_KIE_DOCTRINE,
445
487
  ]
@@ -28,10 +28,14 @@
28
28
  * says the two things a multimodal model needs to hear — match exactly, and
29
29
  * these are not frames. One spelling; both resolvers ride it.
30
30
  */
31
- export function identityRefsSentence(firstOrdinal: number, lastOrdinal: number): string {
31
+ export function identityRefsSentence(
32
+ firstOrdinal: number,
33
+ lastOrdinal: number,
34
+ ordinal: (n: number) => string = REF_BINDING.ordinal,
35
+ ): string {
32
36
  return firstOrdinal === lastOrdinal
33
- ? `${REF_BINDING.ordinal(firstOrdinal)} is an identity reference for this shot's subjects — match its subject's exact appearance; it is not a frame.`
34
- : `${REF_BINDING.ordinal(firstOrdinal)} through ${REF_BINDING.ordinal(lastOrdinal)} are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.`
37
+ ? `${ordinal(firstOrdinal)} is an identity reference for this shot's subjects — match its subject's exact appearance; it is not a frame.`
38
+ : `${ordinal(firstOrdinal)} through ${ordinal(lastOrdinal)} are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.`
35
39
  }
36
40
 
37
41
  export const REF_BINDING = {
@@ -44,6 +48,27 @@ export const REF_BINDING = {
44
48
  `Use @image_${n} as the ${role} (${role === "opening" ? "first" : "last"}) frame of the video.`,
45
49
  } as const
46
50
 
51
+ /** A binding sentence's ordinal + frame directive in one provider syntax. */
52
+ export interface ImageBinding {
53
+ ordinal: (n: number) => string
54
+ frame: (n: number, role: "opening" | "closing") => string
55
+ }
56
+
57
+ /**
58
+ * The image binding in a provider's OWN reference syntax
59
+ * (`videoProviderReferenceSyntax` in @nodaro/shared). `@image_N` IS
60
+ * {@link REF_BINDING} — the platform swap point, byte for byte; the other two
61
+ * spell the same sentence in the form the provider documents.
62
+ */
63
+ export function imageBindingFor(syntax: "@image_N" | "@image(n)" | "[Image N]"): ImageBinding {
64
+ if (syntax === "@image_N") return { ordinal: REF_BINDING.ordinal, frame: REF_BINDING.frame }
65
+ const ordinal = syntax === "@image(n)" ? (n: number) => `@image(${n})` : (n: number) => `[Image ${n}]`
66
+ return {
67
+ ordinal,
68
+ frame: (n, role) => `Use ${ordinal(n)} as the ${role} (${role === "opening" ? "first" : "last"}) frame of the video.`,
69
+ }
70
+ }
71
+
47
72
  /**
48
73
  * The instruction that makes Seedance EDIT a wired clip rather than use it as a
49
74
  * style reference — the Video to Video node's Seedance lane prepends it to the
@@ -2,6 +2,7 @@ import { resolveNodeRefs } from "@nodaro/shared/workflow"
2
2
  import { readPromptAffixes, type PromptAffixes } from "@nodaro/shared/nodes"
3
3
  import { SOCIAL_POST_NODE_TYPES } from "@nodaro/shared/social"
4
4
  import { joinSentences } from "./hint-join.js"
5
+ import { ownEntry } from "./own-entry.js"
5
6
 
6
7
  export interface ResolvePromptArgs {
7
8
  override?: string
@@ -165,7 +166,7 @@ export function computeNodePrompt(
165
166
  ? [data.directText as string | undefined]
166
167
  : []
167
168
  } else {
168
- const fields = NODE_PROMPT_CANDIDATE_FIELDS[nodeType] ?? ["prompt"]
169
+ const fields = ownEntry(NODE_PROMPT_CANDIDATE_FIELDS, nodeType) ?? ["prompt"]
169
170
  typed = fields.map((f) => data[f] as string | undefined)
170
171
  }
171
172
  return resolvePrompt({ override, typed, wired, refMap, appendWired, affixes: readPromptAffixes(data) })
@@ -1,5 +1,5 @@
1
1
  import { SEEDANCE_2_REF_LIMITS } from "@nodaro/shared/models"
2
- import { REF_BINDING } from "./video-reference-resolver.js"
2
+ import { placeFramesAfterReferences } from "./frame-reference-slots.js"
3
3
 
4
4
  export type Seedance2Mode = "first-frame" | "first-last-frame" | "reference"
5
5
 
@@ -46,18 +46,7 @@ const cleanList = (xs?: readonly string[]): string[] =>
46
46
  * reference_image_urls (after the user's own images, so their @Image ordinals do
47
47
  * not shift) and named in a doctrine-compliant prompt suffix.
48
48
  */
49
- /** True when the prompt ALREADY binds an image as the first/opening frame —
50
- * e.g. the gvp overlap transport's colon-position directive ("use @image_1
51
- * as the first frame, it is the last keyframe of @video_1"). Field finding
52
- * (2026-07-20): the first-frame directive only works adjacent to the extend
53
- * colon; a duplicate sentence at the END of the prompt (this resolver's
54
- * default position) dilutes it back into coin-flip behavior — so when the
55
- * prompt carries its own binding, the suffix is suppressed (the frame image
56
- * itself still rides the reference list). */
57
- export function promptBindsFirstFrame(prompt: string | undefined): boolean {
58
- if (!prompt) return false
59
- return /@image_\d+\s+as\s+the\s+(first|opening)\s*(\(first\))?\s*frame/i.test(prompt)
60
- }
49
+ export { promptBindsFirstFrame } from "./frame-reference-slots.js"
61
50
 
62
51
  export function resolveSeedance2Inputs(args: Seedance2InputsArgs): Seedance2InputsResult {
63
52
  const limits = args.limits ?? SEEDANCE_2_REF_LIMITS
@@ -85,33 +74,19 @@ export function resolveSeedance2Inputs(args: Seedance2InputsArgs): Seedance2Inpu
85
74
  }
86
75
 
87
76
  // Reference mode: keep both frames (explicit intent), drop trailing user images
88
- // if the 9-image cap is exceeded. Frames are appended AFTER the kept user
89
- // images so existing user @Image ordinals are preserved.
90
- const frameCount = (firstFrameUrl ? 1 : 0) + (lastFrameUrl ? 1 : 0)
91
- const userImageSlots = Math.max(0, limits.images - frameCount)
92
- const keptUserImages = refImages.slice(0, userImageSlots)
93
- const droppedRefImages = refImages.length - keptUserImages.length
94
-
95
- const referenceImageUrls: string[] = [...keptUserImages]
96
- let firstOrdinal = 0
97
- let lastOrdinal = 0
98
- if (firstFrameUrl) { referenceImageUrls.push(firstFrameUrl); firstOrdinal = referenceImageUrls.length }
99
- if (lastFrameUrl) { referenceImageUrls.push(lastFrameUrl); lastOrdinal = referenceImageUrls.length }
100
-
101
- let promptSuffix = ""
102
- const suppressFrameSentence = promptBindsFirstFrame(args.prompt)
103
- if (suppressFrameSentence && firstOrdinal > 0 && lastOrdinal === 0) {
104
- // The prompt already binds the first frame at its own (working) position.
105
- } else if (firstOrdinal > 0 && lastOrdinal > 0) {
106
- // Combined sentence — REF_BINDING.frame() emits a single-frame sentence, so use
107
- // REF_BINDING.ordinal() for each ordinal inline to keep the combined wording AND
108
- // route both ordinals through the single swap-point.
109
- promptSuffix = `Use ${REF_BINDING.ordinal(firstOrdinal)} as the opening (first) frame and ${REF_BINDING.ordinal(lastOrdinal)} as the closing (last) frame of the video.`
110
- } else if (firstOrdinal > 0) {
111
- promptSuffix = REF_BINDING.frame(firstOrdinal, "opening")
112
- } else if (lastOrdinal > 0) {
113
- promptSuffix = REF_BINDING.frame(lastOrdinal, "closing")
114
- }
77
+ // if the image cap is exceeded. Frames are appended AFTER the kept user images
78
+ // so existing user @Image ordinals are preserved — the platform convention
79
+ // (placeFramesAfterReferences), shared with every frames-in-the-list model.
80
+ const placed = placeFramesAfterReferences({
81
+ prompt: args.prompt,
82
+ firstFrameUrl,
83
+ lastFrameUrl,
84
+ refImageUrls: refImages,
85
+ imageCap: limits.images,
86
+ })
87
+ const referenceImageUrls = placed.imageUrls
88
+ const promptSuffix = placed.promptSuffix
89
+ const droppedRefImages = placed.droppedRefImages
115
90
 
116
91
  return {
117
92
  mode: "reference",
@@ -1,5 +1,4 @@
1
- import { promptBindsFirstFrame } from "./seedance-2-inputs.js"
2
- import { identityRefsSentence, REF_BINDING } from "./video-reference-resolver.js"
1
+ import { placeFramesAfterReferences } from "./frame-reference-slots.js"
3
2
 
4
3
  /**
5
4
  * VEO 3.x i2v input resolution — the mutually-exclusive sibling of
@@ -8,13 +7,15 @@ import { identityRefsSentence, REF_BINDING } from "./video-reference-resolver.js
8
7
  * [first(, last)] frames; REFERENCE_2_VIDEO reads every entry as a
9
8
  * reference ingredient. Frames and identities cannot ride separate
10
9
  * channels, so an anchored call that must carry identity references moves
11
- * to REFERENCE_2_VIDEO with the anchor in seat 1, bound in prose as the
12
- * opening frame (requested, not pixel-guaranteed — the accepted trade,
13
- * same as seedance-2's reference mode).
10
+ * to REFERENCE_2_VIDEO with the frames bound in prose (requested, not
11
+ * pixel-guaranteed — the accepted trade, same as seedance-2's reference mode).
14
12
  *
15
- * REFERENCES WIN THE SEATS (the 2026-08-14 standing rule: refs are a must,
16
- * frames additional): the end anchor is dropped in reference mode rather
17
- * than spending one of three seats on a closing guess. The caller logs it.
13
+ * THE PLATFORM CONVENTION (2026-10-18, `placeFramesAfterReferences`): the
14
+ * caller's references take the first seats, so their image token N is their
15
+ * Nth reference; the start frame — and the end frame, VEO takes one — follow
16
+ * them, bound as the opening / closing frame. At the 3-ingredient cap the
17
+ * frames are kept (explicit intent) and trailing references drop, reported.
18
+ * (Until then the anchor took seat 1 and an end frame was surrendered.)
18
19
  *
19
20
  * BYTE-IDENTICAL with no references: plain frame mode, frames kept, no
20
21
  * generationType, no suffix — exactly what every veo i2v call has always
@@ -30,14 +31,15 @@ export interface VeoI2vInputsArgs {
30
31
  }
31
32
 
32
33
  export interface VeoI2vInputsResult {
33
- /** The `imageUrls` payload: frames in plain mode, [anchor, ...refs] in
34
+ /** The `imageUrls` payload: frames in plain mode, [...refs, start, end?] in
34
35
  * reference mode — never more than VEO's 3-ingredient cap. */
35
36
  imageUrls: string[]
36
37
  /** Present (REFERENCE_2_VIDEO) exactly when references ride. */
37
38
  generationType?: "REFERENCE_2_VIDEO"
38
39
  promptSuffix: string
39
40
  droppedRefImages: number
40
- /** True when an end anchor was surrendered to reference mode. */
41
+ /** Always false since the frames-after-references convention: an end frame
42
+ * is kept in reference mode. Kept for callers that log it. */
41
43
  droppedEndFrame: boolean
42
44
  }
43
45
 
@@ -55,14 +57,19 @@ export function resolveVeoI2vInputs(args: VeoI2vInputsArgs): VeoI2vInputsResult
55
57
  droppedEndFrame: false,
56
58
  }
57
59
  }
58
- const kept = refs.slice(0, VEO_INGREDIENT_SLOTS - 1)
59
- const droppedRefImages = refs.length - kept.length
60
- const frameSentence = promptBindsFirstFrame(args.prompt) ? "" : REF_BINDING.frame(1, "opening")
60
+ const placed = placeFramesAfterReferences({
61
+ prompt: args.prompt,
62
+ firstFrameUrl: args.firstFrameUrl,
63
+ lastFrameUrl: args.endFrameUrl,
64
+ refImageUrls: refs,
65
+ imageCap: VEO_INGREDIENT_SLOTS,
66
+ bindIdentities: true,
67
+ })
61
68
  return {
62
- imageUrls: [args.firstFrameUrl, ...kept],
69
+ imageUrls: placed.imageUrls,
63
70
  generationType: "REFERENCE_2_VIDEO",
64
- promptSuffix: [frameSentence, identityRefsSentence(2, kept.length + 1)].filter(Boolean).join(" "),
65
- droppedRefImages,
66
- droppedEndFrame: Boolean(args.endFrameUrl),
71
+ promptSuffix: placed.promptSuffix,
72
+ droppedRefImages: placed.droppedRefImages,
73
+ droppedEndFrame: false,
67
74
  }
68
75
  }
@@ -54,7 +54,7 @@ import { tidyReferenceTokenGaps, unboundReferenceTokenText } from "./unbound-ref
54
54
  // The binding surface string and the id-addressed token resolver live in their
55
55
  // own modules (see them for the contracts); re-exported here so every existing
56
56
  // importer of this module — and the package index's `export *` — keeps working.
57
- export { REF_BINDING, identityRefsSentence, SEEDANCE_VIDEO_EDIT_PREFIX, buildSeedanceVideoEditPrompt } from "./ref-binding.js"
57
+ export { REF_BINDING, identityRefsSentence, imageBindingFor, SEEDANCE_VIDEO_EDIT_PREFIX, buildSeedanceVideoEditPrompt, type ImageBinding } from "./ref-binding.js"
58
58
  export { resolveRefIdTokens, type RefIdTokenContext } from "./ref-id-tokens.js"
59
59
 
60
60