@nodaro/shared 1.13.1 → 1.14.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/index.cjs +61 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +54 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/gvp-supported-providers.test.ts +31 -0
- package/src/__tests__/video-analysis.test.ts +83 -0
- package/src/index.ts +2 -0
- package/src/model-constants.ts +16 -0
- package/src/social-post.ts +6 -1
- package/src/video-analysis.ts +79 -0
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { GVP_SUPPORTED_PROVIDERS, isGvpSupportedProvider, isSeedance2Provider } from "../model-constants.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate/Edit Video Pro provider-selection guard (2026-07-21): the pro
|
|
6
|
+
* nodes offer ONLY these SKUs. The list is intentionally narrower than the
|
|
7
|
+
* SEEDANCE_2_PROVIDERS capability family (mini is a Seedance-2 variant for
|
|
8
|
+
* capability gating but is NOT offered by the pro engine). If a new SKU is
|
|
9
|
+
* blessed for the pro nodes, update this pin together with the docs pages
|
|
10
|
+
* (docs/nodes/ai-video/generate-video-pro.md, edit-video-pro.md).
|
|
11
|
+
*/
|
|
12
|
+
describe("GVP_SUPPORTED_PROVIDERS", () => {
|
|
13
|
+
it("is exactly the blessed pro SKUs", () => {
|
|
14
|
+
expect([...GVP_SUPPORTED_PROVIDERS]).toEqual(["seedance-2", "seedance-2-fast"])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("is a strict subset of the Seedance-2 capability family", () => {
|
|
18
|
+
for (const p of GVP_SUPPORTED_PROVIDERS) {
|
|
19
|
+
expect(isSeedance2Provider(p)).toBe(true)
|
|
20
|
+
}
|
|
21
|
+
// mini stays in the family (capabilities) but out of pro selection
|
|
22
|
+
expect(isSeedance2Provider("seedance-2-mini")).toBe(true)
|
|
23
|
+
expect(isGvpSupportedProvider("seedance-2-mini")).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("predicate matches the list and rejects outsiders", () => {
|
|
27
|
+
for (const p of GVP_SUPPORTED_PROVIDERS) expect(isGvpSupportedProvider(p)).toBe(true)
|
|
28
|
+
expect(isGvpSupportedProvider("veo3")).toBe(false)
|
|
29
|
+
expect(isGvpSupportedProvider(undefined)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
windowAnalysisSchema, videoAnalysisResultSchema,
|
|
4
4
|
deriveSlotRefs, rewriteSlotTokens, unwrapUnresolvedTokens,
|
|
5
5
|
renderAnalyzedScene, isOversizedScene, aspectRatioFromDims,
|
|
6
|
+
entitySlotSchema, analyzedSceneSchema,
|
|
7
|
+
rewriteSceneBindings, dropUnknownBindings,
|
|
8
|
+
VIDEO_ANALYSIS_MAX_VARIATIONS, VIDEO_ANALYSIS_VARIATION_SLUGS, VIDEO_ANALYSIS_DEFAULT_VARIATION,
|
|
6
9
|
type EntitySlot,
|
|
7
10
|
} from "../video-analysis.js"
|
|
8
11
|
|
|
@@ -48,6 +51,86 @@ describe("token helpers", () => {
|
|
|
48
51
|
})
|
|
49
52
|
})
|
|
50
53
|
|
|
54
|
+
const dreamVariation = {
|
|
55
|
+
variationId: "dream",
|
|
56
|
+
label: "Dream self",
|
|
57
|
+
description: "tan man, mustache — flowing white robe, barefoot, hair loose (dream sequences)",
|
|
58
|
+
refImageUrl: "https://cdn.example/frames/hero-dream.jpg",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("appearance variations (cast-variations spec §4)", () => {
|
|
62
|
+
it("entitySlotSchema round-trips variations[] including refImageUrl", () => {
|
|
63
|
+
const parsed = entitySlotSchema.parse({ ...slot, variations: [dreamVariation] })
|
|
64
|
+
expect(parsed.variations).toEqual([dreamVariation])
|
|
65
|
+
})
|
|
66
|
+
it("absent variations stays absent (no [] materialization)", () => {
|
|
67
|
+
const parsed = entitySlotSchema.parse(slot)
|
|
68
|
+
expect("variations" in parsed && parsed.variations !== undefined).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
it(`rejects more than VIDEO_ANALYSIS_MAX_VARIATIONS (${4}) — window layer rejects, merge folds`, () => {
|
|
71
|
+
expect(VIDEO_ANALYSIS_MAX_VARIATIONS).toBe(4)
|
|
72
|
+
const five = ["dream", "flashback", "disguise", "era", "alt-1"].map((id) => ({ ...dreamVariation, variationId: id }))
|
|
73
|
+
expect(entitySlotSchema.safeParse({ ...slot, variations: five }).success).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
it("rejects the reserved 'default' variationId inside variations[] (D9)", () => {
|
|
76
|
+
expect(VIDEO_ANALYSIS_DEFAULT_VARIATION).toBe("default")
|
|
77
|
+
expect(entitySlotSchema.safeParse({ ...slot, variations: [{ ...dreamVariation, variationId: "default" }] }).success).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
it("rejects a malformed variationId (slug charset only; vocabulary is doctrine-enforced)", () => {
|
|
80
|
+
expect(entitySlotSchema.safeParse({ ...slot, variations: [{ ...dreamVariation, variationId: "Dream Look" }] }).success).toBe(false)
|
|
81
|
+
expect(VIDEO_ANALYSIS_VARIATION_SLUGS).toContain("dream")
|
|
82
|
+
expect(VIDEO_ANALYSIS_VARIATION_SLUGS).toContain("alt-2")
|
|
83
|
+
})
|
|
84
|
+
it("windowAnalysisSchema scenes round-trip slotVariations; absent stays absent", () => {
|
|
85
|
+
const bound = { ...baseScene, slotVariations: { hero: "dream" } }
|
|
86
|
+
const parsed = windowAnalysisSchema.parse({ slots: [{ ...slot, variations: [dreamVariation] }], scenes: [bound, baseScene] })
|
|
87
|
+
expect(parsed.scenes[0].slotVariations).toEqual({ hero: "dream" })
|
|
88
|
+
expect(parsed.scenes[1].slotVariations).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
it("analyzedSceneSchema inherits slotVariations from the same base", () => {
|
|
91
|
+
const parsed = analyzedSceneSchema.parse({
|
|
92
|
+
...baseScene, sceneNumber: 1, visualResolved: "a man juggles", slotRefs: ["hero"], slotVariations: { hero: "dream" },
|
|
93
|
+
})
|
|
94
|
+
expect(parsed.slotVariations).toEqual({ hero: "dream" })
|
|
95
|
+
})
|
|
96
|
+
it("videoAnalysisResultSchema full-document round-trip with both fields", () => {
|
|
97
|
+
const meta = { durationSec: 10, width: 1920, height: 1080, aspectRatio: "16:9" }
|
|
98
|
+
const doc = {
|
|
99
|
+
meta,
|
|
100
|
+
slots: [{ ...slot, variations: [dreamVariation] }],
|
|
101
|
+
scenes: [{ ...baseScene, sceneNumber: 1, visualResolved: "a man juggles", slotRefs: ["hero"], slotVariations: { hero: "dream" } }],
|
|
102
|
+
}
|
|
103
|
+
expect(videoAnalysisResultSchema.parse(doc)).toEqual(doc)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("binding rewrite helpers (merge consumes — spec §4)", () => {
|
|
108
|
+
it("rewriteSceneBindings renames slot keys and per-slot variation values", () => {
|
|
109
|
+
expect(rewriteSceneBindings({ "man-2": "dream", other: "era" }, { "man-2": "hero" }, { hero: { dream: "flashback" } }))
|
|
110
|
+
.toEqual({ hero: "flashback", other: "era" })
|
|
111
|
+
})
|
|
112
|
+
it("rewriteSceneBindings passes undefined through", () => {
|
|
113
|
+
expect(rewriteSceneBindings(undefined, { a: "b" })).toBeUndefined()
|
|
114
|
+
})
|
|
115
|
+
it("dropUnknownBindings drops unknown (slot, variation) pairs and reports them", () => {
|
|
116
|
+
const valid = new Map([["hero", new Set(["dream"])]])
|
|
117
|
+
const r = dropUnknownBindings({ hero: "dream", hero2: "dream", other: "ghost" }, valid)
|
|
118
|
+
expect(r.kept).toEqual({ hero: "dream" })
|
|
119
|
+
expect(r.dropped).toEqual([{ slotId: "hero2", variationId: "dream" }, { slotId: "other", variationId: "ghost" }])
|
|
120
|
+
})
|
|
121
|
+
it("dropUnknownBindings treats 'default' as always valid for a known slot", () => {
|
|
122
|
+
const valid = new Map([["hero", new Set<string>()]])
|
|
123
|
+
const r = dropUnknownBindings({ hero: "default" }, valid)
|
|
124
|
+
expect(r.kept).toEqual({ hero: "default" })
|
|
125
|
+
expect(r.dropped).toEqual([])
|
|
126
|
+
})
|
|
127
|
+
it("dropUnknownBindings returns kept: undefined when nothing survives (no {} materialization)", () => {
|
|
128
|
+
const r = dropUnknownBindings({ ghost: "dream" }, new Map())
|
|
129
|
+
expect(r.kept).toBeUndefined()
|
|
130
|
+
expect(r.dropped).toEqual([{ slotId: "ghost", variationId: "dream" }])
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
51
134
|
describe("misc", () => {
|
|
52
135
|
it("isOversizedScene flags > 8s only", () => {
|
|
53
136
|
expect(isOversizedScene(0, 8)).toBe(false)
|
package/src/index.ts
CHANGED
|
@@ -106,6 +106,8 @@ export {
|
|
|
106
106
|
VIDEO_GEN_COLLAPSED_T2V_IDS,
|
|
107
107
|
resolveVideoProviderForMode,
|
|
108
108
|
isSeedance2Provider,
|
|
109
|
+
GVP_SUPPORTED_PROVIDERS,
|
|
110
|
+
isGvpSupportedProvider,
|
|
109
111
|
defaultVideoAspectRatio,
|
|
110
112
|
CHARACTER_MOTION_PROVIDERS,
|
|
111
113
|
LOCATION_ATMOSPHERE_PROVIDERS,
|
package/src/model-constants.ts
CHANGED
|
@@ -1124,6 +1124,22 @@ export function isSeedance2Provider(provider: string | undefined): boolean {
|
|
|
1124
1124
|
return !!provider && SEEDANCE_2_PROVIDERS.has(provider)
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* Generate/Edit Video Pro SUPPORT subset — the only SKUs the pro multi-segment
|
|
1129
|
+
* engine currently offers (mini withdrawn from selection, 2026-07-21).
|
|
1130
|
+
* Deliberately distinct from SEEDANCE_2_PROVIDERS: the family set gates
|
|
1131
|
+
* CAPABILITIES (ref limits, i2v params, adaptive aspect) and must keep every
|
|
1132
|
+
* variant; this list gates which SKUs the pro nodes offer in selection. The
|
|
1133
|
+
* pro plugin routes stay tolerant of the full family so previously-saved
|
|
1134
|
+
* workflows keep running — the editor fail-safe snaps stale selections to a
|
|
1135
|
+
* supported SKU instead.
|
|
1136
|
+
*/
|
|
1137
|
+
export const GVP_SUPPORTED_PROVIDERS = ["seedance-2", "seedance-2-fast"] as const
|
|
1138
|
+
|
|
1139
|
+
export function isGvpSupportedProvider(provider: string | undefined): boolean {
|
|
1140
|
+
return !!provider && (GVP_SUPPORTED_PROVIDERS as readonly string[]).includes(provider)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1127
1143
|
/**
|
|
1128
1144
|
* Default aspect ratio for a video provider when the node carries no explicit
|
|
1129
1145
|
* `aspectRatio`. Seedance 2.x defaults to `"adaptive"` (output matches the
|
package/src/social-post.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* on Instagram's carousel item limits.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/** Node types that publish to a social platform via POST /v1/social/publish.
|
|
9
|
+
/** Node types that publish to a social platform via POST /v1/social/publish.
|
|
10
|
+
* The 7 per-platform nodes hardcode their platform via `node.type`;
|
|
11
|
+
* `publish-social` is the UNIFIED node whose platform is derived from the
|
|
12
|
+
* chosen connection (`data.platform`) instead. All shared-set-driven routing
|
|
13
|
+
* (carousel accumulation, caption, refMap gate) covers it automatically. */
|
|
10
14
|
export const SOCIAL_POST_NODE_TYPES = new Set([
|
|
11
15
|
"instagram-post",
|
|
12
16
|
"tiktok-post",
|
|
@@ -15,6 +19,7 @@ export const SOCIAL_POST_NODE_TYPES = new Set([
|
|
|
15
19
|
"x-post",
|
|
16
20
|
"facebook-post",
|
|
17
21
|
"telegram-post",
|
|
22
|
+
"publish-social",
|
|
18
23
|
])
|
|
19
24
|
|
|
20
25
|
/** Instagram carousel limits per Meta Graph API.
|
package/src/video-analysis.ts
CHANGED
|
@@ -25,12 +25,46 @@ export type VideoAnalysisEntitySource = (typeof VIDEO_ANALYSIS_ENTITY_SOURCES)[n
|
|
|
25
25
|
/** Matches {slot:<id>} tokens. Distinct from NODE_REF_PATTERN / {image:N} grammars. */
|
|
26
26
|
export const SLOT_TOKEN_RE = /\{slot:([a-z0-9-]+)\}/g
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Appearance variations (cast-variations spec, 2026-07-24): a slot's canonical
|
|
30
|
+
* `description` IS its default look; `variations` enumerates NON-default looks
|
|
31
|
+
* only (dream vs reality, flashback, disguise…). `"default"` is a reserved
|
|
32
|
+
* variationId — used in scene bindings / ledger keys / routing for unbound
|
|
33
|
+
* scenes, never inside `variations[]`. The closed slug vocabulary is
|
|
34
|
+
* doctrine-enforced (the schema pins only the id charset, like `role`);
|
|
35
|
+
* `alt-1`/`alt-2` are the escape hatches.
|
|
36
|
+
*/
|
|
37
|
+
export const VIDEO_ANALYSIS_VARIATION_SLUGS = ["dream", "flashback", "disguise", "costume", "transformation", "era", "alt-1", "alt-2"] as const
|
|
38
|
+
/** Max NON-default looks per slot. The window layer REJECTS past the cap (schema-forced retry); the cross-window merge FOLDS at the cap. */
|
|
39
|
+
export const VIDEO_ANALYSIS_MAX_VARIATIONS = 4
|
|
40
|
+
export const VIDEO_ANALYSIS_DEFAULT_VARIATION = "default"
|
|
41
|
+
|
|
42
|
+
export const slotVariationSchema = z.object({
|
|
43
|
+
variationId: z.string().min(1).regex(/^[a-z0-9-]+$/)
|
|
44
|
+
.refine((id) => id !== VIDEO_ANALYSIS_DEFAULT_VARIATION, { message: `"${VIDEO_ANALYSIS_DEFAULT_VARIATION}" is reserved for the slot's canonical look` }),
|
|
45
|
+
label: z.string().min(1),
|
|
46
|
+
/** Full STANDALONE casting-sheet look restating the slot's invariant identity
|
|
47
|
+
* core (face, build, age) — the manifest substitutes it wholesale, so a
|
|
48
|
+
* wardrobe-only delta would silently delete identity from the prompt. */
|
|
49
|
+
description: z.string().min(1),
|
|
50
|
+
/** Per-variation identity reference (auto-cast frame or user sheet). In the
|
|
51
|
+
* schema from day one: every strip-mode round-trip must carry it. */
|
|
52
|
+
refImageUrl: z.string().url().optional(),
|
|
53
|
+
})
|
|
54
|
+
export type SlotVariation = z.infer<typeof slotVariationSchema>
|
|
55
|
+
|
|
28
56
|
export const entitySlotSchema = z.object({
|
|
29
57
|
slotId: z.string().min(1).regex(/^[a-z0-9-]+$/),
|
|
30
58
|
label: z.string().min(1),
|
|
31
59
|
source: z.enum(VIDEO_ANALYSIS_ENTITY_SOURCES),
|
|
32
60
|
role: z.string().min(1),
|
|
33
61
|
description: z.string().min(1),
|
|
62
|
+
/** Auto-cast visual reference — a hosted frame from the analyzed footage
|
|
63
|
+
* where this entity is clearly visible. Optional/additive: producers may
|
|
64
|
+
* omit it; consumers use it as an identity reference for recreation. */
|
|
65
|
+
refImageUrl: z.string().url().optional(),
|
|
66
|
+
/** NON-default looks only; present only when at least one exists. */
|
|
67
|
+
variations: z.array(slotVariationSchema).max(VIDEO_ANALYSIS_MAX_VARIATIONS).optional(),
|
|
34
68
|
})
|
|
35
69
|
export type EntitySlot = z.infer<typeof entitySlotSchema>
|
|
36
70
|
|
|
@@ -57,6 +91,11 @@ const windowSceneBase = z.object({
|
|
|
57
91
|
transitionOut: z.enum(["cut", "fade", "wipe", "whip"]).optional(),
|
|
58
92
|
// Array of concurrent layers (music + speech + sfx together); [] = silence.
|
|
59
93
|
audio: z.array(audioLayerSchema),
|
|
94
|
+
/** slotId → variationId for slots wearing a NON-default look in this scene
|
|
95
|
+
* (only slots referenced in the scene; absent key ⇒ the default look).
|
|
96
|
+
* Rides windowSceneBase so BOTH the window layer and analyzedSceneSchema
|
|
97
|
+
* inherit it — the out-of-band binding channel (no in-text markers, D6). */
|
|
98
|
+
slotVariations: z.record(z.string(), z.string()).optional(),
|
|
60
99
|
})
|
|
61
100
|
// .strip() (default) drops model-emitted oversized/slotRefs — validator-computed only.
|
|
62
101
|
const windowSceneSchema = windowSceneBase.refine((s) => s.endSec > s.startSec, { message: "endSec must be > startSec" })
|
|
@@ -113,6 +152,46 @@ export function unwrapUnresolvedTokens(text: string, validIds: Set<string>): { t
|
|
|
113
152
|
return { text: out, unresolved }
|
|
114
153
|
}
|
|
115
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Rewrite a scene's `slotVariations` after cross-window slot/variation
|
|
157
|
+
* unification: slot keys map through `slotRenames`, then each variation value
|
|
158
|
+
* maps through `variationRenames[<new slotId>]`. Absent renames pass through.
|
|
159
|
+
*/
|
|
160
|
+
export function rewriteSceneBindings(
|
|
161
|
+
sv: Record<string, string> | undefined,
|
|
162
|
+
slotRenames: Record<string, string>,
|
|
163
|
+
variationRenames?: Record<string, Record<string, string>>,
|
|
164
|
+
): Record<string, string> | undefined {
|
|
165
|
+
if (!sv) return undefined
|
|
166
|
+
const out: Record<string, string> = {}
|
|
167
|
+
for (const [slotId, variationId] of Object.entries(sv)) {
|
|
168
|
+
const newSlot = slotRenames[slotId] ?? slotId
|
|
169
|
+
out[newSlot] = variationRenames?.[newSlot]?.[variationId] ?? variationId
|
|
170
|
+
}
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Drop bindings whose (slotId, variationId) no longer exists after a merge —
|
|
176
|
+
* the unwrap-rule mirror: never persist a dangling binding, and report what
|
|
177
|
+
* was dropped so the caller can warn. `"default"` is always valid for a known
|
|
178
|
+
* slot. `kept` is undefined when nothing survives (no `{}` materialization).
|
|
179
|
+
*/
|
|
180
|
+
export function dropUnknownBindings(
|
|
181
|
+
sv: Record<string, string> | undefined,
|
|
182
|
+
validBySlot: Map<string, Set<string>>,
|
|
183
|
+
): { kept?: Record<string, string>; dropped: Array<{ slotId: string; variationId: string }> } {
|
|
184
|
+
if (!sv) return { dropped: [] }
|
|
185
|
+
const kept: Record<string, string> = {}
|
|
186
|
+
const dropped: Array<{ slotId: string; variationId: string }> = []
|
|
187
|
+
for (const [slotId, variationId] of Object.entries(sv)) {
|
|
188
|
+
const valid = validBySlot.get(slotId)
|
|
189
|
+
if (valid && (variationId === VIDEO_ANALYSIS_DEFAULT_VARIATION || valid.has(variationId))) kept[slotId] = variationId
|
|
190
|
+
else dropped.push({ slotId, variationId })
|
|
191
|
+
}
|
|
192
|
+
return { kept: Object.keys(kept).length > 0 ? kept : undefined, dropped }
|
|
193
|
+
}
|
|
194
|
+
|
|
116
195
|
/** Substitute {slot:x}: castMap binding wins, else the slot's description, else literal id. */
|
|
117
196
|
export function renderAnalyzedScene(scene: { visual: string }, slots: EntitySlot[], castMap?: Record<string, string>): string {
|
|
118
197
|
const byId = new Map(slots.map((s) => [s.slotId, s]))
|