Portable CPU inference for Stable Audio 3 — the LiteRT/TFLite sibling of the
MLX (Apple Silicon) and TensorRT (NVIDIA GPU) releases.
No PyTorch, transformers, or stable-audio-tools at runtime — just ai_edge_litert
(LiteRT) driving fully self-contained .tflite graphs through the XNNPACK CPU
delegate. Runs anywhere LiteRT runs: macOS / Linux / Windows, x86 / ARM
(Windows is x64-only — see Windows).
One line on a fresh machine — installs everything and plays back ~30 seconds of "Impending tribal, epic orchestral buildup":
curl -LsSf https://raw.githubusercontent.com/Stability-AI/stable-audio-3/main/optimized/tflite/bootstrap.sh | bashWindows — stock PowerShell (no bash/git needed; the curl|bash line above requires Git Bash or WSL on Windows):
irm https://raw.githubusercontent.com/Stability-AI/stable-audio-3/main/optimized/tflite/bootstrap.ps1 | iexAlready cloned the repo? Run from inside optimized/tflite/:
# macOS / Linux / Windows Git Bash
./install.sh # one-time setup
./sa3 --prompt "Impending tribal, epic orchestral buildup" --play # generates + plays:: Windows cmd / PowerShell
install.bat
sa3.bat --prompt "Impending tribal, epic orchestral buildup" --play
:: PowerShell users can use .\sa3.ps1 instead of sa3.bat — same flags, and
:: Ctrl-C exits cleanly (cmd's "Terminate batch job (Y/N)?" prompt is a .bat quirk)--dit |
model | best for |
|---|---|---|
sm-music |
sa3-sm-music (50 M block) | fast music generation |
sm-sfx |
sa3-sm-sfx (50 M block) | sound effects |
medium |
sa3-medium-ARC (1.4 B) | higher-quality music, slower |
| mode | flags | example |
|---|---|---|
| text-to-audio | --prompt P |
new clip from a description |
| audio-to-audio | --prompt P --init-audio IN.wav --init-noise-level σ |
variation of an existing clip |
| inpainting | --prompt P --init-audio IN.wav --inpaint-range "S,E" |
regenerate one section, keep rest |
| CFG + negative | --cfg 3.0 --negative-prompt P_NEG |
steer toward / away from prompts |
prompt ─▶ T5Gemma encoder ─▶ DiT pingpong sampler ─▶ SAME-S/L decoder ─▶ WAV
▲
optional: encoder + init audio (audio-to-audio / inpaint)
Precision is split between the DiT and the SAME codec — they react to quantization oppositely:
-
DiT (
--dit-precision, or the global--precision) — all three DiTs (medium, sm-music, sm-sfx) are from-repo rung ladders (see The rung-ladder DiTs below), each with the same two tiers:w8a8(default) — cache-safe int8: ~equal quality to the oldw8a8-dyn(cos ≥ 0.996 per forward) but with flat RAM, faster at the rung sizes, and a single self-contained file.fp32— the bit-exact reference (and the pick for LoRA merges or an A/B).
int8 on the DiT is a different (not worse) sample — the 8-step sampler is chaotically sensitive — so
w8a8is the shipped default andfp32is the reference. medium's ladder tops at 4096 latents (380 s); the small DiTs' ladder tops at 1292 (their 2-min max). The old varlen tiers (w16a32/w8a32/w8a8-dyn) are retired totflite/sa3-*/legacy/. (wXaY = weight/activation bits; "16" = fp16.) -
Codec (
--decoder-precision/--encoder-precision):fp32·w8a8(default). The SAME enc/dec ship as static rung models — see docs/RUNGS.md. They run once on a fixed latent, so int8 is quality-free: a real-music round-trip is identical to fp32.w8a8is ~3× faster and ~half the RAM on an int8-capable CPU;fp32is bit-exact and the pick for CPUs without int8 acceleration. (The oldw16a32/w8a32codec variants added no audible quality and are retired totflite/same-*/legacy/.)
--precision is a global default that sets the DiT directly and maps to the codec (fp32→fp32, any int8→w8a8).
When omitted, the DiT is w8a8 for every family and the codec is w8a8. All variants keep the full feature
set (any --seconds, odd/even L) and lazy-download from HuggingFace on first use.
| component | precisions | size | notes |
|---|---|---|---|
| DiT — medium (rung) | w8a8 (default) / fp32 |
1.6 / 5.8 GB | 12-rung ladder (→ 4096 / 380 s); w8a8 = cache-safe int8, flat ~2–4 GB RAM, ≈ old w8a8-dyn; old w16a32/w8a32/w8a8-dyn → legacy/; litert ≥ 2.2.0 |
| DiT — small (rung) | w8a8 (default) / fp32 |
0.5 / 1.8 GB | 8-rung ladder (→ 1292 / 2 min); same cache-safe int8, flat ~0.8 GB RAM (4.5× less than the old varlen at 2 min); old tiers → legacy/ |
| codec (rung) | fp32 / w8a8 |
SAME-S enc/dec 0.22 / 0.25 → w8a8 0.06 / 0.09 GB · SAME-L 1.7 / 1.8 → w8a8 0.47 / 0.52 GB | w8a8 quality-free, ~3× faster + half RAM; needs litert ≥ 2.2.0 |
| T5Gemma | fp16 | 0.6 GB | single-precision |
(The rung w8a8 uses per-channel int8 weights + per-token dynamic activation quant, so its int8×int8 matmuls
run faster than fp32 on an int8-capable CPU. The codec rungs use dynamic int8 too, where the extra activation-quant
error is far below the AE's own loss.)
All three rung DiTs run batch=1 / sequential CFG (one forward at --cfg 1, a cond+uncond dual-pass otherwise),
so --cfg-batched is a no-op — the old varlen "batched CFG" behavior retired with the varlen graphs.
Every DiT — medium and the two small models — now ships as a static rung ladder, built from the repo's own
PyTorch model (see build/), not a dynamic-shape varlen graph. A render of length L runs on the
smallest fixed rung ≥ L in one forward, with the extra positions masked out of self-attention — so the result is
exact, not tiled. Both fp32 and the cache-safe int8 w8a8 are single self-contained files (dit_{fp32,w8a8}.tflite;
the per-step global-cond preamble rides inside as a gcond signature, so there is no extra file to ship).
Why it matters — RAM. The old varlen graph materialized the full [heads, S, S] attention, so peak RAM grew with
length and hit ~40 GB at the 380 s maximum — unusable on a normal machine. The rung ladder's static shapes let
XNNPACK plan a tight activation arena (reused across layers) and share one weight copy through the cache, so peak RAM
is flat at ~2–4 GB across the entire length range — up to 10× less at long lengths — while being faster at the
rung sizes and ≈ equal quality to the old w8a8-dyn (cos 0.996 per forward).
Medium — 12 rungs (in latents; audio ≈ L / 10.8 s, 4096 = 380 s max):
8, 48, 96, 192, 416, 704, 1056, 1496, 2040, 2824, 3536, 4096 (via DIT_LADDER in build/build_dit.sh).
Small (sm-music / sm-sfx) — 8 rungs, capped at the 2-minute maximum (valid_T_lat(120) = 1292 latents):
8, 48, 96, 192, 416, 704, 1056, 1292 (built by build/build_dit_small.sh). Same win —
peak RAM stays flat at ~0.8 GB across the whole range, vs the old varlen graph's quadratic climb to ~3.8 GB at
2 min (4.5× less). sm-music and sm-sfx are architecturally identical, so the curve is the same for both:
The short rungs keep tiny clips fast (a 1 s clip no longer pays for a long render); a length between rungs pads up to
the next one. The old varlen tiers (fp32, w16a32, w8a32, w8a8-dyn) for all three families are retired to
tflite/sa3-{m,sm-music,sm-sfx}/legacy/ on HuggingFace.
# medium DiT — defaults: w8a8 rung DiT + w8a8 SAME-S codec (the `--decoder` now defaults to same-s)
./sa3 --prompt "lofi house loop" --dit medium
# small music DiT — defaults: fp32 DiT + w8a8 SAME-S codec
./sa3 --prompt "lofi house loop" --dit sm-music
# bit-exact codec (e.g. a CPU without int8 acceleration)
./sa3 --prompt "lofi house loop" --dit sm-music --decoder-precision fp32
# cap codec RAM on a tiny-memory device (slower)
./sa3 --prompt "lofi house loop" --dit medium --max-rung 64--dit-precision / --decoder-precision / --encoder-precision override the global flag per component
(the DiT changes which sample you get; the codec's precision is quality-free on the round-trip):
--dit-precision / --decoder-precision / --encoder-precision override the shared
flag per component — useful because they react to quantization very differently (the
codec's precision maps directly to audio fidelity; the DiT's changes which sample
you get; the encoder's affects a2a/inpaint latents):
# fp32 DiT (bit-exact reference), reference-quality fp32 codec
./sa3 --prompt "lofi house loop" --dit sm-music --decoder same-s \
--dit-precision fp32 --decoder-precision fp32./install.shinstall.sh is uv-based. On a fresh machine it will:
- Install uv via the official curl
installer if it's missing (prompts y/N;
-yskips the prompt). - Create a project-local
.venv/with managed Python 3.11. uv pip installthe runtime deps into the venv (much faster than pip).- Ask which DiT bundles to download from HuggingFace
(
stabilityai/stable-audio-3-optimized). Each pick pulls its matching audio codec; T5Gemma (the shared text encoder) is downloaded once. Already-present weights are skipped.
End-to-end on a fresh machine: ~10 seconds + weight downloads.
Don't want to pre-pick bundles? Skip install entirely and just run
./sa3 --prompt …— any missing model file is downloaded from HF on first use and symlinked intomodels/tflite/from the HuggingFace cache.
Portable CPU (no GPU required). Python 3.9+. ./install.sh --python 3.12 to
pin a different Python.
The stack runs natively on Windows x64 — no WSL needed. ai-edge-litert
ships win_amd64 wheels for Python 3.10–3.13 (x64 only; no Windows-ARM
wheels), so install a Python in that range from
python.org (check "Add python.exe to
PATH"), then from optimized\tflite\:
install.bat :: one-time setup (python -m venv + pip)
sa3.bat --prompt "lofi house loop" --dit sm-music --decoder same-sinstall.bat / sa3.bat are the Windows twins of ./install.sh / ./sa3
(plain venv + pip; uv not required). Notes:
- ffmpeg is optional — only needed for mp3 / 24-bit / non-44.1 kHz
--init-audioinputs:winget install ffmpeg(orchoco install ffmpeg). - Symlinks: downloaded weights are exposed via symlinks where possible; without Developer Mode, Windows disallows creating them, so the downloader automatically falls back to a hardlink (zero-copy) or a plain copy. No action needed either way.
- Long paths: the HuggingFace cache nests deeply — if downloads fail with
path-length errors, enable Windows long paths once (admin PowerShell):
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 1(theLongPathsEnabledregistry switch), then restart the terminal. --playuses the stdlibwinsoundplayer (macOS usesafplay).
./sa3 is a thin shell wrapper around .venv/bin/python scripts/sa3_tflite.py "$@" that prompts to run ./install.sh if uv or .venv/ isn't set up.
# Text-to-audio
./sa3 --prompt "lofi house loop" --dit sm-music --decoder same-s --out lofi.wav
# Sound effects
./sa3 --prompt "footsteps on gravel" --dit sm-sfx --decoder same-s --out steps.wav
# Higher-quality music (medium DiT + SAME-L codec)
./sa3 --prompt "A beautiful piano arpeggio grows into a cinematic climax" \
--dit medium --decoder same-l --seconds 30 --out piano.wav
# Audio-to-audio variation (σmax 0.4-0.8 typical)
./sa3 --prompt "jazz fusion with electric piano" --dit sm-music --decoder same-s \
--init-audio funk.wav --init-noise-level 0.7 --out funk_jazz.wav
# Inpaint seconds 4-7
./sa3 --prompt "explosive drum break" --dit sm-music --decoder same-s \
--init-audio funk.wav --inpaint-range "4,7" --out funk_drums.wav
# CFG + negative prompt
./sa3 --prompt "ambient drone" --cfg 3.0 --negative-prompt "drums, vocals" \
--dit sm-music --decoder same-s --out drone.wav
# Generate + play immediately (afplay/winsound/aplay; Ctrl-C stops both)
./sa3 --prompt "rainforest" --dit sm-sfx --decoder same-s --play
# All options + categorised examples
./sa3 --helpOmit --dit / --decoder for an interactive arrow-key picker. Omit
--prompt for a stdin prompt. Relative --out paths land in output/
(auto-created); absolute paths are honoured as-is. The output path is
printed prominently as a ▸ saved line at the end of each run.
Use --threads to control the XNNPACK CPU thread count (default 8).
A browser UI over the same TFLite backend — every generation mode plus a spectrogram player, a history panel, and playback options (auto-play, infinite radio with pre-generation, loop, hotswap, MP3/WAV save).
./sa3-gradio # sm-music, opens a public share link
./sa3-gradio --dit medium # start on the medium DiT
./sa3-gradio --no-share # local-only (http://localhost:7860)
./sa3-gradio --port 7871 # pick the port./sa3-gradio is a wrapper around .venv/bin/python scripts/sa3_gradio.py; on
first run it offers to install the UI-only extras (gradio, pillow,
soundfile) into .venv. In the browser:
- Precision dropdown next to the model picker —
w8a8(default) /fp32, applied to the DiT; the codec maps it to its rung tier (fp32→ fp32, int8 → w8a8). - LoRA panel — upload a
.safetensorsadapter or pick one fromloras/<model>/(e.g.loras/sa3-medium/), set its strength, stack several; settings are remembered per model. The panel is disabled under a quantized precision (see the LoRA note above) and has no per-step range (frozen graph). - Audio-to-audio / inpainting — upload a reference clip; inpainting adds a start/end range. Both are combinable.
Generated WAV/MP3s land in output/gradio/. First use of a model/precision
loads its weights (cached after).
.venv/bin/python scripts/sa3_tflite.py --prompt "..." --dit medium --decoder same-l
# or, after `source .venv/bin/activate`:
python scripts/sa3_tflite.py --prompt "..." --dit medium --decoder same-lThis is a CPU path — it trades the GPU releases' speed for portability. The
small models comfortably beat realtime on a modern laptop CPU; medium is slower
(its DiT is ~5.8 GB fp32). Use ≥ ~20 s clips: very
short clips have too few latent tokens for the sampler to settle into a coherent
loop. Throughput scales with --threads up to your physical core count (4–8 is
the usual sweet spot; more threads on a short model adds overhead).
For sub-realtime latency on a supported device, prefer the GPU siblings: MLX on Apple Silicon, TensorRT on NVIDIA.
| Flag | Default | Notes |
|---|---|---|
--prompt |
(asks) | Text prompt; empty string = unconditional |
--negative-prompt |
— | CFG uncond branch; only used when --cfg ≠ 1.0 |
--dit |
(asks) | sm-music, sm-sfx, or medium |
--decoder |
same-s |
same-s (default for every DiT — medium's latents decode fine on it, ~7× faster) or same-l (bigger, for max fidelity on medium) |
--seconds |
30 | Output length (use ≥ ~20 s) |
--steps |
8 | Pingpong sampler steps; 1 = single forward (fastest), 8 = sweet spot |
--seed |
random | Set for reproducibility; the chosen seed is printed at the end |
--cfg |
1.0 | Guidance scale; 1.0 = off, >1 toward prompt, <1 toward uncond. ≠1 runs cond+uncond each step |
--apg |
1.0 | Adaptive Projected Guidance; only matters when --cfg ≠ 1 |
--cfg-batched |
on | No-op on the rung DiTs (all batch=1): when --cfg ≠ 1 they always run a sequential cond+uncond dual-pass. Kept for interface parity with the MLX/TRT backends |
--lora |
— | A .safetensors LoRA adapter (SA3-native/underfit or PEFT) merged into the DiT, optional strength=S; repeat to stack. Requires --dit-precision fp32 (the int8 w8a8 rung can't be merged). See LoRA |
--lora-strength |
1.0 | Default strength for --lora adapters without their own strength= |
--init-audio |
— | WAV (any format via ffmpeg) input for audio-to-audio / inpaint |
--init-noise-level |
1.0 | σmax; 0.4–0.8 typical for variation, 1.0 = full regen, >1 = overshoot |
--inpaint-range |
— | START,END seconds; regenerate that span, keep the rest |
--threads |
8 | XNNPACK CPU threads (all TFLite models run on CPU) |
--free-models |
on | Free each model after its last use; --no-free-models keeps them resident |
--out / -o |
(auto) | Relative → output/<file>; absolute → as-is. 16-bit PCM stereo @ 44.1 kHz, trimmed to exactly --seconds |
--play |
off | After writing, play the WAV: afplay (macOS) / winsound (Windows) / aplay (Linux); Ctrl-C stops both |
Precision is per-family (see Precision variants):
the medium DiT defaults to w8a8 — cache-safe rung int8, ~equal quality to fp32 per
forward (cos 0.996) but far smaller and flat-RAM — the small DiTs default to fp32 (no
rung w8a8 yet), the SAME codec defaults to w8a8 (quality-free on the round-trip),
and T5Gemma is fp16. Every component also offers an fp32 tier (bit-exact; the pick
for CPUs without int8 acceleration, and required for LoRA merges). (See "Notes on the design".)
Apply a LoRA finetune by merging it into the DiT's weight buffers at load —
same adapters and semantics as the MLX backend's --lora:
# one adapter
./sa3 --dit medium --decoder same-l --prompt "progressive metal" \
--lora myband.safetensors
# per-adapter strength; stack several
./sa3 --dit medium --decoder same-l --prompt "..." \
--lora a.safetensors strength=0.8 --lora b.safetensors strength=0.5The adapter is a .safetensors file (SA3-native train_lora.py / underfit
output, or a PEFT adapter directory) — pickle .ckpt/.pt is refused. All nine
adapter types (lora, dora-rows/cols, bora, the four -xs) are supported; the base
must match --dit. The merge is written into a cached copy of the DiT under
models/tflite/lora_cache/ (keyed by adapter + strength content hash) and reused
on repeat runs, so the ~5–15 s patch cost is paid once. A medium fp32 cache entry
is ~5.4 GB — delete lora_cache/ to reclaim.
Requires the fp32 DiT. Every family now defaults to w8a8, but when you pass
--lora without pinning a precision the CLI auto-selects fp32 for you (it
prints a one-line note); you only hit the error below if you explicitly pin
w8a8 alongside --lora. LoRA on the int8 w8a8 rung isn't figured out yet —
it stores weights as calibrated int8, so merging would mean dequantize → add the
LoRA delta → re-quantize, and a naive re-quant throws away the quantization grid
(you'd get a model that's both LoRA-adapted and degraded). Doing it well needs a
quant pass over the merged weights; until then --lora refuses w8a8 (the CLI
errors, the web UI disables the LoRA panel). Use fp32 for LoRA — on CPU it's the
fast-and-accurate choice anyway, so this is rarely a real constraint. Per-step
gating (steps=) is also MLX-only — a frozen
TFLite graph merges weights once at load; use optimized/mlx for step-gated LoRA.
sa3_tflite/
├── sa3 ← shell wrapper (use this)
├── install.sh ← uv bootstrap (run once)
├── sa3.bat / install.bat ← Windows twins of sa3 / install.sh
├── bootstrap.sh ← one-line curl installer
├── README.md
├── requirements.txt ← ai_edge_litert, numpy, sentencepiece, soundfile, huggingface_hub
├── output/ ← default landing zone for generated WAVs
├── scripts/
│ ├── sa3_tflite.py ← orchestrator CLI (invoked by ./sa3)
│ ├── weights.py ← weights manifest + HF auto-download
│ ├── examples.py ← shared examples block (--help + post-install)
│ └── install.py ← install.sh's Python half (bundle picker)
└── models/
├── tokenizer.model ← SentencePiece model, BUNDLED (~4 MB; T5Gemma tflite is encoder-only)
├── defs/
│ └── tflite_pipeline.py ← Tokenizer + T5Gemma front-end + pingpong schedule + sampler + WAV
└── tflite/ ← .tflite models (lazy-downloaded; default bundle ~0.7 GB small / ~2.4 GB medium)
├── t5gemma/encoder_fp16.tflite 564 MB text encoder (fp16)
├── sa3-sm-music/dit_w8a8.tflite 0.5 GB small music DiT — DEFAULT (cache-safe rung int8)
├── sa3-sm-music/dit_fp32.tflite 1.8 GB small music DiT (fp32 reference; LoRA / A-B)
├── sa3-sm-sfx/dit_w8a8.tflite 0.5 GB small sfx DiT — DEFAULT (cache-safe rung int8)
├── sa3-sm-sfx/dit_fp32.tflite 1.8 GB small sfx DiT (fp32 reference; LoRA / A-B)
├── sa3-m/dit_w8a8.tflite 1.6 GB medium DiT — DEFAULT (cache-safe rung int8)
├── sa3-m/dit_fp32.tflite 5.8 GB medium DiT (fp32 reference; LoRA / A-B)
├── same-s/{enc,dec}_{fp32,w8a8}.tflite rung codec (fp32 ~0.22 GB · w8a8 ~0.07 GB); legacy/ = pre-rung
└── same-l/{enc,dec}_{fp32,w8a8}.tflite rung codec (fp32 ~1.75 GB · w8a8 ~0.5 GB); legacy/ = pre-rung
The DiT graphs are baked-I/O: the conditioner (prompt-padding + seconds embedder) and the patch/unpatch are compiled into the graph, so the DiT takes the raw T5Gemma output directly and the decoder emits audio directly. The two small DiTs share the SAME-S codec (bit-exact between checkpoints), so only one set of small-codec files is shipped.
Model files aren't bundled — they're pulled from
stabilityai/stable-audio-3-optimized (under tflite/…) on first use and
symlinked into models/tflite/ from the HF cache. No duplication. Anonymous
downloads work but are rate-limited; huggingface-cli login with a free read-only
token lifts the cap. The SentencePiece tokenizer (models/tokenizer.model, ~4 MB)
is the one weight that IS committed, since the .tflite T5Gemma is encoder-only.
- Baked-I/O rung graphs. Each
.tfliteis a single self-contained graph with the conditioner and patch/unpatch in-graph, so you feed raw T5 outputs and the graph handles prompt-padding + seconds-embedding. One file serves any--secondsby dispatching to the smallest fixed rung ≥ L (extra positions masked out of attention — exact, not tiled). The DiT rung takesx, gc, t5_hidden, t5_mask, seconds, local_add_cond, attn_mask; the per-step global-condgcis produced by the bundledgcondsignature in the same file. - Per-family precision (no longer fp32-only). The medium DiT now ships the
cache-safe rung
w8a8as its default: int8 weights with per-token dynamic activation quant, so on an int8-capable CPU (VNNI/AMX) the int8×int8 matmuls run faster than fp32, the file is ~¼ the size, and rung-shared weights keep RAM flat. int8 on the DiT is a different (not worse) sample — per-step error compounds over the 8 chaotic sampling steps into another plausible draw, not a degraded one — so the bit-exactfp32tier ships alongside it (for CPUs without int8 acceleration, LoRA merges, and A/B reference). All three families (medium + both small) now ship this rungfp32/w8a8pair; the old fp16/int8 varlen tiers (w16a32/w8a32/w8a8-dyn) are retired tolegacy/. T5Gemma is fp16 (numerically lossless there, and it halves that file). - Monotonic audio-to-audio schedule. The pingpong schedule applies the LogSNR
shift to the normalized
[1→0]grid, then scales by σmax, so audio-to-audio (σmax < 1) stays monotonically decreasing while keeping all N distilled steps. σmax = 1.0 (text-to-audio) is bit-identical to the classic schedule. - SAME codec = static rung models. Both SAME encoders and decoders are static
rung
.tflite(a ladder of fixed-size subgraphs sharing one copy of the weights, litert ≥ 2.2.0CompiledModel+ weight cache).RungEncoder/RungDecoderauto-dispatch the optimal rung per length and tile bit-exactly — no chunk-size knob, RAM flat. This replaced the dynamic-varlen graphs whose RAM blew up (SAME-L O(S²) → 16 GB@256; SAME-S → ~50 GB@8192). See docs/RUNGS.md. - CFG (
--cfg ≠ 1) combines a cond and an uncond velocity in denoised space (optional APG). The rung DiTs are static batch=1, so cond+uncond always run as a sequential dual-pass (like the TensorRT release, whose engine is static-batch=1);--cfg-batchedis a no-op, kept only for interface parity with the MLX/TRT backends. (The old variable-batch "batched CFG" fast-path retired with the varlen graphs.)
Model weights derived from Stability AI's Stable Audio 3 checkpoints. T5Gemma text encoder from Google.
Use of the Stable Audio 3 weights is governed by the Stability AI Community License. Please refer to the full terms at https://stability.ai/license.

