multimodalart HF Staff commited on
Commit
d95253f
·
verified ·
1 Parent(s): 0ec77e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +737 -576
app.py CHANGED
@@ -1,386 +1,475 @@
1
  import requests
2
  import os
3
  import gradio as gr
4
- from huggingface_hub import update_repo_visibility, upload_folder, create_repo, upload_file
5
  from slugify import slugify
 
6
  import re
7
  import uuid
8
- from typing import Optional, Dict, Any, List
9
  import json
10
- import shutil # For cleaning up local folders
11
- import traceback # For debugging
12
-
13
- TRUSTED_UPLOADERS = [
14
- "KappaNeuro", "CiroN2022", "multimodalart", "Norod78", "joachimsallstrom",
15
- "blink7630", "e-n-v-y", "DoctorDiffusion", "RalFinger", "artificialguybr"
16
- ]
17
-
18
- # --- Helper Functions (CivitAI API, Data Extraction, File Handling) ---
19
-
20
- def get_json_data(url: str) -> Optional[Dict[str, Any]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  url_split = url.split('/')
22
- if len(url_split) < 5 or not url_split[4].isdigit(): # Check if model ID is present and numeric
23
- print(f"Error: Invalid CivitAI URL format or missing model ID: {url}")
24
- # Try to extract model ID if it's just the ID
25
- if url.isdigit():
26
- model_id = url
27
- else:
28
- # Check if it's a slugified URL without /models/ part
29
- match = re.search(r'(\d+)(?:/[^/]+)?$', url)
30
- if match:
31
- model_id = match.group(1)
32
- else:
33
- return None
34
- else:
35
- model_id = url_split[4]
36
-
37
- api_url = f"https://civitai.com/api/v1/models/{model_id}"
38
  try:
39
- response = requests.get(api_url, timeout=15)
40
  response.raise_for_status()
41
  return response.json()
42
  except requests.exceptions.RequestException as e:
43
  print(f"Error fetching JSON data from {api_url}: {e}")
 
44
  return None
45
 
46
  def check_nsfw(json_data: Dict[str, Any], profile: Optional[gr.OAuthProfile]) -> bool:
 
 
 
 
47
  if json_data.get("nsfw", False):
48
- print(f"Model {json_data.get('id', 'Unknown')} flagged as NSFW at model level.")
49
- return False
50
-
 
 
 
 
 
 
 
 
 
 
51
  if profile and profile.username in TRUSTED_UPLOADERS:
52
- print(f"Trusted uploader {profile.username}, bypassing strict image NSFW check for model {json_data.get('id', 'Unknown')}.")
53
  return True
54
-
 
55
  for model_version in json_data.get("modelVersions", []):
56
- for image_media in model_version.get("images", []): # 'images' can contain videos
57
- if image_media.get("nsfwLevel", 0) > 5: # Allow 0-5 (None, Soft, Moderate, Mature, X)
58
- print(f"Model {json_data.get('id', 'Unknown')} version {model_version.get('id')} has media with nsfwLevel > 5.")
 
 
 
 
 
 
 
59
  return False
60
- return True
 
61
 
62
- def get_prompts_from_image(image_id: int) -> (str, str):
 
 
 
 
 
 
 
 
63
  url = f'https://civitai.com/api/trpc/image.getGenerationData?input={{"json":{{"id":{image_id}}}}}'
 
64
  prompt = ""
65
  negative_prompt = ""
66
  try:
67
- response = requests.get(url, timeout=10)
68
- if response.status_code == 200:
69
- data = response.json()
70
- result = data.get('result', {}).get('data', {}).get('json', {})
71
- if result and result.get('meta') is not None:
72
- prompt = result['meta'].get('prompt', "")
73
- negative_prompt = result['meta'].get('negativePrompt', "")
74
- # else:
75
- # print(f"Prompt fetch for {image_id}: Status {response.status_code}")
76
  except requests.exceptions.RequestException as e:
77
- print(f"Error fetching prompt data for image_id {image_id}: {e}")
 
 
 
78
  return prompt, negative_prompt
79
 
80
- def extract_info(json_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
81
  if json_data.get("type") != "LORA":
 
82
  return None
83
 
84
- model_mapping = {
85
- "SDXL 1.0": "stabilityai/stable-diffusion-xl-base-1.0", "SDXL 0.9": "stabilityai/stable-diffusion-xl-base-1.0",
86
- "SD 1.5": "runwayml/stable-diffusion-v1-5", "SD 1.4": "CompVis/stable-diffusion-v1-4",
87
- "SD 2.1": "stabilityai/stable-diffusion-2-1-base", "SD 2.0": "stabilityai/stable-diffusion-2-base",
88
- "SD 2.1 768": "stabilityai/stable-diffusion-2-1", "SD 2.0 768": "stabilityai/stable-diffusion-2",
89
- "SD 3": "stabilityai/stable-diffusion-3-medium-diffusers",
90
- "SD 3.5": "stabilityai/stable-diffusion-3-medium",
91
- "SD 3.5 Large": "stabilityai/stable-diffusion-3-medium",
92
- "SD 3.5 Medium": "stabilityai/stable-diffusion-3-medium",
93
- "SD 3.5 Large Turbo": "stabilityai/stable-diffusion-3-medium-turbo",
94
- "Flux.1 D": "black-forest-labs/FLUX.1-dev", "Flux.1 S": "black-forest-labs/FLUX.1-schnell",
95
- "LTXV": "Lightricks/LTX-Video-0.9.7-dev",
96
- "Hunyuan Video": "hunyuanvideo-community/HunyuanVideo",
97
- "Wan Video 1.3B t2v": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
98
- "Wan Video 14B t2v": "Wan-AI/Wan2.1-T2V-14B-Diffusers",
99
- "Wan Video 14B i2v 480p": "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
100
- "Wan Video 14B i2v 720p": "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers",
101
- "Pony": "SG161222/RealVisXL_V4.0",
102
- "Illustrious": "artificialguybr/LogoRedmond", # Example, could be "stabilityai/stable-diffusion-xl-base-1.0"
103
- }
104
-
105
  for model_version in json_data.get("modelVersions", []):
106
- civic_base_model_name = model_version.get("baseModel")
107
- if civic_base_model_name in model_mapping:
108
- base_model_hf_name = model_mapping[civic_base_model_name]
109
-
110
- urls_to_download: List[Dict[str, Any]] = []
111
- primary_file_found = False
112
- for file_data in model_version.get("files", []):
113
- if file_data.get("primary") and file_data.get("type") == "Model":
114
- urls_to_download.append({
115
- "url": file_data["downloadUrl"],
116
- "filename": os.path.basename(file_data["name"]),
117
- "type": "weightName", "is_video": False
118
- })
119
- primary_file_found = True
 
 
 
 
 
 
 
 
 
 
 
120
  break
121
 
122
- if not primary_file_found: continue
123
-
124
- for media_data in model_version.get("images", []):
125
- if media_data.get("nsfwLevel", 0) > 5: continue
126
-
127
- media_url_parts = media_data.get("url","").split("/") # Add default "" for url
128
- if not media_url_parts or not media_url_parts[-1]: continue # Ensure URL and filename part exist
129
-
130
- filename_part = media_url_parts[-1]
131
- id_candidate = filename_part.split(".")[0].split("?")[0]
 
 
 
 
 
 
 
 
 
132
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  prompt, negative_prompt = "", ""
134
- if media_data.get("hasMeta", False) and media_data.get("type") == "image":
135
- if id_candidate.isdigit():
136
- try:
137
- prompt, negative_prompt = get_prompts_from_image(int(id_candidate))
138
- except ValueError:
139
- print(f"Warning: Non-integer ID '{id_candidate}' for prompt fetching.")
140
- except Exception as e:
141
- print(f"Warning: Prompt fetch failed for ID {id_candidate}: {e}")
142
-
143
- is_video_file = media_data.get("type") == "video"
144
- media_type_key = "videoName" if is_video_file else "imageName"
145
 
146
  urls_to_download.append({
147
- "url": media_data["url"], "filename": os.path.basename(filename_part),
148
- "type": media_type_key, "prompt": prompt, "negative_prompt": negative_prompt,
149
- "is_video": is_video_file
 
 
 
150
  })
151
-
152
- allow_commercial_use_raw = json_data.get("allowCommercialUse", "Sell")
153
- if isinstance(allow_commercial_use_raw, list):
154
- allow_commercial_use_processed = allow_commercial_use_raw[0] if allow_commercial_use_raw else "Sell"
155
- elif isinstance(allow_commercial_use_raw, bool):
156
- allow_commercial_use_processed = "Sell" if allow_commercial_use_raw else "None"
157
- elif isinstance(allow_commercial_use_raw, str):
158
- allow_commercial_use_processed = allow_commercial_use_raw
159
- else: # Fallback for unexpected types
160
- allow_commercial_use_processed = "Sell"
161
-
162
-
163
- info_dict = {
164
- "urls_to_download": urls_to_download, "id": model_version.get("id"),
165
- "baseModel": base_model_hf_name, "modelId": model_version.get("modelId", json_data.get("id")),
166
- "name": json_data.get("name", "Untitled LoRA"),
167
- "description": json_data.get("description", "No description provided."),
168
  "trainedWords": model_version.get("trainedWords", []),
169
- "creator": json_data.get("creator", {}).get("username", "Unknown Creator"),
170
  "tags": json_data.get("tags", []),
171
  "allowNoCredit": json_data.get("allowNoCredit", True),
172
- "allowCommercialUse": allow_commercial_use_processed,
173
  "allowDerivatives": json_data.get("allowDerivatives", True),
174
  "allowDifferentLicense": json_data.get("allowDifferentLicense", True)
175
  }
176
- return info_dict
 
177
  return None
178
 
179
- def download_file_from_url(url: str, filename: str, folder: str = "."):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  headers = {}
181
- local_filepath = os.path.join(folder, filename)
 
 
 
 
 
182
  try:
183
- headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
184
- civitai_token = os.environ.get("CIVITAI_API_TOKEN")
185
- if civitai_token:
186
- headers['Authorization'] = f'Bearer {civitai_token}'
187
-
188
- response = requests.get(url, headers=headers, stream=True, timeout=120)
189
  response.raise_for_status()
 
 
 
 
 
 
 
 
190
 
191
- with open(local_filepath, 'wb') as f:
192
- for chunk in response.iter_content(chunk_size=8192):
193
- f.write(chunk)
194
-
195
- except requests.exceptions.HTTPError as e_http:
196
- if e_http.response.status_code in [401, 403] and not headers.get('Authorization') and not civitai_token:
197
- print(f"Authorization error (401/403) downloading {url}. Consider setting CIVITAI_API_TOKEN for restricted files.")
198
- raise gr.Error(f"HTTP Error downloading {filename}: {e_http.response.status_code} {e_http.response.reason}. URL: {url}")
199
- except requests.exceptions.RequestException as e_req:
200
- raise gr.Error(f"Request Error downloading {filename}: {e_req}. URL: {url}")
201
-
202
-
203
- def download_files(info: Dict[str, Any], folder: str = ".") -> Dict[str, List[Any]]:
204
- downloaded_media_items: List[Dict[str, Any]] = []
205
- downloaded_weights: List[str] = []
206
 
207
- for item in info["urls_to_download"]:
208
- filename_to_save_raw = item["filename"]
209
- filename_to_save = re.sub(r'[<>:"/\\|?*]', '_', filename_to_save_raw)
210
- if not filename_to_save:
211
- base, ext = os.path.splitext(item["url"])
212
- filename_to_save = f"downloaded_file_{uuid.uuid4().hex[:8]}{ext if ext else '.bin'}"
213
-
214
- gr.Info(f"Downloading {filename_to_save}...")
215
- download_file_from_url(item["url"], filename_to_save, folder)
216
-
217
- if item["type"] == "weightName":
218
- downloaded_weights.append(filename_to_save)
219
- elif item["type"] in ["imageName", "videoName"]:
220
- prompt_clean = re.sub(r'<.*?>', '', item.get("prompt", ""))
221
- negative_prompt_clean = re.sub(r'<.*?>', '', item.get("negative_prompt", ""))
222
- downloaded_media_items.append({
223
- "filename": filename_to_save, "prompt": prompt_clean,
224
- "negative_prompt": negative_prompt_clean, "is_video": item.get("is_video", False)
225
- })
226
-
227
- return {"media_items": downloaded_media_items, "weightName": downloaded_weights}
228
 
229
- def process_url(url: str, profile: Optional[gr.OAuthProfile], do_download: bool = True, folder: str = ".") -> (Optional[Dict[str, Any]], Optional[Dict[str, List[Any]]]):
230
  json_data = get_json_data(url)
231
  if json_data:
232
  if check_nsfw(json_data, profile):
233
- info = extract_info(json_data)
234
  if info:
235
- downloaded_files_dict = None
236
  if do_download:
237
- downloaded_files_dict = download_files(info, folder)
238
- return info, downloaded_files_dict
 
 
239
  else:
240
- model_type = json_data.get("type", "Unknown type")
241
- base_models_in_json = [mv.get("baseModel", "Unknown base") for mv in json_data.get("modelVersions", [])]
242
- error_message = f"This LoRA is not supported. Details:\n"
243
- error_message += f"- Model Type: {model_type} (expected LORA)\n"
244
- if base_models_in_json:
245
- error_message += f"- Detected Base Models in CivitAI: {', '.join(list(set(base_models_in_json)))}\n"
246
- error_message += "Ensure it's a LORA for a supported base (SD, SDXL, Pony, Flux, LTXV, Hunyuan, Wan) and has primary files."
247
- raise gr.Error(error_message)
248
  else:
249
- raise gr.Error("This model is flagged as NSFW by CivitAI or its media exceeds the allowed NSFW level (max level 5).")
 
250
  else:
251
- raise gr.Error("Could not fetch CivitAI API data. Check URL or model ID. Example: https://civitai.com/models/12345 or just 12345")
 
252
 
253
- # --- README Creation ---
254
- def create_readme(info: Dict[str, Any], downloaded_files: Dict[str, List[Any]], user_repo_id: str, link_civit: bool = False, is_author: bool = True, folder: str = "."):
255
- original_url = f"https://civitai.com/models/{info['modelId']}"
256
  link_civit_disclaimer = f'([CivitAI]({original_url}))'
257
  non_author_disclaimer = f'This model was originally uploaded on [CivitAI]({original_url}), by [{info["creator"]}](https://civitai.com/user/{info["creator"]}/models). The information below was provided by the author on CivitAI:'
258
 
259
- is_video_model = False
260
- video_base_models_hf = [
261
- "Lightricks/LTX-Video-0.9.7-dev", "hunyuanvideo-community/HunyuanVideo",
262
- "hunyuanvideo-community/HunyuanVideo-I2V", "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
263
- "Wan-AI/Wan2.1-T2V-14B-Diffusers", "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
264
- "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers"
265
- ]
266
- if info["baseModel"] in video_base_models_hf: is_video_model = True
267
- is_i2v_model = "i2v" in info["baseModel"].lower()
268
-
269
- default_tags = ["lora", "diffusers", "migrated"]
270
- if is_video_model:
271
- default_tags.append("video")
272
- default_tags.append("image-to-video" if is_i2v_model else "text-to-video")
273
- default_tags.append("template:video-lora")
274
  else:
275
- default_tags.extend(["text-to-image", "stable-diffusion", "template:sd-lora"])
 
 
 
276
 
277
  civit_tags_raw = info.get("tags", [])
278
- civit_tags_processed = []
279
- if isinstance(civit_tags_raw, list):
280
- civit_tags_processed = [str(t).replace(":", "").strip() for t in civit_tags_raw if str(t).replace(":", "").strip() and str(t).replace(":", "").strip() not in default_tags]
281
 
282
- tags = default_tags + civit_tags_processed
283
- unpacked_tags = "\n- ".join(sorted(list(set(tags))))
284
-
285
- trained_words = [word for word in info.get('trainedWords', []) if word]
286
- formatted_words = ', '.join(f'`{word}`' for word in trained_words)
287
  trigger_words_section = f"## Trigger words\nYou should use {formatted_words} to trigger the generation." if formatted_words else ""
288
 
289
  widget_content = ""
290
- media_items_for_widget = downloaded_files.get("media_items", [])
291
- if not media_items_for_widget:
292
- widget_content = "# No example media available for widget.\n"
293
- else:
294
- for media_item in media_items_for_widget[:5]:
295
- prompt_text = media_item["prompt"]
296
- negative_prompt_text = media_item["negative_prompt"]
297
- filename = media_item["filename"]
298
-
299
- escaped_prompt = prompt_text.replace("'", "''").replace("\n", " ")
300
-
301
- negative_prompt_cleaned_and_escaped = ""
302
- if negative_prompt_text:
303
- negative_prompt_cleaned_and_escaped = negative_prompt_text.replace("'", "''").replace("\n", " ") # Correct
304
-
305
- negative_prompt_widget_entry = ""
306
- if negative_prompt_cleaned_and_escaped: # Only add if non-empty
307
- negative_prompt_widget_entry = f"""parameters:
308
- negative_prompt: '{negative_prompt_cleaned_and_escaped}'"""
309
-
310
- widget_content += f"""- text: '{escaped_prompt if escaped_prompt else ' ' }'
311
- {negative_prompt_widget_entry}
312
- output:
313
  url: >-
314
- {filename}
315
  """
316
- flux_models_bf16 = ["black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-schnell"]
317
- dtype = "torch.bfloat16" if info["baseModel"] in flux_models_bf16 else "torch.float16"
318
-
319
- pipeline_import = "AutoPipelineForText2Image"
320
- example_prompt_for_pipeline = formatted_words if formatted_words else 'Your custom prompt'
321
- if media_items_for_widget and media_items_for_widget[0]["prompt"]:
322
- example_prompt_for_pipeline = media_items_for_widget[0]["prompt"]
323
 
324
- cleaned_example_pipeline_prompt = example_prompt_for_pipeline.replace("'", "\\'").replace("\n", " ")
325
- pipeline_call_example = f"image = pipeline('{cleaned_example_pipeline_prompt}').images[0]"
326
-
327
- if is_video_model:
328
- pipeline_import = "DiffusionPipeline"
329
- video_prompt_example = cleaned_example_pipeline_prompt
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- pipeline_call_example = f"# Example prompt for video generation\nprompt = \"{video_prompt_example}\"\n"
332
- pipeline_call_example += "# Adjust parameters like num_frames, num_inference_steps, height, width as needed for the specific pipeline.\n"
333
- pipeline_call_example += "# video_frames = pipeline(prompt, num_frames=16, guidance_scale=7.5, num_inference_steps=25).frames # Example parameters"
334
- if "LTX-Video" in info["baseModel"]:
335
- pipeline_call_example += "\n# LTX-Video uses a specific setup. Check its model card on Hugging Face."
336
- elif "HunyuanVideo" in info["baseModel"]:
337
- pipeline_call_example += "\n# HunyuanVideo often uses custom pipeline scripts or specific classes (e.g., HunyuanDiTPipeline). Check its HF model card."
338
- elif "Wan-AI" in info["baseModel"]:
339
- pipeline_call_example += "\n# Wan-AI models (e.g., WanVideoTextToVideoPipeline) require specific pipeline classes. Check model card for usage."
340
-
341
- weight_name = (downloaded_files["weightName"][0] if downloaded_files.get("weightName")
342
- else "your_lora_weights.safetensors")
343
-
344
- diffusers_code_block = f"""```py
345
- from diffusers import {pipeline_import}
 
346
  import torch
 
347
 
348
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
349
 
350
- # Note: The pipeline class '{pipeline_import}' is a general suggestion.
351
- # For specific video models (LTX, Hunyuan, Wan), you will likely need a dedicated pipeline class
352
- # (e.g., TextToVideoSDPipeline, HunyuanDiTPipeline, WanVideoTextToVideoPipeline, etc.).
353
- # Please refer to the documentation of the base model '{info["baseModel"]}' on Hugging Face for precise usage.
354
- pipeline = {pipeline_import}.from_pretrained('{info["baseModel"]}', torch_dtype={dtype})
355
- pipeline.to(device)
356
 
357
- # Load LoRA weights
358
- pipeline.load_lora_weights('{user_repo_id}', weight_name='{weight_name}')
 
 
 
 
 
 
 
 
 
 
359
 
360
- # For some pipelines, you might need to fuse LoRA layers before inference
361
- # and unfuse them after, or apply scaling. Check model card.
362
- # Example: pipeline.fuse_lora() or pipeline.set_adapters(["default"], adapter_weights=[0.8])
363
 
364
- # Example generation call (adjust parameters as needed for the specific pipeline)
365
- {pipeline_call_example}
 
 
 
366
 
367
- # If using fused LoRA:
368
- # pipeline.unfuse_lora()
369
- ```"""
370
-
371
- commercial_use_val = info["allowCommercialUse"]
 
 
 
 
 
 
 
 
372
 
 
 
 
 
 
 
 
 
 
 
 
373
  content = f"""---
374
- license: other
375
- license_name: bespoke-lora-trained-license
376
- license_link: https://multimodal.art/civitai-licenses?allowNoCredit={info["allowNoCredit"]}&allowCommercialUse={commercial_use_val}&allowDerivatives={info["allowDerivatives"]}&allowDifferentLicense={info["allowDifferentLicense"]}
377
  tags:
378
  - {unpacked_tags}
379
- base_model: {info["baseModel"]}
 
380
  instance_prompt: {trained_words[0] if trained_words else ''}
381
  widget:
382
- {widget_content}
383
- ---
384
 
385
  # {info["name"]}
386
 
@@ -390,363 +479,435 @@ widget:
390
  {link_civit_disclaimer if link_civit else ''}
391
 
392
  ## Model description
393
- {info["description"]}
394
 
395
  {trigger_words_section}
396
 
397
  ## Download model
398
  Weights for this model are available in Safetensors format.
399
- [Download](/{user_repo_id}/tree/main/{weight_name}) the LoRA in the Files & versions tab.
400
 
401
  ## Use it with the [🧨 diffusers library](https://github.com/huggingface/diffusers)
402
- {diffusers_code_block}
403
-
404
- For more details, including weighting, merging and fusing LoRAs, check the [documentation on loading LoRAs in diffusers](https://huggingface.co/docs/diffusers/main/en/using-diffusers/loading_adapters).
405
  """
 
406
  readme_path = os.path.join(folder, "README.md")
407
- with open(readme_path, "w", encoding="utf-8") as file:
408
- file.write(content)
 
409
 
410
 
411
- # --- Hugging Face Profile / Authorship ---
412
- def get_creator(username: str) -> Dict:
413
- if "COOKIE_INFO" not in os.environ or not os.environ["COOKIE_INFO"]:
414
- print("Warning: COOKIE_INFO env var not set. Cannot fetch CivitAI creator's HF username.")
415
- return {"result": {"data": {"json": {"links": []}}}}
416
-
 
 
 
417
  url = f"https://civitai.com/api/trpc/user.getCreator?input=%7B%22json%22%3A%7B%22username%22%3A%22{username}%22%2C%22authed%22%3Atrue%7D%7D"
418
  headers = {
419
- "authority": "civitai.com", "accept": "*/*", "accept-language": "en-US,en;q=0.9",
420
- "content-type": "application/json", "cookie": os.environ["COOKIE_INFO"],
 
 
 
421
  "referer": f"https://civitai.com/user/{username}/models",
422
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36"
 
 
 
 
 
 
423
  }
424
  try:
425
  response = requests.get(url, headers=headers, timeout=10)
426
  response.raise_for_status()
427
  return response.json()
428
- except requests.RequestException as e:
429
- print(f"Error fetching CivitAI creator data for {username}: {e}")
430
- return {"result": {"data": {"json": {"links": []}}}}
 
431
 
432
- def extract_huggingface_username(civitai_username: str) -> Optional[str]:
433
- data = get_creator(civitai_username)
434
- try:
435
- links = data.get('result', {}).get('data', {}).get('json', {}).get('links', [])
436
- if not isinstance(links, list): return None
437
- for link in links:
438
- if not isinstance(link, dict): continue
439
- url = link.get('url', '')
440
- if isinstance(url, str) and \
441
- (url.startswith('https://huggingface.co/') or url.startswith('https://www.huggingface.co/')):
442
- hf_username = url.split('/')[-1].split('?')[0].split('#')[0]
443
- if hf_username: return hf_username
444
- except Exception as e:
445
- print(f"Error parsing CivitAI creator data for HF username: {e}")
446
  return None
447
 
448
- # --- Gradio UI Logic Functions ---
449
 
450
- def check_civit_link(profile_state: Optional[gr.OAuthProfile], url_input: str):
451
- url_input = url_input.strip()
452
- if not url_input:
453
- return "", gr.update(interactive=False, visible=False), gr.update(visible=False), gr.update(visible=False)
 
 
 
454
 
455
- if not profile_state:
456
- return "Please log in with Hugging Face first.", gr.update(interactive=False, visible=False), gr.update(visible=False), gr.update(visible=False)
457
 
458
  try:
459
- info, _ = process_url(url_input, profile_state, do_download=False)
460
- if not info: # Should be caught by process_url, but as a safeguard
461
- return "Could not process this CivitAI URL. Model might be unsupported or invalid.", gr.update(interactive=False, visible=True), gr.update(visible=False), gr.update(visible=False)
462
- except gr.Error as e: # Catch errors from process_url (like NSFW, unsupported, API fetch failed)
463
- return str(e), gr.update(interactive=False, visible=True), gr.update(visible=False), gr.update(visible=False)
464
- except Exception as e: # Catch any other unexpected error during processing check
465
- print(f"Unexpected error in check_civit_link during process_url: {e}\n{traceback.format_exc()}")
466
- return f"An unexpected error occurred: {str(e)}", gr.update(interactive=False, visible=True), gr.update(visible=False), gr.update(visible=False)
467
-
468
- # If model is processable, then check authorship
469
- civitai_creator_username = info['creator']
470
- hf_username_on_civitai = extract_huggingface_username(civitai_creator_username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
- if profile_state.username in TRUSTED_UPLOADERS:
473
- return f'Welcome, trusted uploader {profile_state.username}! You can upload this model by "{civitai_creator_username}".', gr.update(interactive=True, visible=True), gr.update(visible=False), gr.update(visible=True)
 
474
 
475
  if not hf_username_on_civitai:
476
- no_username_text = (
477
- f'If you are "{civitai_creator_username}" on CivitAI, hi! Your CivitAI profile does not seem to have a Hugging Face username linked. '
478
- f'Please visit <a href="https://civitai.com/user/account" target="_blank">your CivitAI account settings</a> and add your 🤗 username ({profile_state.username}). '
479
- f'Example: <br/><img width="60%" src="https://i.imgur.com/hCbo9uL.png" alt="CivitAI profile settings example"/><br/>'
480
- f'(If you are not "{civitai_creator_username}", you cannot submit their model at this time.)'
481
- )
482
- return no_username_text, gr.update(interactive=False, visible=False), gr.update(visible=True), gr.update(visible=False)
483
-
484
- if profile_state.username.lower() != hf_username_on_civitai.lower():
485
- unmatched_username_text = (
486
- f'The Hugging Face username on "{civitai_creator_username}"\'s CivitAI profile ("{hf_username_on_civitai}") '
487
- f'does not match your logged-in Hugging Face account ("{profile_state.username}"). '
488
- f'Please update it on <a href="https://civitai.com/user/account" target="_blank">CivitAI</a> or log in to Hugging Face as "{hf_username_on_civitai}".<br/>'
489
- f'<img src="https://i.imgur.com/hCbo9uL.png" alt="CivitAI profile settings example"/>'
490
- )
491
- return unmatched_username_text, gr.update(interactive=False, visible=False), gr.update(visible=True), gr.update(visible=False)
492
 
493
- return f'Authorship verified for "{civitai_creator_username}" (🤗 {profile_state.username}). Ready to upload!', gr.update(interactive=True, visible=True), gr.update(visible=False), gr.update(visible=True)
 
 
494
 
495
- def handle_auth_change_and_update_state(profile: Optional[gr.OAuthProfile]):
496
- # This function now returns the profile to update the state
497
- if profile: # Logged in
498
- return profile, gr.update(visible=False), gr.update(visible=True), "", gr.update(value=""), gr.update(interactive=False, visible=False), gr.update(visible=False)
499
- else: # Logged out
500
- return None, gr.update(visible=True), gr.update(visible=False), "", gr.update(value=""), gr.update(interactive=False, visible=False), gr.update(visible=False)
501
 
502
- def show_output_area():
503
  return gr.update(visible=True)
504
 
505
- def list_civit_models(username: str) -> str:
506
- if not username.strip(): return ""
 
 
507
 
508
- url = f"https://civitai.com/api/v1/models?username={username}&limit=100&sort=Newest" # Max limit is 100 per page on CivitAI
509
- json_models_list = []
510
- page_count, max_pages = 0, 1 # Limit to 1 page (100 models) for now to be quicker, can be increased
511
 
512
- gr.Info(f"Fetching LoRAs for CivitAI user: {username}...")
513
  while url and page_count < max_pages:
514
  try:
515
- response = requests.get(url, timeout=15) # Increased timeout
516
  response.raise_for_status()
517
  data = response.json()
518
-
519
- current_items = data.get('items', [])
520
- json_models_list.extend(item for item in current_items if item.get("type") == "LORA" and item.get("name"))
521
-
522
- metadata = data.get('metadata', {})
523
- url = metadata.get('nextPage', None)
524
- page_count += 1
525
- except requests.RequestException as e:
526
- gr.Warning(f"Failed to fetch page {page_count + 1} for {username}: {e}")
527
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
 
529
- if not json_models_list:
530
- gr.Info(f"No suitable LoRA models found for {username} or failed to fetch.")
531
- return ""
532
-
533
- urls_text = "\n".join(
534
- f'https://civitai.com/models/{model["id"]}/{slugify(model["name"])}'
535
- for model in json_models_list
536
- )
537
- gr.Info(f"Found {len(json_models_list)} LoRA models for {username}.")
538
- return urls_text.strip()
539
-
540
- # --- Main Upload Functions ---
541
- def upload_civit_to_hf(profile: Optional[gr.OAuthProfile], oauth_token_obj: gr.OAuthToken, url: str, link_civit_checkbox_val: bool):
542
- if not profile or not profile.username:
543
- raise gr.Error("User profile not available. Please log in.")
544
- if not oauth_token_obj or not oauth_token_obj.token:
545
- raise gr.Error("Hugging Face token not available. Please log in again.")
546
-
547
- hf_auth_token = oauth_token_obj.token
548
-
549
- folder_uuid = str(uuid.uuid4())
550
- base_temp_dir = "temp_uploads"
551
- os.makedirs(base_temp_dir, exist_ok=True)
552
- folder_path = os.path.join(base_temp_dir, folder_uuid)
553
- os.makedirs(folder_path, exist_ok=True)
554
-
555
- gr.Info(f"Starting processing of model {url}")
556
 
 
557
  try:
558
- info, downloaded_data = process_url(url, profile, do_download=True, folder=folder_path)
559
- if not info or not downloaded_data:
560
- # process_url should raise gr.Error, but this is a fallback.
561
- raise gr.Error("Failed to process URL or download files after initial checks.")
562
-
563
- slug_name = slugify(info["name"])
564
- user_repo_id = f"{profile.username}/{slug_name}"
565
-
566
- is_author = False
567
- # Re-verify authorship just before upload, using info from processed model
568
- civitai_creator_username_from_model = info.get('creator', 'Unknown Creator')
569
- hf_username_on_civitai = extract_huggingface_username(civitai_creator_username_from_model)
570
-
571
- if profile.username in TRUSTED_UPLOADERS or \
572
- (hf_username_on_civitai and profile.username.lower() == hf_username_on_civitai.lower()):
 
 
 
 
 
 
573
  is_author = True
574
-
575
- create_readme(info, downloaded_data, user_repo_id, link_civit_checkbox_val, is_author=is_author, folder=folder_path)
576
-
577
- repo_url_huggingface = f"https://huggingface.co/{user_repo_id}"
578
 
579
- gr.Info(f"Creating/updating repository {user_repo_id} on Hugging Face...")
580
- create_repo(repo_id=user_repo_id, private=True, exist_ok=True, token=hf_auth_token)
 
 
 
 
 
 
 
581
 
582
- gr.Info(f"Starting upload to {repo_url_huggingface}...")
583
  upload_folder(
584
- folder_path=folder_path, repo_id=user_repo_id, repo_type="model",
585
- token=hf_auth_token, commit_message=f"Upload LoRA: {info['name']} from CivitAI ID {info['modelId']}"
 
 
 
586
  )
587
- update_repo_visibility(repo_id=user_repo_id, private=False, token=hf_auth_token)
588
- gr.Info(f"Model uploaded successfully!")
589
 
590
- return f'''# Model uploaded to 🤗!
591
- ## Access it here [{user_repo_id}]({repo_url_huggingface}) '''
592
-
593
  except Exception as e:
594
- print(f"Error during Hugging Face repo operations for {url}: {e}\n{traceback.format_exc()}")
595
- raise gr.Error(f"Upload failed for {url}: {str(e)}. Token might be expired. Try re-logging or check server logs.")
 
 
 
596
  finally:
597
- try:
598
- if os.path.exists(folder_path):
599
- shutil.rmtree(folder_path)
600
- except Exception as e_clean:
601
- print(f"Error cleaning up folder {folder_path}: {e_clean}")
602
-
603
-
604
- def bulk_upload(profile: Optional[gr.OAuthProfile], oauth_token_obj: gr.OAuthToken, urls_text: str, link_civit_checkbox_val: bool):
605
- if not profile or not oauth_token_obj or not oauth_token_obj.token:
606
- raise gr.Error("Authentication missing for bulk upload. Please log in.")
 
 
607
 
608
- urls = [url.strip() for url in urls_text.splitlines() if url.strip()]
609
- if not urls:
610
  return "No URLs provided for bulk upload."
611
 
612
- upload_results = []
613
- total_urls = len(urls)
614
- gr.Info(f"Starting bulk upload for {total_urls} models.")
 
 
 
 
615
 
616
  for i, url in enumerate(urls):
617
- gr.Info(f"Processing model {i+1}/{total_urls}: {url}")
618
  try:
619
- result_message = upload_civit_to_hf(profile, oauth_token_obj, url, link_civit_checkbox_val)
620
- upload_results.append(result_message)
621
- gr.Info(f"Successfully processed {url}")
622
- except gr.Error as ge: # Catch Gradio specific errors to display them
623
- gr.Warning(f"Skipping model {url} due to error: {str(ge)}")
624
- upload_results.append(f"Failed to upload {url}: {str(ge)}")
625
- except Exception as e: # Catch any other unhandled exception
626
- gr.Warning(f"Unhandled error uploading model {url}: {str(e)}")
627
- upload_results.append(f"Failed to upload {url}: Unhandled exception - {str(e)}")
628
- print(f"Unhandled exception during bulk upload for {url}: {e}\n{traceback.format_exc()}")
629
-
630
- return "\n\n---\n\n".join(upload_results) if upload_results else "No URLs were processed or all failed."
 
 
 
 
631
 
632
- # --- Gradio UI Definition ---
633
  css = '''
634
- #login_button_area { margin-bottom: 10px; }
635
- #disabled_upload_area { opacity: 0.6; pointer-events: none; }
636
- .gr-html ul { list-style-type: disc; margin-left: 20px; }
637
- .gr-html ol { list-style-type: decimal; margin-left: 20px; }
638
- .gr-html a { color: #007bff; text-decoration: underline; }
639
- .gr-html img { max-width: 100%; height: auto; margin-top: 5px; margin-bottom: 5px; border: 1px solid #ddd; }
640
- #instructions_area { padding: 10px; border: 1px solid #eee; border-radius: 5px; margin-top: 10px; background-color: #f9f9f9; }
 
641
  '''
642
 
643
- with gr.Blocks(css=css, title="CivitAI to Hugging Face LoRA Uploader") as demo:
644
- auth_profile_state = gr.State() # Stores the gr.OAuthProfile object
645
-
646
  gr.Markdown('''# Upload your CivitAI LoRA to Hugging Face 🤗
647
- By uploading your LoRAs to Hugging Face you get diffusers compatibility, a free GPU-based Inference Widget, you'll be listed in [LoRA Studio](https://lorastudio.co/models) after a short review, and get the possibility to submit your model to the [LoRA the Explorer](https://huggingface.co/spaces/multimodalart/LoraTheExplorer) ✨
648
- ''')
 
 
649
 
650
- with gr.Row(elem_id="login_button_area"):
651
- # LoginButton updates auth_profile_state via the .then() chain on demo.load
652
- login_button = gr.LoginButton()
653
-
654
- with gr.Column(visible=True, elem_id="disabled_upload_area") as disabled_area:
655
- gr.HTML("<h3>Please log in with Hugging Face to enable uploads.</h3>")
656
- gr.Textbox(
657
- placeholder="e.g., https://civitai.com/models/12345/my-lora or just 12345",
658
- label="CivitAI Model URL or ID (Log in to enable)",
659
- interactive=False
660
- )
661
 
 
662
  with gr.Column(visible=False) as enabled_area:
663
- gr.HTML("<h3 style='color:green;'>Logged in! You can now upload models.</h3>")
 
 
 
 
 
 
664
 
665
- with gr.Tabs():
666
- with gr.TabItem("Single Model Upload"):
667
- submit_source_civit_enabled = gr.Textbox(
668
- placeholder="e.g., https://civitai.com/models/12345/my-lora or just 12345",
669
- label="CivitAI Model URL or ID",
670
- info="Enter the full URL or just the numeric ID of the CivitAI LoRA model page.",
671
- )
672
- instructions_html = gr.HTML(elem_id="instructions_area") # For feedback
673
- try_again_button = gr.Button("I've updated my CivitAI profile (Re-check Authorship)", visible=False)
674
- link_civit_checkbox_single = gr.Checkbox(label="Add a link back to CivitAI in the README?", value=True, visible=True)
675
- submit_button_single_model = gr.Button("Upload This Model to Hugging Face", interactive=False, visible=False, variant="primary")
676
-
677
- with gr.TabItem("Bulk Upload"):
678
- civit_username_to_bulk = gr.Textbox(
679
- label="Your CivitAI Username (Optional)",
680
- info="Enter your CivitAI username to auto-populate the list below with your LoRAs (up to 100 newest)."
681
- )
682
- submit_bulk_civit_urls = gr.Textbox(
683
- label="CivitAI Model URLs or IDs (One per line)",
684
- info="Paste multiple CivitAI model page URLs or just IDs here, one on each line.",
685
- lines=8,
686
- )
687
- link_civit_checkbox_bulk = gr.Checkbox(label="Add a link back to CivitAI in READMEs?", value=True)
688
- bulk_upload_button = gr.Button("Start Bulk Upload", variant="primary")
689
 
690
- output_markdown_area = gr.Markdown(label="Upload Progress & Results", visible=False)
691
-
692
- # --- Event Handlers Wiring ---
693
- # This demo.load is triggered by login/logout from gr.LoginButton (which is a client-side component that calls this on auth change)
694
- # and also on initial page load (where profile will be None if not logged in via cookies).
695
- # The first input to demo.load for LoginButton is the profile.
696
- demo.load(
697
- fn=handle_auth_change_and_update_state,
698
- inputs=gr.Variable(), # This will receive the profile from LoginButton
699
- outputs=[auth_profile_state, disabled_area, enabled_area, instructions_html, submit_source_civit_enabled, submit_button_single_model, try_again_button],
700
- api_name=False, queue=False
701
- )
702
-
 
 
 
 
703
  submit_source_civit_enabled.change(
704
- fn=check_civit_link,
705
- inputs=[auth_profile_state, submit_source_civit_enabled],
706
- outputs=[instructions_html, submit_button_single_model, try_again_button, submit_button_single_model], # submit_button_single_model is repeated to control both interactivity and visibility
707
- api_name=False
 
708
  )
709
 
710
- try_again_button.click(
711
- fn=check_civit_link,
712
- inputs=[auth_profile_state, submit_source_civit_enabled],
713
- outputs=[instructions_html, submit_button_single_model, try_again_button, submit_button_single_model],
714
- api_name=False
715
  )
716
-
717
- civit_username_to_bulk.submit(
718
- fn=list_civit_models,
719
- inputs=[civit_username_to_bulk],
720
- outputs=[submit_bulk_civit_urls],
721
- api_name=False
722
- )
723
-
724
- submit_button_single_model.click(
725
- fn=show_output_area, inputs=[], outputs=[output_markdown_area], api_name=False
726
- ).then(
727
- fn=upload_civit_to_hf,
728
- inputs=[auth_profile_state, gr.OAuthToken(scopes=["write_repository","read_repository"]), submit_source_civit_enabled, link_civit_checkbox_single],
729
- outputs=[output_markdown_area],
730
- api_name="upload_single_model"
731
  )
732
-
733
- bulk_upload_button.click(
734
- fn=show_output_area, inputs=[], outputs=[output_markdown_area], api_name=False
735
- ).then(
736
- fn=bulk_upload,
737
- inputs=[auth_profile_state, gr.OAuthToken(scopes=["write_repository","read_repository"]), submit_bulk_civit_urls, link_civit_checkbox_bulk],
738
- outputs=[output_markdown_area],
739
- api_name="upload_bulk_models"
740
  )
741
 
742
- demo.queue(default_concurrency_limit=3, max_size=10)
743
- if __name__ == "__main__":
744
- # For local testing, you might need to set these environment variables:
745
- # os.environ["COOKIE_INFO"] = "your_civitai_session_cookie_here" # For creator verification
746
- # os.environ["CIVITAI_API_TOKEN"] = "your_civitai_api_key_here" # For potentially restricted downloads
747
- # os.environ["GRADIO_SERVER_NAME"] = "0.0.0.0" # To make it accessible on local network
748
-
749
- # To enable OAuth locally, you might need to set HF_HUB_DISABLE_OAUTH_CHECKMESSAGES="1"
750
- # and ensure your HF OAuth app is configured for http://localhost:7860 or http://127.0.0.1:7860
751
 
752
- demo.launch(debug=True, share=os.environ.get("GRADIO_SHARE") == "true")
 
 
 
 
 
1
  import requests
2
  import os
3
  import gradio as gr
4
+ from huggingface_hub import update_repo_visibility, whoami, upload_folder, create_repo, upload_file # Removed duplicate update_repo_visibility
5
  from slugify import slugify
6
+ # import gradio as gr # Already imported
7
  import re
8
  import uuid
9
+ from typing import Optional, Dict, Any
10
  import json
11
+ # from bs4 import BeautifulSoup # Not used
12
+
13
+ TRUSTED_UPLOADERS = ["KappaNeuro", "CiroN2022", "multimodalart", "Norod78", "joachimsallstrom", "blink7630", "e-n-v-y", "DoctorDiffusion", "RalFinger", "artificialguybr"]
14
+
15
+ # --- Model Mappings ---
16
+ MODEL_MAPPING_IMAGE = {
17
+ "SDXL 1.0": "stabilityai/stable-diffusion-xl-base-1.0",
18
+ "SDXL 0.9": "stabilityai/stable-diffusion-xl-base-1.0", # Usually mapped to 1.0
19
+ "SD 1.5": "runwayml/stable-diffusion-v1-5",
20
+ "SD 1.4": "CompVis/stable-diffusion-v1-4",
21
+ "SD 2.1": "stabilityai/stable-diffusion-2-1-base",
22
+ "SD 2.0": "stabilityai/stable-diffusion-2-base",
23
+ "SD 2.1 768": "stabilityai/stable-diffusion-2-1",
24
+ "SD 2.0 768": "stabilityai/stable-diffusion-2",
25
+ "SD 3": "stabilityai/stable-diffusion-3-medium-diffusers", # Assuming medium, adjust if others are common
26
+ "SD 3.5": "stabilityai/stable-diffusion-3.5-large", # Assuming large, adjust
27
+ "SD 3.5 Large": "stabilityai/stable-diffusion-3.5-large",
28
+ "SD 3.5 Medium": "stabilityai/stable-diffusion-3.5-medium",
29
+ "SD 3.5 Large Turbo": "stabilityai/stable-diffusion-3.5-large-turbo",
30
+ "Flux.1 D": "black-forest-labs/FLUX.1-dev",
31
+ "Flux.1 S": "black-forest-labs/FLUX.1-schnell",
32
+ "Pony": " ગુરુવર્ય/pony-diffusion-v6-xl", # Example, ensure this is a valid HF repo or use official if available
33
+ "Illustrious": " ગુરુવર્ય/illustrious_ai_art_generator_v1", # Example, ensure this is a valid HF repo
34
+ }
35
+
36
+ MODEL_MAPPING_VIDEO = {
37
+ "LTXV": "Lightricks/LTX-Video-0.9.7-dev",
38
+ # Hunyuan Video is handled specially based on user choice
39
+ "Wan Video 1.3B t2v": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
40
+ "Wan Video 14B t2v": "Wan-AI/Wan2.1-T2V-14B-Diffusers",
41
+ "Wan Video 14B i2v 480p": "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
42
+ "Wan Video 14B i2v 720p": "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers",
43
+ "Hunyuan Video": "hunyuanvideo-community/HunyuanVideo-I2V", # Default, will be overridden by choice
44
+ }
45
+
46
+ SUPPORTED_CIVITAI_BASE_MODELS = list(MODEL_MAPPING_IMAGE.keys()) + list(MODEL_MAPPING_VIDEO.keys())
47
+
48
+
49
+ def get_json_data(url):
50
  url_split = url.split('/')
51
+ if len(url_split) < 5 or not url_split[4].isdigit():
52
+ print(f"Invalid Civitai URL format or model ID not found: {url}")
53
+ gr.Warning(f"Invalid Civitai URL format. Ensure it's like 'https://civitai.com/models/YOUR_MODEL_ID/MODEL_NAME'. Problem with: {url}")
54
+ return None
55
+ api_url = f"https://civitai.com/api/v1/models/{url_split[4]}"
 
 
 
 
 
 
 
 
 
 
 
56
  try:
57
+ response = requests.get(api_url)
58
  response.raise_for_status()
59
  return response.json()
60
  except requests.exceptions.RequestException as e:
61
  print(f"Error fetching JSON data from {api_url}: {e}")
62
+ gr.Warning(f"Error fetching data from Civitai API for {url_split[4]}: {e}")
63
  return None
64
 
65
  def check_nsfw(json_data: Dict[str, Any], profile: Optional[gr.OAuthProfile]) -> bool:
66
+ if not json_data:
67
+ return False # Should not happen if get_json_data succeeded
68
+
69
+ # Overall model boolean flag - highest priority
70
  if json_data.get("nsfw", False):
71
+ print("Model flagged as NSFW by 'nsfw: true'.")
72
+ gr.Info("Reason: Model explicitly flagged as NSFW on Civitai.")
73
+ return False # Unsafe
74
+
75
+ # Overall model numeric nsfwLevel - second priority. Max allowed is 5 (nsfwLevel < 6).
76
+ # nsfwLevel definitions: None (1), Mild (2), Mature (4), Adult (5), X (8), R (16), XXX (32)
77
+ model_nsfw_level = json_data.get("nsfwLevel", 0)
78
+ if model_nsfw_level > 5: # Anything above "Adult"
79
+ print(f"Model's overall nsfwLevel ({model_nsfw_level}) is > 5. Blocking.")
80
+ gr.Info(f"Reason: Model's overall NSFW Level ({model_nsfw_level}) is above the allowed threshold (5).")
81
+ return False # Unsafe
82
+
83
+ # If uploader is trusted and the above checks passed, they bypass further version/image checks.
84
  if profile and profile.username in TRUSTED_UPLOADERS:
85
+ print(f"User {profile.username} is trusted. Model 'nsfw' is false and overall nsfwLevel ({model_nsfw_level}) is <= 5. Allowing.")
86
  return True
87
+
88
+ # For non-trusted users, check nsfwLevel of model versions and individual images/videos
89
  for model_version in json_data.get("modelVersions", []):
90
+ version_nsfw_level = model_version.get("nsfwLevel", 0)
91
+ if version_nsfw_level > 5:
92
+ print(f"Model version nsfwLevel ({version_nsfw_level}) is > 5 for non-trusted user. Blocking.")
93
+ gr.Info(f"Reason: A model version's NSFW Level ({version_nsfw_level}) is above 5.")
94
+ return False
95
+ for image_item in model_version.get("images", []):
96
+ item_nsfw_level = image_item.get("nsfwLevel", 0)
97
+ if item_nsfw_level > 5:
98
+ print(f"Media item nsfwLevel ({item_nsfw_level}) is > 5 for non-trusted user. Blocking.")
99
+ gr.Info(f"Reason: An example image/video's NSFW Level ({item_nsfw_level}) is above 5.")
100
  return False
101
+ return True # Safe for non-trusted user if all checks pass
102
+
103
 
104
+ def get_prompts_from_image(image_id_str: str):
105
+ # image_id_str could be non-numeric if URL parsing failed or format changed
106
+ try:
107
+ image_id = int(image_id_str)
108
+ except ValueError:
109
+ print(f"Invalid image_id_str for TRPC call: {image_id_str}. Skipping prompt fetch.")
110
+ return "", ""
111
+
112
+ print(f"Fetching prompts for image_id: {image_id}")
113
  url = f'https://civitai.com/api/trpc/image.getGenerationData?input={{"json":{{"id":{image_id}}}}}'
114
+
115
  prompt = ""
116
  negative_prompt = ""
117
  try:
118
+ response = requests.get(url, timeout=10) # Added timeout
119
+ response.raise_for_status() # Will raise an HTTPError if the HTTP request returned an unsuccessful status code
120
+ data = response.json()
121
+ # Expected structure: {'result': {'data': {'json': {'meta': {'prompt': '...', 'negativePrompt': '...'}}}}}
122
+ meta = data.get('result', {}).get('data', {}).get('json', {}).get('meta')
123
+ if meta: # meta can be None
124
+ prompt = meta.get('prompt', "")
125
+ negative_prompt = meta.get('negativePrompt', "")
 
126
  except requests.exceptions.RequestException as e:
127
+ print(f"Could not fetch/parse generation data for image_id {image_id}: {e}")
128
+ except json.JSONDecodeError as e:
129
+ print(f"JSONDecodeError for image_id {image_id}: {e}. Response content: {response.text[:200]}")
130
+
131
  return prompt, negative_prompt
132
 
133
+ def extract_info(json_data: Dict[str, Any], hunyuan_type: Optional[str] = None) -> Optional[Dict[str, Any]]:
134
  if json_data.get("type") != "LORA":
135
+ print("Model type is not LORA.")
136
  return None
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  for model_version in json_data.get("modelVersions", []):
139
+ civitai_base_model_name = model_version.get("baseModel")
140
+ if civitai_base_model_name in SUPPORTED_CIVITAI_BASE_MODELS:
141
+ base_model_hf = ""
142
+ is_video = False
143
+
144
+ if civitai_base_model_name == "Hunyuan Video":
145
+ is_video = True
146
+ if hunyuan_type == "Text-to-Video":
147
+ base_model_hf = "hunyuanvideo-community/HunyuanVideo"
148
+ else: # Default or "Image-to-Video"
149
+ base_model_hf = "hunyuanvideo-community/HunyuanVideo-I2V"
150
+ elif civitai_base_model_name in MODEL_MAPPING_VIDEO:
151
+ is_video = True
152
+ base_model_hf = MODEL_MAPPING_VIDEO[civitai_base_model_name]
153
+ elif civitai_base_model_name in MODEL_MAPPING_IMAGE:
154
+ base_model_hf = MODEL_MAPPING_IMAGE[civitai_base_model_name]
155
+ else:
156
+ # Should not happen if SUPPORTED_CIVITAI_BASE_MODELS is derived correctly
157
+ print(f"Logic error: {civitai_base_model_name} in supported list but not mapped.")
158
+ continue
159
+
160
+ primary_file_info = None
161
+ for file_entry in model_version.get("files", []):
162
+ if file_entry.get("primary", False) and file_entry.get("type") == "Model":
163
+ primary_file_info = file_entry
164
  break
165
 
166
+ if not primary_file_info:
167
+ # Sometimes primary might not be explicitly set, take first 'Model' type safetensors
168
+ for file_entry in model_version.get("files", []):
169
+ if file_entry.get("type") == "Model" and file_entry.get("name","").endswith(".safetensors"):
170
+ primary_file_info = file_entry
171
+ print(f"Using first safetensors file as primary: {primary_file_info['name']}")
172
+ break
173
+ if not primary_file_info:
174
+ print(f"No primary or suitable safetensors model file found for version {model_version.get('name')}")
175
+ continue
176
+
177
+
178
+ urls_to_download = [{"url": primary_file_info["downloadUrl"], "filename": primary_file_info["name"], "type": "weightName"}]
179
+
180
+ for image_obj in model_version.get("images", []):
181
+ # Skip if image/video itself is too NSFW for non-trusted, extract_info is called by check_civit_link too early for nsfw check
182
+ # The main nsfw check will handle this before download. Here we just gather info.
183
+ # if image_obj.get("nsfwLevel", 0) > 5: # This check belongs in check_nsfw for non-trusted.
184
+ # continue
185
 
186
+ image_url = image_obj.get("url")
187
+ if not image_url:
188
+ continue
189
+
190
+ # Extract image ID for fetching prompts. This can be fragile.
191
+ # Example URLs:
192
+ # https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/.../12345.jpeg (where 12345 is the id)
193
+ # https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/.../width=1024/12345.jpeg
194
+ # https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/.../12345.mp4
195
+ filename_part = os.path.basename(image_url) # e.g., 12345.jpeg or 12345.mp4
196
+ image_id_str = filename_part.split('.')[0]
197
+
198
  prompt, negative_prompt = "", ""
199
+ if image_obj.get("hasMeta", False) and image_obj.get("type") == "image": # Often only images have reliable meta via this endpoint
200
+ prompt, negative_prompt = get_prompts_from_image(image_id_str)
 
 
 
 
 
 
 
 
 
201
 
202
  urls_to_download.append({
203
+ "url": image_url,
204
+ "filename": filename_part, # Use the extracted filename part
205
+ "type": "imageName", # Keep as imageName for consistency with README generation
206
+ "prompt": prompt,
207
+ "negative_prompt": negative_prompt,
208
+ "media_type": image_obj.get("type", "image") # store if it's 'image' or 'video'
209
  })
210
+
211
+ info = {
212
+ "urls_to_download": urls_to_download,
213
+ "id": model_version["id"],
214
+ "baseModel": base_model_hf, # This is the HF model ID
215
+ "civitai_base_model_name": civitai_base_model_name, # Original name from Civitai
216
+ "is_video_model": is_video,
217
+ "modelId": json_data.get("id", ""), # Main model ID from Civitai
218
+ "name": json_data["name"],
219
+ "description": json_data.get("description", ""), # Description can be None
 
 
 
 
 
 
 
220
  "trainedWords": model_version.get("trainedWords", []),
221
+ "creator": json_data.get("creator", {}).get("username", "Unknown"),
222
  "tags": json_data.get("tags", []),
223
  "allowNoCredit": json_data.get("allowNoCredit", True),
224
+ "allowCommercialUse": json_data.get("allowCommercialUse", "Sell"), # Default to most permissive if missing
225
  "allowDerivatives": json_data.get("allowDerivatives", True),
226
  "allowDifferentLicense": json_data.get("allowDifferentLicense", True)
227
  }
228
+ return info
229
+ print("No suitable model version found with a supported base model.")
230
  return None
231
 
232
+ def download_files(info, folder="."):
233
+ downloaded_files = {
234
+ "imageName": [], # Will contain both image and video filenames
235
+ "imagePrompt": [],
236
+ "imageNegativePrompt": [],
237
+ "weightName": [],
238
+ "mediaType": [] # To distinguish image/video for gallery if needed later
239
+ }
240
+ for item in info["urls_to_download"]:
241
+ # Ensure filename is safe for filesystem
242
+ safe_filename = slugify(item["filename"].rsplit('.', 1)[0]) + '.' + item["filename"].rsplit('.', 1)[-1] if '.' in item["filename"] else slugify(item["filename"])
243
+
244
+ # Civitai URLs might need auth for direct download if not public
245
+ try:
246
+ download_file_with_auth(item["url"], safe_filename, folder) # Changed to use the auth-aware download
247
+ downloaded_files[item["type"]].append(safe_filename)
248
+ if item["type"] == "imageName": # This list now includes videos too
249
+ prompt_clean = re.sub(r'<.*?>', '', item.get("prompt", ""))
250
+ negative_prompt_clean = re.sub(r'<.*?>', '', item.get("negative_prompt", ""))
251
+ downloaded_files["imagePrompt"].append(prompt_clean)
252
+ downloaded_files["imageNegativePrompt"].append(negative_prompt_clean)
253
+ downloaded_files["mediaType"].append(item.get("media_type", "image"))
254
+ except gr.Error as e: # Catch Gradio errors from download_file_with_auth
255
+ print(f"Skipping file {safe_filename} due to download error: {e.message}")
256
+ gr.Warning(f"Skipping file {safe_filename} due to download error: {e.message}")
257
+
258
+ return downloaded_files
259
+
260
+ # Renamed original download_file to download_file_with_auth
261
+ def download_file_with_auth(url, filename, folder="."):
262
  headers = {}
263
+ # Add CIVITAI_API_TOKEN if available, for potentially restricted downloads
264
+ # Note: The prompt example didn't use it for image URLs, only for the model file via API.
265
+ # However, some image/video URLs might also require it if they are not fully public.
266
+ if "CIVITAI_API_TOKEN" in os.environ: # Changed from CIVITAI_API
267
+ headers['Authorization'] = f'Bearer {os.environ["CIVITAI_API_TOKEN"]}'
268
+
269
  try:
270
+ response = requests.get(url, headers=headers, stream=True, timeout=60) # Added stream and timeout
 
 
 
 
 
271
  response.raise_for_status()
272
+ except requests.exceptions.HTTPError as e:
273
+ print(f"HTTPError downloading {url}: {e}")
274
+ # No automatic retry with token here as it was specific to the primary file in original code
275
+ # If it was related to auth, the initial header should have helped.
276
+ raise gr.Error(f"Error downloading file {filename}: {e}")
277
+ except requests.exceptions.RequestException as e:
278
+ print(f"RequestException downloading {url}: {e}")
279
+ raise gr.Error(f"Error downloading file {filename}: {e}")
280
 
281
+ filepath = os.path.join(folder, filename)
282
+ with open(filepath, 'wb') as f:
283
+ for chunk in response.iter_content(chunk_size=8192):
284
+ f.write(chunk)
285
+ print(f"Successfully downloaded {filepath}")
 
 
 
 
 
 
 
 
 
 
286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ def process_url(url, profile, do_download=True, folder=".", hunyuan_type: Optional[str] = None):
289
  json_data = get_json_data(url)
290
  if json_data:
291
  if check_nsfw(json_data, profile):
292
+ info = extract_info(json_data, hunyuan_type=hunyuan_type)
293
  if info:
294
+ downloaded_files_summary = {}
295
  if do_download:
296
+ gr.Info(f"Downloading files for {info['name']}...")
297
+ downloaded_files_summary = download_files(info, folder)
298
+ gr.Info(f"Finished downloading files for {info['name']}.")
299
+ return info, downloaded_files_summary
300
  else:
301
+ raise gr.Error("LoRA extraction failed. The base model might not be supported, or it's not a LoRA model, or no suitable files found in the version.")
 
 
 
 
 
 
 
302
  else:
303
+ # check_nsfw now prints detailed reasons via gr.Info/print
304
+ raise gr.Error("This model has content tagged as unsafe by CivitAI or exceeds NSFW level limits.")
305
  else:
306
+ raise gr.Error("Failed to fetch model data from CivitAI API. Please check the URL and Civitai's status.")
307
+
308
 
309
+ def create_readme(info: Dict[str, Any], downloaded_files: Dict[str, Any], user_repo_id: str, link_civit: bool = False, is_author: bool = True, folder: str = "."):
310
+ readme_content = ""
311
+ original_url = f"https://civitai.com/models/{info['modelId']}" if info.get('modelId') else "CivitAI (ID not found)"
312
  link_civit_disclaimer = f'([CivitAI]({original_url}))'
313
  non_author_disclaimer = f'This model was originally uploaded on [CivitAI]({original_url}), by [{info["creator"]}](https://civitai.com/user/{info["creator"]}/models). The information below was provided by the author on CivitAI:'
314
 
315
+ # Tags
316
+ is_video = info.get("is_video_model", False)
317
+ base_hf_model = info["baseModel"]
318
+ civitai_bm_name_lower = info.get("civitai_base_model_name", "").lower()
319
+
320
+ if is_video:
321
+ default_tags = ["lora", "diffusers", "migrated", "video"]
322
+ if "template:" not in " ".join(info["tags"]): # if no template tag from civitai
323
+ default_tags.append("template:video-lora") # A generic video template tag
324
+ if "t2v" in civitai_bm_name_lower or (civitai_bm_name_lower == "hunyuan video" and base_hf_model.endswith("HunyuanVideo")):
325
+ default_tags.append("text-to-video")
326
+ elif "i2v" in civitai_bm_name_lower or (civitai_bm_name_lower == "hunyuan video" and base_hf_model.endswith("HunyuanVideo-I2V")):
327
+ default_tags.append("image-to-video")
 
 
328
  else:
329
+ default_tags = ["text-to-image", "stable-diffusion", "lora", "diffusers", "migrated"]
330
+ if "template:" not in " ".join(info["tags"]):
331
+ default_tags.append("template:sd-lora")
332
+
333
 
334
  civit_tags_raw = info.get("tags", [])
335
+ civit_tags_clean = [t.replace(":", "").strip() for t in civit_tags_raw if t.replace(":", "").strip()] # Clean and remove empty
336
+ # Filter out tags already covered by default_tags logic (e.g. 'text-to-image', 'lora')
337
+ final_civit_tags = [tag for tag in civit_tags_clean if tag not in default_tags and tag.lower() not in default_tags]
338
 
339
+ tags = default_tags + final_civit_tags
340
+ unpacked_tags = "\n- ".join(sorted(list(set(tags)))) # Sort and unique
341
+
342
+ trained_words = info.get('trainedWords', [])
343
+ formatted_words = ', '.join(f'`{word}`' for word in trained_words if word) # Filter out empty/None words
344
  trigger_words_section = f"## Trigger words\nYou should use {formatted_words} to trigger the generation." if formatted_words else ""
345
 
346
  widget_content = ""
347
+ # Limit number of widget items to avoid overly long READMEs, e.g., max 5
348
+ max_widget_items = 5
349
+ items_for_widget = list(zip(
350
+ downloaded_files.get("imagePrompt", []),
351
+ downloaded_files.get("imageNegativePrompt", []),
352
+ downloaded_files.get("imageName", [])
353
+ ))[:max_widget_items]
354
+
355
+ for index, (prompt, negative_prompt, media_filename) in enumerate(items_for_widget):
356
+ escaped_prompt = prompt.replace("'", "''") if prompt else ' ' # Handle None or empty prompt
357
+
358
+ # Ensure media_filename is just the filename, not a path
359
+ base_media_filename = os.path.basename(media_filename)
360
+
361
+ negative_prompt_content = f" negative_prompt: {negative_prompt}\n" if negative_prompt else ""
362
+ widget_content += f"""- text: '{escaped_prompt}'
363
+ {negative_prompt_content} output:
 
 
 
 
 
 
364
  url: >-
365
+ {base_media_filename}
366
  """
367
+ # Determine dtype
368
+ if base_hf_model in ["black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-schnell"]:
369
+ dtype = "torch.bfloat16"
370
+ else:
371
+ dtype = "torch.float16"
 
 
372
 
373
+ # Diffusers code snippet
374
+ main_prompt_for_snippet = formatted_words if formatted_words else 'Your custom prompt'
375
+ # If a specific prompt exists for the first image/video, use that one
376
+ if items_for_widget and items_for_widget[0][0]: # items_for_widget[0][0] is the prompt of the first media
377
+ main_prompt_for_snippet = items_for_widget[0][0]
378
+
379
+ if is_video:
380
+ # Determine if T2V or I2V for example snippet based on HF model name or Civitai name
381
+ pipeline_class = "AutoPipelineForTextToVideo" # Default for T2V
382
+ example_input = f"'{main_prompt_for_snippet}'"
383
+ output_name = "video_frames"
384
+ output_access = ".frames"
385
+
386
+ if "I2V" in base_hf_model or "i2v" in civitai_bm_name_lower:
387
+ pipeline_class = "AutoPipelineForVideoToVideo" # Or ImageToVideo if more specific class exists
388
+ example_input = f"prompt='{main_prompt_for_snippet}', image=your_input_image_or_pil" # I2V needs an image
389
+ # For I2V, .frames might still be correct but input changes.
390
 
391
+ # Handle Hunyuan specifically for more accurate snippet if possible
392
+ if "HunyuanVideo" in base_hf_model:
393
+ if base_hf_model.endswith("HunyuanVideo"): # T2V
394
+ pipeline_class = "HunyuanDiT2V Pipeline" # from hunyuanvideo_community.pipelines.hunyuan_dit_t2v_pipeline import HunyuanDiT2V Pipeline
395
+ example_input = f"prompt='{main_prompt_for_snippet}', height=576, width=1024, num_frames=16, num_inference_steps=50, guidance_scale=7.5" # Example params
396
+ else: # I2V
397
+ pipeline_class = "HunyuanDiI2V Pipeline" # from hunyuanvideo_community.pipelines.hunyuan_dit_i2v_pipeline import HunyuanDiI2V Pipeline
398
+ example_input = f"pil_image, prompt='{main_prompt_for_snippet}', height=576, width=1024, num_frames=16, num_inference_steps=50, guidance_scale=7.5, strength=0.8" # Example params
399
+
400
+
401
+ diffusers_example = f"""
402
+ ```py
403
+ # This is a video LoRA. Diffusers usage for video models can vary.
404
+ # You may need to install/import specific pipeline classes.
405
+ # Example for a {pipeline_class.split()[0]} based workflow:
406
+ from diffusers import {pipeline_class.split()[0]} # Adjust if pipeline_class includes more than just class name
407
  import torch
408
+ # For Hunyuan, you might need: from hunyuanvideo_community.pipelines import {pipeline_class}
409
 
410
  device = "cuda" if torch.cuda.is_available() else "cpu"
411
+ # pil_image = ... # Load your input image PIL here if it's an Image-to-Video model
412
 
413
+ pipeline = {pipeline_class.split()[0]}.from_pretrained('{base_hf_model}', torch_dtype={dtype}).to(device)
414
+ pipeline.load_lora_weights('{user_repo_id}', weight_name='{downloaded_files["weightName"][0]}')
 
 
 
 
415
 
416
+ # The following generation command is an example and may need adjustments
417
+ # based on the specific pipeline and its required parameters.
418
+ # {output_name} = pipeline({example_input}){output_access}
419
+ # For more details, consult the Hugging Face Hub page for {base_hf_model}
420
+ # and the Diffusers documentation on LoRAs and video pipelines.
421
+ ```
422
+ """
423
+ else: # Image model
424
+ diffusers_example = f"""
425
+ ```py
426
+ from diffusers import AutoPipelineForText2Image
427
+ import torch
428
 
429
+ device = "cuda" if torch.cuda.is_available() else "cpu"
 
 
430
 
431
+ pipeline = AutoPipelineForText2Image.from_pretrained('{base_hf_model}', torch_dtype={dtype}).to(device)
432
+ pipeline.load_lora_weights('{user_repo_id}', weight_name='{downloaded_files["weightName"][0]}')
433
+ image = pipeline('{main_prompt_for_snippet}').images[0]
434
+ ```
435
+ """
436
 
437
+ license_map_simple = {
438
+ "Public Domain": "public-domain",
439
+ "CreativeML Open RAIL-M": "creativeml-openrail-m",
440
+ "CreativeML Open RAIL++-M": "creativeml-openrail-m", # Assuming mapping to openrail-m
441
+ "openrail": "creativeml-openrail-m",
442
+ "SDXL": "sdxl", # This might be a base model, not a license
443
+ # Add more mappings if CivitAI provides other common license names
444
+ }
445
+ # Attempt to map commercial use if possible, otherwise use bespoke
446
+ # "allowCommercialUse": ["Image", "RentCivit", "Rent", "Sell"] or "None", "Sell" etc.
447
+ commercial_use = info.get("allowCommercialUse", "None") # Default to None if not specified
448
+ license_identifier = "other"
449
+ license_name = "bespoke-lora-trained-license" # Default bespoke license
450
 
451
+ # Heuristic for common licenses based on permissions
452
+ if isinstance(commercial_use, str) and commercial_use.lower() == "none" and not info.get("allowDerivatives", True):
453
+ license_identifier = "creativeml-openrail-m" # Or a more restrictive one if known
454
+ license_name = "CreativeML OpenRAIL-M"
455
+ elif isinstance(commercial_use, list) and "Sell" in commercial_use and info.get("allowDerivatives", True):
456
+ # This is a very permissive license, could be Apache 2.0 or MIT if source code, but for models, 'other' is safer
457
+ pass # Keep bespoke for now
458
+
459
+ bespoke_license_link = f"https://multimodal.art/civitai-licenses?allowNoCredit={info['allowNoCredit']}&allowCommercialUse={commercial_use[0] if isinstance(commercial_use, list) and commercial_use else (commercial_use if isinstance(commercial_use, str) else 'None')}&allowDerivatives={info['allowDerivatives']}&allowDifferentLicense={info['allowDifferentLicense']}"
460
+
461
+
462
  content = f"""---
463
+ license: {license_identifier}
464
+ license_name: "{license_name}"
465
+ license_link: {bespoke_license_link}
466
  tags:
467
  - {unpacked_tags}
468
+
469
+ base_model: {base_hf_model}
470
  instance_prompt: {trained_words[0] if trained_words else ''}
471
  widget:
472
+ {widget_content}---
 
473
 
474
  # {info["name"]}
475
 
 
479
  {link_civit_disclaimer if link_civit else ''}
480
 
481
  ## Model description
482
+ {info["description"] if info["description"] else "No description provided."}
483
 
484
  {trigger_words_section}
485
 
486
  ## Download model
487
  Weights for this model are available in Safetensors format.
488
+ [Download](/{user_repo_id}/tree/main) them in the Files & versions tab.
489
 
490
  ## Use it with the [🧨 diffusers library](https://github.com/huggingface/diffusers)
491
+ {diffusers_example}
492
+ For more details, including weighting, merging and fusing LoRAs, check the [documentation on loading LoRAs in diffusers](https://huggingface.co/docs/diffusers/main/en/using-diffusers/loading_adapters)
 
493
  """
494
+ readme_content += content + "\n"
495
  readme_path = os.path.join(folder, "README.md")
496
+ with open(readme_path, "w", encoding="utf-8") as file: # Added encoding
497
+ file.write(readme_content)
498
+ print(f"README.md created at {readme_path}")
499
 
500
 
501
+ def get_creator(username):
502
+ # Ensure CIVITAI_COOKIE_INFO is set as an environment variable
503
+ # Example: "__Host-next-auth.csrf-token=xxx; __Secure-next-auth.callback-url=yyy; __Secure-next-auth.session-token=zzz"
504
+ cookie_info = os.environ.get("CIVITAI_COOKIE_INFO")
505
+ if not cookie_info:
506
+ print("CIVITAI_COOKIE_INFO environment variable not set. Cannot fetch creator's HF username.")
507
+ gr.Warning("CIVITAI_COOKIE_INFO not set. Cannot verify Hugging Face username on Civitai.")
508
+ return None # Cannot proceed without cookie for this specific call
509
+
510
  url = f"https://civitai.com/api/trpc/user.getCreator?input=%7B%22json%22%3A%7B%22username%22%3A%22{username}%22%2C%22authed%22%3Atrue%7D%7D"
511
  headers = {
512
+ "authority": "civitai.com",
513
+ "accept": "*/*",
514
+ "accept-language": "en-US,en;q=0.9", # Simplified
515
+ "content-type": "application/json",
516
+ "cookie": cookie_info, # Use the env var
517
  "referer": f"https://civitai.com/user/{username}/models",
518
+ "sec-ch-ua": "\"Chromium\";v=\"118\", \"Not_A Brand\";v=\"99\"", # Example, update if needed
519
+ "sec-ch-ua-mobile": "?0",
520
+ "sec-ch-ua-platform": "\"Windows\"", # Example
521
+ "sec-fetch-dest": "empty",
522
+ "sec-fetch-mode": "cors",
523
+ "sec-fetch-site": "same-origin",
524
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" # Example
525
  }
526
  try:
527
  response = requests.get(url, headers=headers, timeout=10)
528
  response.raise_for_status()
529
  return response.json()
530
+ except requests.exceptions.RequestException as e:
531
+ print(f"Error fetching creator data for {username}: {e}")
532
+ gr.Warning(f"Could not verify Civitai creator's HF link: {e}")
533
+ return None
534
 
535
+
536
+ def extract_huggingface_username(username_civitai):
537
+ data = get_creator(username_civitai)
538
+ if not data:
539
+ return None
540
+
541
+ links = data.get('result', {}).get('data', {}).get('json', {}).get('links', [])
542
+ for link in links:
543
+ url = link.get('url', '')
544
+ if 'huggingface.co/' in url:
545
+ # Extract username, handling potential variations like www. or trailing slashes
546
+ hf_username = url.split('huggingface.co/')[-1].split('/')[0]
547
+ if hf_username:
548
+ return hf_username
549
  return None
550
 
 
551
 
552
+ def check_civit_link(profile: Optional[gr.OAuthProfile], url: str):
553
+ # Initial return structure: instructions_html, submit_interactive, try_again_visible, other_submit_visible, hunyuan_radio_visible
554
+ # Default to disabling/hiding things if checks fail early
555
+ default_fail_updates = ("", gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False))
556
+
557
+ if not profile: # Should be handled by demo.load and login button
558
+ return "Please log in with Hugging Face.", gr.update(interactive=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
559
 
560
+ if not url or not url.startswith("https://civitai.com/models/"):
561
+ return "Please enter a valid Civitai model URL.", gr.update(interactive=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
562
 
563
  try:
564
+ # We need hunyuan_type for extract_info, but we don't know it yet.
565
+ # Call get_json_data first to check if it's Hunyuan.
566
+ json_data_preview = get_json_data(url)
567
+ if not json_data_preview:
568
+ return ("Failed to fetch basic model info from Civitai. Check URL.",
569
+ gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False))
570
+
571
+ is_hunyuan = False
572
+ original_civitai_base_model = ""
573
+ if json_data_preview.get("type") == "LORA":
574
+ for mv in json_data_preview.get("modelVersions", []):
575
+ # Try to find a relevant model version to check its base model
576
+ # This is a simplified check; extract_info does a more thorough search
577
+ cbm = mv.get("baseModel")
578
+ if cbm and cbm in SUPPORTED_CIVITAI_BASE_MODELS:
579
+ original_civitai_base_model = cbm
580
+ if cbm == "Hunyuan Video":
581
+ is_hunyuan = True
582
+ break
583
+
584
+ # Now call process_url with a default hunyuan_type for other checks
585
+ # The actual hunyuan_type choice will be used during the main upload.
586
+ info, _ = process_url(url, profile, do_download=False, hunyuan_type="Image-to-Video") # Use default for check
587
+
588
+ # If process_url raises an error (e.g. NSFW, not supported), it will be caught by Gradio
589
+ # and displayed as a gr.Error. Here, we assume it passed if no exception.
590
+
591
+ except gr.Error as e: # Catch errors from process_url (like NSFW, not supported)
592
+ return (f"Cannot process this model: {e.message}",
593
+ gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=is_hunyuan)) # Show hunyuan if detected
594
+ except Exception as e: # Catch any other unexpected error during preview
595
+ print(f"Unexpected error in check_civit_link: {e}")
596
+ return (f"An unexpected error occurred: {str(e)}",
597
+ gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=is_hunyuan))
598
+
599
+
600
+ hf_username_on_civitai = extract_huggingface_username(info['creator'])
601
 
602
+ if profile.username == "multimodalart" or profile.username in TRUSTED_UPLOADERS: # Allow multimodalart or other trusted to bypass HF username check
603
+ return ('Admin/Trusted user override: Upload enabled.',
604
+ gr.update(interactive=True), gr.update(visible=False), gr.update(visible=True), gr.update(visible=is_hunyuan))
605
 
606
  if not hf_username_on_civitai:
607
+ no_username_text = (f'If you are {info["creator"]} on Civitai, hi! Your CivitAI profile does not seem to have a link to your Hugging Face account. '
608
+ f'Please visit <a href="https://civitai.com/user/account" target="_blank">https://civitai.com/user/account</a>, '
609
+ f'go to "Edit profile" and add your Hugging Face profile URL (e.g., https://huggingface.co/{profile.username}) to the "Links" section. '
610
+ f'<br><img width="60%" src="https://i.imgur.com/hCbo9uL.png" alt="Civitai profile links example"/><br>'
611
+ f'(If you are not {info["creator"]}, you cannot submit their model at this time unless you are a trusted uploader.)')
612
+ return no_username_text, gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=is_hunyuan)
613
+
614
+ if profile.username.lower() != hf_username_on_civitai.lower():
615
+ unmatched_username_text = (f'Oops! The Hugging Face username found on the CivitAI profile of {info["creator"]} is '
616
+ f'"{hf_username_on_civitai}", but you are logged in as "{profile.username}". '
617
+ f'Please ensure your CivitAI profile links to the correct Hugging Face account: '
618
+ f'<a href="https://civitai.com/user/account" target="_blank">https://civitai.com/user/account</a> (Edit profile -> Links section).'
619
+ f'<br><img width="60%" src="https://i.imgur.com/hCbo9uL.png" alt="Civitai profile links example"/>')
620
+ return unmatched_username_text, gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=is_hunyuan)
 
 
621
 
