@clypra/runtime 1.0.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/.releaserc.json +16 -0
- package/package.json +47 -0
- package/src/__tests__/integration.test.ts +345 -0
- package/src/graph/__tests__/builder.test.ts +204 -0
- package/src/graph/__tests__/validator.test.ts +381 -0
- package/src/graph/builder.ts +263 -0
- package/src/graph/index.ts +14 -0
- package/src/graph/types.ts +176 -0
- package/src/graph/validator.ts +208 -0
- package/src/index.ts +28 -0
- package/src/pixi/filters.ts +98 -0
- package/src/pixi/index.ts +11 -0
- package/src/pixi/renderer.ts +375 -0
- package/src/pixi/texture-pool.ts +159 -0
- package/src/pixi/types.ts +58 -0
- package/src/planner/index.ts +10 -0
- package/src/planner/optimizer.ts +247 -0
- package/src/planner/planner.ts +201 -0
- package/src/planner/types.ts +56 -0
- package/src/resources/cache.ts +166 -0
- package/src/resources/index.ts +9 -0
- package/src/resources/manager.ts +184 -0
- package/src/resources/types.ts +29 -0
- package/src/testing/benchmarkRunner.ts +399 -0
- package/src/testing/goldenTests.ts +390 -0
- package/src/validation/effectValidator.ts +571 -0
- package/src/validation/index.ts +9 -0
- package/src/validation/resource-validator.ts +173 -0
- package/src/validation/shader-validator.ts +154 -0
- package/src/validation/types.ts +31 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +18 -0
package/.releaserc.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
"@semantic-release/npm",
|
|
7
|
+
[
|
|
8
|
+
"@semantic-release/git",
|
|
9
|
+
{
|
|
10
|
+
"assets": ["package.json"],
|
|
11
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"@semantic-release/github"
|
|
15
|
+
]
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clypra/runtime",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared runtime infrastructure for all Clypra Studio Labs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./graph": "./dist/graph/index.js",
|
|
11
|
+
"./planner": "./dist/planner/index.js",
|
|
12
|
+
"./pixi": "./dist/pixi/index.js",
|
|
13
|
+
"./resources": "./dist/resources/index.js",
|
|
14
|
+
"./validation": "./dist/validation/index.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"lint": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"pixi.js": "^8.6.6",
|
|
26
|
+
"pixi-filters": "^6.0.4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.14.0",
|
|
30
|
+
"tsup": "^8.3.5",
|
|
31
|
+
"typescript": "~5.8.2",
|
|
32
|
+
"vitest": "^3.2.4"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/AIEraDev/clypra-studio.git",
|
|
37
|
+
"directory": "packages/runtime"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/AIEraDev/clypra-studio/tree/main/packages/runtime#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/AIEraDev/clypra-studio/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Test - Graph → Planner → Renderer Pipeline
|
|
3
|
+
*
|
|
4
|
+
* This test verifies that the complete pipeline works end-to-end.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { GraphBuilder } from "../graph/builder";
|
|
9
|
+
import { GraphValidator } from "../graph/validator";
|
|
10
|
+
import { FrameGraphPlanner } from "../planner/planner";
|
|
11
|
+
import type { MediaProcessingGraph } from "../graph/types";
|
|
12
|
+
|
|
13
|
+
describe("Integration: Graph → Planner Pipeline", () => {
|
|
14
|
+
it("should build and plan a simple graph", () => {
|
|
15
|
+
// 1. Build a simple graph
|
|
16
|
+
const builder = new GraphBuilder();
|
|
17
|
+
|
|
18
|
+
// Create a simple input → effect → output graph
|
|
19
|
+
const graph: MediaProcessingGraph = {
|
|
20
|
+
id: "test-graph",
|
|
21
|
+
nodes: [
|
|
22
|
+
{
|
|
23
|
+
id: "input",
|
|
24
|
+
type: "source",
|
|
25
|
+
version: 1,
|
|
26
|
+
params: {},
|
|
27
|
+
inputs: {},
|
|
28
|
+
outputs: {
|
|
29
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
30
|
+
},
|
|
31
|
+
capabilities: {
|
|
32
|
+
temporal: false,
|
|
33
|
+
stateful: false,
|
|
34
|
+
spatial: false,
|
|
35
|
+
geometry: false,
|
|
36
|
+
inputsCount: 0,
|
|
37
|
+
},
|
|
38
|
+
requirements: {
|
|
39
|
+
temporalRadius: 0,
|
|
40
|
+
preferredPrecision: "fp16",
|
|
41
|
+
multipass: false,
|
|
42
|
+
supportsHalfResolution: true,
|
|
43
|
+
},
|
|
44
|
+
lifecycle: "Created",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "effect",
|
|
48
|
+
type: "brightness",
|
|
49
|
+
version: 1,
|
|
50
|
+
params: { amount: 1.2 },
|
|
51
|
+
inputs: {
|
|
52
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
53
|
+
},
|
|
54
|
+
outputs: {
|
|
55
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
56
|
+
},
|
|
57
|
+
capabilities: {
|
|
58
|
+
temporal: false,
|
|
59
|
+
stateful: false,
|
|
60
|
+
spatial: true,
|
|
61
|
+
geometry: false,
|
|
62
|
+
inputsCount: 1,
|
|
63
|
+
},
|
|
64
|
+
requirements: {
|
|
65
|
+
temporalRadius: 0,
|
|
66
|
+
preferredPrecision: "fp16",
|
|
67
|
+
multipass: false,
|
|
68
|
+
supportsHalfResolution: true,
|
|
69
|
+
},
|
|
70
|
+
lifecycle: "Created",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "output",
|
|
74
|
+
type: "sink",
|
|
75
|
+
version: 1,
|
|
76
|
+
params: {},
|
|
77
|
+
inputs: {
|
|
78
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
79
|
+
},
|
|
80
|
+
outputs: {},
|
|
81
|
+
capabilities: {
|
|
82
|
+
temporal: false,
|
|
83
|
+
stateful: false,
|
|
84
|
+
spatial: false,
|
|
85
|
+
geometry: false,
|
|
86
|
+
inputsCount: 1,
|
|
87
|
+
},
|
|
88
|
+
requirements: {
|
|
89
|
+
temporalRadius: 0,
|
|
90
|
+
preferredPrecision: "fp16",
|
|
91
|
+
multipass: false,
|
|
92
|
+
supportsHalfResolution: false,
|
|
93
|
+
},
|
|
94
|
+
lifecycle: "Created",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
edges: [
|
|
98
|
+
{
|
|
99
|
+
fromNodeId: "input",
|
|
100
|
+
fromPinId: "out",
|
|
101
|
+
toNodeId: "effect",
|
|
102
|
+
toPinId: "in",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
fromNodeId: "effect",
|
|
106
|
+
fromPinId: "out",
|
|
107
|
+
toNodeId: "output",
|
|
108
|
+
toPinId: "in",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 2. Validate the graph
|
|
114
|
+
const validator = new GraphValidator();
|
|
115
|
+
const validationResult = validator.validate(graph);
|
|
116
|
+
|
|
117
|
+
expect(validationResult.valid).toBe(true);
|
|
118
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
119
|
+
|
|
120
|
+
// 3. Plan a frame
|
|
121
|
+
const planner = new FrameGraphPlanner({
|
|
122
|
+
targetWidth: 1920,
|
|
123
|
+
targetHeight: 1080,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const frameGraph = planner.plan(graph, 0, 0);
|
|
127
|
+
|
|
128
|
+
// Verify frame graph structure
|
|
129
|
+
expect(frameGraph).toBeDefined();
|
|
130
|
+
expect(frameGraph.passes).toBeDefined();
|
|
131
|
+
expect(frameGraph.passes.length).toBeGreaterThan(0);
|
|
132
|
+
expect(frameGraph.resourceRequests).toBeDefined();
|
|
133
|
+
expect(frameGraph.resourceRequests.length).toBeGreaterThan(0);
|
|
134
|
+
|
|
135
|
+
// Verify source and output resources
|
|
136
|
+
const resourceIds = frameGraph.resourceRequests.map((r) => r.id);
|
|
137
|
+
|
|
138
|
+
// Should have source resources
|
|
139
|
+
const hasSource = frameGraph.resourceRequests.some((r) => r.id.includes("source") || r.id.includes("input"));
|
|
140
|
+
expect(hasSource).toBe(true);
|
|
141
|
+
|
|
142
|
+
// Should have at least one resource
|
|
143
|
+
expect(frameGraph.resourceRequests.length).toBeGreaterThan(0);
|
|
144
|
+
|
|
145
|
+
// Verify passes have required properties
|
|
146
|
+
for (const pass of frameGraph.passes) {
|
|
147
|
+
expect(pass.id).toBeDefined();
|
|
148
|
+
expect(pass.shaderId).toBeDefined();
|
|
149
|
+
expect(pass.output).toBeDefined();
|
|
150
|
+
expect(pass.uniforms).toBeDefined();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should handle multi-pass effects", () => {
|
|
155
|
+
const graph: MediaProcessingGraph = {
|
|
156
|
+
id: "multipass-graph",
|
|
157
|
+
nodes: [
|
|
158
|
+
{
|
|
159
|
+
id: "input",
|
|
160
|
+
type: "source",
|
|
161
|
+
version: 1,
|
|
162
|
+
params: {},
|
|
163
|
+
inputs: {},
|
|
164
|
+
outputs: {
|
|
165
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
166
|
+
},
|
|
167
|
+
capabilities: {
|
|
168
|
+
temporal: false,
|
|
169
|
+
stateful: false,
|
|
170
|
+
spatial: false,
|
|
171
|
+
geometry: false,
|
|
172
|
+
inputsCount: 0,
|
|
173
|
+
},
|
|
174
|
+
requirements: {
|
|
175
|
+
temporalRadius: 0,
|
|
176
|
+
preferredPrecision: "fp16",
|
|
177
|
+
multipass: true, // Multi-pass effect
|
|
178
|
+
supportsHalfResolution: true,
|
|
179
|
+
},
|
|
180
|
+
lifecycle: "Created",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "blur",
|
|
184
|
+
type: "blur",
|
|
185
|
+
version: 1,
|
|
186
|
+
params: { radius: 10 },
|
|
187
|
+
inputs: {
|
|
188
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
189
|
+
},
|
|
190
|
+
outputs: {
|
|
191
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
192
|
+
},
|
|
193
|
+
capabilities: {
|
|
194
|
+
temporal: false,
|
|
195
|
+
stateful: false,
|
|
196
|
+
spatial: true,
|
|
197
|
+
geometry: false,
|
|
198
|
+
inputsCount: 1,
|
|
199
|
+
},
|
|
200
|
+
requirements: {
|
|
201
|
+
temporalRadius: 0,
|
|
202
|
+
preferredPrecision: "fp16",
|
|
203
|
+
multipass: true,
|
|
204
|
+
supportsHalfResolution: true,
|
|
205
|
+
},
|
|
206
|
+
lifecycle: "Created",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "output",
|
|
210
|
+
type: "sink",
|
|
211
|
+
version: 1,
|
|
212
|
+
params: {},
|
|
213
|
+
inputs: {
|
|
214
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
215
|
+
},
|
|
216
|
+
outputs: {},
|
|
217
|
+
capabilities: {
|
|
218
|
+
temporal: false,
|
|
219
|
+
stateful: false,
|
|
220
|
+
spatial: false,
|
|
221
|
+
geometry: false,
|
|
222
|
+
inputsCount: 1,
|
|
223
|
+
},
|
|
224
|
+
requirements: {
|
|
225
|
+
temporalRadius: 0,
|
|
226
|
+
preferredPrecision: "fp16",
|
|
227
|
+
multipass: false,
|
|
228
|
+
supportsHalfResolution: false,
|
|
229
|
+
},
|
|
230
|
+
lifecycle: "Created",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
edges: [
|
|
234
|
+
{
|
|
235
|
+
fromNodeId: "input",
|
|
236
|
+
fromPinId: "out",
|
|
237
|
+
toNodeId: "blur",
|
|
238
|
+
toPinId: "in",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
fromNodeId: "blur",
|
|
242
|
+
fromPinId: "out",
|
|
243
|
+
toNodeId: "output",
|
|
244
|
+
toPinId: "in",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const planner = new FrameGraphPlanner({
|
|
250
|
+
targetWidth: 1920,
|
|
251
|
+
targetHeight: 1080,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const frameGraph = planner.plan(graph, 0, 0);
|
|
255
|
+
|
|
256
|
+
// Multi-pass effects should generate multiple passes
|
|
257
|
+
expect(frameGraph.passes.length).toBeGreaterThanOrEqual(2);
|
|
258
|
+
|
|
259
|
+
// Should have transient resources for intermediate results
|
|
260
|
+
const transientResources = frameGraph.resourceRequests.filter((r) => r.transient);
|
|
261
|
+
expect(transientResources.length).toBeGreaterThan(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should detect graph validation errors", () => {
|
|
265
|
+
// Create invalid graph (cycle)
|
|
266
|
+
const invalidGraph: MediaProcessingGraph = {
|
|
267
|
+
id: "invalid-graph",
|
|
268
|
+
nodes: [
|
|
269
|
+
{
|
|
270
|
+
id: "node1",
|
|
271
|
+
type: "effect",
|
|
272
|
+
version: 1,
|
|
273
|
+
params: {},
|
|
274
|
+
inputs: {
|
|
275
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
276
|
+
},
|
|
277
|
+
outputs: {
|
|
278
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
279
|
+
},
|
|
280
|
+
capabilities: {
|
|
281
|
+
temporal: false,
|
|
282
|
+
stateful: false,
|
|
283
|
+
spatial: true,
|
|
284
|
+
geometry: false,
|
|
285
|
+
inputsCount: 1,
|
|
286
|
+
},
|
|
287
|
+
requirements: {
|
|
288
|
+
temporalRadius: 0,
|
|
289
|
+
preferredPrecision: "fp16",
|
|
290
|
+
multipass: false,
|
|
291
|
+
supportsHalfResolution: true,
|
|
292
|
+
},
|
|
293
|
+
lifecycle: "Created",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: "node2",
|
|
297
|
+
type: "effect",
|
|
298
|
+
version: 1,
|
|
299
|
+
params: {},
|
|
300
|
+
inputs: {
|
|
301
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
302
|
+
},
|
|
303
|
+
outputs: {
|
|
304
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
305
|
+
},
|
|
306
|
+
capabilities: {
|
|
307
|
+
temporal: false,
|
|
308
|
+
stateful: false,
|
|
309
|
+
spatial: true,
|
|
310
|
+
geometry: false,
|
|
311
|
+
inputsCount: 1,
|
|
312
|
+
},
|
|
313
|
+
requirements: {
|
|
314
|
+
temporalRadius: 0,
|
|
315
|
+
preferredPrecision: "fp16",
|
|
316
|
+
multipass: false,
|
|
317
|
+
supportsHalfResolution: true,
|
|
318
|
+
},
|
|
319
|
+
lifecycle: "Created",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
edges: [
|
|
323
|
+
{
|
|
324
|
+
fromNodeId: "node1",
|
|
325
|
+
fromPinId: "out",
|
|
326
|
+
toNodeId: "node2",
|
|
327
|
+
toPinId: "in",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
fromNodeId: "node2",
|
|
331
|
+
fromPinId: "out",
|
|
332
|
+
toNodeId: "node1",
|
|
333
|
+
toPinId: "in",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const validator = new GraphValidator();
|
|
339
|
+
const validationResult = validator.validate(invalidGraph);
|
|
340
|
+
|
|
341
|
+
expect(validationResult.valid).toBe(false);
|
|
342
|
+
expect(validationResult.errors.length).toBeGreaterThan(0);
|
|
343
|
+
expect(validationResult.errors.some((e) => e.type === "cycle")).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - Graph Builder
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { GraphBuilder } from "../builder";
|
|
7
|
+
import type { EffectDefinition, MediaInput } from "../builder";
|
|
8
|
+
|
|
9
|
+
describe("GraphBuilder", () => {
|
|
10
|
+
describe("constructor", () => {
|
|
11
|
+
it("should create a new GraphBuilder instance", () => {
|
|
12
|
+
const builder = new GraphBuilder();
|
|
13
|
+
expect(builder).toBeDefined();
|
|
14
|
+
expect(builder).toBeInstanceOf(GraphBuilder);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should accept custom graph ID", () => {
|
|
18
|
+
const builder = new GraphBuilder("custom-graph-id");
|
|
19
|
+
const graph = builder.getGraph();
|
|
20
|
+
expect(graph.id).toBe("custom-graph-id");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should generate graph ID if not provided", () => {
|
|
24
|
+
const builder = new GraphBuilder();
|
|
25
|
+
const graph = builder.getGraph();
|
|
26
|
+
expect(graph.id).toMatch(/^graph-\d+$/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("build", () => {
|
|
31
|
+
it("should build a simple graph with one effect", () => {
|
|
32
|
+
const builder = new GraphBuilder("test-graph");
|
|
33
|
+
|
|
34
|
+
const effect: EffectDefinition = {
|
|
35
|
+
id: "brightness-1",
|
|
36
|
+
type: "brightness",
|
|
37
|
+
parameters: { amount: 1.2 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const inputs: MediaInput[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "input-1",
|
|
43
|
+
type: "video",
|
|
44
|
+
source: "video.mp4",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const graph = builder.build(effect, inputs);
|
|
49
|
+
|
|
50
|
+
expect(graph.nodes).toHaveLength(3); // input, effect, output
|
|
51
|
+
expect(graph.edges).toHaveLength(2); // input->effect, effect->output
|
|
52
|
+
expect(graph.nodes.some((n) => n.type === "brightness")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should create input nodes for all inputs", () => {
|
|
56
|
+
const builder = new GraphBuilder();
|
|
57
|
+
|
|
58
|
+
const effect: EffectDefinition = {
|
|
59
|
+
id: "blend",
|
|
60
|
+
type: "blend",
|
|
61
|
+
inputs: [
|
|
62
|
+
{ id: "input1", name: "Input 1", type: "Texture" },
|
|
63
|
+
{ id: "input2", name: "Input 2", type: "Texture" },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const inputs: MediaInput[] = [
|
|
68
|
+
{ id: "input-1", type: "video", source: "video1.mp4" },
|
|
69
|
+
{ id: "input-2", type: "video", source: "video2.mp4" },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const graph = builder.build(effect, inputs);
|
|
73
|
+
|
|
74
|
+
const inputNodes = graph.nodes.filter((n) => n.type === "MediaInput");
|
|
75
|
+
expect(inputNodes).toHaveLength(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should set effect parameters correctly", () => {
|
|
79
|
+
const builder = new GraphBuilder();
|
|
80
|
+
|
|
81
|
+
const effect: EffectDefinition = {
|
|
82
|
+
id: "blur",
|
|
83
|
+
type: "blur",
|
|
84
|
+
parameters: { radius: 10, quality: "high" },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const inputs: MediaInput[] = [{ id: "input-1", type: "video", source: "video.mp4" }];
|
|
88
|
+
|
|
89
|
+
const graph = builder.build(effect, inputs);
|
|
90
|
+
|
|
91
|
+
const effectNode = graph.nodes.find((n) => n.type === "blur");
|
|
92
|
+
expect(effectNode).toBeDefined();
|
|
93
|
+
expect(effectNode?.params).toEqual({ radius: 10, quality: "high" });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should set effect capabilities", () => {
|
|
97
|
+
const builder = new GraphBuilder();
|
|
98
|
+
|
|
99
|
+
const effect: EffectDefinition = {
|
|
100
|
+
id: "temporal-effect",
|
|
101
|
+
type: "motion-blur",
|
|
102
|
+
capabilities: {
|
|
103
|
+
temporal: true,
|
|
104
|
+
stateful: true,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const inputs: MediaInput[] = [{ id: "input-1", type: "video", source: "video.mp4" }];
|
|
109
|
+
|
|
110
|
+
const graph = builder.build(effect, inputs);
|
|
111
|
+
|
|
112
|
+
const effectNode = graph.nodes.find((n) => n.type === "motion-blur");
|
|
113
|
+
expect(effectNode?.capabilities.temporal).toBe(true);
|
|
114
|
+
expect(effectNode?.capabilities.stateful).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should create output node", () => {
|
|
118
|
+
const builder = new GraphBuilder();
|
|
119
|
+
|
|
120
|
+
const effect: EffectDefinition = {
|
|
121
|
+
id: "effect-1",
|
|
122
|
+
type: "brightness",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const inputs: MediaInput[] = [{ id: "input-1", type: "video", source: "video.mp4" }];
|
|
126
|
+
|
|
127
|
+
const graph = builder.build(effect, inputs);
|
|
128
|
+
|
|
129
|
+
const outputNode = graph.nodes.find((n) => n.type === "Output");
|
|
130
|
+
expect(outputNode).toBeDefined();
|
|
131
|
+
expect(outputNode?.id).toBe("output");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("buildComposite", () => {
|
|
136
|
+
it("should chain multiple effects", () => {
|
|
137
|
+
const builder = new GraphBuilder();
|
|
138
|
+
|
|
139
|
+
const effects: EffectDefinition[] = [
|
|
140
|
+
{ id: "blur", type: "blur", parameters: { radius: 5 } },
|
|
141
|
+
{ id: "brightness", type: "brightness", parameters: { amount: 1.2 } },
|
|
142
|
+
{ id: "contrast", type: "contrast", parameters: { amount: 1.1 } },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const inputs: MediaInput[] = [{ id: "input-1", type: "video", source: "video.mp4" }];
|
|
146
|
+
|
|
147
|
+
const graph = builder.buildComposite(effects, inputs);
|
|
148
|
+
|
|
149
|
+
// Should have: 1 input + 3 effects + 1 output = 5 nodes
|
|
150
|
+
expect(graph.nodes).toHaveLength(5);
|
|
151
|
+
|
|
152
|
+
// Should have: input->blur, blur->brightness, brightness->contrast, contrast->output = 4 edges
|
|
153
|
+
expect(graph.edges).toHaveLength(4);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should preserve effect order", () => {
|
|
157
|
+
const builder = new GraphBuilder();
|
|
158
|
+
|
|
159
|
+
const effects: EffectDefinition[] = [
|
|
160
|
+
{ id: "effect-1", type: "blur" },
|
|
161
|
+
{ id: "effect-2", type: "brightness" },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const inputs: MediaInput[] = [{ id: "input-1", type: "video", source: "video.mp4" }];
|
|
165
|
+
|
|
166
|
+
const graph = builder.buildComposite(effects, inputs);
|
|
167
|
+
|
|
168
|
+
// Find the edges to verify order
|
|
169
|
+
const blurNode = graph.nodes.find((n) => n.id === "effect-1");
|
|
170
|
+
const brightnessNode = graph.nodes.find((n) => n.id === "effect-2");
|
|
171
|
+
|
|
172
|
+
expect(blurNode).toBeDefined();
|
|
173
|
+
expect(brightnessNode).toBeDefined();
|
|
174
|
+
|
|
175
|
+
// Verify blur comes before brightness
|
|
176
|
+
const blurToBrightness = graph.edges.find((e) => e.fromNodeId === "effect-1" && e.toNodeId === "effect-2");
|
|
177
|
+
expect(blurToBrightness).toBeDefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle empty effects array", () => {
|
|
181
|
+
const builder = new GraphBuilder();
|
|
182
|
+
|
|
183
|
+
const effects: EffectDefinition[] = [];
|
|
184
|
+
const inputs: MediaInput[] = [{ id: "input-1", type: "video", source: "video.mp4" }];
|
|
185
|
+
|
|
186
|
+
const graph = builder.buildComposite(effects, inputs);
|
|
187
|
+
|
|
188
|
+
// Should still have input and output
|
|
189
|
+
expect(graph.nodes.length).toBeGreaterThanOrEqual(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("getGraph", () => {
|
|
194
|
+
it("should return current graph", () => {
|
|
195
|
+
const builder = new GraphBuilder("test-graph");
|
|
196
|
+
const graph = builder.getGraph();
|
|
197
|
+
|
|
198
|
+
expect(graph).toBeDefined();
|
|
199
|
+
expect(graph.id).toBe("test-graph");
|
|
200
|
+
expect(graph.nodes).toBeDefined();
|
|
201
|
+
expect(graph.edges).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|