alexander00001 commited on
Commit
f09a9f1
·
verified ·
1 Parent(s): e422165

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +567 -329
app.py CHANGED
@@ -8,56 +8,89 @@ except ImportError:
8
 
9
  import gradio as gr
10
  import torch
11
- from diffusers import StableDiffusionPipeline
12
  from PIL import Image
13
  import datetime
14
  import io
15
  import json
16
  import os
17
- from typing import Optional
 
 
18
 
19
  # ======================
20
- # 配置区(你只需修改这里即可扩展)
21
  # ======================
22
 
23
- # 1. 基础模型
24
- BASE_MODEL = "SG161222/RealisticVisionV6.0"
25
-
26
- # 2. 固定LoRA(不可选,自动加载)
27
- FIXED_LORAS = [
28
- ("Lykon/epiCRealism_LoRA", 0.8), # 质量增强
29
- ("latent-consistency/lora-dreamshaper", 0.7), # 姿势控制
30
- ]
 
 
 
 
 
 
 
 
 
 
31
 
32
- # 3. 风格模板(自动拼接到用户提示词前)
33
  STYLE_PROMPTS = {
34
  "None": "",
35
- "Realistic": "photorealistic, ultra-detailed skin, natural lighting, 8k, professional photography, f/1.8, shallow depth of field, Canon EOS R5, ",
36
- "Anime": "anime style, cel shading, vibrant colors, detailed eyes, studio ghibli, trending on pixiv, ",
37
- "Comic": "comic book style, bold outlines, dynamic angles, comic panel, Marvel style, inked lines, ",
38
- "Watercolor": "watercolor painting, soft brush strokes, translucent layers, artistic, painterly, paper texture, ",
39
  }
40
 
41
- # 4. 可选LoRA下拉菜单(用户可选1个,None表示清除)
42
- OPTIONAL_LORAS = [
43
- "None",
44
- "Add Detail: https://huggingface.co/latent-consistency/lora-add-detail",
45
- "Vintage Photo: https://huggingface.co/ckpt/LoRA-vintage-photo",
46
- "Cinematic: https://huggingface.co/latent-consistency/lora-cinematic",
47
- "Portrait Enhancer: https://huggingface.co/deforum/Portrait-Enhancer-LoRA",
48
- "Soft Focus: https://huggingface.co/latent-consistency/lora-soft-focus",
49
- ]
50
-
51
- # 解析可选LoRA的名称和ID
52
- OPTIONAL_LORA_MAP = {}
53
- for item in OPTIONAL_LORAS:
54
- if item != "None":
55
- name, url = item.split(": ", 1)
56
- OPTIONAL_LORA_MAP[name] = url
57
- else:
58
- OPTIONAL_LORA_MAP["None"] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # 默认参数
61
  DEFAULT_SEED = -1
62
  DEFAULT_WIDTH = 1024
63
  DEFAULT_HEIGHT = 1024
@@ -65,337 +98,542 @@ DEFAULT_LORA_SCALE = 0.8
65
  DEFAULT_STEPS = 30
66
  DEFAULT_CFG = 7.5
67
 
 
 
 
 
 
 
 
 
68
  # ======================
69
- # 全局变量:延迟加载模型
70
  # ======================
71
  pipe = None
 
72
  device = "cuda" if torch.cuda.is_available() else "cpu"
73
 
74
  def load_pipeline():
 
75
  global pipe
76
  if pipe is None:
77
- print("🚀 Loading base model...")
78
- pipe = StableDiffusionPipeline.from_pretrained(
79
  BASE_MODEL,
80
  torch_dtype=torch.float16,
81
- safety_checker=None,
82
- requires_safety_checker=False,
83
  ).to(device)
 
 
84
  pipe.enable_attention_slicing()
85
  pipe.enable_vae_slicing()
86
- pipe.enable_model_cpu_offload() # 适配ZeroGPU
87
- print("✅ Base model loaded.")
 
 
88
  return pipe
89
 
90
  def unload_pipeline():
91
- global pipe
 
92
  if pipe is not None:
 
 
 
 
 
93
  del pipe
94
  torch.cuda.empty_cache()
95
  pipe = None
 
96
  print("🗑️ Pipeline unloaded.")
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  # ======================
99
- # 主生成函数
100
  # ======================
 
101
  def generate_image(
102
- prompt, negative_prompt, style, seed, width, height, optional_lora_name, lora_scale,
103
- steps, cfg_scale
 
 
 
 
 
 
 
 
 
104
  ):
 
105
  global pipe