622
+ # All checks passed
623
+ return ('Username verified! You can now upload this model.',
624
+ gr.update(interactive=True), gr.update(visible=False), gr.update(visible=True), gr.update(visible=is_hunyuan))
625
 
626
+
627
+ def swap_fill(profile: Optional[gr.OAuthProfile]):
628
+ if profile is None: # Not logged in
629
+ return gr.update(visible=True), gr.update(visible=False)
630
+ else: # Logged in
631
+ return gr.update(visible=False), gr.update(visible=True)
632
 
633
+ def show_output():
634
  return gr.update(visible=True)
635
 
636
+ def list_civit_models(username_civitai: str):
637
+ if not username_civitai:
638
+ return ""
639
+ url = f"https://civitai.com/api/v1/models?username={username_civitai}&limit=100&sort=Newest" # Added sort
640
 
641
+ all_model_urls = ""
642
+ page_count = 0
643
+ max_pages = 5 # Limit number of pages to fetch to avoid very long requests
644
 
 
645
  while url and page_count < max_pages:
646
  try:
647
+ response = requests.get(url, timeout=10)
648
  response.raise_for_status()
649
  data = response.json()
650
+ except requests.exceptions.RequestException as e:
651
+ print(f"Error fetching model list for {username_civitai}: {e}")
652
+ gr.Warning(f"Could not fetch full model list for {username_civitai}.")
 
 
 
 
 
 
653
  break
