Architecture¶
strands-sana is a thin agent-tool wrapper. The hard work is delegated to diffusers (inference) and upstream NVlabs/Sana scripts (training).
Module layout¶
strands_sana/
├── __init__.py # Public API: 33 @tool functions
├── tools/
│ ├── generate.py # 13 core inference tools
│ ├── extras.py # 10 extras (schedulers, quant, metrics, …)
│ ├── video.py # 2 video tools
│ ├── img2img.py # 1 img2img tool
│ └── training.py # 7 training tools (subprocess to NVlabs/Sana)
├── pipeline/
│ └── sana_pipeline.py # SanaPipelineWrapper — kind dispatch + caching
├── models/
│ └── registry.py # 15 SanaModelInfo entries, list/filter helpers
└── utils/
├── io.py # save_image, load_image, ensure_output_dir
└── prompts.py # COMPLEX_HUMAN_INSTRUCTION + style helpers
Inference path¶
sequenceDiagram
participant U as User / Agent
participant T as @tool function
participant W as SanaPipelineWrapper
participant D as diffusers
participant G as GPU
U->>T: sana_generate(prompt=..., model=...)
T->>W: get_pipeline(model_name, kind_override)
W->>W: cache lookup
alt cache miss
W->>D: from_pretrained(hf_repo, dtype=bf16)
D->>G: load weights
W->>W: enable_model_cpu_offload()
end
T->>W: pipe.generate(prompt, steps, seed, ...)
W->>D: pipe(**kwargs)
D->>G: forward pass × N steps
D-->>W: PIL.Image[]
W-->>T: list[PIL.Image]
T->>T: save_image(...)
T-->>U: {"path": "...", "model": "...", ...}
Pipeline kinds¶
graph TB
Wrapper["SanaPipelineWrapper"]
Wrapper --> T2I["t2i → SanaPipeline"]
Wrapper --> PAG["pag → SanaPAGPipeline"]
Wrapper --> Sprint["sprint → SanaSprintPipeline"]
Wrapper --> SprintI2I["sprint-i2i → SanaSprintImg2ImgPipeline"]
Wrapper --> Ctrl["controlnet → SanaControlNetPipeline"]
Wrapper --> Vid["video → SanaVideoPipeline"]
Wrapper --> I2V["image-to-video → SanaImageToVideoPipeline"]
style Wrapper fill:#5e35b1,stroke:#333,color:#fff
The kind is determined by:
kind_overrideargument (explicit)SanaModelInfo.pipeline_kind(model registry)pag_scale > 0upgrade (only fort2i→pag)
Cache key¶
So (sana-1.6b-1024, t2i, cuda) and (sana-1.6b-1024, pag, cuda) are separate entries — switching just flips the active pipeline class without re-downloading weights (where shared).
Training path¶
Training is fundamentally different — we shell out:
sequenceDiagram
participant U as User / Agent
participant T as sana_train_*
participant R as resolve_sana_root()
participant FS as Filesystem
participant SP as subprocess
participant SC as NVlabs/Sana script
U->>T: sana_train_lora(instance_data_dir=...)
T->>R: find Sana/ via cwd / SANA_ROOT / package
R->>FS: stat ./Sana/train_scripts
FS-->>R: ✓
T->>T: build [accelerate, launch, ...] argv
alt dry_run=True
T-->>U: {"command": "...", "dry_run": true}
else dry_run=False
T->>SP: Popen(argv, cwd=Sana, env={...})
SP->>SC: torchrun / accelerate ...
T-->>U: {"pid": 12345, "command": "..."}
end
Auto-resolves Sana/ via:
- Explicit
sana_root=argument SANA_ROOTenv var./Sanarelative to cwd- Package-relative
../../Sana
Public API surface¶
33 @tool functions divided into 6 groups — see API Reference.
What we don't do¶
- We don't vendor NVlabs/Sana research code —
diffuserscarries the inference; upstream Sana repo carries training scripts - We don't reimplement scheduling — pulled from
diffusers - We don't ship checkpoints — auto-downloaded from HuggingFace on first use
This keeps the package small and ensures every Sana feature lands here as soon as upstream ships it.