Spaces:
Running on Zero
Running on Zero
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -34,7 +34,7 @@ from pathlib import Path
|
|
| 34 |
import gradio as gr
|
| 35 |
import torch
|
| 36 |
from PIL import Image
|
| 37 |
-
from huggingface_hub import hf_hub_download
|
| 38 |
|
| 39 |
from diffusers import ModularPipeline
|
| 40 |
|
|
@@ -48,6 +48,8 @@ HF_TOKEN = os.environ.get("HF_TOKEN")
|
|
| 48 |
# when switching to the public klein repos.
|
| 49 |
LINK_DISTILLED = os.environ.get("F3_LINK_DISTILLED", "https://huggingface.co/black-forest-labs/flux-3-image-klein")
|
| 50 |
LINK_BASE = os.environ.get("F3_LINK_BASE", "https://huggingface.co/black-forest-labs/flux-3-image-klein-base")
|
|
|
|
|
|
|
| 51 |
|
| 52 |
MAX_SEED = 2**63 - 1
|
| 53 |
DEFAULT_STEPS = int(os.environ.get("F3_STEPS", "16")) # distilled default
|
|
@@ -169,6 +171,59 @@ def _decode_ref(data_uri):
|
|
| 169 |
return img
|
| 170 |
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
def _duration(state_json, *_):
|
| 173 |
try:
|
| 174 |
st = json.loads(state_json) if state_json else {}
|
|
@@ -191,7 +246,8 @@ def _generate(state_json):
|
|
| 191 |
scene = st.get("scene") or ""
|
| 192 |
aspect = st.get("aspect") or "1:1"
|
| 193 |
|
| 194 |
-
|
|
|
|
| 195 |
if not full_prompt.strip():
|
| 196 |
raise gr.Error("Add at least a scene prompt or one described element.")
|
| 197 |
|
|
@@ -251,9 +307,15 @@ os.makedirs(CACHE_DIR, exist_ok=True)
|
|
| 251 |
_CACHE_MEM = {}
|
| 252 |
|
| 253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
def _cache_key(st):
|
| 255 |
if st.get("ref_image"):
|
| 256 |
return None # i2i is not a fixed example
|
|
|
|
|
|
|
| 257 |
seed = st.get("seed")
|
| 258 |
if not (isinstance(seed, (int, float)) and seed >= 0):
|
| 259 |
return None # random seed -> not a reproducible example
|
|
@@ -293,6 +355,17 @@ def generate(state_json):
|
|
| 293 |
st = json.loads(state_json) if state_json else {}
|
| 294 |
except Exception:
|
| 295 |
st = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
key = _cache_key(st)
|
| 297 |
if key:
|
| 298 |
cached = _cache_get(key)
|
|
@@ -380,7 +453,8 @@ RATIO_LIST = list(RATIOS.keys())
|
|
| 380 |
CONFIG_JS = (
|
| 381 |
"<script>window.F3_PRESETS = " + json.dumps(PRESETS) + ";"
|
| 382 |
"window.F3_RATIOS = " + json.dumps(RATIO_LIST) + ";"
|
| 383 |
-
"window.F3_LINKS = " + json.dumps({"distilled": LINK_DISTILLED, "base": LINK_BASE
|
|
|
|
| 384 |
"window.F3_PRESET_ORDER = " + json.dumps(list(PRESETS.keys())) + ";</script>"
|
| 385 |
)
|
| 386 |
|
|
@@ -510,12 +584,18 @@ STYLE2 = """
|
|
| 510 |
#f3-root .titlebar .tb-text{min-width:0;}
|
| 511 |
#f3-root .titlebar h1{margin:0 0 4px;font-size:22px;font-weight:650;color:var(--txt);letter-spacing:-.01em;}
|
| 512 |
#f3-root .titlebar p{margin:0;color:var(--dim);font-size:13.5px;max-width:760px;line-height:1.5;}
|
| 513 |
-
/* klein-style
|
| 514 |
#f3-root .f3-links{display:flex;gap:8px;flex-wrap:wrap;}
|
| 515 |
-
#f3-root .f3-links a,#f3-root .f3-links a:visited{text-decoration:none;font-size:12.5px;padding:6px
|
| 516 |
border:1px solid var(--line2);color:var(--txt);background:var(--panel);white-space:nowrap;transition:border-color .12s,color .12s;}
|
| 517 |
#f3-root .f3-links a:hover{border-color:var(--violet);color:var(--violet);}
|
| 518 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
/* reference image block (i2i) */
|
| 520 |
#f3-root .refblock{padding:14px 18px;border-bottom:1px solid var(--line2);}
|
| 521 |
#f3-root .refblock>label{display:block;font:600 11px/1.4 var(--mono);letter-spacing:.06em;color:var(--dim2);margin-bottom:9px;}
|
|
@@ -583,7 +663,7 @@ SCRIPT = r"""
|
|
| 583 |
["Prompt","One line describing the whole scene — or the edit, if you added a reference image."],
|
| 584 |
];
|
| 585 |
let S = {aspect:"1:1", elements:[], scene:"", seed:-1, rendered:false, step:1, showBoxes:true,
|
| 586 |
-
refImage:null, refW:0, refH:0};
|
| 587 |
let seq = 0, sel = null;
|
| 588 |
|
| 589 |
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
|
|
@@ -608,7 +688,7 @@ SCRIPT = r"""
|
|
| 608 |
}
|
| 609 |
function syncState(){
|
| 610 |
setHidden('f3-state', JSON.stringify({
|
| 611 |
-
aspect:S.aspect, scene:S.scene, seed:S.seed, ref_image:S.refImage,
|
| 612 |
elements:S.elements.map(e=>({id:e.id,bbox:e.bbox,desc:e.desc,color:e.color})),
|
| 613 |
}));
|
| 614 |
}
|
|
@@ -899,8 +979,10 @@ SCRIPT = r"""
|
|
| 899 |
+ '<p>Draw a box for every element, describe it, and the step-distilled FLUX.3 model renders your layout. '
|
| 900 |
+ 'Upload a reference image to edit it with the same boxes.</p></div>'
|
| 901 |
+ '<div class="f3-links">'
|
| 902 |
-
+ '<a href="'+((window.F3_LINKS||{}).distilled||'#')+'" target="_blank" rel="noopener">Distilled
|
| 903 |
-
+ '<a href="'+((window.F3_LINKS||{}).base||'#')+'" target="_blank" rel="noopener">Base
|
|
|
|
|
|
|
| 904 |
+ '</div>'
|
| 905 |
+'</div>'
|
| 906 |
// INPUT BAR — the type-here / drop-here controls, in their own distinct zone above the canvas
|
|
@@ -912,7 +994,9 @@ SCRIPT = r"""
|
|
| 912 |
+ '<button class="refclear">Remove</button></div></div>'
|
| 913 |
+ '<input type="file" accept="image/*" class="reffile" style="display:none"></div>'
|
| 914 |
+ '<div class="ib-prompt"><label class="ib-lab">PROMPT</label>'
|
| 915 |
-
+ '<textarea rows="2" placeholder="Describe the whole scene in one line — or the edit, if you added a reference image."></textarea>
|
|
|
|
|
|
|
| 916 |
+ '<div class="ib-gen"><label class="ib-lab" style="visibility:hidden">GO</label><button class="genbtn">Generate</button></div>'
|
| 917 |
+'</div>'
|
| 918 |
+'<div class="main">'
|
|
@@ -964,6 +1048,7 @@ SCRIPT = r"""
|
|
| 964 |
replayBtn:root.querySelector('.replayBtn'), cnt:root.querySelector('.table-bar .cnt'),
|
| 965 |
etable:root.querySelector('.etable'),
|
| 966 |
reuseBtn:root.querySelector('.reuseBtn'), clearBtn:root.querySelector('.clearBtn'),
|
|
|
|
| 967 |
refBox:root.querySelector('.refbox'), refThumb:root.querySelector('.refthumb'),
|
| 968 |
reffile:root.querySelector('.reffile') };
|
| 969 |
|
|
@@ -974,6 +1059,8 @@ SCRIPT = r"""
|
|
| 974 |
NS.el.clearBtn.addEventListener('click',clearCanvas);
|
| 975 |
NS.el.reuseBtn.addEventListener('click',reuseAsInput);
|
| 976 |
NS.el.toggleBtn.addEventListener('click',()=>{ S.showBoxes=!S.showBoxes; updateToggle(); });
|
|
|
|
|
|
|
| 977 |
root.querySelector('.table-bar').addEventListener('click',()=>NS.el.etable.classList.toggle('open'));
|
| 978 |
window.addEventListener('resize',()=>{ layoutBoard(); });
|
| 979 |
|
|
|
|
| 34 |
import gradio as gr
|
| 35 |
import torch
|
| 36 |
from PIL import Image
|
| 37 |
+
from huggingface_hub import hf_hub_download, InferenceClient
|
| 38 |
|
| 39 |
from diffusers import ModularPipeline
|
| 40 |
|
|
|
|
| 48 |
# when switching to the public klein repos.
|
| 49 |
LINK_DISTILLED = os.environ.get("F3_LINK_DISTILLED", "https://huggingface.co/black-forest-labs/flux-3-image-klein")
|
| 50 |
LINK_BASE = os.environ.get("F3_LINK_BASE", "https://huggingface.co/black-forest-labs/flux-3-image-klein-base")
|
| 51 |
+
LINK_CODE = os.environ.get("F3_LINK_CODE", "https://github.com/black-forest-labs/flux-3-image-klein")
|
| 52 |
+
LINK_BOXES = os.environ.get("F3_LINK_BOXES", "https://docs.bfl.ai/flux_3/flux3_image_bounding_boxes")
|
| 53 |
|
| 54 |
MAX_SEED = 2**63 - 1
|
| 55 |
DEFAULT_STEPS = int(os.environ.get("F3_STEPS", "16")) # distilled default
|
|
|
|
| 171 |
return img
|
| 172 |
|
| 173 |
|
| 174 |
+
# ---- optional VLM prompt upsampling (klein-style) ---------------------------
|
| 175 |
+
VLM_MODEL = os.environ.get("F3_VLM_MODEL", "Qwen/Qwen3-VL-235B-A22B-Instruct")
|
| 176 |
+
_hf_client = InferenceClient(api_key=os.environ.get("INFERENCE_TOKEN") or HF_TOKEN)
|
| 177 |
+
|
| 178 |
+
SYSTEM_PROMPT_TEXT_ONLY = """You are an expert prompt engineer for FLUX.3 by Black Forest Labs. Rewrite user prompts into a full scene caption while strictly preserving their core subject and intent.
|
| 179 |
+
|
| 180 |
+
Guidelines:
|
| 181 |
+
1. Structure: Keep structured inputs structured (enhance within fields). Convert natural language to detailed paragraphs.
|
| 182 |
+
2. Details: Describe the subject, composition, lighting (quality, direction, color), atmosphere, textures, materials, spatial relationships, and environmental context.
|
| 183 |
+
3. Text in Images: Put ALL text in quotation marks, matching the prompt's language. Always provide explicit quoted text for objects that would contain text in reality (signs, labels, screens, etc.) - without it, the model generates gibberish.
|
| 184 |
+
|
| 185 |
+
Output only the revised prompt and nothing else."""
|
| 186 |
+
|
| 187 |
+
SYSTEM_PROMPT_WITH_IMAGES = """You are FLUX.3 by Black Forest Labs, an image-editing expert. You turn an editing request into a complete caption of the target image: describe the reference image(s) in full with the requested edit folded in (60-120 words).
|
| 188 |
+
|
| 189 |
+
Rules:
|
| 190 |
+
- One caption only, no commentary
|
| 191 |
+
- Use clear, analytical language (avoid "whimsical," "cascading," etc.)
|
| 192 |
+
- Keep everything the request does not change (faces, lighting, composition) and describe it as it is
|
| 193 |
+
- Refer to the references as <ref_image_0>, <ref_image_1>, ... in the order given ("the person from <ref_image_0>")
|
| 194 |
+
- Make abstractions concrete ("futuristic" -> "glowing cyan neon, metallic panels")
|
| 195 |
+
- Keep content PG-13
|
| 196 |
+
|
| 197 |
+
Output only the final caption in plain text and nothing else."""
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _img_data_uri(img):
|
| 201 |
+
buf = io.BytesIO()
|
| 202 |
+
img.save(buf, format="PNG")
|
| 203 |
+
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _upsample_prompt(prompt, image_list=None):
|
| 207 |
+
"""Rewrite the prompt into a full scene caption via a VLM; returns the original
|
| 208 |
+
prompt on any failure so generation always proceeds."""
|
| 209 |
+
try:
|
| 210 |
+
if image_list:
|
| 211 |
+
content = [{"type": "text", "text": prompt}]
|
| 212 |
+
for img in image_list:
|
| 213 |
+
content.append({"type": "image_url", "image_url": {"url": _img_data_uri(img)}})
|
| 214 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT_WITH_IMAGES},
|
| 215 |
+
{"role": "user", "content": content}]
|
| 216 |
+
else:
|
| 217 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT_TEXT_ONLY},
|
| 218 |
+
{"role": "user", "content": prompt}]
|
| 219 |
+
r = _hf_client.chat.completions.create(model=VLM_MODEL, messages=messages, max_tokens=1024)
|
| 220 |
+
out = (r.choices[0].message.content or "").strip()
|
| 221 |
+
return out or prompt
|
| 222 |
+
except Exception as e:
|
| 223 |
+
print(f"[app] prompt upsampling failed, using raw prompt: {e!r}", flush=True)
|
| 224 |
+
return prompt
|
| 225 |
+
|
| 226 |
+
|
| 227 |
def _duration(state_json, *_):
|
| 228 |
try:
|
| 229 |
st = json.loads(state_json) if state_json else {}
|
|
|
|
| 246 |
scene = st.get("scene") or ""
|
| 247 |
aspect = st.get("aspect") or "1:1"
|
| 248 |
|
| 249 |
+
# `generate` may have pre-upsampled the prompt (off-GPU); use it if present.
|
| 250 |
+
full_prompt = st.get("_final_prompt") or _build_prompt(scene, elements)
|
| 251 |
if not full_prompt.strip():
|
| 252 |
raise gr.Error("Add at least a scene prompt or one described element.")
|
| 253 |
|
|
|
|
| 307 |
_CACHE_MEM = {}
|
| 308 |
|
| 309 |
|
| 310 |
+
def _has_boxes(st):
|
| 311 |
+
return any((e.get("desc") or "").strip() for e in (st.get("elements") or []))
|
| 312 |
+
|
| 313 |
+
|
| 314 |
def _cache_key(st):
|
| 315 |
if st.get("ref_image"):
|
| 316 |
return None # i2i is not a fixed example
|
| 317 |
+
if st.get("prompt_upsampling") and not _has_boxes(st):
|
| 318 |
+
return None # VLM upsampling is non-deterministic -> don't cache
|
| 319 |
seed = st.get("seed")
|
| 320 |
if not (isinstance(seed, (int, float)) and seed >= 0):
|
| 321 |
return None # random seed -> not a reproducible example
|
|
|
|
| 355 |
st = json.loads(state_json) if state_json else {}
|
| 356 |
except Exception:
|
| 357 |
st = {}
|
| 358 |
+
# Optional VLM prompt upsampling, done OFF-GPU before allocation. A prompt that
|
| 359 |
+
# carries a box table (layout) is sent as-is, matching the klein space.
|
| 360 |
+
if st.get("prompt_upsampling") and not _has_boxes(st):
|
| 361 |
+
base = _build_prompt(st.get("scene") or "", st.get("elements") or [])
|
| 362 |
+
if base.strip():
|
| 363 |
+
ref = _decode_ref(st.get("ref_image"))
|
| 364 |
+
up = _upsample_prompt(base, [ref] if ref is not None else None)
|
| 365 |
+
if up and up.strip():
|
| 366 |
+
st["_final_prompt"] = up
|
| 367 |
+
state_json = json.dumps(st)
|
| 368 |
+
print(f"[app] upsampled prompt -> {up[:90]!r}", flush=True)
|
| 369 |
key = _cache_key(st)
|
| 370 |
if key:
|
| 371 |
cached = _cache_get(key)
|
|
|
|
| 453 |
CONFIG_JS = (
|
| 454 |
"<script>window.F3_PRESETS = " + json.dumps(PRESETS) + ";"
|
| 455 |
"window.F3_RATIOS = " + json.dumps(RATIO_LIST) + ";"
|
| 456 |
+
"window.F3_LINKS = " + json.dumps({"distilled": LINK_DISTILLED, "base": LINK_BASE,
|
| 457 |
+
"code": LINK_CODE, "boxes": LINK_BOXES}) + ";"
|
| 458 |
"window.F3_PRESET_ORDER = " + json.dumps(list(PRESETS.keys())) + ";</script>"
|
| 459 |
)
|
| 460 |
|
|
|
|
| 584 |
#f3-root .titlebar .tb-text{min-width:0;}
|
| 585 |
#f3-root .titlebar h1{margin:0 0 4px;font-size:22px;font-weight:650;color:var(--txt);letter-spacing:-.01em;}
|
| 586 |
#f3-root .titlebar p{margin:0;color:var(--dim);font-size:13.5px;max-width:760px;line-height:1.5;}
|
| 587 |
+
/* klein-style button links */
|
| 588 |
#f3-root .f3-links{display:flex;gap:8px;flex-wrap:wrap;}
|
| 589 |
+
#f3-root .f3-links a,#f3-root .f3-links a:visited{text-decoration:none;font-size:12.5px;padding:6px 12px;border-radius:8px;
|
| 590 |
border:1px solid var(--line2);color:var(--txt);background:var(--panel);white-space:nowrap;transition:border-color .12s,color .12s;}
|
| 591 |
#f3-root .f3-links a:hover{border-color:var(--violet);color:var(--violet);}
|
| 592 |
|
| 593 |
+
/* prompt-upsampling toggle (under the prompt in the input bar) */
|
| 594 |
+
#f3-root .ib-up{margin-top:9px;}
|
| 595 |
+
#f3-root .ib-up .btn{font-size:12px;color:var(--dim);}
|
| 596 |
+
#f3-root .ib-up .btn.on{color:var(--txt);}
|
| 597 |
+
#f3-root .ib-up .btn .hintq{color:var(--dim2);font-size:11px;}
|
| 598 |
+
|
| 599 |
/* reference image block (i2i) */
|
| 600 |
#f3-root .refblock{padding:14px 18px;border-bottom:1px solid var(--line2);}
|
| 601 |
#f3-root .refblock>label{display:block;font:600 11px/1.4 var(--mono);letter-spacing:.06em;color:var(--dim2);margin-bottom:9px;}
|
|
|
|
| 663 |
["Prompt","One line describing the whole scene — or the edit, if you added a reference image."],
|
| 664 |
];
|
| 665 |
let S = {aspect:"1:1", elements:[], scene:"", seed:-1, rendered:false, step:1, showBoxes:true,
|
| 666 |
+
refImage:null, refW:0, refH:0, upsample:false};
|
| 667 |
let seq = 0, sel = null;
|
| 668 |
|
| 669 |
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
|
|
|
|
| 688 |
}
|
| 689 |
function syncState(){
|
| 690 |
setHidden('f3-state', JSON.stringify({
|
| 691 |
+
aspect:S.aspect, scene:S.scene, seed:S.seed, ref_image:S.refImage, prompt_upsampling:S.upsample,
|
| 692 |
elements:S.elements.map(e=>({id:e.id,bbox:e.bbox,desc:e.desc,color:e.color})),
|
| 693 |
}));
|
| 694 |
}
|
|
|
|
| 979 |
+ '<p>Draw a box for every element, describe it, and the step-distilled FLUX.3 model renders your layout. '
|
| 980 |
+ 'Upload a reference image to edit it with the same boxes.</p></div>'
|
| 981 |
+ '<div class="f3-links">'
|
| 982 |
+
+ '<a href="'+((window.F3_LINKS||{}).distilled||'#')+'" target="_blank" rel="noopener">Distilled weights ↗</a>'
|
| 983 |
+
+ '<a href="'+((window.F3_LINKS||{}).base||'#')+'" target="_blank" rel="noopener">Base weights ↗</a>'
|
| 984 |
+
+ '<a href="'+((window.F3_LINKS||{}).code||'#')+'" target="_blank" rel="noopener">Code ↗</a>'
|
| 985 |
+
+ '<a href="'+((window.F3_LINKS||{}).boxes||'#')+'" target="_blank" rel="noopener">Bounding boxes ↗</a>'
|
| 986 |
+ '</div>'
|
| 987 |
+'</div>'
|
| 988 |
// INPUT BAR — the type-here / drop-here controls, in their own distinct zone above the canvas
|
|
|
|
| 994 |
+ '<button class="refclear">Remove</button></div></div>'
|
| 995 |
+ '<input type="file" accept="image/*" class="reffile" style="display:none"></div>'
|
| 996 |
+ '<div class="ib-prompt"><label class="ib-lab">PROMPT</label>'
|
| 997 |
+
+ '<textarea rows="2" placeholder="Describe the whole scene in one line — or the edit, if you added a reference image."></textarea>'
|
| 998 |
+
+ '<div class="ib-up"><span class="btn upBtn"><span class="toggle"></span>Enhance prompt '
|
| 999 |
+
+ '<span class="hintq">· VLM, text prompts only</span></span></div></div>'
|
| 1000 |
+ '<div class="ib-gen"><label class="ib-lab" style="visibility:hidden">GO</label><button class="genbtn">Generate</button></div>'
|
| 1001 |
+'</div>'
|
| 1002 |
+'<div class="main">'
|
|
|
|
| 1048 |
replayBtn:root.querySelector('.replayBtn'), cnt:root.querySelector('.table-bar .cnt'),
|
| 1049 |
etable:root.querySelector('.etable'),
|
| 1050 |
reuseBtn:root.querySelector('.reuseBtn'), clearBtn:root.querySelector('.clearBtn'),
|
| 1051 |
+
upBtn:root.querySelector('.upBtn'),
|
| 1052 |
refBox:root.querySelector('.refbox'), refThumb:root.querySelector('.refthumb'),
|
| 1053 |
reffile:root.querySelector('.reffile') };
|
| 1054 |
|
|
|
|
| 1059 |
NS.el.clearBtn.addEventListener('click',clearCanvas);
|
| 1060 |
NS.el.reuseBtn.addEventListener('click',reuseAsInput);
|
| 1061 |
NS.el.toggleBtn.addEventListener('click',()=>{ S.showBoxes=!S.showBoxes; updateToggle(); });
|
| 1062 |
+
NS.el.upBtn.addEventListener('click',()=>{ S.upsample=!S.upsample;
|
| 1063 |
+
NS.el.upBtn.classList.toggle('on',S.upsample); syncState(); });
|
| 1064 |
root.querySelector('.table-bar').addEventListener('click',()=>NS.el.etable.classList.toggle('open'));
|
| 1065 |
window.addEventListener('resize',()=>{ layoutBoard(); });
|
| 1066 |
|