654
+
655
+ items = data.get('items', [])
656
+ if not items:
657
+ break
658
+
659
+ for model in items:
660
+ # Only list LORAs of supported base model types to avoid cluttering with unsupported ones
661
+ is_supported_lora = False
662
+ if model.get("type") == "LORA":
663
+ # Check modelVersions for baseModel compatibility
664
+ for mv in model.get("modelVersions", []):
665
+ if mv.get("baseModel") in SUPPORTED_CIVITAI_BASE_MODELS:
666
+ is_supported_lora = True
667
+ break
668
+ if is_supported_lora:
669
+ model_slug = slugify(model.get("name", f"model-{model['id']}"))
670
+ all_model_urls += f'https://civitai.com/models/{model["id"]}/{model_slug}\n'
671
+
672
+ metadata = data.get('metadata', {})
673
+ url = metadata.get('nextPage', None)
674
+ page_count += 1
675
+ if page_count >= max_pages and url:
676
+ print(f"Reached max page limit for fetching models for {username_civitai}.")
677
+ gr.Info(f"Showing first {max_pages*100} models. There might be more.")
678
+
679
+ if not all_model_urls:
680
+ gr.Info(f"No compatible LoRA models found for user {username_civitai} or user not found.")
681
+ return all_model_urls.strip()
682
+
683
+
684
+ def upload_civit_to_hf(profile: Optional[gr.OAuthProfile], oauth_token: Optional[gr.OAuthToken], url: str, link_civit: bool, hunyuan_type: str):
685
+ if not profile or not profile.username: # Check profile and username
686
+ raise gr.Error("You must be logged in to Hugging Face to upload.")
687
+ if not oauth_token or not oauth_token.token:
688
+ raise gr.Error("Hugging Face authentication token is missing or invalid. Please log out and log back in.")
689
 
