linoyts HF Staff commited on
Commit
57fc9b4
·
verified ·
1 Parent(s): 4286da6

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +97 -10
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
- full_prompt = _build_prompt(scene, elements)
 
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 pill links */
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 13px;border-radius:999px;
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 model ↗</a>'
903
- + '<a href="'+((window.F3_LINKS||{}).base||'#')+'" target="_blank" rel="noopener">Base model ↗</a>'
 
 
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></div>'
 
 
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