Spaces:
Running on Zero
Running on Zero
Music3-style composer UI + optional MiniMax-H3-Image-PE prompt enhancement
Browse files
app.py
CHANGED
|
@@ -9,10 +9,12 @@ for _wheel in glob.glob(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
| 9 |
|
| 10 |
import spaces # before torch
|
| 11 |
|
|
|
|
| 12 |
import math
|
| 13 |
import os
|
| 14 |
import random
|
| 15 |
import tempfile
|
|
|
|
| 16 |
from functools import cache
|
| 17 |
|
| 18 |
import gradio as gr
|
|
@@ -28,38 +30,10 @@ MAX_IMAGES = 8
|
|
| 28 |
MAX_SEED = 2**31 - 1
|
| 29 |
MULTIPLE = 32
|
| 30 |
|
| 31 |
-
|
| 32 |
-
"4 MP": {
|
| 33 |
-
"1:1": (2048, 2048),
|
| 34 |
-
"4:3": (2368, 1760),
|
| 35 |
-
"3:4": (1760, 2368),
|
| 36 |
-
"3:2": (2496, 1664),
|
| 37 |
-
"2:3": (1664, 2496),
|
| 38 |
-
"16:9": (2720, 1536),
|
| 39 |
-
"9:16": (1536, 2720),
|
| 40 |
-
},
|
| 41 |
-
"8 MP": {
|
| 42 |
-
"1:1": (2912, 2912),
|
| 43 |
-
"4:3": (3360, 2496),
|
| 44 |
-
"3:4": (2496, 3360),
|
| 45 |
-
"3:2": (3552, 2368),
|
| 46 |
-
"2:3": (2368, 3552),
|
| 47 |
-
"16:9": (3872, 2176),
|
| 48 |
-
"9:16": (2176, 3872),
|
| 49 |
-
},
|
| 50 |
-
"16 MP": {
|
| 51 |
-
"1:1": (4096, 4096),
|
| 52 |
-
"4:3": (4736, 3552),
|
| 53 |
-
"3:4": (3552, 4736),
|
| 54 |
-
"3:2": (5024, 3360),
|
| 55 |
-
"2:3": (3360, 5024),
|
| 56 |
-
"16:9": (5472, 3072),
|
| 57 |
-
"9:16": (3072, 5472),
|
| 58 |
-
},
|
| 59 |
-
}
|
| 60 |
MEGAPIXELS = {"4 MP": 4, "8 MP": 8, "16 MP": 16}
|
| 61 |
-
|
| 62 |
-
|
| 63 |
|
| 64 |
# Everything but the Qwen3-VL text encoder, which runs on ENCODER_SPACE: `prompt_embeds` + `text_token_tags` become
|
| 65 |
# inputs of the pipeline.
|
|
@@ -90,12 +64,34 @@ def size_at_area(width, height, pixels):
|
|
| 90 |
|
| 91 |
|
| 92 |
def resolve_size(paths, aspect, resolution):
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
def prepare(prompt, images, aspect, resolution, seed, randomize):
|
|
@@ -143,51 +139,232 @@ def _generate(embeds_path, images, width, height, seed):
|
|
| 143 |
return path
|
| 144 |
|
| 145 |
|
| 146 |
-
def generate(prompt, images, aspect, resolution, seed, randomize,
|
|
|
|
| 147 |
"""Text-to-image, or an edit of up to 8 images referred to as `<Picture 1>`, `<Picture 2>`, ... in the prompt.
|
| 148 |
|
| 149 |
-
|
|
|
|
| 150 |
"""
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
]
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
randomize = gr.Checkbox(value=True, label="Randomize seed")
|
| 180 |
-
button = gr.Button("Generate", variant="primary")
|
| 181 |
-
with gr.Column():
|
| 182 |
-
output = gr.Image(label="Result", type="filepath", format="png")
|
| 183 |
-
gr.Examples(EXAMPLES, inputs=prompt)
|
| 184 |
-
|
| 185 |
-
images.change(lambda imgs: gr.update(value=MATCH) if imgs else gr.update(value="1:1"), images, aspect)
|
| 186 |
-
gr.on(
|
| 187 |
-
[button.click, prompt.submit],
|
| 188 |
generate,
|
| 189 |
-
[
|
| 190 |
-
[output, seed],
|
| 191 |
api_name="generate",
|
| 192 |
)
|
| 193 |
|
|
|
|
| 9 |
|
| 10 |
import spaces # before torch
|
| 11 |
|
| 12 |
+
import json
|
| 13 |
import math
|
| 14 |
import os
|
| 15 |
import random
|
| 16 |
import tempfile
|
| 17 |
+
import time
|
| 18 |
from functools import cache
|
| 19 |
|
| 20 |
import gradio as gr
|
|
|
|
| 30 |
MAX_SEED = 2**31 - 1
|
| 31 |
MULTIPLE = 32
|
| 32 |
|
| 33 |
+
RATIOS = ["1:1", "5:4", "4:3", "3:2", "16:9", "21:9", "4:5", "3:4", "2:3", "9:16"] # MiniMax-H3-Image-PE's set
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
MEGAPIXELS = {"4 MP": 4, "8 MP": 8, "16 MP": 16}
|
| 35 |
+
AUTO = "auto" # PE's recommendation for text-to-image, Picture 1's ratio for edits, else 1:1
|
| 36 |
+
PE_SPACE = os.environ.get("H3_IMAGE_PE", "multimodalart/minimax-h3-image-pe")
|
| 37 |
|
| 38 |
# Everything but the Qwen3-VL text encoder, which runs on ENCODER_SPACE: `prompt_embeds` + `text_token_tags` become
|
| 39 |
# inputs of the pipeline.
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def resolve_size(paths, aspect, resolution):
|
| 67 |
+
pixels = MEGAPIXELS.get(resolution, 4) * 1024 * 1024
|
| 68 |
+
if aspect not in RATIOS:
|
| 69 |
+
if paths:
|
| 70 |
+
width, height = Image.open(paths[0]).size
|
| 71 |
+
return size_at_area(width, height, pixels)
|
| 72 |
+
aspect = "1:1"
|
| 73 |
+
rw, rh = (int(v) for v in aspect.split(":"))
|
| 74 |
+
return size_at_area(rw, rh, pixels)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@cache
|
| 78 |
+
def pe_client():
|
| 79 |
+
from gradio_client import Client
|
| 80 |
+
|
| 81 |
+
return Client(PE_SPACE, token=os.environ.get("HF_TOKEN"), httpx_kwargs={"timeout": 900})
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def enhance_stream(prompt, effort, seed):
|
| 85 |
+
"""Streams MiniMax-H3-Image-PE: yields (thinking, None, None) while it reasons, then (thinking, caption, ratio)."""
|
| 86 |
+
job = pe_client().submit(prompt, effort, int(seed) % (2**31), api_name="/expand")
|
| 87 |
+
shown = 0
|
| 88 |
+
for update in job:
|
| 89 |
+
thinking = update[2] or ""
|
| 90 |
+
if len(thinking) - shown > 200:
|
| 91 |
+
shown = len(thinking)
|
| 92 |
+
yield thinking, None, None
|
| 93 |
+
caption, ratio, thinking, _ = job.result()
|
| 94 |
+
yield thinking, caption, ratio
|
| 95 |
|
| 96 |
|
| 97 |
def prepare(prompt, images, aspect, resolution, seed, randomize):
|
|
|
|
| 139 |
return path
|
| 140 |
|
| 141 |
|
| 142 |
+
def generate(prompt, images=None, aspect=AUTO, resolution="4 MP", seed=0, randomize=True, enhance=False,
|
| 143 |
+
reasoning_effort="low", progress=gr.Progress(track_tqdm=True)):
|
| 144 |
"""Text-to-image, or an edit of up to 8 images referred to as `<Picture 1>`, `<Picture 2>`, ... in the prompt.
|
| 145 |
|
| 146 |
+
`aspect`: one of 1:1, 5:4, 4:3, 3:2, 16:9, 21:9, 4:5, 3:4, 2:3, 9:16, or "auto". `enhance`: expand the prompt with
|
| 147 |
+
MiniMax-H3-Image-PE first (text-to-image only). Returns (image, seed, prompt used).
|
| 148 |
"""
|
| 149 |
+
if not prompt or not prompt.strip():
|
| 150 |
+
raise gr.Error("Enter a prompt.")
|
| 151 |
+
final = prompt.strip()
|
| 152 |
+
if enhance and not image_paths(images):
|
| 153 |
+
progress(0, desc="Enhancing the prompt ...")
|
| 154 |
+
*_, (thinking, caption, ratio) = enhance_stream(final, reasoning_effort, seed)
|
| 155 |
+
final = caption or final
|
| 156 |
+
if aspect == AUTO and ratio:
|
| 157 |
+
aspect = ratio
|
| 158 |
+
progress(0.05, desc="Encoding the prompt ...")
|
| 159 |
+
embeds_path, seed, width, height = prepare(final, images, aspect, resolution, seed, randomize)
|
| 160 |
+
progress(0.1, desc=f"Generating {width}x{height} ...")
|
| 161 |
+
return _generate(embeds_path, images, width, height, seed), seed, final
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def studio(state, images, seed, randomize, effort):
|
| 165 |
+
"""The composer's Generate: optional PE (streamed), then the image."""
|
| 166 |
+
state = state or {}
|
| 167 |
+
prompt = (state.get("prompt") or "").strip()
|
| 168 |
+
aspect = state.get("aspect") or AUTO
|
| 169 |
+
resolution = state.get("resolution") or "4 MP"
|
| 170 |
+
if not prompt:
|
| 171 |
+
raise gr.Error("Describe the image first.")
|
| 172 |
+
paths = image_paths(images)
|
| 173 |
+
if randomize:
|
| 174 |
+
seed = random.randint(0, MAX_SEED)
|
| 175 |
+
final, thinking = prompt, ""
|
| 176 |
+
if state.get("enhance") and paths:
|
| 177 |
+
gr.Info("Prompt enhancement is text-to-image only for now; edits use your prompt as written.")
|
| 178 |
+
elif state.get("enhance"):
|
| 179 |
+
yield gr.update(), "✨ Enhancing the prompt with MiniMax-H3-Image-PE ...", gr.update(), "", seed
|
| 180 |
+
t0 = time.time()
|
| 181 |
+
caption = ratio = None
|
| 182 |
+
for thinking, caption, ratio in enhance_stream(prompt, effort, seed):
|
| 183 |
+
if caption is None:
|
| 184 |
+
yield gr.update(), f"✨ Enhancing the prompt · thinking ({len(thinking.split())} words) ...", gr.update(), thinking, seed
|
| 185 |
+
if caption:
|
| 186 |
+
final = caption
|
| 187 |
+
if aspect == AUTO and ratio:
|
| 188 |
+
aspect = ratio
|
| 189 |
+
yield gr.update(), f"✨ Prompt enhanced in {time.time() - t0:.0f}s · encoding ...", final, thinking, seed
|
| 190 |
+
width, height = resolve_size(paths, aspect, resolution)
|
| 191 |
+
yield gr.update(), f"Encoding the prompt · {width}×{height} ...", final, thinking, seed
|
| 192 |
+
embeds_path, seed, width, height = prepare(final, images, aspect, resolution, seed, False)
|
| 193 |
+
yield gr.update(), f"Generating {width}×{height} ...", final, thinking, seed
|
| 194 |
+
t0 = time.time()
|
| 195 |
+
path = _generate(embeds_path, images, width, height, seed)
|
| 196 |
+
yield path, f"{width}×{height} · seed {seed} · {time.time() - t0:.0f}s", final, thinking, seed
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
IDEAS = [
|
| 200 |
+
"a two-color risograph print of a fox reading a newspaper on a park bench, headline \"FOX NEWS\"",
|
| 201 |
+
"a 1970s film frame of a woman waiting at a rainy Tokyo bus stop, lit by a vending machine",
|
| 202 |
+
"isometric cutaway of a tiny underground bakery",
|
| 203 |
+
"a poster for a jazz festival in Lisbon",
|
| 204 |
]
|
| 205 |
|
| 206 |
+
COMPOSER_DEFAULTS = {"prompt": "", "aspect": AUTO, "resolution": "4 MP", "enhance": True}
|
| 207 |
+
|
| 208 |
+
COMPOSER_HTML = """
|
| 209 |
+
<div class="hx-card">
|
| 210 |
+
<div class="hx-head">
|
| 211 |
+
<div class="hx-seg" data-role="resolution" role="tablist">
|
| 212 |
+
<button type="button" class="hx-seg-btn" data-value="4 MP">4 MP</button>
|
| 213 |
+
<button type="button" class="hx-seg-btn" data-value="8 MP">8 MP</button>
|
| 214 |
+
<button type="button" class="hx-seg-btn" data-value="16 MP">16 MP</button>
|
| 215 |
+
</div>
|
| 216 |
+
<span class="hx-headhint">text-to-image · or edit up to 8 pictures</span>
|
| 217 |
+
</div>
|
| 218 |
+
<textarea class="hx-desc" data-role="prompt" rows="4"
|
| 219 |
+
placeholder="Describe your image… e.g. a poster for a jazz festival in Lisbon. To edit, add pictures below and refer to them as <Picture 1>, <Picture 2>…"></textarea>
|
| 220 |
+
<div class="hx-chips" data-role="idea-chips"></div>
|
| 221 |
+
<div class="hx-row">
|
| 222 |
+
<span class="hx-label">Aspect</span>
|
| 223 |
+
<div class="hx-ratios" data-role="aspect"></div>
|
| 224 |
+
</div>
|
| 225 |
+
<div class="hx-foot">
|
| 226 |
+
<label class="hx-toggle" title="Expand the prompt into a detailed caption (and pick the ratio when Aspect is Auto) with MiniMax-H3-Image-PE">
|
| 227 |
+
<input type="checkbox" data-role="enhance">✨ Enhance prompt
|
| 228 |
+
</label>
|
| 229 |
+
<span class="hx-spacer"></span>
|
| 230 |
+
<button type="button" class="hx-primary" data-role="generate">Generate</button>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
"""
|
| 234 |
+
|
| 235 |
+
COMPOSER_CSS = """
|
| 236 |
+
.hx-card { background: var(--block-background-fill); border: var(--block-border-width, 1px) solid var(--block-border-color, var(--border-color-primary)); border-radius: var(--block-radius, 12px); box-shadow: var(--block-shadow, none); padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
| 237 |
+
.hx-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
| 238 |
+
.hx-seg { display: inline-flex; background: var(--background-fill-secondary); border: 1px solid var(--border-color-primary); border-radius: 999px; padding: 3px; gap: 2px; }
|
| 239 |
+
.hx-seg-btn { border: none; background: transparent; color: var(--body-text-color-subdued); padding: 5px 14px; border-radius: 999px; font-family: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer; transition: background .15s, color .15s; }
|
| 240 |
+
.hx-seg-btn[aria-selected="true"] { background: var(--button-primary-background-fill); color: var(--button-primary-text-color); }
|
| 241 |
+
.hx-headhint { color: var(--body-text-color-subdued); font-size: 12.5px; }
|
| 242 |
+
.hx-card textarea { width: 100%; box-sizing: border-box; background: var(--input-background-fill); border: var(--input-border-width, 1px) solid var(--input-border-color, var(--border-color-primary)); border-radius: var(--input-radius, 8px); padding: 10px 12px; color: var(--body-text-color); font-family: inherit; font-size: 16px; line-height: 1.5; min-height: 118px; resize: vertical; transition: border-color .15s; }
|
| 243 |
+
.hx-card textarea::placeholder { color: var(--input-placeholder-color, var(--body-text-color-subdued)); }
|
| 244 |
+
.hx-card textarea:focus { outline: none; border-color: var(--input-border-color-focus, var(--color-accent)); }
|
| 245 |
+
.hx-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
| 246 |
+
.hx-chip { background: var(--button-secondary-background-fill); color: var(--button-secondary-text-color); border: 1px solid var(--button-secondary-border-color, var(--border-color-primary)); border-radius: 999px; padding: 4px 12px; font-family: inherit; font-size: 12.5px; cursor: pointer; transition: border-color .15s; text-align: left; }
|
| 247 |
+
.hx-chip:hover { border-color: var(--color-accent); }
|
| 248 |
+
.hx-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
| 249 |
+
.hx-label { color: var(--block-title-text-color, var(--body-text-color)); font-size: 13px; font-weight: 600; }
|
| 250 |
+
.hx-ratios { display: flex; flex-wrap: wrap; gap: 4px; }
|
| 251 |
+
.hx-ratio { background: transparent; color: var(--body-text-color-subdued); border: 1px dashed var(--border-color-primary); border-radius: 6px; padding: 3px 9px; font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
|
| 252 |
+
.hx-ratio:hover { color: var(--color-accent); border-color: var(--color-accent); }
|
| 253 |
+
.hx-ratio[aria-selected="true"] { border-style: solid; border-color: var(--color-accent); color: var(--color-accent); background: var(--background-fill-secondary); }
|
| 254 |
+
.hx-foot { display: flex; align-items: center; gap: 10px; }
|
| 255 |
+
.hx-spacer { flex: 1; }
|
| 256 |
+
.hx-toggle { display: inline-flex; align-items: center; gap: 7px; color: var(--body-text-color); font-size: 14px; cursor: pointer; user-select: none; }
|
| 257 |
+
.hx-toggle input { width: 16px; height: 16px; accent-color: var(--color-accent); cursor: pointer; }
|
| 258 |
+
.hx-primary { background: var(--button-primary-background-fill); color: var(--button-primary-text-color); border: var(--button-border-width, 1px) solid var(--button-primary-border-color, transparent); border-radius: var(--button-large-radius, var(--radius-lg, 8px)); padding: 10px 28px; font-family: inherit; font-size: var(--button-large-text-size, 16px); font-weight: 600; cursor: pointer; box-shadow: var(--button-primary-shadow, none); transition: background .15s, transform .05s; }
|
| 259 |
+
.hx-primary:hover { background: var(--button-primary-background-fill-hover, var(--button-primary-background-fill)); }
|
| 260 |
+
.hx-primary:active { transform: translateY(1px); }
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
COMPOSER_JS = """
|
| 264 |
+
const $ = function(s) { return element.querySelector(s); };
|
| 265 |
+
const $$ = function(s) { return Array.from(element.querySelectorAll(s)); };
|
| 266 |
+
const IDEAS = __IDEAS__;
|
| 267 |
+
const RATIOS = ['auto'].concat(__RATIOS__);
|
| 268 |
+
const prompt = $('[data-role="prompt"]');
|
| 269 |
+
const enhance = $('[data-role="enhance"]');
|
| 270 |
+
let aspect = 'auto', resolution = '4 MP', lastPushed = '';
|
| 271 |
+
|
| 272 |
+
RATIOS.forEach(function(r) {
|
| 273 |
+
const b = document.createElement('button');
|
| 274 |
+
b.type = 'button'; b.className = 'hx-ratio'; b.dataset.value = r;
|
| 275 |
+
b.textContent = r === 'auto' ? 'Auto' : r;
|
| 276 |
+
if (r === 'auto') b.title = 'Enhanced: the ratio PE recommends. Editing: Picture 1\\'s ratio. Otherwise 1:1.';
|
| 277 |
+
b.addEventListener('click', function() { setAspect(r); });
|
| 278 |
+
$('[data-role="aspect"]').appendChild(b);
|
| 279 |
+
});
|
| 280 |
+
IDEAS.forEach(function(t) {
|
| 281 |
+
const b = document.createElement('button');
|
| 282 |
+
b.type = 'button'; b.className = 'hx-chip'; b.textContent = t;
|
| 283 |
+
b.addEventListener('click', function() { prompt.value = t; prompt.focus(); });
|
| 284 |
+
$('[data-role="idea-chips"]').appendChild(b);
|
| 285 |
+
});
|
| 286 |
+
function setAspect(r) { aspect = r; $$('.hx-ratio').forEach(function(b) { b.setAttribute('aria-selected', String(b.dataset.value === r)); }); }
|
| 287 |
+
function setResolution(r) { resolution = r; $$('.hx-seg-btn').forEach(function(b) { b.setAttribute('aria-selected', String(b.dataset.value === r)); }); }
|
| 288 |
+
$$('.hx-seg-btn').forEach(function(b) { b.addEventListener('click', function() { setResolution(b.dataset.value); }); });
|
| 289 |
+
function readState() { return { prompt: prompt.value, aspect: aspect, resolution: resolution, enhance: enhance.checked }; }
|
| 290 |
+
function gather() { const s = readState(); lastPushed = JSON.stringify(s); props.value = s; }
|
| 291 |
+
function applyState(v) {
|
| 292 |
+
if (!v) return;
|
| 293 |
+
if (prompt.value !== (v.prompt || '')) prompt.value = v.prompt || '';
|
| 294 |
+
enhance.checked = !!v.enhance;
|
| 295 |
+
setAspect(v.aspect || 'auto');
|
| 296 |
+
setResolution(v.resolution || '4 MP');
|
| 297 |
+
}
|
| 298 |
+
$('[data-role="generate"]').addEventListener('click', function() { gather(); trigger('submit'); });
|
| 299 |
+
prompt.addEventListener('keydown', function(e) { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); gather(); trigger('submit'); } });
|
| 300 |
+
watch('value', function() { if (JSON.stringify(props.value) !== lastPushed) applyState(props.value); });
|
| 301 |
+
applyState(props.value);
|
| 302 |
+
""".replace("__IDEAS__", json.dumps(IDEAS)).replace("__RATIOS__", json.dumps(RATIOS))
|
| 303 |
+
|
| 304 |
+
CSS = """
|
| 305 |
+
#col-container { max-width: 1300px; margin: 0 auto; }
|
| 306 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 307 |
+
.html-container { padding: 0; }
|
| 308 |
+
.hx-title { text-align: center; margin: 14px 0 0; font-size: 34px; font-weight: 800; letter-spacing: -0.02em; }
|
| 309 |
+
.hx-title span { background: linear-gradient(90deg, var(--color-accent), var(--secondary-500, var(--color-accent))); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
| 310 |
+
#hx-links { text-align: center; }
|
| 311 |
+
"""
|
| 312 |
+
|
| 313 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="MiniMax H3 Image") as demo:
|
| 314 |
+
with gr.Column(elem_id="col-container"):
|
| 315 |
+
gr.HTML('<h1 class="hx-title">MiniMax <span>H3 Image</span></h1>', container=False, padding=False)
|
| 316 |
+
gr.Markdown(
|
| 317 |
+
"Text-to-image and editing of up to 8 pictures in 6 steps, with optional prompt enhancement by "
|
| 318 |
+
"MiniMax-H3-Image-PE "
|
| 319 |
+
"[[model]](https://huggingface.co/MiniMaxAI/MiniMax-H3-Image) | "
|
| 320 |
+
"[[prompt enhancer]](https://huggingface.co/MiniMaxAI/MiniMax-H3-Image-PE) | "
|
| 321 |
+
"[[diffusers PR]](https://github.com/huggingface/diffusers-new-model-addition-minimax/pull/1)",
|
| 322 |
+
elem_id="hx-links",
|
| 323 |
+
)
|
| 324 |
+
with gr.Row():
|
| 325 |
+
with gr.Column():
|
| 326 |
+
composer = gr.HTML(
|
| 327 |
+
value=dict(COMPOSER_DEFAULTS),
|
| 328 |
+
html_template=COMPOSER_HTML,
|
| 329 |
+
css_template=COMPOSER_CSS,
|
| 330 |
+
js_on_load=COMPOSER_JS,
|
| 331 |
+
container=False,
|
| 332 |
+
padding=False,
|
| 333 |
+
)
|
| 334 |
+
images = gr.Gallery(
|
| 335 |
+
label="Pictures to edit (optional, up to 8) — refer to them as <Picture 1>, <Picture 2>, …",
|
| 336 |
+
type="filepath", columns=4, height=200,
|
| 337 |
+
)
|
| 338 |
+
with gr.Accordion("Advanced", open=False):
|
| 339 |
+
effort = gr.Radio(["low", "medium", "xhigh"], value="low", label="Prompt enhancer reasoning effort",
|
| 340 |
+
info="xhigh is the released default; low thinks briefly and is several times faster")
|
| 341 |
+
with gr.Row():
|
| 342 |
+
seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
|
| 343 |
+
randomize = gr.Checkbox(label="Randomize seed", value=True)
|
| 344 |
+
with gr.Column():
|
| 345 |
+
output = gr.Image(label="Result", type="filepath", format="png", show_label=False, height=620)
|
| 346 |
+
stats = gr.Markdown()
|
| 347 |
+
with gr.Accordion("Prompt used", open=False):
|
| 348 |
+
prompt_used = gr.Textbox(show_label=False, lines=8)
|
| 349 |
+
with gr.Accordion("Enhancer thinking", open=False):
|
| 350 |
+
thinking = gr.Textbox(show_label=False, lines=10)
|
| 351 |
+
|
| 352 |
+
composer.submit(
|
| 353 |
+
studio, [composer, images, seed, randomize, effort], [output, stats, prompt_used, thinking, seed],
|
| 354 |
+
show_progress="minimal", api_visibility="private",
|
| 355 |
)
|
| 356 |
+
|
| 357 |
+
# Plain API endpoint (also what the composer runs, minus the streaming UI updates).
|
| 358 |
+
api_prompt = gr.Textbox(visible=False)
|
| 359 |
+
api_aspect = gr.Textbox(visible=False, value=AUTO)
|
| 360 |
+
api_resolution = gr.Textbox(visible=False, value="4 MP")
|
| 361 |
+
api_enhance = gr.Checkbox(visible=False, value=False)
|
| 362 |
+
api_effort = gr.Textbox(visible=False, value="low")
|
| 363 |
+
api_button = gr.Button(visible=False)
|
| 364 |
+
api_button.click(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
generate,
|
| 366 |
+
[api_prompt, images, api_aspect, api_resolution, seed, randomize, api_enhance, api_effort],
|
| 367 |
+
[output, seed, prompt_used],
|
| 368 |
api_name="generate",
|
| 369 |
)
|
| 370 |
|