690
+ folder = str(uuid.uuid4())
691
+ os.makedirs(folder, exist_ok=True) # exist_ok=True is safer if folder might exist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
+ gr.Info(f"Starting processing for model {url}")
694
  try:
695
+ # Pass hunyuan_type to process_url
696
+ info, downloaded_files_summary = process_url(url, profile, do_download=True, folder=folder, hunyuan_type=hunyuan_type)
697
+ except gr.Error as e: # Catch errors from process_url (NSFW, not supported, API fail)
698
+ # Cleanup created folder if download failed or was skipped
699
+ if os.path.exists(folder):
700
+ try:
701
+ import shutil
702
+ shutil.rmtree(folder)
703
+ except Exception as clean_e:
704
+ print(f"Error cleaning up folder {folder}: {clean_e}")
705
+ raise e # Re-raise the Gradio error to display it
706
+
707
+ if not downloaded_files_summary.get("weightName"):
708
+ raise gr.Error("No model weight file was downloaded. Cannot proceed with upload.")
709
+
710
+ # Determine if user is the author for README generation
711
+ # This relies on extract_huggingface_username which needs CIVITAI_COOKIE_INFO
712
+ is_author = False
713
+ if "CIVITAI_COOKIE_INFO" in os.environ:
714
+ hf_username_on_civitai = extract_huggingface_username(info['creator'])
715
+ if hf_username_on_civitai and profile.username.lower() == hf_username_on_civitai.lower():
716
  is_author = True