106
-
107
- # 加载模型(懒加载)
108
- pipe = load_pipeline()
109
-
110
- # 处理种子
111
- if seed == -1:
112
- seed = torch.randint(0, 2**32, (1,)).item()
113
- generator = torch.Generator(device=device).manual_seed(seed)
114
-
115
- # 拼接风格提示词
116
- full_prompt = STYLE_PROMPTS[style] + prompt
117
- full_negative_prompt = negative_prompt
118
-
119
- # 加载固定LoRA(每次生成前都加载,确保状态正确)
120
- for lora_id, scale in FIXED_LORAS:
121
- pipe.load_lora_weights(lora_id, adapter_name=lora_id)
122
- pipe.set_adapters([lora_id], adapter_weights=[scale])
123
-
124
- # 加载可选LoRA(如果非None)
125
- if optional_lora_name != "None":
126
- lora_url = OPTIONAL_LORA_MAP[optional_lora_name]
127
- pipe.load_lora_weights(lora_url, adapter_name=optional_lora_name)
128
- pipe.set_adapters([lora_id for lora_id, _ in FIXED_LORAS] + [optional_lora_name],
129
- adapter_weights=[scale for _, scale in FIXED_LORAS] + [lora_scale])
130
- else:
131
- # 清除所有可选LoRA,只保留固定
132
- pipe.set_adapters([lora_id for lora_id, _ in FIXED_LORAS],
133
- adapter_weights=[scale for _, scale in FIXED_LORAS])
134
-
135
- # 生成图像
136
- image = pipe(
137
- prompt=full_prompt,
138
- negative_prompt=full_negative_prompt,
139
- num_inference_steps=steps,
140
- guidance_scale=cfg_scale,
141
- width=width,
142
- height=height,
143
- generator=generator,
144
- ).images[0]
145
-
146
- # 生成元数据
147
- metadata = {
148
- "prompt": full_prompt,
149
- "negative_prompt": full_negative_prompt,
150
- "base_model": BASE_MODEL,
151
- "fixed_loras": [lora_id for lora_id, _ in FIXED_LORAS],
152
- "optional_lora": optional_lora_name if optional_lora_name != "None" else None,
153
- "lora_scale": lora_scale,
154
- "seed": seed,
155
- "steps": steps,
156
- "cfg_scale": cfg_scale,
157
- "style": style,
158
- "width": width,
159
- "height": height,
160
- "timestamp": datetime.datetime.now().isoformat()
161
- }
162
-
163
- # 生成文件名
164
- timestamp = datetime.datetime.now().strftime("%y%m%d%H%M")
165
- filename_base = f"{seed}-{timestamp}"
166
-
167
- # 保存为WebP(高质量)
168
- img_buffer = io.BytesIO()
169
- image.save(img_buffer, format="WEBP", quality=95, method=6)
170
- img_buffer.seek(0)
171
-
172
- # 保存元数据为TXT
173
- metadata_buffer = io.StringIO()
174
- json.dump(metadata, metadata_buffer, indent=2, ensure_ascii=False)
175
- metadata_buffer.seek(0)
176
-
177
- # 返回:图像、元数据、文件名
178
- return (
179
- image,
180
- json.dumps(metadata, indent=2, ensure_ascii=False),
181
- f"{filename_base}.webp",
182
- f"{filename_base}.txt",
183
- img_buffer.getvalue(),
184
- metadata_buffer.getvalue().encode('utf-8')
185
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  # ======================
188
- # Gradio UI
189
  # ======================
190
- with gr.Blocks(
191
- theme=gr.themes.Soft(
192
- primary_hue="blue",
193
- secondary_hue="green",
194
- neutral_hue="slate",
195
- ).set(
196
- body_background_fill="linear-gradient(135deg, #1e40af, #059669)",
197
- button_primary_background_fill="white",
198
- button_primary_text_color="#1e40af",
199
- input_background_fill="rgba(255,255,255,0.9)",
200
- ),
201
- css="""
202
- body { font-family: 'Helvetica Neue', 'Segoe UI', 'Arial', sans-serif; }
203
- .gr-button { font-family: 'Helvetica Neue', 'Arial', sans-serif; font-weight: 500; }
204
- .gr-textarea { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; }
205
- """,
206
- ) as demo:
207
- gr.Markdown(
208
- """
209
- # 🎨 AI Photo Generator (RealisticVision + LoRA)
210
- **PRO + ZeroGPU Optimized | Multi-LoRA | Style Templates | Metadata Export**
211
- """
212
- )
213
-
214
- with gr.Row():
215
- with gr.Column(scale=3):
216
- # a. 提示词输入框
217
- prompt_input = gr.Textbox(
218
- label="Prompt (Positive)",
219
- placeholder="A beautiful woman, golden hour, soft sunlight...",
220
- lines=5,
221
- max_lines=20,
222
- elem_classes=["gr-textarea"]
223
- )
224
-
225
- # b. 负提示词输入框
226
- negative_prompt_input = gr.Textbox(
227
- label="Negative Prompt",
228
- placeholder="blurry, low quality, deformed, cartoon, anime, text, watermark...",
229
- lines=5,
230
- max_lines=20,
231
- elem_classes=["gr-textarea"]
232
- )
233
-
234
- # c. 风格选择(单选)
235
- style_radio = gr.Radio(
236
- choices=list(STYLE_PROMPTS.keys()),
237
- label="Style",
238
- value="Realistic",
239
- elem_classes=["gr-radio"]
240
- )
241
-
242
- # d. 种子选择
243
- with gr.Row():
244
- seed_input = gr.Slider(
245
- minimum=-1,
246
- maximum=99999999,
247
- step=1,
248
- value=DEFAULT_SEED,
249
- label="Seed (-1 = Random)"
250
  )
251
- seed_reset = gr.Button("Reset Seed")
252
-
253
- # e. 宽度选择
254
- with gr.Row():
255
- width_input = gr.Slider(
256
- minimum=512,
257
- maximum=1536,
258
- step=64,
259
- value=DEFAULT_WIDTH,
260
- label="Width"
261
  )
262
- width_reset = gr.Button("Reset Width")
263
-
264
- # f. 高度选择
265
- with gr.Row():
266
- height_input = gr.Slider(
267
- minimum=512,
268
- maximum=1536,
269
- step=64,
270
- value=DEFAULT_HEIGHT,
271
- label="Height"
272
  )
273
- height_reset = gr.Button("Reset Height")
274
-
275
- # g. LoRA选择(下拉)
276
- optional_lora_dropdown = gr.Dropdown(
277
- choices=list(OPTIONAL_LORA_MAP.keys()),
278
- label="Optional LoRA",
279
- value="None",
280
- elem_classes=["gr-dropdown"]
281
- )
282
-
283
- # h. LoRA控制
284
- with gr.Row():
285
- lora_scale_slider = gr.Slider(
286
- minimum=0.0,
287
- maximum=1.5,
288
- step=0.05,
289
- value=DEFAULT_LORA_SCALE,
290
- label="LoRA Scale"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  )
292
- lora_reset = gr.Button("Reset LoRA Scale")
293
-
294
- # i. 功能控制(Steps & CFG)
295
- with gr.Row():
296
- steps_slider = gr.Slider(
297
- minimum=10,
298
- maximum=100,
299
- step=1,
300
- value=DEFAULT_STEPS,
301
- label="Steps"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  )
303
- cfg_slider = gr.Slider(
304
- minimum=1.0,
305
- maximum=20.0,
306
- step=0.5,
307
- value=DEFAULT_CFG,
308
- label="CFG Scale"
 
309
  )
310
- gen_reset = gr.Button("Reset Generation")
311
-
312
- # m. 生成按钮
313
- generate_btn = gr.Button("✨ Generate Image", variant="primary", size="lg")
314
-
315
- with gr.Column(scale=2):
316
- # j. 图片显示区
317
- image_output = gr.Image(label="Generated Image", height=768, format="webp")
318
-
319
- # k. 元数据显示区
320
- metadata_output = gr.Textbox(
321
- label="Metadata (JSON)",
322
- lines=12,
323
- max_lines=20,
324
- elem_classes=["gr-textarea"]
325
- )
326
-
327
- # l. 下载按钮(并列)
328
- with gr.Row():
329
- download_img_btn = gr.Button("⬇️ Download Image (WebP)")
330
- download_meta_btn = gr.Button("⬇️ Download Metadata (TXT)")
331
-
332
- # 隐藏文件输出(用于下载)
333
- hidden_img_file = gr.File(visible=False)
334
- hidden_meta_file = gr.File(visible=False)
335
-
336
- # ======================
337
- # 事件绑定
338
- # ======================
339
-
340
- # 重置种子
341
- seed_reset.click(fn=lambda: -1, outputs=seed_input)
342
- # 重置宽度
343
- width_reset.click(fn=lambda: DEFAULT_WIDTH, outputs=width_input)
344
- # 重置高度
345
- height_reset.click(fn=lambda: DEFAULT_HEIGHT, outputs=height_input)
346
- # 重置LoRA缩放
347
- lora_reset.click(fn=lambda: DEFAULT_LORA_SCALE, outputs=lora_scale_slider)
348
- # 重置生成参数
349
- gen_reset.click(
350
- fn=lambda: (DEFAULT_STEPS, DEFAULT_CFG),
351
- outputs=[steps_slider, cfg_slider]
352
- )
353
-
354
- # 生成
355
- generate_btn.click(
356
- fn=generate_image,
357
- inputs=[
358
- prompt_input, negative_prompt_input, style_radio,
359
- seed_input, width_input, height_input,
360
- optional_lora_dropdown, lora_scale_slider,
361
- steps_slider, cfg_slider
362
- ],
363
- outputs=[
364
- image_output, metadata_output,
365
- hidden_img_file, hidden_meta_file,
366
- hidden_img_file, hidden_meta_file
367
- ]
368
- )
369
-
370
- # 下载图片
371
- download_img_btn.click(
372
- fn=None,
373
- inputs=[hidden_img_file],
374
- outputs=None,
375
- js="(f) => { const a = document.createElement('a'); a.href = f; a.download = f.split('/').pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }"
376
- )
377
-
378
- # 下载元数据
379
- generate_btn.click(
380
- fn=generate_image,
381
- inputs=[
382
- prompt_input, negative_prompt_input, style_radio,
383
- seed_input, width_input, height_input,
384
- optional_lora_dropdown, lora_scale_slider,
385
- steps_slider, cfg_slider
386
- ],
387
- outputs=[
388
- image_output,
389
- metadata_output,
390
- hidden_img_file, # 👈 更新隐藏文件组件(Gradio 自动处理下载)
391
- hidden_meta_file, # 👈
392
- ]
393
- )
394
-
395
- # 设置文件下载(通过返回值触发)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
 
 
397
  # ======================
398
- # 启动
399
  # ======================
400
  if __name__ == "__main__":
401
- demo.launch()
 
 
 
 
 
 
 
 
8
 
9
  import gradio as gr
10
  import torch
11
+ from diffusers import DiffusionPipeline, StableDiffusionXLPipeline
12
  from PIL import Image
13
  import datetime
14
  import io
15
  import json
16
  import os
17
+ import re
18
+ from typing import Optional, List, Dict
19
+ import numpy as np
20
 
21
  # ======================
22
+ # Configuration Section (Modify here to expand)
23
  # ======================
24
 
25
+ # 1. Base Model - Illustrious XL v1.0
26
+ BASE_MODEL = "OnomaAIResearch/Illustrious-XL-v1.0"
27
+
28
+ # 2. Fixed LoRAs (Auto-loaded, not user-selectable)
29
+ FIXED_LORAS = {
30
+ "quality_enhancer": {
31
+ "repo_id": "stabilityai/stable-diffusion-xl-base-1.0",
32
+ "filename": "sd_xl_base_1.0.safetensors",
33
+ "weight": 0.8,
34
+ "trigger_words": "high quality, detailed, sharp"
35
+ },
36
+ "pose_control": {
37
+ "repo_id": "latent-consistency/lcm-lora-sdxl",
38
+ "filename": None,
39
+ "weight": 0.7,
40
+ "trigger_words": "perfect anatomy, natural pose"
41
+ }
42
+ }
43
 
44
+ # 3. Style Templates (Auto-prepended to user prompts)
45
  STYLE_PROMPTS = {
46
  "None": "",
47
+ "Realistic": "photorealistic, ultra-detailed skin, natural lighting, 8k uhd, professional photography, DSLR, soft lighting, high quality, film grain, Fujifilm XT3, masterpiece, ",
48
+ "Anime": "anime style, cel shading, vibrant colors, detailed eyes, studio ghibli style, manga style, trending on pixiv, masterpiece, ",
49
+ "Comic": "comic book style, bold outlines, dynamic angles, comic panel, Marvel DC style, inked lines, pop art, masterpiece, ",
50
+ "Watercolor": "watercolor painting, soft brush strokes, translucent layers, artistic, painterly, paper texture, traditional art, masterpiece, ",
51
  }
52
 
53
+ # 4. Optional LoRAs (User-selectable via dropdown, can select multiple)
54
+ OPTIONAL_LORAS = {
55
+ "None": {
56
+ "repo_id": None,
57
+ "weight": 0.0,
58
+ "trigger_words": "",
59
+ "description": "No additional LoRA"
60
+ },
61
+ "Detail Enhancement": {
62
+ "repo_id": "ByteDance/SDXL-Lightning",
63
+ "weight": 0.8,
64
+ "trigger_words": "extremely detailed, intricate details, hyperdetailed",
65
+ "description": "Enhances fine details and textures"
66
+ },
67
+ "Portrait Master": {
68
+ "repo_id": "ostris/face-helper-sdxl-lora",
69
+ "weight": 0.9,
70
+ "trigger_words": "perfect face, beautiful eyes, detailed skin texture",
71
+ "description": "Specializes in realistic portraits"
72
+ },
73
+ "Cinematic Style": {
74
+ "repo_id": "goofyai/cyborg_style_xl",
75
+ "weight": 0.7,
76
+ "trigger_words": "cinematic lighting, dramatic shadows, film noir",
77
+ "description": "Adds cinematic atmosphere"
78
+ },
79
+ "Vintage Photo": {
80
+ "repo_id": "artificialguybr/LogoRedmond-LogoLoraForSDXL-V2",
81
+ "weight": 0.6,
82
+ "trigger_words": "vintage photo, retro style, film photography",
83
+ "description": "Vintage photography effects"
84
+ },
85
+ "Art Nouveau": {
86
+ "repo_id": "ostris/super-cereal-sdxl-lora",
87
+ "weight": 0.8,
88
+ "trigger_words": "art nouveau style, ornate decorations, flowing lines",
89
+ "description": "Art Nouveau artistic style"
90
+ }
91
+ }
92
 
93
+ # Default Parameters
94
  DEFAULT_SEED = -1
95
  DEFAULT_WIDTH = 1024
96
  DEFAULT_HEIGHT = 1024
 
98
  DEFAULT_STEPS = 30
99
  DEFAULT_CFG = 7.5
100
 
101
+ # Supported Languages (for future expansion)
102
+ SUPPORTED_LANGUAGES = {
103
+ "en": "English",
104
+ "zh": "中文",
105
+ "ja": "日本語",
106
+ "ko": "한국어"
107
+ }
108
+
109
  # ======================
110
+ # Global Variables: Lazy Loading
111
  # ======================
112
  pipe = None
113
+ current_loras = {}
114
  device = "cuda" if torch.cuda.is_available() else "cpu"
115
 
116
  def load_pipeline():
117
+ """Load the base Illustrious XL pipeline"""
118
  global pipe
119
  if pipe is None:
120
+ print("🚀 Loading Illustrious XL base model...")
121
+ pipe = StableDiffusionXLPipeline.from_pretrained(
122
  BASE_MODEL,
123
  torch_dtype=torch.float16,
124
+ use_safetensors=True,
125
+ variant="fp16"
126
  ).to(device)
127
+
128
+ # Enable memory optimizations for ZeroGPU
129
  pipe.enable_attention_slicing()
130
  pipe.enable_vae_slicing()
131
+ pipe.enable_model_cpu_offload()
132
+ pipe.enable_xformers_memory_efficient_attention()
133
+
134
+ print("✅ Illustrious XL model loaded successfully.")
135
  return pipe
136
 
137
  def unload_pipeline():
138
+ """Unload pipeline to free memory"""
139
+ global pipe, current_loras
140
  if pipe is not None:
141
+ # Clear any loaded LoRAs
142
+ try:
143
+ pipe.unload_lora_weights()
144
+ except:
145
+ pass
146
  del pipe
147
  torch.cuda.empty_cache()
148
  pipe = None
149
+ current_loras = {}
150
  print("🗑️ Pipeline unloaded.")
151
 
152
+ def load_lora_weights(lora_configs: List[Dict]):
153
+ """Load multiple LoRA weights efficiently"""
154
+ global pipe, current_loras
155
+
156
+ if not lora_configs:
157
+ return
158
+
159
+ # Unload existing LoRAs if different
160
+ new_lora_ids = [config['repo_id'] for config in lora_configs if config['repo_id']]
161
+ if set(current_loras.keys()) != set(new_lora_ids):
162
+ try:
163
+ pipe.unload_lora_weights()
164
+ current_loras = {}
165
+ except:
166
+ pass
167
+
168
+ # Load new LoRAs
169
+ adapter_names = []
170
+ adapter_weights = []
171
+
172
+ for config in lora_configs:
173
+ if config['repo_id'] and config['repo_id'] not in current_loras:
174
+ try:
175
+ pipe.load_lora_weights(
176
+ config['repo_id'],
177
+ adapter_name=config['name']
178
+ )
179
+ current_loras[config['repo_id']] = config['name']
180
+ print(f"✅ Loaded LoRA: {config['name']}")
181
+ except Exception as e:
182
+ print(f"❌ Failed to load LoRA {config['name']}: {e}")
183
+ continue
184
+
185
+ if config['repo_id']:
186
+ adapter_names.append(config['name'])
187
+ adapter_weights.append(config['weight'])
188
+
189
+ # Set adapter weights
190
+ if adapter_names:
191
+ try:
192
+ pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
193
+ except Exception as e:
194
+ print(f"⚠️ Warning setting adapter weights: {e}")
195
+
196
+ def process_long_prompt(prompt: str, max_length: int = 77) -> str:
197
+ """Process long prompts by intelligent truncation and optimization"""
198
+ if len(prompt.split()) <= max_length:
199
+ return prompt
200
+
201
+ # Split into sentences and prioritize
202
+ sentences = re.split(r'[.!?]+', prompt)
203
+ sentences = [s.strip() for s in sentences if s.strip()]
204
+
205
+ # Keep most important parts (first sentence + key descriptors)
206
+ if sentences:
207
+ result = sentences[0]
208
+ remaining = max_length - len(result.split())
209
+
210
+ for sentence in sentences[1:]:
211
+ words = sentence.split()
212
+ if len(words) <= remaining:
213
+ result += ". " + sentence
214
+ remaining -= len(words)
215
+ else:
216
+ # Add partial sentence with most important words
217
+ important_words = [w for w in words if len(w) > 3][:remaining]
218
+ if important_words:
219
+ result += ". " + " ".join(important_words)
220
+ break
221
+
222
+ return result
223
+
224
+ return " ".join(prompt.split()[:max_length])
225
+
226
  # ======================
227
+ # Main Generation Function
228
  # ======================
229
+ @spaces.GPU(duration=60) if SPACES_AVAILABLE else lambda x: x
230
  def generate_image(
231
+ prompt: str,
232
+ negative_prompt: str,
233
+ style: str,
234
+ seed: int,
235
+ width: int,
236
+ height: int,
237
+ selected_loras: List[str],
238
+ lora_scale: float,
239
+ steps: int,
240
+ cfg_scale: float,
241
+ language: str = "en"
242
  ):
243
+ """Main image generation function with ZeroGPU optimization"""
244
  global pipe
245
+
246
+ try:
247
+ # Load pipeline
248
+ pipe = load_pipeline()
249
+
250
+ # Handle seed
251
+ if seed == -1:
252
+ seed = torch.randint(0, 2**32, (1,)).item()
253
+ generator = torch.Generator(device=device).manual_seed(seed)
254
+
255
+ # Process prompts
256
+ style_prefix = STYLE_PROMPTS.get(style, "")
257
+ processed_prompt = process_long_prompt(style_prefix + prompt, max_length=150)
258
+ processed_negative = process_long_prompt(negative_prompt, max_length=100)
259
+
260
+ # Prepare LoRA configurations
261
+ lora_configs = []
262
+ active_trigger_words = []
263
+
264
+ # Add fixed LoRAs
265
+ for name, config in FIXED_LORAS.items():
266
+ if config["repo_id"]:
267
+ lora_configs.append({
268
+ 'name': name,
269
+ 'repo_id': config["repo_id"],
270
+ 'weight': config["weight"]
271
+ })
272
+ if config["trigger_words"]:
273
+ active_trigger_words.append(config["trigger_words"])
274
+
275
+ # Add selected optional LoRAs
276
+ for lora_name in selected_loras:
277
+ if lora_name != "None" and lora_name in OPTIONAL_LORAS:
278
+ config = OPTIONAL_LORAS[lora_name]
279
+ if config["repo_id"]:
280
+ lora_configs.append({
281
+ 'name': lora_name,
282
+ 'repo_id': config["repo_id"],
283
+ 'weight': config["weight"] * lora_scale
284
+ })
285
+ if config["trigger_words"]:
286
+ active_trigger_words.append(config["trigger_words"])
287
+
288
+ # Load LoRAs
289
+ load_lora_weights(lora_configs)
290
+
291
+ # Combine trigger words with prompt
292
+ if active_trigger_words:
293
+ trigger_text = ", ".join(active_trigger_words)
294
+ final_prompt = f"{processed_prompt}, {trigger_text}"
295
+ else:
296
+ final_prompt = processed_prompt
297
+
298
+ # Generate image
299
+ with torch.autocast(device):
300
+ image = pipe(
301
+ prompt=final_prompt,
302
+ negative_prompt=processed_negative,
303
+ num_inference_steps=steps,
304
+ guidance_scale=cfg_scale,
305
+ width=width,
306
+ height=height,
307
+ generator=generator,
308
+ ).images[0]
309
+
310
+ # Generate metadata
311
+ timestamp = datetime.datetime.now()
312
+ metadata = {
313
+ "prompt": final_prompt,
314
+ "original_prompt": prompt,
315
+ "negative_prompt": processed_negative,
316
+ "base_model": BASE_MODEL,
317
+ "style": style,
318
+ "fixed_loras": [name for name in FIXED_LORAS.keys()],
319
+ "selected_loras": [name for name in selected_loras if name != "None"],
320
+ "lora_scale": lora_scale,
321
+ "seed": seed,
322
+ "steps": steps,
323
+ "cfg_scale": cfg_scale,
324
+ "width": width,
325
+ "height": height,
326
+ "language": language,
327
+ "timestamp": timestamp.isoformat(),
328
+ "trigger_words": active_trigger_words
329
+ }
330
+
331
+ # Generate filenames
332
+ timestamp_str = timestamp.strftime("%y%m%d%H%M")
333
+ filename_base = f"{seed}-{timestamp_str}"
334
+
335
+ # Save image as WebP
336
+ img_buffer = io.BytesIO()
337
+ image.save(img_buffer, format="WEBP", quality=95, method=6)
338
+ img_buffer.seek(0)
339
+
340
+ # Save metadata as JSON
341
+ metadata_str = json.dumps(metadata, indent=2, ensure_ascii=False)
342
+
343
+ return (
344
+ image,
345
+ metadata_str,
346
+ f"{filename_base}.webp",
347
+ f"{filename_base}.txt"
348
+ )
349
+
350
+ except Exception as e:
351
+ error_msg = f"Generation failed: {str(e)}"
352
+ print(f"❌ {error_msg}")
353
+ return None, error_msg, "", ""
354
 
355
  # ======================
356
+ # Gradio Interface
357
  # ======================
358
+ def create_interface():
359
+ """Create the Gradio interface"""
360
+
361
+ with gr.Blocks(
362
+ theme=gr.themes.Soft(
363
+ primary_hue="blue",
364
+ secondary_hue="green",
365
+ neutral_hue="slate",
366
+ ).set(
367
+ body_background_fill="linear-gradient(135deg, #1e40af, #059669)",
368
+ button_primary_background_fill="white",
369
+ button_primary_text_color="#1e40af",
370
+ input_background_fill="rgba(255,255,255,0.9)",
371
+ block_background_fill="rgba(255,255,255,0.1)",
372
+ ),
373
+ css="""
374
+ body {
375
+ font-family: 'Segoe UI', 'Arial', sans-serif;
376
+ background: linear-gradient(135deg, #1e40af, #059669);
377
+ }
378
+ .gr-button {
379
+ font-family: 'Segoe UI', 'Arial', sans-serif;
380
+ font-weight: 600;
381
+ border-radius: 8px;
382
+ }
383
+ .gr-textbox {
384
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
385
+ border-radius: 8px;
386
+ }
387
+ .gr-dropdown, .gr-slider, .gr-radio {
388
+ border-radius: 8px;
389
+ }
390
+ .gr-form {
391
+ background: rgba(255,255,255,0.05);
392
+ border-radius: 16px;
393
+ padding: 20px;
394
+ margin: 10px;
395
+ }
396
+ """,
397
+ title="AI Photo Generator - Illustrious XL"
398
+ ) as demo:
399
+
400
+ gr.Markdown("""
401
+ # 🎨 AI Photo Generator (Illustrious XL + Multi-LoRA)
402
+ **PRO + ZeroGPU Optimized | Multi-LoRA Support | Style Templates | Metadata Export | 1536x1536 Native Resolution**
403
+ """)
404
+
405
+ with gr.Row():
406
+ # Left Column - Controls
407
+ with gr.Column(scale=3, elem_classes=["gr-form"]):
408
+
409
+ # a. Prompt Input
410
+ prompt_input = gr.Textbox(
411
+ label="Prompt (Positive)",
412
+ placeholder="A beautiful woman with flowing hair, golden hour lighting, cinematic composition, high detail...",
413
+ lines=6,
414
+ max_lines=20,
415
+ elem_classes=["gr-textbox"]
 
 
416
  )
417
+
418
+ # b. Negative Prompt Input
419
+ negative_prompt_input = gr.Textbox(
420
+ label="Negative Prompt",
421
+ value="blurry, low quality, deformed, cartoon, anime, text, watermark, signature, username, worst quality, low res, bad anatomy, bad hands, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, bad feet, extra fingers, mutated hands, poorly drawn hands, bad proportions, extra limbs, disfigured, ugly, gross proportions, malformed limbs",
422
+ lines=4,
423
+ max_lines=15,
424
+ elem_classes=["gr-textbox"]
 
 
425
  )
426
+
427
+ # c. Style Selection
428
+ style_radio = gr.Radio(
429
+ choices=list(STYLE_PROMPTS.keys()),
430
+ label="Style Template",
431
+ value="Realistic",
432
+ elem_classes=["gr-radio"]
 
 
 
433
  )
434
+
435
+ # Multi-row controls
436
+ with gr.Row():
437
+ # d. Seed Control
438
+ with gr.Column():
439
+ seed_input = gr.Slider(
440
+ minimum=-1,
441
+ maximum=99999999,
442
+ step=1,
443
+ value=DEFAULT_SEED,
444
+ label="Seed (-1 = Random)"
445
+ )
446
+ seed_reset = gr.Button("Reset Seed", size="sm")
447
+
448
+ with gr.Row():
449
+ # e. Width Control
450
+ with gr.Column():
451
+ width_input = gr.Slider(
452
+ minimum=512,
453
+ maximum=1536,
454
+ step=64,
455
+ value=DEFAULT_WIDTH,
456
+ label="Width"
457
+ )
458
+ width_reset = gr.Button("Reset Width", size="sm")
459
+
460
+ # f. Height Control
461
+ with gr.Column():
462
+ height_input = gr.Slider(
463
+ minimum=512,
464
+ maximum=1536,
465
+ step=64,
466
+ value=DEFAULT_HEIGHT,
467
+ label="Height"
468
+ )
469
+ height_reset = gr.Button("Reset Height", size="sm")
470
+
471
+ # g. LoRA Selection (Multi-select)
472
+ lora_dropdown = gr.Dropdown(
473
+ choices=list(OPTIONAL_LORAS.keys()),
474
+ label="Optional LoRAs (Multi-select)",
475
+ value=["None"],
476
+ multiselect=True,
477
+ elem_classes=["gr-dropdown"]
478
  )
479
+
480
+ # h. LoRA Scale Control
481
+ with gr.Row():
482
+ lora_scale_slider = gr.Slider(
483
+ minimum=0.0,
484
+ maximum=1.5,
485
+ step=0.05,
486
+ value=DEFAULT_LORA_SCALE,
487
+ label="LoRA Scale"
488
+ )
489
+ lora_reset = gr.Button("Reset LoRA", size="sm")
490
+
491
+ # i. Generation Controls
492
+ with gr.Row():
493
+ steps_slider = gr.Slider(
494
+ minimum=10,
495
+ maximum=100,
496
+ step=1,
497
+ value=DEFAULT_STEPS,
498
+ label="Steps"
499
+ )
500
+ cfg_slider = gr.Slider(
501
+ minimum=1.0,
502
+ maximum=20.0,
503
+ step=0.5,
504
+ value=DEFAULT_CFG,
505
+ label="CFG Scale"
506
+ )
507
+ gen_reset = gr.Button("Reset Generation", size="sm")
508
+
509
+ # Language Selection (Optional)
510
+ language_dropdown = gr.Dropdown(
511
+ choices=list(SUPPORTED_LANGUAGES.keys()),
512
+ label="Language (Optional)",
513
+ value="en",
514
+ visible=False # Hidden for now, can be enabled later
515
  )
516
+
517
+ # m. Generate Button
518
+ generate_btn = gr.Button(
519
+ "✨ Generate Image",
520
+ variant="primary",
521
+ size="lg",
522
+ elem_classes=["gr-button"]
523
  )
524
+
525
+ # Right Column - Outputs
526
+ with gr.Column(scale=2):
527
+ # j. Image Display
528
+ image_output = gr.Image(
529
+ label="Generated Image",
530
+ height=600,
531
+ format="webp"
532
+ )
533
+
534
+ # l. Download Buttons (between image and metadata)
535
+ with gr.Row():
536
+ download_img_btn = gr.DownloadButton(
537
+ "⬇️ Download Image (WebP)",
538
+ variant="secondary"
539
+ )
540
+ download_meta_btn = gr.DownloadButton(
541
+ "⬇️ Download Metadata (TXT)",
542
+ variant="secondary"
543
+ )
544
+
545
+ # k. Metadata Display
546
+ metadata_output = gr.Textbox(
547
+ label="Generation Metadata (JSON)",
548
+ lines=15,
549
+ max_lines=25,
550
+ elem_classes=["gr-textbox"]
551
+ )
552
+
553
+ # ======================
554
+ # Event Handlers
555
+ # ======================
556
+
557
+ # Reset buttons
558
+ seed_reset.click(fn=lambda: -1, outputs=seed_input)
559
+ width_reset.click(fn=lambda: DEFAULT_WIDTH, outputs=width_input)
560
+ height_reset.click(fn=lambda: DEFAULT_HEIGHT, outputs=height_input)
561
+ lora_reset.click(fn=lambda: DEFAULT_LORA_SCALE, outputs=lora_scale_slider)
562
+ gen_reset.click(
563
+ fn=lambda: (DEFAULT_STEPS, DEFAULT_CFG),
564
+ outputs=[steps_slider, cfg_slider]
565
+ )
566
+
567
+ # Main generation function
568
+ def generate_and_prepare_downloads(*args):
569
+ result = generate_image(*args)
570
+ if result[0] is not None: # Success
571
+ image, metadata, img_filename, meta_filename = result
572
+
573
+ # Prepare download files
574
+ img_buffer = io.BytesIO()
575
+ image.save(img_buffer, format="WEBP", quality=95)
576
+ img_buffer.seek(0)
577
+
578
+ meta_buffer = io.BytesIO()
579
+ meta_buffer.write(metadata.encode('utf-8'))
580
+ meta_buffer.seek(0)
581
+
582
+ return (
583
+ image,
584
+ metadata,
585
+ gr.DownloadButton.update(value=img_buffer.getvalue(), filename=img_filename),
586
+ gr.DownloadButton.update(value=meta_buffer.getvalue(), filename=meta_filename)
587
+ )
588
+ else: # Error
589
+ return result[0], result[1], None, None
590
+
591
+ # Generate button click
592
+ generate_btn.click(
593
+ fn=generate_and_prepare_downloads,
594
+ inputs=[
595
+ prompt_input, negative_prompt_input, style_radio,
596
+ seed_input, width_input, height_input,
597
+ lora_dropdown, lora_scale_slider,
598
+ steps_slider, cfg_slider, language_dropdown
599
+ ],
600
+ outputs=[
601
+ image_output, metadata_output,
602
+ download_img_btn, download_meta_btn
603
+ ]
604
+ )
605
+
606
+ # Show LoRA descriptions
607
+ def show_lora_info(selected_loras):
608
+ if not selected_loras or selected_loras == ["None"]:
609
+ return "No LoRAs selected"
610
+
611
+ info = "Selected LoRAs:\n"
612
+ for lora_name in selected_loras:
613
+ if lora_name in OPTIONAL_LORAS:
614
+ config = OPTIONAL_LORAS[lora_name]
615
+ info += f"• {lora_name}: {config['description']}\n"
616
+ if config['trigger_words']:
617
+ info += f" Triggers: {config['trigger_words']}\n"
618
+ return info
619
+
620
+ lora_dropdown.change(
621
+ fn=show_lora_info,
622
+ inputs=[lora_dropdown],
623
+ outputs=[gr.Textbox(label="LoRA Information", visible=False)]
624
+ )
625
 
626
+ return demo
627
+
628
  # ======================
629
+ # Launch Application
630
  # ======================
631
  if __name__ == "__main__":
632
+ demo = create_interface()
633
+ demo.queue(max_size=20)
634
+ demo.launch(
635
+ server_name="0.0.0.0",
636
+ server_port=7860,
637
+ share=False,
638
+ show_error=True
639
+ )