RioShiina commited on
Commit
3ecf4c3
·
0 Parent(s):

Initial project: Animated SDXL T2I with LoRAs

Browse files
Files changed (4) hide show
  1. .gitattributes +35 -0
  2. README.md +9 -0
  3. app.py +506 -0
  4. requirements.txt +11 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Animated SDXL T2I with LoRAs
3
+ emoji: 🖼
4
+ colorFrom: purple
5
+ colorTo: red
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: true
9
+ ---
app.py ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces
2
+ import gradio as gr
3
+ import numpy as np
4
+ import PIL.Image
5
+ from PIL import Image, PngImagePlugin
6
+ import random
7
+ from diffusers import StableDiffusionXLPipeline, EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, DPMSolverMultistepScheduler, DDIMScheduler, UniPCMultistepScheduler, HeunDiscreteScheduler, LMSDiscreteScheduler
8
+ import torch
9
+ from compel import Compel, ReturnedEmbeddingsType
10
+ import requests
11
+ import os
12
+ import re
13
+ import gc
14
+ from huggingface_hub import hf_hub_download, snapshot_download
15
+ import time
16
+
17
+ # This dummy function is required to pass the Hugging Face Spaces startup check for GPU apps.
18
+ @spaces.GPU(duration=60)
19
+ def dummy_gpu_for_startup():
20
+ print("Dummy function for startup check executed. This is normal.")
21
+ return "Startup check passed."
22
+
23
+ # --- Constants ---
24
+ MAX_LORAS = 5
25
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
26
+ MAX_SEED = np.iinfo(np.int64).max
27
+ MAX_IMAGE_SIZE = 1216
28
+ SAMPLER_MAP = {
29
+ "Euler a": EulerAncestralDiscreteScheduler,
30
+ "Euler": EulerDiscreteScheduler,
31
+ "DPM++ 2M Karras": DPMSolverMultistepScheduler,
32
+ "DDIM": DDIMScheduler,
33
+ "UniPC": UniPCMultistepScheduler,
34
+ "Heun": HeunDiscreteScheduler,
35
+ "LMS": LMSDiscreteScheduler,
36
+ }
37
+ SCHEDULE_TYPE_MAP = ["Default", "Karras", "Uniform", "SGM Uniform"]
38
+ DEFAULT_SCHEDULE_TYPE = "Default"
39
+ DEFAULT_SAMPLER = "Euler a"
40
+ DEFAULT_NEGATIVE_PROMPT = "monochrome, (low quality, worst quality:1.2), 3d, watermark, signature, ugly, poorly drawn,"
41
+ DOWNLOAD_DIR = "/tmp/loras"
42
+ os.makedirs(DOWNLOAD_DIR, exist_ok=True)
43
+
44
+ # --- Model Lists ---
45
+ MODEL_LIST = [
46
+ "dhead/wai-nsfw-illustrious-sdxl-v140-sdxl",
47
+ "Laxhar/noobai-XL-Vpred-1.0",
48
+ "John6666/hassaku-xl-illustrious-v30-sdxl",
49
+ "RedRayz/hikari_noob_v-pred_1.2.2",
50
+ "bluepen5805/noob_v_pencil-XL",
51
+ "Laxhar/noobai-XL-1.1"
52
+ ]
53
+
54
+ # --- List of V-Prediction Models ---
55
+ V_PREDICTION_MODELS = [
56
+ "Laxhar/noobai-XL-Vpred-1.0",
57
+ "RedRayz/hikari_noob_v-pred_1.2.2",
58
+ "bluepen5805/noob_v_pencil-XL"
59
+ ]
60
+
61
+ # --- Dictionary for single-file models now stores the filename ---
62
+ SINGLE_FILE_MODELS = {
63
+ "bluepen5805/noob_v_pencil-XL": "noob_v_pencil-XL-v3.0.0.safetensors"
64
+ }
65
+
66
+ # --- Model Hash to Name Mapping ---
67
+ HASH_TO_MODEL_MAP = {
68
+ "bdb59bac77": "dhead/wai-nsfw-illustrious-sdxl-v140-sdxl",
69
+ "ea349eeae8": "Laxhar/noobai-XL-Vpred-1.0",
70
+ "b4fb5f829a": "John6666/hassaku-xl-illustrious-v30-sdxl",
71
+ "6681e8e4b1": "Laxhar/noobai-XL-1.1",
72
+ "90b7911a78": "bluepen5805/noob_v_pencil-XL",
73
+ "874170688a": "RedRayz/hikari_noob_v-pred_1.2.2"
74
+ }
75
+
76
+ def download_all_base_models_on_startup():
77
+ """Downloads all base models listed in MODEL_LIST when the app starts."""
78
+ print("--- Starting pre-download of all base models ---")
79
+ for model_name in MODEL_LIST:
80
+ try:
81
+ print(f"Downloading: {model_name}...")
82
+ start_time = time.time()
83
+ # Handle single-file models
84
+ if model_name in SINGLE_FILE_MODELS:
85
+ filename = SINGLE_FILE_MODELS[model_name]
86
+ hf_hub_download(repo_id=model_name, filename=filename)
87
+ # Handle standard diffusers models
88
+ else:
89
+ snapshot_download(repo_id=model_name, ignore_patterns=["*.onnx", "*.flax"])
90
+ end_time = time.time()
91
+ print(f"✅ Successfully downloaded {model_name} in {end_time - start_time:.2f} seconds.")
92
+ except Exception as e:
93
+ print(f"❌ Failed to download {model_name}: {e}")
94
+ finally:
95
+ # Clean up to conserve memory
96
+ gc.collect()
97
+ if torch.cuda.is_available():
98
+ torch.cuda.empty_cache()
99
+ print("--- Finished pre-downloading all base models ---")
100
+
101
+
102
+ def get_civitai_file_info(version_id):
103
+ """Gets the file metadata for a model version via the Civitai API."""
104
+ api_url = f"https://civitai.com/api/v1/model-versions/{version_id}"
105
+ try:
106
+ response = requests.get(api_url)
107
+ response.raise_for_status()
108
+ data = response.json()
109
+ for file_data in data.get('files', []):
110
+ if file_data['name'].endswith('.safetensors'):
111
+ return file_data
112
+ if data.get('files'):
113
+ return data['files'][0]
114
+ return None
115
+ except Exception as e:
116
+ print(f"Could not get file info from Civitai API: {e}")
117
+ return None
118
+
119
+ def download_file(url, save_path, api_key=None, progress=None, desc=""):
120
+ """Downloads a file, skipping if it already exists."""
121
+ if os.path.exists(save_path):
122
+ return f"File already exists: {os.path.basename(save_path)}"
123
+
124
+ headers = {}
125
+ if api_key and api_key.strip():
126
+ headers['Authorization'] = f'Bearer {api_key}'
127
+
128
+ try:
129
+ if progress: progress(0, desc=desc)
130
+ response = requests.get(url, stream=True, headers=headers)
131
+ response.raise_for_status()
132
+
133
+ total_size = int(response.headers.get('content-length', 0))
134
+
135
+ with open(save_path, "wb") as f:
136
+ downloaded = 0
137
+ for chunk in response.iter_content(chunk_size=8192):
138
+ f.write(chunk)
139
+ if progress and total_size > 0:
140
+ downloaded += len(chunk)
141
+ progress(downloaded / total_size, desc=desc)
142
+
143
+ return f"Successfully downloaded: {os.path.basename(save_path)}"
144
+ except Exception as e:
145
+ if os.path.exists(save_path): os.remove(save_path)
146
+ return f"Download failed for {os.path.basename(save_path)}: {e}"
147
+
148
+ def process_long_prompt(compel_proc, prompt, negative_prompt=""):
149
+ try:
150
+ conditioning, pooled = compel_proc([prompt, negative_prompt])
151
+ return conditioning, pooled
152
+ except Exception:
153
+ return None, None
154
+
155
+ def pre_download_loras(civitai_api_key, *lora_data, progress=gr.Progress(track_tqdm=True)):
156
+ civitai_ids = lora_data[0::2]
157
+ status_log = []
158
+
159
+ active_lora_ids = [cid for cid in civitai_ids if cid and cid.strip()]
160
+ if not active_lora_ids:
161
+ return "No LoRA IDs provided to download."
162
+
163
+ for i, civitai_id in enumerate(active_lora_ids):
164
+ version_id = civitai_id.strip()
165
+ progress(i / len(active_lora_ids), desc=f"Getting URL for LoRA ID: {version_id}")
166
+
167
+ local_lora_path = os.path.join(DOWNLOAD_DIR, f"civitai_{version_id}.safetensors")
168
+
169
+ file_info = get_civitai_file_info(version_id)
170
+ if not file_info:
171
+ status_log.append(f"* LoRA ID {version_id}: Could not get file info from Civitai.")
172
+ continue
173
+
174
+ download_url = file_info.get('downloadUrl')
175
+ if not download_url:
176
+ status_log.append(f"* LoRA ID {version_id}: Could not get download link.")
177
+ continue
178
+
179
+ status = download_file(
180
+ download_url,
181
+ local_lora_path,
182
+ api_key=civitai_api_key,
183
+ progress=progress,
184
+ desc=f"Downloading LoRA ID: {version_id}"
185
+ )
186
+ status_log.append(f"* LoRA ID {version_id}: {status}")
187
+
188
+ return "\n".join(status_log)
189
+
190
+ def _infer_logic(base_model_name, prompt, negative_prompt, seed, batch_size, width, height, guidance_scale, num_inference_steps,
191
+ sampler, schedule_type,
192
+ civitai_api_key,
193
+ *lora_data,
194
+ progress=gr.Progress(track_tqdm=True)):
195
+
196
+ pipe = None
197
+ try:
198
+ progress(0, desc=f"Loading model: {base_model_name}")
199
+
200
+ if base_model_name in SINGLE_FILE_MODELS:
201
+ filename = SINGLE_FILE_MODELS[base_model_name]
202
+ print(f"Loading single file: {filename} from repo: {base_model_name}")
203
+ local_path = hf_hub_download(repo_id=base_model_name, filename=filename)
204
+ pipe = StableDiffusionXLPipeline.from_single_file(
205
+ local_path,
206
+ torch_dtype=torch.float16,
207
+ use_safetensors=True
208
+ )
209
+ else:
210
+ print(f"Loading diffusers model: {base_model_name}")
211
+ pipe = StableDiffusionXLPipeline.from_pretrained(
212
+ base_model_name,
213
+ torch_dtype=torch.float16,
214
+ use_safetensors=True
215
+ )
216
+ pipe.to(device)
217
+
218
+ batch_size = int(batch_size)
219
+ seed = int(seed)
220
+
221
+ pipe.unload_lora_weights()
222
+
223
+ scheduler_class = SAMPLER_MAP.get(sampler, EulerAncestralDiscreteScheduler)
224
+ scheduler_config = pipe.scheduler.config
225
+
226
+ if base_model_name in V_PREDICTION_MODELS:
227
+ scheduler_config['prediction_type'] = 'v_prediction'
228
+ else:
229
+ scheduler_config['prediction_type'] = 'epsilon'
230
+
231
+ scheduler_kwargs = {}
232
+ if schedule_type == "Default" and sampler == "DPM++ 2M Karras":
233
+ scheduler_kwargs['use_karras_sigmas'] = True
234
+ elif schedule_type == "Karras":
235
+ scheduler_kwargs['use_karras_sigmas'] = True
236
+ elif schedule_type == "Uniform":
237
+ scheduler_kwargs['use_karras_sigmas'] = False
238
+ elif schedule_type == "SGM Uniform":
239
+ scheduler_kwargs['algorithm_type'] = 'sgm_uniform'
240
+
241
+ pipe.scheduler = scheduler_class.from_config(scheduler_config, **scheduler_kwargs)
242
+
243
+ compel_type = ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED
244
+ compel = Compel(tokenizer=[pipe.tokenizer, pipe.tokenizer_2], text_encoder=[pipe.text_encoder, pipe.text_encoder_2],
245
+ returned_embeddings_type=compel_type, requires_pooled=[False, True], truncate_long_prompts=False)
246
+
247
+ civitai_ids, lora_scales = lora_data[0::2], lora_data[1::2]
248
+ lora_params = list(zip(civitai_ids, lora_scales))
249
+ active_loras, active_lora_names_for_meta = [], []
250
+
251
+ for i, (civitai_id, lora_scale) in enumerate(lora_params):
252
+ if civitai_id and civitai_id.strip() and lora_scale > 0:
253
+ version_id = civitai_id.strip()
254
+ local_lora_path = os.path.join(DOWNLOAD_DIR, f"civitai_{version_id}.safetensors")
255
+
256
+ if not os.path.exists(local_lora_path):
257
+ file_info = get_civitai_file_info(version_id)
258
+ if not file_info:
259
+ print(f"Could not get file info for Civitai ID {version_id}, skipping.")
260
+ continue
261
+
262
+ download_url = file_info.get('downloadUrl')
263
+ if download_url:
264
+ download_file(download_url, local_lora_path, api_key=civitai_api_key, progress=progress, desc=f"Downloading LoRA ID {version_id}")
265
+ else:
266
+ print(f"Could not get download link for Civitai ID {version_id} during inference, skipping."); continue
267
+
268
+ if not os.path.exists(local_lora_path): print(f"LoRA file for ID {version_id} not found, skipping."); continue
269
+
270
+ adapter_name = f"lora_{i+1}"
271
+ progress((i * 0.1) + 0.05, desc=f"Loading LoRA (ID: {version_id})")
272
+ pipe.load_lora_weights(local_lora_path, adapter_name=adapter_name)
273
+ active_loras.append((adapter_name, lora_scale))
274
+ active_lora_names_for_meta.append(f"LoRA {i+1} (ID: {version_id}, Weight: {lora_scale})")
275
+
276
+ if active_loras:
277
+ adapter_names, adapter_weights = zip(*active_loras); pipe.set_adapters(list(adapter_names), list(adapter_weights))
278
+
279
+ conditioning, pooled = process_long_prompt(compel, prompt, negative_prompt)
280
+
281
+ pipe_args = {
282
+ "guidance_scale": guidance_scale,
283
+ "num_inference_steps": num_inference_steps,
284
+ "width": width,
285
+ "height": height,
286
+ }
287
+
288
+ output_images = []
289
+ loras_string = f"LoRAs: [{', '.join(active_lora_names_for_meta)}]" if active_lora_names_for_meta else ""
290
+
291
+ for i in range(batch_size):
292
+ progress(i / batch_size, desc=f"Generating image {i+1}/{batch_size}")
293
+
294
+ if i == 0 and seed != -1:
295
+ current_seed = seed
296
+ else:
297
+ current_seed = random.randint(0, MAX_SEED)
298
+
299
+ generator = torch.Generator(device=device).manual_seed(current_seed)
300
+ pipe_args["generator"] = generator
301
+
302
+ if conditioning is not None:
303
+ image = pipe(prompt_embeds=conditioning[0:1], pooled_prompt_embeds=pooled[0:1], negative_prompt_embeds=conditioning[1:2], negative_pooled_prompt_embeds=pooled[1:2], **pipe_args).images[0]
304
+ else:
305
+ image = pipe(prompt=prompt, negative_prompt=negative_prompt, **pipe_args).images[0]
306
+
307
+ params_string = f"{prompt}\nNegative prompt: {negative_prompt}\n"
308
+ params_string += f"Steps: {num_inference_steps}, Sampler: {sampler}, Schedule type: {schedule_type}, CFG scale: {guidance_scale}, Seed: {current_seed}, Size: {width}x{height}, Base Model: {base_model_name}, {loras_string}".strip()
309
+ image.info = {'parameters': params_string}
310
+ output_images.append(image)
311
+
312
+ return output_images
313
+
314
+ except Exception as e:
315
+ print(f"An error occurred during generation: {e}"); raise gr.Error(f"Generation failed: {e}")
316
+ finally:
317
+ if pipe is not None:
318
+ pipe.disable_lora()
319
+ del pipe
320
+ gc.collect()
321
+ if torch.cuda.is_available():
322
+ torch.cuda.empty_cache()
323
+
324
+ def infer(base_model_name, prompt, negative_prompt, seed, batch_size, width, height, guidance_scale, num_inference_steps,
325
+ sampler, schedule_type,
326
+ civitai_api_key,
327
+ zero_gpu_duration,
328
+ *lora_data,
329
+ progress=gr.Progress(track_tqdm=True)):
330
+
331
+ duration = 60
332
+ if zero_gpu_duration and int(zero_gpu_duration) > 0:
333
+ duration = int(zero_gpu_duration)
334
+
335
+ print(f"Using ZeroGPU duration: {duration} seconds")
336
+
337
+ decorated_infer_logic = spaces.GPU(duration=duration)(_infer_logic)
338
+
339
+ return decorated_infer_logic(
340
+ base_model_name, prompt, negative_prompt, seed, batch_size, width, height, guidance_scale, num_inference_steps,
341
+ sampler, schedule_type, civitai_api_key, *lora_data, progress=progress
342
+ )
343
+
344
+ def _parse_parameters(params_text):
345
+ data = {'lora_ids': [''] * MAX_LORAS, 'lora_scales': [0.0] * MAX_LORAS}
346
+ lines = params_text.strip().split('\n')
347
+ data['prompt'] = lines[0]
348
+ data['negative_prompt'] = lines[1].replace("Negative prompt:", "").strip() if len(lines) > 1 and lines[1].startswith("Negative prompt:") else ""
349
+ params_line = lines[2] if len(lines) > 2 else ""
350
+
351
+ def find_param(key, default, cast_type=str):
352
+ match = re.search(fr"\b{key}: ([^,]+?)(,|$)", params_line)
353
+ if match:
354
+ try:
355
+ return cast_type(match.group(1).strip())
356
+ except (ValueError, TypeError):
357
+ return default
358
+ return default
359
+
360
+ data['steps'] = find_param("Steps", 28, int)
361
+ data['sampler'] = find_param("Sampler", DEFAULT_SAMPLER)
362
+ data['schedule_type'] = find_param("Schedule type", DEFAULT_SCHEDULE_TYPE)
363
+ data['cfg_scale'] = find_param("CFG scale", 7.0, float)
364
+ data['seed'] = find_param("Seed", -1, int)
365
+ data['base_model'] = find_param("Base Model", MODEL_LIST[0])
366
+ data['model_hash'] = find_param("Model hash", None)
367
+
368
+ size_match = re.search(r"Size: (\d+)x(\d+)", params_line); data['width'], data['height'] = (int(size_match.group(1)), int(size_match.group(2))) if size_match else (1024, 1024)
369
+ if loras_match := re.search(r"LoRAs: \[(.+?)\]", params_line):
370
+ for i, (lora_id, lora_scale) in enumerate(re.findall(r"ID: (\d+), Weight: ([\d.]+)", loras_match.group(1))):
371
+ if i < MAX_LORAS: data['lora_ids'][i] = lora_id; data['lora_scales'][i] = float(lora_scale)
372
+ return data
373
+
374
+ def get_png_info(image):
375
+ if image is None: return "", "", "Please upload an image first."
376
+ params = image.info.get('parameters', None)
377
+ if not params: return "", "", "No metadata found in the image."
378
+ try:
379
+ parsed_data = _parse_parameters(params)
380
+ lines = params.strip().split('\n')
381
+ other_params_text = lines[2] if len(lines) > 2 else ""
382
+ other_params_display = "\n".join([p.strip() for p in other_params_text.split(',')])
383
+
384
+ return parsed_data.get('prompt', ''), parsed_data.get('negative_prompt', ''), other_params_display
385
+ except Exception as e:
386
+ return "", "", f"Error parsing metadata: {e}\n\nRaw metadata:\n{params}"
387
+
388
+ def send_info_to_txt2img(image):
389
+ if image is None or not (params := image.info.get('parameters', '')):
390
+ return [gr.update()] * (12 + MAX_LORAS * 2 + 1)
391
+
392
+ data = _parse_parameters(params)
393
+
394
+ model_from_hash = HASH_TO_MODEL_MAP.get(data.get('model_hash'))
395
+ final_base_model = model_from_hash if model_from_hash else data.get('base_model', MODEL_LIST[0])
396
+
397
+ sampler_from_png = data.get('sampler', DEFAULT_SAMPLER)
398
+ final_sampler = sampler_from_png if sampler_from_png in SAMPLER_MAP else DEFAULT_SAMPLER
399
+
400
+ schedule_from_png = data.get('schedule_type', DEFAULT_SCHEDULE_TYPE)
401
+ final_schedule_type = schedule_from_png if schedule_from_png in SCHEDULE_TYPE_MAP else DEFAULT_SCHEDULE_TYPE
402
+
403
+ updates = [final_base_model, data['prompt'], data['negative_prompt'], data['seed'], gr.update(), gr.update(), data['width'], data['height'],
404
+ data['cfg_scale'], data['steps'], final_sampler, final_schedule_type]
405
+
406
+ for i in range(MAX_LORAS): updates.extend([data['lora_ids'][i], data['lora_scales'][i]])
407
+ updates.append(gr.Tabs(selected=0))
408
+ return updates
409
+
410
+ # --- Execute model download on startup ---
411
+ download_all_base_models_on_startup()
412
+
413
+
414
+ with gr.Blocks(css="#col-container {margin: 0 auto; max-width: 1024px;}") as demo:
415
+ gr.Markdown("# Animated SDXL T2I with LoRAs")
416
+ with gr.Tabs(elem_id="tabs_container") as tabs:
417
+ with gr.TabItem("txt2img", id=0):
418
+ gr.Markdown("<div style='background-color: #282828; color: #a0aec0; padding: 10px; border-radius: 5px; margin-bottom: 15px;'>💡 <b>Tip:</b> Pre-downloading LoRAs before 'Run' can maximize ZeroGPU time.</div>")
419
+ with gr.Column(elem_id="col-container"):
420
+ with gr.Row():
421
+ with gr.Column(scale=3):
422
+ base_model_name = gr.Dropdown(label="Base Model", choices=MODEL_LIST, value="Laxhar/noobai-XL-Vpred-1.0")
423
+ with gr.Column(scale=1):
424
+ predownload_lora_button = gr.Button("Pre-download LoRAs")
425
+ run_button = gr.Button("Run", variant="primary")
426
+
427
+ predownload_status = gr.Markdown("")
428
+ prompt = gr.Text(label="Prompt", lines=3, placeholder="Enter your prompt")
429
+ negative_prompt = gr.Text(label="Negative prompt", lines=3, placeholder="Enter a negative prompt", value=DEFAULT_NEGATIVE_PROMPT)
430
+
431
+ # --- UI Layout ---
432
+ with gr.Row():
433
+ with gr.Column(scale=2):
434
+ with gr.Row():
435
+ width = gr.Slider(label="Width", minimum=256, maximum=MAX_IMAGE_SIZE, step=32, value=1024)
436
+ height = gr.Slider(label="Height", minimum=256, maximum=MAX_IMAGE_SIZE, step=32, value=1024)
437
+ with gr.Row():
438
+ sampler = gr.Dropdown(label="Sampling method", choices=list(SAMPLER_MAP.keys()), value=DEFAULT_SAMPLER)
439
+ schedule_type = gr.Dropdown(label="Schedule type", choices=SCHEDULE_TYPE_MAP, value=DEFAULT_SCHEDULE_TYPE)
440
+ with gr.Row():
441
+ guidance_scale = gr.Slider(label="CFG Scale", minimum=0.0, maximum=20.0, step=0.1, value=7)
442
+ num_inference_steps = gr.Slider(label="Sampling steps", minimum=1, maximum=50, step=1, value=28)
443
+
444
+ with gr.Column(scale=1):
445
+ result = gr.Gallery(label="Result", show_label=False, elem_id="result_gallery", columns=2, object_fit="contain", height="auto")
446
+
447
+ with gr.Row():
448
+ seed = gr.Number(label="Seed (-1 for random)", value=-1, precision=0)
449
+ batch_size = gr.Slider(label="Batch size", minimum=1, maximum=8, step=1, value=1)
450
+ zero_gpu_duration = gr.Number(
451
+ label="ZeroGPU Duration (s)",
452
+ value=None,
453
+ placeholder="Default: 60s",
454
+ info="Optional: Leave empty for default (60s), max to 120"
455
+ )
456
+
457
+ with gr.Accordion("LoRA Settings", open=False):
458
+ gr.Markdown("⚠️ **Responsible Use Notice:** Please avoid excessive, rapid, or automated (scripted) use of the pre-download LoRA feature. Overt misuse may lead to service disruption. Thank you for your cooperation.")
459
+ civitai_api_key = gr.Textbox(label="Optional Civitai API Key", info="Get from your Civitai account settings...", placeholder="Enter your Civitai API Key here", type="password", show_label=True)
460
+ gr.Markdown("Find the Model Version ID in the LoRA page URL (e.g., `modelVersionId=12345`) and fill it in below.")
461
+ lora_rows, lora_civitai_id_inputs, lora_scale_inputs = [], [], []
462
+ for i in range(MAX_LORAS):
463
+ with gr.Row(visible=(i == 0)) as row:
464
+ lora_civitai_id = gr.Textbox(label=f"LoRA {i+1} - Civitai Model Version ID", placeholder="e.g.: 1834914")
465
+ lora_scale = gr.Slider(label=f"Weight {i+1}", minimum=0.0, maximum=2.0, step=0.05, value=0.0)
466
+ lora_rows.append(row); lora_civitai_id_inputs.append(lora_civitai_id); lora_scale_inputs.append(lora_scale)
467
+ with gr.Row():
468
+ add_lora_button = gr.Button("✚ Add LoRA", variant="secondary")
469
+ lora_count_state = gr.State(value=1)
470
+ all_lora_inputs = [item for pair in zip(lora_civitai_id_inputs, lora_scale_inputs) for item in pair]
471
+
472
+ with gr.TabItem("PNG Info", id=1):
473
+ with gr.Column(elem_id="col-container"):
474
+ gr.Markdown("Upload a generated image to view its generation data.")
475
+ info_image_input = gr.Image(type="pil", label="Upload Image")
476
+ with gr.Row():
477
+ info_get_button = gr.Button("Get Info", variant="secondary")
478
+ send_to_txt2img_button = gr.Button("Send to txt2img", variant="primary")
479
+ gr.Markdown("### Positive Prompt"); info_prompt_output = gr.Textbox(lines=3, interactive=False, show_label=False)
480
+ gr.Markdown("### Negative Prompt"); info_neg_prompt_output = gr.Textbox(lines=3, interactive=False, show_label=False)
481
+ gr.Markdown("### Other Parameters"); info_params_output = gr.Textbox(lines=5, interactive=False, show_label=False)
482
+
483
+ gr.Markdown("<div style='text-align: center; margin-top: 20px;'>Made by <a href='https://civitai.com/user/RioShiina'>RioShiina</a> with ❤</div>")
484
+
485
+ def add_lora_row(current_count):
486
+ current_count = int(current_count)
487
+ if current_count < MAX_LORAS:
488
+ updates = {lora_count_state: current_count + 1, lora_rows[current_count]: gr.Row(visible=True)}
489
+ if current_count + 1 == MAX_LORAS: updates[add_lora_button] = gr.Button(visible=False)
490
+ return updates
491
+ return {lora_count_state: current_count}
492
+
493
+ add_lora_button.click(fn=add_lora_row, inputs=[lora_count_state], outputs=[lora_count_state, add_lora_button] + lora_rows)
494
+
495
+ predownload_lora_button.click(fn=pre_download_loras, inputs=[civitai_api_key, *all_lora_inputs], outputs=[predownload_status])
496
+
497
+ run_button.click(fn=infer,
498
+ inputs=[base_model_name, prompt, negative_prompt, seed, batch_size, width, height, guidance_scale, num_inference_steps, sampler, schedule_type, civitai_api_key, zero_gpu_duration, *all_lora_inputs],
499
+ outputs=[result])
500
+
501
+ info_get_button.click(fn=get_png_info, inputs=[info_image_input], outputs=[info_prompt_output, info_neg_prompt_output, info_params_output])
502
+
503
+ txt2img_outputs = [base_model_name, prompt, negative_prompt, seed, batch_size, zero_gpu_duration, width, height, guidance_scale, num_inference_steps, sampler, schedule_type, *all_lora_inputs, tabs]
504
+ send_to_txt2img_button.click(fn=send_info_to_txt2img, inputs=[info_image_input], outputs=txt2img_outputs)
505
+
506
+ demo.queue().launch()
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate
2
+ diffusers
3
+ invisible_watermark
4
+ torch
5
+ transformers
6
+ xformers
7
+ compel
8
+ pydantic==2.10.6
9
+ gradio==5.12.0
10
+ requests
11
+ peft