@nodaro/prompts 1.38.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.
- package/dist/chunk-2EW5B7ZV.cjs +86 -0
- package/dist/chunk-2EW5B7ZV.cjs.map +1 -0
- package/dist/chunk-2P26SZX5.cjs +414 -0
- package/dist/chunk-2P26SZX5.cjs.map +1 -0
- package/dist/{chunk-7XKHDXCL.cjs → chunk-3U7X6JZN.cjs} +13 -12
- package/dist/chunk-3U7X6JZN.cjs.map +1 -0
- package/dist/chunk-4GKTOVFM.cjs +8 -0
- package/dist/chunk-4GKTOVFM.cjs.map +1 -0
- package/dist/{chunk-HRBBNODV.js → chunk-6GVFJLDL.js} +4 -3
- package/dist/chunk-6GVFJLDL.js.map +1 -0
- package/dist/{chunk-LVSH7K7R.cjs → chunk-C3NHNECH.cjs} +165 -88
- package/dist/chunk-C3NHNECH.cjs.map +1 -0
- package/dist/{chunk-CEPVQQVO.js → chunk-C46256GZ.js} +43 -5
- package/dist/chunk-C46256GZ.js.map +1 -0
- package/dist/{chunk-JKCR6WRZ.cjs → chunk-DQFQJECU.cjs} +43 -5
- package/dist/chunk-DQFQJECU.cjs.map +1 -0
- package/dist/{chunk-QDEWBBUR.js → chunk-DUXX6CBX.js} +12 -143
- package/dist/chunk-DUXX6CBX.js.map +1 -0
- package/dist/chunk-EAYEZXWT.js +388 -0
- package/dist/chunk-EAYEZXWT.js.map +1 -0
- package/dist/chunk-GILUVQQJ.js +6 -0
- package/dist/chunk-GILUVQQJ.js.map +1 -0
- package/dist/{chunk-KIYNOOPJ.js → chunk-GTDKDD7Y.js} +12 -6
- package/dist/chunk-GTDKDD7Y.js.map +1 -0
- package/dist/{chunk-QBEZMR2F.js → chunk-HQTHDTHN.js} +137 -62
- package/dist/chunk-HQTHDTHN.js.map +1 -0
- package/dist/{chunk-TLVX5PXD.cjs → chunk-IUCNJAHW.cjs} +13 -7
- package/dist/chunk-IUCNJAHW.cjs.map +1 -0
- package/dist/{chunk-ZFIXQRBZ.cjs → chunk-LXPKTI2K.cjs} +12 -83
- package/dist/chunk-LXPKTI2K.cjs.map +1 -0
- package/dist/{chunk-CVPB2N3Z.cjs → chunk-LZ53F5BN.cjs} +103 -5
- package/dist/chunk-LZ53F5BN.cjs.map +1 -0
- package/dist/{chunk-4FDNVNJE.js → chunk-N6VMPG5S.js} +12 -79
- package/dist/chunk-N6VMPG5S.js.map +1 -0
- package/dist/{chunk-LK2CZK5U.cjs → chunk-PWWM7Q3T.cjs} +7 -7
- package/dist/{chunk-LK2CZK5U.cjs.map → chunk-PWWM7Q3T.cjs.map} +1 -1
- package/dist/{chunk-N7TOWXDJ.js → chunk-QDUTMOF4.js} +3 -3
- package/dist/{chunk-N7TOWXDJ.js.map → chunk-QDUTMOF4.js.map} +1 -1
- package/dist/{chunk-NO6XHX4R.js → chunk-RB2F5JQF.js} +100 -7
- package/dist/chunk-RB2F5JQF.js.map +1 -0
- package/dist/{chunk-MENBHEAY.cjs → chunk-WRM52ZBN.cjs} +19 -161
- package/dist/chunk-WRM52ZBN.cjs.map +1 -0
- package/dist/chunk-YS6HOWMW.js +80 -0
- package/dist/chunk-YS6HOWMW.js.map +1 -0
- package/dist/entries/assembly.cjs +34 -25
- package/dist/entries/assembly.d.cts +121 -37
- package/dist/entries/assembly.d.ts +121 -37
- package/dist/entries/assembly.js +5 -4
- package/dist/entries/doctrine.cjs +46 -46
- package/dist/entries/doctrine.d.cts +3 -3
- package/dist/entries/doctrine.d.ts +3 -3
- package/dist/entries/doctrine.js +1 -1
- package/dist/entries/picker-catalogs.cjs +42 -42
- package/dist/entries/picker-catalogs.js +3 -3
- package/dist/entries/presets.cjs +49 -26
- package/dist/entries/presets.d.cts +53 -1
- package/dist/entries/presets.d.ts +53 -1
- package/dist/entries/presets.js +4 -1
- package/dist/entries/prompt-builder.cjs +135 -98
- package/dist/entries/prompt-builder.d.cts +22 -11
- package/dist/entries/prompt-builder.d.ts +22 -11
- package/dist/entries/prompt-builder.js +4 -3
- package/dist/entries/prompt-fields.cjs +41 -40
- package/dist/entries/prompt-fields.d.cts +2 -1
- package/dist/entries/prompt-fields.d.ts +2 -1
- package/dist/entries/prompt-fields.js +2 -1
- package/dist/index.cjs +339 -273
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +11 -9
- package/dist/{subject-registry-BUswuEVD.d.cts → subject-registry-CQi_CZfJ.d.cts} +78 -9
- package/dist/{subject-registry-D7V8PEG-.d.ts → subject-registry-DFiGx49v.d.ts} +78 -9
- package/package.json +2 -2
- package/src/__tests__/assemble-image-input-style-references.test.ts +173 -0
- package/src/__tests__/catalog-id-guard.test.ts +30 -0
- package/src/__tests__/factory-preset-previews.test.ts +123 -0
- package/src/__tests__/factory-preset-references.test.ts +222 -0
- package/src/__tests__/factory-presets-prototype-keys.test.ts +43 -0
- package/src/__tests__/frame-reference-slots.test.ts +43 -0
- package/src/__tests__/gemini-omni-inputs.test.ts +12 -10
- package/src/__tests__/provider-prompt-doctrine.test.ts +14 -0
- package/src/__tests__/user-direction-refs.test.ts +180 -0
- package/src/__tests__/veo-i2v-inputs.test.ts +16 -11
- package/src/assemble-image-input.ts +114 -13
- package/src/catalog-id-guard.ts +18 -2
- package/src/color-names.ts +192 -0
- package/src/direction-registry.ts +66 -5
- package/src/entries/assembly.ts +1 -0
- package/src/entries/prompt-builder.ts +2 -0
- package/src/factory-presets/generate-image.ts +29 -0
- package/src/factory-presets/index.ts +6 -1
- package/src/factory-presets/preview-files.generated.ts +4 -0
- package/src/factory-presets/previews.ts +40 -0
- package/src/factory-presets/references.ts +81 -0
- package/src/factory-presets/shared-image.ts +4 -0
- package/src/factory-presets/types.ts +8 -0
- package/src/frame-reference-slots.ts +92 -0
- package/src/gemini-omni-inputs.ts +23 -22
- package/src/image-reference-doctrine.ts +3 -3
- package/src/image-token-pattern.ts +13 -0
- package/src/node-prompt-fields.ts +5 -3
- package/src/own-entry.ts +11 -0
- package/src/prompt-builder.ts +4 -5
- package/src/prompt-style-section.ts +3 -3
- package/src/prompt-wizard-categories.ts +2 -0
- package/src/provider-prompt-doctrine.ts +45 -3
- package/src/ref-binding.ts +28 -3
- package/src/resolve-prompt.ts +2 -1
- package/src/seedance-2-inputs.ts +15 -40
- package/src/user-direction-refs.ts +110 -0
- package/src/veo-i2v-inputs.ts +24 -17
- package/src/video-reference-resolver.ts +1 -1
- package/dist/chunk-4FDNVNJE.js.map +0 -1
- package/dist/chunk-7XKHDXCL.cjs.map +0 -1
- package/dist/chunk-CEPVQQVO.js.map +0 -1
- package/dist/chunk-CVPB2N3Z.cjs.map +0 -1
- package/dist/chunk-HRBBNODV.js.map +0 -1
- package/dist/chunk-JKCR6WRZ.cjs.map +0 -1
- package/dist/chunk-KIYNOOPJ.js.map +0 -1
- package/dist/chunk-LVSH7K7R.cjs.map +0 -1
- package/dist/chunk-MENBHEAY.cjs.map +0 -1
- package/dist/chunk-NO6XHX4R.js.map +0 -1
- package/dist/chunk-QBEZMR2F.js.map +0 -1
- package/dist/chunk-QDEWBBUR.js.map +0 -1
- package/dist/chunk-TLVX5PXD.cjs.map +0 -1
- package/dist/chunk-UJKFHPZ7.js +0 -37
- package/dist/chunk-UJKFHPZ7.js.map +0 -1
- package/dist/chunk-W5SCZIOB.cjs +0 -43
- package/dist/chunk-W5SCZIOB.cjs.map +0 -1
- package/dist/chunk-ZFIXQRBZ.cjs.map +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { IMAGE_TOKEN_PATTERN } from "../image-token-pattern.js"
|
|
2
|
+
import { getPromptFields, nodeSupportsPromptAffixes, promptAffixCoreField } from "../node-prompt-fields.js"
|
|
3
|
+
import { NODE_PROMPT_CANDIDATE_FIELDS } from "../resolve-prompt.js"
|
|
4
|
+
import { ownEntry } from "../own-entry.js"
|
|
5
|
+
import { MODELS_WITH_REFERENCE_IMAGE_SUPPORT, imageReferenceLimit } from "@nodaro/shared/models"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The most reference images any image model takes. A preset can never need
|
|
9
|
+
* more than some model could be sent, so a count is clamped to it — a typo
|
|
10
|
+
* like `{image:99999}` must not tell a client to ask for 99,999 photos.
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_PRESET_REFERENCES: number = Math.max(
|
|
13
|
+
1,
|
|
14
|
+
...[...MODELS_WITH_REFERENCE_IMAGE_SUPPORT].map((provider) => imageReferenceLimit(provider)),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The data fields of `nodeType` a reference token can sit in — every field the
|
|
19
|
+
* run reads its prompt from: the candidate fields `computeNodePrompt` joins
|
|
20
|
+
* (`NODE_PROMPT_CANDIDATE_FIELDS` — how a preset's prompt is resolved when it
|
|
21
|
+
* runs, on the canvas and through `presetId` alike), the prompt-field
|
|
22
|
+
* registry's own prompt and negative fields, the field the run wraps with
|
|
23
|
+
* pre/post text, and the pre/post text itself where the node takes it. A node
|
|
24
|
+
* whose prompt lives elsewhere (`llm-chat` → `userInput`) is read where its
|
|
25
|
+
* prompt really is; a type with no prompt (an app namespace) has none.
|
|
26
|
+
*/
|
|
27
|
+
function promptFieldsOf(nodeType: string): string[] {
|
|
28
|
+
const spec = getPromptFields(nodeType)
|
|
29
|
+
const candidates = ownEntry(NODE_PROMPT_CANDIDATE_FIELDS, nodeType) ?? []
|
|
30
|
+
if (!spec && candidates.length === 0) return []
|
|
31
|
+
const fields = new Set<string>(candidates)
|
|
32
|
+
if (spec) {
|
|
33
|
+
fields.add(spec.prompt)
|
|
34
|
+
if (spec.negative) fields.add(spec.negative)
|
|
35
|
+
}
|
|
36
|
+
const core = promptAffixCoreField(nodeType)
|
|
37
|
+
if (core) fields.add(core)
|
|
38
|
+
if (nodeSupportsPromptAffixes(nodeType)) {
|
|
39
|
+
fields.add("promptPrefix")
|
|
40
|
+
fields.add("promptSuffix")
|
|
41
|
+
}
|
|
42
|
+
return [...fields]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The highest `{image:N}` / `{image:N:label}` position in `text`, or 0. */
|
|
46
|
+
function highestImageToken(text: string): number {
|
|
47
|
+
let highest = 0
|
|
48
|
+
for (const match of text.matchAll(IMAGE_TOKEN_PATTERN)) {
|
|
49
|
+
const n = Number.parseInt(match[1] ?? "", 10)
|
|
50
|
+
if (Number.isInteger(n) && n > highest) highest = n
|
|
51
|
+
}
|
|
52
|
+
return highest
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How many connected reference images a preset works from — 0 for a preset
|
|
57
|
+
* that needs none. A client shows a preset with a non-zero count as "needs a
|
|
58
|
+
* photo" (`needsImage`) and asks for that many before running it.
|
|
59
|
+
*
|
|
60
|
+
* Two sources, and the larger wins:
|
|
61
|
+
* - the `{image:N}` / `{image:N:label}` tokens in the preset's prompt fields
|
|
62
|
+
* (the highest N): a template that names its references always reports them;
|
|
63
|
+
* - `declared` — the factory preset's own `minReferences`, for presets whose
|
|
64
|
+
* prompt refers to "the attached photo" in words, which no data field marks.
|
|
65
|
+
* A user preset has no declaration, so its count comes from its tokens.
|
|
66
|
+
*
|
|
67
|
+
* Pure; never throws. A declaration that is not a positive whole number is
|
|
68
|
+
* ignored, and the result is at most {@link MAX_PRESET_REFERENCES}.
|
|
69
|
+
*/
|
|
70
|
+
export function presetMinReferences(
|
|
71
|
+
nodeType: string,
|
|
72
|
+
data: Readonly<Record<string, unknown>>,
|
|
73
|
+
declared?: number,
|
|
74
|
+
): number {
|
|
75
|
+
let count = typeof declared === "number" && Number.isInteger(declared) && declared > 0 ? declared : 0
|
|
76
|
+
for (const field of promptFieldsOf(nodeType)) {
|
|
77
|
+
const value = data[field]
|
|
78
|
+
if (typeof value === "string") count = Math.max(count, highestImageToken(value))
|
|
79
|
+
}
|
|
80
|
+
return Math.min(count, MAX_PRESET_REFERENCES)
|
|
81
|
+
}
|
|
@@ -89,6 +89,8 @@ export function stylizedSubjectFor(nodeType: string): FactoryPreset[] {
|
|
|
89
89
|
description: p.description,
|
|
90
90
|
group: "Stylized Subject",
|
|
91
91
|
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
92
|
+
// Restyles the person in the connected photo ("the reference image").
|
|
93
|
+
minReferences: 1,
|
|
92
94
|
}))
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -242,5 +244,7 @@ export function editsFor(nodeType: string): FactoryPreset[] {
|
|
|
242
244
|
description: p.description,
|
|
243
245
|
group: "Edits",
|
|
244
246
|
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
247
|
+
// Every edit operates on the connected photo.
|
|
248
|
+
minReferences: 1,
|
|
245
249
|
}))
|
|
246
250
|
}
|
|
@@ -12,6 +12,14 @@ export interface FactoryPreset {
|
|
|
12
12
|
readonly groupKind?: "folder" | "section"
|
|
13
13
|
/** Capture-shaped config (no label / fieldMappings / runtime keys). */
|
|
14
14
|
readonly data: Readonly<Record<string, unknown>>
|
|
15
|
+
/**
|
|
16
|
+
* How many connected reference images the preset works from, stated for a
|
|
17
|
+
* preset whose prompt refers to "the attached photo" in words (Reference
|
|
18
|
+
* Sheet, Portrait Transformations, Stylized Subject, Edits, …). Leave it out
|
|
19
|
+
* otherwise: `{image:N}` tokens in the prompt are counted on their own, and
|
|
20
|
+
* the count a client sees is the larger of the two (`presetMinReferences`).
|
|
21
|
+
*/
|
|
22
|
+
readonly minReferences?: number
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/** A render-ready bucket of factory presets sharing one `group` (or the leading
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { identityRefsSentence, imageBindingFor } from "./ref-binding.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WHERE A START / END FRAME GOES WHEN IT SHARES A MODEL'S REFERENCE LIST — the
|
|
5
|
+
* one platform convention (2026-10-18), for every video model whose frames and
|
|
6
|
+
* reference images travel in a single image array (`videoProviderAppendsFrames`
|
|
7
|
+
* in @nodaro/shared):
|
|
8
|
+
*
|
|
9
|
+
* - the caller's own reference images come FIRST, in their order, so the
|
|
10
|
+
* caller's image token N (`{image:N}`, `{ref:<id>}`, a typed ordinal)
|
|
11
|
+
* always means their Nth reference;
|
|
12
|
+
* - the frames are appended AFTER them (start, then end) and named in the
|
|
13
|
+
* prompt as the opening / closing frame, in the provider's own syntax;
|
|
14
|
+
* - at the image cap the frames are KEPT — they are explicit intent — and
|
|
15
|
+
* trailing caller references are dropped and reported.
|
|
16
|
+
*
|
|
17
|
+
* `bindIdentities` adds the identity sentence the flat-image-list models carry
|
|
18
|
+
* (Gemini Omni, VEO, Grok, HappyHorse: nothing in their payload says which
|
|
19
|
+
* entries are identities, so the prompt says it). The Seedance-family resolver
|
|
20
|
+
* calls this without it — byte for byte what it always emitted.
|
|
21
|
+
*/
|
|
22
|
+
export interface FrameReferenceSlotsArgs {
|
|
23
|
+
/** Consulted only to skip a duplicate opening-frame sentence. */
|
|
24
|
+
prompt?: string
|
|
25
|
+
firstFrameUrl?: string
|
|
26
|
+
lastFrameUrl?: string
|
|
27
|
+
/** The caller's own reference images, in priority order. */
|
|
28
|
+
refImageUrls?: readonly (string | undefined)[]
|
|
29
|
+
/** The provider's image cap — frames and references together. */
|
|
30
|
+
imageCap: number
|
|
31
|
+
/** The provider's reference syntax (`videoProviderReferenceSyntax`). */
|
|
32
|
+
syntax?: "@image_N" | "@image(n)" | "[Image N]"
|
|
33
|
+
bindIdentities?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FrameReferenceSlotsResult {
|
|
37
|
+
/** `[...keptRefs, start?, end?]` — never longer than `imageCap` (bar the frames themselves). */
|
|
38
|
+
imageUrls: string[]
|
|
39
|
+
promptSuffix: string
|
|
40
|
+
/** Trailing caller references dropped to keep the frames. */
|
|
41
|
+
droppedRefImages: number
|
|
42
|
+
/** 1-based seats of the frames in `imageUrls`; 0 when absent. */
|
|
43
|
+
firstFrameOrdinal: number
|
|
44
|
+
lastFrameOrdinal: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function placeFramesAfterReferences(args: FrameReferenceSlotsArgs): FrameReferenceSlotsResult {
|
|
48
|
+
const refs = (args.refImageUrls ?? []).filter((u): u is string => typeof u === "string" && u.trim().length > 0)
|
|
49
|
+
const frameCount = (args.firstFrameUrl ? 1 : 0) + (args.lastFrameUrl ? 1 : 0)
|
|
50
|
+
const kept = refs.slice(0, Math.max(0, args.imageCap - frameCount))
|
|
51
|
+
const droppedRefImages = refs.length - kept.length
|
|
52
|
+
|
|
53
|
+
const imageUrls = [...kept]
|
|
54
|
+
let firstFrameOrdinal = 0
|
|
55
|
+
let lastFrameOrdinal = 0
|
|
56
|
+
if (args.firstFrameUrl) { imageUrls.push(args.firstFrameUrl); firstFrameOrdinal = imageUrls.length }
|
|
57
|
+
if (args.lastFrameUrl) { imageUrls.push(args.lastFrameUrl); lastFrameOrdinal = imageUrls.length }
|
|
58
|
+
|
|
59
|
+
const syntax = args.syntax ?? "@image_N"
|
|
60
|
+
const binding = imageBindingFor(syntax)
|
|
61
|
+
// The duplicate-binding check reads the platform's `@image_N` form, which is
|
|
62
|
+
// the only form a caller's prompt can already carry it in.
|
|
63
|
+
const promptBindsStart = syntax === "@image_N" && promptBindsFirstFrame(args.prompt)
|
|
64
|
+
let frameSentence = ""
|
|
65
|
+
if (promptBindsStart && firstFrameOrdinal > 0 && lastFrameOrdinal === 0) {
|
|
66
|
+
// The prompt already binds the first frame at its own (working) position.
|
|
67
|
+
} else if (firstFrameOrdinal > 0 && lastFrameOrdinal > 0) {
|
|
68
|
+
frameSentence = `Use ${binding.ordinal(firstFrameOrdinal)} as the opening (first) frame and ${binding.ordinal(lastFrameOrdinal)} as the closing (last) frame of the video.`
|
|
69
|
+
} else if (firstFrameOrdinal > 0) {
|
|
70
|
+
frameSentence = binding.frame(firstFrameOrdinal, "opening")
|
|
71
|
+
} else if (lastFrameOrdinal > 0) {
|
|
72
|
+
frameSentence = binding.frame(lastFrameOrdinal, "closing")
|
|
73
|
+
}
|
|
74
|
+
const identitySentence = args.bindIdentities && kept.length > 0 && frameCount > 0
|
|
75
|
+
? identityRefsSentence(1, kept.length, binding.ordinal)
|
|
76
|
+
: ""
|
|
77
|
+
const promptSuffix = [frameSentence, identitySentence].filter(Boolean).join(" ")
|
|
78
|
+
return { imageUrls, promptSuffix, droppedRefImages, firstFrameOrdinal, lastFrameOrdinal }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when the prompt ALREADY binds an image as the first/opening frame —
|
|
82
|
+
* e.g. the gvp overlap transport's colon-position directive ("use @image_1
|
|
83
|
+
* as the first frame, it is the last keyframe of @video_1"). Field finding
|
|
84
|
+
* (2026-07-20): the first-frame directive only works adjacent to the extend
|
|
85
|
+
* colon; a duplicate sentence at the END of the prompt (this resolver's
|
|
86
|
+
* default position) dilutes it back into coin-flip behavior — so when the
|
|
87
|
+
* prompt carries its own binding, the suffix is suppressed (the frame image
|
|
88
|
+
* itself still rides the reference list). */
|
|
89
|
+
export function promptBindsFirstFrame(prompt: string | undefined): boolean {
|
|
90
|
+
if (!prompt) return false
|
|
91
|
+
return /@image_\d+\s+as\s+the\s+(first|opening)\s*(\(first\))?\s*frame/i.test(prompt)
|
|
92
|
+
}
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import { VIDEO_REF_LIMITS_BY_PROVIDER } from "@nodaro/shared/models"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { identityRefsSentence, REF_BINDING } from "./video-reference-resolver.js"
|
|
3
|
+
import { placeFramesAfterReferences } from "./frame-reference-slots.js"
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Gemini Omni Video i2v input resolution — the sibling of
|
|
8
7
|
* `resolveSeedance2Inputs` for a model whose multimodal channel is ONE flat
|
|
9
8
|
* `image_urls` list.
|
|
10
9
|
*
|
|
10
|
+
* THE PLATFORM CONVENTION (2026-10-18, `placeFramesAfterReferences`): the
|
|
11
|
+
* caller's identity references come first, so their image token N is their Nth
|
|
12
|
+
* reference, and the start frame is appended after them. Until then the frame
|
|
13
|
+
* led the list, so a caller's `@image_1` landed on the frame.
|
|
14
|
+
*
|
|
11
15
|
* WHY BINDING IS LOAD-BEARING: Gemini Omni receives the start frame and the
|
|
12
16
|
* identity references in the same array, with nothing in the payload marking
|
|
13
17
|
* which is which. A multimodal model treats unbound images as loose context —
|
|
14
18
|
* field finding (recast keyframes run, 2026-08-14): the identity references
|
|
15
19
|
* rode every call and the cast still drifted part to part, because the prompt
|
|
16
20
|
* never said the images WERE identities to keep. So the resolver names the
|
|
17
|
-
* roles in a prompt suffix
|
|
18
|
-
*
|
|
19
|
-
* identity references, explicitly not frames.
|
|
21
|
+
* roles in a prompt suffix: the references are identity references, explicitly
|
|
22
|
+
* not frames, and the last image is the opening frame.
|
|
20
23
|
*
|
|
21
24
|
* BUDGETED, NEVER REJECTED, for the list this resolver assembles: KIE's quota
|
|
22
25
|
* is `images + 2×videos ≤ 7`, and `runGeminiOmni` hard-rejects overflow. That
|
|
23
26
|
* reject is right for a caller-assembled list (the user's own images should
|
|
24
27
|
* not silently thin out) and wrong for THIS merge, where the overflow is our
|
|
25
28
|
* own construction — so trailing references are dropped to fit, the start
|
|
26
|
-
* frame always kept
|
|
27
|
-
*
|
|
29
|
+
* frame always kept (the convention's tie-break), and the drop count is
|
|
30
|
+
* reported for the caller to log.
|
|
28
31
|
*
|
|
29
32
|
* BYTE-IDENTICAL when there is nothing to bind: no references ⇒ no suffix and
|
|
30
33
|
* a single-image list — exactly what every plain gemini-omni i2v call has
|
|
@@ -34,7 +37,7 @@ import { identityRefsSentence, REF_BINDING } from "./video-reference-resolver.js
|
|
|
34
37
|
export interface GeminiOmniI2vInputsArgs {
|
|
35
38
|
/** The composed prompt, used only to detect an existing first-frame binding. */
|
|
36
39
|
prompt?: string
|
|
37
|
-
/** The start frame — always kept,
|
|
40
|
+
/** The start frame — always kept, appended after the references. */
|
|
38
41
|
firstFrameUrl: string
|
|
39
42
|
/** Identity references, in priority order (trailing ones drop first). */
|
|
40
43
|
refImageUrls?: Array<string | undefined>
|
|
@@ -48,7 +51,7 @@ export interface GeminiOmniI2vInputsArgs {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface GeminiOmniI2vInputsResult {
|
|
51
|
-
/** `[
|
|
54
|
+
/** `[...keptRefs, firstFrameUrl]` — the `image_urls` payload, quota-fitted. */
|
|
52
55
|
imageUrls: string[]
|
|
53
56
|
/** The role-binding sentences; empty when no reference survived the budget. */
|
|
54
57
|
promptSuffix: string
|
|
@@ -64,18 +67,16 @@ function geminiOmniInputSlots(provider: string | undefined): number {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export function resolveGeminiOmniI2vInputs(args: GeminiOmniI2vInputsArgs): GeminiOmniI2vInputsResult {
|
|
67
|
-
const refs = (args.refImageUrls ?? []).filter((u): u is string => typeof u === "string" && u.length > 0)
|
|
68
70
|
const slots = geminiOmniInputSlots(args.provider) - (args.videoConnected ? 2 : 0)
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return { imageUrls, promptSuffix, droppedRefImages }
|
|
71
|
+
const placed = placeFramesAfterReferences({
|
|
72
|
+
prompt: args.prompt,
|
|
73
|
+
firstFrameUrl: args.firstFrameUrl,
|
|
74
|
+
refImageUrls: args.refImageUrls,
|
|
75
|
+
imageCap: Math.max(1, slots),
|
|
76
|
+
bindIdentities: true,
|
|
77
|
+
})
|
|
78
|
+
if (placed.imageUrls.length === 1) {
|
|
79
|
+
return { imageUrls: placed.imageUrls, promptSuffix: "", droppedRefImages: placed.droppedRefImages }
|
|
80
|
+
}
|
|
81
|
+
return { imageUrls: placed.imageUrls, promptSuffix: placed.promptSuffix, droppedRefImages: placed.droppedRefImages }
|
|
81
82
|
}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* the video node skills.
|
|
7
7
|
*
|
|
8
8
|
* Unlike the per-provider video doctrines, this is a PLATFORM mechanism: the
|
|
9
|
-
* token grammar and expansion are Nodaro's own (`
|
|
10
|
-
* `IMAGE_TOKEN_PATTERN
|
|
11
|
-
* not one per model family.
|
|
9
|
+
* token grammar and expansion are Nodaro's own (`image-token-pattern.ts` —
|
|
10
|
+
* `IMAGE_TOKEN_PATTERN`; `prompt-builder.ts` — `expandImageRefTokens`), so
|
|
11
|
+
* there is one doctrine, not one per model family.
|
|
12
12
|
*
|
|
13
13
|
* Source of truth for the semantics documented here (verified 2026-08-25):
|
|
14
14
|
* - `IMAGE_TOKEN_PATTERN` = `/\{image:(\d+)(?::([^}\n]+))?\}/gi`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `{image:N}` / `{image:N:label}` reference-token grammar — one pattern for
|
|
3
|
+
* the image prompt assembly (`prompt-builder.ts`) and for the preset metadata
|
|
4
|
+
* that reads which references a template names (`factory-presets/references.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Group 1 = position (1-based), group 2 = optional label. The label allows
|
|
7
|
+
* spaces (multi-word labels like "clothes and shoes") but never a newline or
|
|
8
|
+
* `}`, so a match can't run away across lines or token boundaries.
|
|
9
|
+
*
|
|
10
|
+
* Global on purpose (callers use `.replace` / `.matchAll`, which never leave a
|
|
11
|
+
* `lastIndex` behind); never call `.test` / `.exec` on it.
|
|
12
|
+
*/
|
|
13
|
+
export const IMAGE_TOKEN_PATTERN = /\{image:(\d+)(?::([^}\n]+))?\}/gi
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import type { SnippetMedia } from "./factory-snippets/types.js"
|
|
27
27
|
import { NODE_PROMPT_CANDIDATE_FIELDS } from "./resolve-prompt.js"
|
|
28
|
+
import { ownEntry } from "./own-entry.js"
|
|
28
29
|
|
|
29
30
|
/** Which lucide glyph a node's prompt affordance uses. Kept as a string so this
|
|
30
31
|
* module stays icon-library-free; mapped to a component in the strip button. */
|
|
@@ -136,9 +137,10 @@ export const NODE_PROMPT_FIELDS: Readonly<Record<string, PromptFieldSpec>> = {
|
|
|
136
137
|
"edit-plan": { prompt: "instructions", promptLabel: "Editing instructions", media: "video", inline: false },
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
/** The prompt-field spec for a node type, or undefined if it has none
|
|
140
|
+
/** The prompt-field spec for a node type, or undefined if it has none (an
|
|
141
|
+
* Object.prototype member name such as `constructor` has none). */
|
|
140
142
|
export function getPromptFields(nodeType: string | undefined): PromptFieldSpec | undefined {
|
|
141
|
-
return nodeType ? NODE_PROMPT_FIELDS
|
|
143
|
+
return nodeType ? ownEntry(NODE_PROMPT_FIELDS, nodeType) : undefined
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/** True when this node type has a registered, quick-editable prompt field. */
|
|
@@ -206,7 +208,7 @@ export function promptFieldCarriesAffixes(nodeType: string | undefined, promptFi
|
|
|
206
208
|
const core = promptAffixCoreField(nodeType)
|
|
207
209
|
if (core === undefined) return false
|
|
208
210
|
if (promptField === undefined) return getPromptFields(nodeType)!.prompt === core
|
|
209
|
-
return promptField === core || (NODE_PROMPT_CANDIDATE_FIELDS
|
|
211
|
+
return promptField === core || (ownEntry(NODE_PROMPT_CANDIDATE_FIELDS, nodeType) ?? []).includes(promptField)
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
/** The affix-capable node types, derived from the registry (never hand-listed). */
|
package/src/own-entry.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A registry keyed by node type is a plain object, and the key is
|
|
3
|
+
* caller-supplied (`?nodeType=` on a public route, a preset row's `node_type`,
|
|
4
|
+
* workflow JSON). A plain `record[key] ?? fallback` reads an Object.prototype
|
|
5
|
+
* member for `"constructor"`, `"toString"` or `"__proto__"` — a function or
|
|
6
|
+
* the prototype itself, which the caller then maps or iterates and throws on.
|
|
7
|
+
* This reads an OWN entry only, so such a name is "no such type".
|
|
8
|
+
*/
|
|
9
|
+
export function ownEntry<T>(record: Readonly<Record<string, T>>, key: string | undefined): T | undefined {
|
|
10
|
+
return key !== undefined && Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined
|
|
11
|
+
}
|
package/src/prompt-builder.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { joinSentences } from "./hint-join.js"
|
|
8
|
+
import { IMAGE_TOKEN_PATTERN } from "./image-token-pattern.js"
|
|
8
9
|
import { resolveTemplate, applyTemplate } from "./prompt-templates.js"
|
|
9
10
|
import { NATIVE_NEGATIVE_PROMPT_MODELS, MODELS_WITH_REFERENCE_IMAGE_SUPPORT, imageReferenceLimit, getMaxImagePromptChars, getMaxNegativePromptChars } from "@nodaro/shared/models"
|
|
10
11
|
import { getStylePromptHint, isDeniedStyleId } from "./style.js"
|
|
@@ -3276,11 +3277,9 @@ const STRICT_DEFAULT_SOURCES: ReadonlySet<ReferenceSource> = new Set([
|
|
|
3276
3277
|
"wired-location",
|
|
3277
3278
|
])
|
|
3278
3279
|
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
//
|
|
3282
|
-
// newline or `}` so the match can't run away across lines / token boundaries.
|
|
3283
|
-
const IMAGE_TOKEN_PATTERN = /\{image:(\d+)(?::([^}\n]+))?\}/gi
|
|
3280
|
+
// `IMAGE_TOKEN_PATTERN` (`{image:N}` / `{image:N:label}`) lives in
|
|
3281
|
+
// image-token-pattern.ts, shared with the preset metadata that reads which
|
|
3282
|
+
// references a template names.
|
|
3284
3283
|
|
|
3285
3284
|
/**
|
|
3286
3285
|
* The final 1-based slot an `{image:N}` token binds to on the connected-reference
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
renderDirectionHintClauses,
|
|
52
52
|
type DirectionFamily,
|
|
53
53
|
type DirectionFields,
|
|
54
|
-
type
|
|
54
|
+
type DirectionRenderOptions,
|
|
55
55
|
type DirectionStyleGroup,
|
|
56
56
|
} from "./direction-registry.js"
|
|
57
57
|
|
|
@@ -159,7 +159,7 @@ export function asBodyClauses(texts: readonly string[]): SlottedPromptClause[] {
|
|
|
159
159
|
*/
|
|
160
160
|
export function partitionStyleClauses(
|
|
161
161
|
direction: DirectionFields | undefined,
|
|
162
|
-
opts:
|
|
162
|
+
opts: DirectionRenderOptions,
|
|
163
163
|
): SlottedPromptClause[] {
|
|
164
164
|
return renderDirectionHintClauses(direction, opts).map((clause) => ({
|
|
165
165
|
text: clause.text,
|
|
@@ -185,7 +185,7 @@ export function styleSectionFromClauses(clauses: readonly SlottedPromptClause[])
|
|
|
185
185
|
*/
|
|
186
186
|
export function renderStyleSection(
|
|
187
187
|
direction: DirectionFields | undefined,
|
|
188
|
-
opts:
|
|
188
|
+
opts: DirectionRenderOptions,
|
|
189
189
|
): string {
|
|
190
190
|
return styleSectionFromClauses(partitionStyleClauses(direction, opts))
|
|
191
191
|
}
|
|
@@ -267,6 +267,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
267
267
|
"gemini-omni-flash": "Google Gemini Omni Flash — faster, cheaper Omni tier; multimodal video with native audio, 4–10s, up to 4K.",
|
|
268
268
|
"wan-3": "Wan 3.0 — multimodal refs (10 images / 5 videos / 5 audio) or first+last frame, native audio, 2–30s, 480p/720p/1080p",
|
|
269
269
|
"wan-3-prime": "Wan 3.0 Prime — high-speed Wan 3.0 tier; same surface, faster turnaround at a higher rate",
|
|
270
|
+
"vidu-q4": "Vidu Q4 — start frame or up to 15 reference images + 3 MP3 audio refs, native audio, 3–16s, 540p–4K (image required)",
|
|
270
271
|
"grok-imagine-video-1.5": "Grok Imagine 1.5 — image-to-video only; requires an input image",
|
|
271
272
|
},
|
|
272
273
|
"image-to-video": {
|
|
@@ -303,6 +304,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
303
304
|
"gemini-omni-flash": "Google Gemini Omni Flash — faster, cheaper Omni tier; multimodal video with native audio, 4–10s, up to 4K.",
|
|
304
305
|
"wan-3": "Wan 3.0 — multimodal refs (10 images / 5 videos / 5 audio) or first+last frame, native audio, 2–30s, 480p/720p/1080p",
|
|
305
306
|
"wan-3-prime": "Wan 3.0 Prime — high-speed Wan 3.0 tier; same surface, faster turnaround at a higher rate",
|
|
307
|
+
"vidu-q4": "Vidu Q4 — animate a start frame, or direct up to 15 reference images + 3 MP3 audio refs; native audio, 3–16s, 540p–4K",
|
|
306
308
|
"grok-imagine-video-1.5": "Grok Imagine 1.5 — stylized animation, 1–15s, 480p/720p (image required)",
|
|
307
309
|
},
|
|
308
310
|
"video-to-video": {
|
|
@@ -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
|
|
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
|
|
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
|
]
|
package/src/ref-binding.ts
CHANGED
|
@@ -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(
|
|
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
|
-
? `${
|
|
34
|
-
: `${
|
|
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
|
package/src/resolve-prompt.ts
CHANGED
|
@@ -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
|
|
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) })
|