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.
- mmh3_image-0.1.0/LICENSE +21 -0
- mmh3_image-0.1.0/PKG-INFO +96 -0
- mmh3_image-0.1.0/README.md +76 -0
- mmh3_image-0.1.0/mmh3_image/__init__.py +3 -0
- mmh3_image-0.1.0/mmh3_image/cli.py +211 -0
- mmh3_image-0.1.0/mmh3_image/comfy.py +194 -0
- mmh3_image-0.1.0/mmh3_image/download.py +100 -0
- mmh3_image-0.1.0/mmh3_image/workflow.py +35 -0
- mmh3_image-0.1.0/mmh3_image.egg-info/PKG-INFO +96 -0
- mmh3_image-0.1.0/mmh3_image.egg-info/SOURCES.txt +14 -0
- mmh3_image-0.1.0/mmh3_image.egg-info/dependency_links.txt +1 -0
- mmh3_image-0.1.0/mmh3_image.egg-info/entry_points.txt +2 -0
- mmh3_image-0.1.0/mmh3_image.egg-info/requires.txt +1 -0
- mmh3_image-0.1.0/mmh3_image.egg-info/top_level.txt +1 -0
- mmh3_image-0.1.0/pyproject.toml +33 -0
- mmh3_image-0.1.0/setup.cfg +4 -0
mmh3_image-0.1.0/LICENSE
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|