sdmux 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdmux-0.1.0/LICENSE +21 -0
- sdmux-0.1.0/PKG-INFO +274 -0
- sdmux-0.1.0/README.md +252 -0
- sdmux-0.1.0/pyproject.toml +40 -0
- sdmux-0.1.0/setup.cfg +4 -0
- sdmux-0.1.0/src/sdmux/__init__.py +65 -0
- sdmux-0.1.0/src/sdmux/backends/__init__.py +17 -0
- sdmux-0.1.0/src/sdmux/backends/a1111.py +308 -0
- sdmux-0.1.0/src/sdmux/backends/base.py +270 -0
- sdmux-0.1.0/src/sdmux/backends/comfyui.py +611 -0
- sdmux-0.1.0/src/sdmux/backends/invokeai.py +340 -0
- sdmux-0.1.0/src/sdmux/backends/swarmui.py +284 -0
- sdmux-0.1.0/src/sdmux/cli.py +218 -0
- sdmux-0.1.0/src/sdmux/comfygraph.py +392 -0
- sdmux-0.1.0/src/sdmux/errors.py +77 -0
- sdmux-0.1.0/src/sdmux/modelinfo/__init__.py +265 -0
- sdmux-0.1.0/src/sdmux/modelinfo/civitai.py +123 -0
- sdmux-0.1.0/src/sdmux/modelinfo/family.py +375 -0
- sdmux-0.1.0/src/sdmux/modelinfo/headers.py +344 -0
- sdmux-0.1.0/src/sdmux/modelinfo/lookups.py +254 -0
- sdmux-0.1.0/src/sdmux/modelinfo/profiles.py +106 -0
- sdmux-0.1.0/src/sdmux/modelinfo/rules.py +331 -0
- sdmux-0.1.0/src/sdmux/names.py +254 -0
- sdmux-0.1.0/src/sdmux/probe.py +140 -0
- sdmux-0.1.0/src/sdmux/resources/profiles.json +53 -0
- sdmux-0.1.0/src/sdmux/transport.py +268 -0
- sdmux-0.1.0/src/sdmux/types.py +179 -0
- sdmux-0.1.0/src/sdmux.egg-info/PKG-INFO +274 -0
- sdmux-0.1.0/src/sdmux.egg-info/SOURCES.txt +33 -0
- sdmux-0.1.0/src/sdmux.egg-info/dependency_links.txt +1 -0
- sdmux-0.1.0/src/sdmux.egg-info/entry_points.txt +2 -0
- sdmux-0.1.0/src/sdmux.egg-info/requires.txt +8 -0
- sdmux-0.1.0/src/sdmux.egg-info/top_level.txt +1 -0
- sdmux-0.1.0/tests/test_backends.py +610 -0
- sdmux-0.1.0/tests/test_identify.py +410 -0
sdmux-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deepratna Awale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sdmux-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sdmux
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One client for every local image generation backend, with checkpoint identification built in.
|
|
5
|
+
Author: Deepratna Awale
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/deepratna-awale/sd-mux
|
|
8
|
+
Keywords: stable-diffusion,comfyui,automatic1111,forge,sdnext,swarmui,invokeai,safetensors
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: requests>=2.25
|
|
16
|
+
Requires-Dist: pillow>=9
|
|
17
|
+
Provides-Extra: progress
|
|
18
|
+
Requires-Dist: websocket-client>=1.4; extra == "progress"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# sdmux
|
|
24
|
+
|
|
25
|
+
One Python client for every local image generation backend, with checkpoint identification built in.
|
|
26
|
+
|
|
27
|
+
- **One API** over AUTOMATIC1111, Forge / reForge, SD.Next, ComfyUI, SwarmUI and InvokeAI 5+.
|
|
28
|
+
- **One vocabulary in, native vocabulary out.** A1111 sampler and scheduler names (or each backend's own),
|
|
29
|
+
`<lora:name:weight>` in the prompt everywhere, loose model and LoRA names.
|
|
30
|
+
- **Typed errors.** Generation raises; it never returns `None`.
|
|
31
|
+
- **Remote-friendly.** Localhost, a LAN address, or an https proxy with a sub-path, a login and extra headers.
|
|
32
|
+
- **`identify()`** tells a model file's kind, architecture, family, prediction type and variant from its header,
|
|
33
|
+
sidecar files and metadata, and hands back a prompt profile with starting defaults.
|
|
34
|
+
|
|
35
|
+
Dependencies: `requests` and `pillow`. `websocket-client` (the `progress` extra) adds ComfyUI step progress.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e . # or: pip install -e ".[progress]"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import sdmux
|
|
45
|
+
|
|
46
|
+
sd = sdmux.connect("http://127.0.0.1:8188") # detects the backend
|
|
47
|
+
sd.kind # "comfyui"
|
|
48
|
+
sd.models(); sd.loras(); sd.samplers(); sd.schedulers(); sd.info()
|
|
49
|
+
|
|
50
|
+
r = sd.txt2img("1girl, smile <lora:detail:0.6>", negative="lowres",
|
|
51
|
+
model="waiIllustrious", width=1024, height=1024,
|
|
52
|
+
steps=28, cfg=6, sampler="Euler a", scheduler="Karras",
|
|
53
|
+
seed=42, clip_skip=2)
|
|
54
|
+
r.image, r.images, r.seed, r.parameters # PIL images + A1111-style infotext
|
|
55
|
+
r.save("out.png") # PNG with the infotext in its parameters chunk
|
|
56
|
+
|
|
57
|
+
sd.img2img(image, "prompt", denoise=0.5)
|
|
58
|
+
sd.inpaint(image, mask, "prompt", denoise=0.7) # white = repaint
|
|
59
|
+
sd.progress(); sd.interrupt() # interrupt() from another thread -> Cancelled
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Images can be PIL images, bytes, file paths or base64 strings. Every generation also accepts
|
|
63
|
+
`batch_size`, `vae`, `mask_blur`, `inpaint_full_res`, `inpaint_padding`, and `extra={...}` for
|
|
64
|
+
backend-native fields.
|
|
65
|
+
|
|
66
|
+
### Defaults come from the model
|
|
67
|
+
|
|
68
|
+
Settings a call leaves out (size, steps, cfg, sampler and scheduler, clip skip) are filled from the model's profile,
|
|
69
|
+
so the same call suits whatever model it runs on, on every backend:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
r = sd.txt2img("1girl, smile", model="waiIllustrious") # 1024x1024, 28 steps, cfg 6, Euler a, clip skip 2
|
|
73
|
+
r.defaults # {"steps": (28, "profile:waiIllustriousSDXL_v170.safetensors"), ...}: what sdmux filled, and from where
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Anything passed explicitly wins. A sampler given without a scheduler is not paired with the profile's scheduler.
|
|
77
|
+
img2img and inpaint keep the input image's size. Without `model=`, the profile is that of the model the backend
|
|
78
|
+
will use (A1111's loaded checkpoint, otherwise the first listed). When a model cannot be identified, or with
|
|
79
|
+
`sdmux.connect(url, defaults="fixed")`, missing size/steps/cfg become 512x512, 20 steps, cfg 7 and the server picks
|
|
80
|
+
sampler, scheduler and clip skip.
|
|
81
|
+
|
|
82
|
+
### Profiles: prompt helpers
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
p = sd.profile("waiIllustrious") # identify the listed model, then look up its profile
|
|
86
|
+
p.dialect # "danbooru"
|
|
87
|
+
r = sd.txt2img(p.prompt("1girl, smile"), negative=p.negative_prompt("lowres"), model="waiIllustrious")
|
|
88
|
+
# p.prompt() adds the family's quality tags ("masterpiece, best quality, amazing quality"); settings come from p.defaults automatically
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Profiles are **starting points, not tuned truth**. Quality tags and negatives are seeded from
|
|
92
|
+
AutoExpress's per-family prompt sets.
|
|
93
|
+
|
|
94
|
+
### Remote servers
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
sdmux.connect("https://user:pass@abc123-7860.proxy.runpod.net/sub/path")
|
|
98
|
+
sdmux.connect(url, token="...") # bearer token (InvokeAI, SwarmUI's swarm_token)
|
|
99
|
+
sdmux.connect(url, headers={"CF-Access-Client-Id": "...", "CF-Access-Client-Secret": "..."})
|
|
100
|
+
sdmux.connect(url, timeout=60, job_timeout=3600, retries=3, verify_tls=False)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Credentials never appear in error messages (`***@host`), and extra headers are dropped if a redirect leaves the host.
|
|
104
|
+
|
|
105
|
+
### Errors
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
try:
|
|
109
|
+
sd.txt2img("a cat", model="nope")
|
|
110
|
+
except sdmux.Unreachable: ... # nothing answers
|
|
111
|
+
except sdmux.TimedOut: ... # connected, no answer in time (or job_timeout passed)
|
|
112
|
+
except sdmux.AuthRefused: ... # 401/403 or a failed login
|
|
113
|
+
except sdmux.NotReady: ... # up, but not a supported backend / missing what the request needs
|
|
114
|
+
except sdmux.Rejected: ... # the backend refused or failed this request (unknown model, validation error, OOM)
|
|
115
|
+
except sdmux.Cancelled: ... # interrupt() stopped it
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`Unsupported` (a `NotReady`) means sdmux cannot build that request for that backend at all.
|
|
119
|
+
All errors derive from `sdmux.SdmuxError` and carry `.code`, `.backend`, `.details`.
|
|
120
|
+
|
|
121
|
+
### ComfyUI: any node, typed and checked
|
|
122
|
+
|
|
123
|
+
ComfyUI describes every installed node, custom packs included, at `/object_info`. sdmux turns that into typed specs and
|
|
124
|
+
a graph builder, so any node is usable without hand-written JSON:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
sd.nodes() # every node: .name, .category, .pack ("core" or the custom pack)
|
|
128
|
+
print(sd.node("FaceDetailer")) # inputs with type, default, range, options; named outputs
|
|
129
|
+
|
|
130
|
+
g = sd.graph()
|
|
131
|
+
ckpt = g.add("CheckpointLoaderSimple", ckpt_name="waiIllustrious") # file names matched loosely
|
|
132
|
+
pos = g.add("CLIPTextEncode", text="1girl", clip=ckpt.CLIP) # outputs by name, index (ckpt[2]) or the node itself
|
|
133
|
+
neg = g.add("CLIPTextEncode", text="lowres", clip=ckpt.CLIP)
|
|
134
|
+
lat = g.add("EmptyLatentImage", width=1024, height=1024) # missing inputs get the server's defaults
|
|
135
|
+
smp = g.add("KSampler", model=ckpt.MODEL, positive=pos, negative=neg, latent_image=lat, steps=28)
|
|
136
|
+
img = g.add("VAEDecode", samples=smp, vae=ckpt.VAE)
|
|
137
|
+
det = g.add("UltralyticsDetectorProvider")
|
|
138
|
+
fd = g.add("FaceDetailer", image=img, model=ckpt.MODEL, clip=ckpt.CLIP, vae=ckpt.VAE,
|
|
139
|
+
positive=pos, negative=neg, bbox_detector=det.BBOX_DETECTOR)
|
|
140
|
+
g.add("PreviewImage", images=fd.image)
|
|
141
|
+
r = sd.run(g) # r.images; r.raw["outputs"] maps each output node to its images
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Before anything is queued, `sd.run()` checks the graph against the server and raises `sdmux.GraphError` listing every
|
|
145
|
+
problem: unknown node (pack not installed) or input, missing required input, link of the wrong type, number out of
|
|
146
|
+
range, value not among the options.
|
|
147
|
+
|
|
148
|
+
Splice nodes into sdmux's own graphs with `patch=`, or get the graph without running it:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
def upscale(g):
|
|
152
|
+
up = g.add("ImageUpscaleWithModel", upscale_model=g.add("UpscaleModelLoader", model_name="4x-AnimeSharp"),
|
|
153
|
+
image=g.ref("decode"))
|
|
154
|
+
g.ref("output").set(images=up) # node "output" is what txt2img returns
|
|
155
|
+
|
|
156
|
+
sd.txt2img("1girl", model="waiIllustrious", patch=upscale)
|
|
157
|
+
g = sd.workflow(prompt="1girl", model="waiIllustrious") # a Graph; g.to_api() for the raw dict
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Escape hatches
|
|
161
|
+
|
|
162
|
+
| Backend | Native path |
|
|
163
|
+
|---|---|
|
|
164
|
+
| A1111 / Forge / SD.Next | `sd.api("txt2img", payload)` posts to any `/sdapi/v1` route; `extra=` merges into the payload |
|
|
165
|
+
| ComfyUI | `sd.graph()` + `sd.run()` for any node; `sd.run_workflow(api_format_dict)`; also `upload()`, `queue()`, `wait()`, `fetch()`, `object_info()` |
|
|
166
|
+
| SwarmUI | `sd.call("Route", body)` with a live session; `extra=` merges into `GenerateText2Image` |
|
|
167
|
+
| InvokeAI | `sd.enqueue_graph(graph)`, `sd.wait(item_id)`, `sd.fetch(item)`, `sd.upload(image)` |
|
|
168
|
+
|
|
169
|
+
## Checkpoint identification
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
info = sdmux.identify("model.safetensors") # header only; never loads weights, never unpickles
|
|
173
|
+
info.kind # checkpoint | diffusion_model | lora | vae | controlnet | text_encoder | embedding | unknown
|
|
174
|
+
info.architecture # sd15 | sd2 | sdxl | sdxl-refiner | sd3 | flux | flux2 | chroma | qwen-image | wan | lumina2 | ...
|
|
175
|
+
info.family # pony | illustrious | noobai | animagine | anime | furry | sdxl | ... (None when unknown)
|
|
176
|
+
info.prediction # epsilon | v | flow | edm | None
|
|
177
|
+
info.variant # inpaint | instruct | dev | schnell | fill | turbo | lightning | ...
|
|
178
|
+
info.evidence # [Evidence(field, value, source, detail, confidence)]
|
|
179
|
+
info.conflicts # where sources disagreed, and what was kept
|
|
180
|
+
info.profile # dialect + defaults
|
|
181
|
+
|
|
182
|
+
sdmux.identify(path, online=True) # adds by-hash lookups on Civitai, CivArchive, modelindex.dev, Hugging Face
|
|
183
|
+
info.links # where the file is published: civitai.com/.red, Tensor.Art, SeaArt, PixAI, Hugging Face, ...
|
|
184
|
+
sd.identify("hassaku") # a model the backend lists: reads the file when it is on this machine
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Formats: `.safetensors`, `.gguf`, and torch `.ckpt` / `.pt` (zip or legacy). Pickle files are walked opcode by opcode
|
|
188
|
+
by a data-only stack machine; nothing in them is imported or executed.
|
|
189
|
+
|
|
190
|
+
Family sources, most trusted first: sidecar files (`.metadata.json` from ComfyUI LoRA Manager, `.civitai.info`,
|
|
191
|
+
`.civitai.json`, StabilityMatrix `.cm-info.json`, Civitai Browser+/Helper `.json`), online lookups (only with
|
|
192
|
+
`online=True`), `__metadata__` (`modelspec.*`, kohya `ss_*`, merge recipes), then the file name. Tensors decide kind
|
|
193
|
+
and architecture.
|
|
194
|
+
|
|
195
|
+
Online lookups (sha256 reused from a sidecar or A1111's hash cache when there is one, otherwise computed once and
|
|
196
|
+
cached by path, size and mtime; every answer is cached):
|
|
197
|
+
|
|
198
|
+
| Service | What it adds |
|
|
199
|
+
|---|---|
|
|
200
|
+
| Civitai (`civitai.com`, then `civitai.red`) | `baseModel` -> architecture/family/variant, model and version ids, trigger words |
|
|
201
|
+
| CivArchive (`civarchive.com`) | models on Tensor.Art, SeaArt, Shakker, PixAI, Yodayo/Moescape, Tungsten, ... and models deleted from Civitai; `baseModel` and mirror links |
|
|
202
|
+
| modelindex.dev | Hugging Face and ModelScope repositories hosting the exact file |
|
|
203
|
+
| Hugging Face model card | `base_model` lineage and diffusers pipeline (architecture) for those repositories |
|
|
204
|
+
|
|
205
|
+
Pick services with `identify(path, online=True, sources=("civitai", "civarchive"))`, `sdmux identify --sources ...`,
|
|
206
|
+
or `SDMUX_LOOKUPS=civitai,huggingface`. Hugging Face, ModelScope, Tensor.Art, SeaArt and Shakker have no public
|
|
207
|
+
by-hash search of their own, which is why the aggregators are used.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
sdmux detect http://127.0.0.1:7860
|
|
211
|
+
sdmux models http://127.0.0.1:8188 [--loras] [--json]
|
|
212
|
+
sdmux nodes http://127.0.0.1:8188 [FaceDetailer] [--pack ComfyUI-Impact-Pack]
|
|
213
|
+
sdmux identify ~/models/checkpoints -r [--online] [--json] [-v]
|
|
214
|
+
sdmux txt2img http://127.0.0.1:7860 "a red apple" -o apple.png --model hassaku --steps 20
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Support matrix
|
|
218
|
+
|
|
219
|
+
**Live** = exercised against a real server on an Apple M4 (MPS) on 2026-11-12 with an SD 1.5 checkpoint and a LoRA
|
|
220
|
+
(ComfyUI also with an SDXL checkpoint). **Mock** = covered only by the offline fake-server tests.
|
|
221
|
+
|
|
222
|
+
| | A1111 1.10.1 | Forge 2.0.1 | reForge | SD.Next | ComfyUI 0.41 | SwarmUI 0.9.8.4 | InvokeAI 6.14.4 |
|
|
223
|
+
|---|---|---|---|---|---|---|---|
|
|
224
|
+
| Detection | Live | Live | Live (as `forge`) | Live | Live | Live | Live |
|
|
225
|
+
| Models / LoRAs / samplers / schedulers | Live | Live | Live | Live | Live | Live | Live (no separate schedulers) |
|
|
226
|
+
| txt2img (+ LoRA, clip skip, sampler/scheduler mapping) | Live | Live | Live | Live | Live | Live | Live |
|
|
227
|
+
| img2img | Live | Live | Live | Live | Live | Live | Live |
|
|
228
|
+
| inpaint (white = repaint) | Live | Live | Live | Live | Live | Live | Live |
|
|
229
|
+
| interrupt -> `Cancelled` | Live | Live | Live | Live | Live | Live | Live |
|
|
230
|
+
| progress | Live (steps) | Live (steps) | Live (steps) | Live (steps) | Live (active); websocket steps not verified | Live (active only) | Live (active only) |
|
|
231
|
+
| Login | Mock (basic) | Mock (basic) | Mock (basic) | Mock (basic) | Mock (basic/bearer) | Mock (`/API/Login`, token) | Mock (JWT) |
|
|
232
|
+
| `identify()` of listed models | Live (local paths) | Live | Live | Live | Live (local paths) | Live (name + reported arch) | Live (local paths) |
|
|
233
|
+
|
|
234
|
+
Model coverage for generation:
|
|
235
|
+
|
|
236
|
+
- **A1111 family, SwarmUI**: whatever the backend can load (sdmux passes the model name through).
|
|
237
|
+
- **ComfyUI**: single-file checkpoints (SD 1.x, SD 2.x, SDXL) are built with core nodes and live-verified.
|
|
238
|
+
Split-file models in `models/diffusion_models` (Flux, Flux.2, Chroma, Qwen-Image, SD3, Lumina 2) get
|
|
239
|
+
architecture-specific loaders chosen by `identify()`; those graphs are **mock-only** (no such models on the test machine).
|
|
240
|
+
Anything else raises `Unsupported`; use `run_workflow()`.
|
|
241
|
+
- **InvokeAI**: SD 1.x, SD 2.x and SDXL main models. Flux, SD3 and others use other node families in InvokeAI
|
|
242
|
+
and raise `Unsupported`; use `enqueue_graph()`.
|
|
243
|
+
|
|
244
|
+
Not built (room is left in `Request.extra`): hires-fix, refiner passes, ControlNet, IP-Adapter, upscalers, video.
|
|
245
|
+
|
|
246
|
+
## Backend notes
|
|
247
|
+
|
|
248
|
+
- **A1111 / Forge / reForge**: start with `--api`. A model chosen with `model=` stays loaded afterwards
|
|
249
|
+
(`override_settings_restore_afterwards: false`); A1111's default would reload the previous model after every call.
|
|
250
|
+
The `scheduler` field needs A1111 1.10+; on older versions the scheduler is appended to the sampler name.
|
|
251
|
+
An interrupted job returns a partial image on these servers; sdmux raises `Cancelled` and keeps it in `details`.
|
|
252
|
+
- **SD.Next**: schedulers are diffusers classes picked by the sampler; the A1111 scheduler maps to SD.Next's
|
|
253
|
+
"Sigma algorithm" (`schedulers_sigma`: karras, exponential, betas, ...). Models are switched through `/sdapi/v1/options`.
|
|
254
|
+
- **ComfyUI**: LoRAs become `LoraLoader` nodes, clip skip `CLIPSetLastLayer`, inpaint uses `SetLatentNoiseMask`
|
|
255
|
+
and composites the original back outside the mask. `/view` downloads are retried when a proxy drops them.
|
|
256
|
+
- **SwarmUI**: LoRAs stay in the prompt. With `donotsave` the image carries no seed, so sdmux resolves the seed itself.
|
|
257
|
+
Mask blur needs SwarmUI's own ComfyUI nodes; on a ComfyUI backend without them it is skipped.
|
|
258
|
+
- **InvokeAI**: the mask is inverted for `create_gradient_mask` (which repaints black), and the original pixels are
|
|
259
|
+
composited back outside the mask.
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
pip install -e ".[test,progress]"
|
|
265
|
+
pytest # offline: fake servers + synthetic headers, no GPU, no network
|
|
266
|
+
python tests/live_check.py http://127.0.0.1:7860 --model hassakuSD15_v13 --lora some_lora --out /tmp/live
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
See `docs/api-coverage.md` for every native route and auth method per backend, `docs/backends-setup.md` for the local UI installs used for live checks, and `docs/verification.md` for what was
|
|
270
|
+
checked against which source.
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
sdmux-0.1.0/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# sdmux
|
|
2
|
+
|
|
3
|
+
One Python client for every local image generation backend, with checkpoint identification built in.
|
|
4
|
+
|
|
5
|
+
- **One API** over AUTOMATIC1111, Forge / reForge, SD.Next, ComfyUI, SwarmUI and InvokeAI 5+.
|
|
6
|
+
- **One vocabulary in, native vocabulary out.** A1111 sampler and scheduler names (or each backend's own),
|
|
7
|
+
`<lora:name:weight>` in the prompt everywhere, loose model and LoRA names.
|
|
8
|
+
- **Typed errors.** Generation raises; it never returns `None`.
|
|
9
|
+
- **Remote-friendly.** Localhost, a LAN address, or an https proxy with a sub-path, a login and extra headers.
|
|
10
|
+
- **`identify()`** tells a model file's kind, architecture, family, prediction type and variant from its header,
|
|
11
|
+
sidecar files and metadata, and hands back a prompt profile with starting defaults.
|
|
12
|
+
|
|
13
|
+
Dependencies: `requests` and `pillow`. `websocket-client` (the `progress` extra) adds ComfyUI step progress.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e . # or: pip install -e ".[progress]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import sdmux
|
|
23
|
+
|
|
24
|
+
sd = sdmux.connect("http://127.0.0.1:8188") # detects the backend
|
|
25
|
+
sd.kind # "comfyui"
|
|
26
|
+
sd.models(); sd.loras(); sd.samplers(); sd.schedulers(); sd.info()
|
|
27
|
+
|
|
28
|
+
r = sd.txt2img("1girl, smile <lora:detail:0.6>", negative="lowres",
|
|
29
|
+
model="waiIllustrious", width=1024, height=1024,
|
|
30
|
+
steps=28, cfg=6, sampler="Euler a", scheduler="Karras",
|
|
31
|
+
seed=42, clip_skip=2)
|
|
32
|
+
r.image, r.images, r.seed, r.parameters # PIL images + A1111-style infotext
|
|
33
|
+
r.save("out.png") # PNG with the infotext in its parameters chunk
|
|
34
|
+
|
|
35
|
+
sd.img2img(image, "prompt", denoise=0.5)
|
|
36
|
+
sd.inpaint(image, mask, "prompt", denoise=0.7) # white = repaint
|
|
37
|
+
sd.progress(); sd.interrupt() # interrupt() from another thread -> Cancelled
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Images can be PIL images, bytes, file paths or base64 strings. Every generation also accepts
|
|
41
|
+
`batch_size`, `vae`, `mask_blur`, `inpaint_full_res`, `inpaint_padding`, and `extra={...}` for
|
|
42
|
+
backend-native fields.
|
|
43
|
+
|
|
44
|
+
### Defaults come from the model
|
|
45
|
+
|
|
46
|
+
Settings a call leaves out (size, steps, cfg, sampler and scheduler, clip skip) are filled from the model's profile,
|
|
47
|
+
so the same call suits whatever model it runs on, on every backend:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
r = sd.txt2img("1girl, smile", model="waiIllustrious") # 1024x1024, 28 steps, cfg 6, Euler a, clip skip 2
|
|
51
|
+
r.defaults # {"steps": (28, "profile:waiIllustriousSDXL_v170.safetensors"), ...}: what sdmux filled, and from where
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Anything passed explicitly wins. A sampler given without a scheduler is not paired with the profile's scheduler.
|
|
55
|
+
img2img and inpaint keep the input image's size. Without `model=`, the profile is that of the model the backend
|
|
56
|
+
will use (A1111's loaded checkpoint, otherwise the first listed). When a model cannot be identified, or with
|
|
57
|
+
`sdmux.connect(url, defaults="fixed")`, missing size/steps/cfg become 512x512, 20 steps, cfg 7 and the server picks
|
|
58
|
+
sampler, scheduler and clip skip.
|
|
59
|
+
|
|
60
|
+
### Profiles: prompt helpers
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
p = sd.profile("waiIllustrious") # identify the listed model, then look up its profile
|
|
64
|
+
p.dialect # "danbooru"
|
|
65
|
+
r = sd.txt2img(p.prompt("1girl, smile"), negative=p.negative_prompt("lowres"), model="waiIllustrious")
|
|
66
|
+
# p.prompt() adds the family's quality tags ("masterpiece, best quality, amazing quality"); settings come from p.defaults automatically
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Profiles are **starting points, not tuned truth**. Quality tags and negatives are seeded from
|
|
70
|
+
AutoExpress's per-family prompt sets.
|
|
71
|
+
|
|
72
|
+
### Remote servers
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
sdmux.connect("https://user:pass@abc123-7860.proxy.runpod.net/sub/path")
|
|
76
|
+
sdmux.connect(url, token="...") # bearer token (InvokeAI, SwarmUI's swarm_token)
|
|
77
|
+
sdmux.connect(url, headers={"CF-Access-Client-Id": "...", "CF-Access-Client-Secret": "..."})
|
|
78
|
+
sdmux.connect(url, timeout=60, job_timeout=3600, retries=3, verify_tls=False)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Credentials never appear in error messages (`***@host`), and extra headers are dropped if a redirect leaves the host.
|
|
82
|
+
|
|
83
|
+
### Errors
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
try:
|
|
87
|
+
sd.txt2img("a cat", model="nope")
|
|
88
|
+
except sdmux.Unreachable: ... # nothing answers
|
|
89
|
+
except sdmux.TimedOut: ... # connected, no answer in time (or job_timeout passed)
|
|
90
|
+
except sdmux.AuthRefused: ... # 401/403 or a failed login
|
|
91
|
+
except sdmux.NotReady: ... # up, but not a supported backend / missing what the request needs
|
|
92
|
+
except sdmux.Rejected: ... # the backend refused or failed this request (unknown model, validation error, OOM)
|
|
93
|
+
except sdmux.Cancelled: ... # interrupt() stopped it
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`Unsupported` (a `NotReady`) means sdmux cannot build that request for that backend at all.
|
|
97
|
+
All errors derive from `sdmux.SdmuxError` and carry `.code`, `.backend`, `.details`.
|
|
98
|
+
|
|
99
|
+
### ComfyUI: any node, typed and checked
|
|
100
|
+
|
|
101
|
+
ComfyUI describes every installed node, custom packs included, at `/object_info`. sdmux turns that into typed specs and
|
|
102
|
+
a graph builder, so any node is usable without hand-written JSON:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
sd.nodes() # every node: .name, .category, .pack ("core" or the custom pack)
|
|
106
|
+
print(sd.node("FaceDetailer")) # inputs with type, default, range, options; named outputs
|
|
107
|
+
|
|
108
|
+
g = sd.graph()
|
|
109
|
+
ckpt = g.add("CheckpointLoaderSimple", ckpt_name="waiIllustrious") # file names matched loosely
|
|
110
|
+
pos = g.add("CLIPTextEncode", text="1girl", clip=ckpt.CLIP) # outputs by name, index (ckpt[2]) or the node itself
|
|
111
|
+
neg = g.add("CLIPTextEncode", text="lowres", clip=ckpt.CLIP)
|
|
112
|
+
lat = g.add("EmptyLatentImage", width=1024, height=1024) # missing inputs get the server's defaults
|
|
113
|
+
smp = g.add("KSampler", model=ckpt.MODEL, positive=pos, negative=neg, latent_image=lat, steps=28)
|
|
114
|
+
img = g.add("VAEDecode", samples=smp, vae=ckpt.VAE)
|
|
115
|
+
det = g.add("UltralyticsDetectorProvider")
|
|
116
|
+
fd = g.add("FaceDetailer", image=img, model=ckpt.MODEL, clip=ckpt.CLIP, vae=ckpt.VAE,
|
|
117
|
+
positive=pos, negative=neg, bbox_detector=det.BBOX_DETECTOR)
|
|
118
|
+
g.add("PreviewImage", images=fd.image)
|
|
119
|
+
r = sd.run(g) # r.images; r.raw["outputs"] maps each output node to its images
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Before anything is queued, `sd.run()` checks the graph against the server and raises `sdmux.GraphError` listing every
|
|
123
|
+
problem: unknown node (pack not installed) or input, missing required input, link of the wrong type, number out of
|
|
124
|
+
range, value not among the options.
|
|
125
|
+
|
|
126
|
+
Splice nodes into sdmux's own graphs with `patch=`, or get the graph without running it:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
def upscale(g):
|
|
130
|
+
up = g.add("ImageUpscaleWithModel", upscale_model=g.add("UpscaleModelLoader", model_name="4x-AnimeSharp"),
|
|
131
|
+
image=g.ref("decode"))
|
|
132
|
+
g.ref("output").set(images=up) # node "output" is what txt2img returns
|
|
133
|
+
|
|
134
|
+
sd.txt2img("1girl", model="waiIllustrious", patch=upscale)
|
|
135
|
+
g = sd.workflow(prompt="1girl", model="waiIllustrious") # a Graph; g.to_api() for the raw dict
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Escape hatches
|
|
139
|
+
|
|
140
|
+
| Backend | Native path |
|
|
141
|
+
|---|---|
|
|
142
|
+
| A1111 / Forge / SD.Next | `sd.api("txt2img", payload)` posts to any `/sdapi/v1` route; `extra=` merges into the payload |
|
|
143
|
+
| ComfyUI | `sd.graph()` + `sd.run()` for any node; `sd.run_workflow(api_format_dict)`; also `upload()`, `queue()`, `wait()`, `fetch()`, `object_info()` |
|
|
144
|
+
| SwarmUI | `sd.call("Route", body)` with a live session; `extra=` merges into `GenerateText2Image` |
|
|
145
|
+
| InvokeAI | `sd.enqueue_graph(graph)`, `sd.wait(item_id)`, `sd.fetch(item)`, `sd.upload(image)` |
|
|
146
|
+
|
|
147
|
+
## Checkpoint identification
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
info = sdmux.identify("model.safetensors") # header only; never loads weights, never unpickles
|
|
151
|
+
info.kind # checkpoint | diffusion_model | lora | vae | controlnet | text_encoder | embedding | unknown
|
|
152
|
+
info.architecture # sd15 | sd2 | sdxl | sdxl-refiner | sd3 | flux | flux2 | chroma | qwen-image | wan | lumina2 | ...
|
|
153
|
+
info.family # pony | illustrious | noobai | animagine | anime | furry | sdxl | ... (None when unknown)
|
|
154
|
+
info.prediction # epsilon | v | flow | edm | None
|
|
155
|
+
info.variant # inpaint | instruct | dev | schnell | fill | turbo | lightning | ...
|
|
156
|
+
info.evidence # [Evidence(field, value, source, detail, confidence)]
|
|
157
|
+
info.conflicts # where sources disagreed, and what was kept
|
|
158
|
+
info.profile # dialect + defaults
|
|
159
|
+
|
|
160
|
+
sdmux.identify(path, online=True) # adds by-hash lookups on Civitai, CivArchive, modelindex.dev, Hugging Face
|
|
161
|
+
info.links # where the file is published: civitai.com/.red, Tensor.Art, SeaArt, PixAI, Hugging Face, ...
|
|
162
|
+
sd.identify("hassaku") # a model the backend lists: reads the file when it is on this machine
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Formats: `.safetensors`, `.gguf`, and torch `.ckpt` / `.pt` (zip or legacy). Pickle files are walked opcode by opcode
|
|
166
|
+
by a data-only stack machine; nothing in them is imported or executed.
|
|
167
|
+
|
|
168
|
+
Family sources, most trusted first: sidecar files (`.metadata.json` from ComfyUI LoRA Manager, `.civitai.info`,
|
|
169
|
+
`.civitai.json`, StabilityMatrix `.cm-info.json`, Civitai Browser+/Helper `.json`), online lookups (only with
|
|
170
|
+
`online=True`), `__metadata__` (`modelspec.*`, kohya `ss_*`, merge recipes), then the file name. Tensors decide kind
|
|
171
|
+
and architecture.
|
|
172
|
+
|
|
173
|
+
Online lookups (sha256 reused from a sidecar or A1111's hash cache when there is one, otherwise computed once and
|
|
174
|
+
cached by path, size and mtime; every answer is cached):
|
|
175
|
+
|
|
176
|
+
| Service | What it adds |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Civitai (`civitai.com`, then `civitai.red`) | `baseModel` -> architecture/family/variant, model and version ids, trigger words |
|
|
179
|
+
| CivArchive (`civarchive.com`) | models on Tensor.Art, SeaArt, Shakker, PixAI, Yodayo/Moescape, Tungsten, ... and models deleted from Civitai; `baseModel` and mirror links |
|
|
180
|
+
| modelindex.dev | Hugging Face and ModelScope repositories hosting the exact file |
|
|
181
|
+
| Hugging Face model card | `base_model` lineage and diffusers pipeline (architecture) for those repositories |
|
|
182
|
+
|
|
183
|
+
Pick services with `identify(path, online=True, sources=("civitai", "civarchive"))`, `sdmux identify --sources ...`,
|
|
184
|
+
or `SDMUX_LOOKUPS=civitai,huggingface`. Hugging Face, ModelScope, Tensor.Art, SeaArt and Shakker have no public
|
|
185
|
+
by-hash search of their own, which is why the aggregators are used.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
sdmux detect http://127.0.0.1:7860
|
|
189
|
+
sdmux models http://127.0.0.1:8188 [--loras] [--json]
|
|
190
|
+
sdmux nodes http://127.0.0.1:8188 [FaceDetailer] [--pack ComfyUI-Impact-Pack]
|
|
191
|
+
sdmux identify ~/models/checkpoints -r [--online] [--json] [-v]
|
|
192
|
+
sdmux txt2img http://127.0.0.1:7860 "a red apple" -o apple.png --model hassaku --steps 20
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Support matrix
|
|
196
|
+
|
|
197
|
+
**Live** = exercised against a real server on an Apple M4 (MPS) on 2026-11-12 with an SD 1.5 checkpoint and a LoRA
|
|
198
|
+
(ComfyUI also with an SDXL checkpoint). **Mock** = covered only by the offline fake-server tests.
|
|
199
|
+
|
|
200
|
+
| | A1111 1.10.1 | Forge 2.0.1 | reForge | SD.Next | ComfyUI 0.41 | SwarmUI 0.9.8.4 | InvokeAI 6.14.4 |
|
|
201
|
+
|---|---|---|---|---|---|---|---|
|
|
202
|
+
| Detection | Live | Live | Live (as `forge`) | Live | Live | Live | Live |
|
|
203
|
+
| Models / LoRAs / samplers / schedulers | Live | Live | Live | Live | Live | Live | Live (no separate schedulers) |
|
|
204
|
+
| txt2img (+ LoRA, clip skip, sampler/scheduler mapping) | Live | Live | Live | Live | Live | Live | Live |
|
|
205
|
+
| img2img | Live | Live | Live | Live | Live | Live | Live |
|
|
206
|
+
| inpaint (white = repaint) | Live | Live | Live | Live | Live | Live | Live |
|
|
207
|
+
| interrupt -> `Cancelled` | Live | Live | Live | Live | Live | Live | Live |
|
|
208
|
+
| progress | Live (steps) | Live (steps) | Live (steps) | Live (steps) | Live (active); websocket steps not verified | Live (active only) | Live (active only) |
|
|
209
|
+
| Login | Mock (basic) | Mock (basic) | Mock (basic) | Mock (basic) | Mock (basic/bearer) | Mock (`/API/Login`, token) | Mock (JWT) |
|
|
210
|
+
| `identify()` of listed models | Live (local paths) | Live | Live | Live | Live (local paths) | Live (name + reported arch) | Live (local paths) |
|
|
211
|
+
|
|
212
|
+
Model coverage for generation:
|
|
213
|
+
|
|
214
|
+
- **A1111 family, SwarmUI**: whatever the backend can load (sdmux passes the model name through).
|
|
215
|
+
- **ComfyUI**: single-file checkpoints (SD 1.x, SD 2.x, SDXL) are built with core nodes and live-verified.
|
|
216
|
+
Split-file models in `models/diffusion_models` (Flux, Flux.2, Chroma, Qwen-Image, SD3, Lumina 2) get
|
|
217
|
+
architecture-specific loaders chosen by `identify()`; those graphs are **mock-only** (no such models on the test machine).
|
|
218
|
+
Anything else raises `Unsupported`; use `run_workflow()`.
|
|
219
|
+
- **InvokeAI**: SD 1.x, SD 2.x and SDXL main models. Flux, SD3 and others use other node families in InvokeAI
|
|
220
|
+
and raise `Unsupported`; use `enqueue_graph()`.
|
|
221
|
+
|
|
222
|
+
Not built (room is left in `Request.extra`): hires-fix, refiner passes, ControlNet, IP-Adapter, upscalers, video.
|
|
223
|
+
|
|
224
|
+
## Backend notes
|
|
225
|
+
|
|
226
|
+
- **A1111 / Forge / reForge**: start with `--api`. A model chosen with `model=` stays loaded afterwards
|
|
227
|
+
(`override_settings_restore_afterwards: false`); A1111's default would reload the previous model after every call.
|
|
228
|
+
The `scheduler` field needs A1111 1.10+; on older versions the scheduler is appended to the sampler name.
|
|
229
|
+
An interrupted job returns a partial image on these servers; sdmux raises `Cancelled` and keeps it in `details`.
|
|
230
|
+
- **SD.Next**: schedulers are diffusers classes picked by the sampler; the A1111 scheduler maps to SD.Next's
|
|
231
|
+
"Sigma algorithm" (`schedulers_sigma`: karras, exponential, betas, ...). Models are switched through `/sdapi/v1/options`.
|
|
232
|
+
- **ComfyUI**: LoRAs become `LoraLoader` nodes, clip skip `CLIPSetLastLayer`, inpaint uses `SetLatentNoiseMask`
|
|
233
|
+
and composites the original back outside the mask. `/view` downloads are retried when a proxy drops them.
|
|
234
|
+
- **SwarmUI**: LoRAs stay in the prompt. With `donotsave` the image carries no seed, so sdmux resolves the seed itself.
|
|
235
|
+
Mask blur needs SwarmUI's own ComfyUI nodes; on a ComfyUI backend without them it is skipped.
|
|
236
|
+
- **InvokeAI**: the mask is inverted for `create_gradient_mask` (which repaints black), and the original pixels are
|
|
237
|
+
composited back outside the mask.
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
pip install -e ".[test,progress]"
|
|
243
|
+
pytest # offline: fake servers + synthetic headers, no GPU, no network
|
|
244
|
+
python tests/live_check.py http://127.0.0.1:7860 --model hassakuSD15_v13 --lora some_lora --out /tmp/live
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
See `docs/api-coverage.md` for every native route and auth method per backend, `docs/backends-setup.md` for the local UI installs used for live checks, and `docs/verification.md` for what was
|
|
248
|
+
checked against which source.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sdmux"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One client for every local image generation backend, with checkpoint identification built in."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Deepratna Awale" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["requests>=2.25", "pillow>=9"]
|
|
14
|
+
keywords = ["stable-diffusion", "comfyui", "automatic1111", "forge", "sdnext", "swarmui", "invokeai", "safetensors"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Multimedia :: Graphics",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
progress = ["websocket-client>=1.4"]
|
|
23
|
+
test = ["pytest>=7"]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
sdmux = "sdmux.cli:main"
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/deepratna-awale/sd-mux"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
sdmux = ["resources/*.json"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
markers = ["live: needs a running backend (set SDMUX_LIVE_URL)"]
|
|
40
|
+
addopts = "-m 'not live'"
|
sdmux-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sdmux: one client for every local image generation backend, with checkpoint identification built in.
|
|
3
|
+
|
|
4
|
+
import sdmux
|
|
5
|
+
|
|
6
|
+
sd = sdmux.connect("http://127.0.0.1:8188") # detects ComfyUI, A1111, Forge, SD.Next, SwarmUI or InvokeAI
|
|
7
|
+
r = sd.txt2img("1girl, smile <lora:detail:0.6>", model="waiIllustrious", steps=28, cfg=6,
|
|
8
|
+
sampler="Euler a", scheduler="Karras", seed=42, clip_skip=2)
|
|
9
|
+
r.image.save("out.png")
|
|
10
|
+
|
|
11
|
+
info = sdmux.identify("model.safetensors") # reads only the header
|
|
12
|
+
info.architecture, info.family, info.profile
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
from .errors import AuthRefused, Cancelled, NotReady, Rejected, SdmuxError, TimedOut, Unreachable, Unsupported # noqa: E402
|
|
18
|
+
from .transport import Options # noqa: E402
|
|
19
|
+
from .types import Model, Progress, Request, Result # noqa: E402
|
|
20
|
+
from .comfygraph import Graph, GraphError, NodeSpec # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def connect(url: str, kind: str | None = None, options: Options | None = None, defaults: str = "profile",
|
|
24
|
+
**option_overrides):
|
|
25
|
+
"""
|
|
26
|
+
Connect to the backend at url and return its client.
|
|
27
|
+
|
|
28
|
+
kind skips detection ("a1111", "forge", "reforge", "sdnext", "comfyui", "swarmui", "invokeai").
|
|
29
|
+
Connection options: username, password, token, headers, connect_timeout, timeout, job_timeout,
|
|
30
|
+
retries, verify_tls, poll_interval. Credentials in the URL (https://user:pass@host) work too.
|
|
31
|
+
|
|
32
|
+
defaults: "profile" (default) fills settings a call leaves out from the model's profile; "fixed" uses
|
|
33
|
+
sdmux's fixed size/steps/cfg and leaves sampler, scheduler and clip skip to the server.
|
|
34
|
+
"""
|
|
35
|
+
from .backends import BACKENDS
|
|
36
|
+
from .probe import detect
|
|
37
|
+
|
|
38
|
+
if kind is None:
|
|
39
|
+
kind = detect(url, options, **option_overrides).kind
|
|
40
|
+
try:
|
|
41
|
+
cls = BACKENDS[kind.lower()]
|
|
42
|
+
except KeyError:
|
|
43
|
+
raise ValueError(f"Unknown backend kind {kind!r}. Known: {', '.join(sorted(BACKENDS))}") from None
|
|
44
|
+
return cls(url, options, defaults=defaults, **option_overrides)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def detect(url: str, **option_overrides):
|
|
48
|
+
"""Which backend answers at url. Returns a Detection (kind, evidence, version, tried)."""
|
|
49
|
+
from .probe import detect as _detect
|
|
50
|
+
|
|
51
|
+
return _detect(url, **option_overrides)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def identify(path, online: bool = False, **kwargs):
|
|
55
|
+
"""Identify a model file from its header (and sidecars, metadata, name; Civitai when online=True)."""
|
|
56
|
+
from .modelinfo import identify as _identify
|
|
57
|
+
|
|
58
|
+
return _identify(path, online=online, **kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"connect", "detect", "identify", "Options", "Graph", "GraphError", "NodeSpec", "Request", "Result", "Progress", "Model",
|
|
63
|
+
"SdmuxError", "Unreachable", "TimedOut", "AuthRefused", "NotReady", "Rejected", "Cancelled", "Unsupported",
|
|
64
|
+
"__version__",
|
|
65
|
+
]
|