@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,176 @@
1
+ /**
2
+ * @clypra/runtime — Graph Types
3
+ *
4
+ * Defines immutable node, value, capability, and lifecycle schemas for the Media Processing Graph.
5
+ * These types are shared across all Labs (Video, Transition, Body).
6
+ */
7
+
8
+ export type NodeLifecycleState = "Created" | "Validated" | "Compiled" | "Prepared" | "Executing" | "Completed" | "Disposed";
9
+
10
+ export type GraphDataType = "Texture" | "Depth" | "MotionField" | "BoundingBoxes" | "Mask" | "Pose" | "FaceMesh" | "Particles" | "AudioSpectrum" | "Metadata";
11
+
12
+ export interface GraphValue<T = any> {
13
+ readonly type: GraphDataType;
14
+ readonly payload: T;
15
+ }
16
+
17
+ export interface EffectCapabilities {
18
+ readonly temporal: boolean;
19
+ readonly stateful: boolean;
20
+ readonly spatial: boolean;
21
+ readonly geometry: boolean;
22
+ readonly inputsCount: number;
23
+ }
24
+
25
+ export interface EffectRequirements {
26
+ readonly temporalRadius: number;
27
+ readonly preferredPrecision: "fp8" | "fp16" | "fp32";
28
+ readonly multipass: boolean;
29
+ readonly supportsHalfResolution: boolean;
30
+ }
31
+
32
+ export interface GraphPin {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly type: GraphDataType;
36
+ }
37
+
38
+ export interface GraphNode {
39
+ readonly id: string;
40
+ readonly type: string;
41
+ readonly version: number; // Current parameter version for dirty propagation
42
+ readonly params: Readonly<Record<string, any>>;
43
+ readonly inputs: Readonly<Record<string, GraphPin>>;
44
+ readonly outputs: Readonly<Record<string, GraphPin>>;
45
+ readonly capabilities: EffectCapabilities;
46
+ readonly requirements: EffectRequirements;
47
+ readonly lifecycle: NodeLifecycleState;
48
+ }
49
+
50
+ export interface GraphEdge {
51
+ readonly fromNodeId: string;
52
+ readonly fromPinId: string;
53
+ readonly toNodeId: string;
54
+ readonly toPinId: string;
55
+ }
56
+
57
+ export interface MediaProcessingGraph {
58
+ readonly id: string;
59
+ readonly nodes: readonly GraphNode[];
60
+ readonly edges: readonly GraphEdge[];
61
+ }
62
+
63
+ /**
64
+ * Immutable graph helper functions
65
+ */
66
+ export class GraphHelper {
67
+ static create(id: string): MediaProcessingGraph {
68
+ return { id, nodes: [], edges: [] };
69
+ }
70
+
71
+ static withNode(graph: MediaProcessingGraph, node: GraphNode): MediaProcessingGraph {
72
+ return {
73
+ ...graph,
74
+ nodes: [...graph.nodes.filter((n) => n.id !== node.id), node],
75
+ };
76
+ }
77
+
78
+ static withEdge(graph: MediaProcessingGraph, fromNodeId: string, fromPinId: string, toNodeId: string, toPinId: string): MediaProcessingGraph {
79
+ const edge: GraphEdge = { fromNodeId, fromPinId, toNodeId, toPinId };
80
+ return {
81
+ ...graph,
82
+ edges: [...graph.edges, edge],
83
+ };
84
+ }
85
+
86
+ static getIncomingEdges(graph: MediaProcessingGraph, nodeId: string): GraphEdge[] {
87
+ return graph.edges.filter((e) => e.toNodeId === nodeId);
88
+ }
89
+
90
+ static getOutgoingEdges(graph: MediaProcessingGraph, nodeId: string): GraphEdge[] {
91
+ return graph.edges.filter((e) => e.fromNodeId === nodeId);
92
+ }
93
+
94
+ static findNode(graph: MediaProcessingGraph, nodeId: string): GraphNode | undefined {
95
+ return graph.nodes.find((n) => n.id === nodeId);
96
+ }
97
+
98
+ static getDependencies(graph: MediaProcessingGraph, nodeId: string): GraphNode[] {
99
+ const incoming = this.getIncomingEdges(graph, nodeId);
100
+ return incoming.map((edge) => this.findNode(graph, edge.fromNodeId)).filter((node): node is GraphNode => node !== undefined);
101
+ }
102
+
103
+ static hasCycles(graph: MediaProcessingGraph): boolean {
104
+ const visited = new Set<string>();
105
+ const recursionStack = new Set<string>();
106
+
107
+ const dfs = (nodeId: string): boolean => {
108
+ visited.add(nodeId);
109
+ recursionStack.add(nodeId);
110
+
111
+ const outgoing = this.getOutgoingEdges(graph, nodeId);
112
+ for (const edge of outgoing) {
113
+ if (!visited.has(edge.toNodeId)) {
114
+ if (dfs(edge.toNodeId)) return true;
115
+ } else if (recursionStack.has(edge.toNodeId)) {
116
+ return true; // Cycle detected
117
+ }
118
+ }
119
+
120
+ recursionStack.delete(nodeId);
121
+ return false;
122
+ };
123
+
124
+ for (const node of graph.nodes) {
125
+ if (!visited.has(node.id)) {
126
+ if (dfs(node.id)) return true;
127
+ }
128
+ }
129
+
130
+ return false;
131
+ }
132
+
133
+ static topologicalSort(graph: MediaProcessingGraph): GraphNode[] {
134
+ const inDegree = new Map<string, number>();
135
+ const sorted: GraphNode[] = [];
136
+ const queue: GraphNode[] = [];
137
+
138
+ // Initialize in-degrees
139
+ for (const node of graph.nodes) {
140
+ inDegree.set(node.id, 0);
141
+ }
142
+
143
+ // Calculate in-degrees
144
+ for (const edge of graph.edges) {
145
+ inDegree.set(edge.toNodeId, (inDegree.get(edge.toNodeId) || 0) + 1);
146
+ }
147
+
148
+ // Find nodes with no incoming edges
149
+ for (const node of graph.nodes) {
150
+ if (inDegree.get(node.id) === 0) {
151
+ queue.push(node);
152
+ }
153
+ }
154
+
155
+ // Process nodes
156
+ while (queue.length > 0) {
157
+ const node = queue.shift()!;
158
+ sorted.push(node);
159
+
160
+ const outgoing = this.getOutgoingEdges(graph, node.id);
161
+ for (const edge of outgoing) {
162
+ const targetNode = this.findNode(graph, edge.toNodeId);
163
+ if (!targetNode) continue;
164
+
165
+ const newInDegree = (inDegree.get(edge.toNodeId) || 0) - 1;
166
+ inDegree.set(edge.toNodeId, newInDegree);
167
+
168
+ if (newInDegree === 0) {
169
+ queue.push(targetNode);
170
+ }
171
+ }
172
+ }
173
+
174
+ return sorted;
175
+ }
176
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @clypra/runtime — Graph Validator
3
+ *
4
+ * Validates media processing graphs for correctness.
5
+ * Checks for cycles, type mismatches, missing connections, etc.
6
+ */
7
+
8
+ import type { MediaProcessingGraph, GraphNode, GraphEdge } from "./types";
9
+ import { GraphHelper } from "./types";
10
+
11
+ export interface ValidationError {
12
+ type: "cycle" | "type-mismatch" | "missing-connection" | "invalid-node";
13
+ message: string;
14
+ nodeId?: string;
15
+ edgeFrom?: string;
16
+ edgeTo?: string;
17
+ }
18
+
19
+ export interface GraphValidationResult {
20
+ valid: boolean;
21
+ errors: ValidationError[];
22
+ warnings: ValidationWarning[];
23
+ }
24
+
25
+ export interface ValidationWarning {
26
+ type: "performance" | "compatibility" | "best-practice";
27
+ message: string;
28
+ severity: "low" | "medium" | "high";
29
+ nodeId?: string;
30
+ }
31
+
32
+ /**
33
+ * GraphValidator - Validates graph structure and semantics
34
+ */
35
+ export class GraphValidator {
36
+ /**
37
+ * Validate a media processing graph
38
+ */
39
+ validate(graph: MediaProcessingGraph): GraphValidationResult {
40
+ const errors: ValidationError[] = [];
41
+ const warnings: ValidationWarning[] = [];
42
+
43
+ // Check for cycles
44
+ if (GraphHelper.hasCycles(graph)) {
45
+ errors.push({
46
+ type: "cycle",
47
+ message: "Graph contains cycles",
48
+ });
49
+ }
50
+
51
+ // Validate each node
52
+ for (const node of graph.nodes) {
53
+ const nodeErrors = this.validateNode(graph, node);
54
+ errors.push(...nodeErrors);
55
+
56
+ const nodeWarnings = this.generateWarnings(graph, node);
57
+ warnings.push(...nodeWarnings);
58
+ }
59
+
60
+ // Validate each edge
61
+ for (const edge of graph.edges) {
62
+ const edgeErrors = this.validateEdge(graph, edge);
63
+ errors.push(...edgeErrors);
64
+ }
65
+
66
+ return {
67
+ valid: errors.length === 0,
68
+ errors,
69
+ warnings,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Validate a single node
75
+ */
76
+ private validateNode(graph: MediaProcessingGraph, node: GraphNode): ValidationError[] {
77
+ const errors: ValidationError[] = [];
78
+
79
+ // Check if node type is valid
80
+ if (!node.type) {
81
+ errors.push({
82
+ type: "invalid-node",
83
+ message: `Node ${node.id} has no type`,
84
+ nodeId: node.id,
85
+ });
86
+ }
87
+
88
+ // Check if required inputs are connected
89
+ const inputKeys = Object.keys(node.inputs);
90
+ const incomingEdges = GraphHelper.getIncomingEdges(graph, node.id);
91
+
92
+ for (const inputKey of inputKeys) {
93
+ const hasConnection = incomingEdges.some((edge) => edge.toPinId === inputKey);
94
+ if (!hasConnection && node.type !== "MediaInput") {
95
+ errors.push({
96
+ type: "missing-connection",
97
+ message: `Node ${node.id} input '${inputKey}' is not connected`,
98
+ nodeId: node.id,
99
+ });
100
+ }
101
+ }
102
+
103
+ return errors;
104
+ }
105
+
106
+ /**
107
+ * Validate a single edge
108
+ */
109
+ private validateEdge(graph: MediaProcessingGraph, edge: GraphEdge): ValidationError[] {
110
+ const errors: ValidationError[] = [];
111
+
112
+ const fromNode = GraphHelper.findNode(graph, edge.fromNodeId);
113
+ const toNode = GraphHelper.findNode(graph, edge.toNodeId);
114
+
115
+ if (!fromNode) {
116
+ errors.push({
117
+ type: "invalid-node",
118
+ message: `Edge references non-existent source node: ${edge.fromNodeId}`,
119
+ edgeFrom: edge.fromNodeId,
120
+ edgeTo: edge.toNodeId,
121
+ });
122
+ return errors;
123
+ }
124
+
125
+ if (!toNode) {
126
+ errors.push({
127
+ type: "invalid-node",
128
+ message: `Edge references non-existent target node: ${edge.toNodeId}`,
129
+ edgeFrom: edge.fromNodeId,
130
+ edgeTo: edge.toNodeId,
131
+ });
132
+ return errors;
133
+ }
134
+
135
+ // Check if pins exist
136
+ const fromPin = fromNode.outputs[edge.fromPinId];
137
+ const toPin = toNode.inputs[edge.toPinId];
138
+
139
+ if (!fromPin) {
140
+ errors.push({
141
+ type: "missing-connection",
142
+ message: `Source node ${edge.fromNodeId} has no output pin '${edge.fromPinId}'`,
143
+ edgeFrom: edge.fromNodeId,
144
+ edgeTo: edge.toNodeId,
145
+ });
146
+ }
147
+
148
+ if (!toPin) {
149
+ errors.push({
150
+ type: "missing-connection",
151
+ message: `Target node ${edge.toNodeId} has no input pin '${edge.toPinId}'`,
152
+ edgeFrom: edge.fromNodeId,
153
+ edgeTo: edge.toNodeId,
154
+ });
155
+ }
156
+
157
+ // Check type compatibility
158
+ if (fromPin && toPin && fromPin.type !== toPin.type) {
159
+ errors.push({
160
+ type: "type-mismatch",
161
+ message: `Type mismatch: ${edge.fromNodeId}.${edge.fromPinId} (${fromPin.type}) → ${edge.toNodeId}.${edge.toPinId} (${toPin.type})`,
162
+ edgeFrom: edge.fromNodeId,
163
+ edgeTo: edge.toNodeId,
164
+ });
165
+ }
166
+
167
+ return errors;
168
+ }
169
+
170
+ /**
171
+ * Generate performance and best-practice warnings
172
+ */
173
+ private generateWarnings(graph: MediaProcessingGraph, node: GraphNode): ValidationWarning[] {
174
+ const warnings: ValidationWarning[] = [];
175
+
176
+ // Warn about multipass effects
177
+ if (node.requirements.multipass) {
178
+ warnings.push({
179
+ type: "performance",
180
+ message: `Node ${node.id} requires multiple render passes`,
181
+ severity: "medium",
182
+ nodeId: node.id,
183
+ });
184
+ }
185
+
186
+ // Warn about high temporal radius
187
+ if (node.requirements.temporalRadius > 5) {
188
+ warnings.push({
189
+ type: "performance",
190
+ message: `Node ${node.id} has high temporal radius (${node.requirements.temporalRadius} frames)`,
191
+ severity: "high",
192
+ nodeId: node.id,
193
+ });
194
+ }
195
+
196
+ // Warn about stateful effects
197
+ if (node.capabilities.stateful) {
198
+ warnings.push({
199
+ type: "compatibility",
200
+ message: `Node ${node.id} is stateful - may not work well with random access`,
201
+ severity: "low",
202
+ nodeId: node.id,
203
+ });
204
+ }
205
+
206
+ return warnings;
207
+ }
208
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @clypra/runtime
3
+ *
4
+ * Shared runtime infrastructure for all Clypra Studio Labs.
5
+ * This package contains the core execution engine used by Video Lab, Transition Lab, and Body Lab.
6
+ */
7
+
8
+ // Graph building
9
+ export * from "./graph";
10
+
11
+ // Frame planning
12
+ export * from "./planner";
13
+
14
+ // Pixi rendering backend
15
+ export * from "./pixi";
16
+
17
+ // Resource management
18
+ export * from "./resources";
19
+
20
+ // Runtime validation
21
+ export * from "./validation";
22
+
23
+ // Testing & Publishing (Phase 6)
24
+ export * from "./testing/goldenTests";
25
+ export * from "./testing/benchmarkRunner";
26
+ export { validateEffect, EffectValidator, type ValidationResult, type EffectDefinition } from "./validation/effectValidator";
27
+
28
+ export const RUNTIME_VERSION = "1.0.0";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @clypra/runtime — Pixi Filter Utilities
3
+ *
4
+ * Helper functions for creating and configuring Pixi filters.
5
+ */
6
+
7
+ import * as PIXI from "pixi.js";
8
+ import { AdjustmentFilter } from "pixi-filters";
9
+
10
+ /**
11
+ * Create a filter for a given shader ID
12
+ */
13
+ export function createFilter(shaderId: string, uniforms: Record<string, any> = {}): PIXI.Filter {
14
+ switch (shaderId) {
15
+ case "brightness":
16
+ return new AdjustmentFilter({ brightness: uniforms.brightness ?? 1.0 });
17
+
18
+ case "contrast":
19
+ return new AdjustmentFilter({ contrast: uniforms.contrast ?? 1.0 });
20
+
21
+ case "saturation":
22
+ return new AdjustmentFilter({ saturation: uniforms.saturation ?? 1.0 });
23
+
24
+ case "gaussian-blur":
25
+ case "gaussian-blur-h":
26
+ case "gaussian-blur-v":
27
+ return new PIXI.BlurFilter({
28
+ strength: uniforms.strength ?? 8,
29
+ quality: uniforms.quality ?? 4,
30
+ });
31
+
32
+ case "color-adjustments":
33
+ return new AdjustmentFilter({
34
+ brightness: uniforms.brightness ?? 1.0,
35
+ contrast: uniforms.contrast ?? 1.0,
36
+ saturation: uniforms.saturation ?? 1.0,
37
+ red: uniforms.red ?? 1.0,
38
+ green: uniforms.green ?? 1.0,
39
+ blue: uniforms.blue ?? 1.0,
40
+ });
41
+
42
+ case "copy":
43
+ case "blit":
44
+ // No-op filter
45
+ return new AdjustmentFilter({ brightness: 1, contrast: 1, saturation: 1 });
46
+
47
+ default:
48
+ // Default adjustment filter
49
+ return new AdjustmentFilter({});
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Update filter uniforms
55
+ */
56
+ export function updateFilterUniforms(filter: PIXI.Filter, uniforms: Record<string, any>, shaderId?: string): void {
57
+ if (filter instanceof AdjustmentFilter) {
58
+ if (uniforms.brightness !== undefined) {
59
+ filter.brightness = Number(uniforms.brightness);
60
+ }
61
+ if (uniforms.contrast !== undefined) {
62
+ filter.contrast = Number(uniforms.contrast);
63
+ }
64
+ if (uniforms.saturation !== undefined) {
65
+ filter.saturation = Number(uniforms.saturation);
66
+ }
67
+ if (uniforms.red !== undefined) {
68
+ filter.red = Number(uniforms.red);
69
+ }
70
+ if (uniforms.green !== undefined) {
71
+ filter.green = Number(uniforms.green);
72
+ }
73
+ if (uniforms.blue !== undefined) {
74
+ filter.blue = Number(uniforms.blue);
75
+ }
76
+ } else if (filter instanceof PIXI.BlurFilter) {
77
+ if (uniforms.strength !== undefined) {
78
+ filter.strength = Number(uniforms.strength);
79
+ }
80
+ if (uniforms.quality !== undefined) {
81
+ filter.quality = Number(uniforms.quality);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Normalize color adjustment uniforms
88
+ */
89
+ export function normalizeColorUniforms(uniforms: Record<string, any>): Record<string, number> {
90
+ return {
91
+ brightness: uniforms.brightness !== undefined ? 1.0 + Number(uniforms.brightness) : 1.0,
92
+ contrast: uniforms.contrast !== undefined ? 1.0 + Number(uniforms.contrast) : 1.0,
93
+ saturation: uniforms.saturation !== undefined ? 1.0 + Number(uniforms.saturation) : 1.0,
94
+ red: uniforms.red ?? 1.0,
95
+ green: uniforms.green ?? 1.0,
96
+ blue: uniforms.blue ?? 1.0,
97
+ };
98
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @clypra/runtime — Pixi.js Rendering Backend
3
+ *
4
+ * GPU-accelerated rendering using Pixi.js.
5
+ * Executes frame graphs and manages texture resources.
6
+ */
7
+
8
+ export * from "./types";
9
+ export * from "./renderer";
10
+ export * from "./filters";
11
+ export * from "./texture-pool";