717
+ elif profile.username.lower() == info['creator'].lower(): # Fallback if cookie not set, direct match
718
+ is_author = True
719
+
 
720
 
721
+ slug_name = slugify(info["name"])
722
+ user_repo_id = f"{profile.username}/{slug_name}"
723
+
724
+ gr.Info(f"Creating README for {user_repo_id}...")
725
+ create_readme(info, downloaded_files_summary, user_repo_id, link_civit, is_author, folder=folder)
726
+
727
+ try:
728
+ gr.Info(f"Creating repository {user_repo_id} on Hugging Face...")
729
+ create_repo(repo_id=user_repo_id, private=True, exist_ok=True, token=oauth_token.token)
730
 
731
+ gr.Info(f"Starting upload of all files to {user_repo_id}...")
732
  upload_folder(
733
+ folder_path=folder,
734
+ repo_id=user_repo_id,
735
+ repo_type="model",
736
+ token=oauth_token.token,
737
+ commit_message=f"Upload LoRA: {info['name']} from Civitai model ID {info['modelId']}" # Add commit message
738
  )
 
 
739
 
740
+ gr.Info(f"Setting repository {user_repo_id} to public...")
741
+ update_repo_visibility(repo_id=user_repo_id, private=False, token=oauth_token.token)
742
+ gr.Info(f"Model {info['name']} uploaded successfully to {user_repo_id}!")
743
  except Exception as e:
