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.
Files changed (35) hide show
  1. sdmux-0.1.0/LICENSE +21 -0
  2. sdmux-0.1.0/PKG-INFO +274 -0
  3. sdmux-0.1.0/README.md +252 -0
  4. sdmux-0.1.0/pyproject.toml +40 -0
  5. sdmux-0.1.0/setup.cfg +4 -0
  6. sdmux-0.1.0/src/sdmux/__init__.py +65 -0
  7. sdmux-0.1.0/src/sdmux/backends/__init__.py +17 -0
  8. sdmux-0.1.0/src/sdmux/backends/a1111.py +308 -0
  9. sdmux-0.1.0/src/sdmux/backends/base.py +270 -0
  10. sdmux-0.1.0/src/sdmux/backends/comfyui.py +611 -0
  11. sdmux-0.1.0/src/sdmux/backends/invokeai.py +340 -0
  12. sdmux-0.1.0/src/sdmux/backends/swarmui.py +284 -0
  13. sdmux-0.1.0/src/sdmux/cli.py +218 -0
  14. sdmux-0.1.0/src/sdmux/comfygraph.py +392 -0
  15. sdmux-0.1.0/src/sdmux/errors.py +77 -0
  16. sdmux-0.1.0/src/sdmux/modelinfo/__init__.py +265 -0
  17. sdmux-0.1.0/src/sdmux/modelinfo/civitai.py +123 -0
  18. sdmux-0.1.0/src/sdmux/modelinfo/family.py +375 -0
  19. sdmux-0.1.0/src/sdmux/modelinfo/headers.py +344 -0
  20. sdmux-0.1.0/src/sdmux/modelinfo/lookups.py +254 -0
  21. sdmux-0.1.0/src/sdmux/modelinfo/profiles.py +106 -0
  22. sdmux-0.1.0/src/sdmux/modelinfo/rules.py +331 -0
  23. sdmux-0.1.0/src/sdmux/names.py +254 -0
  24. sdmux-0.1.0/src/sdmux/probe.py +140 -0
  25. sdmux-0.1.0/src/sdmux/resources/profiles.json +53 -0
  26. sdmux-0.1.0/src/sdmux/transport.py +268 -0
  27. sdmux-0.1.0/src/sdmux/types.py +179 -0
  28. sdmux-0.1.0/src/sdmux.egg-info/PKG-INFO +274 -0
  29. sdmux-0.1.0/src/sdmux.egg-info/SOURCES.txt +33 -0
  30. sdmux-0.1.0/src/sdmux.egg-info/dependency_links.txt +1 -0
  31. sdmux-0.1.0/src/sdmux.egg-info/entry_points.txt +2 -0
  32. sdmux-0.1.0/src/sdmux.egg-info/requires.txt +8 -0
  33. sdmux-0.1.0/src/sdmux.egg-info/top_level.txt +1 -0
  34. sdmux-0.1.0/tests/test_backends.py +610 -0
  35. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]