fantaxy commited on
Commit
25a1f48
·
verified ·
1 Parent(s): dda5fcf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +784 -523
app.py CHANGED
@@ -1,539 +1,800 @@
1
- import gradio as gr
2
- import requests
3
- import io
4
- import random
5
- import os
6
- import time
7
- from PIL import Image
8
- import json
9
-
10
- # Get API token from environment variable
11
- HF_TOKEN = os.getenv("HF_TOKEN")
12
- if not HF_TOKEN:
13
- raise ValueError("HF_TOKEN environment variable is not set")
14
-
15
- def query(
16
- prompt,
17
- model,
18
- custom_lora,
19
- negative_prompt="", # ← 기존 is_negative=False → negative_prompt="" 로 변경
20
- steps=35,
21
- cfg_scale=7,
22
- sampler="DPM++ 2M Karras",
23
- seed=-1,
24
- strength=0.7,
25
- width=1024,
26
- height=1024
27
- ):
28
- print("Starting query function...")
29
-
30
- if not prompt:
31
- raise gr.Error("Prompt cannot be empty")
32
-
33
- # Set headers with API token
34
- headers = {"Authorization": f"Bearer {HF_TOKEN}"}
35
-
36
- # Generate a unique key for tracking
37
- key = random.randint(0, 999)
38
-
39
- # Enhance prompt
40
- prompt = f"{prompt} | ultra detail, ultra elaboration, ultra quality, perfect."
41
- print(f'Generation {key}: {prompt}')
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  try:
44
- # Set API URL based on model selection
45
- if custom_lora.strip():
46
- API_URL = f"https://api-inference.huggingface.co/models/{custom_lora.strip()}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  else:
48
- if model == 'Stable Diffusion XL':
49
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
50
- elif model == 'FLUX.1 [Dev]':
51
- API_URL = "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-dev"
52
- elif model == 'FLUX.1 [Schnell]':
53
- API_URL = "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell"
54
- elif model == 'Flux Logo Design':
55
- API_URL = "https://api-inference.huggingface.co/models/Shakker-Labs/FLUX.1-dev-LoRA-Logo-Design"
56
- prompt = f"wablogo, logo, Minimalist, {prompt}"
57
- elif model == 'Flux Uncensored':
58
- API_URL = "https://api-inference.huggingface.co/models/enhanceaiteam/Flux-uncensored"
59
- elif model == 'Flux Uncensored V2':
60
- API_URL = "https://api-inference.huggingface.co/models/enhanceaiteam/Flux-Uncensored-V2"
61
- elif model == 'Flux Tarot Cards':
62
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Ton618-Tarot-Cards-Flux-LoRA"
63
- prompt = f"Tarot card, {prompt}"
64
- elif model == 'Pixel Art Sprites':
65
- API_URL = "https://api-inference.huggingface.co/models/sWizad/pokemon-trainer-sprites-pixelart-flux"
66
- prompt = f"a pixel image, {prompt}"
67
- elif model == '3D Sketchfab':
68
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Castor-3D-Sketchfab-Flux-LoRA"
69
- prompt = f"3D Sketchfab, {prompt}"
70
- elif model == 'Retro Comic Flux':
71
- API_URL = "https://api-inference.huggingface.co/models/renderartist/retrocomicflux"
72
- prompt = f"c0m1c, comic book panel, {prompt}"
73
- elif model == 'Caricature':
74
- API_URL = "https://api-inference.huggingface.co/models/TheAwakenOne/caricature"
75
- prompt = f"CCTUR3, {prompt}"
76
- elif model == 'Huggieverse':
77
- API_URL = "https://api-inference.huggingface.co/models/Chunte/flux-lora-Huggieverse"
78
- prompt = f"HGGRE, {prompt}"
79
- elif model == 'Propaganda Poster':
80
- API_URL = "https://api-inference.huggingface.co/models/AlekseyCalvin/Propaganda_Poster_Schnell_by_doctor_diffusion"
81
- prompt = f"propaganda poster, {prompt}"
82
- elif model == 'Flux Game Assets V2':
83
- API_URL = "https://api-inference.huggingface.co/models/gokaygokay/Flux-Game-Assets-LoRA-v2"
84
- prompt = f"wbgmsst, white background, {prompt}"
85
- elif model == 'SoftPasty Flux':
86
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/softpasty-flux-dev"
87
- prompt = f"araminta_illus illustration style, {prompt}"
88
- elif model == 'Flux Stickers':
89
- API_URL = "https://api-inference.huggingface.co/models/diabolic6045/Flux_Sticker_Lora"
90
- prompt = f"5t1cker 5ty1e, {prompt}"
91
- elif model == 'Flux Animex V2':
92
- API_URL = "https://api-inference.huggingface.co/models/strangerzonehf/Flux-Animex-v2-LoRA"
93
- prompt = f"Animex, {prompt}"
94
- elif model == 'Flux Animeo V1':
95
- API_URL = "https://api-inference.huggingface.co/models/strangerzonehf/Flux-Animeo-v1-LoRA"
96
- prompt = f"Animeo, {prompt}"
97
- elif model == 'Movie Board':
98
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Flux.1-Dev-Movie-Boards-LoRA"
99
- prompt = f"movieboard, {prompt}"
100
- elif model == 'Purple Dreamy':
101
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Purple-Dreamy-Flux-LoRA"
102
- prompt = f"Purple Dreamy, {prompt}"
103
- elif model == 'PS1 Style Flux':
104
- API_URL = "https://api-inference.huggingface.co/models/veryVANYA/ps1-style-flux"
105
- prompt = f"ps1 game screenshot, {prompt}"
106
- elif model == 'Softserve Anime':
107
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/softserve_anime"
108
- prompt = f"sftsrv style illustration, {prompt}"
109
- elif model == 'Flux Tarot v1':
110
- API_URL = "https://api-inference.huggingface.co/models/multimodalart/flux-tarot-v1"
111
- prompt = f"in the style of TOK a trtcrd tarot style, {prompt}"
112
- elif model == 'Half Illustration':
113
- API_URL = "https://api-inference.huggingface.co/models/davisbro/half_illustration"
114
- prompt = f"in the style of TOK, {prompt}"
115
- elif model == 'OpenDalle v1.1':
116
- API_URL = "https://api-inference.huggingface.co/models/dataautogpt3/OpenDalleV1.1"
117
- elif model == 'Flux Ghibsky Illustration':
118
- API_URL = "https://api-inference.huggingface.co/models/aleksa-codes/flux-ghibsky-illustration"
119
- prompt = f"GHIBSKY style, {prompt}"
120
- elif model == 'Flux Koda':
121
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/flux-koda"
122
- prompt = f"flmft style, {prompt}"
123
- elif model == 'Soviet Diffusion XL':
124
- API_URL = "https://api-inference.huggingface.co/models/openskyml/soviet-diffusion-xl"
125
- prompt = f"soviet poster, {prompt}"
126
- elif model == 'Flux Realism LoRA':
127
- API_URL = "https://api-inference.huggingface.co/models/XLabs-AI/flux-RealismLora"
128
- elif model == 'Frosting Lane Flux':
129
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/frosting_lane_flux"
130
- prompt = f"frstingln illustration, {prompt}"
131
- elif model == 'Phantasma Anime':
132
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/phantasma-anime"
133
- elif model == 'Boreal':
134
- API_URL = "https://api-inference.huggingface.co/models/kudzueye/Boreal"
135
- prompt = f"photo, {prompt}"
136
- elif model == 'How2Draw':
137
- API_URL = "https://api-inference.huggingface.co/models/glif/how2draw"
138
- prompt = f"How2Draw, {prompt}"
139
- elif model == 'Flux AestheticAnime':
140
- API_URL = "https://api-inference.huggingface.co/models/dataautogpt3/FLUX-AestheticAnime"
141
- elif model == 'Fashion Hut Modeling LoRA':
142
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Fashion-Hut-Modeling-LoRA"
143
- prompt = f"Modeling of, {prompt}"
144
- elif model == 'Flux SyntheticAnime':
145
- API_URL = "https://api-inference.huggingface.co/models/dataautogpt3/FLUX-SyntheticAnime"
146
- prompt = f"1980s anime screengrab, VHS quality, syntheticanime, {prompt}"
147
- elif model == 'Flux Midjourney Anime':
148
- API_URL = "https://api-inference.huggingface.co/models/brushpenbob/flux-midjourney-anime"
149
- prompt = f"egmid, {prompt}"
150
- elif model == 'Coloring Book Generator':
151
- API_URL = "https://api-inference.huggingface.co/models/robert123231/coloringbookgenerator"
152
- elif model == 'Collage Flux':
153
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Castor-Collage-Dim-Flux-LoRA"
154
- prompt = f"collage, {prompt}"
155
- elif model == 'Flux Product Ad Backdrop':
156
- API_URL = "https://api-inference.huggingface.co/models/prithivMLmods/Flux-Product-Ad-Backdrop"
157
- prompt = f"Product Ad, {prompt}"
158
- elif model == 'Product Design':
159
- API_URL = "https://api-inference.huggingface.co/models/multimodalart/product-design"
160
- prompt = f"product designed by prdsgn, {prompt}"
161
- elif model == '90s Anime Art':
162
- API_URL = "https://api-inference.huggingface.co/models/glif/90s-anime-art"
163
- elif model == 'Brain Melt Acid Art':
164
- API_URL = "https://api-inference.huggingface.co/models/glif/Brain-Melt-Acid-Art"
165
- prompt = f"maximalism, in an acid surrealism style, {prompt}"
166
- elif model == 'Lustly Flux Uncensored v1':
167
- API_URL = "https://api-inference.huggingface.co/models/lustlyai/Flux_Lustly.ai_Uncensored_nsfw_v1"
168
- elif model == 'NSFW Master Flux':
169
- API_URL = "https://api-inference.huggingface.co/models/Keltezaa/NSFW_MASTER_FLUX"
170
- prompt = f"NSFW, {prompt}"
171
- elif model == 'Flux Outfit Generator':
172
- API_URL = "https://api-inference.huggingface.co/models/tryonlabs/FLUX.1-dev-LoRA-Outfit-Generator"
173
- elif model == 'Midjourney':
174
- API_URL = "https://api-inference.huggingface.co/models/Jovie/Midjourney"
175
- elif model == 'DreamPhotoGASM':
176
- API_URL = "https://api-inference.huggingface.co/models/Yntec/DreamPhotoGASM"
177
- elif model == 'Flux Super Realism LoRA':
178
- API_URL = "https://api-inference.huggingface.co/models/strangerzonehf/Flux-Super-Realism-LoRA"
179
- elif model == 'Stable Diffusion 2-1':
180
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-2-1-base"
181
- elif model == 'Stable Diffusion 3.5 Large':
182
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-3.5-large"
183
- elif model == 'Stable Diffusion 3.5 Large Turbo':
184
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-3.5-large-turbo"
185
- elif model == 'Stable Diffusion 3 Medium':
186
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-3-medium-diffusers"
187
- prompt = f"A, {prompt}"
188
- elif model == 'Duchaiten Real3D NSFW XL':
189
- API_URL = "https://api-inference.huggingface.co/models/stablediffusionapi/duchaiten-real3d-nsfw-xl"
190
- elif model == 'Pixel Art XL':
191
- API_URL = "https://api-inference.huggingface.co/models/nerijs/pixel-art-xl"
192
- prompt = f"pixel art, {prompt}"
193
- elif model == 'Character Design':
194
- API_URL = "https://api-inference.huggingface.co/models/KappaNeuro/character-design"
195
- prompt = f"Character Design, {prompt}"
196
- elif model == 'Sketched Out Manga':
197
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/sketchedoutmanga"
198
- prompt = f"daiton, {prompt}"
199
- elif model == 'Archfey Anime':
200
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/archfey_anime"
201
- elif model == 'Lofi Cuties':
202
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/lofi-cuties"
203
- elif model == 'YiffyMix':
204
- API_URL = "https://api-inference.huggingface.co/models/Yntec/YiffyMix"
205
- elif model == 'Analog Madness Realistic v7':
206
- API_URL = "https://api-inference.huggingface.co/models/digiplay/AnalogMadness-realistic-model-v7"
207
- elif model == 'Selfie Photography':
208
- API_URL = "https://api-inference.huggingface.co/models/artificialguybr/selfiephotographyredmond-selfie-photography-lora-for-sdxl"
209
- prompt = f"instagram model, discord profile picture, {prompt}"
210
- elif model == 'Filmgrain':
211
- API_URL = "https://api-inference.huggingface.co/models/artificialguybr/filmgrain-redmond-filmgrain-lora-for-sdxl"
212
- prompt = f"Film Grain, FilmGrainAF, {prompt}"
213
- elif model == 'Leonardo AI Style Illustration':
214
- API_URL = "https://api-inference.huggingface.co/models/goofyai/Leonardo_Ai_Style_Illustration"
215
- prompt = f"leonardo style, illustration, vector art, {prompt}"
216
- elif model == 'Cyborg Style XL':
217
- API_URL = "https://api-inference.huggingface.co/models/goofyai/cyborg_style_xl"
218
- prompt = f"cyborg style, {prompt}"
219
- elif model == 'Little Tinies':
220
- API_URL = "https://api-inference.huggingface.co/models/alvdansen/littletinies"
221
- elif model == 'NSFW XL':
222
- API_URL = "https://api-inference.huggingface.co/models/Dremmar/nsfw-xl"
223
- elif model == 'Analog Redmond':
224
- API_URL = "https://api-inference.huggingface.co/models/artificialguybr/analogredmond"
225
- prompt = f"timeless style, {prompt}"
226
- elif model == 'Pixel Art Redmond':
227
- API_URL = "https://api-inference.huggingface.co/models/artificialguybr/PixelArtRedmond"
228
- prompt = f"Pixel Art, {prompt}"
229
- elif model == 'Ascii Art':
230
- API_URL = "https://api-inference.huggingface.co/models/CiroN2022/ascii-art"
231
- prompt = f"ascii art, {prompt}"
232
- elif model == 'Analog':
233
- API_URL = "https://api-inference.huggingface.co/models/Yntec/Analog"
234
- elif model == 'Maple Syrup':
235
- API_URL = "https://api-inference.huggingface.co/models/Yntec/MapleSyrup"
236
- elif model == 'Perfect Lewd Fantasy':
237
- API_URL = "https://api-inference.huggingface.co/models/digiplay/perfectLewdFantasy_v1.01"
238
- elif model == 'AbsoluteReality 1.8.1':
239
- API_URL = "https://api-inference.huggingface.co/models/digiplay/AbsoluteReality_v1.8.1"
240
- elif model == 'Disney':
241
- API_URL = "https://api-inference.huggingface.co/models/goofyai/disney_style_xl"
242
- prompt = f"Disney style, {prompt}"
243
- elif model == 'Redmond SDXL':
244
- API_URL = "https://api-inference.huggingface.co/models/artificialguybr/LogoRedmond-LogoLoraForSDXL-V2"
245
- elif model == 'epiCPhotoGasm':
246
- API_URL = "https://api-inference.huggingface.co/models/Yntec/epiCPhotoGasm"
247
- else:
248
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
249
-
250
- # Prepare payload in Hugging Face Inference API style
251
- # (negative_prompt, steps, cfg_scale, seed, strength 등은 parameters 안에 배치)
252
- payload = {
253
- "inputs": prompt,
254
- "parameters": {
255
- "negative_prompt": negative_prompt,
256
- "num_inference_steps": steps,
257
- "guidance_scale": cfg_scale,
258
- "width": width,
259
- "height": height,
260
- "strength": strength,
261
- # seed를 지원하는 모델/엔드포인트에 따라 무시될 수도 있음
262
- "seed": seed if seed != -1 else random.randint(1, 1000000000),
263
- },
264
- # 모델이 로딩 중일 경우 기다리도록 설정
265
- "options": {"wait_for_model": True}
266
- }
267
 