744
+ print(f"Error during Hugging Face repo operations for {user_repo_id}: {e}")
745
+ # Attempt to provide a more specific error message for token issues
746
+ if "401" in str(e) or "Unauthorized" in str(e):
747
+ raise gr.Error("Hugging Face authentication failed (e.g. token expired or insufficient permissions). Please log out and log back in with a token that has write permissions.")
748
+ raise gr.Error(f"Error during Hugging Face upload: {str(e)}")
749
  finally:
750
+ # Clean up the temporary folder
751
+ if os.path.exists(folder):
752
+ try:
753
+ import shutil
754
+ shutil.rmtree(folder)
755
+ print(f"Cleaned up temporary folder: {folder}")
756
+ except Exception as clean_e:
757
+ print(f"Error cleaning up folder {folder}: {clean_e}")
758
+
759
+ return f"""# Model uploaded to 🤗!
760
+ Access it here: [{user_repo_id}](https://huggingface.co/{user_repo_id})
761
+ """
762
 
763
+ def bulk_upload(profile: Optional[gr.OAuthProfile], oauth_token: Optional[gr.OAuthToken], urls_text: str, link_civit: bool, hunyuan_type: str):
764
+ if not urls_text.strip():
765
  return "No URLs provided for bulk upload."
766
 
767
+ urls = [url.strip() for url in urls_text.split("\n") if url.strip()]
768
+ if not urls:
769
+ return "No valid URLs found in the input."
770
+
771
+ upload_results_md = "## Bulk Upload Results:\n\n"
772
+ success_count = 0
773
+ failure_count = 0
774
 
