Opera8 commited on
Commit
a516620
·
verified ·
1 Parent(s): d583f76

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +478 -257
app.py CHANGED
@@ -1,69 +1,63 @@
1
  import os
2
- import subprocess
3
- import sys
4
- import io
5
  import gradio as gr
6
  import numpy as np
7
- import random
8
  import spaces
9
  import torch
10
- from diffusers import Flux2Pipeline, Flux2Transformer2DModel
11
- from diffusers import BitsAndBytesConfig as DiffBitsAndBytesConfig
12
- import requests
13
- from PIL import Image
14
- import json
15
  import base64
 
 
 
16
  from huggingface_hub import InferenceClient
 
 
 
 
17
 
18
- subprocess.check_call([sys.executable, "-m", "pip", "install", "spaces==0.43.0"])
19
-
20
- dtype = torch.bfloat16
21
- device = "cuda" if torch.cuda.is_available() else "cpu"
22
 
 
 
 
 
23
  MAX_SEED = np.iinfo(np.int32).max
24
  MAX_IMAGE_SIZE = 1024
 
25
 
26
- hf_client = InferenceClient(
27
- api_key=os.environ.get("HF_TOKEN"),
28
- )
29
- VLM_MODEL = "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT"
30
-
31
- SYSTEM_PROMPT_TEXT_ONLY = """You are an expert prompt engineer for FLUX.2 by Black Forest Labs. Rewrite user prompts to be more descriptive while strictly preserving their core subject and intent.
32
-
33
- Guidelines:
34
- 1. Structure: Keep structured inputs structured (enhance within fields). Convert natural language to detailed paragraphs.
35
- 2. Details: Add concrete visual specifics - form, scale, textures, materials, lighting (quality, direction, color), shadows, spatial relationships, and environmental context.
36
- 3. Text in Images: Put ALL text in quotation marks, matching the prompt's language. Always provide explicit quoted text for objects that would contain text in reality (signs, labels, screens, etc.) - without it, the model generates gibberish.
37
-
38
- Output only the revised prompt and nothing else."""
39
-
40
- SYSTEM_PROMPT_WITH_IMAGES = """You are FLUX.2 by Black Forest Labs, an image-editing expert. You convert editing requests into one concise instruction (50-80 words, ~30 for brief requests).
41
-
42
- Rules:
43
- - Single instruction only, no commentary
44
- - Use clear, analytical language (avoid "whimsical," "cascading," etc.)
45
- - Specify what changes AND what stays the same (face, lighting, composition)
46
- - Reference actual image elements
47
- - Turn negatives into positives ("don't change X" → "keep X")
48
- - Make abstractions concrete ("futuristic" → "glowing cyan neon, metallic panels")
49
- - Keep content PG-13
50
 
51
- Output only the final instruction in plain text and nothing else."""
 
 
52
 
53
- def remote_text_encoder(prompts):
54
- from gradio_client import Client
55
-
56
- client = Client("multimodalart/mistral-text-encoder")
57
- result = client.predict(
58
- prompt=prompts,
59
- api_name="/encode_text"
60
- )
61
-
62
- # Load returns a tensor, usually on CPU by default
63
- prompt_embeds = torch.load(result[0])
64
- return prompt_embeds
 
 
 
 
 
 
65
 
66
- # Load model
 
 
 
67
  repo_id = "black-forest-labs/FLUX.2-dev"
68
 
