Up-to-date with original repo
Browse files- .github/CODEOWNERS +6 -0
- .gitignore +46 -0
- README.md +288 -3
- STANNO_IS_NOT.md +155 -0
- __init__.py +99 -0
- nodes.py +979 -0
- 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 |
-
|
| 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
|