tyaphon commited on
Commit
ef2835d
·
verified ·
1 Parent(s): 6eb99d7

FIBO-Edit 1.5 turbo — evaluation build

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +40 -79
  3. requirements.txt +0 -1
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: klein
3
- emoji: "🎨"
4
  colorFrom: gray
5
  colorTo: gray
6
  sdk: gradio
 
1
  ---
2
  title: klein
3
+ emoji: "🧪"
4
  colorFrom: gray
5
  colorTo: gray
6
  sdk: gradio
app.py CHANGED
@@ -1,14 +1,17 @@
1
- # FLUX.2 Klein 9B — reference-image editing with runtime LoRA.
2
  #
3
- # This is the model that answers both open requests at once. Qwen-Image-Edit
4
- # holds identity but renders glossy; Z-Image looks photographic but its editing
5
- # pipeline needs siglip weights Tongyi has not released; neither supports LoRA
6
- # on our stack. Klein is tagged image-to-image, takes one or more reference
7
- # images, and Flux2KleinPipeline carries load_lora_weights — so one checkpoint
8
- # covers generation, editing and per-character LoRA.
9
  #
10
- # Started on the 4B because it is Apache-2.0 and ungated. Its output was not
11
- # convincing enough, so this runs the 9B — see the note at MODEL below.
 
 
 
 
 
12
  #
13
  # `import spaces` first and stays first — it patches torch.cuda.*, and once CUDA
14
  # is initialised in the main process it is too late to intercept.
@@ -22,87 +25,55 @@ from io import BytesIO
22
  import gradio as gr
23
  import numpy as np
24
  import torch
25
- from diffusers import Flux2KleinPipeline
26
 
27
- # 9B, not the 4B this started on — the 4B's output was not convincing enough.
28
- # It is gated behind Black Forest Labs' flux-non-commercial-license, so the
29
- # account running this Space must accept the terms on the model page first, and
30
- # the Space needs an HF_TOKEN secret to authenticate the download. Without both,
31
- # the build dies on a 403 fetching model_index.json.
32
- MODEL = "black-forest-labs/FLUX.2-klein-9B"
33
 
34
  dtype = torch.bfloat16
35
- pipe = Flux2KleinPipeline.from_pretrained(MODEL, torch_dtype=dtype)
36
 
37
  # Eagerly at module scope: ZeroGPU intercepts this, packs the weights to disk
38
- # and streams them into VRAM on the first decorated call. Loading lazily inside
39
- # the decorator makes every caller pay the cost instead of nobody.
40
  pipe = pipe.to("cuda")
41
 
42
  MAX_SEED = np.iinfo(np.int32).max
43
- STEPS = 4 # Klein is distilled; the reference Space defaults to 4
44
- GUIDANCE = 2.5
45
-
46
- # Adapters are cached by name and never re-downloaded. Loading a LoRA per
47
- # request would re-fetch tens of megabytes every call, and the Space process
48
- # survives between calls, so the cache is free. Adapters also STACK if you do
49
- # not manage them — set_adapters replaces the active set, and disable_lora
50
- # turns them all off, which is what an unadorned request needs.
51
- LOADED = set()
52
-
53
-
54
- def _apply_loras(specs):
55
- """specs is a list of {repo, weight_name, scale}. Empty disables all."""
56
- if not specs:
57
- pipe.disable_lora()
58
- return []
59
- names, scales = [], []
60
- for i, spec in enumerate(specs):
61
- repo = spec.get("repo")
62
- if not repo:
63
- continue
64
- weight_name = spec.get("weight_name") or None
65
- name = spec.get("adapter_name") or f"a{abs(hash((repo, weight_name))) % 10**8}"
66
- if name not in LOADED:
67
- pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=name)
68
- LOADED.add(name)
69
- names.append(name)
70
- scales.append(float(spec.get("scale", 1.0)))
71
- if not names:
72
- pipe.disable_lora()
73
- return []
74
- pipe.set_adapters(names, adapter_weights=scales)
75
- return names
76
 
77
 
78
  def _duration(*a, **k):
79
- # ZeroGPU bills the duration REQUESTED but ABORTS anything that overruns,
80
- # so one flat number either wastes quota on small images or kills big ones.
81
  w = int(k.get("width") or 1024)
82
  h = int(k.get("height") or 1024)
83
- return min(120, max(30, round((w * h) / 40000)))
84
 
85
 
86
  @spaces.GPU(duration=_duration)
87
  def infer(image_1, image_2, prompt, seed=0, randomize_seed=True,
88
  guidance_scale=GUIDANCE, num_inference_steps=STEPS,
89
- width=1024, height=1024, loras=None,
90
  progress=gr.Progress(track_tqdm=True)):
91
- """Edit one or two reference images according to a prompt."""
92
  if randomize_seed:
93
  seed = random.randint(0, MAX_SEED)
94
 
95
- _apply_loras(loras or [])
96
-
97
  images = [im for im in (image_1, image_2) if im is not None]
98
  if not images:
99
  raise gr.Error("At least one reference image is required.")