69
  dit = Flux2Transformer2DModel.from_pretrained(
@@ -80,94 +74,157 @@ pipe = Flux2Pipeline.from_pretrained(
80
  )
81
  pipe.to(device)
82
 
83
- # Pull pre-compiled Flux2 Transformer blocks from HF hub
84
  spaces.aoti_blocks_load(pipe.transformer, "zerogpu-aoti/FLUX.2", variant="fa3")
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  def image_to_data_uri(img):
87
  buffered = io.BytesIO()
88
  img.save(buffered, format="PNG")
89
  img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
90
  return f"data:image/png;base64,{img_str}"
91
 
 
 
 
 
 
 
92
  def upsample_prompt_logic(prompt, image_list):
93
  try:
94
  if image_list and len(image_list) > 0:
95
- # Image + Text Editing Mode
96
  system_content = SYSTEM_PROMPT_WITH_IMAGES
97
-
98
- # Construct user message with text and images
99
  user_content = [{"type": "text", "text": prompt}]
100
-
101
  for img in image_list:
102
  data_uri = image_to_data_uri(img)
103
- user_content.append({
104
- "type": "image_url",
105
- "image_url": {"url": data_uri}
106
- })
107
-
108
- messages = [
109
- {"role": "system", "content": system_content},
110
- {"role": "user", "content": user_content}
111
- ]
112
  else:
113
- # Text Only Mode
114
  system_content = SYSTEM_PROMPT_TEXT_ONLY
115
- messages = [
116
- {"role": "system", "content": system_content},
117
- {"role": "user", "content": prompt}
118
- ]
119
-
120
- completion = hf_client.chat.completions.create(
121
- model=VLM_MODEL,
122
- messages=messages,
123
- max_tokens=1024
124
- )
125
 
 
126
  return completion.choices[0].message.content
127
  except Exception as e:
128
  print(f"Upsampling failed: {e}")
129
  return prompt
130
 
131
- def update_dimensions_from_image(image_list):
132
- """Update width/height sliders based on uploaded image aspect ratio.
133
- Keeps one side at 1024 and scales the other proportionally, with both sides as multiples of 8."""
134
- if image_list is None or len(image_list) == 0:
135
- return 1024, 1024 # Default dimensions
136
-
137
- # Get the first image to determine dimensions
138
- img = image_list[0][0] # Gallery returns list of tuples (image, caption)
139
- img_width, img_height = img.size
140
-
141
- aspect_ratio = img_width / img_height
142
-
143
- if aspect_ratio >= 1: # Landscape or square
144
- new_width = 1024
145
- new_height = int(1024 / aspect_ratio)
146
- else: # Portrait
147
- new_height = 1024
148
- new_width = int(1024 * aspect_ratio)
149
-
150
- # Round to nearest multiple of 8
151
- new_width = round(new_width / 8) * 8
152
- new_height = round(new_height / 8) * 8
153
-
154
- # Ensure within valid range (minimum 256, maximum 1024)
155
- new_width = max(256, min(1024, new_width))
156
- new_height = max(256, min(1024, new_height))
157
-
158
- return new_width, new_height
159
-
160
- # Updated duration function to match generate_image arguments (including progress)
161
  def get_duration(prompt_embeds, image_list, width, height, num_inference_steps, guidance_scale, seed, progress=gr.Progress(track_tqdm=True)):
162
  num_images = 0 if image_list is None else len(image_list)
163
  step_duration = 1 + 0.8 * num_images
164
  return max(65, num_inference_steps * step_duration + 10)
165
 
 
 
 
 
166
  @spaces.GPU(duration=get_duration)
167
  def generate_image(prompt_embeds, image_list, width, height, num_inference_steps, guidance_scale, seed, progress=gr.Progress(track_tqdm=True)):
168
- # Move embeddings to GPU only when inside the GPU decorated function
169
  prompt_embeds = prompt_embeds.to(device)
170
-
171
  generator = torch.Generator(device=device).manual_seed(seed)
172
 
173
  pipe_kwargs = {
@@ -180,191 +237,355 @@ def generate_image(prompt_embeds, image_list, width, height, num_inference_steps
180
  "height": height,
181
  }
182
 
183
- # Progress bar for the actual generation steps
184
- if progress:
185
- progress(0, desc="Starting generation...")
186
-
187
  image = pipe(**pipe_kwargs).images[0]
188
  return image
189
 
190
- def infer(prompt, input_images=None, seed=42, randomize_seed=False, width=1024, height=1024, num_inference_steps=50, guidance_scale=2.5, prompt_upsampling=False, progress=gr.Progress(track_tqdm=True)):
191
-
192
- if randomize_seed:
193
- seed = random.randint(0, MAX_SEED)
194
-
195
- # Prepare image list (convert None or empty gallery to None)
 
 
 
 
 
 
 
 
196
  image_list = None
197
  if input_images is not None and len(input_images) > 0:
198
- image_list = []
199
- for item in input_images:
200
- image_list.append(item[0])
201
-
202
- # 1. Upsampling (Network bound - No GPU needed)
203
- final_prompt = prompt
204
- if prompt_upsampling:
205
- progress(0.05, desc="Upsampling prompt...")
206
- final_prompt = upsample_prompt_logic(prompt, image_list)
207
- print(f"Original Prompt: {prompt}")
208
- print(f"Upsampled Prompt: {final_prompt}")
209
-
210
- # 2. Text Encoding (Network bound - No GPU needed)
211
- progress(0.1, desc="Encoding prompt...")
212
- # This returns CPU tensors
213
- prompt_embeds = remote_text_encoder(final_prompt)
214
-
215
- # 3. Image Generation (GPU bound)
216
- progress(0.3, desc="Waiting for GPU...")
217
- image = generate_image(
218
- prompt_embeds,
219
- image_list,
220
- width,
221
- height,
222
- num_inference_steps,
223
- guidance_scale,
224
- seed,
225
- progress
226
- )
227
 
228
- return image, seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- examples = [
231
- ["Create a vase on a table in living room, the color of the vase is a gradient of color, starting with #02eb3c color and finishing with #edfa3c. The flowers inside the vase have the color #ff0088"],
232
- ["Photorealistic infographic showing the complete Berlin TV Tower (Fernsehturm) from ground base to antenna tip, full vertical view with entire structure visible including concrete shaft, metallic sphere, and antenna spire. Slight upward perspective angle looking up toward the iconic sphere, perfectly centered on clean white background. Left side labels with thin horizontal connector lines: the text '368m' in extra large bold dark grey numerals (#2D3748) positioned at exactly the antenna tip with 'TOTAL HEIGHT' in small caps below. The text '207m' in extra large bold with 'TELECAFÉ' in small caps below, with connector line touching the sphere precisely at the window level. Right side label with horizontal connector line touching the sphere's equator: the text '32m' in extra large bold dark grey numerals with 'SPHERE DIAMETER' in small caps below. Bottom section arranged in three balanced columns: Left - Large text '986' in extra bold dark grey with 'STEPS' in caps below. Center - 'BERLIN TV TOWER' in bold caps with 'FERNSEHTURM' in lighter weight below. Right - 'INAUGURATED' in bold caps with 'OCTOBER 3, 1969' below. All typography in modern sans-serif font (such as Inter or Helvetica), color #2D3748, clean minimal technical diagram style. Horizontal connector lines are thin, precise, and clearly visible, touching the tower structure at exact corresponding measurement points. Professional architectural elevation drawing aesthetic with dynamic low angle perspective creating sense of height and grandeur, poster-ready infographic design with perfect visual hierarchy."],
233
- ["Soaking wet capybara taking shelter under a banana leaf in the rainy jungle, close up photo"],
234
- ["A kawaii die-cut sticker of a chubby orange cat, featuring big sparkly eyes and a happy smile with paws raised in greeting and a heart-shaped pink nose. The design should have smooth rounded lines with black outlines and soft gradient shading with pink cheeks."],
235
- ]
236
 
237
- examples_images = [
238
- # ["Replace the top of the person from image 1 with the one from image 2", ["person1.webp", "woman2.webp"]],
239
- ["The person from image 1 is petting the cat from image 2, the bird from image 3 is next to them", ["woman1.webp", "cat_window.webp", "bird.webp"]]
240
- ]
 
 
 
241
 
242
- css="""
243
- #col-container {
244
- margin: 0 auto;
245
- max-width: 1200px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  }
247
- .gallery-container img{
248
- object-fit: contain;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  """
251
 
252
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
253
 
254
  with gr.Column(elem_id="col-container"):
255
- gr.Markdown(f"""# FLUX.2 [dev]
256
- FLUX.2 [dev] is a 32B model rectified flow capable of generating, editing and combining images based on text instructions model [[model](https://huggingface.co/black-forest-labs/FLUX.2-dev)], [[blog](https://bfl.ai/blog/flux-2)]
257
- """)
 
258
  with gr.Row():
259
  with gr.Column():
260
  with gr.Row():
261
  prompt = gr.Text(
262
- label="Prompt",
263
- show_label=False,
264
- max_lines=2,
265
- placeholder="Enter your prompt",
266
- container=False,
267
- scale=3
268
  )
269
-
270
- run_button = gr.Button("Run", scale=1)
271
-
272
- with gr.Accordion("Input image(s) (optional)", open=True):
273
  input_images = gr.Gallery(
274
- label="Input Image(s)",
275
  type="pil",
276
  columns=3,
277
  rows=1,
 
278
  )
279
 
280
- with gr.Accordion("Advanced Settings", open=False):
281
- prompt_upsampling = gr.Checkbox(
282
- label="Prompt Upsampling",
283
- value=True,
284
- info="Automatically enhance the prompt using a VLM"
285
- )
286
-
287
- seed = gr.Slider(
288
- label="Seed",
289
- minimum=0,
290
- maximum=MAX_SEED,
291
- step=1,
292
- value=0,
293
- )
294
-
295
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
296
-
297
- with gr.Row():
298
-
299
- width = gr.Slider(
300
- label="Width",
301
- minimum=256,
302
- maximum=MAX_IMAGE_SIZE,
303
- step=8,
304
- value=1024,
305
- )
306
-
307
- height = gr.Slider(
308
- label="Height",
309
- minimum=256,
310
- maximum=MAX_IMAGE_SIZE,
311
- step=8,
312
- value=1024,
313
- )
314
-
315
- with gr.Row():
316
-
317
- num_inference_steps = gr.Slider(
318
- label="Number of inference steps",
319
- minimum=1,
320
- maximum=100,
321
- step=1,
322
- value=30,
323
- )
324
-
325
- guidance_scale = gr.Slider(
326
- label="Guidance scale",
327
- minimum=0.0,
328
- maximum=10.0,
329
- step=0.1,
330
- value=4,
331
- )
332
 
 
 
333
 
334
- with gr.Column():
335
- result = gr.Image(label="Result", show_label=False)
336
-
337
-
338
- gr.Examples(
339
- examples=examples,
340
- fn=infer,
341
- inputs=[prompt],
342
- outputs=[result, seed],
343
- cache_examples=True,
344
- cache_mode="lazy"
345
- )
346
 
347
- gr.Examples(
348
- examples=examples_images,
349
- fn=infer,
350
- inputs=[prompt, input_images],
351
- outputs=[result, seed],
352
- cache_examples=True,
353
- cache_mode="lazy"
354
- )
355
 
356
- # Auto-update dimensions when images are uploaded
 
 
357
  input_images.upload(
358
  fn=update_dimensions_from_image,
359
  inputs=[input_images],
360
  outputs=[width, height]
361
  )
362
 
363
- gr.on(
364
- triggers=[run_button.click, prompt.submit],
 
 
 
 
 
 
 
365
  fn=infer,
366
- inputs=[prompt, input_images, seed, randomize_seed, width, height, num_inference_steps, guidance_scale, prompt_upsampling],
367
- outputs=[result, seed]
 
 
 
 
368
  )
369
 
370
- demo.launch(css=css)
 
 
 
 
 
 
1
  import os
 
 
 
2
  import gradio as gr
3
  import numpy as np
 
4
  import spaces
5
  import torch
6
+ import random
7
+ import io
 
 
 
8
  import base64
9
+ import json
10
+ from PIL import Image
11
+ from gradio_client import Client
12
  from huggingface_hub import InferenceClient
13
+ from deep_translator import GoogleTranslator
14
+ from transformers import pipeline
15
+ from diffusers import Flux2Pipeline, Flux2Transformer2DModel
16
+ from datetime import date
17
 
18
+ # ==========================================
19
+ # 1. تنظیمات و پیکربندی سیستم (Configuration)
20
+ # ==========================================
 
21
 
22
+ # رنگ‌ها و تنظیمات ظاهری
23
+ USAGE_LIMIT = 5
24
+ DATA_FILE = "usage_data.json"
25
+ PREMIUM_PAGE_ID = '1149636'
26
  MAX_SEED = np.iinfo(np.int32).max
27
  MAX_IMAGE_SIZE = 1024
28
+ device = "cuda" if torch.cuda.is_available() else "cpu"
29
 
30
+ # بارگذاری مدل تشخیص محتوای نامناسب (Safety Checker)
31
+ print("Loading Safety Checker...")
32
+ safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # کلاینت‌های هوش مصنوعی
35
+ hf_client = InferenceClient(api_key=os.environ.get("HF_TOKEN"))
36
+ VLM_MODEL = "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT"
37
 
38
+ # پرامپت‌های سیستمی برای بهبود متن
39
+ SYSTEM_PROMPT_TEXT_ONLY = """You are an expert prompt engineer for FLUX.2. Rewrite user prompts to be more descriptive while strictly preserving their core subject and intent. Add concrete visual specifics."""
40
+ SYSTEM_PROMPT_WITH_IMAGES = """You are FLUX.2 image-editing expert. Convert editing requests into one concise instruction (50-80 words)."""
41
+
42
+ # لیست کلمات ممنوعه (Strict Mode)
43
+ BANNED_WORDS = [
44
+ "nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "adult",
45
+ "explicit", "uncensored", "sexual", "lewd", "sensual", "lust", "horny",
46
+ "breast", "breasts", "nipple", "nipples", "vagina", "pussy", "cunt",
47
+ "penis", "dick", "cock", "genital", "genitals", "groin", "pubic",
48
+ "ass", "butt", "buttocks", "anus", "anal", "rectum",
49
+ "intercourse", "masturbation", "orgasm", "blowjob", "bj", "cum", "sperm",
50
+ "ejaculation", "penetration", "fucking", "sucking", "licking",
51
+ "lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "thong",
52
+ "topless", "bottomless", "undressed", "unclothed", "skimpy", "transparent",
53
+ "fetish", "bdsm", "bondage", "latex", "hentai", "ecchi", "ahegao",
54
+ "gore", "bloody", "blood", "kill", "murder", "dead", "torture", "abuse"
55
+ ]
56
 
57
+ # ==========================================
58
+ # 2. بارگذاری مدل FLUX.2
59
+ # ==========================================
60
+ print("Loading FLUX.2 Pipeline...")
61
  repo_id = "black-forest-labs/FLUX.2-dev"
62
 
63
  dit = Flux2Transformer2DModel.from_pretrained(
 
74
  )
75
  pipe.to(device)
76
 
77
+ # بهینه‌سازی ZeroGPU
78
  spaces.aoti_blocks_load(pipe.transformer, "zerogpu-aoti/FLUX.2", variant="fa3")
79
 
80
+ # ==========================================
81
+ # 3. توابع کمکی (Helpers)
82
+ # ==========================================
83
+
84
+ def load_usage_data():
85
+ if os.path.exists(DATA_FILE):
86
+ try:
87
+ with open(DATA_FILE, 'r') as f:
88
+ return json.load(f)
89
+ except:
90
+ return {}
91
+ return {}
92
+
93
+ def save_usage_data(data):
94
+ try:
95
+ with open(DATA_FILE, 'w') as f:
96
+ json.dump(data, f)
97
+ except Exception as e:
98
+ print(f"Error saving data: {e}")
99
+
100
+ usage_data_cache = load_usage_data()
101
+
102
+ def is_image_nsfw(image):
103
+ if image is None: return False
104
+ try:
105
+ # اگر ورودی لیست گالری باشد، اولین تصویر را چک کن
106
+ img_to_check = image
107
+ if isinstance(image, list):
108
+ # هندل کردن فرمت گالری گرادیو
109
+ if len(image) > 0:
110
+ img_to_check = image[0][0] if isinstance(image[0], tuple) else image[0]
111
+ else:
112
+ return False
113
+
114
+ results = safety_classifier(img_to_check)
115
+ for result in results:
116
+ if result['label'] == 'nsfw' and result['score'] > 0.75:
117
+ return True
118
+ return False
119
+ except Exception as e:
120
+ print(f"Safety check error: {e}")
121
+ return False
122
+
123
+ def check_text_safety(text):
124
+ if not text: return True
125
+ text_lower = text.lower()
126
+ padded_text = f" {text_lower} "
127
+ for char in [".", ",", "!", "?", "-", "_", "(", ")", "[", "]", "{", "}"]:
128
+ padded_text = padded_text.replace(char, " ")
129
+
130
+ for word in BANNED_WORDS:
131
+ if f" {word} " in padded_text:
132
+ return False
133
+ return True
134
+
135
+ def translate_prompt(text):
136
+ if not text: return ""
137
+ try:
138
+ translated = GoogleTranslator(source='auto', target='en').translate(text)
139
+ return translated
140
+ except Exception as e:
141
+ print(f"Translation Error: {e}")
142
+ return text
143
+
144
+ def get_error_html(message):
145
+ return f"""<div style="background-color: #fee2e2; border: 1px solid #ef4444; color: #b91c1c; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">⛔</span>{message}</div>"""
146
+
147
+ def get_success_html(message):
148
+ return f"""<div style="background-color: #dcfce7; border: 1px solid #22c55e; color: #15803d; padding: 12px; border-radius: 8px; text-align: center; margin-bottom: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 8px;"><span style="font-size: 1.2em;">✅</span>{message}</div>"""
149
+
150
+ def get_quota_exceeded_html():
151
+ return """<div style="background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border: 2px solid #f59e0b; padding: 20px; border-radius: 16px; text-align: center; box-shadow: 0 4px 15px rgba(245, 158, 11, 0.1);"><div style="font-size: 3rem; margin-bottom: 10px;">💎</div><h3 style="color: #92400e; margin: 0 0 10px 0; font-weight: 800;">اعتبار رایگان امروز تمام شد</h3><p style="color: #b45309; margin: 0; font-size: 0.95em;">شما از ۵ تصویر رایگان امروز استفاده کرده‌اید.<br>برای ساخت تصاویر نامحدود و حرفه‌ای، لطفا نسخه خود را ارتقا دهید.</p></div>"""
152
+
153
+ def get_user_record(fingerprint):
154
+ global usage_data_cache
155
+ if not fingerprint: return None
156
+ usage_data_cache = load_usage_data()
157
+ today_str = date.today().isoformat()
158
+ user_record = usage_data_cache.get(fingerprint)
159
+ if not user_record or user_record.get("last_reset") != today_str:
160
+ return {"count": 0, "last_reset": today_str}
161
+ return user_record
162
+
163
+ def consume_quota(fingerprint):
164
+ global usage_data_cache
165
+ today_str = date.today().isoformat()
166
+ usage_data_cache = load_usage_data()
167
+ user_record = usage_data_cache.get(fingerprint)
168
+ if not user_record or user_record.get("last_reset") != today_str:
169
+ user_record = {"count": 0, "last_reset": today_str}
170
+ user_record["count"] += 1
171
+ usage_data_cache[fingerprint] = user_record
172
+ save_usage_data(usage_data_cache)
173
+ return user_record["count"]
174
+
175
+ def check_initial_quota(fingerprint, subscription_status):
176
+ if not fingerprint: return gr.update(visible=True), gr.update(visible=False), None
177
+ if subscription_status == 'paid': return gr.update(visible=True), gr.update(visible=False), None
178
+ user_record = get_user_record(fingerprint)
179
+ current_usage = user_record["count"] if user_record else 0
180
+ if current_usage >= USAGE_LIMIT:
181
+ return gr.update(visible=False), gr.update(visible=True), get_quota_exceeded_html()
182
+ else:
183
+ return gr.update(visible=True), gr.update(visible=False), None
184
+
185
  def image_to_data_uri(img):
186
  buffered = io.BytesIO()
187
  img.save(buffered, format="PNG")
188
  img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
189
  return f"data:image/png;base64,{img_str}"
190
 
191
+ def remote_text_encoder(prompts):
192
+ client = Client("multimodalart/mistral-text-encoder")
193
+ result = client.predict(prompt=prompts, api_name="/encode_text")
194
+ prompt_embeds = torch.load(result[0])
195
+ return prompt_embeds
196
+
197
  def upsample_prompt_logic(prompt, image_list):
198
  try:
199
  if image_list and len(image_list) > 0:
 
200
  system_content = SYSTEM_PROMPT_WITH_IMAGES
 
 
201
  user_content = [{"type": "text", "text": prompt}]
 
202
  for img in image_list:
203
  data_uri = image_to_data_uri(img)
204
+ user_content.append({"type": "image_url", "image_url": {"url": data_uri}})
205
+ messages = [{"role": "system", "content": system_content}, {"role": "user", "content": user_content}]
 
 
 
 
 
 
 
206
  else:
 
207
  system_content = SYSTEM_PROMPT_TEXT_ONLY
208
+ messages = [{"role": "system", "content": system_content}, {"role": "user", "content": prompt}]
 
 
 
 
 
 
 
 
 
209
 
210
+ completion = hf_client.chat.completions.create(model=VLM_MODEL, messages=messages, max_tokens=1024)
211
  return completion.choices[0].message.content
212
  except Exception as e:
213
  print(f"Upsampling failed: {e}")
214
  return prompt
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  def get_duration(prompt_embeds, image_list, width, height, num_inference_steps, guidance_scale, seed, progress=gr.Progress(track_tqdm=True)):
217
  num_images = 0 if image_list is None else len(image_list)
218
  step_duration = 1 + 0.8 * num_images
219
  return max(65, num_inference_steps * step_duration + 10)
220
 
221
+ # ==========================================
222
+ # 4. تابع اصلی GPU (Inference)
223
+ # ==========================================
224
+
225
  @spaces.GPU(duration=get_duration)
226
  def generate_image(prompt_embeds, image_list, width, height, num_inference_steps, guidance_scale, seed, progress=gr.Progress(track_tqdm=True)):
 
227
  prompt_embeds = prompt_embeds.to(device)
 
228
  generator = torch.Generator(device=device).manual_seed(seed)
229
 
230
  pipe_kwargs = {
 
237
  "height": height,
238
  }
239
 
240
+ if progress: progress(0, desc="Starting generation...")
 
 
 
241
  image = pipe(**pipe_kwargs).images[0]
242
  return image
243
 
244
+ def infer(
245
+ prompt, input_images, seed, randomize_seed, width, height,
246
+ num_inference_steps, guidance_scale, prompt_upsampling,
247
+ fingerprint, subscription_status,
248
+ progress=gr.Progress(track_tqdm=True)
249
+ ):
250
+ # 1. بررسی اعتبار قبل از شروع
251
+ if subscription_status != 'paid':
252
+ user_record = get_user_record(fingerprint)
253
+ if user_record and user_record["count"] >= USAGE_LIMIT:
254
+ return None, seed, get_quota_exceeded_html(), gr.update(visible=False), gr.update(visible=True)
255
+
256
+ # 2. بررسی‌های ایمنی (Safety Checks)
257
+ # الف) بررسی تصویر ورودی
258
  image_list = None
259
  if input_images is not None and len(input_images) > 0:
260
+ image_list = [item[0] for item in input_images]
261
+ if is_image_nsfw(image_list):
262
+ return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است."), gr.update(visible=True), gr.update(visible=False)
263
+
264
+ # ب) ترجمه و بررسی متن
265
+ progress(0.1, desc="Translating...")
266
+ english_prompt = translate_prompt(prompt)
267
+ if not check_text_safety(english_prompt):
268
+ return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز است."), gr.update(visible=True), gr.update(visible=False)
269
+
270
+ # 3. کسر اعتبار (اگر کاربر رایگان است)
271
+ if subscription_status != 'paid':
272
+ consume_quota(fingerprint)
273
+
274
+ # 4. آماده‌سازی تنظیمات
275
+ if randomize_seed:
276
+ seed = random.randint(0, MAX_SEED)
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
+ try:
279
+ # Upsampling Prompt (Optional)
280
+ final_prompt = english_prompt
281
+ if prompt_upsampling:
282
+ progress(0.2, desc="Enhancing prompt...")
283
+ final_prompt = upsample_prompt_logic(english_prompt, image_list)
284
+
285
+ # Text Encoding (CPU/Network)
286
+ progress(0.3, desc="Encoding...")
287
+ prompt_embeds = remote_text_encoder(final_prompt)
288
+
289
+ # Generation (GPU)
290
+ progress(0.4, desc="Generating...")
291
+ result_image = generate_image(
292
+ prompt_embeds, image_list, width, height,
293
+ num_inference_steps, guidance_scale, seed, progress
294
+ )
295
+
296
+ # 5. بررسی تصویر خروجی
297
+ if is_image_nsfw(result_image):
298
+ return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود."), gr.update(visible=True), gr.update(visible=False)
299
+
300
+ # 6. محاسبه اعتبار باقی‌مانده
301
+ user_record = get_user_record(fingerprint)
302
+ remaining = USAGE_LIMIT - user_record["count"] if user_record else 0
303
+ success_msg = f"تصویر با موفقیت ساخته شد."
304
+ if subscription_status != 'paid':
305
+ success_msg += f" (اعتبار باقی‌مانده امروز: {remaining})"
306
+
307
+ btn_run_update = gr.update(visible=True)
308
+ btn_upg_update = gr.update(visible=False)
309
 
310
+ if subscription_status != 'paid' and remaining <= 0:
311
+ btn_run_update = gr.update(visible=False)
312
+ btn_upg_update = gr.update(visible=True)
 
 
 
313
 
314
+ return result_image, seed, get_success_html(success_msg), btn_run_update, btn_upg_update
315
+
316
+ except Exception as e:
317
+ error_str = str(e)
318
+ if "quota" in error_str.lower() or "exceeded" in error_str.lower():
319
+ raise e # Raise to be caught by JS
320
+ return None, seed, get_error_html(f"خطا در پردازش: {error_str}"), gr.update(visible=True), gr.update(visible=False)
321
 
322
+
323
+ def update_dimensions_from_image(image_list):
324
+ if image_list is None or len(image_list) == 0:
325
+ return 1024, 1024
326
+ img = image_list[0][0]
327
+ img_width, img_height = img.size
328
+ aspect_ratio = img_width / img_height
329
+ if aspect_ratio >= 1:
330
+ new_width = 1024
331
+ new_height = int(1024 / aspect_ratio)
332
+ else:
333
+ new_height = 1024
334
+ new_width = int(1024 * aspect_ratio)
335
+ new_width = round(new_width / 8) * 8
336
+ new_height = round(new_height / 8) * 8
337
+ return max(256, min(1024, new_width)), max(256, min(1024, new_height))
338
+
339
+ # ==========================================
340
+ # 5. جاوااسکریپت و CSS (UI/UX)
341
+ # ==========================================
342
+
343
+ js_download_func = """
344
+ async (image) => {
345
+ if (!image) { alert("لطفاً ابتدا تصویر را تولید کنید."); return; }
346
+ let fileUrl = image.url;
347
+ if (fileUrl && !fileUrl.startsWith('http')) { fileUrl = window.location.origin + fileUrl; }
348
+ else if (!fileUrl && image.path) { fileUrl = window.location.origin + "/file=" + image.path; }
349
+ window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fileUrl }, '*');
350
  }
351
+ """
352
+
353
+ js_upgrade_func = """() => { window.parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); }"""
354
+
355
+ js_global_content = """
356
+ <script>
357
+ document.addEventListener('DOMContentLoaded', () => {
358
+ async function getBrowserFingerprint() {
359
+ const components = [navigator.userAgent, navigator.language, screen.colorDepth, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()];
360
+ try {
361
+ const canvas = document.createElement('canvas');
362
+ const ctx = canvas.getContext('2d');
363
+ ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic";
364
+ ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
365
+ ctx.fillStyle = "#069"; ctx.fillText("Alpha_Flux_FP_v1", 2, 15);
366
+ components.push(canvas.toDataURL());
367
+ } catch (e) { components.push("canvas-err"); }
368
+ const str = components.join('~~~');
369
+ let hash = 0;
370
+ for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; }
371
+ return 'fp_' + Math.abs(hash).toString(16);
372
+ }
373
+
374
+ function isUserPaid(userObject) {
375
+ const PREMIUM_PAGE_ID = '1149636';
376
+ if (userObject && userObject.isLogin && userObject.accessible_pages) {
377
+ if (Array.isArray(userObject.accessible_pages)) return userObject.accessible_pages.some(page => String(page) === String(PREMIUM_PAGE_ID));
378
+ }
379
+ return false;
380
+ }
381
+
382
+ function updateHiddenInputs(fingerprint, status) {
383
+ const fpInput = document.querySelector('#fingerprint_storage textarea');
384
+ const stInput = document.querySelector('#status_storage textarea');
385
+ if(fpInput && fingerprint && fpInput.value !== fingerprint) { fpInput.value = fingerprint; fpInput.dispatchEvent(new Event('input', { bubbles: true })); }
386
+ if(stInput && status && stInput.value !== status) { stInput.value = status; stInput.dispatchEvent(new Event('input', { bubbles: true })); }
387
+ }
388
+
389
+ function updateSubscriptionBadge(status) {
390
+ const badge = document.getElementById('user-sub-badge');
391
+ if (!badge) return;
392
+ if (status === 'paid') {
393
+ badge.innerHTML = '✨ اشتراک: <span style="color: #FFD700; font-weight: bold;">نامحدود (PRO)</span>';
394
+ badge.style.background = 'linear-gradient(45deg, #1e3a8a, #3b82f6)';
395
+ } else {
396
+ badge.innerHTML = '👤 اشتراک: <span style="color: #fff; font-weight: bold;">رایگان (۵ اعتبار روزانه)</span>';
397
+ badge.style.background = 'linear-gradient(45deg, #4b5563, #6b7280)';
398
+ }
399
+ badge.style.display = 'inline-block';
400
+ }
401
+
402
+ async function initUserIdentity() {
403
+ window.userFingerprint = await getBrowserFingerprint();
404
+ window.userStatus = 'free';
405
+ window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
406
+ updateSubscriptionBadge('free');
407
+ updateHiddenInputs(window.userFingerprint, window.userStatus);
408
+ setInterval(() => { if(window.userFingerprint) updateHiddenInputs(window.userFingerprint, window.userStatus || 'free'); }, 1500);
409
+ }
410
+
411
+ window.addEventListener('message', (event) => {
412
+ if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
413
+ try {
414
+ const userObject = typeof event.data.payload === 'string' ? JSON.parse(event.data.payload) : event.data.payload;
415
+ const status = isUserPaid(userObject) ? 'paid' : 'free';
416
+ window.userStatus = status;
417
+ updateSubscriptionBadge(status);
418
+ updateHiddenInputs(window.userFingerprint, status);
419
+ } catch (e) { console.error(e); }
420
+ }
421
+ });
422
+
423
+ initUserIdentity();
424
+
425
+ // GPU Quota Modal
426
+ window.retryGeneration = function() { document.getElementById('custom-quota-modal')?.remove(); document.getElementById('run-btn')?.click(); };
427
+ window.closeErrorModal = function() { document.getElementById('custom-quota-modal')?.remove(); };
428
+
429
+ const showQuotaModal = () => {
430
+ if (document.getElementById('custom-quota-modal')) return;
431
+ const modalHtml = `
432
+ <div id="custom-quota-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 99999; display: flex; align-items: center; justify-content: center; font-family: 'Vazirmatn', sans-serif;">
433
+ <div class="ip-reset-guide-container">
434
+ <div class="guide-header">
435
+ <h2>یک قدم تا ساخت تصاویر جدید</h2>
436
+ </div>
437
+ <div class="guide-content">
438
+ <p>برای ادامه ساخت تصویر، لطفاً طبق آموزش زیر IP خود را تغییر دهید (اینترنت را خاموش/روشن کنید یا VPN را قطع کنید) و سپس دکمه تلاش مجدد را بزنید.</p>
439
+ <div class="video-button-container">
440
+ <button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635' }, '*')" class="elegant-video-button">
441
+ <span>دیدن ویدیو آموزشی</span>
442
+ </button>
443
+ </div>
444
+ </div>
445
+ <div class="guide-actions">
446
+ <button class="action-button back-button" onclick="window.closeErrorModal()">بازگشت</button>
447
+ <button class="action-button retry-button" onclick="window.retryGeneration()">تلاش مجدد</button>
448
+ </div>
449
+ </div>
450
+ </div>`;
451
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
452
+ setTimeout(window.closeErrorModal, 15000);
453
+ };
454
+
455
+ setInterval(() => {
456
+ const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap');
457
+ potentialErrors.forEach(el => {
458
+ const text = el.innerText || "";
459
+ if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
460
+ showQuotaModal();
461
+ el.style.display = 'none';
462
+ const parent = el.closest('.toast-wrap');
463
+ if(parent) parent.style.display = 'none';
464
+ }
465
+ });
466
+ }, 100);
467
+
468
+ const forceLight = () => {
469
+ document.body.classList.remove('dark');
470
+ document.body.style.backgroundColor = '#f5f7fa';
471
+ document.body.style.color = '#333333';
472
+ };
473
+ forceLight(); setInterval(forceLight, 1000);
474
+ });
475
+ </script>
476
+ """
477
+
478
+ css_code = """
479
+ <style>
480
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
481
+ :root, .dark, body, .gradio-container {
482
+ --body-background-fill: #f5f7fa !important;
483
+ --body-text-color: #1f2937 !important;
484
+ font-family: 'Vazirmatn', sans-serif !important;
485
  }
486
+ .ip-reset-guide-container { text-align: right; direction: rtl; background: white; padding: 20px; border-radius: 16px; width: 90%; max-width: 420px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); }
487
+ .elegant-video-button { background: #fff; color: #667eea; border: 1px solid #e2e8f0; padding: 10px 20px; border-radius: 50px; cursor: pointer; font-weight: bold; margin-top: 10px; }
488
+ .guide-actions { display: flex; gap: 10px; margin-top: 20px; }
489
+ .action-button { flex: 1; padding: 10px; border-radius: 12px; border: none; cursor: pointer; font-weight: bold; }
490
+ .retry-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
491
+ .back-button { background: white; border: 1px solid #e2e8f0; }
492
+
493
+ #col-container { max-width: 1200px; margin: 0 auto; direction: rtl; text-align: right; padding: 30px; background: white; border-radius: 24px; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08); }
494
+ #badge-container { text-align: center; margin-bottom: 20px; height: 30px; }
495
+ #user-sub-badge { padding: 6px 16px; border-radius: 20px; font-size: 0.9em; color: white; display: none; }
496
+ .primary-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: white !important; font-size: 1.1em !important; border-radius: 14px !important; margin-top: 15px; border: none !important; }
497
+ .upgrade-btn { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important; color: white !important; font-size: 1.1em !important; border-radius: 14px !important; margin-top: 15px; animation: pulse 2s infinite; border: none !important; }
498
+ @keyframes pulse { 0% { transform: scale(1); } 70% { transform: scale(1.02); } 100% { transform: scale(1); } }
499
+ footer { display: none !important; }
500
+ #fingerprint_storage, #status_storage { display: none !important; }
501
+ </style>
502
  """
503
 
504
+ # ==========================================
505
+ # 6. ساخت رابط کاربری (Gradio Blocks)
506
+ # ==========================================
507
+
508
+ with gr.Blocks(css=css_code) as demo:
509
+ gr.HTML(js_global_content + css_code)
510
+
511
+ fingerprint_box = gr.Textbox(elem_id="fingerprint_storage", visible=True)
512
+ status_box_input = gr.Textbox(elem_id="status_storage", visible=True)
513
 
514
  with gr.Column(elem_id="col-container"):
515
+ gr.Markdown("# **ساخت تصویر با FLUX.2 (پیشرفته)**", elem_id="main-title")
516
+ gr.Markdown("با استفاده از مدل قدرتمند FLUX.2 متن فارسی خود را به تصاویر شگفت‌انگیز تبدیل کنید.", elem_id="main-description")
517
+ gr.HTML('<div id="badge-container"><span id="user-sub-badge"></span></div>')
518
+
519
  with gr.Row():
520
  with gr.Column():
521
  with gr.Row():
522
  prompt = gr.Text(
523
+ label="توصیف تصویر (به فارسی)",
524
+ show_label=True,
525
+ max_lines=3,
526
+ placeholder="یک منظره زیبا از...",
527
+ rtl=True
 
528
  )
529
+
530
+ with gr.Accordion("بارگذاری تصویر (اختیاری برای ویرایش/ایده)", open=False):
 
 
531
  input_images = gr.Gallery(
532
+ label="تصاویر ورودی",
533
  type="pil",
534
  columns=3,
535
  rows=1,
536
+ height=200
537
  )
538
 
539
+ status_box = gr.HTML(label="وضعیت")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ run_button = gr.Button("✨ ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn", visible=True)
542
+ upgrade_button = gr.Button("💎 خرید نسخه نامحدود", variant="primary", elem_classes="upgrade-btn", elem_id="upgrade-btn", visible=False)
543
 
544
+ with gr.Accordion("تنظیمات پیشرفته", open=False):
545
+ prompt_upsampling = gr.Checkbox(label="بهبود خودکار پرامپت (هوشمند)", value=True)
546
+ seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
547
+ randomize_seed = gr.Checkbox(label="Seed تصادفی", value=True)
548
+ with gr.Row():
549
+ width = gr.Slider(label="عرض (Width)", minimum=256, maximum=MAX_IMAGE_SIZE, step=8, value=1024)
550
+ height = gr.Slider(label="ارتفاع (Height)", minimum=256, maximum=MAX_IMAGE_SIZE, step=8, value=1024)
551
+ with gr.Row():
552
+ num_inference_steps = gr.Slider(label="تعداد مراحل (Steps)", minimum=1, maximum=50, step=1, value=28)
553
+ guidance_scale = gr.Slider(label="میزان وفاداری (Guidance)", minimum=1.0, maximum=10.0, step=0.1, value=3.5)
 
 
554
 
555
+ with gr.Column():
556
+ result = gr.Image(label="تصویر نهایی", show_label=True, interactive=False)
557
+ download_button = gr.Button("📥 دانلود تصویر", variant="secondary", elem_id="download-btn")
 
 
 
 
 
558
 
559
+ # اتصال رویدادها
560
+
561
+ # 1. آپدیت ابعاد بر اساس تصویر آپلودی
562
  input_images.upload(
563
  fn=update_dimensions_from_image,
564
  inputs=[input_images],
565
  outputs=[width, height]
566
  )
567
 
568
+ # 2. بررسی اولیه اعتبار
569
+ fingerprint_box.change(
570
+ fn=check_initial_quota,
571
+ inputs=[fingerprint_box, status_box_input],
572
+ outputs=[run_button, upgrade_button, status_box]
573
+ )
574
+
575
+ # 3. اجرای مدل
576
+ run_button.click(
577
  fn=infer,
578
+ inputs=[
579
+ prompt, input_images, seed, randomize_seed, width, height,
580
+ num_inference_steps, guidance_scale, prompt_upsampling,
581
+ fingerprint_box, status_box_input
582
+ ],
583
+ outputs=[result, seed, status_box, run_button, upgrade_button]
584
  )
585
 
586
+ # 4. دکمه‌های دانلود و ارتقا
587
+ upgrade_button.click(fn=None, js=js_upgrade_func)
588
+ download_button.click(fn=None, inputs=[result], js=js_download_func)
589
+
590
+ if __name__ == "__main__":
591
+ demo.queue(max_size=30).launch(show_error=True)