268
- # Improved retry logic with exponential backoff
269
- max_retries = 3
270
- current_retry = 0
271
- backoff_factor = 2 # Exponential backoff
272
-
273
- while current_retry < max_retries:
274
- try:
275
- response = requests.post(API_URL, headers=headers, json=payload, timeout=180)
276
-
277
- # 디버깅용 정보 출력
278
- print("Response Content-Type:", response.headers.get("Content-Type"))
279
- print("Response Text (snippet):", response.text[:500])
280
-
281
- response.raise_for_status() # HTTP 에러 코드 시 예외 발생
282
- image = Image.open(io.BytesIO(response.content))
283
-
284
- print(f'Generation {key} completed successfully')
285
- return image
286
-
287
- except (requests.exceptions.Timeout,
288
- requests.exceptions.ConnectionError,
289
- requests.exceptions.HTTPError,
290
- requests.exceptions.RequestException) as e:
291
- current_retry += 1
292
- if current_retry < max_retries:
293
- wait_time = backoff_factor ** current_retry # Exponential backoff
294
- print(f"Network error occurred: {str(e)}. Retrying in {wait_time} seconds... (Attempt {current_retry + 1}/{max_retries})")
295
- time.sleep(wait_time)
296
- continue
297
- else:
298
- # Detailed error message based on exception type
299
- if isinstance(e, requests.exceptions.Timeout):
300
- error_msg = f"Request timed out after {max_retries} attempts. The model might be busy, please try again later."
301
- elif isinstance(e, requests.exceptions.ConnectionError):
302
- error_msg = f"Connection error after {max_retries} attempts. Please check your network connection."
303
- elif isinstance(e, requests.exceptions.HTTPError):
304
- status_code = e.response.status_code if hasattr(e, 'response') and e.response is not None else "unknown"
305
- error_msg = f"HTTP error (status code: {status_code}) after {max_retries} attempts."
306
- else:
307
- error_msg = f"Request failed after {max_retries} attempts: {str(e)}"
308
-
309
- raise gr.Error(error_msg)
310
-
311
- except Exception as e:
312
- error_message = f"Unexpected error: {str(e)}"
313
- if isinstance(e, requests.exceptions.RequestException) and hasattr(e, 'response') and e.response is not None:
314
- if e.response.status_code == 401:
315
- error_message = "Invalid API token. Please check your Hugging Face API token."
316
- elif e.response.status_code == 403:
317
- error_message = "Access denied. Please check your API token permissions."
318
- elif e.response.status_code == 503:
319
- error_message = "Model is currently loading. Please try again in a few moments."
320
- raise gr.Error(error_message)
321
-
322
-
323
- def generate_grid(prompt, selected_models, custom_lora, negative_prompt, steps, cfg_scale, seed, strength, width, height, progress=gr.Progress()):
324
- if len(selected_models) > 4:
325
- raise gr.Error("Please select up to 4 models")
326
- if len(selected_models) == 0:
327
- raise gr.Error("Please select at least 1 model")
328
-
329
- # Initialize image array
330
- images = [None] * 4
331
- total_models = len(selected_models[:4])
332
-
333
- def update_gallery():
334
- # Only include non-None images for gallery update
335
- return [img for img in images if img is not None]
336
-
337
- # Create placeholder for failed models
338
- placeholder_image = None
339
-
340
- # Generate image for each model
341
- for idx, model_name in enumerate(selected_models[:4]):
342
- try:
343
- progress((idx + 1) / total_models, f"Generating image for {model_name}...")
344
- img = query(prompt, model_name, custom_lora, negative_prompt, steps, cfg_scale, seed, strength, width, height)
345
- images[idx] = img
346
-
347
- # If this is the first successful generation, save as placeholder for failed models
348
- if placeholder_image is None:
349
- placeholder_image = img
350
-
351
- # Update gallery after each successful generation
352
- yield update_gallery()
353
- except Exception as e:
354
- print(f"Error generating image for {model_name}: {str(e)}")
355
- # Keep the slot as None and continue with next model
356
- continue
357
-
358
- # Fill empty slots with a placeholder (either the last successful image or a blank image)
359
- if placeholder_image:
360
- for i in range(len(images)):
361
- if images[i] is None:
362
- # Create a copy of placeholder to avoid reference issues
363
- images[i] = placeholder_image.copy()
364
  else:
