@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.
@@ -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
+ });