oldman-dev commited on
Commit
8a0f449
Β·
verified Β·
1 Parent(s): 0c31031

Up-to-date with original repo

Browse files
Files changed (7) hide show
  1. .github/CODEOWNERS +6 -0
  2. .gitignore +46 -0
  3. README.md +288 -3
  4. STANNO_IS_NOT.md +155 -0
  5. __init__.py +99 -0
  6. nodes.py +979 -0
  7. requirements.txt +5 -0
.github/CODEOWNERS ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Everyone needs approval from ME to change code
2
+ * @nitroxido
3
+
4
+ # Core trainer changes need extra scrutiny
5
+ /stanno/trainers/ @nitroxido
6
+ /stanno/core/ @nitroxido
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .Python
11
+
12
+ # Markdown files (but keep documentation)
13
+ *.md
14
+ !README.md
15
+ !STANNO_IS_NOT.md
16
+
17
+ # Virtual environments
18
+ venv/
19
+ env/
20
+ ENV/
21
+ .venv
22
+
23
+ # IDEs
24
+ .vscode/
25
+ .idea/
26
+ *.swp
27
+ *.swo
28
+ *~
29
+ .DS_Store
30
+
31
+ # Testing
32
+ .pytest_cache/
33
+ .coverage
34
+ htmlcov/
35
+
36
+ # Cache
37
+ .cache/
38
+ .mypy_cache/
39
+
40
+ # Environment variables
41
+ .env
42
+ .env.local
43
+
44
+ # Temporary files
45
+ *.tmp
46
+ *.log
README.md CHANGED
@@ -1,3 +1,288 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # STANNO β€” Neural Networks That Train Neural Networks
2
+
3
+ A modern, open-source Python library implementing the **Artificial Neurogenesis Network** concept from US Patent 5,852,815 (Thaler, 1998). One network (the trainer) decides how another network (the trainee) should update its weights β€” no backpropagation needed. Multiple STANNOs can be chained into cascade pipelines, and any trained STANNO can be turned into a data scanner that finds matching rows in large datasets.
4
+
5
+ > **Attribution**: This is a faithful, open-source implementation of Thaler's patented design with modern extensions (cascading, data scanning, ComfyUI integration). The original patent has expired. All core concepts are credited to the original patent.
6
+
7
+ ## ⚠️ What STANNO Is (and Isn't)
8
+
9
+ **STANNO is specialized**, not a drop-in replacement for PyTorch.
10
+
11
+ **Good for:**
12
+ - Anomaly detection (reconstruction-based scoring)
13
+ - Online/continual learning (one-sample-at-a-time updates)
14
+ - Interpretable weight modification (see exactly what changes)
15
+ - Multi-stage cascade pipelines (encoder β†’ bottleneck β†’ decoder, end-to-end)
16
+ - Semantic data scanning (find rows in a large dataset that match learned distribution)
17
+ - ComfyUI creative workflows (style transfer via dream mode)
18
+
19
+ **NOT for:**
20
+ - General regression (accuracy ~0.4, use PyTorch instead)
21
+ - Image generation alone (need Stable Diffusion + nodes)
22
+ - High-throughput training (slow NumPy)
23
+
24
+ For details, see [STANNO_IS_NOT.md](./STANNO_IS_NOT.md).
25
+
26
+ **What you can do with this:**
27
+
28
+ **Train networks on your data:**
29
+ ```python
30
+ from stanno import STANNO
31
+ from stanno.config.schema import STANNOConfig
32
+ import numpy as np
33
+
34
+ config = STANNOConfig(layers=[784, 256, 10])
35
+ stanno = STANNO(config)
36
+ stanno.fit(x_train, y_train, epochs=100)
37
+ predictions = stanno.predict(x_test)
38
+ ```
39
+
40
+ **Chain into cascade pipelines:**
41
+ ```python
42
+ from stanno import STANNO, STANNOConfig, CascadeSTANNO
43
+
44
+ # Encoder-decoder autoencoder
45
+ enc = STANNO(STANNOConfig(layers=[768, 256, 64], learning_rate=0.05))
46
+ dec = STANNO(STANNOConfig(layers=[64, 256, 768], learning_rate=0.05))
47
+
48
+ ae = CascadeSTANNO([enc, dec])
49
+ ae.fit(embeddings, embeddings, epochs=200) # end-to-end gradient cascade
50
+
51
+ # Extract compressed representations
52
+ codes = ae.intermediate_output(embeddings, stage=0) # (N, 64)
53
+
54
+ # Freeze the encoder, continue adapting the decoder
55
+ ae.freeze(0)
56
+ ae.fit(new_domain_embeddings, new_domain_embeddings, epochs=100)
57
+ ```
58
+
59
+ **Scan large datasets for matching rows (DSANNO):**
60
+ ```python
61
+ from stanno import STANNO, STANNOConfig, DSANNO
62
+
63
+ # Train on known-good data
64
+ detector = STANNO(STANNOConfig(layers=[64, 128, 64], learning_rate=0.05))
65
+ detector.fit(normal_data, normal_data, epochs=200)
66
+
67
+ scanner = DSANNO(detector, mode="reconstruction")
68
+
69
+ # Auto-calibrate threshold from training distribution
70
+ threshold = scanner.calibrate_threshold(normal_data, percentile=95)
71
+
72
+ # Find matching rows in a large corpus
73
+ result = scanner.scan(large_corpus, threshold=threshold)
74
+ matching = large_corpus[result.matched_indices()]
75
+
76
+ # Or retrieve the top-k best matches
77
+ indices, scores, _ = scanner.top_k(large_corpus, k=20)
78
+
79
+ # Stream huge files without loading all at once
80
+ for batch_result in scanner.scan_stream(file_batches, threshold=threshold):
81
+ process(batch_result.matched_indices())
82
+ ```
83
+
84
+ **Detect when inputs are unusual (anomaly filter):**
85
+ ```python
86
+ from stanno.integration.filter import STANNOFilter
87
+
88
+ # Train on normal data
89
+ stanno.fit(normal_data, normal_data, epochs=50)
90
+
91
+ # Score new input
92
+ score, metadata = stanno_filter.score(new_input)
93
+ # score ranges [0, 1]: low = normal, high = anomaly
94
+ ```
95
+
96
+ **Generate variations via "dream mode":**
97
+ ```python
98
+ # Start with a seed input, add noise, generate a sequence
99
+ dream_sequence = stanno.dream(
100
+ num_steps=64,
101
+ input_seed=seed_vector,
102
+ noise_sigma=0.1 # controls creativity
103
+ )
104
+ ```
105
+
106
+ **Use in ComfyUI workflows (9 nodes):**
107
+ - Load/create STANNO models
108
+ - Train on image batches
109
+ - Score/filter images
110
+ - Inject dream creativity into CLIP conditioning
111
+ - Apply dream output as LoRA-style patches
112
+ - Route images by style match
113
+ - Scan image batches for best matches with auto-calibrated thresholds
114
+ - Build multi-stage cascade autoencoders
115
+
116
+ ## Why use STANNO?
117
+
118
+ - **Interpretable**: You can see exactly what the trainer does to weights. No black-box backprop.
119
+ - **Flexible**: Three trainer types (Fixed, LocalRule, Evolutionary) fit different problems.
120
+ - **Learnable**: The trainer itself can adapt (meta-learning).
121
+ - **Cascadable**: Chain STANNOs into multi-stage pipelines with end-to-end gradient flow across stages.
122
+ - **Scannable**: Turn any trained STANNO into a semantic scanner over large datasets.
123
+ - **No autodiff**: Works with NumPy. No GPU required (but supports PyTorch if you have it).
124
+ - **ComfyUI ready**: Nine custom nodes for image generation workflows.
125
+
126
+ ## Install
127
+
128
+ ```bash
129
+ pip install git+https://github.com/nitroxido/stanno.git
130
+ ```
131
+
132
+ ## Quick examples
133
+
134
+ ### Regression on sin(x)
135
+ ```bash
136
+ python -m stanno train --config examples/sin_regression.json
137
+ python -m stanno predict --config examples/sin_regression.json --input 0.5
138
+ python -m stanno dream --config examples/sin_regression.json
139
+ ```
140
+
141
+ ### Autoencoder on images
142
+ ```python
143
+ from stanno import STANNO
144
+ from stanno.config.schema import STANNOConfig
145
+ import numpy as np
146
+
147
+ # Reshape images to flat vectors (B, H*W*C)
148
+ x = images.reshape(images.shape[0], -1).astype('float32')
149
+
150
+ # Autoencoder: input and output have same size
151
+ config = STANNOConfig(layers=[x.shape[1], 256, x.shape[1]])
152
+ stanno = STANNO(config)
153
+ stanno.fit(x, x, epochs=100, batch_size=32)
154
+
155
+ # Get reconstruction
156
+ x_reconstructed = stanno.predict(x[:10])
157
+ ```
158
+
159
+ ### Online learning (continual)
160
+ ```python
161
+ from stanno.integration.continual import ContinualSTANNO
162
+
163
+ cont = ContinualSTANNO(stanno)
164
+
165
+ for sample, label in data_stream:
166
+ loss = cont.observe(sample, label)
167
+ if cont.steps % 100 == 0:
168
+ test_loss = cont.test_loss(x_test, y_test)
169
+ print(f"Step {cont.steps}: train_loss={loss:.4f}, test_loss={test_loss:.4f}")
170
+ ```
171
+
172
+ ### Anomaly scoring
173
+ ```python
174
+ from stanno.config.schema import FilterConfig
175
+ from stanno.integration.filter import STANNOFilter
176
+
177
+ # Train on normal embeddings
178
+ stanno.fit(normal_embeddings, normal_embeddings, epochs=50)
179
+
180
+ # Create filter
181
+ filt = STANNOFilter(stanno, FilterConfig(anomaly_threshold=0.7))
182
+
183
+ # Score new embedding
184
+ score, info = filt.score(new_embedding)
185
+ print(f"Anomaly score: {score:.3f} (0=normal, 1=anomaly)")
186
+ if info["blocked"]:
187
+ print("Blocked: input is too unusual")
188
+ ```
189
+
190
+ ## How it works
191
+
192
+ **The core idea:**
193
+ - **TraineeNet**: A neural network with weights you want to train.
194
+ - **TrainerNet**: Another network that looks at the TraineeNet's internal state (activations, errors, weights) and computes how to update those weights.
195
+ - **No backprop**: The update formula is explicit, not learned via autodiff.
196
+ - **Cascades**: Multiple TraineeNet+TrainerNet pairs can be chained so that gradient signals flow backward across stage boundaries, enabling end-to-end training of multi-stage pipelines.
197
+ - **Scanning**: Any trained STANNO can be used as a similarity function to scan and rank rows in large datasets by how closely they match the learned distribution.
198
+
199
+ **The three trainer types:**
200
+
201
+ | Type | Mechanism | Best for |
202
+ |------|-----------|----------|
203
+ | **Fixed** | 4-module design (patent 5852815A), cascade-aware | Baseline, reproducibility, understanding the concept |
204
+ | **LocalRule** | Shared MLP per synapse | Adaptive training, interpretability |
205
+ | **Evolutionary** | Evolve per-layer scales (ES) | Unconventional problems, when autodiff fails |
206
+
207
+ ## Technical details
208
+
209
+ - **Backend agnostic**: Uses NumPy by default, but can swap in PyTorch.
210
+ - **Variable architecture**: Networks can be any depth (list of layer sizes).
211
+ - **Configurable feedback**: Dream mode can "repeat" outputs, use a learned "linear" projection, or "zero" them.
212
+ - **Pickle-serializable**: Save/load trained models easily.
213
+
214
+ ## Benchmark
215
+
216
+ On sin(x) regression (512 samples, 100 epochs):
217
+
218
+ ```
219
+ Fixed MSE=0.047
220
+ LocalRule MSE=0.021 (learnable rules = better fit)
221
+ Evolutionary MSE=0.053
222
+ ```
223
+
224
+ ## For ComfyUI users
225
+
226
+ The [comfyui-stanno](https://github.com/[your-username]/comfyui-stanno) custom node package provides nine nodes in the **STANNO** category:
227
+
228
+ | Node | What it does |
229
+ |------|--------------|
230
+ | **STANNOLoad** | Create or load a model (JSON config or .pkl file) |
231
+ | **STANNOTrainImages** | Train on image batches |
232
+ | **STANNOScoreImages** | Filter images by reconstruction error |
233
+ | **STANNODreamCond** | Modify CLIP embeddings with dream mode |
234
+ | **STANNODynamicLoRA** | Apply learned style as LoRA patches |
235
+ | **STANNOCompositeCheck** | Route images to whichever of two STANNOs matches best |
236
+ | **STANNOScan** | DSANNO scanner: auto-calibrated threshold + top-k image retrieval |
237
+ | **STANNOCascadeLoad** | Create or load a multi-stage CascadeSTANNO |
238
+ | **STANNOCascadeTrainImages** | Train a cascade end-to-end on an image batch |
239
+
240
+ Install via ComfyUI-Manager or manually.
241
+
242
+ ## Patent & Attribution
243
+
244
+ **STANNO is an open-source implementation of US Patent 5,852,815** (*Artificial Neurogenesis Network*), filed by Stephen L. Thaler. The patent has expired (US utility patents: 20 years from filing). We fully acknowledge and credit all core architectural concepts to the original patent.
245
+
246
+ **This implementation adds:**
247
+ - Modern Python/NumPy/PyTorch backend
248
+ - CascadeSTANNO (multi-stage gradient cascade)
249
+ - DSANNO (data scanning and semantic search)
250
+ - Three trainer types (Fixed, LocalRule, Evolutionary)
251
+ - ComfyUI integration (9 custom nodes)
252
+ - CLI tools for common tasks
253
+
254
+ See **Citation** below for how to cite the original patent and this implementation.
255
+
256
+ ## Citation
257
+
258
+ If you use STANNO in research, cite the original patent:
259
+
260
+ ```bibtex
261
+ @patent{thaler1998artificial,
262
+ title={Artificial neurogenesis network},
263
+ author={Thaler, Stephen L},
264
+ year={1998},
265
+ number={5852815},
266
+ institution={United States Patent}
267
+ }
268
+ ```
269
+
270
+ And mention this implementation:
271
+ ```bibtex
272
+ @software{stanno2026,
273
+ title={STANNO: Self-Training Artificial Neural Network Object},
274
+ author={Raides J. RodrΓ­guez},
275
+ year={2026},
276
+ url={https://github.com/nitroxido/stanno}
277
+ }
278
+ ```
279
+
280
+ ## Questions?
281
+
282
+ - **Bug report**: Open an issue on GitHub
283
+ - **Question**: Start a discussion
284
+ - **Feature request**: Describe what you want to build
285
+
286
+ ## License
287
+
288
+ MIT
STANNO_IS_NOT.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # STANNO: What It Is, What It Isn't
2
+
3
+ STANNO trains networks using direct weight modification, not backpropagation. It's specialized for specific tasks where this is useful (anomaly detection, online learning, interpretability). It's not a replacement for PyTorch or TensorFlow.
4
+
5
+ ---
6
+
7
+ ## STANNO Works Well For
8
+
9
+ ### 1. Anomaly Detection & Filtering
10
+
11
+ Train on normal data, then score new inputs by reconstruction error. Works reliably in production.
12
+
13
+ ```python
14
+ from stanno.integration.filter import STANNOFilter
15
+
16
+ stanno.fit(normal_embeddings, normal_embeddings, epochs=50)
17
+ filter = STANNOFilter(stanno)
18
+ score = filter.score(new_embedding) # returns [0, 1]: 0=normal, 1=anomaly
19
+ ```
20
+
21
+ ### 2. Online / Continual Learning
22
+
23
+ Update weights one sample at a time with no batch accumulation. Fast and interpretable.
24
+
25
+ ```python
26
+ from stanno.integration.continual import ContinualSTANNO
27
+
28
+ cont = ContinualSTANNO(stanno)
29
+ for x_i, y_i in stream:
30
+ loss = cont.observe(x_i, y_i) # single-sample update
31
+ ```
32
+
33
+ ### 3. Interpretable Weight Modification
34
+
35
+ See exactly what the trainer does at each synapse β€” the weight deltas are explicit, not hidden inside autodiff.
36
+
37
+ ```python
38
+ dW, db = trainer.compute_updates(state) # explicit weight changes
39
+ print(dW) # actual numbers, not gradients
40
+ ```
41
+
42
+ ### 4. Multi-Stage Cascades
43
+
44
+ Chain multiple STANNOs into encoder-decoder pipelines or progressive compression networks, then train end-to-end with gradient flow across stage boundaries.
45
+
46
+ ```python
47
+ from stanno import CascadeSTANNO
48
+
49
+ enc = STANNO(STANNOConfig(layers=[768, 256, 64]))
50
+ dec = STANNO(STANNOConfig(layers=[64, 256, 768]))
51
+
52
+ ae = CascadeSTANNO([enc, dec])
53
+ ae.fit(embeddings, embeddings, epochs=200) # trains both end-to-end
54
+ ```
55
+
56
+ ---
57
+
58
+ ## STANNO Does NOT Work Well For
59
+
60
+ ### Regression (General Function Fitting)
61
+
62
+ STANNO is not optimized for regression. If you train on sin(x), you'll get MAE β‰ˆ 0.4–0.5. A standard neural network with Adam easily reaches MAE < 0.01.
63
+
64
+ **Why?** The fixed 4-module trainer applies the same update formula at every step. This works well for the tasks above, but not for learning arbitrary functions.
65
+
66
+ **Better choice:** Use PyTorch, TensorFlow, or scikit-learn.
67
+
68
+ ### Replacement for PyTorch/TensorFlow
69
+
70
+ STANNO intentionally avoids autodiff. If you need GPU acceleration, backpropagation, or access to a model zoo, use a standard framework.
71
+
72
+ ```python
73
+ # Bad idea
74
+ stanno = STANNO(...) # slow NumPy, no GPU
75
+
76
+ # Good idea
77
+ torch.nn.Sequential(...) # fast, GPU, backprop, pretrained weights
78
+ ```
79
+
80
+ ### Standalone Image Generation
81
+
82
+ Alone, STANNO is just a small neural network. For image workflows, use the ComfyUI nodes which integrate with Stable Diffusion and provide the full pipeline.
83
+
84
+ ```python
85
+ # Incomplete
86
+ stanno = STANNO(STANNOConfig(layers=[768, 512, 768])) # just a network
87
+
88
+ # Complete (in ComfyUI)
89
+ # STANNOLoad β†’ STANNODreamCond β†’ KSampler β†’ STANNOScoreImages
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Training Divergence (Why It Happens, How We Guard Against It)
95
+
96
+ Direct weight modification can diverge if training runs too long without safeguards. The weights keep changing, accumulate errors, and blow up.
97
+
98
+ **How we prevent it:**
99
+ - Divergence detection: Stop if loss > 100
100
+ - Early stopping: Stop if no improvement for N epochs (default: patience=20)
101
+ - Default epochs: 300 (enough to converge without risking divergence)
102
+
103
+ If training stops with a divergence warning, reduce epochs or batch size.
104
+
105
+ ---
106
+
107
+ ## Realistic Performance Expectations
108
+
109
+ | Task | Realistic Performance | Notes |
110
+ |------|-----------------------|-------|
111
+ | Anomaly detection | > 90% accuracy | βœ“ Achievable, used in production |
112
+ | Online learning | < 100 steps to converge | βœ“ Fast adaptation |
113
+ | Cascades (end-to-end) | Stable training, gradient flow | βœ“ Works well |
114
+ | Sin regression (MAE) | β‰ˆ 0.4–0.5 | βœ— Not the right tool β€” use PyTorch |
115
+ | Image reconstruction | Depends on model size | βœ“ Fine-tuning with ComfyUI nodes |
116
+ | General regression | Baseline only | βœ— Not optimized |
117
+
118
+ ---
119
+
120
+ ## When to Use STANNO (Decision Tree)
121
+
122
+ **Do you want to:**
123
+ - Detect anomalies in a stream? β†’ Use STANNO + filter βœ“
124
+ - Learn from one sample at a time? β†’ Use ContinualSTANNO βœ“
125
+ - Train an encoder-decoder pipeline? β†’ Use CascadeSTANNO βœ“
126
+ - Fit sin(x) accurately? β†’ Use PyTorch βœ—
127
+ - Fine-tune a large pretrained model? β†’ Use PyTorch βœ—
128
+ - Generate images from scratch? β†’ Use Stable Diffusion directly βœ—
129
+ - Compose STANNO with image generation? β†’ Use ComfyUI nodes βœ“
130
+
131
+ ---
132
+
133
+ ## FAQ
134
+
135
+ **Q: Why doesn't STANNO fit sin(x) well?**
136
+
137
+ A: It's not designed for regression. The fixed 4-module trainer works great for anomaly detection and online learning, but arbitrary function fitting needs backpropagation or evolution. Use PyTorch for that.
138
+
139
+ **Q: Will longer training improve accuracy?**
140
+
141
+ A: No. Longer training will diverge. Training has built-in early stopping (patience parameter), so it stops when it's done learning. If you increase epochs, you risk overfitting and divergence.
142
+
143
+ **Q: Which trainer should I use: Fixed, LocalRule, or Evolutionary?**
144
+
145
+ A: Start with **Fixed** β€” it's stable and interpretable. **LocalRule** learns per-synapse rules, which can be powerful but also unstable. **Evolutionary** uses evolutionary strategies and is slower but novel. Experiment for your problem.
146
+
147
+ **Q: Is STANNO production-ready?**
148
+
149
+ A: For anomaly detection and online learning: **yes**. For regression or general purpose training: **no**. For ComfyUI image workflows: **yes, use the nodes**.
150
+
151
+ ---
152
+
153
+ ## Bottom Line
154
+
155
+ STANNO is specialized for anomaly detection, online learning, cascading, and ComfyUI workflows. It's not a general-purpose neural network and not a replacement for PyTorch or TensorFlow. Use it where the strengths match your problem.
__init__.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # comfyui-stanno β€” STANNO Custom Nodes for ComfyUI
2
+ #
3
+ # Nodes provided (category "STANNO"):
4
+ # STANNOLoad β€” load or create a STANNO model from disk
5
+ # STANNOTrainImages β€” train STANNO as autoencoder on an image batch
6
+ # STANNOScoreImages β€” score / filter images by reconstruction error
7
+ # STANNODreamCond β€” inject dream-mode creativity into CONDITIONING
8
+ # STANNODynamicLoRA β€” patch MODEL attention weights via STANNO dream output
9
+ # STANNOCompositeCheck β€” route a batch to whichever of two STANNOs it matches best
10
+ # STANNOScan β€” DSANNO scanner: auto-calibrated threshold + top-k retrieval
11
+ # STANNOCascadeLoad β€” load or create a CascadeSTANNO (multi-stage chain)
12
+ # STANNOCascadeTrainImagesβ€” train a CascadeSTANNO end-to-end on an image batch
13
+
14
+ import os as _os
15
+ import sys as _sys
16
+
17
+
18
+ def _ensure_stanno_importable() -> None:
19
+ """
20
+ Make the `stanno` Python package importable.
21
+
22
+ Resolution order:
23
+ 1. Already pip-installed (pip install stanno or pip install -e .) β†’ done.
24
+ 2. Monorepo layout: this __init__.py is at <repo>/comfyui-stanno/__init__.py
25
+ and the stanno Python package lives at <repo>/stanno/.
26
+ β†’ add <repo> to sys.path.
27
+ 3. Neither works β†’ raise ImportError with install instructions.
28
+ """
29
+ try:
30
+ import stanno # noqa: F401
31
+ return
32
+ except ImportError:
33
+ pass
34
+
35
+ # Monorepo detection: look for stanno/ one level above this file's directory.
36
+ _here = _os.path.dirname(_os.path.abspath(__file__))
37
+ _parent = _os.path.dirname(_here)
38
+ if _os.path.isdir(_os.path.join(_parent, "stanno")):
39
+ if _parent not in _sys.path:
40
+ _sys.path.insert(0, _parent)
41
+ try:
42
+ import stanno # noqa: F401
43
+ return
44
+ except ImportError:
45
+ pass
46
+
47
+ raise ImportError(
48
+ "The `stanno` package is not installed and could not be found automatically.\n"
49
+ "Install it with:\n"
50
+ " pip install git+https://github.com/USER/stanno.git\n"
51
+ "Or clone the stanno repo and place this comfyui-stanno/ folder inside it."
52
+ )
53
+
54
+
55
+ _ensure_stanno_importable()
56
+
57
+ from .nodes import ( # noqa: E402
58
+ comfy_entrypoint,
59
+ STANNOLoad,
60
+ STANNOTrainImages,
61
+ STANNOScoreImages,
62
+ STANNODreamCond,
63
+ STANNODynamicLoRA,
64
+ STANNOCompositeCheck,
65
+ STANNOScan,
66
+ STANNOCascadeLoad,
67
+ STANNOCascadeTrainImages,
68
+ )
69
+
70
+ # NODE_CLASS_MAPPINGS: required by older ComfyUI builds; harmless on newer ones.
71
+ NODE_CLASS_MAPPINGS = {
72
+ "STANNOLoad": STANNOLoad,
73
+ "STANNOTrainImages": STANNOTrainImages,
74
+ "STANNOScoreImages": STANNOScoreImages,
75
+ "STANNODreamCond": STANNODreamCond,
76
+ "STANNODynamicLoRA": STANNODynamicLoRA,
77
+ "STANNOCompositeCheck": STANNOCompositeCheck,
78
+ "STANNOScan": STANNOScan,
79
+ "STANNOCascadeLoad": STANNOCascadeLoad,
80
+ "STANNOCascadeTrainImages": STANNOCascadeTrainImages,
81
+ }
82
+
83
+ NODE_DISPLAY_NAME_MAPPINGS = {
84
+ "STANNOLoad": "STANNO Loader",
85
+ "STANNOTrainImages": "STANNO Train from Images",
86
+ "STANNOScoreImages": "STANNO Image Scorer",
87
+ "STANNODreamCond": "STANNO Dream Conditioning",
88
+ "STANNODynamicLoRA": "STANNO Dynamic LoRA",
89
+ "STANNOCompositeCheck": "STANNO Composite Style Checker",
90
+ "STANNOScan": "STANNO Scan (DSANNO)",
91
+ "STANNOCascadeLoad": "STANNO Cascade Loader",
92
+ "STANNOCascadeTrainImages": "STANNO Cascade Train from Images",
93
+ }
94
+
95
+ __all__ = [
96
+ "comfy_entrypoint",
97
+ "NODE_CLASS_MAPPINGS",
98
+ "NODE_DISPLAY_NAME_MAPPINGS",
99
+ ]
nodes.py ADDED
@@ -0,0 +1,979 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ STANNO custom nodes for ComfyUI.
3
+
4
+ Nodes:
5
+ - STANNOLoad Load or create a STANNO model
6
+ - STANNOTrainImages Train STANNO as autoencoder on a batch of images
7
+ - STANNOScoreImages Score and filter images by reconstruction error
8
+ - STANNODreamCond Inject dream-mode creativity into CONDITIONING
9
+ - STANNODynamicLoRA Patch MODEL attention weights using STANNO dream output
10
+ - STANNOCompositeCheck Score images against two STANNOs and route by winner
11
+
12
+ Full integration guide:
13
+ /mnt/juegos/proyectos/especiales/stanno/comfyui-stanno-integration.md
14
+ """
15
+
16
+ from __future__ import annotations
17
+ import os
18
+ import json
19
+ import pickle
20
+ import sys
21
+ import numpy as np
22
+ import torch
23
+
24
+ from comfy_api.latest import ComfyExtension, io
25
+
26
+
27
+ # ─── helpers ────────────────────────────────────────────────────────────────
28
+
29
+ def _flatten_images(image_tensor: torch.Tensor, target_dim: int) -> np.ndarray:
30
+ """
31
+ Resize each image in a ComfyUI IMAGE batch to produce a flat vector of
32
+ exactly `target_dim` floats. IMAGE format: (B, H, W, C) float32 [0, 1].
33
+ """
34
+ import torch.nn.functional as F
35
+ b, h, w, c = image_tensor.shape
36
+ side = max(1, int(((target_dim // c) ** 0.5)))
37
+ x = image_tensor.permute(0, 3, 1, 2) # B C H W
38
+ x = F.interpolate(x, size=(side, side), mode="bilinear", align_corners=False)
39
+ x = x.permute(0, 2, 3, 1).reshape(b, -1) # B (sideΒ²Β·C)
40
+ # Trim or pad to exactly target_dim
41
+ if x.shape[1] > target_dim:
42
+ x = x[:, :target_dim]
43
+ elif x.shape[1] < target_dim:
44
+ pad = torch.zeros(b, target_dim - x.shape[1], device=x.device)
45
+ x = torch.cat([x, pad], dim=1)
46
+ return x.detach().cpu().numpy()
47
+
48
+
49
+ # ─── Node 1: Load / Create STANNO ───────────────────────────────────────────
50
+
51
+ class STANNOLoad(io.ComfyNode):
52
+ """
53
+ Load a saved STANNO model from disk, or create a new untrained one.
54
+
55
+ If `model_path` points to an existing .pkl file it is loaded unchanged.
56
+ Otherwise a new STANNO is created with the given architecture and trainer.
57
+ The returned STANNO object can be passed to any other STANNO node.
58
+ """
59
+
60
+ @classmethod
61
+ def define_schema(cls) -> io.Schema:
62
+ return io.Schema(
63
+ node_id="STANNOLoad",
64
+ display_name="STANNO Loader",
65
+ category="STANNO",
66
+ inputs=[
67
+ io.String.Input(
68
+ "model_path",
69
+ default="stanno_model.pkl",
70
+ multiline=False,
71
+ tooltip="Path to a saved STANNO .pkl file, or a new filename to create.",
72
+ ),
73
+ io.String.Input(
74
+ "layers_json",
75
+ default="[1, 32, 1]",
76
+ multiline=False,
77
+ tooltip=(
78
+ "JSON list of layer sizes. Examples:\n"
79
+ " [1, 32, 1] sin regression (poc)\n"
80
+ " [768, 256, 768] CLIP-embedding autoencoder (SD 1.5)\n"
81
+ " [3072, 512, 3072] 32Γ—32 pixel autoencoder\n"
82
+ " [784, 256, 128, 10] classifier"
83
+ ),
84
+ ),
85
+ io.Combo.Input(
86
+ "trainer_type",
87
+ options=["fixed", "local_rule", "evolutionary"],
88
+ ),
89
+ io.Float.Input(
90
+ "learning_rate",
91
+ default=0.01,
92
+ min=1e-5,
93
+ max=1.0,
94
+ step=0.001,
95
+ display_mode=io.NumberDisplay.number,
96
+ ),
97
+ ],
98
+ outputs=[
99
+ io.Custom.Output("STANNO"),
100
+ io.String.Output("info"),
101
+ ],
102
+ )
103
+
104
+ @classmethod
105
+ def execute(cls, model_path, layers_json, trainer_type, learning_rate) -> io.NodeOutput:
106
+ from stanno.config.schema import STANNOConfig
107
+ from stanno.core.stanno import STANNO
108
+
109
+ if os.path.isfile(model_path):
110
+ with open(model_path, "rb") as f:
111
+ stanno_obj = pickle.load(f)
112
+ info = f"Loaded: {model_path} | layers={stanno_obj.config.layers}"
113
+ else:
114
+ layers = json.loads(layers_json)
115
+ config = STANNOConfig(
116
+ layers=layers,
117
+ trainer_type=trainer_type,
118
+ learning_rate=learning_rate,
119
+ )
120
+ stanno_obj = STANNO(config)
121
+ info = f"Created new STANNO | layers={layers} trainer={trainer_type} lr={learning_rate}"
122
+
123
+ print(f"[STANNO Loader] {info}")
124
+ return io.NodeOutput(stanno_obj, info)
125
+
126
+
127
+ # ─── Node 2: Train on Images ────────────────��────────────────────────────────
128
+
129
+ class STANNOTrainImages(io.ComfyNode):
130
+ """
131
+ Train a STANNO as an autoencoder on a batch of images.
132
+
133
+ Images are resized to match the STANNO's input dimension, normalized to
134
+ [-1, 1], and used as both input and target (autoencoder). After training
135
+ the STANNO 'remembers' the style/distribution of those images.
136
+
137
+ Tip: connect the output STANNO to STANNOScoreImages to filter later
138
+ generated images against this learned distribution.
139
+ """
140
+
141
+ @classmethod
142
+ def define_schema(cls) -> io.Schema:
143
+ return io.Schema(
144
+ node_id="STANNOTrainImages",
145
+ display_name="STANNO Train from Images",
146
+ category="STANNO",
147
+ inputs=[
148
+ io.Image.Input("images"),
149
+ io.Custom.Input("STANNO", "stanno"),
150
+ io.Int.Input("epochs", default=100, min=1, max=10000, step=10,
151
+ display_mode=io.NumberDisplay.number),
152
+ io.Int.Input("batch_size", default=16, min=1, max=512, step=8,
153
+ display_mode=io.NumberDisplay.number),
154
+ io.String.Input(
155
+ "save_path",
156
+ default="",
157
+ multiline=False,
158
+ tooltip="Optional: absolute path to save the trained STANNO as .pkl. Leave empty to skip.",
159
+ ),
160
+ ],
161
+ outputs=[
162
+ io.Custom.Output("STANNO"),
163
+ io.String.Output("training_log"),
164
+ ],
165
+ )
166
+
167
+ @classmethod
168
+ def execute(cls, images, stanno, epochs, batch_size, save_path) -> io.NodeOutput:
169
+ import copy
170
+ stanno_copy = copy.deepcopy(stanno)
171
+ input_dim = stanno_copy.config.layers[0]
172
+
173
+ x = _flatten_images(images, input_dim).astype(np.float32)
174
+ x = x * 2.0 - 1.0 # normalize to [-1, 1]
175
+
176
+ log_lines: list[str] = []
177
+ report_every = max(1, epochs // 5)
178
+
179
+ def log_cb(epoch: int, loss: float) -> None:
180
+ if (epoch + 1) % report_every == 0:
181
+ line = f"epoch {epoch + 1:5d} loss={loss:.5f}"
182
+ log_lines.append(line)
183
+ print(f"[STANNO Train] {line}")
184
+
185
+ stanno_copy.fit(x, x, epochs=epochs, batch_size=batch_size, callback=log_cb)
186
+
187
+ save = save_path.strip()
188
+ if save:
189
+ os.makedirs(os.path.dirname(os.path.abspath(save)), exist_ok=True)
190
+ with open(save, "wb") as f:
191
+ pickle.dump(stanno_copy, f)
192
+ log_lines.append(f"Saved β†’ {save}")
193
+
194
+ return io.NodeOutput(stanno_copy, "\n".join(log_lines))
195
+
196
+
197
+ # ─── Node 3: Score & Filter Images ───────────────────────────────────────────
198
+
199
+ class STANNOScoreImages(io.ComfyNode):
200
+ """
201
+ Score a batch of images using a trained STANNO autoencoder.
202
+
203
+ Reconstruction MSE is the anomaly score: low = in-distribution (style match),
204
+ high = outlier. Outputs the full batch sorted by score plus a filtered
205
+ sub-batch containing only images below the threshold.
206
+ """
207
+
208
+ @classmethod
209
+ def define_schema(cls) -> io.Schema:
210
+ return io.Schema(
211
+ node_id="STANNOScoreImages",
212
+ display_name="STANNO Image Scorer",
213
+ category="STANNO",
214
+ inputs=[
215
+ io.Image.Input("images"),
216
+ io.Custom.Input("STANNO", "stanno"),
217
+ io.Float.Input(
218
+ "threshold",
219
+ default=0.10,
220
+ min=0.0,
221
+ max=2.0,
222
+ step=0.005,
223
+ display_mode=io.NumberDisplay.slider,
224
+ tooltip="MSE above this value is flagged as anomaly / style mismatch.",
225
+ ),
226
+ io.Combo.Input(
227
+ "sort_order",
228
+ options=["best_first", "worst_first", "original"],
229
+ ),
230
+ ],
231
+ outputs=[
232
+ io.Image.Output(), # sorted batch
233
+ io.Image.Output(), # filtered batch (below threshold)
234
+ io.String.Output("scores_json"),
235
+ ],
236
+ )
237
+
238
+ @classmethod
239
+ def execute(cls, images, stanno, threshold, sort_order) -> io.NodeOutput:
240
+ from stanno.integration.dsanno import DSANNO
241
+
242
+ input_dim = stanno.config.layers[0]
243
+ x = _flatten_images(images, input_dim).astype(np.float32) * 2.0 - 1.0
244
+
245
+ scanner = DSANNO(stanno, mode="reconstruction")
246
+ scores_arr, preds = scanner.score_batch(x)
247
+
248
+ scores = scores_arr.tolist()
249
+ max_s = max(scores) if max(scores) > 0 else 1.0
250
+ norm_scores = [s / max_s for s in scores]
251
+
252
+ indices = list(range(len(scores)))
253
+ if sort_order == "best_first":
254
+ indices.sort(key=lambda i: scores[i])
255
+ elif sort_order == "worst_first":
256
+ indices.sort(key=lambda i: -scores[i])
257
+
258
+ sorted_images = images[torch.tensor(indices, device=images.device)]
259
+ filtered_idx = [i for i in indices if scores[i] < threshold]
260
+ filtered_images = (
261
+ images[torch.tensor(filtered_idx, device=images.device)]
262
+ if filtered_idx else images[:1]
263
+ )
264
+
265
+ scores_data = [
266
+ {
267
+ "index": i,
268
+ "mse": round(scores[i], 5),
269
+ "norm": round(norm_scores[i], 4),
270
+ "pass": scores[i] < threshold,
271
+ }
272
+ for i in range(len(scores))
273
+ ]
274
+
275
+ return io.NodeOutput(sorted_images, filtered_images, json.dumps(scores_data, indent=2))
276
+
277
+
278
+ # ─── Node 4: Dream Conditioning ──────────────────────────────────────────────
279
+
280
+ class STANNODreamCond(io.ComfyNode):
281
+ """
282
+ Modify a CLIP CONDITIONING tensor using STANNO dream mode.
283
+
284
+ The STANNO must have been trained on CLIP embeddings (768-dim per token for
285
+ SD 1.5). Each token in the conditioning is fed as an input seed to dream(),
286
+ perturbed by noise, and the result is blended back with the original.
287
+
288
+ noise_sigma controls the creativity spectrum:
289
+ 0.00–0.02 almost identical to original prompt
290
+ 0.05–0.15 subtle but noticeable style shift (recommended starting point)
291
+ 0.20–0.40 creative variations, may drift from original prompt meaning
292
+ 0.50+ chaotic, unpredictable (useful for pure exploration)
293
+
294
+ blend_strength controls how much of the dream replaces the original:
295
+ 0.0 = original conditioning unchanged
296
+ 1.0 = full dream output (ignore original)
297
+ 0.1–0.3 recommended for most workflows
298
+ """
299
+
300
+ @classmethod
301
+ def define_schema(cls) -> io.Schema:
302
+ return io.Schema(
303
+ node_id="STANNODreamCond",
304
+ display_name="STANNO Dream Conditioning",
305
+ category="STANNO",
306
+ inputs=[
307
+ io.Conditioning.Input("conditioning"),
308
+ io.Custom.Input("STANNO", "stanno"),
309
+ io.Float.Input(
310
+ "noise_sigma",
311
+ default=0.05,
312
+ min=0.0,
313
+ max=2.0,
314
+ step=0.01,
315
+ display_mode=io.NumberDisplay.slider,
316
+ ),
317
+ io.Float.Input(
318
+ "blend_strength",
319
+ default=0.20,
320
+ min=0.0,
321
+ max=1.0,
322
+ step=0.01,
323
+ display_mode=io.NumberDisplay.slider,
324
+ ),
325
+ io.Int.Input(
326
+ "seed",
327
+ default=42,
328
+ min=0,
329
+ max=2 ** 31,
330
+ display_mode=io.NumberDisplay.number,
331
+ ),
332
+ io.Combo.Input(
333
+ "feedback_projection",
334
+ options=["repeat", "linear", "zeros"],
335
+ ),
336
+ ],
337
+ outputs=[
338
+ io.Conditioning.Output(), # modified conditioning
339
+ io.Conditioning.Output(), # original pass-through
340
+ ],
341
+ )
342
+
343
+ @classmethod
344
+ def execute(
345
+ cls, conditioning, stanno, noise_sigma, blend_strength, seed, feedback_projection
346
+ ) -> io.NodeOutput:
347
+ import copy
348
+ rng = np.random.default_rng(seed)
349
+ result = []
350
+
351
+ for cond_tensor, cond_meta in conditioning:
352
+ # cond_tensor: (1, seq_len, embed_dim) e.g. (1, 77, 768) for SD 1.5
353
+ orig_np = cond_tensor.detach().cpu().numpy().astype(np.float32)
354
+ b, seq, dim = orig_np.shape
355
+
356
+ dream_tokens: list[np.ndarray] = []
357
+ for token_idx in range(seq):
358
+ seed_vec = orig_np[0, token_idx, :].reshape(1, -1) # (1, dim)
359
+ dream_out = stanno.dream(
360
+ num_steps=1,
361
+ input_seed=seed_vec,
362
+ noise_sigma=noise_sigma,
363
+ blind_inputs=False,
364
+ rng=rng,
365
+ )
366
+ dream_tokens.append(dream_out[0]) # (dim,)
367
+
368
+ dream_cond = np.stack(dream_tokens, axis=0)[np.newaxis] # (1, seq, dim)
369
+ blended = (1.0 - blend_strength) * orig_np + blend_strength * dream_cond
370
+ blended_t = torch.from_numpy(blended).to(
371
+ device=cond_tensor.device, dtype=cond_tensor.dtype
372
+ )
373
+ result.append((blended_t, copy.deepcopy(cond_meta)))
374
+
375
+ return io.NodeOutput(result, conditioning)
376
+
377
+
378
+ # ─── Node 5: Dynamic LoRA (weight-space patching) ────────────────────────────
379
+
380
+ class STANNODynamicLoRA(io.ComfyNode):
381
+ """
382
+ Inject STANNO dream output as LoRA-equivalent weight patches into a MODEL.
383
+
384
+ STANNO generates `lora_rank` dream vectors. These are stacked into A (up)
385
+ and B (down) projection matrices and applied to the SD 1.5 cross-attention
386
+ layers via ComfyUI's native add_patches() mechanism.
387
+
388
+ Requirements:
389
+ - STANNO layers[0] == layers[-1] == 768 (SD 1.5 cross-attention dim)
390
+ - Recommended: train STANNO on CLIP embeddings of your target style first
391
+
392
+ Parameter guide:
393
+ lora_rank 1–2 β†’ subtle and stable; 4–8 β†’ stronger but may cause drift
394
+ alpha 0.3–0.5 is a good starting point for SD 1.5
395
+ noise_sigma 0.0 β†’ deterministic style from STANNO weights
396
+ 0.1–0.2 β†’ creative variations per run
397
+ """
398
+
399
+ # Cross-attention projections in SD 1.5 UNet (12 representative layers;
400
+ # add more from the full model key list for stronger effect).
401
+ _ATTN_KEYS = [
402
+ "diffusion_model.input_blocks.1.1.transformer_blocks.0.attn2.to_q.weight",
403
+ "diffusion_model.input_blocks.1.1.transformer_blocks.0.attn2.to_k.weight",
404
+ "diffusion_model.input_blocks.1.1.transformer_blocks.0.attn2.to_v.weight",
405
+ "diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_q.weight",
406
+ "diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight",
407
+ "diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_v.weight",
408
+ "diffusion_model.middle_block.1.transformer_blocks.0.attn2.to_q.weight",
409
+ "diffusion_model.middle_block.1.transformer_blocks.0.attn2.to_k.weight",
410
+ "diffusion_model.middle_block.1.transformer_blocks.0.attn2.to_v.weight",
411
+ "diffusion_model.output_blocks.9.1.transformer_blocks.0.attn2.to_q.weight",
412
+ "diffusion_model.output_blocks.9.1.transformer_blocks.0.attn2.to_k.weight",
413
+ "diffusion_model.output_blocks.9.1.transformer_blocks.0.attn2.to_v.weight",
414
+ ]
415
+
416
+ @classmethod
417
+ def define_schema(cls) -> io.Schema:
418
+ return io.Schema(
419
+ node_id="STANNODynamicLoRA",
420
+ display_name="STANNO Dynamic LoRA",
421
+ category="STANNO",
422
+ inputs=[
423
+ io.Model.Input("model"),
424
+ io.Custom.Input("STANNO", "stanno"),
425
+ io.Float.Input(
426
+ "alpha",
427
+ default=0.5,
428
+ min=0.0,
429
+ max=2.0,
430
+ step=0.05,
431
+ display_mode=io.NumberDisplay.slider,
432
+ tooltip="LoRA scaling factor. Start at 0.3–0.5 for SD 1.5.",
433
+ ),
434
+ io.Int.Input(
435
+ "lora_rank",
436
+ default=2,
437
+ min=1,
438
+ max=16,
439
+ step=1,
440
+ display_mode=io.NumberDisplay.number,
441
+ tooltip="Rank of the injected AΓ—B matrices. Lower = more stable.",
442
+ ),
443
+ io.Float.Input(
444
+ "noise_sigma",
445
+ default=0.10,
446
+ min=0.0,
447
+ max=1.0,
448
+ step=0.01,
449
+ display_mode=io.NumberDisplay.slider,
450
+ ),
451
+ io.Int.Input(
452
+ "seed",
453
+ default=0,
454
+ min=0,
455
+ max=2 ** 31,
456
+ display_mode=io.NumberDisplay.number,
457
+ ),
458
+ ],
459
+ outputs=[
460
+ io.Model.Output(),
461
+ io.String.Output("patch_info"),
462
+ ],
463
+ )
464
+
465
+ @classmethod
466
+ def execute(cls, model, stanno, alpha, lora_rank, noise_sigma, seed) -> io.NodeOutput:
467
+ rng = np.random.default_rng(seed)
468
+ dim = stanno.config.layers[0]
469
+ rank = min(lora_rank, dim)
470
+
471
+ # Generate `rank` dream vectors β€” each is a (dim,) style direction
472
+ basis: list[np.ndarray] = []
473
+ for _ in range(rank):
474
+ seed_vec = rng.normal(0.0, 0.1, (1, dim)).astype(np.float32)
475
+ dream_out = stanno.dream(
476
+ num_steps=1,
477
+ input_seed=seed_vec,
478
+ noise_sigma=noise_sigma,
479
+ blind_inputs=False,
480
+ rng=rng,
481
+ )
482
+ basis.append(dream_out[0]) # (dim,)
483
+
484
+ # A: (rank, dim) B: (dim, rank)
485
+ A = np.stack(basis, axis=0)
486
+ norms = np.linalg.norm(A, axis=1, keepdims=True).clip(min=1e-8)
487
+ A_norm = (A / norms).astype(np.float32)
488
+ B = A_norm.T.astype(np.float32)
489
+
490
+ A_t = torch.from_numpy(A_norm)
491
+ B_t = torch.from_numpy(B)
492
+
493
+ # ComfyUI LoRA patch format: {key: ("lora", (down, up))}
494
+ patches = {key: ("lora", (B_t, A_t)) for key in cls._ATTN_KEYS}
495
+
496
+ patched_model = model.clone()
497
+ patched_model.add_patches(patches, alpha)
498
+
499
+ info = (
500
+ f"Patched {len(patches)} attention layers | "
501
+ f"rank={rank} | alpha={alpha:.2f} | noise={noise_sigma:.3f} | seed={seed}"
502
+ )
503
+ print(f"[STANNO DynamicLoRA] {info}")
504
+ return io.NodeOutput(patched_model, info)
505
+
506
+
507
+ # ─── Node 6: Composite Style Checker ──��──────────────────────────────────────
508
+
509
+ class STANNOCompositeCheck(io.ComfyNode):
510
+ """
511
+ Score a batch of images against two STANNOs and route by the closer match.
512
+
513
+ In a composite / inpainting workflow different image zones should each match
514
+ a particular trained style. This node splits a batch into two sub-batches
515
+ based on which STANNO has lower reconstruction error, and reports the margin
516
+ so you can identify ambiguous images.
517
+
518
+ Typical use: connect two STANNOs trained on 'background style' and
519
+ 'foreground style'; route each generated image to the right inpaint layer.
520
+ """
521
+
522
+ @classmethod
523
+ def define_schema(cls) -> io.Schema:
524
+ return io.Schema(
525
+ node_id="STANNOCompositeCheck",
526
+ display_name="STANNO Composite Style Checker",
527
+ category="STANNO",
528
+ inputs=[
529
+ io.Image.Input("images"),
530
+ io.Custom.Input("STANNO", "stanno_a"),
531
+ io.Custom.Input("STANNO", "stanno_b"),
532
+ io.String.Input("label_a", default="Style A", multiline=False),
533
+ io.String.Input("label_b", default="Style B", multiline=False),
534
+ ],
535
+ outputs=[
536
+ io.Image.Output(), # images closest to Style A
537
+ io.Image.Output(), # images closest to Style B
538
+ io.String.Output("report_json"),
539
+ ],
540
+ )
541
+
542
+ @classmethod
543
+ def execute(cls, images, stanno_a, stanno_b, label_a, label_b) -> io.NodeOutput:
544
+ dim_a = stanno_a.config.layers[0]
545
+ dim_b = stanno_b.config.layers[0]
546
+
547
+ xa = _flatten_images(images, dim_a).astype(np.float32) * 2.0 - 1.0
548
+ xb = _flatten_images(images, dim_b).astype(np.float32) * 2.0 - 1.0
549
+
550
+ scores_a = np.mean((stanno_a.predict(xa) - xa) ** 2, axis=1)
551
+ scores_b = np.mean((stanno_b.predict(xb) - xb) ** 2, axis=1)
552
+
553
+ idx_a = [i for i in range(len(scores_a)) if scores_a[i] <= scores_b[i]]
554
+ idx_b = [i for i in range(len(scores_a)) if scores_a[i] > scores_b[i]]
555
+
556
+ imgs_a = (
557
+ images[torch.tensor(idx_a, device=images.device)]
558
+ if idx_a else images[:1]
559
+ )
560
+ imgs_b = (
561
+ images[torch.tensor(idx_b, device=images.device)]
562
+ if idx_b else images[:1]
563
+ )
564
+
565
+ report = [
566
+ {
567
+ "index": i,
568
+ label_a: round(float(scores_a[i]), 5),
569
+ label_b: round(float(scores_b[i]), 5),
570
+ "winner": label_a if scores_a[i] <= scores_b[i] else label_b,
571
+ "margin": round(abs(float(scores_a[i]) - float(scores_b[i])), 5),
572
+ }
573
+ for i in range(len(scores_a))
574
+ ]
575
+
576
+ return io.NodeOutput(imgs_a, imgs_b, json.dumps(report, indent=2))
577
+
578
+
579
+ # ─── Node 7: DSANNO Scan ──────────────────────────────────────────────────────
580
+
581
+ class STANNOScan(io.ComfyNode):
582
+ """
583
+ DSANNO β€” Data Scanning Artificial Neural Network Object.
584
+
585
+ Scans a batch of images and finds the ones that best match what the STANNO
586
+ has learned, implementing the patent's DSANNO concept: "scan large regions
587
+ of the data space looking for patterns that match the learned representation."
588
+
589
+ Two modes
590
+ ─────────
591
+ auto_calibrate = ON (recommended)
592
+ The threshold is computed automatically from this very batch at the
593
+ given percentile. E.g. percentile=20 keeps the best-matching 20 %.
594
+
595
+ auto_calibrate = OFF
596
+ Use the manually supplied ``threshold`` value directly.
597
+
598
+ Outputs
599
+ ───────
600
+ top_k_images β€” the k images with lowest reconstruction error
601
+ matched_images β€” all images below the threshold
602
+ scores_json β€” per-image scores and match flags
603
+ threshold β€” the threshold that was applied (useful for display/routing)
604
+ """
605
+
606
+ @classmethod
607
+ def define_schema(cls) -> io.Schema:
608
+ return io.Schema(
609
+ node_id="STANNOScan",
610
+ display_name="STANNO Scan (DSANNO)",
611
+ category="STANNO",
612
+ inputs=[
613
+ io.Image.Input("images"),
614
+ io.Custom.Input("STANNO", "stanno"),
615
+ io.Int.Input(
616
+ "top_k",
617
+ default=4,
618
+ min=1,
619
+ max=64,
620
+ step=1,
621
+ display_mode=io.NumberDisplay.number,
622
+ tooltip="Return this many best-matching images regardless of threshold.",
623
+ ),
624
+ io.Combo.Input(
625
+ "auto_calibrate",
626
+ options=["on", "off"],
627
+ tooltip=(
628
+ "on: compute threshold automatically from this batch at the "
629
+ "given percentile.\n"
630
+ "off: use the manual threshold value."
631
+ ),
632
+ ),
633
+ io.Float.Input(
634
+ "percentile",
635
+ default=30.0,
636
+ min=1.0,
637
+ max=99.0,
638
+ step=1.0,
639
+ display_mode=io.NumberDisplay.slider,
640
+ tooltip=(
641
+ "Used when auto_calibrate=on. "
642
+ "30 = keep the best-matching 30 % of the batch."
643
+ ),
644
+ ),
645
+ io.Float.Input(
646
+ "threshold",
647
+ default=0.10,
648
+ min=0.0,
649
+ max=5.0,
650
+ step=0.005,
651
+ display_mode=io.NumberDisplay.number,
652
+ tooltip="Manual threshold (used only when auto_calibrate=off).",
653
+ ),
654
+ ],
655
+ outputs=[
656
+ io.Image.Output(), # top_k_images
657
+ io.Image.Output(), # matched_images
658
+ io.String.Output("scores_json"),
659
+ io.Float.Output("threshold"),
660
+ ],
661
+ )
662
+
663
+ @classmethod
664
+ def execute(cls, images, stanno, top_k, auto_calibrate, percentile, threshold) -> io.NodeOutput:
665
+ from stanno.integration.dsanno import DSANNO
666
+
667
+ input_dim = stanno.config.layers[0]
668
+ x = _flatten_images(images, input_dim).astype(np.float32) * 2.0 - 1.0
669
+
670
+ scanner = DSANNO(stanno, mode="reconstruction")
671
+ result = scanner.scan(x)
672
+
673
+ # Determine threshold
674
+ if auto_calibrate == "on":
675
+ used_threshold = float(np.percentile(result.scores, percentile))
676
+ else:
677
+ used_threshold = float(threshold)
678
+
679
+ result.set_threshold(used_threshold)
680
+
681
+ # top_k images
682
+ k = min(int(top_k), len(images))
683
+ top_indices, top_scores, _ = scanner.top_k(x, k=k)
684
+ top_images = images[torch.tensor(top_indices.tolist(), device=images.device)]
685
+
686
+ # matched images (below threshold)
687
+ matched_idx = result.matched_indices().tolist()
688
+ matched_images = (
689
+ images[torch.tensor(matched_idx, device=images.device)]
690
+ if matched_idx else images[:1]
691
+ )
692
+
693
+ scores_data = [
694
+ {
695
+ "index": int(i),
696
+ "mse": round(float(result.scores[i]), 5),
697
+ "matched": bool(result.matched_mask[i]),
698
+ "rank": int(np.where(np.argsort(result.scores) == i)[0][0]) + 1,
699
+ }
700
+ for i in range(len(result.scores))
701
+ ]
702
+
703
+ print(
704
+ f"[STANNO Scan] threshold={used_threshold:.4f} | "
705
+ f"matched={len(matched_idx)}/{len(images)} | top_k={k}"
706
+ )
707
+
708
+ return io.NodeOutput(
709
+ top_images,
710
+ matched_images,
711
+ json.dumps(scores_data, indent=2),
712
+ used_threshold,
713
+ )
714
+
715
+
716
+ # ─── Node 8: Cascade Load / Create ───────────────────────────────────────────
717
+
718
+ class STANNOCascadeLoad(io.ComfyNode):
719
+ """
720
+ Load or create a CascadeSTANNO β€” a chain of STANNO stages.
721
+
722
+ Implements the patent's "cascading networks to form system models":
723
+ the output of stage k feeds the input of stage k+1, and each stage
724
+ can be independently frozen or adapted.
725
+
726
+ Typical uses
727
+ ────────────
728
+ Encoder + Decoder autoencoder:
729
+ stages_json = [{"layers": [3072, 512]}, {"layers": [512, 3072]}]
730
+
731
+ Progressive compression pipeline:
732
+ stages_json = [{"layers":[768,256]}, {"layers":[256,64]}, {"layers":[64,256]}, {"layers":[256,768]}]
733
+
734
+ Frozen pre-processor + adaptive head:
735
+ stages_json = [{"layers":[768,256]}, {"layers":[256,10]}]
736
+ frozen_json = [true, false]
737
+
738
+ stages_json format
739
+ ──────────────────
740
+ JSON array of objects. Keys:
741
+ "layers" required β€” e.g. [768, 256, 768]
742
+ "trainer_type" optional β€” "fixed"|"local_rule"|"evolutionary" (default "fixed")
743
+ "learning_rate" optional β€” per-stage lr (default: uses the top-level lr)
744
+ """
745
+
746
+ @classmethod
747
+ def define_schema(cls) -> io.Schema:
748
+ return io.Schema(
749
+ node_id="STANNOCascadeLoad",
750
+ display_name="STANNO Cascade Loader",
751
+ category="STANNO",
752
+ inputs=[
753
+ io.String.Input(
754
+ "model_path",
755
+ default="cascade_model.pkl",
756
+ multiline=False,
757
+ tooltip="Path to a saved CascadeSTANNO .pkl, or a new filename to create.",
758
+ ),
759
+ io.String.Input(
760
+ "stages_json",
761
+ default='[{"layers": [3072, 512]}, {"layers": [512, 3072]}]',
762
+ multiline=True,
763
+ tooltip=(
764
+ "JSON array of stage configs. Each stage needs at minimum "
765
+ '{"layers": [in, ..., out]}. Used only when creating a new cascade.'
766
+ ),
767
+ ),
768
+ io.String.Input(
769
+ "frozen_json",
770
+ default="[]",
771
+ multiline=False,
772
+ tooltip=(
773
+ "JSON bool array of frozen flags per stage. "
774
+ "[] = all trainable. Example: [true, false] = freeze stage 0."
775
+ ),
776
+ ),
777
+ io.Combo.Input(
778
+ "trainer_type",
779
+ options=["fixed", "local_rule", "evolutionary"],
780
+ tooltip="Default trainer type applied to stages that don't override it.",
781
+ ),
782
+ io.Float.Input(
783
+ "learning_rate",
784
+ default=0.01,
785
+ min=1e-5,
786
+ max=1.0,
787
+ step=0.001,
788
+ display_mode=io.NumberDisplay.number,
789
+ tooltip="Default learning rate applied to stages that don't override it.",
790
+ ),
791
+ ],
792
+ outputs=[
793
+ io.Custom.Output("CASCADE"),
794
+ io.String.Output("info"),
795
+ ],
796
+ )
797
+
798
+ @classmethod
799
+ def execute(
800
+ cls, model_path, stages_json, frozen_json, trainer_type, learning_rate
801
+ ) -> io.NodeOutput:
802
+ import os
803
+ from stanno.config.schema import STANNOConfig
804
+ from stanno.core.stanno import STANNO
805
+ from stanno.integration.cascade import CascadeSTANNO
806
+
807
+ if os.path.isfile(model_path):
808
+ cascade = CascadeSTANNO.load(model_path)
809
+ info = (
810
+ f"Loaded: {model_path} | "
811
+ f"{len(cascade.stages)} stages | "
812
+ f"frozen={cascade.frozen}"
813
+ )
814
+ else:
815
+ stage_defs = json.loads(stages_json)
816
+ frozen = json.loads(frozen_json) if frozen_json.strip() else []
817
+ if not frozen:
818
+ frozen = [False] * len(stage_defs)
819
+
820
+ stages = []
821
+ for sd in stage_defs:
822
+ lr = sd.get("learning_rate", learning_rate)
823
+ tt = sd.get("trainer_type", trainer_type)
824
+ scfg = STANNOConfig(
825
+ layers=sd["layers"],
826
+ trainer_type=tt,
827
+ learning_rate=lr,
828
+ )
829
+ stages.append(STANNO(scfg))
830
+
831
+ cascade = CascadeSTANNO(stages, frozen=frozen)
832
+ topology = " β†’ ".join(
833
+ "Γ—".join(str(d) for d in s.config.layers) for s in stages
834
+ )
835
+ info = f"Created CascadeSTANNO: {topology} | frozen={frozen}"
836
+
837
+ print(f"[STANNO Cascade Loader] {info}")
838
+ return io.NodeOutput(cascade, info)
839
+
840
+
841
+ # ─── Node 9: Cascade Train on Images ─────────────────────────────────────────
842
+
843
+ class STANNOCascadeTrainImages(io.ComfyNode):
844
+ """
845
+ Train a CascadeSTANNO end-to-end on a batch of images.
846
+
847
+ Implements the patent's "self-training within cascaded systems":
848
+ gradient flows from the final stage back through every non-frozen stage
849
+ via the cascade mechanism in FixedTrainerNet.
850
+
851
+ Autoencoder use-case (most common)
852
+ ───────────────────────────────────
853
+ Set up a CascadeSTANNO with an encoder stage and a decoder stage. This
854
+ node trains the whole chain with input == target so the bottleneck is
855
+ forced to compress image content.
856
+
857
+ Partial training (frozen stages)
858
+ ─────────────────────────────────
859
+ Freeze the encoder in STANNOCascadeLoad, then connect here. Only the
860
+ unfrozen decoder receives weight updates β€” useful for domain adaptation.
861
+ """
862
+
863
+ @classmethod
864
+ def define_schema(cls) -> io.Schema:
865
+ return io.Schema(
866
+ node_id="STANNOCascadeTrainImages",
867
+ display_name="STANNO Cascade Train from Images",
868
+ category="STANNO",
869
+ inputs=[
870
+ io.Image.Input("images"),
871
+ io.Custom.Input("CASCADE", "cascade"),
872
+ io.Int.Input(
873
+ "epochs",
874
+ default=100,
875
+ min=1,
876
+ max=5000,
877
+ step=10,
878
+ display_mode=io.NumberDisplay.number,
879
+ ),
880
+ io.Int.Input(
881
+ "batch_size",
882
+ default=16,
883
+ min=1,
884
+ max=256,
885
+ step=8,
886
+ display_mode=io.NumberDisplay.number,
887
+ ),
888
+ io.Int.Input(
889
+ "patience",
890
+ default=30,
891
+ min=0,
892
+ max=500,
893
+ step=5,
894
+ display_mode=io.NumberDisplay.number,
895
+ tooltip="Early stopping patience in epochs. 0 = disabled.",
896
+ ),
897
+ io.String.Input(
898
+ "save_path",
899
+ default="",
900
+ multiline=False,
901
+ tooltip="Optional path to save the trained cascade as .pkl.",
902
+ ),
903
+ ],
904
+ outputs=[
905
+ io.Custom.Output("CASCADE"),
906
+ io.String.Output("training_log"),
907
+ ],
908
+ )
909
+
910
+ @classmethod
911
+ def execute(cls, images, cascade, epochs, batch_size, patience, save_path) -> io.NodeOutput:
912
+ import copy
913
+
914
+ cascade_copy = copy.deepcopy(cascade)
915
+
916
+ # Use the first stage's input_dim for flattening
917
+ input_dim = cascade_copy.stages[0].config.layers[0]
918
+ output_dim = cascade_copy.stages[-1].config.layers[-1]
919
+
920
+ x = _flatten_images(images, input_dim).astype(np.float32) * 2.0 - 1.0
921
+
922
+ # Autoencoder: target is the image itself (needs matching output dim)
923
+ if output_dim == input_dim:
924
+ y = x
925
+ else:
926
+ # If dims differ, pad/trim y to match output dim
927
+ if x.shape[1] >= output_dim:
928
+ y = x[:, :output_dim]
929
+ else:
930
+ pad = np.zeros((x.shape[0], output_dim - x.shape[1]), dtype=np.float32)
931
+ y = np.hstack([x, pad])
932
+
933
+ log_lines: list[str] = []
934
+ report_every = max(1, epochs // 5)
935
+
936
+ def log_cb(epoch: int, loss: float) -> None:
937
+ if (epoch + 1) % report_every == 0:
938
+ line = f"epoch {epoch + 1:5d} loss={loss:.5f}"
939
+ log_lines.append(line)
940
+ print(f"[STANNO Cascade Train] {line}")
941
+
942
+ cascade_copy.fit(
943
+ x, y,
944
+ epochs=epochs,
945
+ batch_size=batch_size,
946
+ patience=patience,
947
+ log_every=0, # use callback instead
948
+ callback=log_cb,
949
+ )
950
+
951
+ save = save_path.strip()
952
+ if save:
953
+ os.makedirs(os.path.dirname(os.path.abspath(save)), exist_ok=True)
954
+ cascade_copy.save(save)
955
+ log_lines.append(f"Saved β†’ {save}")
956
+ print(f"[STANNO Cascade Train] Saved β†’ {save}")
957
+
958
+ return io.NodeOutput(cascade_copy, "\n".join(log_lines))
959
+
960
+
961
+ # ─── Extension registration ───────────────────────────────────────────────────
962
+
963
+ class STANNOExtension(ComfyExtension):
964
+ async def get_node_list(self) -> list[type[io.ComfyNode]]:
965
+ return [
966
+ STANNOLoad,
967
+ STANNOTrainImages,
968
+ STANNOScoreImages,
969
+ STANNODreamCond,
970
+ STANNODynamicLoRA,
971
+ STANNOCompositeCheck,
972
+ STANNOScan,
973
+ STANNOCascadeLoad,
974
+ STANNOCascadeTrainImages,
975
+ ]
976
+
977
+
978
+ async def comfy_entrypoint() -> STANNOExtension:
979
+ return STANNOExtension()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # stanno Python package β€” required core dependency.
2
+ # Once published to PyPI, this line becomes simply: stanno>=0.1.0
3
+ # Until then, install from GitHub:
4
+ git+https://github.com/USER/stanno.git
5
+ numpy>=1.24