dssddsdf commited on
Commit
4f0ce4c
Β·
verified Β·
1 Parent(s): 8538e57

rebuild: purpose-built tabs with optimal model/LoRA per task, Z-Image txt2img first tab

Browse files
Files changed (1) hide show
  1. app.py +381 -422
app.py CHANGED
@@ -7,156 +7,109 @@ import torch
7
  import random
8
  from PIL import Image
9
 
 
 
10
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
11
  MAX_SEED = np.iinfo(np.int32).max
12
 
13
- # ---- Model Registry ----
14
- MODELS = {
15
- "FLUX.2 Klein 9B (4 steps, editing)": {
16
- "id": "black-forest-labs/FLUX.2-klein-9B",
17
- "pipeline": "Flux2KleinPipeline",
18
- "defaults": {"steps": 4, "guidance": 1.0},
19
- "supports_strength": False,
20
- "multi_image": True,
21
- },
22
- "Z-Image Turbo (9 steps, img2img)": {
23
- "id": "Tongyi-MAI/Z-Image-Turbo",
24
- "pipeline": "ZImageImg2ImgPipeline",
25
- "defaults": {"steps": 9, "guidance": 0.0},
26
- "supports_strength": True,
27
- "multi_image": False,
28
- },
29
- "Qwen-Image-Edit 2511 (50 steps, editing)": {
30
- "id": "Qwen/Qwen-Image-Edit-2511",
31
- "pipeline": "QwenImageEditPlusPipeline",
32
- "defaults": {"steps": 50, "guidance": 1.0},
33
- "supports_strength": False,
34
- "multi_image": True,
35
- },
36
  }