365
- # If all models failed, create a blank image with error text
366
- for i in range(len(images)):
367
- blank_img = Image.new('RGB', (width, height), color=(240, 240, 240))
368
- images[i] = blank_img
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- progress(1.0, "Generation complete!")
371
- yield update_gallery()
372
 
 
 
 
 
 
 
 
 
 
373
 
374
- def check_network_connectivity():
375
- """Utility function to check network connectivity to the Hugging Face API"""
 
 
 
 
 
 
 
 
 
 
 
 
376
  try:
377
- response = requests.get("https://api-inference.huggingface.co", timeout=5)
378
- if response.status_code == 200:
379
- return True
380
- return False
381
- except:
382
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
- css = """
386
- footer {
387
- visibility: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
- """
390
-
391
- with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css) as dalle:
392
- gr.Markdown("# ZeroWeight Studio")
393
-
394
- with gr.Row():
395
- with gr.Column(scale=2):
396
- text_prompt = gr.Textbox(
397
- label="Prompt",
398
- placeholder="Describe what you want to create...",
399
- lines=3
400
- )
401
-
402
- negative_prompt = gr.Textbox(
403
- label="Negative Prompt",
404
- placeholder="What should not be in the image",
405
- value="(deformed, distorted, disfigured), poorly drawn, bad anatomy, wrong anatomy, extra limb, missing limb, floating limbs, (mutated hands and fingers), disconnected limbs, mutation, mutated, ugly, disgusting, blurry, amputation",
406
- lines=2
407
- )
408
-
409
- custom_lora = gr.Textbox(
410
- label="Custom LoRA Path (Optional)",
411
- placeholder="e.g., multimodalart/vintage-ads-flux",
412
- lines=1
413
- )
414
-
415
- with gr.Column(scale=1):
416
- with gr.Group():
417
- gr.Markdown("### Image Settings")
418
- width = gr.Slider(label="Width", value=1024, minimum=512, maximum=1216, step=64)
419
- height = gr.Slider(label="Height", value=1024, minimum=512, maximum=1216, step=64)
420
-
421
- with gr.Group():
422
- gr.Markdown("### Generation Parameters")
423
- steps = gr.Slider(label="Steps", value=35, minimum=1, maximum=100, step=1)
424
- cfg = gr.Slider(label="CFG Scale", value=7, minimum=1, maximum=20, step=0.5)
425
- strength = gr.Slider(label="Strength", value=0.7, minimum=0, maximum=1, step=0.1)
426
- seed = gr.Slider(label="Seed (-1 for random)", value=-1, minimum=-1, maximum=1000000000, step=1)
427
-
428
- with gr.Accordion("Model Selection", open=False):
429
- model_search = gr.Textbox(
430
- label="Search Models",
431
- placeholder="Type to filter models...",
432
- lines=1
433
- )
434
-
435
- # Set top 4 models as default
436
- default_models = [
437
- "FLUX.1 [Schnell]",
438
- "Stable Diffusion 3.5 Large",
439
- "Stable Diffusion 3.5 Large Turbo",
440
- "Midjourney"
441
- ]
442
-
443
- # Full model list
444
- models_list = [
445
- "FLUX.1 [Schnell]",
446
- "Stable Diffusion 3.5 Large",
447
- "Stable Diffusion 3.5 Large Turbo",
448
- "Stable Diffusion XL",
449
- "FLUX.1 [Dev]",
450
- "Midjourney",
451
- "DreamPhotoGASM",
452
- "Disney",
453
- "Leonardo AI Style Illustration",
454
- "AbsoluteReality 1.8.1",
455
- "Analog Redmond",
456
- "Stable Diffusion 3 Medium",
457
- "Flux Super Realism LoRA",
458
- "Flux Realism LoRA",
459
- "Selfie Photography",
460
- "Character Design",
461
- "Pixel Art XL",
462
- "3D Sketchfab",
463
- "Flux Animex V2",
464
- "Flux Animeo V1",
465
- "Flux AestheticAnime",
466
- "90s Anime Art",
467
- "Softserve Anime",
468
- "Brain Melt Acid Art",
469
- "Retro Comic Flux",
470
- "Purple Dreamy",
471
- "SoftPasty Flux",
472
- "Flux Logo Design",
473
- "Product Design",
474
- "Propaganda Poster",
475
- "Movie Board",
476
- "Collage Flux"
477
- ]
478
-
479
- model = gr.Checkboxgroup(
480
- label="Select Models (Choose up to 4)",
481
- choices=models_list,
482
- value=default_models,
483
- interactive=True
484
- )
485
-
486
- with gr.Row():
487
- generate_btn = gr.Button("Generate 2x2 Grid", variant="primary", size="lg")
488
-
489
- # Add network status indicator
490
- network_status = gr.Markdown("", elem_id="network_status")
491
-
492
- # Function to check and update network status
493
- def update_network_status():
494
- if check_network_connectivity():
495
- return "✅ Connected to Hugging Face API"
496
- else:
497
- return "❌ No connection to Hugging Face API. Please check your network."
498
-
499
- with gr.Row():
500
- gallery = gr.Gallery(
501
- label="Generated Images",
502
- show_label=True,
503
- elem_id="gallery",
504
- columns=2,
505
- rows=2,
506
- height="auto",
507
- preview=True,
508
- )
509
-
510
- # Event handlers
511
- generate_btn.click(
512
- fn=generate_grid,
513
- inputs=[
514
- text_prompt,
515
- model,
516
- custom_lora,
517
- negative_prompt,
518
- steps,
519
- cfg,
520
- seed,
521
- strength,
522
- width,
523
- height
524
- ],
525
- outputs=gallery,
526
- show_progress=True
527
- )
528
 