775
  for i, url in enumerate(urls):
776
+ gr.Info(f"Processing URL {i+1}/{len(urls)}: {url}")
777
  try:
778
+ result = upload_civit_to_hf(profile, oauth_token, url, link_civit, hunyuan_type)
779
+ upload_results_md += f"**SUCCESS**: {url}\n{result}\n\n---\n\n"
780
+ success_count +=1
781
+ except gr.Error as e: # Catch Gradio-raised errors (expected failures)
782
+ upload_results_md += f"**FAILED**: {url}\n*Reason*: {e.message}\n\n---\n\n"
783
+ gr.Warning(f"Failed to upload {url}: {e.message}")
784
+ failure_count +=1
785
+ except Exception as e: # Catch unexpected Python errors
786
+ upload_results_md += f"**FAILED**: {url}\n*Unexpected Error*: {str(e)}\n\n---\n\n"
787
+ gr.Warning(f"Unexpected error uploading {url}: {str(e)}")
788
+ failure_count +=1
789
+
790
+ summary = f"Finished bulk upload: {success_count} successful, {failure_count} failed."
791
+ gr.Info(summary)
792
+ upload_results_md = f"## {summary}\n\n" + upload_results_md
793
+ return upload_results_md
794
 
795
+ # --- Gradio UI ---
796
  css = '''
797
+ #login_button_row button { /* Target login button specifically */
798
+ width: 100% !important;
799
+ margin: 0 auto;
800
+ }
801
+ #disabled_upload_area { /* ID for the disabled area */
802
+ opacity: 0.5;
803
+ pointer-events: none;
804
+ }
805
  '''
