tyaphon commited on
Commit
c90af4c
·
verified ·
1 Parent(s): 0e48ed7

Restore FLUX.2 Klein 9B (Fibo evaluation complete)

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +201 -202
  3. requirements.txt +1 -0
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,202 +1,201 @@
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.
18
- import spaces
19
-
20
- import base64
21
- import random
22
- import re
23
- import traceback
24
- from io import BytesIO
25
-
26
- import gradio as gr
27
- import numpy as np
28
- import torch
29
- from diffusers import BriaFiboEditPipeline
30
-
31
- MODEL = "briaai/Fibo-Edit-1.5-turbo"
32
-
33
- dtype = torch.bfloat16
34
- pipe = BriaFiboEditPipeline.from_pretrained(MODEL, torch_dtype=dtype)
35
-
36
- # Eagerly at module scope: ZeroGPU intercepts this, packs the weights to disk
37
- # and streams them in on the first decorated call.
38
- pipe = pipe.to("cuda")
39
-
40
- MAX_SEED = np.iinfo(np.int32).max
41
-
42
- # The card is explicit: distilled from 30 steps to 4, and CFG 1.0 because no
43
- # classifier-free guidance is needed. Raising either is wasted quota.
44
- STEPS = 4
45
- GUIDANCE = 1.0
46
-
47
- # PROMPTS ARE JSON, NOT PROSE, AND THIS IS ENFORCED.
48
- #
49
- # check_inputs runs is_valid_edit_json on every prompt and raises ValueError on
50
- # anything without an "edit_instruction" key, so plain prose does not reach the
51
- # model at all — it dies before the first step. Bria build that JSON with a
52
- # Gemini-backed VLM ("VGL JSON") which needs a GEMINI_API_KEY we do not have.
53
- #
54
- # The validator is far laxer than the schema: any JSON object carrying
55
- # edit_instruction passes, and the pipeline then json.dumps it and tokenises the
56
- # result as ordinary text. So a wrapper is enough to make the model reachable,
57
- # which is what _to_vgl below does. Two consequences worth stating plainly for
58
- # whoever reads the evaluation:
59
- #
60
- # - The other VGL fields (short_description, objects, lighting, aesthetics …)
61
- # need a captioner to fill and are omitted. The model gets less conditioning
62
- # than Bria intend it to have.
63
- # - The format wants ONE imperative sentence in edit_instruction and forbids
64
- # negative constraints elsewhere. DressFit prompts are the opposite: long,
65
- # and mostly a list of things not to do. That mismatch is a real handicap,
66
- # so the harness also sends a short imperative variant of each case to tell
67
- # a weak model apart from a badly-shaped prompt.
68
-
69
-
70
- def _to_vgl(prompt: str) -> str:
71
- """Coerce a prompt into the edit JSON the pipeline demands.
72
-
73
- Already-JSON prompts carrying edit_instruction pass through untouched, so a
74
- caller that builds proper VGL keeps full control. Prose is wrapped, with
75
- "image 1"/"image 2" rewritten to the <image_N> reference tokens the
76
- multi-reference format uses.
77
- """
78
- import json as _json
79
- text = (prompt or "").strip()
80
- if text.startswith("{"):
81
- try:
82
- parsed = _json.loads(text)
83
- if isinstance(parsed, dict) and "edit_instruction" in parsed:
84
- return text
85
- except _json.JSONDecodeError:
86
- pass
87
-
88
- instruction = re.sub(r"image\s*1", "<image_1>", text, flags=re.I)
89
- instruction = re.sub(r"image\s*2", "<image_2>", instruction, flags=re.I)
90
- return _json.dumps({"edit_instruction": instruction})
91
-
92
-
93
- def _duration(*a, **k):
94
- w = int(k.get("width") or 1024)
95
- h = int(k.get("height") or 1024)
96
- return min(120, max(30, round((w * h) / 50000)))
97
-
98
-
99
- @spaces.GPU(duration=_duration)
100
- def infer(image_1, image_2, prompt, seed=0, randomize_seed=True,
101
- guidance_scale=GUIDANCE, num_inference_steps=STEPS,
102
- width=1024, height=1024,
103
- progress=gr.Progress(track_tqdm=True)):
104
- """Edit one or more reference images according to a prompt."""
105
- if randomize_seed:
106
- seed = random.randint(0, MAX_SEED)
107
-
108
- images = [im for im in (image_1, image_2) if im is not None]
109
- if not images:
110
- raise gr.Error("At least one reference image is required.")
111
-
112
- out = pipe(
113
- # A LIST is multi-reference, not a batch — the card is explicit that
114
- # several references combine into one result.
115
- image=images if len(images) > 1 else images[0],
116
- prompt=_to_vgl(prompt),
117
- num_inference_steps=int(num_inference_steps),
118
- guidance_scale=float(guidance_scale),
119
- generator=torch.Generator("cuda").manual_seed(int(seed)),
120
- ).images[0]
121
-
122
- return out, seed
123
-
124
-
125
- with gr.Blocks(delete_cache=(3600, 86400)) as demo:
126
- with gr.Column():
127
- gr.HTML(
128
- """
129
- <div style="text-align:center;padding:4rem 1rem;
130
- font-family:system-ui,sans-serif;color:#999;font-size:.9rem">
131
- This Space has no public interface.
132
- </div>
133
- """
134
- )
135
- # Hidden, but present: removing the components removes the event
136
- # registration that publishes the API with them.
137
- with gr.Row(visible=False):
138
- image_1 = gr.Image(label="Image 1", type="pil")
139
- image_2 = gr.Image(label="Image 2", type="pil")
140
- prompt = gr.Text(label="Prompt")
141
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
142
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
143
- guidance = gr.Slider(label="Guidance", minimum=0.0, maximum=10.0, step=0.1, value=GUIDANCE)
144
- steps = gr.Slider(label="Steps", minimum=1, maximum=40, step=1, value=STEPS)
145
- run = gr.Button("Edit")
146
- result = gr.Image(label="Result")
147
- out_seed = gr.Number(label="Seed used")
148
-
149
- gr.on(
150
- triggers=[run.click, prompt.submit],
151
- fn=infer,
152
- inputs=[image_1, image_2, prompt, seed, randomize_seed, guidance, steps],
153
- outputs=[result, out_seed],
154
- )
155
-
156
-
157
- # ============================================================
158
- # API — same positional contract as the other edit Spaces, so the existing
159
- # client can call this one by changing a URL and nothing else.
160
- # ============================================================
161
- def _load(src):
162
- """Fetch an https URL or decode a data URL into a PIL image."""
163
- import base64 as _b64
164
- import urllib.request as _url
165
- from PIL import Image as _Image
166
- if src.startswith("http://") or src.startswith("https://"):
167
- req = _url.Request(src, headers={"User-Agent": "tecwyze/1.0"})
168
- with _url.urlopen(req, timeout=30) as resp:
169
- raw = resp.read(25 * 1024 * 1024)
170
- else:
171
- if src.startswith("data:"):
172
- src = src.split(",", 1)[1]
173
- raw = _b64.b64decode(src)
174
- return _Image.open(BytesIO(raw)).convert("RGB")
175
-
176
-
177
- def dressfit_tryon(person: str, garment: str = "", prompt: str = "",
178
- steps: float = STEPS, guidance: float = GUIDANCE,
179
- seed: float = 0, randomize: bool = True) -> dict:
180
- """Edit an image. person is the image to edit, garment is an optional second
181
- reference, prompt says what to change. Both are https or data URLs."""
182
- try:
183
- first = _load(person)
184
- second = _load(garment) if garment else None
185
- w, h = first.size
186
- w = max(256, min(2048, round(w / 32) * 32))
187
- h = max(256, min(2048, round(h / 32) * 32))
188
- out, used = infer(first, second, prompt, int(seed), bool(randomize),
189
- float(guidance), int(steps), w, h)
190
- buf = BytesIO()
191
- out.save(buf, format="PNG")
192
- return {"status": "success", "seed": int(used),
193
- "image": "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()}
194
- except Exception as exc:
195
- return {"status": "error", "message": "".join(traceback.format_exception(exc))[-2500:]}
196
-
197
-
198
- with demo:
199
- gr.api(dressfit_tryon, api_name="dressfit_tryon")
200
-
201
- if __name__ == "__main__":
202
- demo.launch()
 
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.
15
+ import spaces
16
+
17
+ import base64
18
+ import random
19
+ import traceback
20
+ from io import BytesIO
21
+
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)),
109
+ ).images[0]
110
+
111
+ return out, seed
112
+
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;
124
+ font-family:system-ui,sans-serif;color:#999;font-size:.9rem">
125
+ This Space has no public interface.
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")
132
+ prompt = gr.Text(label="Prompt")
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")
140
+
141
+ gr.on(
142
+ triggers=[run.click, prompt.submit],
143
+ fn=infer,
144
+ inputs=[image_1, image_2, prompt, seed, randomize_seed, guidance, steps],
145
+ outputs=[result, out_seed],
146
+ )
147
+
148
+
149
+ # ============================================================
150
+ # API
151
+ # ============================================================
152
+ def _load(src):
153
+ """Fetch an https URL or decode a data URL into a PIL image."""
154
+ import base64 as _b64
155
+ import urllib.request as _url
156
+ from PIL import Image as _Image
157
+ if src.startswith("http://") or src.startswith("https://"):
158
+ req = _url.Request(src, headers={"User-Agent": "tecwyze/1.0"})
159
+ with _url.urlopen(req, timeout=30) as resp:
160
+ raw = resp.read(25 * 1024 * 1024)
161
+ else:
162
+ if src.startswith("data:"):
163
+ src = src.split(",", 1)[1]
164
+ raw = _b64.b64decode(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
+
197
+ with demo:
198
+ gr.api(dressfit_tryon, api_name="dressfit_tryon")
199
+
200
+ if __name__ == "__main__":
201
+ demo.launch()
 
requirements.txt CHANGED
@@ -3,3 +3,4 @@ transformers
3
  accelerate
4
  safetensors
5
  sentencepiece
 
 
3
  accelerate
4
  safetensors
5
  sentencepiece
6
+ peft