@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.
- package/dist/{chunk-XIWWDGT5.cjs → chunk-2EW5B7ZV.cjs} +7 -7
- package/dist/{chunk-XIWWDGT5.cjs.map → chunk-2EW5B7ZV.cjs.map} +1 -1
- package/dist/{chunk-22KZOAGL.cjs → chunk-C3NHNECH.cjs} +86 -48
- package/dist/chunk-C3NHNECH.cjs.map +1 -0
- package/dist/{chunk-XRLOMTBZ.js → chunk-C46256GZ.js} +43 -5
- package/dist/chunk-C46256GZ.js.map +1 -0
- package/dist/{chunk-NMWKXAYA.cjs → chunk-DQFQJECU.cjs} +43 -5
- package/dist/chunk-DQFQJECU.cjs.map +1 -0
- package/dist/{chunk-XU54YJXQ.js → chunk-HQTHDTHN.js} +85 -49
- package/dist/chunk-HQTHDTHN.js.map +1 -0
- package/dist/{chunk-KUBZVWME.cjs → chunk-LXPKTI2K.cjs} +12 -5
- package/dist/chunk-LXPKTI2K.cjs.map +1 -0
- package/dist/{chunk-3INXNDV7.cjs → chunk-LZ53F5BN.cjs} +8 -8
- package/dist/chunk-LZ53F5BN.cjs.map +1 -0
- package/dist/{chunk-7XMTVYS5.js → chunk-N6VMPG5S.js} +12 -6
- package/dist/chunk-N6VMPG5S.js.map +1 -0
- package/dist/{chunk-6MPZHFIS.js → chunk-RB2F5JQF.js} +5 -5
- package/dist/chunk-RB2F5JQF.js.map +1 -0
- package/dist/{chunk-Z3SZXWRU.js → chunk-YS6HOWMW.js} +3 -3
- package/dist/{chunk-Z3SZXWRU.js.map → chunk-YS6HOWMW.js.map} +1 -1
- package/dist/entries/assembly.cjs +30 -22
- package/dist/entries/assembly.d.cts +90 -34
- package/dist/entries/assembly.d.ts +90 -34
- package/dist/entries/assembly.js +1 -1
- package/dist/entries/doctrine.cjs +46 -46
- package/dist/entries/doctrine.js +1 -1
- package/dist/entries/presets.cjs +32 -32
- package/dist/entries/presets.d.cts +2 -0
- package/dist/entries/presets.d.ts +2 -0
- package/dist/entries/presets.js +2 -2
- package/dist/entries/prompt-fields.cjs +26 -26
- 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 -2
- package/dist/index.cjs +133 -125
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -5
- package/package.json +2 -2
- 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__/veo-i2v-inputs.test.ts +16 -11
- package/src/entries/assembly.ts +1 -0
- package/src/factory-presets/index.ts +4 -1
- package/src/factory-presets/references.ts +2 -1
- package/src/frame-reference-slots.ts +92 -0
- package/src/gemini-omni-inputs.ts +23 -22
- package/src/node-prompt-fields.ts +5 -3
- package/src/own-entry.ts +11 -0
- 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/veo-i2v-inputs.ts +24 -17
- package/src/video-reference-resolver.ts +1 -1
- package/dist/chunk-22KZOAGL.cjs.map +0 -1
- package/dist/chunk-3INXNDV7.cjs.map +0 -1
- package/dist/chunk-6MPZHFIS.js.map +0 -1
- package/dist/chunk-7XMTVYS5.js.map +0 -1
- package/dist/chunk-KUBZVWME.cjs.map +0 -1
- package/dist/chunk-NMWKXAYA.cjs.map +0 -1
- package/dist/chunk-XRLOMTBZ.js.map +0 -1
- package/dist/chunk-XU54YJXQ.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { P as PICKER_CATALOGS, a as PickerCatalog, b as PickerCatalogDetail, c as PickerCatalogSummary, d as PickerDimension, e as PickerOption, f as ProjectPickerCatalogOptions, g as ProjectedPickerCatalog, h as ProjectedPickerDimension, i as ProjectedPickerOption, j as getPickerCatalog, k as getRegisteredPickerCatalogs, l as listPickerCatalogs, p as projectAllCatalogs, m as projectPickerCatalog, s as summarizePickerCatalogs } from './picker-catalogs-DDjtZND3.js';
|
|
2
|
-
export { AssembleImageInput, AssembleImageResult, AssembleSunoInput, AssembleSunoResult, CharacterMeta, FrameDeliveryPlan, FrameDeliveryPlanArgs, GeminiOmniI2vInputsArgs, GeminiOmniI2vInputsResult, REF_BINDING, RefIdTokenContext, ReferenceCounts, ResolveVideoReferenceCoreArgs, SEEDANCE_VIDEO_EDIT_PREFIX, Seedance2InputsArgs, Seedance2InputsResult, Seedance2Mode, SoundComposition, SoundCompositionFields, SoundConsumerType, VeoI2vInputsArgs, VeoI2vInputsResult, VideoExtraRef, VideoPromptCapOptions, appendField, appendMusicMeta, assembleImageInput, assembleSunoInput, buildSeedanceVideoEditPrompt, buildSurroundFillPrompt, composeSoundHintFromConnections, composeVideoPromptText, getEffectiveSunoCustomMode, identityRefsSentence, planFrameDelivery, promptBindsFirstFrame, resolveGeminiOmniI2vInputs, resolveRefIdTokens, resolveReferenceTokens, resolveSeedance2Inputs, resolveVeoI2vInputs, resolveVideoReferenceCore, truncateForField } from './entries/assembly.js';
|
|
2
|
+
export { AssembleImageInput, AssembleImageResult, AssembleSunoInput, AssembleSunoResult, CharacterMeta, FrameDeliveryPlan, FrameDeliveryPlanArgs, FrameReferenceSlotsArgs, FrameReferenceSlotsResult, GeminiOmniI2vInputsArgs, GeminiOmniI2vInputsResult, ImageBinding, REF_BINDING, RefIdTokenContext, ReferenceCounts, ResolveVideoReferenceCoreArgs, SEEDANCE_VIDEO_EDIT_PREFIX, Seedance2InputsArgs, Seedance2InputsResult, Seedance2Mode, SoundComposition, SoundCompositionFields, SoundConsumerType, VeoI2vInputsArgs, VeoI2vInputsResult, VideoExtraRef, VideoPromptCapOptions, appendField, appendMusicMeta, assembleImageInput, assembleSunoInput, buildSeedanceVideoEditPrompt, buildSurroundFillPrompt, composeSoundHintFromConnections, composeVideoPromptText, getEffectiveSunoCustomMode, identityRefsSentence, imageBindingFor, placeFramesAfterReferences, planFrameDelivery, promptBindsFirstFrame, resolveGeminiOmniI2vInputs, resolveRefIdTokens, resolveReferenceTokens, resolveSeedance2Inputs, resolveVeoI2vInputs, resolveVideoReferenceCore, truncateForField } from './entries/assembly.js';
|
|
3
3
|
export { P as PickerHintMode, S as SuspiciousTermOptions, T as TERM_MAX_CHARS, a as TermCarrier, d as deriveTerm, i as isSuspiciousDerivedTerm, r as resolveTerm } from './term-C01_g6s1.js';
|
|
4
4
|
export { C as CatalogPack, a as CatalogPackMode, P as PickerCatalogInput, b as PickerDimensionInput, c as PickerOptionInput, d as catalogPacksVersion, e as composePickerCatalogs, g as getRegisteredCatalogPacks, h as hasCatalogPacksFor, r as registerCatalogPack, f as resetCatalogPacks, s as subscribeCatalogPacks } from './catalog-packs-3SxAVxPF.js';
|
|
5
5
|
export { SidecarCoverageReport, computePackSidecarCoverage } from './entries/catalog-packs.js';
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export { BODY_CAPTIONS_PRESET_ID, BRAND_PRESETS, BRAND_PRESET_IDS, BRAND_PRESET_META, CAPTION_LEVERS_BY_LANGUAGE, CAPTION_SEGMENT_LEVER_KEYS, FACTORY_PRESETS, FACTORY_PRESET_PREVIEW_FILES, FACTORY_SNIPPETS, HOOK_PLATE_PRESET_ID, MAX_PRESET_REFERENCES, OBJECT_ANGLE_PRESETS, OBJECT_ANGLE_PROMPTS, OBJECT_ASSET_PRESETS, OBJECT_ASSET_PROMPTS, OBJECT_MATERIAL_PRESETS, OBJECT_MATERIAL_PROMPTS, OBJECT_VARIATION_PRESETS, OBJECT_VARIATION_PROMPTS, PRESET_PREVIEW_PATH, SNIPPET_MEDIA_VALUES, STYLE_PRESETS, factoryPresetPreviewUrl, getFactoryPresets, getFactorySnippets, getStylePreset, groupFactoryPresets, hookPlateCaptionSegments, presetMinReferences, resolveBrandInput } from './chunk-
|
|
1
|
+
export { BODY_CAPTIONS_PRESET_ID, BRAND_PRESETS, BRAND_PRESET_IDS, BRAND_PRESET_META, CAPTION_LEVERS_BY_LANGUAGE, CAPTION_SEGMENT_LEVER_KEYS, FACTORY_PRESETS, FACTORY_PRESET_PREVIEW_FILES, FACTORY_SNIPPETS, HOOK_PLATE_PRESET_ID, MAX_PRESET_REFERENCES, OBJECT_ANGLE_PRESETS, OBJECT_ANGLE_PROMPTS, OBJECT_ASSET_PRESETS, OBJECT_ASSET_PROMPTS, OBJECT_MATERIAL_PRESETS, OBJECT_MATERIAL_PROMPTS, OBJECT_VARIATION_PRESETS, OBJECT_VARIATION_PROMPTS, PRESET_PREVIEW_PATH, SNIPPET_MEDIA_VALUES, STYLE_PRESETS, factoryPresetPreviewUrl, getFactoryPresets, getFactorySnippets, getStylePreset, groupFactoryPresets, hookPlateCaptionSegments, presetMinReferences, resolveBrandInput } from './chunk-RB2F5JQF.js';
|
|
2
2
|
export { readDirectionFields, readStructuredFields, readSubjectFields } from './chunk-6GVFJLDL.js';
|
|
3
|
-
export { TEXT_REQUIRED_NODE_TYPES, WIRED_OUTPUT_NODE_TYPES, computeAiWriterInput, computeNodeSendText, stripAudioTags } from './chunk-
|
|
4
|
-
export { NODE_PROMPT_CANDIDATE_FIELDS, NODE_PROMPT_FIELDS, PROMPT_AFFIX_CORE_FIELD_OVERRIDES, PROMPT_AFFIX_NODE_TYPES, applyPromptAffixes, composeNegative, computeLlmChatFields, computeNodePrompt, computeScriptTopic, getPromptFields, getSnippetMedia, joinPromptParts, nodeHasInlinePrompt, nodeHasPromptField, nodeSupportsPromptAffixes, promptAffixCoreField, promptFieldCarriesAffixes, promptPartSeparator, resolvePrompt } from './chunk-
|
|
5
|
-
export { REF_BINDING, SEEDANCE_VIDEO_EDIT_PREFIX, appendField, appendMusicMeta, assembleImageInput, assembleSunoInput, buildSeedanceVideoEditPrompt, buildSurroundFillPrompt, composeSoundHintFromConnections, composeVideoPromptText, getEffectiveSunoCustomMode, identityRefsSentence, planFrameDelivery, promptBindsFirstFrame, resolveGeminiOmniI2vInputs, resolveRefIdTokens, resolveReferenceTokens, resolveSeedance2Inputs, resolveVeoI2vInputs, resolveVideoReferenceCore, truncateForField } from './chunk-
|
|
3
|
+
export { TEXT_REQUIRED_NODE_TYPES, WIRED_OUTPUT_NODE_TYPES, computeAiWriterInput, computeNodeSendText, stripAudioTags } from './chunk-YS6HOWMW.js';
|
|
4
|
+
export { NODE_PROMPT_CANDIDATE_FIELDS, NODE_PROMPT_FIELDS, PROMPT_AFFIX_CORE_FIELD_OVERRIDES, PROMPT_AFFIX_NODE_TYPES, applyPromptAffixes, composeNegative, computeLlmChatFields, computeNodePrompt, computeScriptTopic, getPromptFields, getSnippetMedia, joinPromptParts, nodeHasInlinePrompt, nodeHasPromptField, nodeSupportsPromptAffixes, promptAffixCoreField, promptFieldCarriesAffixes, promptPartSeparator, resolvePrompt } from './chunk-N6VMPG5S.js';
|
|
5
|
+
export { REF_BINDING, SEEDANCE_VIDEO_EDIT_PREFIX, appendField, appendMusicMeta, assembleImageInput, assembleSunoInput, buildSeedanceVideoEditPrompt, buildSurroundFillPrompt, composeSoundHintFromConnections, composeVideoPromptText, getEffectiveSunoCustomMode, identityRefsSentence, imageBindingFor, placeFramesAfterReferences, planFrameDelivery, promptBindsFirstFrame, resolveGeminiOmniI2vInputs, resolveRefIdTokens, resolveReferenceTokens, resolveSeedance2Inputs, resolveVeoI2vInputs, resolveVideoReferenceCore, truncateForField } from './chunk-HQTHDTHN.js';
|
|
6
6
|
export { ANGLE_LABELS, ASPECT_RATIO_LABELS, MAX_SUBJECT_KEYS, MOVEMENT_LABELS, SCENE_PROMPT_MAX_LENGTH, SHOT_LABELS, STYLE_SECTION_GAP, STYLE_SECTION_HEADER, SUBJECT_ARRAY_CEILING, SUBJECT_CUSTOM_AGE_KEY, SUBJECT_FIELDS, SUBJECT_FOLD_KEYS, SUBJECT_ID_MAX_CHARS, SUBJECT_IMAGE_HINT_MODE_DEFAULT, SUBJECT_KEYS, SUBJECT_KEY_MAX_CHARS, SUBJECT_VIDEO_HINT_MODE_DEFAULT, appendReferenceLines, applyReferenceOrderToVideo, asBodyClauses, buildIdentityDirectives, buildImagePrompt, buildImagePromptSegments, buildImagePromptWithOverflow, buildReferenceBlocks, buildScenePrompt, composeSectionedPrompt, endsInsideStyleSection, expandImagePositionRefs, expandImageRefTokens, getRegisteredSubjectKeys, insertBeforeStyleSection, normalizeSubjectFields, partitionStyleClauses, referenceDescriptionLine, renderDescribedReferenceLines, renderReferenceCaptionLines, renderStructuredFields, renderStyleSection, renderSubjectHints, resolveCharacterMentions, resolveLocationMentions, sectionedClauseCosts, splitStyleSection, styleSectionFromClauses, styleSlotFor, subjectFieldsForSurface, truncateText } from './chunk-DUXX6CBX.js';
|
|
7
7
|
import './chunk-GILUVQQJ.js';
|
|
8
8
|
export { keepableDirectionHints } from './chunk-JZOPMVGY.js';
|
|
9
9
|
export { PROMPT_HINT_SEPARATOR, joinPromptHints } from './chunk-35UHMDGS.js';
|
|
10
10
|
export { computePackSidecarCoverage } from './chunk-FDOG7BNB.js';
|
|
11
|
-
export { AD_CREATIVE_ANALYSIS_FIELDS, AD_CREATIVE_ANALYSIS_SYSTEM_PROMPT, AUDIO_WIZARD_CATEGORIES, DEFAULT_OWN_MOTION, IMAGE_REFERENCE_PROMPT_DOCTRINE, IMAGE_WIZARD_CATEGORIES, LLM_CHAT_WIZARD_CATEGORIES, MUSIC_WIZARD_CATEGORIES, OWN_MOTION_NODE_TYPES, POST_CONTENT_ANALYSIS_SYSTEM_PROMPT, PROVIDER_CAPABILITIES, PROVIDER_PROMPT_DOCTRINES, REFERENCE_IMAGE_ROLES, SCENE3D_FIGURE_REFERENCE_RULE, SCENE3D_FREE_PLATE_SLOTS, SCENE3D_LAYOUT_REFERENCE_SCOPING_FIXTURE, SCENE3D_LAYOUT_REFERENCE_SCOPING_LINE, SCENE3D_LAYOUT_SCOPING_FOR, SCENE3D_LAYOUT_SCOPING_IGNORE, SCENE3D_LAYOUT_SCOPING_LOOK_SOURCE, SCENE3D_LAYOUT_SCOPING_MARKER, SCENE3D_UNREFERENCED_FIGURES_WARNING_CODE, TEXT_WIZARD_CATEGORIES, VIDEO_WIZARD_CATEGORIES, appendScene3DStillScopingLines, buildAdCreativeAnalysisUserText, buildCharacterPrompt, buildCreaturePrompt, buildFaceTemplateInputs, buildLocationMotionPrompt, buildLocationPrompt, buildLocationRefinePrompt, buildMotionPrompt, buildObjectMotionPrompt, buildObjectPrompt, buildScene3DLayoutScopingLine, buildScene3DUnreferencedFiguresWarning, getCategoriesForNodeType, getPromptDoctrine, getPromptTips, hasScene3DLayoutScopingLine, isWizardSupported, ownMotionHint, renderScene3DLayoutScopingLine, scene3DLayoutVideoCaptions } from './chunk-
|
|
11
|
+
export { AD_CREATIVE_ANALYSIS_FIELDS, AD_CREATIVE_ANALYSIS_SYSTEM_PROMPT, AUDIO_WIZARD_CATEGORIES, DEFAULT_OWN_MOTION, IMAGE_REFERENCE_PROMPT_DOCTRINE, IMAGE_WIZARD_CATEGORIES, LLM_CHAT_WIZARD_CATEGORIES, MUSIC_WIZARD_CATEGORIES, OWN_MOTION_NODE_TYPES, POST_CONTENT_ANALYSIS_SYSTEM_PROMPT, PROVIDER_CAPABILITIES, PROVIDER_PROMPT_DOCTRINES, REFERENCE_IMAGE_ROLES, SCENE3D_FIGURE_REFERENCE_RULE, SCENE3D_FREE_PLATE_SLOTS, SCENE3D_LAYOUT_REFERENCE_SCOPING_FIXTURE, SCENE3D_LAYOUT_REFERENCE_SCOPING_LINE, SCENE3D_LAYOUT_SCOPING_FOR, SCENE3D_LAYOUT_SCOPING_IGNORE, SCENE3D_LAYOUT_SCOPING_LOOK_SOURCE, SCENE3D_LAYOUT_SCOPING_MARKER, SCENE3D_UNREFERENCED_FIGURES_WARNING_CODE, TEXT_WIZARD_CATEGORIES, VIDEO_WIZARD_CATEGORIES, appendScene3DStillScopingLines, buildAdCreativeAnalysisUserText, buildCharacterPrompt, buildCreaturePrompt, buildFaceTemplateInputs, buildLocationMotionPrompt, buildLocationPrompt, buildLocationRefinePrompt, buildMotionPrompt, buildObjectMotionPrompt, buildObjectPrompt, buildScene3DLayoutScopingLine, buildScene3DUnreferencedFiguresWarning, getCategoriesForNodeType, getPromptDoctrine, getPromptTips, hasScene3DLayoutScopingLine, isWizardSupported, ownMotionHint, renderScene3DLayoutScopingLine, scene3DLayoutVideoCaptions } from './chunk-C46256GZ.js';
|
|
12
12
|
export { CINEMATIC_LOOK_TAIL, FILM_STILL_PREFIX, NEUTRAL_PERSON, REALISM_FINISH, REALISM_PASS_INSTRUCTION, REALISM_SKIN, REFERENCE_RULES, REFERENCE_RULES_MULTI_PERSON, SCENE_FRAME_RULE, filmStillPrefix, realismEmptyHands, realismViewpoint, referenceRulesBlock } from './chunk-T64D26P4.js';
|
|
13
13
|
export { DEFAULT_IDENTITY_LOCK, DEFAULT_TEMPLATES, applyTemplate, buildIdentityLockLine, characterLockToRefLock, collectIdentityLockClause, getIdentityLockClause, hasUpstreamCharacter, resolveTemplate, toIdentityLockMode, withForcedIdentityLock } from './chunk-BQWIPLLR.js';
|
|
14
14
|
import './chunk-ADMHDFHB.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Nodaro's prompt-engineering layer — person/picker catalogs with prompt hints, identity-lock clauses, entity prompt builders, brand presets, and prompt/reference assembly shared by the Nodaro platform and SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"test": "vitest run"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@nodaro/shared": "^3.
|
|
73
|
+
"@nodaro/shared": "^3.34.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"tsup": "^8.5.0",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { FACTORY_PRESETS, getFactoryPresets, presetMinReferences } from "../factory-presets.js"
|
|
3
|
+
import { getPromptFields, nodeSupportsPromptAffixes, promptAffixCoreField, promptFieldCarriesAffixes } from "../node-prompt-fields.js"
|
|
4
|
+
import { computeNodePrompt } from "../resolve-prompt.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The registries keyed by node type are plain objects, and a node type is
|
|
8
|
+
* caller-supplied (`?nodeType=` on the public factory route, a preset row's
|
|
9
|
+
* `node_type`, workflow JSON). A name that is an Object.prototype member —
|
|
10
|
+
* `constructor`, `toString`, `__proto__` — must read as "no such type", never
|
|
11
|
+
* as the prototype's member: `FACTORY_PRESETS["constructor"]` is `Object`, so
|
|
12
|
+
* `?? []` did not fire and `.map` threw an anonymous 500.
|
|
13
|
+
*/
|
|
14
|
+
const PROTOTYPE_KEYS = ["constructor", "__proto__", "toString", "hasOwnProperty", "valueOf"] as const
|
|
15
|
+
|
|
16
|
+
describe("node-type lookups and Object.prototype member names", () => {
|
|
17
|
+
it.each(PROTOTYPE_KEYS)("getFactoryPresets(%j) is an empty catalog", (key) => {
|
|
18
|
+
expect(getFactoryPresets(key)).toEqual([])
|
|
19
|
+
expect(key in FACTORY_PRESETS).toBe(key === "__proto__" || Object.prototype.hasOwnProperty.call(Object.prototype, key))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it.each(PROTOTYPE_KEYS)("presetMinReferences(%j, …) counts nothing and never throws", (key) => {
|
|
23
|
+
expect(presetMinReferences(key, { prompt: "{image:1}" })).toBe(0)
|
|
24
|
+
expect(presetMinReferences(key, {}, 2)).toBe(2)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it.each(PROTOTYPE_KEYS)("the prompt-field registry has no spec for %j", (key) => {
|
|
28
|
+
expect(getPromptFields(key)).toBeUndefined()
|
|
29
|
+
expect(nodeSupportsPromptAffixes(key)).toBe(false)
|
|
30
|
+
expect(promptAffixCoreField(key)).toBeUndefined()
|
|
31
|
+
expect(promptFieldCarriesAffixes(key, "prompt")).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it.each(PROTOTYPE_KEYS)("computeNodePrompt(%j, …) falls back to the plain prompt field", (key) => {
|
|
35
|
+
expect(computeNodePrompt(key, { prompt: "a fox" }, { refMap: new Map() })).toBe("a fox")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("a real node type still resolves (the fix narrows nothing)", () => {
|
|
39
|
+
expect(getFactoryPresets("generate-image").length).toBeGreaterThan(0)
|
|
40
|
+
expect(presetMinReferences("generate-image", { prompt: "{image:2:person}" })).toBe(2)
|
|
41
|
+
expect(getPromptFields("generate-image")?.prompt).toBe("prompt")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { placeFramesAfterReferences } from "../frame-reference-slots.js"
|
|
3
|
+
|
|
4
|
+
const R = (n: number) => Array.from({ length: n }, (_, i) => `r${i + 1}`)
|
|
5
|
+
|
|
6
|
+
describe("placeFramesAfterReferences", () => {
|
|
7
|
+
it("puts the caller's references first and the frames after them, bound by seat", () => {
|
|
8
|
+
const r = placeFramesAfterReferences({ firstFrameUrl: "F", lastFrameUrl: "L", refImageUrls: R(2), imageCap: 9 })
|
|
9
|
+
expect(r.imageUrls).toEqual(["r1", "r2", "F", "L"])
|
|
10
|
+
expect(r.firstFrameOrdinal).toBe(3)
|
|
11
|
+
expect(r.lastFrameOrdinal).toBe(4)
|
|
12
|
+
expect(r.promptSuffix).toBe("Use @image_3 as the opening (first) frame and @image_4 as the closing (last) frame of the video.")
|
|
13
|
+
expect(r.droppedRefImages).toBe(0)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("keeps the frames at the cap and drops trailing caller references", () => {
|
|
17
|
+
const r = placeFramesAfterReferences({ firstFrameUrl: "F", lastFrameUrl: "L", refImageUrls: R(3), imageCap: 3 })
|
|
18
|
+
expect(r.imageUrls).toEqual(["r1", "F", "L"])
|
|
19
|
+
expect(r.droppedRefImages).toBe(2)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("writes the binding in the provider's own syntax", () => {
|
|
23
|
+
const grok = placeFramesAfterReferences({ firstFrameUrl: "F", refImageUrls: R(2), imageCap: 7, syntax: "@image(n)", bindIdentities: true })
|
|
24
|
+
expect(grok.promptSuffix).toBe(
|
|
25
|
+
"Use @image(3) as the opening (first) frame of the video. @image(1) through @image(2) are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.",
|
|
26
|
+
)
|
|
27
|
+
const hh = placeFramesAfterReferences({ firstFrameUrl: "F", refImageUrls: R(1), imageCap: 9, syntax: "[Image N]", bindIdentities: true })
|
|
28
|
+
expect(hh.promptSuffix).toBe(
|
|
29
|
+
"Use [Image 2] as the opening (first) frame of the video. [Image 1] is an identity reference for this shot's subjects — match its subject's exact appearance; it is not a frame.",
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("skips the opening sentence when the prompt already binds it (@image_N form)", () => {
|
|
34
|
+
const r = placeFramesAfterReferences({ prompt: "use @image_2 as the first frame", firstFrameUrl: "F", refImageUrls: R(1), imageCap: 9 })
|
|
35
|
+
expect(r.promptSuffix).toBe("")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("binds nothing with no frame, and emits no identity sentence without one", () => {
|
|
39
|
+
const r = placeFramesAfterReferences({ refImageUrls: R(2), imageCap: 9, bindIdentities: true })
|
|
40
|
+
expect(r.imageUrls).toEqual(["r1", "r2"])
|
|
41
|
+
expect(r.promptSuffix).toBe("")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -13,19 +13,20 @@ describe("resolveGeminiOmniI2vInputs", () => {
|
|
|
13
13
|
const FIRST = "https://r2/anchor.png"
|
|
14
14
|
const refs = (n: number) => Array.from({ length: n }, (_, i) => `https://r2/ref-${i + 1}.png`)
|
|
15
15
|
|
|
16
|
-
it("binds the roles:
|
|
16
|
+
it("binds the roles: the references first (identities, not frames), the start frame appended after them", () => {
|
|
17
17
|
const r = resolveGeminiOmniI2vInputs({ prompt: "a walk on the beach", firstFrameUrl: FIRST, refImageUrls: refs(3) })
|
|
18
|
-
expect(r.imageUrls).toEqual([
|
|
18
|
+
expect(r.imageUrls).toEqual([...refs(3), FIRST])
|
|
19
19
|
expect(r.promptSuffix).toBe(
|
|
20
|
-
"Use @
|
|
21
|
-
"@
|
|
20
|
+
"Use @image_4 as the opening (first) frame of the video. " +
|
|
21
|
+
"@image_1 through @image_3 are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.",
|
|
22
22
|
)
|
|
23
23
|
expect(r.droppedRefImages).toBe(0)
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
it("a single reference gets the singular sentence", () => {
|
|
27
27
|
const r = resolveGeminiOmniI2vInputs({ firstFrameUrl: FIRST, refImageUrls: refs(1) })
|
|
28
|
-
expect(r.promptSuffix).toContain("@
|
|
28
|
+
expect(r.promptSuffix).toContain("@image_1 is an identity reference")
|
|
29
|
+
expect(r.promptSuffix).toContain("Use @image_2 as the opening (first) frame")
|
|
29
30
|
expect(r.promptSuffix).not.toContain("through")
|
|
30
31
|
})
|
|
31
32
|
|
|
@@ -37,11 +38,12 @@ describe("resolveGeminiOmniI2vInputs", () => {
|
|
|
37
38
|
it("drops TRAILING references to fit the 7-input quota — the start frame is never the one that goes", () => {
|
|
38
39
|
const r = resolveGeminiOmniI2vInputs({ firstFrameUrl: FIRST, refImageUrls: refs(9) })
|
|
39
40
|
expect(r.imageUrls).toHaveLength(7)
|
|
40
|
-
expect(r.imageUrls
|
|
41
|
-
expect(r.imageUrls
|
|
41
|
+
expect(r.imageUrls.at(-1)).toBe(FIRST)
|
|
42
|
+
expect(r.imageUrls[5]).toBe("https://r2/ref-6.png")
|
|
42
43
|
expect(r.droppedRefImages).toBe(3)
|
|
43
|
-
// The binding names exactly the kept span.
|
|
44
|
-
expect(r.promptSuffix).toContain("@
|
|
44
|
+
// The binding names exactly the kept span, and the frame's own seat.
|
|
45
|
+
expect(r.promptSuffix).toContain("@image_1 through @image_6")
|
|
46
|
+
expect(r.promptSuffix).toContain("Use @image_7 as the opening (first) frame")
|
|
45
47
|
})
|
|
46
48
|
|
|
47
49
|
it("a connected source video eats two slots (images + 2×videos ≤ 7)", () => {
|
|
@@ -62,7 +64,7 @@ describe("resolveGeminiOmniI2vInputs", () => {
|
|
|
62
64
|
|
|
63
65
|
it("skips empty/undefined reference entries without burning slots", () => {
|
|
64
66
|
const r = resolveGeminiOmniI2vInputs({ firstFrameUrl: FIRST, refImageUrls: [undefined, "", ...refs(2)] })
|
|
65
|
-
expect(r.imageUrls).toEqual([
|
|
67
|
+
expect(r.imageUrls).toEqual([...refs(2), FIRST])
|
|
66
68
|
expect(r.droppedRefImages).toBe(0)
|
|
67
69
|
})
|
|
68
70
|
})
|
|
@@ -98,6 +98,20 @@ describe("PROVIDER_PROMPT_DOCTRINES", () => {
|
|
|
98
98
|
expect(d.doctrine).not.toMatch(/higher quality/i)
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
it("Vidu Q4 has its own doctrine carrying the two-endpoint contract", () => {
|
|
102
|
+
const d = getPromptDoctrine("vidu-q4")!
|
|
103
|
+
expect(d.providers).toEqual(["vidu-q4"])
|
|
104
|
+
// Mode pick, the frame fold and the hard reference-audio contract.
|
|
105
|
+
expect(d.doctrine).toMatch(/reference-to-video/)
|
|
106
|
+
expect(d.doctrine).toMatch(/prompt is REQUIRED/)
|
|
107
|
+
expect(d.doctrine).toMatch(/no text-only mode/i)
|
|
108
|
+
expect(d.doctrine).toMatch(/MP3, each 3-12 seconds/)
|
|
109
|
+
expect(d.doctrine).toMatch(/1-15 reference images/)
|
|
110
|
+
// The binding the editor actually emits, not the contract's prose form.
|
|
111
|
+
expect(d.doctrine).toContain("@image_N")
|
|
112
|
+
expect(d.doctrine).toMatch(/20,000 characters/)
|
|
113
|
+
})
|
|
114
|
+
|
|
101
115
|
it("Gemini Omni Flash rides the sibling's doctrine (identical request surface)", () => {
|
|
102
116
|
const d = getPromptDoctrine("gemini-omni-flash")!
|
|
103
117
|
expect(d).toBe(getPromptDoctrine("gemini-omni-video"))
|
|
@@ -6,9 +6,10 @@ import { resolveVeoI2vInputs } from "../veo-i2v-inputs.js"
|
|
|
6
6
|
* VEO 3.x i2v input resolution. VEO's API makes frame conditioning and
|
|
7
7
|
* reference ingredients mutually exclusive (one imageUrls array, ≤3, whose
|
|
8
8
|
* meaning flips with generationType) — so an anchored call that must carry
|
|
9
|
-
* identity references moves to REFERENCE_2_VIDEO
|
|
10
|
-
*
|
|
11
|
-
* frames
|
|
9
|
+
* identity references moves to REFERENCE_2_VIDEO. The platform frame-slot
|
|
10
|
+
* convention (2026-10-18): the caller's references take the first seats, the
|
|
11
|
+
* frames follow them, and at the 3-seat cap the frames are kept while
|
|
12
|
+
* trailing references drop.
|
|
12
13
|
*/
|
|
13
14
|
describe("resolveVeoI2vInputs", () => {
|
|
14
15
|
const FIRST = "https://r2/anchor.png"
|
|
@@ -24,26 +25,30 @@ describe("resolveVeoI2vInputs", () => {
|
|
|
24
25
|
})
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
it("references flip the call to REFERENCE_2_VIDEO
|
|
28
|
+
it("references flip the call to REFERENCE_2_VIDEO, references first and the anchor after them, capped at 3", () => {
|
|
28
29
|
const r = resolveVeoI2vInputs({ prompt: "p", firstFrameUrl: FIRST, refImageUrls: refs(4) })
|
|
29
30
|
expect(r.generationType).toBe("REFERENCE_2_VIDEO")
|
|
30
|
-
expect(r.imageUrls).toEqual([
|
|
31
|
+
expect(r.imageUrls).toEqual(["https://r2/ref-1.png", "https://r2/ref-2.png", FIRST])
|
|
31
32
|
expect(r.droppedRefImages).toBe(2)
|
|
32
33
|
expect(r.promptSuffix).toBe(
|
|
33
|
-
"Use @
|
|
34
|
-
"@
|
|
34
|
+
"Use @image_3 as the opening (first) frame of the video. " +
|
|
35
|
+
"@image_1 through @image_2 are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.",
|
|
35
36
|
)
|
|
36
37
|
})
|
|
37
38
|
|
|
38
|
-
it("the end
|
|
39
|
+
it("the end frame is KEPT in reference mode — frames win the seats, a trailing reference drops", () => {
|
|
39
40
|
const r = resolveVeoI2vInputs({ firstFrameUrl: FIRST, endFrameUrl: "https://r2/end.png", refImageUrls: refs(2) })
|
|
40
|
-
expect(r.imageUrls).toEqual([
|
|
41
|
-
expect(r.
|
|
41
|
+
expect(r.imageUrls).toEqual(["https://r2/ref-1.png", FIRST, "https://r2/end.png"])
|
|
42
|
+
expect(r.droppedRefImages).toBe(1)
|
|
43
|
+
expect(r.droppedEndFrame).toBe(false)
|
|
44
|
+
expect(r.promptSuffix).toContain("Use @image_2 as the opening (first) frame and @image_3 as the closing (last) frame of the video.")
|
|
45
|
+
expect(r.promptSuffix).toContain("@image_1 is an identity reference")
|
|
42
46
|
})
|
|
43
47
|
|
|
44
48
|
it("a single kept reference gets the singular sentence", () => {
|
|
45
49
|
const r = resolveVeoI2vInputs({ firstFrameUrl: FIRST, refImageUrls: refs(1) })
|
|
46
|
-
expect(r.promptSuffix).toContain("@
|
|
50
|
+
expect(r.promptSuffix).toContain("@image_1 is an identity reference")
|
|
51
|
+
expect(r.promptSuffix).toContain("Use @image_2 as the opening (first) frame")
|
|
47
52
|
})
|
|
48
53
|
|
|
49
54
|
it("suppresses the opening-frame sentence when the prompt already binds it", () => {
|
package/src/entries/assembly.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from "../assemble-video-input.js"
|
|
|
20
20
|
export * from "../seedance-2-inputs.js"
|
|
21
21
|
export * from "../gemini-omni-inputs.js"
|
|
22
22
|
export * from "../veo-i2v-inputs.js"
|
|
23
|
+
export * from "../frame-reference-slots.js"
|
|
23
24
|
export * from "../surround-fill.js"
|
|
24
25
|
// --- Start/end frame delivery: one plan for the dispatch and the editor ---
|
|
25
26
|
export * from "../frame-delivery.js"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { T2I_TO_I2I_VARIANT } from "@nodaro/shared/models"
|
|
2
|
+
import { ownEntry } from "../own-entry.js"
|
|
2
3
|
import type { FactoryPreset } from "./types.js"
|
|
3
4
|
import { stylizedSubjectFor, editsFor } from "./shared-image.js"
|
|
4
5
|
import { GENERATE_IMAGE_PRESETS } from "./generate-image.js"
|
|
@@ -57,6 +58,8 @@ export const FACTORY_PRESETS: Readonly<Record<string, readonly FactoryPreset[]>>
|
|
|
57
58
|
"lottie-overlay": LOTTIE_OVERLAY_PRESETS,
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/** The catalog for `nodeType`, or an empty one — also for a name that is an
|
|
62
|
+
* Object.prototype member (`constructor`), which is no catalog of ours. */
|
|
60
63
|
export function getFactoryPresets(nodeType: string): readonly FactoryPreset[] {
|
|
61
|
-
return FACTORY_PRESETS
|
|
64
|
+
return ownEntry(FACTORY_PRESETS, nodeType) ?? []
|
|
62
65
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IMAGE_TOKEN_PATTERN } from "../image-token-pattern.js"
|
|
2
2
|
import { getPromptFields, nodeSupportsPromptAffixes, promptAffixCoreField } from "../node-prompt-fields.js"
|
|
3
3
|
import { NODE_PROMPT_CANDIDATE_FIELDS } from "../resolve-prompt.js"
|
|
4
|
+
import { ownEntry } from "../own-entry.js"
|
|
4
5
|
import { MODELS_WITH_REFERENCE_IMAGE_SUPPORT, imageReferenceLimit } from "@nodaro/shared/models"
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -25,7 +26,7 @@ export const MAX_PRESET_REFERENCES: number = Math.max(
|
|
|
25
26
|
*/
|
|
26
27
|
function promptFieldsOf(nodeType: string): string[] {
|
|
27
28
|
const spec = getPromptFields(nodeType)
|
|
28
|
-
const candidates = NODE_PROMPT_CANDIDATE_FIELDS
|
|
29
|
+
const candidates = ownEntry(NODE_PROMPT_CANDIDATE_FIELDS, nodeType) ?? []
|
|
29
30
|
if (!spec && candidates.length === 0) return []
|
|
30
31
|
const fields = new Set<string>(candidates)
|
|
31
32
|
if (spec) {
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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": {
|