37
- MODEL_NAMES = list(MODELS.keys())
38
-
39
- # ---- Prompt Templates ----
40
- FACE_SWAP_PROMPT = """head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. Remove the head from Picture 1 completely and replace it with the head from Picture 2.
41
- FROM PICTURE 1 (strictly preserve):
42
- - Scene: lighting conditions, shadows, highlights, color temperature, environment, background
43
- - Head positioning: exact rotation angle, tilt, direction the head is facing
44
- - Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion
45
- FROM PICTURE 2 (strictly preserve identity):
46
- - Facial structure: face shape, bone structure, jawline, chin
47
- - All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows
48
- - Hair: color, style, texture, hairline
49
- - Skin: texture, tone, complexion
50
- The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
51
-
52
- PROMPT_TEMPLATES = {
53
- "None": "",
54
- "Face Swap (Detailed)": FACE_SWAP_PROMPT,
55
- "Face Swap (Concise)": "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, nose structure of Picture 2. copy the direction of the eye, head rotation, micro expressions from Picture 1, high quality, sharp details, 4k.",
56
- "Expression Transfer": "h34d_sw4p: replace the head of Picture 1 by the head from Picture 2, strictly preserving the identity, facial features (eyes, nose, mouth), and skin texture of Picture 2. ensure the new head mimics the identical expression, angle, and rotation found in Picture 1.",
57
- "Clothing Transfer": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions.",
58
- "Style Transfer": "Transform this image into the artistic style shown, preserving the subject's identity and pose exactly. Match the color palette, brushwork, and aesthetic of the reference.",
59
- "Pose Change - Sitting": "Change the person's pose to sitting comfortably in an armchair, preserving their identity and clothing exactly.",
60
- "Pose Change - Side Profile": "Show the person from a side profile, looking to the right, preserving their identity and clothing exactly.",
61
- "Pose Change - Arms Crossed": "Change the pose so the person has their arms crossed, looking confident. Preserve identity and clothing exactly.",
62
- "Relight (Neutral Studio)": "Relight the image to remove all existing lighting conditions and replace them with neutral, uniform illumination. Apply soft, evenly distributed lighting with no directional shadows. Maintain the original identity of all subjects exactly.",
63
- "Remove Background": "Remove the background and replace it with a clean white studio backdrop. Keep the subject exactly as they are.",
64
- "De-censor (Remove Bars)": "De-censor the image by removing black bars. Restore the original image content underneath naturally.",
65
- "Upscale / Enhance": "Enhance this image to higher quality. Sharpen details, improve clarity, fix any compression artifacts. Preserve everything exactly as-is but at higher quality, 4k, sharp details.",
66
  }
67
 
68
- # Character sheet view prompts
69
- CHAR_SHEET_VIEWS = [
70
- ("Front Face", "Show the person's face from directly in front, looking straight at the camera. Head and shoulders only. Preserve identity exactly. Clean background."),
71
- ("Left Profile", "Show the person's face from the left side, 90 degree left profile view. Head and shoulders only. Preserve identity exactly. Clean background."),
72
- ("Right Profile", "Show the person's face from the right side, 90 degree right profile view. Head and shoulders only. Preserve identity exactly. Clean background."),
73
- ("Front Body", "Show the person's full body from directly in front, standing in a neutral pose. Preserve identity, clothing, and proportions exactly. Clean background."),
74
- ("Left Body", "Show the person's full body from the left side, 90 degree profile. Standing in a neutral pose. Preserve identity, clothing, and proportions exactly. Clean background."),
75
- ("Right Body", "Show the person's full body from the right side, 90 degree profile. Standing in a neutral pose. Preserve identity, clothing, and proportions exactly. Clean background."),
76
- ("Back Body", "Show the person's full body from behind, back view. Standing in a neutral pose. Preserve identity, clothing, and proportions exactly. Clean background."),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  ]
78
 
79
- # ---- LoRA Registry ----
80
- LORA_STYLES = [
81
- {
82
- "title": "None (Base Klein 9B)",
83
- "adapter_name": None,
84
- "repo": None,
85
- "weights": None,
86
- },
87
- {
88
- "title": "BFS Head Swap V1 (rank64)",
89
- "adapter_name": "bfs-head-v1-r64",
90
- "repo": "Alissonerdx/BFS-Best-Face-Swap",
91
- "weights": "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors",
92
- },
93
- {
94
- "title": "BFS Head Swap V1 (rank128)",
95
- "adapter_name": "bfs-head-v1-r128",
96
- "repo": "Alissonerdx/BFS-Best-Face-Swap",
97
- "weights": "bfs_head_v1_flux-klein_9b_step3500_rank128.safetensors",
98
- },
99
- {
100
- "title": "Klein Consistency",
101
- "adapter_name": "klein-consistency",
102
- "repo": "dx8152/Flux2-Klein-9B-Consistency",
103
- "weights": "Klein-consistency.safetensors",
104
- },
105
- {
106
- "title": "Klein Delight Style",
107
- "adapter_name": "klein-delight",
108
- "repo": "linoyts/Flux2-Klein-Delight-LoRA",
109
- "weights": "pytorch_lora_weights.safetensors",
110
- },
111
- {
112
- "title": "Ghost Mannequin",
113
- "adapter_name": "ghost-mannequin",
114
- "repo": "nhathoangfoto/FLUX.2-klein-ghost-mannequin",
115
- "weights": "3D-GhosMannequinRank-256_000005000.safetensors",
116
- },
117
- ]
118
 
119
- LORA_TITLES = [s["title"] for s in LORA_STYLES]
120
- LORA_MAP = {s["title"]: s for s in LORA_STYLES}
121
-
122
- # ---- Model Manager ----
123
- _current_model = None
124
- _current_model_name = None
125
- pipe = None
126
-
127
- def load_model(model_name):
128
- """Load a model by name. Swaps out the previous model to save VRAM."""
129
- global pipe, _current_model_name
130
- if model_name == _current_model_name and pipe is not None:
131
- return pipe
132
-
133
- # Unload previous
134
- if pipe is not None:
135
- del pipe
136
- gc.collect()
137
- torch.cuda.empty_cache()
138
- print(f"Unloaded {_current_model_name}")
139
-
140
- cfg = MODELS[model_name]
141
- print(f"Loading {cfg['id']}...")
142
-
143
- if cfg["pipeline"] == "Flux2KleinPipeline":
144
- from diffusers import Flux2KleinPipeline
145
- pipe = Flux2KleinPipeline.from_pretrained(cfg["id"], torch_dtype=torch.bfloat16).to(device)
146
- elif cfg["pipeline"] == "ZImageImg2ImgPipeline":
147
- from diffusers import ZImageImg2ImgPipeline
148
- pipe = ZImageImg2ImgPipeline.from_pretrained(cfg["id"], torch_dtype=torch.bfloat16).to(device)
149
- elif cfg["pipeline"] == "QwenImageEditPlusPipeline":
150
- from diffusers import QwenImageEditPlusPipeline
151
- pipe = QwenImageEditPlusPipeline.from_pretrained(cfg["id"], torch_dtype=torch.bfloat16).to(device)
152
-
153
- _current_model_name = model_name
154
- print(f"{model_name} loaded!")
155
- return pipe
156
-
157
- # Pre-load Klein 9B at startup
158
- pipe = load_model("FLUX.2 Klein 9B (4 steps, editing)")
159
- LOADED_ADAPTERS = set()
160
 
161
 
162
  # ---- Helpers ----
@@ -165,355 +118,361 @@ def update_dimensions(image):
165
  return 1024, 1024
166
  w, h = image.size
167
  scale = min(1024 / w, 1024 / h)
168
- nw = (int(w * scale) // 16) * 16
169
- nh = (int(h * scale) // 16) * 16
170
- return nw, nh
171
 
172
 
173
- def process_gallery_images(images):
174
  if not images:
175
  return []
176
- pil_images = []
177
  for item in images:
178
  try:
179
- path_or_img = item[0] if isinstance(item, (tuple, list)) else item
180
- if isinstance(path_or_img, str):
181
- pil_images.append(Image.open(path_or_img).convert("RGB"))
182
- elif isinstance(path_or_img, Image.Image):
183
- pil_images.append(path_or_img.convert("RGB"))
184
  else:
185
- pil_images.append(Image.open(path_or_img.name).convert("RGB"))
186
  except Exception as e:
187
- print(f"Skipping image: {e}")
188
- return pil_images
189
-
190
-
191
- def apply_template(template_name):
192
- return PROMPT_TEMPLATES.get(template_name, "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
 
195
- def run_pipeline(model_name, images, prompt, guidance, steps, seed, strength=0.6):
196
- """Run the active pipeline. Handles API differences between models."""
197
- global pipe
198
- pipe = load_model(model_name)
199
- cfg = MODELS[model_name]
200
- gen = torch.Generator(device=device).manual_seed(seed)
201
  w, h = update_dimensions(images[0])
202
  processed = [img.resize((w, h), Image.LANCZOS).convert("RGB") for img in images]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
- kwargs = dict(prompt=prompt, guidance_scale=guidance, width=w, height=h,
205
- num_inference_steps=steps, generator=gen)
206
-
207
- if cfg["multi_image"]:
208
- kwargs["image"] = processed if len(processed) > 1 else processed[0]
209
- else:
210
- kwargs["image"] = processed[0]
211
-
212
- if cfg["supports_strength"]:
213
- kwargs["strength"] = strength
214
-
215
- return pipe(**kwargs).images[0]
216
-
217
-
218
- # ---- Inference ----
219
- def activate_lora(lora_name, lora_strength, custom_repo="", custom_file="", custom_strength=1.0):
220
- """Load and activate LoRAs on the current pipeline."""
221
- active_adapters = []
222
- active_weights = []
223
-
224
- style = LORA_MAP.get(lora_name)
225
- if style and style["adapter_name"]:
226
- try:
227
- pipe.load_lora_weights(style["repo"], weight_name=style["weights"],
228
- adapter_name=style["adapter_name"])
229
- except (ValueError, AttributeError):
230
- pass
231
- active_adapters.append(style["adapter_name"])
232
- active_weights.append(lora_strength)
233
 
234
- if custom_repo and custom_repo.strip() and custom_file and custom_file.strip():
235
- try:
236
- pipe.load_lora_weights(custom_repo.strip(), weight_name=custom_file.strip(),
237
- adapter_name="custom-lora")
238
- except (ValueError, AttributeError):
239
- pass
240
- active_adapters.append("custom-lora")
241
- active_weights.append(custom_strength)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- if active_adapters:
244
- pipe.set_adapters(active_adapters, adapter_weights=active_weights)
245
- print(f"Active LoRAs: {list(zip(active_adapters, active_weights))}")
246
- else:
247
- try:
248
- pipe.disable_lora()
249
- except AttributeError:
250
- pass
251
 
252
 
 
 
 
253
  @spaces.GPU
254
- def infer(
255
- input_images, prompt, model_name, lora_name, lora_strength,
256
- custom_lora_repo, custom_lora_file, custom_lora_strength,
257
- seed, randomize_seed, guidance_scale, steps, strength,
258
- progress=gr.Progress(track_tqdm=True),
259
- ):
260
- gc.collect()
261
- torch.cuda.empty_cache()
262
-
263
  try:
264
- pil_images = process_gallery_images(input_images)
265
- if not pil_images:
266
- raise gr.Error("Upload at least one image!")
267
  if not prompt or not prompt.strip():
268
- raise gr.Error("Enter a prompt!")
269
 
270
- global pipe
271
- pipe = load_model(model_name)
272
- activate_lora(lora_name, lora_strength, custom_lora_repo, custom_lora_file, custom_lora_strength)
 
273
 
274
  if randomize_seed:
275
  seed = random.randint(0, MAX_SEED)
276
 
277
- image = run_pipeline(model_name, pil_images, prompt, guidance_scale, steps, seed, strength)
278
- return image, seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
 
280
  finally:
281
- gc.collect()
282
- torch.cuda.empty_cache()
283
-
284
-
285
- POSE_LIBRARY = (
286
- # 7 character sheet views
287
- [v[1] for v in CHAR_SHEET_VIEWS] +
288
- # 50 additional poses
289
- [
290
- "standing facing the camera, neutral pose, arms at sides",
291
- "standing with arms crossed, confident pose",
292
- "standing with hands on hips",
293
- "standing, slight lean to the left, relaxed",
294
- "standing three-quarter view from the left",
295
- "standing three-quarter view from the right",
296
- "standing side profile, looking right",
297
- "standing side profile, looking left",
298
- "standing from behind, back view",
299
- "standing over the shoulder look, glancing back at camera",
300
- "sitting on a chair, legs crossed, relaxed",
301
- "sitting on the floor, legs extended",
302
- "sitting cross-legged on the ground",
303
- "sitting on a stool, leaning forward slightly",
304
- "sitting sideways on a chair, arm draped over backrest",
305
- "kneeling on one knee",
306
- "kneeling on both knees, upright posture",
307
- "leaning against a wall, arms crossed",
308
- "leaning against a wall, one foot up",
309
- "leaning forward with hands on knees",
310
- "walking towards the camera, mid-stride",
311
- "walking away from camera, back view mid-stride",
312
- "walking side view, profile mid-stride",
313
- "running towards the camera, dynamic pose",
314
- "looking up at the sky, chin raised",
315
- "looking down, contemplative",
316
- "head tilted to the left, slight smile",
317
- "head tilted to the right, serious expression",
318
- "laughing naturally, candid expression",
319
- "hands behind head, stretching",
320
- "one hand touching hair, casual pose",
321
- "hands in pockets, casual standing",
322
- "waving at camera, friendly gesture",
323
- "arms raised above head, celebratory",
324
- "crouching down, low angle",
325
- "bending forward slightly, looking at camera",
326
- "twisting torso, looking over shoulder",
327
- "dancing pose, one leg lifted",
328
- "lying on back, looking up at camera from above",
329
- "lying on side, propped on elbow",
330
- "lying on stomach, chin in hands",
331
- "close-up portrait, direct eye contact",
332
- "close-up portrait, eyes looking away",
333
- "close-up portrait, slight smile",
334
- "medium shot from waist up, arms at sides",
335
- "medium shot from waist up, one hand raised",
336
- "full body shot, standing tall, power pose",
337
- ]
338
- )
339
 
340
 
 
 
 
341
  @spaces.GPU(duration=300)
342
- def generate_dataset(
343
- ref_images, subject, extra, count, model_name, lora_name, lora_str,
344
- seed, guidance, steps, strength,
345
- progress=gr.Progress(track_tqdm=True),
346
- ):
347
- gc.collect()
348
- torch.cuda.empty_cache()
349
  try:
350
- pil_images = process_gallery_images(ref_images)
351
- if not pil_images:
352
  raise gr.Error("Upload at least one reference image!")
353
 
354
- global pipe
355
- pipe = load_model(model_name)
356
- activate_lora(lora_name, lora_str)
 
357
 
358
  count = int(count)
359
  poses = (POSE_LIBRARY * ((count // len(POSE_LIBRARY)) + 1))[:count]
360
-
361
- cfg = MODELS[model_name]
362
- w, h = update_dimensions(pil_images[0])
363
- refs = [img.resize((w, h), Image.LANCZOS).convert("RGB") for img in pil_images]
364
 
365
  results = []
366
  captions = []
367
- subject_text = subject.strip() if subject else "a person"
368
- extra_text = ", " + extra.strip() if extra and extra.strip() else ""
369
 
370
  for i, pose in enumerate(poses):
371
  progress((i + 1) / count, desc=f"Image {i+1}/{count}")
372
  caption = f"{subject_text}, {pose}{extra_text}"
373
- image = run_pipeline(model_name, pil_images, caption, guidance, steps, seed + i, strength)
374
- results.append((image, f"{i:03d}"))
375
  captions.append(f"{i:03d}.txt: {caption}")
376
 
377
- status = f"Generated {count} images with {model_name}.\n\nCaption preview:\n" + "\n".join(captions[:10])
378
- if count > 10:
379
- status += f"\n... and {count - 10} more"
 
380
  return results, status
381
  finally:
382
- gc.collect()
383
- torch.cuda.empty_cache()
384
 
385
 
386
- # ---- UI ----
387
- css = "#col-container { margin: 0 auto; max-width: 1100px; }"
 
 
388
 
389
  with gr.Blocks(css=css) as demo:
390
- with gr.Column(elem_id="col-container"):
391
- gr.Markdown("# FLUX.2 Klein Studio\nMulti-model image editing: Klein 9B, Z-Image Turbo, Qwen-Image-Edit")
392
 
393
  with gr.Tabs():
394
- # ==================== IMAGE EDIT / FACE SWAP ====================
395
- with gr.TabItem("Image Edit / Face Swap"):
 
396
  with gr.Row():
397
- with gr.Column(scale=2):
398
- input_images = gr.Gallery(
399
- label="Input Images (1 for edit, 2 for face swap: body first, face second)",
400
- type="filepath", columns=2, rows=1, height=280, allow_preview=True,
401
- )
402
- prompt = gr.Textbox(label="Prompt", lines=3, placeholder="Describe the edit...")
403
  with gr.Row():
404
- template_select = gr.Dropdown(list(PROMPT_TEMPLATES.keys()), value="None", label="Prompt Template", scale=2)
405
- run_btn = gr.Button("Generate", variant="primary", scale=1, size="lg")
406
-
407
- model_select = gr.Dropdown(MODEL_NAMES, value=MODEL_NAMES[0], label="Model")
408
- strength = gr.Slider(0.0, 1.0, value=0.6, step=0.05, label="Strength (Z-Image Turbo only)",
409
- info="How much to change from original. Ignored by Klein/Qwen.")
410
-
411
- with gr.Accordion("LoRA Adapters", open=True):
412
- lora_select = gr.Dropdown(LORA_TITLES, value="None (Base Klein 9B)", label="Built-in LoRA")
413
- lora_strength = gr.Slider(0.0, 2.0, value=1.0, step=0.05, label="LoRA Strength")
414
- gr.Markdown("**Custom LoRA** (stacks with built-in)")
415
- with gr.Row():
416
- custom_repo = gr.Textbox(label="HF Repo", placeholder="user/my-lora", scale=2)
417
- custom_file = gr.Textbox(label="Filename", placeholder="weights.safetensors", scale=2)
418
- custom_strength = gr.Slider(0.0, 2.0, value=1.0, step=0.05, label="Strength", scale=1)
419
-
420
- with gr.Accordion("Generation Settings", open=False):
421
- seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
422
- randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
423
- with gr.Row():
424
- guidance = gr.Slider(0.0, 10.0, value=1.0, step=0.1, label="Guidance Scale")
425
- steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
426
 
427
- with gr.Column(scale=2):
428
- output_image = gr.Image(label="Result", interactive=False, format="png", height=500)
429
- seed_output = gr.Number(label="Seed Used", interactive=False)
430
-
431
- template_select.change(fn=apply_template, inputs=[template_select], outputs=[prompt])
432
- run_btn.click(
433
- fn=infer,
434
- inputs=[input_images, prompt, model_select, lora_select, lora_strength,
435
- custom_repo, custom_file, custom_strength,
436
- seed, randomize_seed, guidance, steps, strength],
437
- outputs=[output_image, seed_output],
438
- )
439
-
440
- # ==================== QUICK EDIT ====================
441
- with gr.TabItem("Quick Image Edit"):
442
- gr.Markdown("Upload one image + prompt. Simple and fast.")
443
  with gr.Row():
444
  with gr.Column():
445
- qe_image = gr.Image(label="Source Image", type="pil", sources=["upload"], height=350)
446
- qe_prompt = gr.Textbox(label="Edit instruction", lines=2, placeholder="e.g. remove the shirt, add tattoos...")
447
- qe_model = gr.Dropdown(MODEL_NAMES, value=MODEL_NAMES[0], label="Model")
448
- qe_strength = gr.Slider(0.0, 1.0, value=0.6, step=0.05, label="Strength (Z-Image only)")
449
- qe_lora = gr.Dropdown(LORA_TITLES, value="None (Base Klein 9B)", label="LoRA")
450
- qe_lora_str = gr.Slider(0.0, 2.0, value=1.0, step=0.05, label="LoRA Strength")
451
  with gr.Row():
452
- qe_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
453
- qe_guidance = gr.Slider(0.0, 10.0, value=1.0, step=0.1, label="Guidance")
454
- qe_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
455
- qe_rand = gr.Checkbox(value=True, label="Randomize seed")
456
- qe_btn = gr.Button("Edit Image", variant="primary", size="lg")
 
457
  with gr.Column():
458
- qe_output = gr.Image(label="Result", interactive=False, format="png", height=500)
459
- qe_seed_out = gr.Number(label="Seed Used", interactive=False)
460
-
461
- @spaces.GPU
462
- def quick_edit(image, prompt, model_name, img_strength, lora_name, lora_str,
463
- steps, guidance, seed, randomize):
464
- gc.collect()
465
- torch.cuda.empty_cache()
466
- try:
467
- if image is None:
468
- raise gr.Error("Upload an image!")
469
- if not prompt or not prompt.strip():
470
- raise gr.Error("Enter an edit instruction!")
471
- global pipe
472
- pipe = load_model(model_name)
473
- activate_lora(lora_name, lora_str)
474
- if randomize:
475
- seed = random.randint(0, MAX_SEED)
476
- return run_pipeline(model_name, [image], prompt, guidance, steps, seed, img_strength), seed
477
- finally:
478
- gc.collect()
479
- torch.cuda.empty_cache()
480
-
481
- qe_btn.click(
482
- fn=quick_edit,
483
- inputs=[qe_image, qe_prompt, qe_model, qe_strength, qe_lora, qe_lora_str,
484
- qe_steps, qe_guidance, qe_seed, qe_rand],
485
- outputs=[qe_output, qe_seed_out],
486
- )
487
-
488
- # ==================== DATASET GENERATOR (includes 360 sheet) ====================
489
- with gr.TabItem("LoRA Dataset / 360 Sheet"):
490
- gr.Markdown("Generate captioned pose datasets for LoRA training. First 7 images are 360 character sheet views, then 50 additional poses cycle. Multi-reference supported.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  with gr.Row():
492
  with gr.Column(scale=1):
493
  ds_ref = gr.Gallery(label="Reference Images", type="filepath", columns=2, rows=1, height=200)
494
- ds_subject = gr.Textbox(label="Subject description (caption prefix)", placeholder="e.g. a woman with red hair, green eyes", lines=2)
495
- ds_extra = gr.Textbox(label="Extra prompt (appended to each)", placeholder="e.g. nude, studio lighting, white background", lines=1)
496
- ds_count = gr.Slider(7, 150, value=50, step=1, label="Number of images (first 7 = 360 sheet)")
497
- ds_model = gr.Dropdown(MODEL_NAMES, value=MODEL_NAMES[0], label="Model")
498
- ds_strength = gr.Slider(0.0, 1.0, value=0.6, step=0.05, label="Strength (Z-Image only)")
499
- ds_lora = gr.Dropdown(LORA_TITLES, value="None (Base Klein 9B)", label="LoRA")
500
- ds_lora_str = gr.Slider(0.0, 2.0, value=1.0, step=0.05, label="LoRA Strength")
501
  with gr.Row():
502
- ds_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Starting Seed")
503
  ds_guidance = gr.Slider(0.0, 10.0, value=1.0, step=0.1, label="Guidance")
504
  ds_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
505
  ds_btn = gr.Button("Generate Dataset", variant="primary", size="lg")
506
-
507
  with gr.Column(scale=2):
508
- ds_gallery = gr.Gallery(label="Generated Dataset", columns=5, rows=3, height=500, object_fit="contain")
509
- ds_status = gr.Textbox(label="Status / Captions", lines=8, interactive=False)
510
-
511
- ds_btn.click(
512
- fn=generate_dataset,
513
- inputs=[ds_ref, ds_subject, ds_extra, ds_count, ds_model, ds_lora, ds_lora_str,
514
- ds_seed, ds_guidance, ds_steps, ds_strength],
515
- outputs=[ds_gallery, ds_status],
516
- )
517
 
518
  if __name__ == "__main__":
519
  demo.queue().launch(ssr_mode=False, show_error=True)
 
7
  import random
8
  from PIL import Image
9
 
10
+ from diffusers import Flux2KleinPipeline, ZImagePipeline
11
+
12
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
13
  MAX_SEED = np.iinfo(np.int32).max
14
 
15
+ # ---- LoRA Config ----
16
+ LORAS = {
17
+ "bfs-swap": {"repo": "Alissonerdx/BFS-Best-Face-Swap", "weights": "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors"},
18
+ "nsfw": {"repo": "AntiLeecher/Flux-Klein-NSFW-Lora", "weights": "Flux Klein - NSFW v2.safetensors"},
19
+ "consistency": {"repo": "dx8152/Flux2-Klein-9B-Consistency", "weights": "Klein-consistency.safetensors"},
20
+ "delight": {"repo": "linoyts/Flux2-Klein-Delight-LoRA", "weights": "pytorch_lora_weights.safetensors"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
+
23
+ # ---- Prompt Presets ----
24
+ FACE_SWAP_PROMPT = (
25
+ "head_swap: start with Picture 1 as the base image, keeping its lighting, "
26
+ "environment, and background. Remove the head from Picture 1 completely and "
27
+ "replace it with the head from Picture 2, strictly preserving the hair, eye color, "
28
+ "nose structure of Picture 2. copy the direction of the eye, head rotation, "
29
+ "micro expressions from Picture 1, high quality, sharp details, 4k."
30
+ )
31
+
32
+ EDIT_TEMPLATES = {
33
+ "Custom": "",
34
+ "Remove clothing": "Remove all clothing from the person. Artistic nudity, full body visible, photorealistic, sharp details.",
35
+ "Change outfit": "Change the person's outfit to: ",
36
+ "Add tattoos": "Add detailed tattoos covering the person's arms and torso. Preserve identity and pose exactly.",
37
+ "Change hair": "Change the person's hairstyle to: ",
38
+ "Remove background": "Remove the background and replace with a clean white studio backdrop.",
39
+ "Relight (studio)": "Relight with neutral, uniform studio illumination. Soft, evenly distributed lighting. Preserve identity exactly.",
40
+ "Age up": "Make the person appear 20 years older. Preserve identity, add wrinkles, grey hair, aged skin naturally.",
41
+ "Age down": "Make the person appear 15 years younger. Preserve identity, smoother skin, more youthful features.",
42
+ "De-censor": "De-censor the image by removing black bars and mosaic censoring. Restore the original image content underneath naturally.",
43
+ "Enhance / Upscale": "Enhance this image to higher quality. Sharpen details, improve clarity, 4k, sharp details.",
 
 
 
 
 
 
 
44
  }
45
 
46
+ POSE_LIBRARY = [
47
+ # 7 character sheet views first
48
+ "face from directly in front, looking straight at camera, head and shoulders, clean background",
49
+ "face from left side, 90 degree left profile, head and shoulders, clean background",
50
+ "face from right side, 90 degree right profile, head and shoulders, clean background",
51
+ "full body from directly in front, standing neutral pose, clean background",
52
+ "full body from left side, 90 degree profile, standing neutral, clean background",
53
+ "full body from right side, 90 degree profile, standing neutral, clean background",
54
+ "full body from behind, back view, standing neutral, clean background",
55
+ # Additional poses
56
+ "standing facing camera, neutral pose, arms at sides",
57
+ "standing with arms crossed, confident pose",
58
+ "standing with hands on hips",
59
+ "standing three-quarter view from the left",
60
+ "standing three-quarter view from the right",
61
+ "standing side profile, looking right",
62
+ "standing from behind, back view",
63
+ "over the shoulder look, glancing back at camera",
64
+ "sitting on a chair, legs crossed, relaxed",
65
+ "sitting on the floor, legs extended",
66
+ "sitting cross-legged on the ground",
67
+ "sitting on a stool, leaning forward",
68
+ "kneeling on one knee",
69
+ "kneeling on both knees, upright",
70
+ "leaning against a wall, arms crossed",
71
+ "leaning against a wall, one foot up",
72
+ "walking towards camera, mid-stride",
73
+ "walking away from camera, back view",
74
+ "looking up at the sky, chin raised",
75
+ "looking down, contemplative",
76
+ "head tilted to the left, slight smile",
77
+ "laughing naturally, candid expression",
78
+ "hands behind head, stretching",
79
+ "one hand touching hair, casual",
80
+ "hands in pockets, casual standing",
81
+ "arms raised above head, celebratory",
82
+ "crouching down, low angle",
83
+ "bending forward, looking at camera",
84
+ "twisting torso, looking over shoulder",
85
+ "dancing pose, one leg lifted",
86
+ "lying on back, looking up at camera from above",
87
+ "lying on side, propped on elbow",
88
+ "lying on stomach, chin in hands",
89
+ "close-up portrait, direct eye contact",
90
+ "close-up portrait, eyes looking away",
91
+ "close-up portrait, slight smile",
92
+ "medium shot from waist up, arms at sides",
93
+ "full body shot, standing tall, power pose",
94
+ "sitting sideways on chair, arm draped over backrest",
95
+ "leaning forward with hands on knees",
96
+ "running towards camera, dynamic pose",
97
+ "head tilted to the right, serious expression",
98
+ "waving at camera, friendly gesture",
99
  ]
100
 
101
+ # ---- Load Models ----
102
+ print("Loading FLUX.2 Klein 9B...")
103
+ pipe = Flux2KleinPipeline.from_pretrained(
104
+ "black-forest-labs/FLUX.2-klein-9B", torch_dtype=torch.bfloat16,
105
+ ).to(device)
106
+ print("Klein loaded!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ print("Loading Z-Image Turbo...")
109
+ zimage_pipe = ZImagePipeline.from_pretrained(
110
+ "Tongyi-MAI/Z-Image-Turbo", torch_dtype=torch.bfloat16,
111
+ ).to(device)
112
+ print("Z-Image Turbo loaded!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
 
115
  # ---- Helpers ----
 
118
  return 1024, 1024
119
  w, h = image.size
120
  scale = min(1024 / w, 1024 / h)
121
+ return (int(w * scale) // 16) * 16, (int(h * scale) // 16) * 16
 
 
122
 
123
 
124
+ def process_images(images):
125
  if not images:
126
  return []
127
+ out = []
128
  for item in images:
129
  try:
130
+ p = item[0] if isinstance(item, (tuple, list)) else item
131
+ if isinstance(p, str):
132
+ out.append(Image.open(p).convert("RGB"))
133
+ elif isinstance(p, Image.Image):
134
+ out.append(p.convert("RGB"))
135
  else:
136
+ out.append(Image.open(p.name).convert("RGB"))
137
  except Exception as e:
138
+ print(f"Skip: {e}")
139
+ return out
140
+
141
+
142
+ def activate_loras(names_and_weights):
143
+ """Activate a set of LoRAs by name. names_and_weights = [(name, weight), ...]"""
144
+ active = []
145
+ weights = []
146
+ for name, w in names_and_weights:
147
+ if name not in LORAS:
148
+ continue
149
+ cfg = LORAS[name]
150
+ try:
151
+ pipe.load_lora_weights(cfg["repo"], weight_name=cfg["weights"], adapter_name=name)
152
+ except ValueError:
153
+ pass # already loaded
154
+ active.append(name)
155
+ weights.append(w)
156
+
157
+ if active:
158
+ pipe.set_adapters(active, adapter_weights=weights)
159
+ print(f"LoRAs: {list(zip(active, weights))}")
160
+ else:
161
+ try:
162
+ pipe.disable_lora()
163
+ except Exception:
164
+ pass
165
 
166
 
167
+ def generate(images, prompt, guidance, steps, seed):
 
 
 
 
 
168
  w, h = update_dimensions(images[0])
169
  processed = [img.resize((w, h), Image.LANCZOS).convert("RGB") for img in images]
170
+ image_input = processed if len(processed) > 1 else processed[0]
171
+ return pipe(
172
+ image=image_input, prompt=prompt,
173
+ guidance_scale=guidance, width=w, height=h,
174
+ num_inference_steps=steps,
175
+ generator=torch.Generator(device=device).manual_seed(seed),
176
+ ).images[0]
177
+
178
+
179
+ # ===========================================================
180
+ # Tab 0: Text to Image (Z-Image Turbo)
181
+ # ===========================================================
182
+ @spaces.GPU
183
+ def txt2img(prompt, negative_prompt, seed, randomize_seed, steps, guidance, width, height,
184
+ progress=gr.Progress(track_tqdm=True)):
185
+ gc.collect(); torch.cuda.empty_cache()
186
+ try:
187
+ if not prompt or not prompt.strip():
188
+ raise gr.Error("Enter a prompt!")
189
+ if randomize_seed:
190
+ seed = random.randint(0, MAX_SEED)
191
+ result = zimage_pipe(
192
+ prompt=prompt.strip(),
193
+ negative_prompt=negative_prompt.strip() if negative_prompt else None,
194
+ width=width, height=height,
195
+ num_inference_steps=steps,
196
+ guidance_scale=guidance,
197
+ generator=torch.Generator(device=device).manual_seed(seed),
198
+ ).images[0]
199
+ return result, seed
200
+ finally:
201
+ gc.collect(); torch.cuda.empty_cache()
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
+ # ===========================================================
205
+ # Tab 1: Face Swap
206
+ # ===========================================================
207
+ @spaces.GPU
208
+ def face_swap(body_img, face_img, custom_prompt, nsfw_on, nsfw_str, swap_str,
209
+ seed, randomize_seed, progress=gr.Progress(track_tqdm=True)):
210
+ gc.collect(); torch.cuda.empty_cache()
211
+ try:
212
+ body_images = process_images(body_img)
213
+ face_images = process_images(face_img)
214
+ if not body_images:
215
+ raise gr.Error("Upload a body/scene image!")
216
+ if not face_images:
217
+ raise gr.Error("Upload a face reference image!")
218
+
219
+ loras = [("bfs-swap", swap_str)]
220
+ if nsfw_on:
221
+ loras.append(("nsfw", nsfw_str))
222
+ activate_loras(loras)
223
+
224
+ prompt = custom_prompt.strip() if custom_prompt.strip() else FACE_SWAP_PROMPT
225
+ if randomize_seed:
226
+ seed = random.randint(0, MAX_SEED)
227
 
228
+ images = body_images + face_images
229
+ result = generate(images, prompt, 1.0, 4, seed)
230
+ return result, seed
231
+ finally:
232
+ gc.collect(); torch.cuda.empty_cache()
 
 
 
233
 
234
 
235
+ # ===========================================================
236
+ # Tab 2: Image Edit
237
+ # ===========================================================
238
  @spaces.GPU
239
+ def image_edit(ref_images, prompt, nsfw_on, nsfw_str,
240
+ seed, randomize_seed, steps, guidance,
241
+ progress=gr.Progress(track_tqdm=True)):
242
+ gc.collect(); torch.cuda.empty_cache()
 
 
 
 
 
243
  try:
244
+ images = process_images(ref_images)
245
+ if not images:
246
+ raise gr.Error("Upload an image!")
247
  if not prompt or not prompt.strip():
248
+ raise gr.Error("Enter an edit prompt!")
249
 
250
+ loras = []
251
+ if nsfw_on:
252
+ loras.append(("nsfw", nsfw_str))
253
+ activate_loras(loras)
254
 
255
  if randomize_seed:
256
  seed = random.randint(0, MAX_SEED)
257
 
258
+ result = generate(images, prompt.strip(), guidance, steps, seed)
259
+ return result, seed
260
+ finally:
261
+ gc.collect(); torch.cuda.empty_cache()
262
+
263
+
264
+ # ===========================================================
265
+ # Tab 3: Pose Variations
266
+ # ===========================================================
267
+ @spaces.GPU
268
+ def pose_variations(ref_images, subject, extra, poses_selected, nsfw_on, nsfw_str,
269
+ seed, guidance, steps, progress=gr.Progress(track_tqdm=True)):
270
+ gc.collect(); torch.cuda.empty_cache()
271
+ try:
272
+ images = process_images(ref_images)
273
+ if not images:
274
+ raise gr.Error("Upload a reference image!")
275
+ if not poses_selected:
276
+ raise gr.Error("Select at least one pose!")
277
+
278
+ loras = [("consistency", 1.0)]
279
+ if nsfw_on:
280
+ loras.append(("nsfw", nsfw_str))
281
+ activate_loras(loras)
282
+
283
+ subject_text = subject.strip() if subject and subject.strip() else "the person"
284
+ extra_text = ", " + extra.strip() if extra and extra.strip() else ""
285
+ results = []
286
+
287
+ for i, pose in enumerate(poses_selected):
288
+ progress((i + 1) / len(poses_selected), desc=f"Pose {i+1}/{len(poses_selected)}")
289
+ prompt = f"{subject_text}, {pose}{extra_text}"
290
+ img = generate(images, prompt, guidance, steps, seed + i)
291
+ results.append((img, pose[:50]))
292
 
293
+ return results
294
  finally:
295
+ gc.collect(); torch.cuda.empty_cache()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
 
298
+ # ===========================================================
299
+ # Tab 4: Dataset Generator
300
+ # ===========================================================
301
  @spaces.GPU(duration=300)
302
+ def generate_dataset(ref_images, subject, extra, count, nsfw_on, nsfw_str,
303
+ seed, guidance, steps, progress=gr.Progress(track_tqdm=True)):
304
+ gc.collect(); torch.cuda.empty_cache()
 
 
 
 
305
  try:
306
+ images = process_images(ref_images)
307
+ if not images:
308
  raise gr.Error("Upload at least one reference image!")
309
 
310
+ loras = [("consistency", 1.0)]
311
+ if nsfw_on:
312
+ loras.append(("nsfw", nsfw_str))
313
+ activate_loras(loras)
314
 
315
  count = int(count)
316
  poses = (POSE_LIBRARY * ((count // len(POSE_LIBRARY)) + 1))[:count]
317
+ subject_text = subject.strip() if subject and subject.strip() else "a person"
318
+ extra_text = ", " + extra.strip() if extra and extra.strip() else ""
 
 
319
 
320
  results = []
321
  captions = []
 
 
322
 
323
  for i, pose in enumerate(poses):
324
  progress((i + 1) / count, desc=f"Image {i+1}/{count}")
325
  caption = f"{subject_text}, {pose}{extra_text}"
326
+ img = generate(images, caption, guidance, steps, seed + i)
327
+ results.append((img, f"{i:03d}"))
328
  captions.append(f"{i:03d}.txt: {caption}")
329
 
330
+ status = f"Generated {count} images.\nFirst 7 = 360 character sheet views.\n\n"
331
+ status += "Caption preview:\n" + "\n".join(captions[:15])
332
+ if count > 15:
333
+ status += f"\n... +{count - 15} more"
334
  return results, status
335
  finally:
336
+ gc.collect(); torch.cuda.empty_cache()
 
337
 
338
 
339
+ # ===========================================================
340
+ # UI
341
+ # ===========================================================
342
+ css = "#app { margin: 0 auto; max-width: 1100px; }"
343
 
344
  with gr.Blocks(css=css) as demo:
345
+ with gr.Column(elem_id="app"):
346
+ gr.Markdown("# FLUX.2 Klein Studio\nText prompt β†’ Generate β†’ Edit β†’ Pose β†’ LoRA Dataset. Full pipeline.")
347
 
348
  with gr.Tabs():
349
+ # ==================== TEXT TO IMAGE ====================
350
+ with gr.TabItem("Text to Image"):
351
+ gr.Markdown("Generate from a text prompt using Z-Image Turbo. No censorship. Use the output as a starting point for the other tabs.")
352
  with gr.Row():
353
+ with gr.Column():
354
+ t2i_prompt = gr.Textbox(label="Prompt", lines=3, placeholder="Describe the character/scene...")
355
+ t2i_neg = gr.Textbox(label="Negative prompt", lines=1, value="worst quality, low quality, blurry, deformed")
356
+ with gr.Row():
357
+ t2i_w = gr.Slider(512, 1536, value=1024, step=64, label="Width")
358
+ t2i_h = gr.Slider(512, 1536, value=1024, step=64, label="Height")
359
  with gr.Row():
360
+ t2i_steps = gr.Slider(1, 20, value=9, step=1, label="Steps")
361
+ t2i_guidance = gr.Slider(0.0, 10.0, value=0.0, step=0.1, label="Guidance (0 for Turbo)")
362
+ t2i_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
363
+ t2i_rand = gr.Checkbox(value=True, label="Randomize seed")
364
+ t2i_btn = gr.Button("Generate", variant="primary", size="lg")
365
+ with gr.Column():
366
+ t2i_out = gr.Image(label="Result", interactive=False, format="png", height=500)
367
+ t2i_seed_out = gr.Number(label="Seed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
+ t2i_btn.click(fn=txt2img,
370
+ inputs=[t2i_prompt, t2i_neg, t2i_seed, t2i_rand, t2i_steps, t2i_guidance, t2i_w, t2i_h],
371
+ outputs=[t2i_out, t2i_seed_out])
372
+
373
+ # ==================== FACE SWAP ====================
374
+ with gr.TabItem("Face Swap"):
375
+ gr.Markdown("Upload body/scene as Picture 1, face reference as Picture 2. BFS Head Swap LoRA auto-loaded.")
 
 
 
 
 
 
 
 
 
376
  with gr.Row():
377
  with gr.Column():
378
+ fs_body = gr.Gallery(label="Body / Scene (Picture 1)", type="filepath", columns=1, rows=1, height=220)
379
+ fs_face = gr.Gallery(label="Face Reference (Picture 2)", type="filepath", columns=1, rows=1, height=220)
380
+ fs_prompt = gr.Textbox(label="Custom prompt (leave empty for default swap prompt)", lines=2)
 
 
 
381
  with gr.Row():
382
+ fs_nsfw = gr.Checkbox(value=True, label="NSFW LoRA")
383
+ fs_nsfw_str = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="NSFW strength")
384
+ fs_swap_str = gr.Slider(0.0, 2.0, value=1.0, step=0.05, label="Swap strength")
385
+ fs_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
386
+ fs_rand = gr.Checkbox(value=True, label="Randomize seed")
387
+ fs_btn = gr.Button("Swap Faces", variant="primary", size="lg")
388
  with gr.Column():
389
+ fs_out = gr.Image(label="Result", interactive=False, format="png", height=500)
390
+ fs_seed_out = gr.Number(label="Seed")
391
+
392
+ fs_btn.click(fn=face_swap,
393
+ inputs=[fs_body, fs_face, fs_prompt, fs_nsfw, fs_nsfw_str, fs_swap_str, fs_seed, fs_rand],
394
+ outputs=[fs_out, fs_seed_out])
395
+
396
+ # ==================== IMAGE EDIT ====================
397
+ with gr.TabItem("Image Edit"):
398
+ gr.Markdown("Upload image(s) and describe the edit. NSFW LoRA on by default.")
399
+ with gr.Row():
400
+ with gr.Column():
401
+ ie_images = gr.Gallery(label="Input Images", type="filepath", columns=2, rows=1, height=280)
402
+ ie_template = gr.Dropdown(list(EDIT_TEMPLATES.keys()), value="Custom", label="Preset")
403
+ ie_prompt = gr.Textbox(label="Edit prompt", lines=3, placeholder="Describe what to change...")
404
+ with gr.Row():
405
+ ie_nsfw = gr.Checkbox(value=True, label="NSFW LoRA")
406
+ ie_nsfw_str = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="NSFW strength")
407
+ with gr.Row():
408
+ ie_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
409
+ ie_guidance = gr.Slider(0.0, 10.0, value=1.0, step=0.1, label="Guidance")
410
+ ie_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
411
+ ie_rand = gr.Checkbox(value=True, label="Randomize seed")
412
+ ie_btn = gr.Button("Edit", variant="primary", size="lg")
413
+ with gr.Column():
414
+ ie_out = gr.Image(label="Result", interactive=False, format="png", height=500)
415
+ ie_seed_out = gr.Number(label="Seed")
416
+
417
+ ie_template.change(fn=lambda t: EDIT_TEMPLATES.get(t, ""), inputs=[ie_template], outputs=[ie_prompt])
418
+ ie_btn.click(fn=image_edit,
419
+ inputs=[ie_images, ie_prompt, ie_nsfw, ie_nsfw_str, ie_seed, ie_rand, ie_steps, ie_guidance],
420
+ outputs=[ie_out, ie_seed_out])
421
+
422
+ # ==================== POSE VARIATIONS ====================
423
+ with gr.TabItem("Pose Variations"):
424
+ gr.Markdown("Generate the same character in different poses. Consistency + NSFW LoRAs auto-loaded.")
425
+ with gr.Row():
426
+ with gr.Column(scale=1):
427
+ pv_ref = gr.Gallery(label="Reference Images", type="filepath", columns=2, rows=1, height=200)
428
+ pv_subject = gr.Textbox(label="Subject description", placeholder="e.g. a woman with red hair", lines=1)
429
+ pv_extra = gr.Textbox(label="Extra prompt (appended to each)", placeholder="e.g. nude, studio lighting", lines=1)
430
+ pv_poses = gr.CheckboxGroup(
431
+ choices=POSE_LIBRARY[:20], # Show first 20 for selection
432
+ value=POSE_LIBRARY[:7], # Default: 360 sheet views
433
+ label="Select poses (first 7 = 360 character sheet)",
434
+ )
435
+ with gr.Row():
436
+ pv_nsfw = gr.Checkbox(value=True, label="NSFW LoRA")
437
+ pv_nsfw_str = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="NSFW strength")
438
+ with gr.Row():
439
+ pv_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
440
+ pv_guidance = gr.Slider(0.0, 10.0, value=1.0, step=0.1, label="Guidance")
441
+ pv_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
442
+ pv_btn = gr.Button("Generate Poses", variant="primary", size="lg")
443
+ with gr.Column(scale=2):
444
+ pv_gallery = gr.Gallery(label="Results", columns=4, rows=2, height=500, object_fit="contain")
445
+
446
+ pv_btn.click(fn=pose_variations,
447
+ inputs=[pv_ref, pv_subject, pv_extra, pv_poses, pv_nsfw, pv_nsfw_str,
448
+ pv_seed, pv_guidance, pv_steps],
449
+ outputs=[pv_gallery])
450
+
451
+ # ==================== DATASET GENERATOR ====================
452
+ with gr.TabItem("LoRA Dataset"):
453
+ gr.Markdown("Batch-generate captioned images for LoRA training. First 7 = 360 sheet, then cycles through 50 poses. Consistency + NSFW LoRAs auto-loaded.")
454
  with gr.Row():
455
  with gr.Column(scale=1):
456
  ds_ref = gr.Gallery(label="Reference Images", type="filepath", columns=2, rows=1, height=200)
457
+ ds_subject = gr.Textbox(label="Subject (caption prefix)", placeholder="e.g. a woman with red hair, green eyes, freckles", lines=2)
458
+ ds_extra = gr.Textbox(label="Extra (appended to each caption)", placeholder="e.g. nude, studio lighting, white background", lines=1)
459
+ ds_count = gr.Slider(7, 150, value=50, step=1, label="Number of images")
460
+ with gr.Row():
461
+ ds_nsfw = gr.Checkbox(value=True, label="NSFW LoRA")
462
+ ds_nsfw_str = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="NSFW strength")
 
463
  with gr.Row():
464
+ ds_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Starting seed")
465
  ds_guidance = gr.Slider(0.0, 10.0, value=1.0, step=0.1, label="Guidance")
466
  ds_steps = gr.Slider(1, 50, value=4, step=1, label="Steps")
467
  ds_btn = gr.Button("Generate Dataset", variant="primary", size="lg")
 
468
  with gr.Column(scale=2):
469
+ ds_gallery = gr.Gallery(label="Dataset", columns=5, rows=3, height=500, object_fit="contain")
470
+ ds_status = gr.Textbox(label="Captions", lines=8, interactive=False)
471
+
472
+ ds_btn.click(fn=generate_dataset,
473
+ inputs=[ds_ref, ds_subject, ds_extra, ds_count, ds_nsfw, ds_nsfw_str,
474
+ ds_seed, ds_guidance, ds_steps],
475
+ outputs=[ds_gallery, ds_status])
 
 
476
 
477
  if __name__ == "__main__":
478
  demo.queue().launch(ssr_mode=False, show_error=True)