@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
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - Graph Validator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { GraphValidator } from "../validator";
|
|
7
|
+
import type { MediaProcessingGraph, GraphNode } from "../types";
|
|
8
|
+
|
|
9
|
+
describe("GraphValidator", () => {
|
|
10
|
+
const createNode = (id: string, type: string, inputCount: number = 1): GraphNode => ({
|
|
11
|
+
id,
|
|
12
|
+
type,
|
|
13
|
+
version: 1,
|
|
14
|
+
params: {},
|
|
15
|
+
inputs:
|
|
16
|
+
inputCount > 0
|
|
17
|
+
? {
|
|
18
|
+
in: { id: "in", name: "input", type: "Texture" },
|
|
19
|
+
}
|
|
20
|
+
: {},
|
|
21
|
+
outputs: {
|
|
22
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
23
|
+
},
|
|
24
|
+
capabilities: {
|
|
25
|
+
temporal: false,
|
|
26
|
+
stateful: false,
|
|
27
|
+
spatial: false,
|
|
28
|
+
geometry: false,
|
|
29
|
+
inputsCount: inputCount,
|
|
30
|
+
},
|
|
31
|
+
requirements: {
|
|
32
|
+
temporalRadius: 0,
|
|
33
|
+
preferredPrecision: "fp16",
|
|
34
|
+
multipass: false,
|
|
35
|
+
supportsHalfResolution: true,
|
|
36
|
+
},
|
|
37
|
+
lifecycle: "Created",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("validate", () => {
|
|
41
|
+
it("should validate a correct graph", () => {
|
|
42
|
+
const graph: MediaProcessingGraph = {
|
|
43
|
+
id: "test-graph",
|
|
44
|
+
nodes: [createNode("node1", "source", 0), createNode("node2", "effect", 1)],
|
|
45
|
+
edges: [
|
|
46
|
+
{
|
|
47
|
+
fromNodeId: "node1",
|
|
48
|
+
fromPinId: "out",
|
|
49
|
+
toNodeId: "node2",
|
|
50
|
+
toPinId: "in",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const validator = new GraphValidator();
|
|
56
|
+
const result = validator.validate(graph);
|
|
57
|
+
|
|
58
|
+
expect(result.valid).toBe(true);
|
|
59
|
+
expect(result.errors).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should detect cycles", () => {
|
|
63
|
+
const graph: MediaProcessingGraph = {
|
|
64
|
+
id: "test-graph",
|
|
65
|
+
nodes: [createNode("node1", "effect"), createNode("node2", "effect")],
|
|
66
|
+
edges: [
|
|
67
|
+
{
|
|
68
|
+
fromNodeId: "node1",
|
|
69
|
+
fromPinId: "out",
|
|
70
|
+
toNodeId: "node2",
|
|
71
|
+
toPinId: "in",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
fromNodeId: "node2",
|
|
75
|
+
fromPinId: "out",
|
|
76
|
+
toNodeId: "node1",
|
|
77
|
+
toPinId: "in",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const validator = new GraphValidator();
|
|
83
|
+
const result = validator.validate(graph);
|
|
84
|
+
|
|
85
|
+
expect(result.valid).toBe(false);
|
|
86
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
87
|
+
expect(result.errors.some((e) => e.type === "cycle")).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should detect missing source nodes", () => {
|
|
91
|
+
const graph: MediaProcessingGraph = {
|
|
92
|
+
id: "test-graph",
|
|
93
|
+
nodes: [createNode("node2", "effect")],
|
|
94
|
+
edges: [
|
|
95
|
+
{
|
|
96
|
+
fromNodeId: "node1", // This node doesn't exist
|
|
97
|
+
fromPinId: "out",
|
|
98
|
+
toNodeId: "node2",
|
|
99
|
+
toPinId: "in",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const validator = new GraphValidator();
|
|
105
|
+
const result = validator.validate(graph);
|
|
106
|
+
|
|
107
|
+
expect(result.valid).toBe(false);
|
|
108
|
+
expect(result.errors.some((e) => e.type === "invalid-node")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should detect missing target nodes", () => {
|
|
112
|
+
const graph: MediaProcessingGraph = {
|
|
113
|
+
id: "test-graph",
|
|
114
|
+
nodes: [createNode("node1", "source", 0)],
|
|
115
|
+
edges: [
|
|
116
|
+
{
|
|
117
|
+
fromNodeId: "node1",
|
|
118
|
+
fromPinId: "out",
|
|
119
|
+
toNodeId: "node2", // This node doesn't exist
|
|
120
|
+
toPinId: "in",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const validator = new GraphValidator();
|
|
126
|
+
const result = validator.validate(graph);
|
|
127
|
+
|
|
128
|
+
expect(result.valid).toBe(false);
|
|
129
|
+
expect(result.errors.some((e) => e.type === "invalid-node")).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should detect missing output pins", () => {
|
|
133
|
+
const graph: MediaProcessingGraph = {
|
|
134
|
+
id: "test-graph",
|
|
135
|
+
nodes: [createNode("node1", "source", 0), createNode("node2", "effect")],
|
|
136
|
+
edges: [
|
|
137
|
+
{
|
|
138
|
+
fromNodeId: "node1",
|
|
139
|
+
fromPinId: "nonexistent", // This pin doesn't exist
|
|
140
|
+
toNodeId: "node2",
|
|
141
|
+
toPinId: "in",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const validator = new GraphValidator();
|
|
147
|
+
const result = validator.validate(graph);
|
|
148
|
+
|
|
149
|
+
expect(result.valid).toBe(false);
|
|
150
|
+
expect(result.errors.some((e) => e.type === "missing-connection")).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should detect missing input pins", () => {
|
|
154
|
+
const graph: MediaProcessingGraph = {
|
|
155
|
+
id: "test-graph",
|
|
156
|
+
nodes: [createNode("node1", "source", 0), createNode("node2", "effect")],
|
|
157
|
+
edges: [
|
|
158
|
+
{
|
|
159
|
+
fromNodeId: "node1",
|
|
160
|
+
fromPinId: "out",
|
|
161
|
+
toNodeId: "node2",
|
|
162
|
+
toPinId: "nonexistent", // This pin doesn't exist
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const validator = new GraphValidator();
|
|
168
|
+
const result = validator.validate(graph);
|
|
169
|
+
|
|
170
|
+
expect(result.valid).toBe(false);
|
|
171
|
+
expect(result.errors.some((e) => e.type === "missing-connection")).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should detect type mismatches", () => {
|
|
175
|
+
const node1 = createNode("node1", "source", 0);
|
|
176
|
+
const node2 = createNode("node2", "effect");
|
|
177
|
+
|
|
178
|
+
// Change output type to Depth
|
|
179
|
+
node1.outputs.out.type = "Depth";
|
|
180
|
+
// Input expects Texture
|
|
181
|
+
|
|
182
|
+
const graph: MediaProcessingGraph = {
|
|
183
|
+
id: "test-graph",
|
|
184
|
+
nodes: [node1, node2],
|
|
185
|
+
edges: [
|
|
186
|
+
{
|
|
187
|
+
fromNodeId: "node1",
|
|
188
|
+
fromPinId: "out",
|
|
189
|
+
toNodeId: "node2",
|
|
190
|
+
toPinId: "in",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const validator = new GraphValidator();
|
|
196
|
+
const result = validator.validate(graph);
|
|
197
|
+
|
|
198
|
+
expect(result.valid).toBe(false);
|
|
199
|
+
expect(result.errors.some((e) => e.type === "type-mismatch")).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should accept valid empty graph", () => {
|
|
203
|
+
const graph: MediaProcessingGraph = {
|
|
204
|
+
id: "empty-graph",
|
|
205
|
+
nodes: [],
|
|
206
|
+
edges: [],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const validator = new GraphValidator();
|
|
210
|
+
const result = validator.validate(graph);
|
|
211
|
+
|
|
212
|
+
expect(result.valid).toBe(true);
|
|
213
|
+
expect(result.errors).toHaveLength(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should handle graph with isolated nodes", () => {
|
|
217
|
+
const graph: MediaProcessingGraph = {
|
|
218
|
+
id: "test-graph",
|
|
219
|
+
nodes: [
|
|
220
|
+
createNode("node1", "source", 0),
|
|
221
|
+
createNode("node2", "effect"),
|
|
222
|
+
createNode("node3", "source", 0), // Isolated
|
|
223
|
+
],
|
|
224
|
+
edges: [
|
|
225
|
+
{
|
|
226
|
+
fromNodeId: "node1",
|
|
227
|
+
fromPinId: "out",
|
|
228
|
+
toNodeId: "node2",
|
|
229
|
+
toPinId: "in",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const validator = new GraphValidator();
|
|
235
|
+
const result = validator.validate(graph);
|
|
236
|
+
|
|
237
|
+
// Graph structure is valid, but node3 has missing connection
|
|
238
|
+
// This may generate errors for unconnected inputs
|
|
239
|
+
expect(result).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should generate warnings for multipass effects", () => {
|
|
243
|
+
const node = createNode("node1", "blur", 0);
|
|
244
|
+
node.requirements.multipass = true;
|
|
245
|
+
|
|
246
|
+
const graph: MediaProcessingGraph = {
|
|
247
|
+
id: "test-graph",
|
|
248
|
+
nodes: [node],
|
|
249
|
+
edges: [],
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const validator = new GraphValidator();
|
|
253
|
+
const result = validator.validate(graph);
|
|
254
|
+
|
|
255
|
+
expect(result.warnings).toBeDefined();
|
|
256
|
+
expect(result.warnings.some((w) => w.type === "performance")).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should generate warnings for high temporal radius", () => {
|
|
260
|
+
const node = createNode("node1", "temporal-effect", 0);
|
|
261
|
+
node.requirements.temporalRadius = 10;
|
|
262
|
+
|
|
263
|
+
const graph: MediaProcessingGraph = {
|
|
264
|
+
id: "test-graph",
|
|
265
|
+
nodes: [node],
|
|
266
|
+
edges: [],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const validator = new GraphValidator();
|
|
270
|
+
const result = validator.validate(graph);
|
|
271
|
+
|
|
272
|
+
expect(result.warnings).toBeDefined();
|
|
273
|
+
expect(result.warnings.some((w) => w.type === "performance" && w.severity === "high")).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should generate warnings for stateful effects", () => {
|
|
277
|
+
const node = createNode("node1", "stateful-effect", 0);
|
|
278
|
+
node.capabilities.stateful = true;
|
|
279
|
+
|
|
280
|
+
const graph: MediaProcessingGraph = {
|
|
281
|
+
id: "test-graph",
|
|
282
|
+
nodes: [node],
|
|
283
|
+
edges: [],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const validator = new GraphValidator();
|
|
287
|
+
const result = validator.validate(graph);
|
|
288
|
+
|
|
289
|
+
expect(result.warnings).toBeDefined();
|
|
290
|
+
expect(result.warnings.some((w) => w.type === "compatibility")).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should handle complex valid graph", () => {
|
|
294
|
+
const input1 = createNode("input1", "MediaInput", 0);
|
|
295
|
+
const input2 = createNode("input2", "MediaInput", 0);
|
|
296
|
+
const effect1 = createNode("effect1", "blur");
|
|
297
|
+
const effect2 = createNode("effect2", "brightness");
|
|
298
|
+
|
|
299
|
+
// Create blend node with 2 inputs
|
|
300
|
+
const blend: GraphNode = {
|
|
301
|
+
id: "blend",
|
|
302
|
+
type: "blend",
|
|
303
|
+
version: 1,
|
|
304
|
+
params: {},
|
|
305
|
+
inputs: {
|
|
306
|
+
in1: { id: "in1", name: "input1", type: "Texture" },
|
|
307
|
+
in2: { id: "in2", name: "input2", type: "Texture" },
|
|
308
|
+
},
|
|
309
|
+
outputs: {
|
|
310
|
+
out: { id: "out", name: "output", type: "Texture" },
|
|
311
|
+
},
|
|
312
|
+
capabilities: {
|
|
313
|
+
temporal: false,
|
|
314
|
+
stateful: false,
|
|
315
|
+
spatial: true,
|
|
316
|
+
geometry: false,
|
|
317
|
+
inputsCount: 2,
|
|
318
|
+
},
|
|
319
|
+
requirements: {
|
|
320
|
+
temporalRadius: 0,
|
|
321
|
+
preferredPrecision: "fp16",
|
|
322
|
+
multipass: false,
|
|
323
|
+
supportsHalfResolution: true,
|
|
324
|
+
},
|
|
325
|
+
lifecycle: "Created",
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const output = createNode("output", "Output");
|
|
329
|
+
|
|
330
|
+
const graph: MediaProcessingGraph = {
|
|
331
|
+
id: "complex-graph",
|
|
332
|
+
nodes: [input1, input2, effect1, effect2, blend, output],
|
|
333
|
+
edges: [
|
|
334
|
+
{ fromNodeId: "input1", fromPinId: "out", toNodeId: "effect1", toPinId: "in" },
|
|
335
|
+
{ fromNodeId: "input2", fromPinId: "out", toNodeId: "effect2", toPinId: "in" },
|
|
336
|
+
{ fromNodeId: "effect1", fromPinId: "out", toNodeId: "blend", toPinId: "in1" },
|
|
337
|
+
{ fromNodeId: "effect2", fromPinId: "out", toNodeId: "blend", toPinId: "in2" },
|
|
338
|
+
{ fromNodeId: "blend", fromPinId: "out", toNodeId: "output", toPinId: "in" },
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const validator = new GraphValidator();
|
|
343
|
+
const result = validator.validate(graph);
|
|
344
|
+
|
|
345
|
+
expect(result.valid).toBe(true);
|
|
346
|
+
expect(result.errors).toHaveLength(0);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("error reporting", () => {
|
|
351
|
+
it("should provide detailed error information", () => {
|
|
352
|
+
const graph: MediaProcessingGraph = {
|
|
353
|
+
id: "test-graph",
|
|
354
|
+
nodes: [createNode("node1", "effect"), createNode("node2", "effect")],
|
|
355
|
+
edges: [
|
|
356
|
+
{
|
|
357
|
+
fromNodeId: "node1",
|
|
358
|
+
fromPinId: "out",
|
|
359
|
+
toNodeId: "node2",
|
|
360
|
+
toPinId: "in",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
fromNodeId: "node2",
|
|
364
|
+
fromPinId: "out",
|
|
365
|
+
toNodeId: "node1",
|
|
366
|
+
toPinId: "in",
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const validator = new GraphValidator();
|
|
372
|
+
const result = validator.validate(graph);
|
|
373
|
+
|
|
374
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
375
|
+
const error = result.errors[0];
|
|
376
|
+
expect(error.type).toBeDefined();
|
|
377
|
+
expect(error.message).toBeDefined();
|
|
378
|
+
expect(typeof error.message).toBe("string");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/runtime — Graph Builder
|
|
3
|
+
*
|
|
4
|
+
* Constructs media processing graphs from effect definitions.
|
|
5
|
+
* Used by all Labs to build execution graphs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GraphNode, GraphEdge, MediaProcessingGraph, EffectCapabilities, EffectRequirements, GraphDataType } from "./types";
|
|
9
|
+
import { GraphHelper } from "./types";
|
|
10
|
+
|
|
11
|
+
export interface EffectDefinition {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
parameters?: Record<string, any>;
|
|
15
|
+
inputs?: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: GraphDataType;
|
|
19
|
+
}>;
|
|
20
|
+
outputs?: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: GraphDataType;
|
|
24
|
+
}>;
|
|
25
|
+
capabilities?: Partial<EffectCapabilities>;
|
|
26
|
+
requirements?: Partial<EffectRequirements>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MediaInput {
|
|
30
|
+
id: string;
|
|
31
|
+
type: "video" | "image" | "feature-map";
|
|
32
|
+
source: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GraphBuilder - Constructs media processing graphs
|
|
37
|
+
*/
|
|
38
|
+
export class GraphBuilder {
|
|
39
|
+
private graphId: string;
|
|
40
|
+
private graph: MediaProcessingGraph;
|
|
41
|
+
|
|
42
|
+
constructor(graphId: string = "graph-" + Date.now()) {
|
|
43
|
+
this.graphId = graphId;
|
|
44
|
+
this.graph = GraphHelper.create(graphId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build graph from effect definition and inputs
|
|
49
|
+
*/
|
|
50
|
+
build(effect: EffectDefinition, inputs: MediaInput[]): MediaProcessingGraph {
|
|
51
|
+
// Reset graph
|
|
52
|
+
this.graph = GraphHelper.create(this.graphId);
|
|
53
|
+
|
|
54
|
+
// Add input nodes
|
|
55
|
+
const inputNodes = this.createInputNodes(inputs);
|
|
56
|
+
for (const node of inputNodes) {
|
|
57
|
+
this.graph = GraphHelper.withNode(this.graph, node);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add effect node
|
|
61
|
+
const effectNode = this.createEffectNode(effect);
|
|
62
|
+
this.graph = GraphHelper.withNode(this.graph, effectNode);
|
|
63
|
+
|
|
64
|
+
// Connect inputs to effect
|
|
65
|
+
for (let i = 0; i < inputNodes.length; i++) {
|
|
66
|
+
const inputNode = inputNodes[i];
|
|
67
|
+
const inputPin = effect.inputs?.[i] || { id: "input", name: "Input", type: "Texture" };
|
|
68
|
+
|
|
69
|
+
this.graph = GraphHelper.withEdge(this.graph, inputNode.id, "output", effectNode.id, inputPin.id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add output node
|
|
73
|
+
const outputNode = this.createOutputNode();
|
|
74
|
+
this.graph = GraphHelper.withNode(this.graph, outputNode);
|
|
75
|
+
|
|
76
|
+
// Connect effect to output
|
|
77
|
+
const outputPin = effect.outputs?.[0] || { id: "output", name: "Output", type: "Texture" };
|
|
78
|
+
this.graph = GraphHelper.withEdge(this.graph, effectNode.id, outputPin.id, outputNode.id, "input");
|
|
79
|
+
|
|
80
|
+
return this.graph;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build graph from multiple effects (composition)
|
|
85
|
+
*/
|
|
86
|
+
buildComposite(effects: EffectDefinition[], inputs: MediaInput[]): MediaProcessingGraph {
|
|
87
|
+
this.graph = GraphHelper.create(this.graphId);
|
|
88
|
+
|
|
89
|
+
// Add input nodes
|
|
90
|
+
const inputNodes = this.createInputNodes(inputs);
|
|
91
|
+
for (const node of inputNodes) {
|
|
92
|
+
this.graph = GraphHelper.withNode(this.graph, node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Chain effects
|
|
96
|
+
let previousNodes = inputNodes;
|
|
97
|
+
for (const effect of effects) {
|
|
98
|
+
const effectNode = this.createEffectNode(effect);
|
|
99
|
+
this.graph = GraphHelper.withNode(this.graph, effectNode);
|
|
100
|
+
|
|
101
|
+
// Connect previous nodes to this effect
|
|
102
|
+
for (let i = 0; i < previousNodes.length && i < (effect.inputs?.length || 1); i++) {
|
|
103
|
+
const inputPin = effect.inputs?.[i] || { id: "input", name: "Input", type: "Texture" };
|
|
104
|
+
this.graph = GraphHelper.withEdge(this.graph, previousNodes[i].id, "output", effectNode.id, inputPin.id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
previousNodes = [effectNode];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add output node
|
|
111
|
+
const outputNode = this.createOutputNode();
|
|
112
|
+
this.graph = GraphHelper.withNode(this.graph, outputNode);
|
|
113
|
+
|
|
114
|
+
// Connect last effect to output
|
|
115
|
+
if (previousNodes.length > 0) {
|
|
116
|
+
this.graph = GraphHelper.withEdge(this.graph, previousNodes[0].id, "output", outputNode.id, "input");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return this.graph;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the current graph
|
|
124
|
+
*/
|
|
125
|
+
getGraph(): MediaProcessingGraph {
|
|
126
|
+
return this.graph;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create input nodes from media inputs
|
|
131
|
+
*/
|
|
132
|
+
private createInputNodes(inputs: MediaInput[]): GraphNode[] {
|
|
133
|
+
return inputs.map((input, index) => ({
|
|
134
|
+
id: `input-${index}`,
|
|
135
|
+
type: "MediaInput",
|
|
136
|
+
version: 0,
|
|
137
|
+
params: {
|
|
138
|
+
source: input.source,
|
|
139
|
+
mediaType: input.type,
|
|
140
|
+
},
|
|
141
|
+
inputs: {},
|
|
142
|
+
outputs: {
|
|
143
|
+
output: {
|
|
144
|
+
id: "output",
|
|
145
|
+
name: "Output",
|
|
146
|
+
type: "Texture",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
capabilities: {
|
|
150
|
+
temporal: false,
|
|
151
|
+
stateful: false,
|
|
152
|
+
spatial: false,
|
|
153
|
+
geometry: false,
|
|
154
|
+
inputsCount: 0,
|
|
155
|
+
},
|
|
156
|
+
requirements: {
|
|
157
|
+
temporalRadius: 0,
|
|
158
|
+
preferredPrecision: "fp16",
|
|
159
|
+
multipass: false,
|
|
160
|
+
supportsHalfResolution: true,
|
|
161
|
+
},
|
|
162
|
+
lifecycle: "Created",
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create effect node from definition
|
|
168
|
+
*/
|
|
169
|
+
private createEffectNode(effect: EffectDefinition): GraphNode {
|
|
170
|
+
const inputs: Record<string, any> = {};
|
|
171
|
+
if (effect.inputs) {
|
|
172
|
+
for (const input of effect.inputs) {
|
|
173
|
+
inputs[input.id] = {
|
|
174
|
+
id: input.id,
|
|
175
|
+
name: input.name,
|
|
176
|
+
type: input.type,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Default single input
|
|
181
|
+
inputs.input = {
|
|
182
|
+
id: "input",
|
|
183
|
+
name: "Input",
|
|
184
|
+
type: "Texture",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const outputs: Record<string, any> = {};
|
|
189
|
+
if (effect.outputs) {
|
|
190
|
+
for (const output of effect.outputs) {
|
|
191
|
+
outputs[output.id] = {
|
|
192
|
+
id: output.id,
|
|
193
|
+
name: output.name,
|
|
194
|
+
type: output.type,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Default single output
|
|
199
|
+
outputs.output = {
|
|
200
|
+
id: "output",
|
|
201
|
+
name: "Output",
|
|
202
|
+
type: "Texture",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: effect.id,
|
|
208
|
+
type: effect.type,
|
|
209
|
+
version: 0,
|
|
210
|
+
params: effect.parameters || {},
|
|
211
|
+
inputs,
|
|
212
|
+
outputs,
|
|
213
|
+
capabilities: {
|
|
214
|
+
temporal: effect.capabilities?.temporal || false,
|
|
215
|
+
stateful: effect.capabilities?.stateful || false,
|
|
216
|
+
spatial: effect.capabilities?.spatial || true,
|
|
217
|
+
geometry: effect.capabilities?.geometry || false,
|
|
218
|
+
inputsCount: effect.inputs?.length || 1,
|
|
219
|
+
},
|
|
220
|
+
requirements: {
|
|
221
|
+
temporalRadius: effect.requirements?.temporalRadius || 0,
|
|
222
|
+
preferredPrecision: effect.requirements?.preferredPrecision || "fp16",
|
|
223
|
+
multipass: effect.requirements?.multipass || false,
|
|
224
|
+
supportsHalfResolution: effect.requirements?.supportsHalfResolution || true,
|
|
225
|
+
},
|
|
226
|
+
lifecycle: "Created",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create output node
|
|
232
|
+
*/
|
|
233
|
+
private createOutputNode(): GraphNode {
|
|
234
|
+
return {
|
|
235
|
+
id: "output",
|
|
236
|
+
type: "Output",
|
|
237
|
+
version: 0,
|
|
238
|
+
params: {},
|
|
239
|
+
inputs: {
|
|
240
|
+
input: {
|
|
241
|
+
id: "input",
|
|
242
|
+
name: "Input",
|
|
243
|
+
type: "Texture",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
outputs: {},
|
|
247
|
+
capabilities: {
|
|
248
|
+
temporal: false,
|
|
249
|
+
stateful: false,
|
|
250
|
+
spatial: false,
|
|
251
|
+
geometry: false,
|
|
252
|
+
inputsCount: 1,
|
|
253
|
+
},
|
|
254
|
+
requirements: {
|
|
255
|
+
temporalRadius: 0,
|
|
256
|
+
preferredPrecision: "fp16",
|
|
257
|
+
multipass: false,
|
|
258
|
+
supportsHalfResolution: true,
|
|
259
|
+
},
|
|
260
|
+
lifecycle: "Created",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Builder - Media Processing Graph Construction
|
|
3
|
+
*
|
|
4
|
+
* This module handles the construction of media processing graphs from effect definitions.
|
|
5
|
+
* It is responsible for:
|
|
6
|
+
* - Building node trees from effect compositions
|
|
7
|
+
* - Validating graph structure
|
|
8
|
+
* - Resolving dependencies
|
|
9
|
+
* - Capability tracking
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from "./types";
|
|
13
|
+
export * from "./builder";
|
|
14
|
+
export * from "./validator";
|