mmh3-image 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yunfengwang
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.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: mmh3-image
3
+ Version: 0.1.0
4
+ Summary: MiniMax-H3-Image GGUF on ComfyUI: one command to download and generate.
5
+ Author: yunfengwang
6
+ License: MIT
7
+ Project-URL: Model (GGUF), https://huggingface.co/Abiray/MiniMax-H3-Image-GGUF
8
+ Project-URL: Model (original), https://huggingface.co/MiniMaxAI/MiniMax-H3-Image
9
+ Keywords: minimax,h3,text-to-image,gguf,comfyui,dit
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: huggingface_hub>=0.23
19
+ Dynamic: license-file
20
+
21
+ # mmh3-image
22
+
23
+ MiniMax-H3-Image (33B DiT, GGUF) as a one-command CLI. It installs a private
24
+ ComfyUI with the GGUF loader, downloads the quantization you pick plus the
25
+ required 24-channel VAE and Qwen3-VL-32B text encoder, starts the server and
26
+ writes a PNG. Everything is cached, so only the first run pays the setup cost.
27
+
28
+ ```bash
29
+ uvx --from mmh3-image mmh3-image "a clay badger baker outside a tiny bakery, warm lantern light" -o badger.png
30
+ ```
31
+
32
+ ## What it sets up
33
+
34
+ * `~/.cache/mmh3-image/ComfyUI` — ComfyUI checkout, venv, ComfyUI-GGUF node
35
+ * `~/.cache/mmh3-image/models` — DiT GGUF, text encoder, VAE
36
+ * an `extra_model_paths.yaml` in that checkout pointing at the cache
37
+
38
+ Need `git` and Python 3.9+. Weights are 20–28 GB and the model needs roughly
39
+ 24 GB of RAM or VRAM; Apple silicon uses MPS, NVIDIA uses CUDA.
40
+
41
+ ## Usage
42
+
43
+ ```bash
44
+ # recommended quantization, native-quality 2048px
45
+ mmh3-image "prompt" -q q4 -r 2048
46
+
47
+ # smallest weights, 16:9 at 1080p, fixed seed
48
+ mmh3-image "prompt" -q q2 -r 1080p --seed 42 -o shot.png
49
+
50
+ # 4K still (slow: about 12 min on an M5 Pro)
51
+ mmh3-image "prompt" -r 4k -s 6
52
+
53
+ # prefetch only, no generation
54
+ mmh3-image -q q5 --download-only
55
+
56
+ # reuse an existing ComfyUI checkout or an already running server
57
+ mmh3-image "prompt" --comfyui ~/projects/ComfyUI
58
+ mmh3-image "prompt" --server-url http://127.0.0.1:8188
59
+ ```
60
+
61
+ `-r` takes a preset (`720p`, `1080p`, `1440p`, `4k`, `square`), a single number
62
+ for a square, or `WIDTHxHEIGHT`. Values snap to 32 pixels, which is why `1080p`
63
+ is `1920x1088` — 1080 is not a multiple of 32 and would otherwise be floored.
64
+
65
+ Defaults matter for this model: **6 steps**, `euler` / `simple`, **CFG 1.0**.
66
+ It is a distilled 6-step model; more steps or a higher CFG degrade the result.
67
+
68
+ ## Quantizations
69
+
70
+ | `-q` | File | Size | Notes |
71
+ |---|---|---|---|
72
+ | `q2` | Q2_K | 6.9 GB | smallest, fits 8–10 GB VRAM |
73
+ | `q3` | Q3_K_M | 8.9 GB | 12 GB VRAM |
74
+ | `q4s` | Q4_K_S | 11.6 GB | fast 4-bit |
75
+ | `q4` | Q4_K_M | 11.6 GB | recommended sweet spot (default) |
76
+ | `q5` | Q5_K_M | 14.1 GB | closest to BF16 |
77
+
78
+ Text encoder: `--text-encoder q3` (12 GB, default) or `q4` (14.6 GB).
79
+
80
+ ## List everything
81
+
82
+ ```bash
83
+ mmh3-image --list
84
+ ```
85
+
86
+ ## Notes
87
+
88
+ * Weights come from [`Abiray/MiniMax-H3-Image-GGUF`](https://huggingface.co/Abiray/MiniMax-H3-Image-GGUF)
89
+ and [`Comfy-Org/MiniMax-H3-Image`](https://huggingface.co/Comfy-Org/MiniMax-H3-Image).
90
+ The model itself is under the MiniMax-H3-Image Community License; this CLI is
91
+ MIT and ships code only.
92
+ * `--hf-endpoint https://hf-mirror.com` switches the download mirror.
93
+ * The CLI leaves the server running only with `--keep-server`; `--serve` skips
94
+ generation and just serves.
95
+ * The package does not enable the optional Qwen3.5-9B prompt-expansion branch;
96
+ the prompt is used verbatim, which is what the model card examples assume.
@@ -0,0 +1,76 @@
1
+ # mmh3-image
2
+
3
+ MiniMax-H3-Image (33B DiT, GGUF) as a one-command CLI. It installs a private
4
+ ComfyUI with the GGUF loader, downloads the quantization you pick plus the
5
+ required 24-channel VAE and Qwen3-VL-32B text encoder, starts the server and
6
+ writes a PNG. Everything is cached, so only the first run pays the setup cost.
7
+
8
+ ```bash
9
+ uvx --from mmh3-image mmh3-image "a clay badger baker outside a tiny bakery, warm lantern light" -o badger.png
10
+ ```
11
+
12
+ ## What it sets up
13
+
14
+ * `~/.cache/mmh3-image/ComfyUI` — ComfyUI checkout, venv, ComfyUI-GGUF node
15
+ * `~/.cache/mmh3-image/models` — DiT GGUF, text encoder, VAE
16
+ * an `extra_model_paths.yaml` in that checkout pointing at the cache
17
+
18
+ Need `git` and Python 3.9+. Weights are 20–28 GB and the model needs roughly
19
+ 24 GB of RAM or VRAM; Apple silicon uses MPS, NVIDIA uses CUDA.
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # recommended quantization, native-quality 2048px
25
+ mmh3-image "prompt" -q q4 -r 2048
26
+
27
+ # smallest weights, 16:9 at 1080p, fixed seed
28
+ mmh3-image "prompt" -q q2 -r 1080p --seed 42 -o shot.png
29
+
30
+ # 4K still (slow: about 12 min on an M5 Pro)
31
+ mmh3-image "prompt" -r 4k -s 6
32
+
33
+ # prefetch only, no generation
34
+ mmh3-image -q q5 --download-only
35
+
36
+ # reuse an existing ComfyUI checkout or an already running server
37
+ mmh3-image "prompt" --comfyui ~/projects/ComfyUI
38
+ mmh3-image "prompt" --server-url http://127.0.0.1:8188
39
+ ```
40
+
41
+ `-r` takes a preset (`720p`, `1080p`, `1440p`, `4k`, `square`), a single number
42
+ for a square, or `WIDTHxHEIGHT`. Values snap to 32 pixels, which is why `1080p`
43
+ is `1920x1088` — 1080 is not a multiple of 32 and would otherwise be floored.
44
+
45
+ Defaults matter for this model: **6 steps**, `euler` / `simple`, **CFG 1.0**.
46
+ It is a distilled 6-step model; more steps or a higher CFG degrade the result.
47
+
48
+ ## Quantizations
49
+
50
+ | `-q` | File | Size | Notes |
51
+ |---|---|---|---|
52
+ | `q2` | Q2_K | 6.9 GB | smallest, fits 8–10 GB VRAM |
53
+ | `q3` | Q3_K_M | 8.9 GB | 12 GB VRAM |
54
+ | `q4s` | Q4_K_S | 11.6 GB | fast 4-bit |
55
+ | `q4` | Q4_K_M | 11.6 GB | recommended sweet spot (default) |
56
+ | `q5` | Q5_K_M | 14.1 GB | closest to BF16 |
57
+
58
+ Text encoder: `--text-encoder q3` (12 GB, default) or `q4` (14.6 GB).
59
+
60
+ ## List everything
61
+
62
+ ```bash
63
+ mmh3-image --list
64
+ ```
65
+
66
+ ## Notes
67
+
68
+ * Weights come from [`Abiray/MiniMax-H3-Image-GGUF`](https://huggingface.co/Abiray/MiniMax-H3-Image-GGUF)
69
+ and [`Comfy-Org/MiniMax-H3-Image`](https://huggingface.co/Comfy-Org/MiniMax-H3-Image).
70
+ The model itself is under the MiniMax-H3-Image Community License; this CLI is
71
+ MIT and ships code only.
72
+ * `--hf-endpoint https://hf-mirror.com` switches the download mirror.
73
+ * The CLI leaves the server running only with `--keep-server`; `--serve` skips
74
+ generation and just serves.
75
+ * The package does not enable the optional Qwen3.5-9B prompt-expansion branch;
76
+ the prompt is used verbatim, which is what the model card examples assume.
@@ -0,0 +1,3 @@
1
+ """MiniMax-H3-Image GGUF text-to-image on a local ComfyUI, from one command."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,211 @@
1
+ """``mmh3-image``: MiniMax-H3-Image GGUF text-to-image from one command.
2
+
3
+ mmh3-image "a clay badger baker outside a tiny bakery" -o badger.png
4
+
5
+ The first run installs ComfyUI into ``~/.cache/mmh3-image/ComfyUI`` (Python
6
+ venv plus the ComfyUI-GGUF node), downloads the selected quantization and the
7
+ required companions (24-channel image VAE, Qwen3-VL-32B text encoder), starts
8
+ a local ComfyUI server and writes the PNG. Later runs reuse everything.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import random
16
+ import sys
17
+ import time
18
+
19
+ from . import __version__
20
+
21
+ #: Resolutions in the model's native multiples of 32. 1080p is 1920x1088
22
+ #: because 1080 is not a multiple of 32; the model would floor it to 1072.
23
+ PRESETS = {
24
+ "720p": (1280, 736),
25
+ "1080p": (1920, 1088),
26
+ "1440p": (2560, 1440),
27
+ "4k": (4096, 4096),
28
+ "square": (1024, 1024),
29
+ }
30
+
31
+
32
+ def build_parser():
33
+ from .download import QUANTS, TEXT_ENCODERS
34
+
35
+ parser = argparse.ArgumentParser(
36
+ prog="mmh3-image",
37
+ description="MiniMax-H3-Image (GGUF) text-to-image via a local ComfyUI.",
38
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
39
+ )
40
+ parser.add_argument("prompt", nargs="?", help="the prompt (dense English works best)")
41
+ parser.add_argument("--prompt-file", default=None, help="read the prompt from a file")
42
+ parser.add_argument("-o", "--output", default=None,
43
+ help="output PNG path (default: mmh3-image-<seed>.png)")
44
+
45
+ model = parser.add_argument_group("model")
46
+ model.add_argument("-q", "--quant", default="q4", choices=sorted(QUANTS),
47
+ help="DiT quantization: " + ", ".join(
48
+ f"{k}={v[2]}" for k, v in QUANTS.items()))
49
+ model.add_argument("--text-encoder", default="q3", choices=sorted(TEXT_ENCODERS),
50
+ help="Qwen3-VL-32B text encoder quantization")
51
+ model.add_argument("--cache-dir", default=None,
52
+ help="where weights and ComfyUI live (default: ~/.cache/mmh3-image)")
53
+ model.add_argument("--hf-endpoint", default=None,
54
+ help="Hub endpoint, e.g. https://hf-mirror.com")
55
+ model.add_argument("--download-only", action="store_true",
56
+ help="fetch ComfyUI and the weights, then exit")
57
+
58
+ size = parser.add_argument_group("size")
59
+ size.add_argument("-r", "--resolution", default="1024",
60
+ help="a preset (" + ", ".join(PRESETS) + "), a single number "
61
+ "for a square, or WIDTHxHEIGHT; values snap to 32")
62
+ size.add_argument("--width", type=int, default=None, help="override width")
63
+ size.add_argument("--height", type=int, default=None, help="override height")
64
+
65
+ sample = parser.add_argument_group("sampling")
66
+ sample.add_argument("-s", "--steps", type=int, default=6,
67
+ help="denoise steps; the distilled model is calibrated for 6")
68
+ sample.add_argument("--seed", type=int, default=None,
69
+ help="seed (default: random; the used seed is printed)")
70
+ sample.add_argument("--sampler", default="euler", help="KSampler sampler name")
71
+ sample.add_argument("--scheduler", default="simple", help="KSampler scheduler")
72
+ sample.add_argument("--cfg", type=float, default=1.0,
73
+ help="CFG scale; 1.0 is correct, higher values degrade")
74
+
75
+ run = parser.add_argument_group("runtime")
76
+ run.add_argument("--comfyui", default=None,
77
+ help="existing ComfyUI checkout to use instead of the cache one")
78
+ run.add_argument("--server-url", default=None,
79
+ help="use an already running ComfyUI (skips install and download)")
80
+ run.add_argument("--dit-name", default=None, help="override DiT filename on that server")
81
+ run.add_argument("--te-name", default=None, help="override text encoder filename")
82
+ run.add_argument("--vae-name", default=None, help="override VAE filename")
83
+ run.add_argument("--port", type=int, default=8188)
84
+ run.add_argument("--keep-server", action="store_true",
85
+ help="leave the ComfyUI server running after the image is saved")
86
+ run.add_argument("--serve", action="store_true",
87
+ help="only prepare everything and run the server in the foreground")
88
+ run.add_argument("--timeout", type=float, default=3600.0,
89
+ help="generation timeout in seconds")
90
+ run.add_argument("--quiet", action="store_true", help="only print errors")
91
+
92
+ parser.add_argument("--list", action="store_true", help="show quantizations and exit")
93
+ parser.add_argument("--version", action="version", version=f"mmh3-image {__version__}")
94
+ return parser
95
+
96
+
97
+ def resolve_size(spec, width=None, height=None):
98
+ """Turn a preset / ``1024`` / ``1920x1088`` into 32-aligned (w, h)."""
99
+ if width and height:
100
+ w, h = width, height
101
+ elif spec in PRESETS:
102
+ w, h = PRESETS[spec]
103
+ elif "x" in spec.lower():
104
+ left, right = spec.lower().split("x", 1)
105
+ w, h = int(left), int(right)
106
+ else:
107
+ w = h = int(spec)
108
+ snap = lambda n: max(32, round(n / 32) * 32)
109
+ return snap(w), snap(h)
110
+
111
+
112
+ def main(argv=None):
113
+ parser = build_parser()
114
+ args = parser.parse_args(argv)
115
+
116
+ from .download import QUANTS, TEXT_ENCODERS, describe, ensure_models
117
+
118
+ if args.list:
119
+ print(f"mmh3-image {__version__}\n")
120
+ print("quantizations (DiT):")
121
+ for key in sorted(QUANTS):
122
+ filename, size, note = QUANTS[key]
123
+ print(f" {key:<4} {size / 1e9:5.2f} GB {note}")
124
+ print("\ntext encoders (Qwen3-VL-32B):")
125
+ for key in sorted(TEXT_ENCODERS):
126
+ _, _, size, note = TEXT_ENCODERS[key]
127
+ print(f" {key:<4} {size / 1e9:5.2f} GB {note}")
128
+ print("\npresets:", ", ".join(f"{k}={v[0]}x{v[1]}" for k, v in PRESETS.items()))
129
+ return 0
130
+
131
+ quiet = args.quiet
132
+
133
+ def say(message):
134
+ if not quiet:
135
+ print(message, flush=True)
136
+
137
+ prompt = args.prompt
138
+ if args.prompt_file:
139
+ with open(args.prompt_file, encoding="utf-8") as handle:
140
+ prompt = handle.read().strip()
141
+ if not prompt and not (args.download_only or args.serve):
142
+ parser.error("no prompt given")
143
+
144
+ seed = args.seed if args.seed is not None else random.randint(0, 2**31 - 1)
145
+ width, height = resolve_size(args.resolution, args.width, args.height)
146
+ output = args.output or f"mmh3-image-{seed}.png"
147
+
148
+ diT = QUANTS[args.quant][0]
149
+ text_encoder = TEXT_ENCODERS[args.text_encoder][1]
150
+
151
+ models_root = None
152
+ server = None
153
+ if args.server_url:
154
+ url = args.server_url.rstrip("/")
155
+ diT = args.dit_name or diT
156
+ text_encoder = args.te_name or text_encoder
157
+ vae_name = args.vae_name or "minimax_h3_image_vae_fp16.safetensors"
158
+ say(f"[mmh3-image] using running server {url}")
159
+ else:
160
+ from . import comfy
161
+
162
+ root = comfy.ensure_comfyui(args.comfyui, quiet=quiet)
163
+ from .download import cache_root
164
+ cache = cache_root(args.cache_dir)
165
+ models = os.path.join(cache, "models")
166
+ say(f"[mmh3-image] ComfyUI: {root}")
167
+ say(f"[mmh3-image] weights: {models}")
168
+ say(f"[mmh3-image] {describe(args.quant, args.text_encoder)}")
169
+ paths = ensure_models(args.quant, args.text_encoder, cache_dir=cache,
170
+ endpoint=args.hf_endpoint, quiet=quiet)
171
+ if args.download_only:
172
+ say("[mmh3-image] downloads complete")
173
+ return 0
174
+ vae_name = os.path.basename(paths["vae"])
175
+ comfy.write_extra_model_paths(root, models, quiet=quiet)
176
+ if not comfy.server_ready(f"http://127.0.0.1:{args.port}"):
177
+ server = comfy.start_server(root, args.port, quiet=quiet)
178
+ url = f"http://127.0.0.1:{args.port}"
179
+
180
+ if args.serve:
181
+ say(f"[mmh3-image] server on {url} (Ctrl-C to stop)")
182
+ try:
183
+ while True:
184
+ time.sleep(3600)
185
+ except KeyboardInterrupt:
186
+ return 0
187
+
188
+ from .workflow import build_prompt
189
+ graph = build_prompt(dit_name=diT, te_name=text_encoder, vae_name=vae_name,
190
+ prompt=prompt, width=width, height=height,
191
+ steps=args.steps, seed=seed, sampler=args.sampler,
192
+ scheduler=args.scheduler, cfg=args.cfg)
193
+
194
+ say(f"[mmh3-image] {width}x{height}, {args.steps} steps, seed {seed}")
195
+ started = time.time()
196
+ try:
197
+ from . import comfy
198
+ image = comfy.generate(url, graph, timeout=args.timeout, say=say)
199
+ finally:
200
+ if server is not None and not args.keep_server:
201
+ comfy.stop_server(server)
202
+
203
+ with open(output, "wb") as handle:
204
+ handle.write(image)
205
+ say(f"[mmh3-image] wrote {output} ({len(image) / 1e6:.1f} MB) "
206
+ f"in {time.time() - started:.1f}s")
207
+ return 0
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main())
@@ -0,0 +1,194 @@
1
+ """Install, point at and talk to a local ComfyUI.
2
+
3
+ The image model only exists as a ComfyUI node (``MiniMaxH3ImageConditioning``),
4
+ so the CLI bootstraps a private ComfyUI checkout with the ComfyUI-GGUF node and
5
+ drives it over the HTTP API. An existing checkout or a running server can be
6
+ used instead with ``--comfyui`` / ``--server-url``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ import urllib.error
17
+ import urllib.parse
18
+ import urllib.request
19
+ import uuid
20
+
21
+ COMFY_REPO = "https://github.com/comfyanonymous/ComfyUI"
22
+ GGUF_REPO = "https://github.com/city96/ComfyUI-GGUF"
23
+ MARKER = ".mmh3-image-installed"
24
+
25
+ EXTRA_PATHS_BLOCK = """mmh3_image:
26
+ base_path: {models}
27
+ diffusion_models: diffusion_models
28
+ text_encoders: text_encoders
29
+ vae: vae
30
+ """
31
+
32
+
33
+ def _run(cmd, cwd=None, quiet=False):
34
+ if not quiet:
35
+ print(f"[mmh3-image] $ {' '.join(str(c) for c in cmd)}", flush=True)
36
+ subprocess.run(cmd, cwd=cwd, check=True)
37
+
38
+
39
+ def _venv_python(root):
40
+ if os.name == "nt":
41
+ return os.path.join(root, "venv", "Scripts", "python.exe")
42
+ return os.path.join(root, "venv", "bin", "python")
43
+
44
+
45
+ def ensure_comfyui(root=None, cache_dir=None, quiet=False):
46
+ """Return a ComfyUI checkout with the GGUF node and its dependencies."""
47
+ if root is None:
48
+ from .download import cache_root
49
+ root = os.path.join(cache_root(cache_dir), "ComfyUI")
50
+ root = os.path.abspath(os.path.expanduser(root))
51
+
52
+ if not os.path.isfile(os.path.join(root, "main.py")):
53
+ os.makedirs(os.path.dirname(root) or ".", exist_ok=True)
54
+ _run(["git", "clone", "--depth", "1", COMFY_REPO, root], quiet=quiet)
55
+
56
+ python = _venv_python(root)
57
+ if not os.path.isfile(python):
58
+ if not quiet:
59
+ print(f"[mmh3-image] creating venv in {root}", flush=True)
60
+ _run([sys.executable, "-m", "venv", os.path.join(root, "venv")], quiet=quiet)
61
+
62
+ marker = os.path.join(root, MARKER)
63
+ if not os.path.isfile(marker):
64
+ _run([python, "-m", "pip", "install", "-q", "--upgrade", "pip"], quiet=quiet)
65
+ _run([python, "-m", "pip", "install", "-r",
66
+ os.path.join(root, "requirements.txt")], quiet=quiet)
67
+ open(marker, "w").close()
68
+
69
+ node = os.path.join(root, "custom_nodes", "ComfyUI-GGUF")
70
+ if not os.path.isdir(node):
71
+ _run(["git", "clone", "--depth", "1", GGUF_REPO, node], quiet=quiet)
72
+ req = os.path.join(node, "requirements.txt")
73
+ if os.path.isfile(req):
74
+ _run([python, "-m", "pip", "install", "-r", req], quiet=quiet)
75
+ return root
76
+
77
+
78
+ def write_extra_model_paths(root, models_root, quiet=False):
79
+ """Teach that ComfyUI checkout where the cached weights are."""
80
+ path = os.path.join(root, "extra_model_paths.yaml")
81
+ block = EXTRA_PATHS_BLOCK.format(models=models_root)
82
+ existing = ""
83
+ if os.path.isfile(path):
84
+ with open(path, encoding="utf-8") as handle:
85
+ existing = handle.read()
86
+ if f"base_path: {models_root}" in existing:
87
+ return path
88
+ if existing and not existing.endswith("\n"):
89
+ existing += "\n"
90
+ with open(path, "w", encoding="utf-8") as handle:
91
+ handle.write(existing + ("\n" if existing else "") + block)
92
+ if not quiet:
93
+ print(f"[mmh3-image] model paths -> {path}", flush=True)
94
+ return path
95
+
96
+
97
+ def server_ready(url, timeout=1.0):
98
+ try:
99
+ with urllib.request.urlopen(url.rstrip("/") + "/system_stats",
100
+ timeout=timeout) as response:
101
+ return response.status == 200
102
+ except Exception:
103
+ return False
104
+
105
+
106
+ def start_server(root, port=8188, quiet=False, wait=900):
107
+ url = f"http://127.0.0.1:{port}"
108
+ python = _venv_python(root)
109
+ log_path = os.path.join(os.path.dirname(root.rstrip(os.sep)), "comfyui.log")
110
+ log = open(log_path, "ab")
111
+ if not quiet:
112
+ print(f"[mmh3-image] starting ComfyUI on {url} (log: {log_path})", flush=True)
113
+ proc = subprocess.Popen(
114
+ [python, "main.py", "--listen", "127.0.0.1", "--port", str(port),
115
+ "--disable-auto-launch"],
116
+ cwd=root, stdout=log, stderr=subprocess.STDOUT)
117
+ started = time.time()
118
+ while time.time() - started < wait:
119
+ if proc.poll() is not None:
120
+ raise RuntimeError(f"ComfyUI exited with {proc.returncode}; see {log_path}")
121
+ if server_ready(url):
122
+ if not quiet:
123
+ print(f"[mmh3-image] server ready in {time.time() - started:.1f}s",
124
+ flush=True)
125
+ return proc
126
+ time.sleep(1.0)
127
+ proc.terminate()
128
+ raise RuntimeError(f"ComfyUI did not become ready within {wait}s; see {log_path}")
129
+
130
+
131
+ def stop_server(proc):
132
+ if proc is None or proc.poll() is not None:
133
+ return
134
+ proc.terminate()
135
+ try:
136
+ proc.wait(timeout=30)
137
+ except subprocess.TimeoutExpired:
138
+ proc.kill()
139
+
140
+
141
+ def _post(url, payload):
142
+ request = urllib.request.Request(
143
+ url, json.dumps(payload).encode(), {"Content-Type": "application/json"})
144
+ return json.loads(urllib.request.urlopen(request, timeout=60).read())
145
+
146
+
147
+ def _get_json(url):
148
+ with urllib.request.urlopen(url, timeout=60) as response:
149
+ return json.loads(response.read())
150
+
151
+
152
+ def generate(url, graph, timeout=3600, say=print):
153
+ """Queue ``graph``, wait for it and return the first PNG's bytes."""
154
+ url = url.rstrip("/")
155
+ client_id = uuid.uuid4().hex
156
+ try:
157
+ queued = _post(url + "/prompt", {"prompt": graph, "client_id": client_id})
158
+ except urllib.error.HTTPError as exc:
159
+ detail = exc.read().decode("utf-8", "replace")
160
+ raise SystemExit(f"ComfyUI rejected the workflow:\n{detail[:3000]}")
161
+ if queued.get("node_errors"):
162
+ raise SystemExit("workflow validation failed:\n" +
163
+ json.dumps(queued["node_errors"], indent=1)[:3000])
164
+
165
+ prompt_id = queued["prompt_id"]
166
+ started = time.time()
167
+ last_report = started
168
+ while True:
169
+ if time.time() - started > timeout:
170
+ raise SystemExit(f"timed out after {timeout:.0f}s")
171
+ history = _get_json(f"{url}/history/{prompt_id}")
172
+ entry = history.get(prompt_id)
173
+ if entry:
174
+ status = entry.get("status", {})
175
+ if status.get("status_str") == "error":
176
+ raise SystemExit("generation failed:\n" +
177
+ json.dumps(status, indent=1)[:3000])
178
+ images = [img for node in entry.get("outputs", {}).values()
179
+ for img in node.get("images", [])]
180
+ if images:
181
+ image = images[0]
182
+ query = urllib.parse.urlencode({
183
+ "filename": image["filename"],
184
+ "subfolder": image.get("subfolder", ""),
185
+ "type": image.get("type", "output"),
186
+ })
187
+ with urllib.request.urlopen(f"{url}/view?{query}", timeout=300) as resp:
188
+ return resp.read()
189
+ raise SystemExit("generation finished but produced no image")
190
+ now = time.time()
191
+ if now - last_report > 20:
192
+ say(f"[mmh3-image] still sampling... {now - started:.0f}s")
193
+ last_report = now
194
+ time.sleep(2.0)
@@ -0,0 +1,100 @@
1
+ """Fetch the GGUF weights this package drives.
2
+
3
+ Nothing is bundled in the wheel: the selected quantization plus the two
4
+ required companions are pulled into ``~/.cache/mmh3-image/models`` on first
5
+ use and reused afterwards. ``hf_hub_download`` resumes partial files, so an
6
+ interrupted download picks up where it stopped.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+
14
+ DEFAULT_CACHE = "~/.cache/mmh3-image"
15
+
16
+ DIT_REPO = "Abiray/MiniMax-H3-Image-GGUF"
17
+ TE_Q3_REPO = "Abiray/Qwen3-VL-32B-Instruct-GGUF"
18
+ TE_Q4_REPO = "Abiray/MiniMax-H3-GGUF"
19
+ VAE_REPO = "Comfy-Org/MiniMax-H3-Image"
20
+
21
+ VAE_FILENAME = "vae/minimax_h3_image_vae_fp16.safetensors"
22
+
23
+
24
+ #: key -> (filename, approximate bytes, description)
25
+ QUANTS = {
26
+ "q2": ("MiniMax-H3-Image-Pruned-Q2_K.gguf", 6.88e9, "smallest, 8-10 GB VRAM"),
27
+ "q3": ("MiniMax-H3-Image-Pruned-Q3_K_M.gguf", 8.91e9, "efficient, 12 GB VRAM"),
28
+ "q4s": ("MiniMax-H3-Image-Pruned-Q4_K_S.gguf", 11.60e9, "fast 4-bit"),
29
+ "q4": ("MiniMax-H3-Image-Pruned-Q4_K_M.gguf", 11.60e9, "recommended sweet spot"),
30
+ "q5": ("MiniMax-H3-Image-Pruned-Q5_K_M.gguf", 14.08e9, "closest to BF16"),
31
+ }
32
+
33
+ #: key -> (repo, filename, approximate bytes, description)
34
+ TEXT_ENCODERS = {
35
+ "q3": (TE_Q3_REPO, "Qwen3vl_32b_h3-Q3_K_M.gguf", 12.00e9, "12 GB, recommended"),
36
+ "q4": (TE_Q4_REPO, "text_encoders/qwen3vl_32b_minimax_h3-Q4_K_M.gguf", 14.60e9,
37
+ "14.6 GB, slightly better prompt fidelity"),
38
+ }
39
+
40
+
41
+ def cache_root(cache_dir=None):
42
+ return os.path.abspath(os.path.expanduser(cache_dir or os.environ.get(
43
+ "MMH3_IMAGE_CACHE", DEFAULT_CACHE)))
44
+
45
+
46
+ def describe(quant, text_encoder):
47
+ dit = QUANTS[quant]
48
+ te = TEXT_ENCODERS[text_encoder]
49
+ total = (dit[1] + te[2] + 1.17e9) / 1e9
50
+ return (f"DiT {quant} ({dit[1] / 1e9:.2f} GB, {dit[2]}) + "
51
+ f"text encoder {text_encoder} ({te[2] / 1e9:.2f} GB) + VAE 1.17 GB "
52
+ f"= about {total:.1f} GB")
53
+
54
+
55
+ def _file(repo, filename, local_dir, expected, endpoint=None, quiet=False):
56
+ os.makedirs(local_dir, exist_ok=True)
57
+ path = os.path.join(local_dir, filename)
58
+ if os.path.isfile(path) and abs(os.path.getsize(path) - expected) < expected * 0.01:
59
+ return path
60
+ if endpoint:
61
+ os.environ["HF_ENDPOINT"] = endpoint.rstrip("/")
62
+ from huggingface_hub import hf_hub_download
63
+
64
+ if not quiet:
65
+ print(f"[mmh3-image] downloading {filename} ({expected / 1e9:.2f} GB) "
66
+ f"from {repo}", flush=True)
67
+ downloaded = hf_hub_download(repo_id=repo, filename=filename,
68
+ local_dir=local_dir)
69
+ size = os.path.getsize(downloaded)
70
+ if abs(size - expected) > expected * 0.01:
71
+ raise RuntimeError(f"{downloaded} is {size} bytes, expected about "
72
+ f"{int(expected)}; the download is incomplete, rerun "
73
+ f"the command to resume")
74
+ return downloaded
75
+
76
+
77
+ def ensure_models(quant="q4", text_encoder="q3", cache_dir=None, endpoint=None,
78
+ quiet=False):
79
+ """Download the DiT, text encoder and VAE; return their paths."""
80
+ if quant not in QUANTS:
81
+ raise SystemExit(f"unknown quantization {quant!r}; pick one of {sorted(QUANTS)}")
82
+ if text_encoder not in TEXT_ENCODERS:
83
+ raise SystemExit(f"unknown text encoder {text_encoder!r}; "
84
+ f"pick one of {sorted(TEXT_ENCODERS)}")
85
+
86
+ models = os.path.join(cache_root(cache_dir), "models")
87
+ dit_name = QUANTS[quant][0]
88
+ te_repo, te_name = TEXT_ENCODERS[text_encoder][0], TEXT_ENCODERS[text_encoder][1]
89
+ # Files that live under a subdirectory in their repo are mirrored from the
90
+ # models root, so they land where ComfyUI scans for them.
91
+ te_local = models if "/" in te_name else os.path.join(models, "text_encoders")
92
+
93
+ dit = _file(DIT_REPO, dit_name, os.path.join(models, "diffusion_models"),
94
+ QUANTS[quant][1], endpoint, quiet)
95
+ if not quiet:
96
+ print(f"[mmh3-image] text encoder {te_name}", flush=True)
97
+ te = _file(te_repo, te_name, te_local,
98
+ TEXT_ENCODERS[text_encoder][2], endpoint, quiet)
99
+ vae = _file(VAE_REPO, VAE_FILENAME, models, 1.168505312e9, endpoint, quiet)
100
+ return {"dit": dit, "text_encoder": te, "vae": vae, "models": models}
@@ -0,0 +1,35 @@
1
+ """The ComfyUI API graph for one MiniMax-H3-Image generation.
2
+
3
+ Defaults follow the model card: euler / simple, exactly six steps, CFG 1.0 and
4
+ no negative prompt (the distilled PDD heads are calibrated for that).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ ALIGN = 32
10
+
11
+
12
+ def build_prompt(dit_name, te_name, vae_name, prompt, width, height,
13
+ steps=6, seed=0, sampler="euler", scheduler="simple", cfg=1.0):
14
+ if width % ALIGN or height % ALIGN:
15
+ raise ValueError(f"width and height must be multiples of {ALIGN}")
16
+ return {
17
+ "1": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": dit_name}},
18
+ "2": {"class_type": "CLIPLoaderGGUF",
19
+ "inputs": {"clip_name": te_name, "type": "minimax"}},
20
+ "3": {"class_type": "VAELoader", "inputs": {"vae_name": vae_name}},
21
+ "4": {"class_type": "MiniMaxH3ImageConditioning",
22
+ "inputs": {"clip": ["2", 0], "vae": ["3", 0], "prompt": prompt,
23
+ "width": width, "height": height}},
24
+ "5": {"class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["4", 0]}},
25
+ "6": {"class_type": "KSampler",
26
+ "inputs": {"seed": seed, "steps": steps, "cfg": cfg,
27
+ "sampler_name": sampler, "scheduler": scheduler,
28
+ "denoise": 1.0, "model": ["1", 0],
29
+ "positive": ["4", 0], "negative": ["5", 0],
30
+ "latent_image": ["4", 1]}},
31
+ "7": {"class_type": "VAEDecode",
32
+ "inputs": {"samples": ["6", 0], "vae": ["3", 0]}},
33
+ "8": {"class_type": "SaveImage",
34
+ "inputs": {"images": ["7", 0], "filename_prefix": "mmh3-image"}},
35
+ }
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: mmh3-image
3
+ Version: 0.1.0
4
+ Summary: MiniMax-H3-Image GGUF on ComfyUI: one command to download and generate.
5
+ Author: yunfengwang
6
+ License: MIT
7
+ Project-URL: Model (GGUF), https://huggingface.co/Abiray/MiniMax-H3-Image-GGUF
8
+ Project-URL: Model (original), https://huggingface.co/MiniMaxAI/MiniMax-H3-Image
9
+ Keywords: minimax,h3,text-to-image,gguf,comfyui,dit
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: huggingface_hub>=0.23
19
+ Dynamic: license-file
20
+
21
+ # mmh3-image
22
+
23
+ MiniMax-H3-Image (33B DiT, GGUF) as a one-command CLI. It installs a private
24
+ ComfyUI with the GGUF loader, downloads the quantization you pick plus the
25
+ required 24-channel VAE and Qwen3-VL-32B text encoder, starts the server and
26
+ writes a PNG. Everything is cached, so only the first run pays the setup cost.
27
+
28
+ ```bash
29
+ uvx --from mmh3-image mmh3-image "a clay badger baker outside a tiny bakery, warm lantern light" -o badger.png
30
+ ```
31
+
32
+ ## What it sets up
33
+
34
+ * `~/.cache/mmh3-image/ComfyUI` — ComfyUI checkout, venv, ComfyUI-GGUF node
35
+ * `~/.cache/mmh3-image/models` — DiT GGUF, text encoder, VAE
36
+ * an `extra_model_paths.yaml` in that checkout pointing at the cache
37
+
38
+ Need `git` and Python 3.9+. Weights are 20–28 GB and the model needs roughly
39
+ 24 GB of RAM or VRAM; Apple silicon uses MPS, NVIDIA uses CUDA.
40
+
41
+ ## Usage
42
+
43
+ ```bash
44
+ # recommended quantization, native-quality 2048px
45
+ mmh3-image "prompt" -q q4 -r 2048
46
+
47
+ # smallest weights, 16:9 at 1080p, fixed seed
48
+ mmh3-image "prompt" -q q2 -r 1080p --seed 42 -o shot.png
49
+
50
+ # 4K still (slow: about 12 min on an M5 Pro)
51
+ mmh3-image "prompt" -r 4k -s 6
52
+
53
+ # prefetch only, no generation
54
+ mmh3-image -q q5 --download-only
55
+
56
+ # reuse an existing ComfyUI checkout or an already running server
57
+ mmh3-image "prompt" --comfyui ~/projects/ComfyUI
58
+ mmh3-image "prompt" --server-url http://127.0.0.1:8188
59
+ ```
60
+
61
+ `-r` takes a preset (`720p`, `1080p`, `1440p`, `4k`, `square`), a single number
62
+ for a square, or `WIDTHxHEIGHT`. Values snap to 32 pixels, which is why `1080p`
63
+ is `1920x1088` — 1080 is not a multiple of 32 and would otherwise be floored.
64
+
65
+ Defaults matter for this model: **6 steps**, `euler` / `simple`, **CFG 1.0**.
66
+ It is a distilled 6-step model; more steps or a higher CFG degrade the result.
67
+
68
+ ## Quantizations
69
+
70
+ | `-q` | File | Size | Notes |
71
+ |---|---|---|---|
72
+ | `q2` | Q2_K | 6.9 GB | smallest, fits 8–10 GB VRAM |
73
+ | `q3` | Q3_K_M | 8.9 GB | 12 GB VRAM |
74
+ | `q4s` | Q4_K_S | 11.6 GB | fast 4-bit |
75
+ | `q4` | Q4_K_M | 11.6 GB | recommended sweet spot (default) |
76
+ | `q5` | Q5_K_M | 14.1 GB | closest to BF16 |
77
+
78
+ Text encoder: `--text-encoder q3` (12 GB, default) or `q4` (14.6 GB).
79
+
80
+ ## List everything
81
+
82
+ ```bash
83
+ mmh3-image --list
84
+ ```
85
+
86
+ ## Notes
87
+
88
+ * Weights come from [`Abiray/MiniMax-H3-Image-GGUF`](https://huggingface.co/Abiray/MiniMax-H3-Image-GGUF)
89
+ and [`Comfy-Org/MiniMax-H3-Image`](https://huggingface.co/Comfy-Org/MiniMax-H3-Image).
90
+ The model itself is under the MiniMax-H3-Image Community License; this CLI is
91
+ MIT and ships code only.
92
+ * `--hf-endpoint https://hf-mirror.com` switches the download mirror.
93
+ * The CLI leaves the server running only with `--keep-server`; `--serve` skips
94
+ generation and just serves.
95
+ * The package does not enable the optional Qwen3.5-9B prompt-expansion branch;
96
+ the prompt is used verbatim, which is what the model card examples assume.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ mmh3_image/__init__.py
5
+ mmh3_image/cli.py
6
+ mmh3_image/comfy.py
7
+ mmh3_image/download.py
8
+ mmh3_image/workflow.py
9
+ mmh3_image.egg-info/PKG-INFO
10
+ mmh3_image.egg-info/SOURCES.txt
11
+ mmh3_image.egg-info/dependency_links.txt
12
+ mmh3_image.egg-info/entry_points.txt
13
+ mmh3_image.egg-info/requires.txt
14
+ mmh3_image.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mmh3-image = mmh3_image.cli:main
@@ -0,0 +1 @@
1
+ huggingface_hub>=0.23
@@ -0,0 +1 @@
1
+ mmh3_image
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mmh3-image"
7
+ version = "0.1.0"
8
+ description = "MiniMax-H3-Image GGUF on ComfyUI: one command to download and generate."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "yunfengwang" }]
13
+ keywords = ["minimax", "h3", "text-to-image", "gguf", "comfyui", "dit"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Multimedia :: Graphics",
20
+ ]
21
+ dependencies = [
22
+ "huggingface_hub>=0.23",
23
+ ]
24
+
25
+ [project.scripts]
26
+ mmh3-image = "mmh3_image.cli:main"
27
+
28
+ [project.urls]
29
+ "Model (GGUF)" = "https://huggingface.co/Abiray/MiniMax-H3-Image-GGUF"
30
+ "Model (original)" = "https://huggingface.co/MiniMaxAI/MiniMax-H3-Image"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["mmh3_image*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+