100
 
101
  out = pipe(
 
 
102
  image=images if len(images) > 1 else images[0],
103
  prompt=prompt,
104
- width=int(width),
105
- height=int(height),
106
  num_inference_steps=int(num_inference_steps),
107
  guidance_scale=float(guidance_scale),
108
  generator=torch.Generator("cuda").manual_seed(int(seed)),
@@ -113,11 +84,6 @@ def infer(image_1, image_2, prompt, seed=0, randomize_seed=True,
113
 
114
  with gr.Blocks(delete_cache=(3600, 86400)) as demo:
115
  with gr.Column():
116
- # No public interface. This is a backend for one application, and a
117
- # landing page only attracts traffic that spends the ZeroGPU quota. The
118
- # controls below still exist, hidden, because hiding them removes the
119
- # interface without removing the event registration that publishes the
120
- # API — delete the components and the endpoint goes with them.
121
  gr.HTML(
122
  """
123
  <div style="text-align:center;padding:4rem 1rem;
@@ -126,6 +92,8 @@ with gr.Blocks(delete_cache=(3600, 86400)) as demo:
126
  </div>
127
  """
128
  )
 
 
129
  with gr.Row(visible=False):
130
  image_1 = gr.Image(label="Image 1", type="pil")
131
  image_2 = gr.Image(label="Image 2", type="pil")
@@ -133,7 +101,7 @@ with gr.Blocks(delete_cache=(3600, 86400)) as demo:
133
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
134
  randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
135
  guidance = gr.Slider(label="Guidance", minimum=0.0, maximum=10.0, step=0.1, value=GUIDANCE)
136
- steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=STEPS)
137
  run = gr.Button("Edit")
138
  result = gr.Image(label="Result")
139
  out_seed = gr.Number(label="Seed used")
@@ -147,7 +115,8 @@ with gr.Blocks(delete_cache=(3600, 86400)) as demo:
147
 
148
 
149
  # ============================================================
150
- # API
 
151
  # ============================================================
152
  def _load(src):
153
  """Fetch an https URL or decode a data URL into a PIL image."""
@@ -165,32 +134,24 @@ def _load(src):
165
  return _Image.open(BytesIO(raw)).convert("RGB")
166
 
167
 
168
- # Named dressfit_tryon because the existing client already calls that with this
169
- # positional order. A tidier name would break every caller for no gain.
170
  def dressfit_tryon(person: str, garment: str = "", prompt: str = "",
171
  steps: float = STEPS, guidance: float = GUIDANCE,
172
- seed: float = 0, randomize: bool = True,
173
- loras: list | None = None) -> dict:
174
  """Edit an image. person is the image to edit, garment is an optional second
175
- image, prompt says what to change, loras is an optional list of
176
- {repo, weight_name, scale}. Images are https or data URLs."""
177
  try:
178
  first = _load(person)
179
  second = _load(garment) if garment else None
180
- # Match the reference's own aspect. The pipeline draws a fresh canvas at
181
- # whatever size it is given, so forcing a square distorts a portrait.
182
  w, h = first.size
183
  w = max(256, min(2048, round(w / 32) * 32))
184
  h = max(256, min(2048, round(h / 32) * 32))
185
  out, used = infer(first, second, prompt, int(seed), bool(randomize),
186
- float(guidance), int(steps), w, h, loras or [])
187
  buf = BytesIO()
188
  out.save(buf, format="PNG")
189
  return {"status": "success", "seed": int(used),
190
  "image": "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()}
191
  except Exception as exc:
192
- # Return the traceback to the caller, not only the logs — the client is
193
- # the only thing watching and each log round trip costs an attempt.
194
  return {"status": "error", "message": "".join(traceback.format_exception(exc))[-2500:]}
195
 
196
 
 
1
+ # FIBO-Edit 1.5 turbo — evaluation build.
2
  #
3
+ # EVALUATION ONLY. The weights are CC BY-NC 4.0 and the gate form states that
4
+ # commercial use needs a separate agreement with Bria. DressFit is a commercial
5
+ # storefront, so nothing here may be pointed at vdf.tecwyze.lk. The purpose is
6
+ # to find out whether that agreement would be worth buying.
 
 
7
  #
8
+ # Why it is interesting for garment fitting specifically:
9
+ # - 8.3B, so it fits a ZeroGPU slot in bf16 with no FP8 and no torchao pin,
10
+ # which is the chain that made the 20B Qwen build painful.
11
+ # - Native multi-reference: image= takes a LIST, which is exactly the
12
+ # person-plus-garment shape DressFit needs.
13
+ # - Trained on fully licensed data, which is worth something to a retailer
14
+ # quite apart from output quality.
15
  #
16
  # `import spaces` first and stays first — it patches torch.cuda.*, and once CUDA
17
  # is initialised in the main process it is too late to intercept.
 
25
  import gradio as gr
26
  import numpy as np
27
  import torch
28
+ from diffusers import BriaFiboEditPipeline
29
 
30
+ MODEL = "briaai/Fibo-Edit-1.5-turbo"
 
 
 
 
 
31
 
32
  dtype = torch.bfloat16
33
+ pipe = BriaFiboEditPipeline.from_pretrained(MODEL, torch_dtype=dtype)
34
 
35
  # Eagerly at module scope: ZeroGPU intercepts this, packs the weights to disk
36
+ # and streams them in on the first decorated call.
 
37
  pipe = pipe.to("cuda")
38
 
39
  MAX_SEED = np.iinfo(np.int32).max
40
+
41
+ # The card is explicit: distilled from 30 steps to 4, and CFG 1.0 because no
42
+ # classifier-free guidance is needed. Raising either is wasted quota.
43
+ STEPS = 4
44
+ GUIDANCE = 1.0
45
+
46
+ # NOTE ON PROMPTS. Bria's example converts prose into a structured "VGL JSON"
47
+ # prompt using a Gemini-backed VLM block, which needs a GEMINI_API_KEY we do not
48
+ # have. This build passes prose straight through instead. If results look weak,
49
+ # that is a candidate explanation and not proof the model is weak — the JSON
50
+ # path is the one Bria tuned for.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
 
53
  def _duration(*a, **k):
 
 
54
  w = int(k.get("width") or 1024)
55
  h = int(k.get("height") or 1024)
56
+ return min(120, max(30, round((w * h) / 50000)))
57
 
58
 
59
  @spaces.GPU(duration=_duration)
60
  def infer(image_1, image_2, prompt, seed=0, randomize_seed=True,
61
  guidance_scale=GUIDANCE, num_inference_steps=STEPS,
62
+ width=1024, height=1024,
63
  progress=gr.Progress(track_tqdm=True)):
64
+ """Edit one or more reference images according to a prompt."""
65
  if randomize_seed:
66
  seed = random.randint(0, MAX_SEED)
67
 
 
 
68
  images = [im for im in (image_1, image_2) if im is not None]
69
  if not images:
70
  raise gr.Error("At least one reference image is required.")
71
 
72
  out = pipe(
73
+ # A LIST is multi-reference, not a batch — the card is explicit that
74
+ # several references combine into one result.
75
  image=images if len(images) > 1 else images[0],
76
  prompt=prompt,
 
 
77
  num_inference_steps=int(num_inference_steps),
78
  guidance_scale=float(guidance_scale),
79
  generator=torch.Generator("cuda").manual_seed(int(seed)),
 
84
 
85
  with gr.Blocks(delete_cache=(3600, 86400)) as demo:
86
  with gr.Column():
 
 
 
 
 
87
  gr.HTML(
88
  """
89
  <div style="text-align:center;padding:4rem 1rem;
 
92
  </div>
93
  """
94
  )
95
+ # Hidden, but present: removing the components removes the event
96
+ # registration that publishes the API with them.
97
  with gr.Row(visible=False):
98
  image_1 = gr.Image(label="Image 1", type="pil")
99
  image_2 = gr.Image(label="Image 2", type="pil")
 
101
  seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
102
  randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
103
  guidance = gr.Slider(label="Guidance", minimum=0.0, maximum=10.0, step=0.1, value=GUIDANCE)
104
+ steps = gr.Slider(label="Steps", minimum=1, maximum=40, step=1, value=STEPS)
105
  run = gr.Button("Edit")
106
  result = gr.Image(label="Result")
107
  out_seed = gr.Number(label="Seed used")
 
115
 
116
 
117
  # ============================================================
118
+ # API — same positional contract as the other edit Spaces, so the existing
119
+ # client can call this one by changing a URL and nothing else.
120
  # ============================================================
121
  def _load(src):
122
  """Fetch an https URL or decode a data URL into a PIL image."""
 
134
  return _Image.open(BytesIO(raw)).convert("RGB")
135
 
136
 
 
 
137
  def dressfit_tryon(person: str, garment: str = "", prompt: str = "",
138
  steps: float = STEPS, guidance: float = GUIDANCE,
139
+ seed: float = 0, randomize: bool = True) -> dict:
 
140
  """Edit an image. person is the image to edit, garment is an optional second
141
+ reference, prompt says what to change. Both are https or data URLs."""
 
142
  try:
143
  first = _load(person)
144
  second = _load(garment) if garment else None
 
 
145
  w, h = first.size
146
  w = max(256, min(2048, round(w / 32) * 32))
147
  h = max(256, min(2048, round(h / 32) * 32))
148
  out, used = infer(first, second, prompt, int(seed), bool(randomize),
149
+ float(guidance), int(steps), w, h)
150
  buf = BytesIO()
151
  out.save(buf, format="PNG")
152
  return {"status": "success", "seed": int(used),
153
  "image": "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()}
154
  except Exception as exc:
 
 
155
  return {"status": "error", "message": "".join(traceback.format_exception(exc))[-2500:]}
156
 
157
 
requirements.txt CHANGED
@@ -3,4 +3,3 @@ transformers
3
  accelerate
4
  safetensors
5
  sentencepiece
6
- peft
 
3
  accelerate
4
  safetensors
5
  sentencepiece