529
- def filter_models(search_term):
530
- filtered_models = [m for m in models_list if search_term.lower() in m.lower()]
531
- return gr.update(choices=filtered_models, value=[])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
- model_search.change(filter_models, inputs=model_search, outputs=model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
- # Update network status when the app loads
536
- dalle.load(fn=update_network_status, outputs=network_status)
537
 
538
- if __name__ == "__main__":
539
- dalle.launch(show_api=False, share=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import os, re, json, sqlite3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ app = Flask(__name__)
5
+
6
+ # ────────────────────────── 1. CONFIGURATION ──────────────────────────
7
+ DB_FILE = "favorite_sites.json" # JSON file for backward compatibility
8
+ SQLITE_DB = "favorite_sites.db" # SQLite database for persistence
9
+
10
+ # Domains that commonly block iframes
11
+ BLOCKED_DOMAINS = [
12
+ "naver.com", "daum.net", "google.com",
13
+ "facebook.com", "instagram.com", "kakao.com",
14
+ "ycombinator.com"
15
+ ]
16
+
17
+ # ────────────────────────── 2. CURATED CATEGORIES ──────────────────────────
18
+ CATEGORIES = {
19
+ "Productivity": [
20
+ "https://huggingface.co/spaces/ginipick/AI-BOOK",
21
+ "https://huggingface.co/spaces/ginigen/perflexity-clone",
22
+ "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
23
+ "https://huggingface.co/spaces/VIDraft/mouse-webgen",
24
+ "https://huggingface.co/spaces/openfree/Vibe-Game",
25
+ "https://huggingface.co/spaces/openfree/Game-Gallery",
26
+ "https://huggingface.co/spaces/aiqtech/Contributors-Leaderboard",
27
+ "https://huggingface.co/spaces/fantaxy/Model-Leaderboard",
28
+ "https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
29
+ "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
30
+ ],
31
+ "Multimodal": [
32
+ "https://huggingface.co/spaces/openfree/DreamO-video",
33
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
34
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
35
+ "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
36
+ "https://huggingface.co/spaces/ginigen/SFX-Sound-magic",
37
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
38
+ "https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
39
+ "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
40
+ ],
41
+ "Professional": [
42
+ "https://huggingface.co/spaces/ginigen/blogger",
43
+ "https://huggingface.co/spaces/VIDraft/money-radar",
44
+ "https://huggingface.co/spaces/immunobiotech/drug-discovery",
45
+ "https://huggingface.co/spaces/immunobiotech/Gemini-MICHELIN",
46
+ "https://huggingface.co/spaces/Heartsync/Papers-Leaderboard",
47
+ "https://huggingface.co/spaces/VIDraft/PapersImpact",
48
+ "https://huggingface.co/spaces/ginipick/AgentX-Papers",
49
+ "https://huggingface.co/spaces/openfree/Cycle-Navigator",
50
+ ],
51
+ "Image": [
52
+ "https://huggingface.co/spaces/ginigen/interior-design",
53
+ "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
54
+ "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
55
+ "https://huggingface.co/spaces/ginigen/Every-Text",
56
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
57
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
58
+ "https://huggingface.co/spaces/Heartsync/FLUX-Vision",
59
+ "https://huggingface.co/spaces/ginigen/VisualCloze",
60
+ "https://huggingface.co/spaces/seawolf2357/Ghibli-Multilingual-Text-rendering",
61
+ "https://huggingface.co/spaces/ginigen/Ghibli-Meme-Studio",
62
+ "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
63
+ "https://huggingface.co/spaces/ginigen/3D-LLAMA",
64
+ ],
65
+ "LLM / VLM": [
66
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
67
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
68
+ "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
69
+ "https://huggingface.co/spaces/aiqcamp/gemini-2.5-flash-preview",
70
+ "https://huggingface.co/spaces/openfree/qwen3-30b-a3b-research",
71
+ "https://huggingface.co/spaces/openfree/qwen3-235b-a22b-research",
72
+ "https://huggingface.co/spaces/openfree/Llama-4-Maverick-17B-Research",
73
+ ],
74
+ }
75
+
76
+ # ────────────────────────── 3. DATABASE FUNCTIONS ──────────────────────────
77
+ def init_db():
78
+ # Initialize JSON file if it doesn't exist
79
+ if not os.path.exists(DB_FILE):
80
+ with open(DB_FILE, "w", encoding="utf-8") as f:
81
+ json.dump([], f, ensure_ascii=False)
82
+
83
+ # Initialize SQLite database
84
+ conn = sqlite3.connect(SQLITE_DB)
85
+ cursor = conn.cursor()
86
+ cursor.execute('''
87
+ CREATE TABLE IF NOT EXISTS urls (
88
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ url TEXT UNIQUE NOT NULL,
90
+ date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
91
+ )
92
+ ''')
93
+ conn.commit()
94
+
95
+ # If we have data in JSON but not in SQLite (first run with new SQLite DB),
96
+ # migrate the data from JSON to SQLite
97
+ json_urls = load_json()
98
+ if json_urls:
99
+ db_urls = load_db_sqlite()
100
+ for url in json_urls:
101
+ if url not in db_urls:
102
+ add_url_to_sqlite(url)
103
+
104
+ conn.close()
105
+
106
+ def load_json():
107
+ """Load URLs from JSON file (for backward compatibility)"""
108
  try:
109
+ with open(DB_FILE, "r", encoding="utf-8") as f:
110
+ raw = json.load(f)
111
+ return raw if isinstance(raw, list) else []
112
+ except Exception:
113
+ return []
114
+
115
+ def save_json(lst):
116
+ """Save URLs to JSON file (for backward compatibility)"""
117
+ try:
118
+ with open(DB_FILE, "w", encoding="utf-8") as f:
119
+ json.dump(lst, f, ensure_ascii=False, indent=2)
120
+ return True
121
+ except Exception:
122
+ return False
123
+
124
+ def load_db_sqlite():
125
+ """Load URLs from SQLite database"""
126
+ conn = sqlite3.connect(SQLITE_DB)
127
+ cursor = conn.cursor()
128
+ cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
129
+ urls = [row[0] for row in cursor.fetchall()]
130
+ conn.close()
131
+ return urls
132
+
133
+ def add_url_to_sqlite(url):
134
+ """Add a URL to SQLite database"""
135
+ conn = sqlite3.connect(SQLITE_DB)
136
+ cursor = conn.cursor()
137
+ try:
138
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
139
+ conn.commit()
140
+ success = True
141
+ except sqlite3.IntegrityError:
142
+ # URL already exists
143
+ success = False
144
+ conn.close()
145
+ return success
146
+
147
+ def update_url_in_sqlite(old_url, new_url):
148
+ """Update a URL in SQLite database"""
149
+ conn = sqlite3.connect(SQLITE_DB)
150
+ cursor = conn.cursor()
151
+ try:
152
+ cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
153
+ if cursor.rowcount > 0:
154
+ conn.commit()
155
+ success = True
156
  else:
157
+ success = False
158
+ except sqlite3.IntegrityError:
159
+ # New URL already exists
160
+ success = False
161
+ conn.close()
162
+ return success
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ def delete_url_from_sqlite(url):
165
+ """Delete a URL from SQLite database"""
166
+ conn = sqlite3.connect(SQLITE_DB)
167
+ cursor = conn.cursor()
168
+ cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
169
+ if cursor.rowcount > 0:
170
+ conn.commit()
171
+ success = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  else:
173
+ success = False
174
+ conn.close()
175
+ return success
176
+
177
+ def load_db():
178
+ """Primary function to load URLs - prioritizes SQLite DB but falls back to JSON"""
179
+ urls = load_db_sqlite()
180
+ if not urls:
181
+ # If SQLite DB is empty, try loading from JSON
182
+ urls = load_json()
183
+ # If we found URLs in JSON, migrate them to SQLite
184
+ for url in urls:
185
+ add_url_to_sqlite(url)
186
+ return urls
187
+
188
+ def save_db(lst):
189
+ """Save URLs to both SQLite and JSON"""
190
+ # Get existing URLs from SQLite for comparison
191
+ existing_urls = load_db_sqlite()
192
+
193
+ # Clear all URLs from SQLite and add the new list
194
+ conn = sqlite3.connect(SQLITE_DB)
195
+ cursor = conn.cursor()
196
+ cursor.execute("DELETE FROM urls")
197
+ for url in lst:
198
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
199
+ conn.commit()
200
+ conn.close()
201
 
202
+ # Also save to JSON for backward compatibility
203
+ return save_json(lst)
204
 
205
+ # ────────────────────────── 4. URL HELPERS ──────────────────────────
206
+ def direct_url(hf_url):
207
+ m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
208
+ if not m:
209
+ return hf_url
210
+ owner, name = m.groups()
211
+ owner = owner.lower()
212
+ name = name.replace('.', '-').replace('_', '-').lower()
213
+ return f"https://{owner}-{name}.hf.space"
214
 
215
+ def screenshot_url(url):
216
+ return f"https://image.thum.io/get/fullpage/{url}"
217
+
218
+ def process_url_for_preview(url):
219
+ """Returns (preview_url, mode)"""
220
+ # Handle blocked domains first
221
+ if any(d for d in BLOCKED_DOMAINS if d in url):
222
+ return screenshot_url(url), "snapshot"
223
+
224
+ # Special case handling for problematic URLs
225
+ if "vibe-coding-tetris" in url or "World-of-Tank-GAME" in url or "Minesweeper-Game" in url:
226
+ return screenshot_url(url), "snapshot"
227
+
228
+ # General HF space handling
229
  try:
230
+ if "huggingface.co/spaces" in url:
231
+ parts = url.rstrip("/").split("/")
232
+ if len(parts) >= 5:
233
+ owner = parts[-2]
234
+ name = parts[-1]
235
+ embed_url = f"https://huggingface.co/spaces/{owner}/{name}/embed"
236
+ return embed_url, "iframe"
237
+ except Exception:
238
+ return screenshot_url(url), "snapshot"
239
+
240
+ # Default handling
241
+ return url, "iframe"
242
+
243
+ # ────────────────────────── 5. API ROUTES ──────────────────────────
244
+ @app.route('/api/category')
245
+ def api_category():
246
+ cat = request.args.get('name', '')
247
+ urls = CATEGORIES.get(cat, [])
248
+
249
+ # Add pagination for categories as well
250
+ page = int(request.args.get('page', 1))
251
+ per_page = int(request.args.get('per_page', 4)) # Changed to 4 per page
252
+
253
+ total_pages = max(1, (len(urls) + per_page - 1) // per_page)
254
+ start = (page - 1) * per_page
255
+ end = min(start + per_page, len(urls))
256
+
257
+ urls_page = urls[start:end]
258
+
259
+ items = [
260
+ {
261
+ "title": url.split('/')[-1],
262
+ "owner": url.split('/')[-2] if '/spaces/' in url else '',
263
+ "iframe": direct_url(url),
264
+ "shot": screenshot_url(url),
265
+ "hf": url
266
+ } for url in urls_page
267
+ ]
268
+
269
+ return jsonify({
270
+ "items": items,
271
+ "page": page,
272
+ "total_pages": total_pages
273
+ })
274
+
275
+ @app.route('/api/favorites')
276
+ def api_favorites():
277
+ # Load URLs from SQLite database
278
+ urls = load_db()
279
+
280
+ page = int(request.args.get('page', 1))
281
+ per_page = int(request.args.get('per_page', 4)) # Changed to 4 per page
282
+
283
+ total_pages = max(1, (len(urls) + per_page - 1) // per_page)
284
+ start = (page - 1) * per_page
285
+ end = min(start + per_page, len(urls))
286
+
287
+ urls_page = urls[start:end]
288
+
289
+ result = []
290
+ for url in urls_page:
291
+ try:
292
+ preview_url, mode = process_url_for_preview(url)
293
+ result.append({
294
+ "title": url.split('/')[-1],
295
+ "url": url,
296
+ "preview_url": preview_url,
297
+ "mode": mode
298
+ })
299
+ except Exception:
300
+ # Fallback to screenshot mode
301
+ result.append({
302
+ "title": url.split('/')[-1],
303
+ "url": url,
304
+ "preview_url": screenshot_url(url),
305
+ "mode": "snapshot"
306
+ })
307
+
308
+ return jsonify({
309
+ "items": result,
310
+ "page": page,
311
+ "total_pages": total_pages
312
+ })
313
 
314
+ @app.route('/api/url/add', methods=['POST'])
315
+ def add_url():
316
+ url = request.form.get('url', '').strip()
317
+ if not url:
318
+ return jsonify({"success": False, "message": "URL is required"})
319
+
320
+ # SQLite에 추가 시도
321
+ conn = sqlite3.connect(SQLITE_DB)
322
+ cursor = conn.cursor()
323
+ try:
324
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
325
+ conn.commit()
326
+ success = True
327
+ except sqlite3.IntegrityError:
328
+ # URL이 이미 존재하는 경우
329
+ success = False
330
+ except Exception as e:
331
+ print(f"SQLite error: {str(e)}")
332
+ success = False
333
+ finally:
334
+ conn.close()
335
+
336
+ if not success:
337
+ return jsonify({"success": False, "message": "URL already exists or could not be added"})
338
+
339
+ # JSON 파일에도 추가 (백업용)
340
+ data = load_json()
341
+ if url not in data:
342
+ data.insert(0, url)
343
+ save_json(data)
344
+
345
+ return jsonify({"success": True, "message": "URL added successfully"})
346
 
347
+ @app.route('/api/url/update', methods=['POST'])
348
+ def update_url():
349
+ old = request.form.get('old', '')
350
+ new = request.form.get('new', '').strip()
351
+
352
+ if not new:
353
+ return jsonify({"success": False, "message": "New URL is required"})
354
+
355
+ # Update in SQLite DB
356
+ if not update_url_in_sqlite(old, new):
357
+ return jsonify({"success": False, "message": "URL not found or new URL already exists"})
358
+
359
+ # Also update JSON file for backward compatibility
360
+ data = load_json()
361
+ try:
362
+ idx = data.index(old)
363
+ data[idx] = new
364
+ save_json(data)
365
+ except ValueError:
366
+ # If URL not in JSON, add it
367
+ data.append(new)
368
+ save_json(data)
369
+
370
+ return jsonify({"success": True, "message": "URL updated successfully"})
371
+
372
+ @app.route('/api/url/delete', methods=['POST'])
373
+ def delete_url():
374
+ url = request.form.get('url', '')
375
+
376
+ # Delete from SQLite DB
377
+ if not delete_url_from_sqlite(url):
378
+ return jsonify({"success": False, "message": "URL not found"})
379
+
380
+ # Also update JSON file for backward compatibility
381
+ data = load_json()
382
+ try:
383
+ data.remove(url)
384
+ save_json(data)
385
+ except ValueError:
386
+ pass
387
+
388
+ return jsonify({"success": True, "message": "URL deleted successfully"})
389
+
390
+ # ────────────────────────── 6. MAIN ROUTES ──────────────────────────
391
+ @app.route('/')
392
+ def home():
393
+ os.makedirs('templates', exist_ok=True)
394
+
395
+ with open('templates/index.html', 'w', encoding='utf-8') as fp:
396
+ fp.write(r'''<!DOCTYPE html>
397
+ <html>
398
+ <head>
399
+ <meta charset="utf-8">
400
+ <meta name="viewport" content="width=device-width, initial-scale=1">
401
+ <title>Web Gallery</title>
402
+ <style>
403
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;600&display=swap');
404
+ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
405
+ .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
406
+ .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
407
+ .tab.active{background:#a78bfa;color:#1a202c;}
408
+ .tab.manage{background:#ff6e91;color:white;}
409
+ .tab.manage.active{background:#ff2d62;color:white;}
410
+ /* Updated grid to show 2x2 layout */
411
+ .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;padding:0 16px 60px;max-width:1200px;margin:0 auto;}
412
+ @media(max-width:800px){.grid{grid-template-columns:1fr;}}
413
+ /* Increased card height for larger display */
414
+ .card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:540px;display:flex;flex-direction:column;position:relative;}
415
+ .frame{flex:1;position:relative;overflow:hidden;}
416
+ .frame iframe{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0;}
417
+ .frame img{width:100%;height:100%;object-fit:cover;}
418
+ .card-label{position:absolute;top:10px;left:10px;padding:4px 8px;border-radius:4px;font-size:11px;font-weight:bold;z-index:100;text-transform:uppercase;letter-spacing:0.5px;box-shadow:0 2px 4px rgba(0,0,0,0.2);}
419
+ .label-live{background:linear-gradient(135deg, #00c6ff, #0072ff);color:white;}
420
+ .label-static{background:linear-gradient(135deg, #ff9a9e, #fad0c4);color:#333;}
421
+ .foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee;}
422
+ .foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none;}
423
+ .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
424
+ .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
425
+ .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
426
+ .manage-panel{background:white;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);margin:16px;padding:20px;}
427
+ .form-group{margin-bottom:15px;}
428
+ .form-group label{display:block;margin-bottom:5px;font-weight:600;}
429
+ .form-control{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;}
430
+ .btn{padding:8px 15px;border:none;border-radius:4px;cursor:pointer;font-weight:600;}
431
+ .btn-primary{background:#4a6dd8;color:white;}
432
+ .btn-danger{background:#e53e3e;color:white;}
433
+ .btn-success{background:#38a169;color:white;}
434
+ .status{padding:10px;margin:10px 0;border-radius:4px;display:none;}
435
+ .status.success{display:block;background:#c6f6d5;color:#22543d;}
436
+ .status.error{display:block;background:#fed7d7;color:#822727;}
437
+ .url-list{margin:20px 0;border:1px solid #eee;border-radius:4px;max-height:300px;overflow-y:auto;}
438
+ .url-item{padding:10px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;}
439
+ .url-item:last-child{border-bottom:none;}
440
+ .url-controls{display:flex;gap:5px;}
441
+ </style>
442
+ </head>
443
+ <body>
444
+ <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
445
+ <h1 style="margin-bottom: 10px;">🌟 Web Gallery Manager</h1>
446
+ <p class="description" style="margin-bottom: 15px;">
447
+ 🚀 <strong>Free AI Spaces & Website Gallery</strong> ✨ Save and manage your favorite sites! Supports <span style="background: linear-gradient(135deg, #00c6ff, #0072ff); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px;">LIVE</span> and <span style="background: linear-gradient(135deg, #ff9a9e, #fad0c4); color: #333; padding: 2px 6px; border-radius: 4px; font-size: 12px;">Static</span> preview modes.
448
+ </p>
449
+ <p style="font-size: 0.8rem; color: #888; margin-top: 10px;">
450
+ Your desired URL will be permanently saved when recorded in the <code>favorite_sites.json</code> file.
451
+ </p>
452
+ <p>
453
+ <a href="https://discord.gg/openfreeai" target="_blank"><img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="badge"></a>
454
+ </p>
455
+ </header>
456
+ <div class="tabs" id="tabs"></div>
457
+ <div id="content"></div>
458
+
459
+ <script>
460
+ // Basic configuration
461
+ const cats = {{cats|tojson}};
462
+ const tabs = document.getElementById('tabs');
463
+ const content = document.getElementById('content');
464
+ let active = "";
465
+ let currentPage = 1;
466
+
467
+ // Simple utility functions
468
+ function loadHTML(url, callback) {
469
+ const xhr = new XMLHttpRequest();
470
+ xhr.open('GET', url, true);
471
+ xhr.onreadystatechange = function() {
472
+ if (xhr.readyState === 4 && xhr.status === 200) {
473
+ callback(xhr.responseText);
474
+ }
475
+ };
476
+ xhr.send();
477
  }
478
+
479
+ function makeRequest(url, method, data, callback) {
480
+ const xhr = new XMLHttpRequest();
481
+ xhr.open(method, url, true);
482
+ xhr.onreadystatechange = function() {
483
+ if (xhr.readyState === 4 && xhr.status === 200) {
484
+ callback(JSON.parse(xhr.responseText));
485
+ }
486
+ };
487
+ if (method === 'POST') {
488
+ xhr.send(data);
489
+ } else {
490
+ xhr.send();
491
+ }
492
+ }
493
+
494
+ function updateTabs() {
495
+ Array.from(tabs.children).forEach(b => {
496
+ b.classList.toggle('active', b.dataset.c === active);
497
+ });
498
+ }
499
+
500
+ // Tab handlers
501
+ function loadCategory(cat, page) {
502
+ if(cat === active && currentPage === page) return;
503
+ active = cat;
504
+ currentPage = page || 1;
505
+ updateTabs();
506
+
507
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loading…</p>';
508
+
509
+ makeRequest('/api/category?name=' + encodeURIComponent(cat) + '&page=' + currentPage + '&per_page=4', 'GET', null, function(data) {
510
+ let html = '<div class="grid">';
511
+
512
+ if(data.items.length === 0) {
513
+ html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No items in this category.</p>';
514
+ } else {
515
+ data.items.forEach(item => {
516
+ html += `
517
+ <div class="card">
518
+ <div class="card-label label-live">LIVE</div>
519
+ <div class="frame">
520
+ <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
521
+ </div>
522
+ <div class="foot">
523
+ <a href="${item.hf}" target="_blank">${item.title}</a>
524
+ </div>
525
+ </div>
526
+ `;
527
+ });
528
+ }
529
+
530
+ html += '</div>';
531
+
532
+ // Add pagination
533
+ html += `
534
+ <div class="pagination">
535
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadCategory('${cat}', ${currentPage-1})">« Previous</button>
536
+ <span>Page ${currentPage} of ${data.total_pages}</span>
537
+ <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadCategory('${cat}', ${currentPage+1})">Next »</button>
538
+ </div>
539
+ `;
540
+
541
+ content.innerHTML = html;
542
+ });
543
+ }
544
+
545
+ function loadFavorites(page) {
546
+ if(active === 'Favorites' && currentPage === page) return;
547
+ active = 'Favorites';
548
+ currentPage = page || 1;
549
+ updateTabs();
550
+
551
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loading…</p>';
552
+
553
+ makeRequest('/api/favorites?page=' + currentPage + '&per_page=4', 'GET', null, function(data) {
554
+ let html = '<div class="grid">';
555
+
556
+ if(data.items.length === 0) {
557
+ html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No favorites saved yet.</p>';
558
+ } else {
559
+ data.items.forEach(item => {
560
+ if(item.mode === 'snapshot') {
561
+ html += `
562
+ <div class="card">
563
+ <div class="card-label label-static">Static</div>
564
+ <div class="frame">
565
+ <img src="${item.preview_url}" loading="lazy">
566
+ </div>
567
+ <div class="foot">
568
+ <a href="${item.url}" target="_blank">${item.title}</a>
569
+ </div>
570
+ </div>
571
+ `;
572
+ } else {
573
+ html += `
574
+ <div class="card">
575
+ <div class="card-label label-live">LIVE</div>
576
+ <div class="frame">
577
+ <iframe src="${item.preview_url}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
578
+ </div>
579
+ <div class="foot">
580
+ <a href="${item.url}" target="_blank">${item.title}</a>
581
+ </div>
582
+ </div>
583
+ `;
584
+ }
585
+ });
586
+ }
587
+
588
+ html += '</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
 
590
+ // Add pagination
591
+ html += `
592
+ <div class="pagination">
593
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadFavorites(${currentPage-1})">« Previous</button>
594
+ <span>Page ${currentPage} of ${data.total_pages}</span>
595
+ <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadFavorites(${currentPage+1})">Next »</button>
596
+ </div>
597
+ `;
598
+
599
+ content.innerHTML = html;
600
+ });
601
+ }
602
+
603
+ function loadManage() {
604
+ if(active === 'Manage') return;
605
+ active = 'Manage';
606
+ updateTabs();
607
+
608
+ content.innerHTML = `
609
+ <div class="manage-panel">
610
+ <h2>Add New URL</h2>
611
+ <div class="form-group">
612
+ <label for="new-url">URL</label>
613
+ <input type="text" id="new-url" class="form-control" placeholder="https://example.com">
614
+ </div>
615
+ <button onclick="addUrl()" class="btn btn-primary">Add URL</button>
616
+ <div id="add-status" class="status"></div>
617
+
618
+ <h2>Manage Saved URLs</h2>
619
+ <div id="url-list" class="url-list">Loading...</div>
620
+ </div>
621
+ `;
622
+
623
+ loadUrlList();
624
+ }
625
+
626
+ // URL management functions
627
+ function loadUrlList() {
628
+ makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
629
+ const urlList = document.getElementById('url-list');
630
+
631
+ if(data.items.length === 0) {
632
+ urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
633
+ return;
634
+ }
635
+
636
+ let html = '';
637
+ data.items.forEach(item => {
638
+ // Escape the URL to prevent JavaScript injection when used in onclick handlers
639
+ const escapedUrl = item.url.replace(/'/g, "\\'");
640
+
641
+ html += `
642
+ <div class="url-item">
643
+ <div>${item.url}</div>
644
+ <div class="url-controls">
645
+ <button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
646
+ <button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
647
+ </div>
648
+ </div>
649
+ `;
650
+ });
651
+
652
+ urlList.innerHTML = html;
653
+ });
654
+ }
655
+
656
+ function addUrl() {
657
+ const url = document.getElementById('new-url').value.trim();
658
+
659
+ if(!url) {
660
+ showStatus('add-status', 'Please enter a URL', false);
661
+ return;
662
+ }
663
+
664
+ const formData = new FormData();
665
+ formData.append('url', url);
666
+
667
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
668
+ showStatus('add-status', data.message, data.success);
669
+ if(data.success) {
670
+ document.getElementById('new-url').value = '';
671
+ loadUrlList();
672
+ // If currently in Favorites tab, reload to see changes immediately
673
+ if(active === 'Favorites') {
674
+ loadFavorites(currentPage);
675
+ }
676
+ }
677
+ });
678
+ }
679
+
680
+ function editUrl(url) {
681
+ // Decode URL if it was previously escaped
682
+ const decodedUrl = url.replace(/\\'/g, "'");
683
+ const newUrl = prompt('Edit URL:', decodedUrl);
684
+
685
+ if(!newUrl || newUrl === decodedUrl) return;
686
+
687
+ const formData = new FormData();
688
+ formData.append('old', decodedUrl);
689
+ formData.append('new', newUrl);
690
+
691
+ makeRequest('/api/url/update', 'POST', formData, function(data) {
692
+ if(data.success) {
693
+ loadUrlList();
694
+ // If currently in Favorites tab, reload to see changes immediately
695
+ if(active === 'Favorites') {
696
+ loadFavorites(currentPage);
697
+ }
698
+ } else {
699
+ alert(data.message);
700
+ }
701
+ });
702
+ }
703
 
704
+ function deleteUrl(url) {
705
+ // Decode URL if it was previously escaped
706
+ const decodedUrl = url.replace(/\\'/g, "'");
707
+ if(!confirm('Are you sure you want to delete this URL?')) return;
708
+
709
+ const formData = new FormData();
710
+ formData.append('url', decodedUrl);
711
+
712
+ makeRequest('/api/url/delete', 'POST', formData, function(data) {
713
+ if(data.success) {
714
+ loadUrlList();
715
+ // If currently in Favorites tab, reload to see changes immediately
716
+ if(active === 'Favorites') {
717
+ loadFavorites(currentPage);
718
+ }
719
+ } else {
720
+ alert(data.message);
721
+ }
722
+ });
723
+ }
724
+
725
+ function showStatus(id, message, success) {
726
+ const status = document.getElementById(id);
727
+ status.textContent = message;
728
+ status.className = success ? 'status success' : 'status error';
729
+ setTimeout(() => {
730
+ status.className = 'status';
731
+ }, 3000);
732
+ }
733
+
734
+ // Create tabs
735
+ // Favorites tab first
736
+ const favTab = document.createElement('button');
737
+ favTab.className = 'tab';
738
+ favTab.textContent = 'Favorites';
739
+ favTab.dataset.c = 'Favorites';
740
+ favTab.onclick = function() { loadFavorites(1); };
741
+ tabs.appendChild(favTab);
742
+
743
+ // Category tabs
744
+ cats.forEach(c => {
745
+ const b = document.createElement('button');
746
+ b.className = 'tab';
747
+ b.textContent = c;
748
+ b.dataset.c = c;
749
+ b.onclick = function() { loadCategory(c, 1); };
750
+ tabs.appendChild(b);
751
+ });
752
+
753
+ // Manage tab last
754
+ const manageTab = document.createElement('button');
755
+ manageTab.className = 'tab manage';
756
+ manageTab.textContent = 'Manage';
757
+ manageTab.dataset.c = 'Manage';
758
+ manageTab.onclick = function() { loadManage(); };
759
+ tabs.appendChild(manageTab);
760
+
761
+ // Start with Favorites tab
762
+ loadFavorites(1);
763
+ </script>
764
+ </body>
765
+ </html>''')
766
 
767
+ # Return the rendered template
768
+ return render_template('index.html', cats=list(CATEGORIES.keys()))
769
 
770
+ # Initialize database on startup
771
+ init_db()
772
+
773
+ # Define a function to ensure database consistency
774
+ def ensure_db_consistency():
775
+ # Make sure we have the latest data in both JSON and SQLite
776
+ urls = load_db_sqlite()
777
+ save_json(urls)
778
+
779
+ # For Flask 2.0+ compatibility
780
+ @app.before_request
781
+ def before_request_func():
782
+ # Use a flag to run this only once
783
+ if not hasattr(app, '_got_first_request'):
784
+ ensure_db_consistency()
785
+ app._got_first_request = True
786
+
787
+ if __name__ == '__main__':
788
+ # 앱 시작 전에 명시적으로 DB 초기화
789
+ print("Initializing database...")
790
+ init_db()
791
+
792
+ # 데이터베이스 파일 경로 및 존재 여부 확인
793
+ db_path = os.path.abspath(SQLITE_DB)
794
+ print(f"SQLite DB path: {db_path}")
795
+ if os.path.exists(SQLITE_DB):
796
+ print(f"Database file exists, size: {os.path.getsize(SQLITE_DB)} bytes")
797
+ else:
798
+ print("Warning: Database file does not exist after initialization!")
799
+
800
+ app.run(host='0.0.0.0', port=7860)