gradio-workflowcanvas 0.0.1__tar.gz
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.
- gradio_workflowcanvas-0.0.1/.gitignore +12 -0
- gradio_workflowcanvas-0.0.1/PKG-INFO +39 -0
- gradio_workflowcanvas-0.0.1/README.md +11 -0
- gradio_workflowcanvas-0.0.1/WORKFLOW_SPEC.md +450 -0
- gradio_workflowcanvas-0.0.1/app.py +133 -0
- gradio_workflowcanvas-0.0.1/backend/gradio_workflowcanvas/__init__.py +4 -0
- gradio_workflowcanvas-0.0.1/backend/gradio_workflowcanvas/workflowcanvas.py +380 -0
- gradio_workflowcanvas-0.0.1/demo/__init__.py +0 -0
- gradio_workflowcanvas-0.0.1/demo/app.py +120 -0
- gradio_workflowcanvas-0.0.1/demo/css.css +157 -0
- gradio_workflowcanvas-0.0.1/frontend/Index.svelte +22 -0
- gradio_workflowcanvas-0.0.1/frontend/gradio.config.js +9 -0
- gradio_workflowcanvas-0.0.1/frontend/package-lock.json +2753 -0
- gradio_workflowcanvas-0.0.1/frontend/package.json +34 -0
- gradio_workflowcanvas-0.0.1/frontend/tsconfig.json +14 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/WorkflowCanvas.svelte +1690 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/WorkflowEdges.svelte +212 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/WorkflowNode.svelte +1209 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/WorkflowSidebar.svelte +858 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/node-library.ts +256 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/space-api.ts +180 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/workflow-executor.ts +230 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/workflow-store.ts +153 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/workflow-to-space.ts +236 -0
- gradio_workflowcanvas-0.0.1/frontend/workflow/workflow-types.ts +99 -0
- gradio_workflowcanvas-0.0.1/pyproject.toml +50 -0
- gradio_workflowcanvas-0.0.1/requirements.txt +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gradio-workflowcanvas
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Visual workflow builder for connecting Hugging Face Spaces into AI pipelines
|
|
5
|
+
Project-URL: Repository, https://github.com/gradio-app/workflow-builder
|
|
6
|
+
Project-URL: Space, https://huggingface.co/spaces/hmb/workflow
|
|
7
|
+
Project-URL: Documentation, https://github.com/gradio-app/workflow-builder/blob/main/WORKFLOW_SPEC.md
|
|
8
|
+
Author-email: Gradio Team <hello@gradio.app>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: gradio-custom-component,gradio-template-HTML
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Requires-Dist: gradio<7.0,>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == 'dev'
|
|
26
|
+
Requires-Dist: twine; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
title: Workflow Builder
|
|
31
|
+
app_file: app.py
|
|
32
|
+
sdk: gradio
|
|
33
|
+
sdk_version: 6.12.0
|
|
34
|
+
hf_oauth: true
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Workflow Builder
|
|
38
|
+
|
|
39
|
+
Build and run AI pipelines visually by connecting Hugging Face Spaces together.
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# Workflow Builder Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
**Workflow Builder** is a visual, no-code platform for building AI pipelines by connecting Hugging Face Spaces together. Users design workflows by:
|
|
6
|
+
1. Creating input/output nodes
|
|
7
|
+
2. Dragging Spaces onto the canvas
|
|
8
|
+
3. Wiring ports between nodes based on type compatibility
|
|
9
|
+
4. Running the workflow end-to-end
|
|
10
|
+
5. Exporting workflows as JSON for sharing
|
|
11
|
+
|
|
12
|
+
**Key Principle**: Workflows are first-class artifacts (JSON files). They're separate from any UI/deployment. A single workflow can power multiple vibenodes or integrations.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
### Frontend (TypeScript/Svelte)
|
|
19
|
+
- **WorkflowCanvas.svelte** (1690 lines) — main UI, toolbar, modals, pan/zoom, keyboard shortcuts
|
|
20
|
+
- **WorkflowNode.svelte** (1209 lines) — node rendering, inline widgets, ports, drag-and-drop
|
|
21
|
+
- **WorkflowEdges.svelte** (212 lines) — edge rendering with type badges, DOM-based port positioning
|
|
22
|
+
- **WorkflowSidebar.svelte** (858 lines) — component library, Space search, favorites, resizable
|
|
23
|
+
- **workflow-store.ts** (153 lines) — Svelte writable store, node/edge CRUD, undo/redo foundation
|
|
24
|
+
- **workflow-executor.ts** (230 lines) — parallel layer-based execution, client caching, retry logic
|
|
25
|
+
- **space-api.ts** (180 lines) — API introspection via `client.view_api()`, endpoint heuristics
|
|
26
|
+
- **node-library.ts** (256 lines) — preset Spaces (RMBG, FLUX, Whisper, etc.), component templates
|
|
27
|
+
- **workflow-types.ts** (99 lines) — TypeScript interfaces and port color scheme
|
|
28
|
+
- **workflow-to-space.ts** (236 lines) — generates `app.py` using `gradio_client.Client.predict()`
|
|
29
|
+
|
|
30
|
+
### Backend (Python)
|
|
31
|
+
- **app.py** (133 lines) — Gradio block hosting the custom WorkflowCanvas component
|
|
32
|
+
- **workflowcanvas.py** — Python wrapper for the custom component
|
|
33
|
+
- `server_functions` exposed to frontend:
|
|
34
|
+
- `get_token()` — returns HF token from OAuth
|
|
35
|
+
- `call_space(data, token)` — calls remote Space with file handling
|
|
36
|
+
|
|
37
|
+
### Custom Component
|
|
38
|
+
- Built with Gradio's custom component SDK
|
|
39
|
+
- Compiled to wheel file for easy distribution
|
|
40
|
+
- Exposes `server_functions` for frontend→backend RPC
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Data Model
|
|
45
|
+
|
|
46
|
+
### Workflow (JSON)
|
|
47
|
+
```typescript
|
|
48
|
+
interface Workflow {
|
|
49
|
+
version: "1"; // schema version
|
|
50
|
+
name: string; // workflow name (used in export filename)
|
|
51
|
+
nodes: WFNode[];
|
|
52
|
+
edges: WFEdge[];
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Node
|
|
57
|
+
```typescript
|
|
58
|
+
interface WFNode {
|
|
59
|
+
id: string; // unique ID (e.g., "n1", "n2")
|
|
60
|
+
kind: "input" | "transform" | "output";
|
|
61
|
+
label: string; // display name
|
|
62
|
+
source: "local" | "space"; // local = input/output, space = HF Space
|
|
63
|
+
space_id?: string; // "username/space-name" if source=space
|
|
64
|
+
endpoint?: string; // API endpoint (e.g., "/predict", "/image")
|
|
65
|
+
inputs: Port[];
|
|
66
|
+
outputs: Port[];
|
|
67
|
+
x: number; // canvas position (px)
|
|
68
|
+
y: number;
|
|
69
|
+
width: number; // node card size
|
|
70
|
+
height: number;
|
|
71
|
+
data: Record<string, unknown>; // state for input nodes or inline widgets
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Port
|
|
76
|
+
```typescript
|
|
77
|
+
interface Port {
|
|
78
|
+
id: string; // "in_0", "out_1", etc.
|
|
79
|
+
label: string; // display name
|
|
80
|
+
type: PortType; // e.g., "image", "text", "file"
|
|
81
|
+
required?: boolean; // from API introspection
|
|
82
|
+
default_value?: unknown;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Port Types
|
|
87
|
+
```
|
|
88
|
+
"image" | "text" | "audio" | "video" | "number" | "boolean" |
|
|
89
|
+
"file" | "json" | "gallery" | "model3d" | "any"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Edge
|
|
93
|
+
```typescript
|
|
94
|
+
interface WFEdge {
|
|
95
|
+
id: string;
|
|
96
|
+
from_node_id: string;
|
|
97
|
+
from_port_id: string;
|
|
98
|
+
to_node_id: string;
|
|
99
|
+
to_port_id: string;
|
|
100
|
+
type: PortType; // enforced at connection time
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Core Features
|
|
107
|
+
|
|
108
|
+
### 1. Canvas & Interaction
|
|
109
|
+
- **Pan/Zoom**: scroll to pan, pinch/scroll to zoom, `Cmd+0` to reset
|
|
110
|
+
- **Grid**: dot grid, snap-to-grid on node drop
|
|
111
|
+
- **Node Selection**: click to select, `Delete` to remove
|
|
112
|
+
- **Duplication**: `Cmd+D` to duplicate selected node
|
|
113
|
+
- **Rename**: double-click node label to edit
|
|
114
|
+
- **Auto-layout**: `Layout` button distributes nodes vertically
|
|
115
|
+
|
|
116
|
+
### 2. Wiring & Type Safety
|
|
117
|
+
- Ports colored by type (image=cyan, text=purple, audio=orange, etc.)
|
|
118
|
+
- Type checking: can only wire compatible types
|
|
119
|
+
- Auto-create input/output nodes: `+` button on ports
|
|
120
|
+
- Connection badges show port type on hover
|
|
121
|
+
- Edge delete on hover (X icon)
|
|
122
|
+
|
|
123
|
+
### 3. Node Library
|
|
124
|
+
- **Presets**: BRIA RMBG, FLUX.1-schnell, Whisper, Finegrain, UNESCO/nllb, etc.
|
|
125
|
+
- **Space Search**: search HuggingFace Hub, filter to running Spaces
|
|
126
|
+
- **API Introspection**: auto-detect inputs, outputs, required/optional params
|
|
127
|
+
- **Endpoint Selection**: heuristic picks best endpoint (filters event handlers, prioritizes rich outputs)
|
|
128
|
+
- **Custom Spaces**: add by space ID, saved to localStorage
|
|
129
|
+
|
|
130
|
+
### 4. Execution Engine
|
|
131
|
+
- **Topological Sort**: executes nodes in dependency order
|
|
132
|
+
- **Parallel Layers**: nodes at same depth run in parallel
|
|
133
|
+
- **Client Caching**: one `Client` instance per Space to share auth/connection
|
|
134
|
+
- **Retry Logic**: retries stale clients (connection reset)
|
|
135
|
+
- **File Handling**:
|
|
136
|
+
- Blob URLs → uploaded to server → passed to Space
|
|
137
|
+
- Remote file paths → converted to `/file=` URLs
|
|
138
|
+
- Results stored in node output for downstream use
|
|
139
|
+
- **Status Tracking**: idle→running→done/error per node
|
|
140
|
+
- **Abort Signal**: can stop running workflow with `Cmd+.`
|
|
141
|
+
|
|
142
|
+
### 5. API Introspection
|
|
143
|
+
- Uses `client.view_api(return_format="dict")` to inspect Space
|
|
144
|
+
- Extracts `named_endpoints`, parameter names, types, defaults, labels
|
|
145
|
+
- Maps Gradio component types to port types (e.g., `Image` → `image`)
|
|
146
|
+
- Falls back to type field parsing (e.g., "filepath" → `file`)
|
|
147
|
+
- Detects optional params via `parameter_has_default`
|
|
148
|
+
|
|
149
|
+
### 6. Export & Import
|
|
150
|
+
- **Export**: downloads workflow as `workflow_name.workflow.json`
|
|
151
|
+
- **Import**: loads JSON back into canvas
|
|
152
|
+
- **No Server Dependency**: JSON is self-contained, portable
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Execution Flow
|
|
157
|
+
|
|
158
|
+
1. **User clicks Run**
|
|
159
|
+
- Collect all node inputs from canvas state
|
|
160
|
+
- Build dependency graph from edges
|
|
161
|
+
- Topologically sort nodes
|
|
162
|
+
|
|
163
|
+
2. **Execute Layer by Layer**
|
|
164
|
+
- For each layer (depth level):
|
|
165
|
+
- For each node in parallel:
|
|
166
|
+
- Resolve inputs (from upstream outputs or node.data)
|
|
167
|
+
- If transform node: call Space endpoint
|
|
168
|
+
- Update node output data
|
|
169
|
+
- Update UI status
|
|
170
|
+
|
|
171
|
+
3. **File Handling During Execution**
|
|
172
|
+
- Blob URLs (from file upload) → `POST /upload` → get server path
|
|
173
|
+
- Remote file paths → passed to Space as-is
|
|
174
|
+
- Space returns local paths → convert to `/file=/path` URLs
|
|
175
|
+
- Downstream nodes receive readable URLs
|
|
176
|
+
|
|
177
|
+
4. **Error Handling**
|
|
178
|
+
- Classify errors (GPU unavailable, quota exceeded, timeout, etc.)
|
|
179
|
+
- Show contextual toast messages
|
|
180
|
+
- Mark node as error, stop execution
|
|
181
|
+
- User can fix inputs and re-run
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Backend API
|
|
186
|
+
|
|
187
|
+
### Python Backend (app.py)
|
|
188
|
+
|
|
189
|
+
**Server Functions** exposed to frontend:
|
|
190
|
+
|
|
191
|
+
#### `get_token(token: Optional[OAuthToken]) -> str`
|
|
192
|
+
Returns the user's HF token if logged in, empty string otherwise.
|
|
193
|
+
|
|
194
|
+
#### `call_space(data: list, token: Optional[OAuthToken]) -> str`
|
|
195
|
+
Calls a remote Gradio Space.
|
|
196
|
+
|
|
197
|
+
**Parameters:**
|
|
198
|
+
```python
|
|
199
|
+
data = [
|
|
200
|
+
space_id, # "username/space-name"
|
|
201
|
+
endpoint, # "/predict" or "/image" etc.
|
|
202
|
+
args_json, # JSON array of arguments
|
|
203
|
+
manual_token # Optional token from localStorage
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Returns:** JSON-encoded output array
|
|
208
|
+
```json
|
|
209
|
+
[
|
|
210
|
+
{ "path": "/tmp/file", "url": "/file=/tmp/file", "is_file": true },
|
|
211
|
+
{ "data": "text result" },
|
|
212
|
+
...
|
|
213
|
+
]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Error Response:**
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"error": "error message",
|
|
220
|
+
"error_type": "gpu|quota|timeout|not_found|...",
|
|
221
|
+
"suggestion": "user-facing hint",
|
|
222
|
+
"title": "optional error title"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Error Classification:**
|
|
227
|
+
- `gpu`: ZeroGPU unavailable
|
|
228
|
+
- `quota`: Rate limit / GPU quota exceeded
|
|
229
|
+
- `sleeping`: Space paused or sleeping
|
|
230
|
+
- `not_found`: Space deleted or renamed
|
|
231
|
+
- `build_error`: Space has build errors
|
|
232
|
+
- `connection`: Cannot connect to Space (timeout, down, etc.)
|
|
233
|
+
- `unknown`: Other errors
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Workflow JSON Example
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"version": "1",
|
|
242
|
+
"name": "Image Background Removal",
|
|
243
|
+
"nodes": [
|
|
244
|
+
{
|
|
245
|
+
"id": "input_1",
|
|
246
|
+
"kind": "input",
|
|
247
|
+
"label": "Image",
|
|
248
|
+
"source": "local",
|
|
249
|
+
"inputs": [],
|
|
250
|
+
"outputs": [
|
|
251
|
+
{ "id": "out_0", "label": "Image", "type": "image" }
|
|
252
|
+
],
|
|
253
|
+
"x": 100,
|
|
254
|
+
"y": 100,
|
|
255
|
+
"width": 220,
|
|
256
|
+
"height": 160,
|
|
257
|
+
"data": {}
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "transform_1",
|
|
261
|
+
"kind": "transform",
|
|
262
|
+
"label": "Remove Background",
|
|
263
|
+
"source": "space",
|
|
264
|
+
"space_id": "not-lain/background-removal",
|
|
265
|
+
"endpoint": "/image",
|
|
266
|
+
"inputs": [
|
|
267
|
+
{ "id": "in_0", "label": "Image", "type": "image", "required": true }
|
|
268
|
+
],
|
|
269
|
+
"outputs": [
|
|
270
|
+
{ "id": "out_0", "label": "Image", "type": "image" }
|
|
271
|
+
],
|
|
272
|
+
"x": 450,
|
|
273
|
+
"y": 100,
|
|
274
|
+
"width": 200,
|
|
275
|
+
"height": 90,
|
|
276
|
+
"data": {}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"id": "output_1",
|
|
280
|
+
"kind": "output",
|
|
281
|
+
"label": "Result",
|
|
282
|
+
"source": "local",
|
|
283
|
+
"inputs": [
|
|
284
|
+
{ "id": "in_0", "label": "Image", "type": "image" }
|
|
285
|
+
],
|
|
286
|
+
"outputs": [],
|
|
287
|
+
"x": 750,
|
|
288
|
+
"y": 100,
|
|
289
|
+
"width": 220,
|
|
290
|
+
"height": 160,
|
|
291
|
+
"data": {}
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"edges": [
|
|
295
|
+
{
|
|
296
|
+
"id": "edge_1",
|
|
297
|
+
"from_node_id": "input_1",
|
|
298
|
+
"from_port_id": "out_0",
|
|
299
|
+
"to_node_id": "transform_1",
|
|
300
|
+
"to_port_id": "in_0",
|
|
301
|
+
"type": "image"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"id": "edge_2",
|
|
305
|
+
"from_node_id": "transform_1",
|
|
306
|
+
"from_port_id": "out_0",
|
|
307
|
+
"to_node_id": "output_1",
|
|
308
|
+
"to_port_id": "in_0",
|
|
309
|
+
"type": "image"
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## UI/UX
|
|
318
|
+
|
|
319
|
+
### Toolbar
|
|
320
|
+
- **Token Input**: paste HF token for GPU access (saved to localStorage)
|
|
321
|
+
- **Templates**: quick-start workflows (background removal, image generation, etc.)
|
|
322
|
+
- **Export/Import**: download/upload workflow.json
|
|
323
|
+
- **Layout**: auto-arrange nodes
|
|
324
|
+
- **Clear**: delete all nodes and edges
|
|
325
|
+
- **Run**: execute workflow end-to-end
|
|
326
|
+
- **Stop**: abort running workflow
|
|
327
|
+
|
|
328
|
+
### Keyboard Shortcuts
|
|
329
|
+
- `Cmd+R` or Click Run → execute
|
|
330
|
+
- `Cmd+.` → stop running
|
|
331
|
+
- `Cmd+S` → export workflow
|
|
332
|
+
- `Delete` → remove selected node
|
|
333
|
+
- `Cmd+D` → duplicate selected node
|
|
334
|
+
- `Cmd+0` → reset pan/zoom
|
|
335
|
+
- `?` → show shortcuts help
|
|
336
|
+
|
|
337
|
+
### Sidebar
|
|
338
|
+
- **Component Library**: input/output/transform node templates
|
|
339
|
+
- **Spaces**: preset and custom Spaces with categories (IMAGE, AUDIO, TEXT, etc.)
|
|
340
|
+
- **Search**: find Spaces on HF Hub, filter by name
|
|
341
|
+
- **Resizable**: drag divider to expand/collapse
|
|
342
|
+
- **Collapsible**: click chevron to collapse header
|
|
343
|
+
|
|
344
|
+
### Modals
|
|
345
|
+
- **Deploy** (removed): was generating Gradio Space apps, no longer needed
|
|
346
|
+
- **Templates**: quick-start workflows
|
|
347
|
+
- **Shortcuts**: keyboard reference
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Constraints & Design Decisions
|
|
352
|
+
|
|
353
|
+
1. **Workflows ≠ Gradio Apps**: Workflows are JSON artifacts, not UI deployments. Separate concerns.
|
|
354
|
+
|
|
355
|
+
2. **Type System**: Simple 10-type system covers most cases. `any` for unknowns.
|
|
356
|
+
|
|
357
|
+
3. **API Introspection**: Uses Gradio client's `view_api()` for zero-configuration Space discovery.
|
|
358
|
+
|
|
359
|
+
4. **File Handling**:
|
|
360
|
+
- Blob URLs must be uploaded to server before passing to remote Spaces
|
|
361
|
+
- Remote file paths use `/file=` URL scheme for browser access
|
|
362
|
+
- Gradio handles auth/CORS transparently via client
|
|
363
|
+
|
|
364
|
+
5. **Execution**: Topological sort + layer-based parallel execution maximizes throughput without complexity.
|
|
365
|
+
|
|
366
|
+
6. **Storage**:
|
|
367
|
+
- Workflow JSON is portable (no server state)
|
|
368
|
+
- HF token saved to localStorage (client-side only)
|
|
369
|
+
- Node state persisted only in running workflow
|
|
370
|
+
|
|
371
|
+
7. **Error Recovery**:
|
|
372
|
+
- Client caching + retry handles transient connection issues
|
|
373
|
+
- Classified errors provide user-facing guidance
|
|
374
|
+
- No auto-recovery for invalid Space IDs or missing endpoints
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Limitations & Future Work
|
|
379
|
+
|
|
380
|
+
### Current Limitations
|
|
381
|
+
- No loops or conditional branching
|
|
382
|
+
- No multi-Space chains (edges can only wire N→1, 1→N, never M→N split)
|
|
383
|
+
- No node comments or versioning
|
|
384
|
+
- No collaborative editing
|
|
385
|
+
- No workflow versioning or rollback
|
|
386
|
+
- Endpoint selection heuristic can fail on complex Spaces (needs manual override UI)
|
|
387
|
+
|
|
388
|
+
### Future Enhancements
|
|
389
|
+
1. **Workflow Sharing Registry**: platform to publish/discover workflows
|
|
390
|
+
2. **Vibenode Integration**: export workflow → auto-generate vibenode boilerplate
|
|
391
|
+
3. **Branches & Loops**: conditional execution, map operations
|
|
392
|
+
4. **Caching Layer**: skip re-running unchanged nodes
|
|
393
|
+
5. **Scheduling**: trigger workflows on cron schedule
|
|
394
|
+
6. **Monitoring**: runtime logs, performance metrics
|
|
395
|
+
7. **Version Control**: git-like workflow snapshots
|
|
396
|
+
8. **Collaborative**: real-time multi-user editing
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Tech Stack
|
|
401
|
+
|
|
402
|
+
- **Frontend**: Svelte 4, TypeScript, Vite
|
|
403
|
+
- **Canvas**: DOM-based (no canvas library), custom pan/zoom
|
|
404
|
+
- **Backend**: Python, Gradio 6.12+, gradio_client
|
|
405
|
+
- **Component Distribution**: Gradio custom component wheel
|
|
406
|
+
- **Hosting**: HuggingFace Spaces
|
|
407
|
+
- **External API**: HuggingFace Hub (`gradio_client.Client`)
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## File Structure
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
workflow-app/
|
|
415
|
+
├── app.py # Main Gradio app
|
|
416
|
+
├── pyproject.toml # Python package config
|
|
417
|
+
├── backend/
|
|
418
|
+
│ └── gradio_workflowcanvas/ # Custom component backend
|
|
419
|
+
│ └── workflowcanvas.py
|
|
420
|
+
├── frontend/
|
|
421
|
+
│ ├── Index.svelte # Component root
|
|
422
|
+
│ ├── gradio.config.js
|
|
423
|
+
│ ├── package.json
|
|
424
|
+
│ ├── tsconfig.json
|
|
425
|
+
│ └── workflow/ # Main app logic
|
|
426
|
+
│ ├── WorkflowCanvas.svelte # Main UI
|
|
427
|
+
│ ├── WorkflowNode.svelte # Node renderer
|
|
428
|
+
│ ├── WorkflowEdges.svelte # Edge renderer
|
|
429
|
+
│ ├── WorkflowSidebar.svelte # Component library
|
|
430
|
+
│ ├── workflow-types.ts # Type definitions
|
|
431
|
+
│ ├── workflow-store.ts # State management
|
|
432
|
+
│ ├── workflow-executor.ts # Execution engine
|
|
433
|
+
│ ├── space-api.ts # API introspection
|
|
434
|
+
│ ├── node-library.ts # Preset Spaces
|
|
435
|
+
│ └── workflow-to-space.ts # Code generation (removed)
|
|
436
|
+
└── dist/ # Built wheel file
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Glossary
|
|
442
|
+
|
|
443
|
+
- **Node**: A UI card on canvas representing either an input, transformation (Space call), or output
|
|
444
|
+
- **Port**: An input or output pin on a node (e.g., "image in", "text out")
|
|
445
|
+
- **Edge**: A connection between two ports
|
|
446
|
+
- **Workflow**: A complete DAG of nodes and edges, exportable as JSON
|
|
447
|
+
- **Space**: A Gradio app on HuggingFace Hub
|
|
448
|
+
- **Endpoint**: An API function in a Space (e.g., `/predict`, `/image`)
|
|
449
|
+
- **Transform**: A node that calls a Space endpoint (fn node)
|
|
450
|
+
- **Vibenode**: A separate UI layer built on top of workflows (not part of this spec)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# WorkflowCanvas component is installed via pip (published to PyPI)
|
|
2
|
+
# For local development: pip install -e .
|
|
3
|
+
|
|
4
|
+
import gradio as gr
|
|
5
|
+
from gradio.oauth import OAuthToken
|
|
6
|
+
from gradio_workflowcanvas import WorkflowCanvas
|
|
7
|
+
from gradio_client import Client, handle_file
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_token(token: Optional[OAuthToken] = None) -> str:
|
|
14
|
+
if token is None:
|
|
15
|
+
return ""
|
|
16
|
+
return token.token
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def classify_error(e: Exception) -> dict:
|
|
20
|
+
"""Classify an error into a type and suggestion for the frontend."""
|
|
21
|
+
title = getattr(e, 'title', None) or ""
|
|
22
|
+
message = getattr(e, 'message', None) or str(e)
|
|
23
|
+
full = f"{title} {message}".lower()
|
|
24
|
+
|
|
25
|
+
if "zerogpu" in full or "gpu" in full and "worker" in full:
|
|
26
|
+
return {"error_type": "gpu", "suggestion": "GPU unavailable — try again or log in with your HF account"}
|
|
27
|
+
if "quota" in full or "rate limit" in full:
|
|
28
|
+
return {"error_type": "quota", "suggestion": "GPU quota exceeded — log in with your HF account for more compute"}
|
|
29
|
+
if "sleeping" in full or "paused" in full:
|
|
30
|
+
return {"error_type": "sleeping", "suggestion": "Space is sleeping or paused — try again in a minute"}
|
|
31
|
+
if "not found" in full or "404" in full or "repository not found" in full:
|
|
32
|
+
return {"error_type": "not_found", "suggestion": "Space not found — it may have been deleted or renamed"}
|
|
33
|
+
if "build_error" in full or "build error" in full:
|
|
34
|
+
return {"error_type": "build_error", "suggestion": "Space has a build error — contact the Space owner"}
|
|
35
|
+
if "timed out" in full or "timeout" in full or "connection" in full:
|
|
36
|
+
return {"error_type": "connection", "suggestion": "Could not connect to the Space — it may be down"}
|
|
37
|
+
|
|
38
|
+
return {"error_type": "unknown", "suggestion": ""}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def call_space(data, token: Optional[OAuthToken] = None) -> str:
|
|
42
|
+
try:
|
|
43
|
+
space_id = data[0]
|
|
44
|
+
endpoint = data[1] if len(data) > 1 else None
|
|
45
|
+
args_json = data[2] if len(data) > 2 else "[]"
|
|
46
|
+
manual_token = data[3] if len(data) > 3 else None
|
|
47
|
+
|
|
48
|
+
# Use manual token if provided, otherwise fall back to OAuth token
|
|
49
|
+
hf_token = manual_token or (token.token if token else None)
|
|
50
|
+
client = Client(space_id, token=hf_token)
|
|
51
|
+
args = json.loads(args_json)
|
|
52
|
+
|
|
53
|
+
if not endpoint or endpoint == "/predict":
|
|
54
|
+
api_info = client.view_api(return_format="dict")
|
|
55
|
+
named = list(api_info.get("named_endpoints", {}).keys())
|
|
56
|
+
if endpoint and endpoint in named:
|
|
57
|
+
pass
|
|
58
|
+
elif named:
|
|
59
|
+
endpoint = named[0]
|
|
60
|
+
else:
|
|
61
|
+
endpoint = "/predict"
|
|
62
|
+
|
|
63
|
+
processed = []
|
|
64
|
+
for arg in args:
|
|
65
|
+
if arg is None:
|
|
66
|
+
processed.append(None)
|
|
67
|
+
elif isinstance(arg, dict) and ("url" in arg or "path" in arg):
|
|
68
|
+
url = arg.get("url") or arg.get("path", "")
|
|
69
|
+
if url:
|
|
70
|
+
processed.append(handle_file(url))
|
|
71
|
+
else:
|
|
72
|
+
processed.append(None)
|
|
73
|
+
else:
|
|
74
|
+
processed.append(arg)
|
|
75
|
+
|
|
76
|
+
# Strip trailing None args so the Space uses its own defaults.
|
|
77
|
+
# view_api(return_format="dict") returns default=None even when defaults
|
|
78
|
+
# exist, so we'd otherwise pass null for optional args and get
|
|
79
|
+
# "No value provided for required argument".
|
|
80
|
+
while processed and processed[-1] is None:
|
|
81
|
+
processed.pop()
|
|
82
|
+
|
|
83
|
+
result = client.predict(*processed, api_name=endpoint)
|
|
84
|
+
|
|
85
|
+
if not isinstance(result, (list, tuple)):
|
|
86
|
+
result = [result]
|
|
87
|
+
else:
|
|
88
|
+
result = list(result)
|
|
89
|
+
|
|
90
|
+
output = []
|
|
91
|
+
for item in result:
|
|
92
|
+
if isinstance(item, dict) and "url" in item:
|
|
93
|
+
# Already processed by gradio_client, use as-is
|
|
94
|
+
output.append(item)
|
|
95
|
+
elif isinstance(item, str) and os.path.exists(item):
|
|
96
|
+
# Local file path - convert to /file= URL for browser access
|
|
97
|
+
output.append({"path": item, "url": f"/file={item}", "is_file": True})
|
|
98
|
+
elif isinstance(item, dict):
|
|
99
|
+
output.append(item)
|
|
100
|
+
elif isinstance(item, (list, tuple)):
|
|
101
|
+
sub = []
|
|
102
|
+
for s in item:
|
|
103
|
+
if isinstance(s, dict) and "url" in s:
|
|
104
|
+
sub.append(s)
|
|
105
|
+
elif isinstance(s, str) and os.path.exists(s):
|
|
106
|
+
sub.append({"path": s, "url": f"/file={s}", "is_file": True})
|
|
107
|
+
elif isinstance(s, dict):
|
|
108
|
+
sub.append(s)
|
|
109
|
+
else:
|
|
110
|
+
sub.append(s)
|
|
111
|
+
output.append(sub)
|
|
112
|
+
else:
|
|
113
|
+
output.append(item)
|
|
114
|
+
|
|
115
|
+
return json.dumps(output)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
title = getattr(e, 'title', None)
|
|
118
|
+
message = getattr(e, 'message', None) or str(e)
|
|
119
|
+
classified = classify_error(e)
|
|
120
|
+
error_info = {
|
|
121
|
+
"error": message,
|
|
122
|
+
**classified,
|
|
123
|
+
}
|
|
124
|
+
if title:
|
|
125
|
+
error_info["title"] = title
|
|
126
|
+
return json.dumps(error_info)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
with gr.Blocks() as demo:
|
|
130
|
+
canvas = WorkflowCanvas(server_functions=[get_token, call_space])
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
demo.launch(css=".toast-wrap { display: none !important; }")
|