Nekochu commited on
Commit
73cfa03
·
0 Parent(s):

restore Klein 4B Space

Browse files
Files changed (4) hide show
  1. README.md +45 -0
  2. app.py +264 -0
  3. packages.txt +3 -0
  4. requirements.txt +5 -0
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FLUX.2 Klein 4B CPU
3
+ emoji: 🎨
4
+ colorFrom: pink
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.9.0
8
+ app_file: app.py
9
+ python_version: "3.11"
10
+ suggested_hardware: cpu-basic
11
+ startup_duration_timeout: 1h
12
+ preload_from_hub:
13
+ - unsloth/FLUX.2-klein-4B-GGUF flux-2-klein-4b-Q4_K_M.gguf
14
+ - Comfy-Org/vae-text-encorder-for-flux-klein-4b split_files/vae/flux2-vae.safetensors
15
+ short_description: Text-to-image and editing with FLUX.2 Klein on CPU
16
+ tags:
17
+ - text-to-image
18
+ - image-editing
19
+ - flux
20
+ - klein
21
+ - cpu
22
+ - gguf
23
+ license: apache-2.0
24
+ ---
25
+
26
+ # FLUX.2 Klein 4B on Free CPU
27
+
28
+ Generate and edit images with FLUX.2 Klein 4B. Supports LoRA search from HuggingFace Hub.
29
+
30
+ Klein 4B is the smallest model that does both text-to-image and image editing in one model (Apache 2.0).
31
+
32
+ Upload a reference image + describe your edit, or just type a prompt to generate from scratch.
33
+
34
+ - Engine: stable-diffusion.cpp (GGUF Q4_K_M)
35
+ - Text encoder: Uncensored Qwen3-4B (Cordux)
36
+ - Steps: 4 (distilled) / Resolution: 512x512 default
37
+ - LoRA: Search and load any Klein 4B LoRA from HuggingFace
38
+ - Hardware: CPU Basic (2 vCPU, 16GB RAM)
39
+
40
+ ## Credits
41
+
42
+ - [FLUX.2 Klein](https://bfl.ai/models/flux-2-klein) by Black Forest Labs
43
+ - [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) by leejet
44
+ - GGUF by [Unsloth](https://huggingface.co/unsloth)
45
+ - Uncensored encoder by [Cordux](https://huggingface.co/Cordux/flux2-klein-4B-uncensored-text-encoder)
app.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FLUX.2 Klein 4B - Free CPU Space with dynamic LoRA search from HuggingFace Hub"""
2
+
3
+ import os, time, gc, shutil
4
+ from pathlib import Path
5
+ from PIL import Image
6
+ import requests as req
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Thread config (cgroup-aware)
10
+ # ---------------------------------------------------------------------------
11
+ def get_cpu_count() -> int:
12
+ try:
13
+ with open("/sys/fs/cgroup/cpu.max") as f:
14
+ q, p = f.read().strip().split()
15
+ if q != "max": return max(1, int(q) // int(p))
16
+ except Exception: pass
17
+ try:
18
+ with open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") as f: q = int(f.read().strip())
19
+ with open("/sys/fs/cgroup/cpu/cpu.cfs_period_us") as f: p = int(f.read().strip())
20
+ if q > 0: return max(1, q // p)
21
+ except Exception: pass
22
+ return max(1, os.cpu_count() or 2)
23
+
24
+ N_THREADS = get_cpu_count()
25
+ for k in ["OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"]:
26
+ os.environ.setdefault(k, str(N_THREADS))
27
+ print(f"[init] CPU threads: {N_THREADS}")
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Model resolution
31
+ # ---------------------------------------------------------------------------
32
+ HF_CACHE = Path(os.environ.get("HF_HOME", Path.home() / ".cache" / "huggingface" / "hub"))
33
+
34
+ def find_model(filename: str) -> str:
35
+ for d in [Path("."), Path("models")]:
36
+ if (d / filename).exists(): return str(d / filename)
37
+ for p in HF_CACHE.rglob(filename): return str(p)
38
+ raise FileNotFoundError(f"Not found: {filename}")
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Load base models
42
+ # ---------------------------------------------------------------------------
43
+ from huggingface_hub import hf_hub_download, list_repo_files
44
+ from stable_diffusion_cpp import StableDiffusion
45
+
46
+ DIFFUSION_FILE = "flux-2-klein-4b-Q4_K_M.gguf"
47
+ LLM_FILE = "qwen3-4b-abl-q4_0.gguf"
48
+ VAE_FILE = "flux2-vae.safetensors"
49
+
50
+ print("[init] Locating models...")
51
+ diffusion_path = find_model(DIFFUSION_FILE)
52
+ vae_path = find_model(VAE_FILE)
53
+
54
+ try:
55
+ llm_path = find_model(LLM_FILE)
56
+ except FileNotFoundError:
57
+ print("[init] Downloading gated uncensored text encoder...")
58
+ llm_path = hf_hub_download(
59
+ repo_id="Cordux/flux2-klein-4B-uncensored-text-encoder",
60
+ filename=LLM_FILE, token=os.environ.get("HF_TOKEN"),
61
+ )
62
+
63
+ print(f"[init] Diffusion: {diffusion_path}")
64
+ print(f"[init] LLM: {llm_path}")
65
+ print(f"[init] VAE: {vae_path}")
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # LoRA management
69
+ # ---------------------------------------------------------------------------
70
+ LORA_DIR = "/tmp/loras"
71
+ os.makedirs(LORA_DIR, exist_ok=True)
72
+ DOWNLOADED_LORAS: dict[str, str] = {}
73
+
74
+
75
+ def fetch_all_loras(query: str = "") -> list[str]:
76
+ search = f"klein 4b {query}".strip()
77
+ try:
78
+ r = req.get("https://huggingface.co/api/models", params={
79
+ "search": search, "filter": "lora",
80
+ "sort": "downloads", "direction": "-1", "limit": 50,
81
+ }, timeout=10)
82
+ r.raise_for_status()
83
+ results = []
84
+ for m in r.json():
85
+ mid = m.get("id", "")
86
+ tags = m.get("tags", [])
87
+ if "lora" in tags or "lora" in mid.lower():
88
+ results.append(mid)
89
+ return results if results else []
90
+ except Exception as e:
91
+ print(f"[lora] Search error: {e}")
92
+ return []
93
+
94
+
95
+ def download_lora(repo_id: str) -> tuple[str, str]:
96
+ if not repo_id or repo_id.startswith("("):
97
+ return "", "Select a LoRA first"
98
+ try:
99
+ token = os.environ.get("HF_TOKEN")
100
+ files = list_repo_files(repo_id, token=token)
101
+ sf_files = [f for f in files if f.endswith(".safetensors")]
102
+ if not sf_files:
103
+ return "", f"No .safetensors found in {repo_id}"
104
+ target = sf_files[0]
105
+ for f in sf_files:
106
+ if "lora" in f.lower() or "adapter" in f.lower():
107
+ target = f
108
+ break
109
+ label = f"{repo_id}/{target}"
110
+ lora_name = label.replace("/", "_").replace("-", "_").replace(".", "_")
111
+ lora_name = lora_name.rsplit("_safetensors", 1)[0]
112
+ lora_dst = os.path.join(LORA_DIR, f"{lora_name}.safetensors")
113
+ if label in DOWNLOADED_LORAS:
114
+ size_mb = os.path.getsize(lora_dst) / 1024**2
115
+ return label, f"Already cached ({size_mb:.0f} MB)"
116
+ print(f"[lora] Downloading {repo_id}/{target}...")
117
+ src = hf_hub_download(repo_id=repo_id, filename=target, token=token)
118
+ shutil.copy2(src, lora_dst)
119
+ size_mb = os.path.getsize(lora_dst) / 1024**2
120
+ DOWNLOADED_LORAS[label] = lora_name
121
+ print(f"[lora] Downloaded: {label} ({size_mb:.0f} MB)")
122
+ return label, f"Downloaded: {label} ({size_mb:.0f} MB)"
123
+ except Exception as e:
124
+ return "", f"Failed: {e}"
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Engine
129
+ # ---------------------------------------------------------------------------
130
+ SD_ENGINE = {"instance": None, "lora_state": None}
131
+
132
+ def _reload_engine():
133
+ lora_files = set(os.listdir(LORA_DIR)) if os.path.exists(LORA_DIR) else set()
134
+ state_key = frozenset(lora_files)
135
+ if SD_ENGINE["instance"] is not None and SD_ENGINE["lora_state"] == state_key:
136
+ return
137
+ print(f"[engine] Loading (loras: {len(lora_files)})...")
138
+ t0 = time.time()
139
+ kwargs = dict(
140
+ diffusion_model_path=diffusion_path, llm_path=llm_path, vae_path=vae_path,
141
+ diffusion_flash_attn=True, n_threads=N_THREADS, verbose=True,
142
+ )
143
+ if lora_files:
144
+ kwargs["lora_model_dir"] = LORA_DIR
145
+ SD_ENGINE["instance"] = StableDiffusion(**kwargs)
146
+ SD_ENGINE["lora_state"] = state_key
147
+ print(f"[engine] Loaded in {time.time()-t0:.1f}s")
148
+
149
+ def get_engine():
150
+ if SD_ENGINE["instance"] is None:
151
+ _reload_engine()
152
+ return SD_ENGINE["instance"]
153
+
154
+ _reload_engine()
155
+
156
+ print("[init] Fetching Klein 4B LoRA catalog...")
157
+ INITIAL_LORAS = fetch_all_loras("")
158
+ print(f"[init] Found {len(INITIAL_LORAS)} LoRAs")
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Inference
162
+ # ---------------------------------------------------------------------------
163
+ RESOLUTIONS = ["512x512", "768x768", "1024x1024", "1024x768", "768x1024", "1024x576", "576x1024"]
164
+
165
+ def parse_res(s):
166
+ w, h = s.split("x")
167
+ return int(w), int(h)
168
+
169
+ def generate(prompt, ref_image, resolution, steps, seed, lora_strength, active_loras, progress=None):
170
+ try:
171
+ gc.collect()
172
+ sd = get_engine()
173
+ w, h = parse_res(resolution)
174
+ steps, seed = int(steps), int(seed) if int(seed) >= 0 else -1
175
+ actual_prompt = prompt
176
+ lora_tags = []
177
+ if active_loras:
178
+ for label in active_loras:
179
+ lora_name = DOWNLOADED_LORAS.get(label)
180
+ if lora_name:
181
+ actual_prompt = f'<lora:{lora_name}:{lora_strength:.2f}> {actual_prompt}'
182
+ lora_tags.append(label.split("/")[-1])
183
+ is_edit = ref_image is not None
184
+ mode = "edit" if is_edit else "gen"
185
+ print(f"[{mode}] {w}x{h} steps={steps} seed={seed} loras={lora_tags}")
186
+ t0 = time.time()
187
+ kwargs = dict(prompt=actual_prompt, width=w, height=h, sample_steps=steps, cfg_scale=1.0, seed=seed)
188
+ if is_edit:
189
+ kwargs["ref_images"] = [ref_image]
190
+ images = sd.generate_image(**kwargs)
191
+ elapsed = time.time() - t0
192
+ lora_info = f" +{len(lora_tags)} LoRA(s)" if lora_tags else ""
193
+ edit_info = " [edit]" if is_edit else ""
194
+ status = f"{elapsed:.1f}s | {w}x{h}, {steps} steps, seed {seed}{lora_info}{edit_info}"
195
+ print(f"[{mode}] {status}")
196
+ return (images[0] if images else None), status
197
+ except Exception as e:
198
+ import traceback; traceback.print_exc()
199
+ return None, f"Error: {e}"
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Gradio UI
203
+ # ---------------------------------------------------------------------------
204
+ import gradio as gr
205
+
206
+ with gr.Blocks(theme="NoCrypt/miku", title="FLUX.2 Klein 4B CPU") as demo:
207
+ gr.Markdown(
208
+ "# FLUX.2 Klein 4B / Free CPU\n"
209
+ "Type a prompt to generate. Upload a reference image to edit it instead. "
210
+ "Expect **15-30 min** per image at 512x512 on free CPU."
211
+ )
212
+ with gr.Row():
213
+ with gr.Column(scale=1):
214
+ prompt = gr.Textbox(label="Prompt", lines=3, placeholder="Describe what to generate or edit...")
215
+ ref_image = gr.Image(label="Reference Image (optional, for editing)", type="pil")
216
+ resolution = gr.Dropdown(choices=RESOLUTIONS, value="512x512", label="Resolution")
217
+ with gr.Row():
218
+ steps = gr.Slider(2, 8, value=4, step=1, label="Steps", scale=1)
219
+ seed = gr.Number(value=-1, label="Seed", precision=0, scale=1)
220
+ lora_strength = gr.Slider(0.1, 1.5, value=0.8, step=0.05, label="LoRA str", scale=1)
221
+ with gr.Accordion("LoRA (search Klein 4B LoRAs on HuggingFace)", open=False):
222
+ lora_search = gr.Dropdown(
223
+ choices=INITIAL_LORAS, value=None,
224
+ label="Search LoRA repos (type to filter, select to download)",
225
+ filterable=True, allow_custom_value=True, interactive=True,
226
+ )
227
+ lora_status = gr.Textbox(label="Status", interactive=False, value="No LoRA active")
228
+ active_loras = gr.Dropdown(
229
+ choices=[], value=[], multiselect=True, interactive=True,
230
+ label="Active LoRAs (click X to remove)",
231
+ )
232
+ gen_btn = gr.Button("Generate / Edit", variant="primary", size="lg")
233
+ with gr.Column(scale=1):
234
+ output_image = gr.Image(label="Output", type="pil")
235
+ status_text = gr.Textbox(label="Status", interactive=False)
236
+
237
+ def on_search_type(query):
238
+ if not query or query in INITIAL_LORAS:
239
+ return gr.update(choices=INITIAL_LORAS)
240
+ results = fetch_all_loras(query)
241
+ return gr.update(choices=results if results else INITIAL_LORAS)
242
+
243
+ def on_lora_select(repo_id, current_active):
244
+ if not repo_id or repo_id.startswith("("):
245
+ return current_active or [], "Select a LoRA", gr.update()
246
+ label, status_msg = download_lora(repo_id)
247
+ if not label:
248
+ return current_active or [], status_msg, gr.update()
249
+ _reload_engine()
250
+ active = list(current_active) if current_active else []
251
+ if label not in active:
252
+ active.append(label)
253
+ all_downloaded = list(DOWNLOADED_LORAS.keys())
254
+ return gr.update(choices=all_downloaded, value=active), status_msg, gr.update(value=None)
255
+
256
+ lora_search.input(fn=on_search_type, inputs=[lora_search], outputs=[lora_search])
257
+ lora_search.select(fn=on_lora_select, inputs=[lora_search, active_loras], outputs=[active_loras, lora_status, lora_search])
258
+ gen_btn.click(fn=generate, inputs=[prompt, ref_image, resolution, steps, seed, lora_strength, active_loras], outputs=[output_image, status_text])
259
+
260
+ gr.Markdown("---\nsd.cpp Q4_K_M | Cordux uncensored encoder | "
261
+ "[BFL](https://bfl.ai/models/flux-2-klein) | [sd.cpp](https://github.com/leejet/stable-diffusion.cpp) | "
262
+ "[Browse LoRAs](https://huggingface.co/models?search=klein+4b&filter=lora)")
263
+
264
+ demo.queue().launch(ssr_mode=False, show_error=True)
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ build-essential
2
+ cmake
3
+ libopenblas-dev
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ stable-diffusion-cpp-python
2
+ gradio
3
+ Pillow
4
+ huggingface-hub
5
+ requests