@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
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-6MPZHFIS.js';
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-Z3SZXWRU.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-7XMTVYS5.js';
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-XU54YJXQ.js';
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-XRLOMTBZ.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-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.39.0",
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.33.0"
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: image 1 is the opening frame, the rest are identities — not frames", () => {
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([FIRST, ...refs(3)])
18
+ expect(r.imageUrls).toEqual([...refs(3), FIRST])
19
19
  expect(r.promptSuffix).toBe(
20
- "Use @image_1 as the opening (first) frame of the video. " +
21
- "@image_2 through @image_4 are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.",
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("@image_2 is an identity reference")
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[0]).toBe(FIRST)
41
- expect(r.imageUrls.at(-1)).toBe("https://r2/ref-6.png")
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("@image_2 through @image_7")
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([FIRST, ...refs(2)])
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 with the anchor in seat 1.
10
- * References win the seats (the 2026-08-14 standing rule: refs are a must,
11
- * frames additional): the end anchor is dropped in reference mode.
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 with the anchor in seat 1, capped at 3", () => {
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([FIRST, "https://r2/ref-1.png", "https://r2/ref-2.png"])
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 @image_1 as the opening (first) frame of the video. " +
34
- "@image_2 through @image_3 are identity references for this shot's subjects — match each subject's exact appearance; they are not frames.",
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 anchor is DROPPED in reference mode — references win the seats", () => {
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([FIRST, "https://r2/ref-1.png", "https://r2/ref-2.png"])
41
- expect(r.droppedEndFrame).toBe(true)
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("@image_2 is an identity reference")
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", () => {
@@ -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[nodeType] ?? []
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[nodeType] ?? []
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 { promptBindsFirstFrame } from "./seedance-2-inputs.js"
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, through the same `REF_BINDING` swap-point every
18
- * other video binding uses: image 1 is the opening frame; the rest are
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, mirroring `resolveSeedance2Inputs`' drop-trailing
27
- * convention, and the drop count is reported for the caller to log.
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, always first in the list. */
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
- /** `[firstFrameUrl, ...keptRefs]` — the `image_urls` payload, quota-fitted. */
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 refSlots = Math.max(0, slots - 1)
70
- const kept = refs.slice(0, refSlots)
71
- const droppedRefImages = refs.length - kept.length
72
- const imageUrls = [args.firstFrameUrl, ...kept]
73
- if (kept.length === 0) return { imageUrls, promptSuffix: "", droppedRefImages }
74
-
75
- // The opening-frame sentence is suppressed when the prompt already binds it
76
- // at its own (working) position — same field-finding rule as seedance-2: a
77
- // duplicate directive at the end dilutes the one that works.
78
- const frameSentence = promptBindsFirstFrame(args.prompt) ? "" : REF_BINDING.frame(1, "opening")
79
- const promptSuffix = [frameSentence, identityRefsSentence(2, kept.length + 1)].filter(Boolean).join(" ")
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[nodeType] : undefined
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[nodeType!] ?? []).includes(promptField)
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). */
@@ -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": {