806
 
807
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo: # Added a theme
 
 
808
  gr.Markdown('''# Upload your CivitAI LoRA to Hugging Face 🤗
809
+ By uploading your LoRAs to Hugging Face you get diffusers compatibility, a free GPU-based Inference Widget (for many models),
810
+ listing in [LoRA Studio](https://lorastudio.co/models) after review, and the possibility to submit your model to [LoRA the Explorer](https://huggingface.co/spaces/multimodalart/LoraTheExplorer) ✨
811
+ **Important**: Ensure your `CIVITAI_COOKIE_INFO` environment variable is set if you want to automatically verify your Hugging Face username linked on your Civitai profile. This is required for non-trusted users.
812
+ ''')
813
 
814
+ with gr.Row(elem_id="login_button_row"):
815
+ login_button = gr.LoginButton() # Moved login_button definition here
816
+
817
+ # Area shown when not logged in (or login fails)
818
+ with gr.Column(elem_id="disabled_upload_area", visible=True) as disabled_area:
819
+ gr.HTML("<i>Please log in with Hugging Face to enable uploads.</i>")
820
+ # Add some dummy placeholders to mirror the enabled_area structure if needed for consistent layout
821
+ gr.Textbox(label="CivitAI model URL (Log in to enable)", interactive=False)
822
+ gr.Button("Upload (Log in to enable)", interactive=False)
 
 
823
 
824
+ # Area shown when logged in
825
  with gr.Column(visible=False) as enabled_area:
826
+ with gr.Row():
827
+ submit_source_civit_enabled = gr.Textbox(
828
+ placeholder="https://civitai.com/models/144684/pixelartredmond-pixel-art-loras-for-sd-xl",
829
+ label="CivitAI model URL",
830
+ info="URL of the CivitAI LoRA model page.",
831
+ elem_id="submit_source_civit_main" # Unique ID
832
+ )
833
 
834
+ hunyuan_type_radio = gr.Radio(
835
+ choices=["Image-to-Video", "Text-to-Video"],
836
+ label="HunyuanVideo Type (Select if model is Hunyuan Video)",
837
+ value="Image-to-Video", # Default as per prompt
838
+ visible=False, # Initially hidden
839
+ interactive=True
840
+ )
841
+
842
+ link_civit_checkbox = gr.Checkbox(label="Link back to original CivitAI page in README?", value=False)
843
+
844
+ with gr.Accordion("Bulk Upload (Multiple LoRAs)", open=False):
845
+ civit_username_to_bulk = gr.Textbox(
846
+ label="Your CivitAI Username (Optional)",
847
+ info="Type your CivitAI username here to automatically populate the list below with your compatible LoRAs."
848
+ )
849
+ submit_bulk_civit_urls = gr.Textbox(
850
+ label="CivitAI Model URLs (One per line)",
851
+ info="Add one CivitAI model URL per line for bulk processing.",
852
+ lines=6,
853
+ )
854
+ bulk_button = gr.Button("Start Bulk Upload")
 
 
 
855
 
856
+ instructions_html = gr.HTML("") # For messages from check_civit_link
857
+
858
+ # Buttons for single upload
859
+ # try_again_button is shown if username check fails
860
+ try_again_button_single = gr.Button("I've updated my CivitAI profile, check again", visible=False)
861
+ # submit_button_single is the main upload button for single model
862
+ submit_button_single = gr.Button("Upload Model to Hugging Face", interactive=False, variant="primary")
863
+
864
+ output_markdown = gr.Markdown(label="Upload Progress & Results", visible=False)
865
+
866
+ # Event Handling
867
+ # When login status changes (login_button implicitly handles profile state for demo.load)
868
+ # demo.load updates visibility of disabled_area and enabled_area based on login.
869
+ # The `profile` argument is implicitly passed by Gradio to functions that declare it.
870
+ # `oauth_token` is also implicitly passed if `login_button` is used and function expects `gr.OAuthToken`.
871
+
872
+ # When URL changes in the enabled area
873
  submit_source_civit_enabled.change(
874
+ fn=check_civit_link,
875
+ inputs=[submit_source_civit_enabled], # profile is implicitly passed
876
+ outputs=[instructions_html, submit_button_single, try_again_button_single, submit_button_single, hunyuan_type_radio],
877
+ # Outputs map to: instructions, submit_interactive, try_again_visible, (submit_visible - seems redundant here, check_civit_link logic ensures one is visible), hunyuan_radio_visible
878
+ # For submit_button_single: 2nd output controls 'interactive', 4th controls 'visible' (often paired with try_again_button's visibility)
879
  )
880
 
881
+ # Try again button for single upload (re-checks the same URL)
882
+ try_again_button_single.click(
883
+ fn=check_civit_link,
884
+ inputs=[submit_source_civit_enabled],
885
+ outputs=[instructions_html, submit_button_single, try_again_button_single, submit_button_single, hunyuan_type_radio],
886
  )
887
+
888
+ # Autofill bulk URLs from CivitAI username
889
+ civit_username_to_bulk.change(
890
+ fn=list_civit_models,
891
+ inputs=[civit_username_to_bulk],
892
+ outputs=[submit_bulk_civit_urls]
 
 
 
 
 
 
 
 
 
893
  )
894
+
895
+ # Single model upload button click
896
+ submit_button_single.click(fn=show_output, outputs=[output_markdown]).then(
897
+ fn=upload_civit_to_hf,
898
+ inputs=[submit_source_civit_enabled, link_civit_checkbox, hunyuan_type_radio], # profile, oauth_token implicit
899
+ outputs=[output_markdown]
 
 
900
  )
901
 
902
+ # Bulk model upload button click
903
+ bulk_button.click(fn=show_output, outputs=[output_markdown]).then(
904
+ fn=bulk_upload,
905
+ inputs=[submit_bulk_civit_urls, link_civit_checkbox, hunyuan_type_radio], # profile, oauth_token implicit
906
+ outputs=[output_markdown]
907
+ )
 
 
 
908
 
909
+ # Initial state of visible areas based on login status
910
+ demo.load(fn=swap_fill, outputs=[disabled_area, enabled_area], queue=False)
911
+
912
+ demo.queue(default_concurrency_limit=5) # Reduced concurrency from 50, can be demanding
913
+ demo.launch(debug=True) # Added debug=True for development