@nodaro/shared 3.10.0 → 3.11.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 +195 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -6
- package/dist/index.d.ts +224 -6
- package/dist/index.js +181 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/parameter-node-value.test.ts +13 -1
- package/src/__tests__/video-analysis.test.ts +15 -0
- package/src/__tests__/video-frame-fit.test.ts +189 -0
- package/src/catalog-projection.ts +3 -0
- package/src/character-motion-metadata.ts +19 -0
- package/src/i18n/character-motion.ar.ts +126 -75
- package/src/i18n/character-motion.de.ts +126 -75
- package/src/i18n/character-motion.es.ts +126 -75
- package/src/i18n/character-motion.fr.ts +126 -75
- package/src/i18n/character-motion.he.ts +126 -75
- package/src/i18n/character-motion.hi.ts +126 -75
- package/src/i18n/character-motion.ja.ts +126 -75
- package/src/i18n/character-motion.ko.ts +126 -75
- package/src/i18n/character-motion.pt-BR.ts +126 -75
- package/src/i18n/character-motion.ru.ts +126 -75
- package/src/i18n/character-motion.zh-CN.ts +126 -75
- package/src/index.ts +5 -0
- package/src/parameter-node-value.ts +31 -5
- package/src/video-analysis.ts +15 -0
- package/src/video-frame-fit.ts +228 -0
- package/src/video-output-canvas.ts +119 -0
package/package.json
CHANGED
|
@@ -154,7 +154,19 @@ describe("VIDEO_ONLY_PARAMETER_NODE_TYPES", () => {
|
|
|
154
154
|
|
|
155
155
|
describe("EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES", () => {
|
|
156
156
|
it("names the pickers whose fragment needs the graph at execution", () => {
|
|
157
|
-
|
|
157
|
+
// Transition and character-fx joined camera-motion and character-motion in
|
|
158
|
+
// the signed-off "compose from the graph on every path" change: they always
|
|
159
|
+
// composed in the config-panel preview, on the canvas card and on the
|
|
160
|
+
// frontend `{Label}` path, and now do so on both cinematography collectors
|
|
161
|
+
// too. Unwired pickers are byte-identical either way — proved entry-by-entry
|
|
162
|
+
// in packages/prompts/src/__tests__/graph-composed-unwired-identity.test.ts.
|
|
163
|
+
expect([...EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES].sort()).toEqual(
|
|
164
|
+
["camera-motion", "character-fx", "character-motion", "transition"],
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it("every member is a real parameter node type", () => {
|
|
169
|
+
for (const t of EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES) expect(PARAMETER_NODE_TYPES.has(t), t).toBe(true)
|
|
158
170
|
})
|
|
159
171
|
})
|
|
160
172
|
|
|
@@ -62,6 +62,21 @@ const dreamVariation = {
|
|
|
62
62
|
refImageUrl: "https://cdn.example/frames/hero-dream.jpg",
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
describe("owned objects (2026-09-17)", () => {
|
|
66
|
+
it("entitySlotSchema round-trips owner { slotId, relation }", () => {
|
|
67
|
+
const owner = { slotId: "man-blue", relation: "worn by" }
|
|
68
|
+
const parsed = entitySlotSchema.parse({ ...slot, owner })
|
|
69
|
+
expect(parsed.owner).toEqual(owner)
|
|
70
|
+
})
|
|
71
|
+
it("absent owner stays absent", () => {
|
|
72
|
+
expect("owner" in entitySlotSchema.parse(slot)).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
it("rejects an owner without a relation or with a malformed slot id", () => {
|
|
75
|
+
expect(entitySlotSchema.safeParse({ ...slot, owner: { slotId: "man-blue" } }).success).toBe(false)
|
|
76
|
+
expect(entitySlotSchema.safeParse({ ...slot, owner: { slotId: "Man Blue", relation: "worn by" } }).success).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
65
80
|
describe("appearance variations (cast-variations spec §4)", () => {
|
|
66
81
|
it("entitySlotSchema round-trips variations[] including refImageUrl", () => {
|
|
67
82
|
const parsed = entitySlotSchema.parse({ ...slot, variations: [dreamVariation] })
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
computeFrameFitPlan,
|
|
4
|
+
resolveFrameFitAspect,
|
|
5
|
+
resolveFrameDelivery,
|
|
6
|
+
minimalRatioDimensions,
|
|
7
|
+
centreCropToAspect,
|
|
8
|
+
parseAspectToken,
|
|
9
|
+
FRAME_FIT_STRETCH_TOLERANCE,
|
|
10
|
+
} from "../video-frame-fit.js"
|
|
11
|
+
import { resolveOutputCanvas, measuredCanvasCombinations } from "../video-output-canvas.js"
|
|
12
|
+
|
|
13
|
+
/** The image behind every number in this file: a 1K 9:16 GPT image. */
|
|
14
|
+
const GPT_1K = { sourceWidth: 940, sourceHeight: 1672 }
|
|
15
|
+
|
|
16
|
+
describe("measured output canvases", () => {
|
|
17
|
+
it("answers the combinations we measured, case-insensitively on resolution", () => {
|
|
18
|
+
expect(resolveOutputCanvas("seedance-2-5", "720p", "9:16")).toEqual([720, 1280])
|
|
19
|
+
expect(resolveOutputCanvas("minimax-h3", "768P", "9:16")).toEqual([768, 1344])
|
|
20
|
+
expect(resolveOutputCanvas("minimax-h3", "2K", "16:9")).toEqual([2560, 1440])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("keeps the 2.0 family apart from 2.5 at 480p — they really do differ", () => {
|
|
24
|
+
expect(resolveOutputCanvas("seedance-2-5", "480p", "16:9")).toEqual([854, 480])
|
|
25
|
+
expect(resolveOutputCanvas("seedance-2-fast", "480p", "16:9")).toEqual([864, 496])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("answers undefined for anything never measured", () => {
|
|
29
|
+
expect(resolveOutputCanvas("seedance-2-5", "4k", "9:16")).toBeUndefined()
|
|
30
|
+
expect(resolveOutputCanvas("kling-3.0", "720p", "16:9")).toBeUndefined()
|
|
31
|
+
expect(resolveOutputCanvas(undefined, "720p", "9:16")).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("stores only even, positive dimensions", () => {
|
|
35
|
+
for (const { provider, resolution, aspect, canvas } of measuredCanvasCombinations()) {
|
|
36
|
+
const where = `${provider} ${resolution} ${aspect}`
|
|
37
|
+
expect(canvas[0] % 2, where).toBe(0)
|
|
38
|
+
expect(canvas[1] % 2, where).toBe(0)
|
|
39
|
+
expect(canvas[0] > 0 && canvas[1] > 0, where).toBe(true)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("does not claim a canvas whose ratio is wildly off the label", () => {
|
|
44
|
+
// H3's 768P 9:16 really is 0.5714 — a 1.6% lie we keep on purpose. Anything
|
|
45
|
+
// further out is a typo, not a provider quirk.
|
|
46
|
+
for (const { provider, resolution, aspect, canvas } of measuredCanvasCombinations()) {
|
|
47
|
+
const label = parseAspectToken(aspect)
|
|
48
|
+
if (label === undefined) continue
|
|
49
|
+
const actual = canvas[0] / canvas[1]
|
|
50
|
+
expect(Math.abs(actual - label) / label, `${provider} ${resolution} ${aspect}`).toBeLessThan(0.06)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("aspect resolution", () => {
|
|
56
|
+
it("uses an explicit ratio as-is", () => {
|
|
57
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: "16:9", ...GPT_1K })).toBe("16:9")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("snaps adaptive and Auto to the model's nearest listed ratio", () => {
|
|
61
|
+
// 940x1672 = 0.5622, which is 9:16 to within 0.05%.
|
|
62
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: "adaptive", ...GPT_1K })).toBe("9:16")
|
|
63
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: "Auto", ...GPT_1K })).toBe("9:16")
|
|
64
|
+
expect(resolveFrameFitAspect({ provider: "seedance-2-5", requestedAspect: undefined, sourceWidth: 1920, sourceHeight: 1080 })).toBe("16:9")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("gives up when the model declares no ratios", () => {
|
|
68
|
+
expect(resolveFrameFitAspect({ provider: "not-a-model", requestedAspect: "adaptive", ...GPT_1K })).toBeUndefined()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("computeFrameFitPlan", () => {
|
|
73
|
+
it("resizes the 1K image to the measured canvas — the case that fixed the snap", () => {
|
|
74
|
+
const plan = computeFrameFitPlan({
|
|
75
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "720p", aspect: "9:16", ...GPT_1K,
|
|
76
|
+
})
|
|
77
|
+
expect(plan).toEqual({ width: 720, height: 1280, reason: "resolution" })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("targets H3's real 768x1344 canvas rather than a true 9:16", () => {
|
|
81
|
+
const plan = computeFrameFitPlan({
|
|
82
|
+
fit: "resolution", provider: "minimax-h3", resolution: "768P", aspect: "9:16", ...GPT_1K,
|
|
83
|
+
})
|
|
84
|
+
expect(plan?.width).toBe(768)
|
|
85
|
+
expect(plan?.height).toBe(1344)
|
|
86
|
+
expect(plan?.crop).toBeUndefined() // 1.6% gap is inside the tolerance → stretch
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("does nothing when the frame is already the canvas", () => {
|
|
90
|
+
expect(computeFrameFitPlan({
|
|
91
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "720p", aspect: "9:16",
|
|
92
|
+
sourceWidth: 720, sourceHeight: 1280,
|
|
93
|
+
})).toBeNull()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("does nothing in original mode, whatever the size", () => {
|
|
97
|
+
expect(computeFrameFitPlan({
|
|
98
|
+
fit: "original", provider: "seedance-2-5", resolution: "720p", aspect: "9:16", ...GPT_1K,
|
|
99
|
+
})).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("degrades resolution → ratio when the combination was never measured", () => {
|
|
103
|
+
const plan = computeFrameFitPlan({
|
|
104
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "4k", aspect: "16:9",
|
|
105
|
+
sourceWidth: 1000, sourceHeight: 1000,
|
|
106
|
+
})
|
|
107
|
+
// No 4k canvas on file, so it falls back to the minimal change that makes 16:9.
|
|
108
|
+
expect(plan?.reason).toBe("ratio")
|
|
109
|
+
expect(plan!.width / plan!.height).toBeCloseTo(16 / 9, 2)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("degrades to nothing when neither a canvas nor an aspect can be resolved", () => {
|
|
113
|
+
expect(computeFrameFitPlan({
|
|
114
|
+
fit: "resolution", provider: "not-a-model", resolution: "720p", aspect: undefined, ...GPT_1K,
|
|
115
|
+
})).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("stretches inside the tolerance and crops outside it", () => {
|
|
119
|
+
// 4% off 9:16 — inside 5%, so a plain stretch, no crop.
|
|
120
|
+
const inside = computeFrameFitPlan({
|
|
121
|
+
fit: "ratio", provider: "seedance-2-5", resolution: "720p", aspect: "9:16",
|
|
122
|
+
sourceWidth: 1000, sourceHeight: 1710,
|
|
123
|
+
})
|
|
124
|
+
expect(Math.abs(1000 / 1710 - 9 / 16) / (9 / 16)).toBeLessThan(FRAME_FIT_STRETCH_TOLERANCE)
|
|
125
|
+
expect(inside?.crop).toBeUndefined()
|
|
126
|
+
|
|
127
|
+
// A square photo into 9:16 is a 78% gap — crop first, never squash.
|
|
128
|
+
const outside = computeFrameFitPlan({
|
|
129
|
+
fit: "resolution", provider: "seedance-2-5", resolution: "720p", aspect: "9:16",
|
|
130
|
+
sourceWidth: 1024, sourceHeight: 1024,
|
|
131
|
+
})
|
|
132
|
+
expect(outside).toMatchObject({ width: 720, height: 1280, reason: "resolution" })
|
|
133
|
+
expect(outside?.crop).toEqual({ left: 224, top: 0, width: 576, height: 1024 })
|
|
134
|
+
expect(outside!.crop!.width / outside!.crop!.height).toBeCloseTo(9 / 16, 3)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("produces even dimensions (yuv420p rejects odd ones)", () => {
|
|
138
|
+
const plan = computeFrameFitPlan({
|
|
139
|
+
fit: "ratio", provider: "seedance-2-5", resolution: "720p", aspect: "16:9",
|
|
140
|
+
sourceWidth: 1001, sourceHeight: 667,
|
|
141
|
+
})
|
|
142
|
+
expect(plan!.width % 2).toBe(0)
|
|
143
|
+
expect(plan!.height % 2).toBe(0)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("geometry helpers", () => {
|
|
148
|
+
it("minimalRatioDimensions keeps the long side and moves the short one", () => {
|
|
149
|
+
// 940 wide is 9:16 at 1671.1 tall, which rounds back to the image's own
|
|
150
|
+
// 1672 — the 1K GPT image really is 9:16 to the nearest even pixel, so
|
|
151
|
+
// "match ratio" alone would leave it untouched (and the snap would stay).
|
|
152
|
+
expect(minimalRatioDimensions(940, 1672, 9 / 16)).toEqual({ width: 940, height: 1672 })
|
|
153
|
+
expect(minimalRatioDimensions(1920, 1000, 16 / 9)).toEqual({ width: 1920, height: 1080 })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("centreCropToAspect drops the overhang evenly", () => {
|
|
157
|
+
expect(centreCropToAspect(1024, 1024, 9 / 16)).toEqual({ left: 224, top: 0, width: 576, height: 1024 })
|
|
158
|
+
expect(centreCropToAspect(1000, 1000, 16 / 9)).toEqual({ left: 0, top: 219, width: 1000, height: 562 })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("parseAspectToken reads the tokens the catalog uses", () => {
|
|
162
|
+
expect(parseAspectToken("16:9")).toBeCloseTo(16 / 9, 6)
|
|
163
|
+
expect(parseAspectToken("9:16")).toBeCloseTo(9 / 16, 6)
|
|
164
|
+
expect(parseAspectToken("adaptive")).toBeUndefined()
|
|
165
|
+
expect(parseAspectToken(undefined)).toBeUndefined()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe("frame delivery", () => {
|
|
170
|
+
it("sends the Seedance 2.0 family as references and everything else as frames", () => {
|
|
171
|
+
const ref = { requested: "auto" as const, supportsReferenceImages: true }
|
|
172
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-fast", ...ref })).toBe("reference")
|
|
173
|
+
expect(resolveFrameDelivery({ provider: "seedance-2", ...ref })).toBe("reference")
|
|
174
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-mini", ...ref })).toBe("reference")
|
|
175
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-5", ...ref })).toBe("frame")
|
|
176
|
+
expect(resolveFrameDelivery({ provider: "wan-3", ...ref })).toBe("frame")
|
|
177
|
+
expect(resolveFrameDelivery({ provider: "veo3.1", ...ref })).toBe("frame")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("honours an explicit choice", () => {
|
|
181
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-fast", requested: "frame", supportsReferenceImages: true })).toBe("frame")
|
|
182
|
+
expect(resolveFrameDelivery({ provider: "veo3.1", requested: "reference", supportsReferenceImages: true })).toBe("reference")
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("never asks for reference delivery on a model that takes no references", () => {
|
|
186
|
+
expect(resolveFrameDelivery({ provider: "seedance-2-fast", requested: "auto", supportsReferenceImages: false })).toBe("frame")
|
|
187
|
+
expect(resolveFrameDelivery({ provider: "wan-i2v", requested: "reference", supportsReferenceImages: false })).toBe("frame")
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CharacterMotionMetadata } from "./character-motion-metadata.js"
|
|
1
2
|
/**
|
|
2
3
|
* Tag-free, policy-free wire shape for the `GET /v1/catalogs` projection — the
|
|
3
4
|
* server-driven, pack-composed catalog view thin clients render their own
|
|
@@ -21,6 +22,8 @@ export interface ProjectedCatalogOption {
|
|
|
21
22
|
*/
|
|
22
23
|
term?: string
|
|
23
24
|
icon?: string
|
|
25
|
+
/** Authored Character Motion prerequisites and sequence state. Missing means unknown. */
|
|
26
|
+
motion?: CharacterMotionMetadata
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface ProjectedCatalogDimension {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Structural public catalog API metadata. Authored values remain in @nodaro/prompts. */
|
|
2
|
+
export interface CharacterMotionMetadata {
|
|
3
|
+
/** Search terms, including previous display names. IDs remain stable. */
|
|
4
|
+
readonly aliases?: readonly string[]
|
|
5
|
+
/** Hidden from new choices; saved workflows still resolve this entry. */
|
|
6
|
+
readonly deprecated?: true
|
|
7
|
+
readonly replacementId?: string
|
|
8
|
+
/** Authored prerequisites; omission is unknown, never a compatibility claim. */
|
|
9
|
+
readonly requires?: readonly string[]
|
|
10
|
+
readonly startPose?: "standing" | "seated" | "floor" | "any"
|
|
11
|
+
readonly endPose?: "standing" | "seated" | "floor" | "any"
|
|
12
|
+
readonly endVisibility?: "in-frame" | "out-of-frame"
|
|
13
|
+
readonly handsAfter?: "free" | "occupied" | "holding-partner"
|
|
14
|
+
readonly needsFreeHands?: true
|
|
15
|
+
readonly kind?: "single" | "compound"
|
|
16
|
+
readonly fixedPace?: true
|
|
17
|
+
/** Non-human/dependent recipient substituted through the Partner handle. */
|
|
18
|
+
readonly counterpart?: string
|
|
19
|
+
}
|