Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,117 +4,53 @@ import numpy as np
|
|
| 4 |
import spaces
|
| 5 |
import torch
|
| 6 |
import random
|
| 7 |
-
from PIL import Image
|
| 8 |
from typing import Iterable
|
| 9 |
-
from gradio.themes
|
|
|
|
| 10 |
from deep_translator import GoogleTranslator
|
| 11 |
from transformers import pipeline
|
| 12 |
-
import datetime
|
| 13 |
-
import time
|
| 14 |
-
from collections import defaultdict
|
| 15 |
-
import threading
|
| 16 |
|
| 17 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
colors.steel_blue = colors.Color(
|
| 19 |
-
name="steel_blue",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
-
|
| 23 |
-
USER_DATA = {}
|
| 24 |
-
USER_LOCKS = defaultdict(threading.Lock)
|
| 25 |
-
DAILY_CREDIT_LIMIT = 5
|
| 26 |
-
|
| 27 |
-
def check_and_use_credit(fingerprint: str) -> bool:
|
| 28 |
-
"""اعتبار کاربر را بررسی و در صورت وجود، یکی کم میکند."""
|
| 29 |
-
if not fingerprint:
|
| 30 |
-
return False
|
| 31 |
-
with USER_LOCKS[fingerprint]:
|
| 32 |
-
today = datetime.date.today().isoformat()
|
| 33 |
-
user = USER_DATA.get(fingerprint, {'credits': DAILY_CREDIT_LIMIT, 'last_reset': today})
|
| 34 |
-
|
| 35 |
-
# اگر تاریخ گذشته بود، اعتبار را ریست کن
|
| 36 |
-
if user['last_reset'] != today:
|
| 37 |
-
user['credits'] = DAILY_CREDIT_LIMIT
|
| 38 |
-
user['last_reset'] = today
|
| 39 |
-
|
| 40 |
-
if user['credits'] > 0:
|
| 41 |
-
user['credits'] -= 1
|
| 42 |
-
USER_DATA[fingerprint] = user
|
| 43 |
-
print(f"Credit used for {fingerprint}. Remaining: {user['credits']}")
|
| 44 |
-
return True
|
| 45 |
-
else:
|
| 46 |
-
print(f"No credits left for {fingerprint}.")
|
| 47 |
-
return False
|
| 48 |
-
|
| 49 |
-
def get_user_status_api(fingerprint: str) -> dict:
|
| 50 |
-
"""وضعیت فعلی کاربر را برای نمایش در UI برمیگرداند."""
|
| 51 |
-
if not fingerprint:
|
| 52 |
-
return {'credits': 0, 'next_reset_timestamp': 0}
|
| 53 |
-
|
| 54 |
-
with USER_LOCKS[fingerprint]:
|
| 55 |
-
today = datetime.date.today()
|
| 56 |
-
user = USER_DATA.get(fingerprint, {'credits': DAILY_CREDIT_LIMIT, 'last_reset': today.isoformat()})
|
| 57 |
-
|
| 58 |
-
if user['last_reset'] != today.isoformat():
|
| 59 |
-
user['credits'] = DAILY_CREDIT_LIMIT
|
| 60 |
-
user['last_reset'] = today.isoformat()
|
| 61 |
-
USER_DATA[fingerprint] = user
|
| 62 |
-
|
| 63 |
-
tomorrow = today + datetime.timedelta(days=1)
|
| 64 |
-
next_reset_timestamp = int(time.mktime(tomorrow.timetuple()))
|
| 65 |
-
|
| 66 |
-
return {'credits': user['credits'], 'next_reset_timestamp': next_reset_timestamp}
|
| 67 |
-
|
| 68 |
|
| 69 |
-
# --- بارگذاری
|
| 70 |
-
print("Loading Safety
|
| 71 |
-
|
| 72 |
-
safety_classifier_2 = pipeline("image-classification", model="AdamCodd/vit-base-nsfw-detector", device=-1)
|
| 73 |
|
| 74 |
def is_image_nsfw(image):
|
| 75 |
if image is None: return False
|
| 76 |
try:
|
| 77 |
-
|
| 78 |
-
for result in
|
| 79 |
-
if result['label'] == 'nsfw' and result['score'] > 0.
|
| 80 |
-
print(f"Safety Check 1 Failed: {result['score']}")
|
| 81 |
-
return True
|
| 82 |
-
results2 = safety_classifier_2(image)
|
| 83 |
-
for result in results2:
|
| 84 |
-
label, score = result['label'].lower(), result['score']
|
| 85 |
-
if label == 'nsfw' and score > 0.3:
|
| 86 |
-
print(f"Safety Check 2 (NSFW) Failed: {score}")
|
| 87 |
-
return True
|
| 88 |
-
if label in ['sexy', 'porn', 'hentai'] and score > 0.4:
|
| 89 |
-
print(f"Safety Check 2 (Partial) Failed: {label} - {score}")
|
| 90 |
return True
|
| 91 |
return False
|
| 92 |
except Exception as e:
|
| 93 |
print(f"Safety check error: {e}")
|
| 94 |
-
return
|
| 95 |
-
|
| 96 |
-
BANNED_WORDS = [
|
| 97 |
-
"nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "uncensored", "breast", "nipple", "areola", "cleavage", "topless", "open chest", "genital", "vagina", "penis", "dick", "cock", "pussy", "ass", "butt", "anus", "lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "fetish", "bdsm", "bondage", "exhibitionism", "voyeur", "hentai", "ecchi", "ahegao", "paizuri", "undressed", "stripping", "naked body", "exposed skin", "sheer", "see-through", "rape", "violence", "blood", "gore", "sexual"
|
| 98 |
-
]
|
| 99 |
-
|
| 100 |
-
def check_text_safety(text):
|
| 101 |
-
if not text: return True
|
| 102 |
-
text_lower = text.lower()
|
| 103 |
-
for word in BANNED_WORDS:
|
| 104 |
-
if word in text_lower:
|
| 105 |
-
print(f"Banned word found: {word}")
|
| 106 |
-
return False
|
| 107 |
-
return True
|
| 108 |
-
|
| 109 |
-
def translate_prompt(text):
|
| 110 |
-
if not text: return ""
|
| 111 |
-
try:
|
| 112 |
-
return GoogleTranslator(source='auto', target='en').translate(text)
|
| 113 |
-
except Exception as e:
|
| 114 |
-
print(f"Translation Error: {e}")
|
| 115 |
-
return text
|
| 116 |
|
| 117 |
-
# --- بارگذاری مدل اصلی
|
| 118 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
| 119 |
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
|
| 120 |
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
|
|
@@ -123,15 +59,19 @@ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
|
|
| 123 |
dtype = torch.bfloat16
|
| 124 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 125 |
|
| 126 |
-
print("Loading
|
| 127 |
pipe = QwenImageEditPlusPipeline.from_pretrained(
|
| 128 |
"Qwen/Qwen-Image-Edit-2509",
|
| 129 |
transformer=QwenImageTransformer2DModel.from_pretrained(
|
| 130 |
-
"linoyts/Qwen-Image-Edit-Rapid-AIO",
|
|
|
|
|
|
|
|
|
|
| 131 |
),
|
| 132 |
torch_dtype=dtype
|
| 133 |
).to(device)
|
| 134 |
|
|
|
|
| 135 |
pipe.load_lora_weights("autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime", weight_name="Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors", adapter_name="anime")
|
| 136 |
pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
|
| 137 |
pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
|
|
@@ -145,406 +85,861 @@ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
|
|
| 145 |
MAX_SEED = np.iinfo(np.int32).max
|
| 146 |
|
| 147 |
LORA_MAPPING = {
|
| 148 |
-
"تبدیل عکس به انیمه": "anime",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
|
|
|
| 150 |
ASPECT_RATIOS_LIST = [
|
| 151 |
-
"خودکار (پیشفرض)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
]
|
|
|
|
| 153 |
ASPECT_RATIOS_MAP = {
|
| 154 |
-
"خودکار (پیشفرض)": "Auto",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
def update_dimensions_on_upload(image):
|
| 158 |
-
if image is None:
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
def get_error_html(message):
|
| 164 |
-
return f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
def get_success_html(message):
|
| 167 |
-
return f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
@spaces.GPU(duration=
|
| 170 |
def infer(
|
| 171 |
-
input_image,
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
progress=gr.Progress(track_tqdm=True)
|
| 175 |
):
|
| 176 |
-
# ---
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
english_prompt = translate_prompt(prompt)
|
| 186 |
-
if not check_text_safety(english_prompt):
|
|
|
|
| 187 |
|
| 188 |
adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
|
| 189 |
-
if adapter_internal_name:
|
|
|
|
| 190 |
|
| 191 |
-
if randomize_seed:
|
|
|
|
|
|
|
| 192 |
generator = torch.Generator(device=device).manual_seed(seed)
|
| 193 |
|
| 194 |
-
safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple,
|
| 195 |
-
|
|
|
|
| 196 |
|
| 197 |
original_image = input_image.convert("RGB")
|
|
|
|
| 198 |
selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
|
| 199 |
|
| 200 |
if selection_value == "Custom":
|
| 201 |
-
width
|
| 202 |
-
|
|
|
|
| 203 |
width, height = update_dimensions_on_upload(original_image)
|
| 204 |
else:
|
| 205 |
width, height = selection_value
|
| 206 |
|
| 207 |
try:
|
| 208 |
-
result = pipe(
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
|
|
|
|
| 211 |
except Exception as e:
|
| 212 |
error_str = str(e)
|
| 213 |
-
if "quota" in error_str.lower() or "exceeded" in error_str.lower():
|
|
|
|
| 214 |
return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
|
| 215 |
|
| 216 |
@spaces.GPU(duration=30)
|
| 217 |
def infer_example(input_image, prompt, lora_adapter):
|
| 218 |
-
# مثالها
|
| 219 |
-
res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیشفرض)", 1024, 1024, True,
|
| 220 |
return res, s, status
|
| 221 |
|
| 222 |
-
# --- جاوااسکریپت برای
|
| 223 |
-
js_download_func = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
-
# --- جاوااسکریپت
|
| 226 |
-
|
| 227 |
<script>
|
| 228 |
-
//
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
upgradeBtn.style.display = 'none';
|
| 258 |
-
runBtn.disabled = false;
|
| 259 |
-
} else {
|
| 260 |
-
badgeEl.textContent = 'کاربر رایگان';
|
| 261 |
-
badgeEl.className = 'free-badge';
|
| 262 |
-
const credits = statusData.credits || 0;
|
| 263 |
-
|
| 264 |
-
if (credits > 0) {
|
| 265 |
-
creditTextEl.innerHTML = `شما <strong>${credits} اعتبار</strong> ساخت تصویر برای امروز دارید.`;
|
| 266 |
-
runBtn.disabled = false;
|
| 267 |
-
upgradeBtn.style.display = 'none';
|
| 268 |
-
} else {
|
| 269 |
-
creditTextEl.innerHTML = 'اعتبار رایگان امروز شما تمام شده است.';
|
| 270 |
-
runBtn.disabled = true;
|
| 271 |
-
upgradeBtn.style.display = 'block';
|
| 272 |
}
|
| 273 |
-
|
| 274 |
-
// شروع شمارش معکوس
|
| 275 |
-
const resetTimestamp = statusData.next_reset_timestamp;
|
| 276 |
-
const updateTimer = () => {
|
| 277 |
-
const now = Date.now() / 1000;
|
| 278 |
-
const timeLeft = Math.max(0, resetTimestamp - now);
|
| 279 |
-
if (timeLeft === 0) {
|
| 280 |
-
clearInterval(countdownInterval);
|
| 281 |
-
initializeApp(); // برای گرفتن اعتبار جدید
|
| 282 |
-
return;
|
| 283 |
-
}
|
| 284 |
-
const h = Math.floor(timeLeft / 3600);
|
| 285 |
-
const m = Math.floor((timeLeft % 3600) / 60);
|
| 286 |
-
const s = Math.floor(timeLeft % 60);
|
| 287 |
-
countdownEl.querySelector('span').textContent = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
| 288 |
-
};
|
| 289 |
-
updateTimer();
|
| 290 |
-
countdownInterval = setInterval(updateTimer, 1000);
|
| 291 |
-
countdownEl.style.display = 'block';
|
| 292 |
-
}
|
| 293 |
}
|
|
|
|
| 294 |
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
// آپدیت کردن کامپوننتهای مخفی گرادیو
|
| 304 |
-
const fingerprintInput = document.querySelector('#fingerprint-input-for-backend textarea');
|
| 305 |
-
const isPaidInput = document.querySelector('#is-paid-input-for-backend textarea');
|
| 306 |
-
if(fingerprintInput) fingerprintInput.value = userFingerprint;
|
| 307 |
-
if(isPaidInput) isPaidInput.value = isPaidUser ? 'true' : 'false';
|
| 308 |
-
|
| 309 |
-
// ایجاد یک Event برای اینکه گرادیو تغییر را تشخیص دهد
|
| 310 |
-
const eventToDispatch = new Event('input', { bubbles: true });
|
| 311 |
-
if(fingerprintInput) fingerprintInput.dispatchEvent(eventToDispatch);
|
| 312 |
-
if(isPaidInput) isPaidInput.dispatchEvent(eventToDispatch);
|
| 313 |
-
|
| 314 |
-
// حالا که اطلاعات کاربر را داریم، از بکاند گرادیو وضعیت اعتبار را میپرسیم
|
| 315 |
-
// با کلیک کردن روی دکمه مخفی، تابع پایتون را فراخوانی میکنیم
|
| 316 |
-
const statusCheckBtn = document.getElementById('status-check-btn-hidden');
|
| 317 |
-
if(statusCheckBtn) statusCheckBtn.click();
|
| 318 |
-
|
| 319 |
-
} catch (e) {
|
| 320 |
-
console.error("Error processing user status:", e);
|
| 321 |
-
isPaidUser = false; // در صورت خطا، کاربر رایگان در نظر گرفته میشود
|
| 322 |
-
}
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
if (event.data.type === 'DOWNLOAD_REQUEST' && event.data.url) {
|
| 326 |
-
// این بخش برای دکمه دانلود است که در HTML دیگر تعریف شده
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
if (event.data.type === 'NAVIGATE_TO_URL' && event.data.url) {
|
| 330 |
-
window.top.location.href = event.data.url;
|
| 331 |
-
}
|
| 332 |
-
});
|
| 333 |
-
|
| 334 |
-
// تابع برای ساخت اثرانگشت مرورگر
|
| 335 |
-
async function getBrowserFingerprint() {
|
| 336 |
-
const components = [navigator.userAgent, navigator.language, screen.width+'x'+screen.height, new Date().getTimezoneOffset()];
|
| 337 |
-
const fingerprintString = components.join('~~~');
|
| 338 |
-
let hash = 0;
|
| 339 |
-
for (let i = 0; i < fingerprintString.length; i++) {
|
| 340 |
-
const char = fingerprintString.charCodeAt(i);
|
| 341 |
-
hash = ((hash << 5) - hash) + char;
|
| 342 |
-
hash |= 0;
|
| 343 |
}
|
| 344 |
-
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
-
//
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
}
|
| 360 |
|
| 361 |
-
//
|
| 362 |
const showQuotaModal = () => {
|
| 363 |
if (document.getElementById('custom-quota-modal')) return;
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
};
|
|
|
|
|
|
|
| 367 |
setInterval(() => {
|
| 368 |
-
document.querySelectorAll('.toast-body, .error
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
}
|
| 373 |
});
|
| 374 |
}, 100);
|
| 375 |
-
|
| 376 |
-
initializeApp();
|
| 377 |
-
|
| 378 |
-
// این تابع مخصوص گرادیو است تا وقتی خروجی JSON آپدیت شد، UI را آپدیت کند
|
| 379 |
-
window.updateUIFromGradio = function(jsonData) {
|
| 380 |
-
updateUIForUserStatus(jsonData);
|
| 381 |
-
}
|
| 382 |
});
|
| 383 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
<style>
|
| 385 |
-
/* --- استایلهای اصلی (مشابه قبل با اضافات جدید) --- */
|
| 386 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
text-align: center;
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
color: #
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
margin-top: 15px;
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
box-shadow: none !important;
|
| 445 |
-
transform: none !important;
|
| 446 |
}
|
| 447 |
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
| 452 |
</style>
|
| 453 |
"""
|
| 454 |
|
|
|
|
|
|
|
|
|
|
| 455 |
# استفاده از gr.Blocks
|
| 456 |
-
with gr.Blocks(
|
| 457 |
-
|
|
|
|
| 458 |
|
| 459 |
with gr.Column(elem_id="col-container"):
|
| 460 |
gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
|
| 461 |
-
gr.Markdown(
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
<div id="user-status-section">
|
| 466 |
-
<div id="subscription-badge">...</div>
|
| 467 |
-
<p id="credit-status-text">در حال بررسی وضعیت حساب...</p>
|
| 468 |
-
<div id="countdown-timer" style="display: none;">
|
| 469 |
-
زمان تا شارژ مجدد اعتبار: <span></span>
|
| 470 |
-
</div>
|
| 471 |
-
<button id="upgrade-btn">⭐️ ارتقا به نسخه کامل و نامحدود</button>
|
| 472 |
-
</div>
|
| 473 |
-
""")
|
| 474 |
-
|
| 475 |
-
# --- بخش جدید: کامپوننتهای مخفی برای ارتباط JS و Python ---
|
| 476 |
-
with gr.Row(visible=False):
|
| 477 |
-
fingerprint_input = gr.Textbox(elem_id="fingerprint-input-for-backend", label="FP")
|
| 478 |
-
is_paid_input = gr.Textbox(elem_id="is-paid-input-for-backend", label="Paid")
|
| 479 |
-
status_check_btn = gr.Button(elem_id="status-check-btn-hidden")
|
| 480 |
-
status_json_output = gr.JSON(label="Status JSON")
|
| 481 |
|
| 482 |
with gr.Row(equal_height=True):
|
| 483 |
with gr.Column():
|
| 484 |
input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
status_box = gr.HTML(label="وضعیت")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
|
| 488 |
|
| 489 |
with gr.Column():
|
| 490 |
output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
|
|
|
|
| 491 |
download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
|
| 492 |
-
lora_adapter = gr.Dropdown(label="انتخاب سبک ویرایش (LoRA)", choices=list(LORA_MAPPING.keys()), value="تبدیل عکس به انیمه")
|
| 493 |
|
| 494 |
-
with gr.
|
| 495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
with gr.Row(visible=False) as custom_dims_row:
|
| 497 |
-
custom_width = gr.Slider(
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
|
| 500 |
randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
|
| 501 |
-
guidance_scale = gr.Slider(label="وفاداری به متن", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
|
| 502 |
-
steps = gr.Slider(label="مراحل پردازش", minimum=1, maximum=50, step=1, value=4)
|
| 503 |
|
|
|
|
| 504 |
def toggle_row(choice):
|
| 505 |
-
|
| 506 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
gr.Examples(
|
| 509 |
-
examples=[
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
)
|
| 512 |
|
| 513 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
run_button.click(
|
| 515 |
fn=infer,
|
| 516 |
-
inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height,
|
| 517 |
outputs=[output_image, seed, status_box],
|
|
|
|
| 518 |
api_name="predict"
|
| 519 |
)
|
| 520 |
-
|
| 521 |
-
# رویداد دکمه مخفی برای گرفتن وضعیت کاربر
|
| 522 |
-
status_check_btn.click(
|
| 523 |
-
fn=get_user_status_api,
|
| 524 |
-
inputs=[fingerprint_input],
|
| 525 |
-
outputs=[status_json_output]
|
| 526 |
-
)
|
| 527 |
|
| 528 |
-
|
| 529 |
-
status_json_output.change(
|
| 530 |
fn=None,
|
| 531 |
-
inputs=[
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
// Check if the function exists on window before calling
|
| 535 |
-
if (window.updateUIFromGradio) {
|
| 536 |
-
window.updateUIFromGradio(jsonData);
|
| 537 |
-
} else {
|
| 538 |
-
console.error("Gradio UI update function not found!");
|
| 539 |
-
}
|
| 540 |
-
}
|
| 541 |
-
"""
|
| 542 |
)
|
| 543 |
-
|
| 544 |
-
download_button.click(fn=None, inputs=[output_image], outputs=None, js=js_download_func)
|
| 545 |
-
|
| 546 |
-
# --- تعریف API برای ارتباط مستقیم (در صورت نیاز) ---
|
| 547 |
-
demo.api(name="get_status")(get_user_status_api)
|
| 548 |
|
| 549 |
if __name__ == "__main__":
|
| 550 |
demo.queue(max_size=30).launch(show_error=True)
|
|
|
|
| 4 |
import spaces
|
| 5 |
import torch
|
| 6 |
import random
|
| 7 |
+
from PIL import Image, ImageFilter
|
| 8 |
from typing import Iterable
|
| 9 |
+
from gradio.themes import Soft
|
| 10 |
+
from gradio.themes.utils import colors, fonts, sizes
|
| 11 |
from deep_translator import GoogleTranslator
|
| 12 |
from transformers import pipeline
|
| 13 |
+
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# --- متغیرهای سراسری برای مدیریت محدودیت ---
|
| 16 |
+
USAGE_TRACKER = {} # ذخیره: {ip: {'count': 0, 'date': 'YYYY-MM-DD'}}
|
| 17 |
+
DAILY_LIMIT = 5 # محدودیت تعداد عکس در روز برای کاربران رایگان
|
| 18 |
+
|
| 19 |
+
# --- تعریف تم ---
|
| 20 |
colors.steel_blue = colors.Color(
|
| 21 |
+
name="steel_blue",
|
| 22 |
+
c50="#EBF3F8",
|
| 23 |
+
c100="#D3E5F0",
|
| 24 |
+
c200="#A8CCE1",
|
| 25 |
+
c300="#7DB3D2",
|
| 26 |
+
c400="#529AC3",
|
| 27 |
+
c500="#4682B4",
|
| 28 |
+
c600="#3E72A0",
|
| 29 |
+
c700="#36638C",
|
| 30 |
+
c800="#2E5378",
|
| 31 |
+
c900="#264364",
|
| 32 |
+
c950="#1E3450",
|
| 33 |
)
|
| 34 |
|
| 35 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
# --- بارگذاری مدل تشخیص محتوای نامناسب (NSFW) ---
|
| 38 |
+
print("Loading Safety Checker...")
|
| 39 |
+
safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
|
|
|
|
| 40 |
|
| 41 |
def is_image_nsfw(image):
|
| 42 |
if image is None: return False
|
| 43 |
try:
|
| 44 |
+
results = safety_classifier(image)
|
| 45 |
+
for result in results:
|
| 46 |
+
if result['label'] == 'nsfw' and result['score'] > 0.75:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
return True
|
| 48 |
return False
|
| 49 |
except Exception as e:
|
| 50 |
print(f"Safety check error: {e}")
|
| 51 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
# --- بارگذاری مدل اصلی ---
|
| 54 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
| 55 |
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
|
| 56 |
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
|
|
|
|
| 59 |
dtype = torch.bfloat16
|
| 60 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 61 |
|
| 62 |
+
print("Loading pipeline...")
|
| 63 |
pipe = QwenImageEditPlusPipeline.from_pretrained(
|
| 64 |
"Qwen/Qwen-Image-Edit-2509",
|
| 65 |
transformer=QwenImageTransformer2DModel.from_pretrained(
|
| 66 |
+
"linoyts/Qwen-Image-Edit-Rapid-AIO",
|
| 67 |
+
subfolder='transformer',
|
| 68 |
+
torch_dtype=dtype,
|
| 69 |
+
device_map='cuda'
|
| 70 |
),
|
| 71 |
torch_dtype=dtype
|
| 72 |
).to(device)
|
| 73 |
|
| 74 |
+
# بارگذاری LoRA ها
|
| 75 |
pipe.load_lora_weights("autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime", weight_name="Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors", adapter_name="anime")
|
| 76 |
pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
|
| 77 |
pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
|
|
|
|
| 85 |
MAX_SEED = np.iinfo(np.int32).max
|
| 86 |
|
| 87 |
LORA_MAPPING = {
|
| 88 |
+
"تبدیل عکس به انیمه": "anime",
|
| 89 |
+
"تغییر زاویه دید": "multiple-angles",
|
| 90 |
+
"اصلاح نور و سایه": "light-restoration",
|
| 91 |
+
"نورپردازی مجدد (Relight)": "relight",
|
| 92 |
+
"نورپردازی چند زاویهای": "multi-angle-lighting",
|
| 93 |
+
"روتوش پوست": "edit-skin",
|
| 94 |
+
"صحنه بعدی (سینمایی)": "next-scene",
|
| 95 |
+
"افزایش کیفیت (Upscale)": "upscale-image"
|
| 96 |
}
|
| 97 |
+
|
| 98 |
ASPECT_RATIOS_LIST = [
|
| 99 |
+
"خودکار (پیشفرض)",
|
| 100 |
+
"۱:۱ (مربع - 1024x1024)",
|
| 101 |
+
"۱۶:۹ (افقی - 1344x768)",
|
| 102 |
+
"۹:۱۶ (عمودی - 768x1344)",
|
| 103 |
+
"شخصیسازی (Custom)"
|
| 104 |
]
|
| 105 |
+
|
| 106 |
ASPECT_RATIOS_MAP = {
|
| 107 |
+
"خودکار (پیشفرض)": "Auto",
|
| 108 |
+
"۱:۱ (مربع - 1024x1024)": (1024, 1024),
|
| 109 |
+
"۱۶:۹ (افقی - 1344x768)": (1344, 768),
|
| 110 |
+
"۹:۱۶ (عمودی - 768x1344)": (768, 1344),
|
| 111 |
+
"شخصیسازی (Custom)": "Custom"
|
| 112 |
}
|
| 113 |
|
| 114 |
+
BANNED_WORDS = [
|
| 115 |
+
"nude", "naked", "sex", "porn", "undressed", "nsfw", "erotic", "xxx",
|
| 116 |
+
"breast", "nipple", "genital", "vagina", "penis", "ass", "butt", "sexual",
|
| 117 |
+
"lingerie", "bikini", "swimwear", "underwear", "fetish", "topless",
|
| 118 |
+
"exhibitionism", "hentai", "ecchi", "18+"
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
def check_text_safety(text):
|
| 122 |
+
text_lower = text.lower()
|
| 123 |
+
for word in BANNED_WORDS:
|
| 124 |
+
if f" {word} " in f" {text_lower} ":
|
| 125 |
+
return False
|
| 126 |
+
return True
|
| 127 |
+
|
| 128 |
+
def translate_prompt(text):
|
| 129 |
+
if not text:
|
| 130 |
+
return ""
|
| 131 |
+
try:
|
| 132 |
+
translated = GoogleTranslator(source='auto', target='en').translate(text)
|
| 133 |
+
return translated
|
| 134 |
+
except Exception as e:
|
| 135 |
+
print(f"Translation Error: {e}")
|
| 136 |
+
return text
|
| 137 |
+
|
| 138 |
def update_dimensions_on_upload(image):
|
| 139 |
+
if image is None:
|
| 140 |
+
return 1024, 1024
|
| 141 |
+
original_width, original_height = image.size
|
| 142 |
+
if original_width > original_height:
|
| 143 |
+
new_width = 1024
|
| 144 |
+
aspect_ratio = original_height / original_width
|
| 145 |
+
new_height = int(new_width * aspect_ratio)
|
| 146 |
+
else:
|
| 147 |
+
new_height = 1024
|
| 148 |
+
aspect_ratio = original_width / original_height
|
| 149 |
+
new_width = int(new_height * aspect_ratio)
|
| 150 |
+
new_width = (new_width // 8) * 8
|
| 151 |
+
new_height = (new_height // 8) * 8
|
| 152 |
+
return new_width, new_height
|
| 153 |
+
|
| 154 |
+
def update_sliders_visibility(choice):
|
| 155 |
+
if choice == "شخصیسازی (Custom)":
|
| 156 |
+
return gr.update(visible=True), gr.update(visible=True)
|
| 157 |
+
else:
|
| 158 |
+
return gr.update(visible=False), gr.update(visible=False)
|
| 159 |
|
| 160 |
def get_error_html(message):
|
| 161 |
+
return f"""
|
| 162 |
+
<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;">
|
| 163 |
+
<span style="font-size: 1.2em;">⛔</span>
|
| 164 |
+
{message}
|
| 165 |
+
</div>
|
| 166 |
+
"""
|
| 167 |
|
| 168 |
def get_success_html(message):
|
| 169 |
+
return f"""
|
| 170 |
+
<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;">
|
| 171 |
+
<span style="font-size: 1.2em;">✅</span>
|
| 172 |
+
{message}
|
| 173 |
+
</div>
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
def check_and_update_quota(request: gr.Request, is_vip: bool):
|
| 177 |
+
"""
|
| 178 |
+
بررسی محدودیت کاربر و بروزرسانی شمارنده.
|
| 179 |
+
اگر VIP باشد، محدودیت ندارد.
|
| 180 |
+
اگر رایگان باشد، بر اساس IP محدودیت ۵ بار در روز اعمال میشود.
|
| 181 |
+
"""
|
| 182 |
+
if is_vip:
|
| 183 |
+
return True, ""
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
client_ip = request.client.host
|
| 187 |
+
except:
|
| 188 |
+
client_ip = "unknown"
|
| 189 |
+
|
| 190 |
+
today = datetime.now().strftime("%Y-%m-%d")
|
| 191 |
+
|
| 192 |
+
# مقداردهی اولیه برای IP جدید
|
| 193 |
+
if client_ip not in USAGE_TRACKER:
|
| 194 |
+
USAGE_TRACKER[client_ip] = {'count': 0, 'date': today}
|
| 195 |
+
|
| 196 |
+
user_data = USAGE_TRACKER[client_ip]
|
| 197 |
+
|
| 198 |
+
# ریست کردن اگر روز تغییر کرده باشد
|
| 199 |
+
if user_data['date'] != today:
|
| 200 |
+
user_data['count'] = 0
|
| 201 |
+
user_data['date'] = today
|
| 202 |
+
|
| 203 |
+
# بررسی سقف مجاز
|
| 204 |
+
if user_data['count'] >= DAILY_LIMIT:
|
| 205 |
+
return False, f"شما به سقف مجاز روزانه ({DAILY_LIMIT} تصویر) رسیدهاید. فردا مجدداً تلاش کنید یا حساب خود را ارتقا دهید."
|
| 206 |
+
|
| 207 |
+
return True, ""
|
| 208 |
+
|
| 209 |
+
def increment_quota(request: gr.Request, is_vip: bool):
|
| 210 |
+
"""
|
| 211 |
+
افزایش شمارنده پس از موفقیت آمیز بودن عملیات
|
| 212 |
+
"""
|
| 213 |
+
if is_vip: return
|
| 214 |
+
try:
|
| 215 |
+
client_ip = request.client.host
|
| 216 |
+
if client_ip in USAGE_TRACKER:
|
| 217 |
+
USAGE_TRACKER[client_ip]['count'] += 1
|
| 218 |
+
except:
|
| 219 |
+
pass
|
| 220 |
|
| 221 |
+
@spaces.GPU(duration=30)
|
| 222 |
def infer(
|
| 223 |
+
input_image,
|
| 224 |
+
prompt,
|
| 225 |
+
lora_adapter_persian,
|
| 226 |
+
seed,
|
| 227 |
+
randomize_seed,
|
| 228 |
+
guidance_scale,
|
| 229 |
+
steps,
|
| 230 |
+
aspect_ratio_selection,
|
| 231 |
+
custom_width,
|
| 232 |
+
custom_height,
|
| 233 |
+
is_vip_state, # ورودی جدید برای وضعیت VIP
|
| 234 |
+
request: gr.Request,
|
| 235 |
progress=gr.Progress(track_tqdm=True)
|
| 236 |
):
|
| 237 |
+
# --- بررسی سهمیه (Quota) ---
|
| 238 |
+
allowed, message = check_and_update_quota(request, is_vip_state)
|
| 239 |
+
if not allowed:
|
| 240 |
+
return None, seed, get_error_html(message)
|
| 241 |
+
|
| 242 |
+
if input_image is None:
|
| 243 |
+
return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید.")
|
| 244 |
+
|
| 245 |
+
if is_image_nsfw(input_image):
|
| 246 |
+
return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمیشود.")
|
| 247 |
+
|
| 248 |
english_prompt = translate_prompt(prompt)
|
| 249 |
+
if not check_text_safety(english_prompt):
|
| 250 |
+
return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز یا غیراخلاقی است.")
|
| 251 |
|
| 252 |
adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
|
| 253 |
+
if adapter_internal_name:
|
| 254 |
+
pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
|
| 255 |
|
| 256 |
+
if randomize_seed:
|
| 257 |
+
seed = random.randint(0, MAX_SEED)
|
| 258 |
+
|
| 259 |
generator = torch.Generator(device=device).manual_seed(seed)
|
| 260 |
|
| 261 |
+
safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood"
|
| 262 |
+
base_negative = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
|
| 263 |
+
final_negative_prompt = f"{safety_negative}, {base_negative}"
|
| 264 |
|
| 265 |
original_image = input_image.convert("RGB")
|
| 266 |
+
|
| 267 |
selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
|
| 268 |
|
| 269 |
if selection_value == "Custom":
|
| 270 |
+
width = (int(custom_width) // 8) * 8
|
| 271 |
+
height = (int(custom_height) // 8) * 8
|
| 272 |
+
elif selection_value == "Auto" or selection_value is None:
|
| 273 |
width, height = update_dimensions_on_upload(original_image)
|
| 274 |
else:
|
| 275 |
width, height = selection_value
|
| 276 |
|
| 277 |
try:
|
| 278 |
+
result = pipe(
|
| 279 |
+
image=original_image,
|
| 280 |
+
prompt=english_prompt,
|
| 281 |
+
negative_prompt=final_negative_prompt,
|
| 282 |
+
height=height,
|
| 283 |
+
width=width,
|
| 284 |
+
num_inference_steps=steps,
|
| 285 |
+
generator=generator,
|
| 286 |
+
true_cfg_scale=guidance_scale,
|
| 287 |
+
).images[0]
|
| 288 |
+
|
| 289 |
+
if is_image_nsfw(result):
|
| 290 |
+
return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد.")
|
| 291 |
+
|
| 292 |
+
# افزایش شمارنده مصرف فقط در صورت موفقیت
|
| 293 |
+
increment_quota(request, is_vip_state)
|
| 294 |
+
|
| 295 |
return result, seed, get_success_html("تصویر با موفقیت ویرایش شد.")
|
| 296 |
+
|
| 297 |
except Exception as e:
|
| 298 |
error_str = str(e)
|
| 299 |
+
if "quota" in error_str.lower() or "exceeded" in error_str.lower():
|
| 300 |
+
raise e
|
| 301 |
return None, seed, get_error_html(f"خطا در پردازش: {error_str}")
|
| 302 |
|
| 303 |
@spaces.GPU(duration=30)
|
| 304 |
def infer_example(input_image, prompt, lora_adapter):
|
| 305 |
+
# برای مثالها محدودیت VIP بررسی نمیشود و IP لوکال فرض میشود
|
| 306 |
+
res, s, status = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیشفرض)", 1024, 1024, True, None)
|
| 307 |
return res, s, status
|
| 308 |
|
| 309 |
+
# --- جاوااسکریپت برای دکمه دانلود ---
|
| 310 |
+
js_download_func = """
|
| 311 |
+
async (image) => {
|
| 312 |
+
if (!image) {
|
| 313 |
+
alert("لطفاً ابتدا تصویر را تولید کنید.");
|
| 314 |
+
return;
|
| 315 |
+
}
|
| 316 |
+
let fileUrl = image.url;
|
| 317 |
+
if (fileUrl && !fileUrl.startsWith('http')) {
|
| 318 |
+
fileUrl = window.location.origin + fileUrl;
|
| 319 |
+
} else if (!fileUrl && image.path) {
|
| 320 |
+
fileUrl = window.location.origin + "/file=" + image.path;
|
| 321 |
+
}
|
| 322 |
+
window.parent.postMessage({
|
| 323 |
+
type: 'DOWNLOAD_REQUEST',
|
| 324 |
+
url: fileUrl
|
| 325 |
+
}, '*');
|
| 326 |
+
}
|
| 327 |
+
"""
|
| 328 |
|
| 329 |
+
# --- جاوااسکریپت سراسری (استراتژی موبایل + بستن خودکار ۱۰ ثانیه + تشخیص VIP) ---
|
| 330 |
+
js_global_content = """
|
| 331 |
<script>
|
| 332 |
+
// Global variable to store VIP status
|
| 333 |
+
window.userIsVip = false;
|
| 334 |
+
const PREMIUM_PAGE_ID = '1149635'; // شناسه صفحه پرداخت برای بررسی دسترسی
|
| 335 |
+
|
| 336 |
+
// Request User Status on Load
|
| 337 |
+
setTimeout(() => {
|
| 338 |
+
window.parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*');
|
| 339 |
+
}, 1000);
|
| 340 |
+
|
| 341 |
+
// Listener for User Status
|
| 342 |
+
window.addEventListener('message', (event) => {
|
| 343 |
+
if (event.data && event.data.type === 'USER_STATUS_RESPONSE') {
|
| 344 |
+
try {
|
| 345 |
+
const user = JSON.parse(event.data.payload);
|
| 346 |
+
// بررسی دسترسی کاربر به صفحه پرمیوم
|
| 347 |
+
if (user && user.isLogin && user.accessible_pages &&
|
| 348 |
+
(user.accessible_pages.includes(PREMIUM_PAGE_ID) ||
|
| 349 |
+
user.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID)))) {
|
| 350 |
+
|
| 351 |
+
window.userIsVip = true;
|
| 352 |
+
|
| 353 |
+
// نمایش نشان VIP
|
| 354 |
+
const badge = document.createElement('div');
|
| 355 |
+
badge.innerHTML = '👑 نسخه ویژه (نامحدود)';
|
| 356 |
+
badge.style.cssText = 'position:fixed;top:10px;left:10px;background:linear-gradient(45deg, #FFD700, #FFC107);color:#333;padding:6px 12px;border-radius:20px;z-index:9999;font-weight:bold;box-shadow:0 2px 5px rgba(0,0,0,0.2);font-family:"Vazirmatn",sans-serif;font-size:0.8rem;';
|
| 357 |
+
document.body.appendChild(badge);
|
| 358 |
+
|
| 359 |
+
// حذف مدال محدودیت اگر باز باشد
|
| 360 |
+
window.closeErrorModal();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
}
|
| 362 |
+
} catch (e) { console.log('Error parsing user status:', e); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
}
|
| 364 |
+
});
|
| 365 |
|
| 366 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 367 |
+
// 1. Force Light Mode
|
| 368 |
+
const forceLight = () => {
|
| 369 |
+
const body = document.querySelector('body');
|
| 370 |
+
if (body) {
|
| 371 |
+
body.classList.remove('dark');
|
| 372 |
+
body.style.backgroundColor = '#f5f7fa';
|
| 373 |
+
body.style.color = '#333333';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
}
|
| 375 |
+
document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
|
| 376 |
+
};
|
| 377 |
+
forceLight();
|
| 378 |
+
setInterval(forceLight, 1000);
|
| 379 |
+
|
| 380 |
+
// 2. RETRY FUNCTION
|
| 381 |
+
window.retryGeneration = function() {
|
| 382 |
+
const modal = document.getElementById('custom-quota-modal');
|
| 383 |
+
if (modal) modal.remove();
|
| 384 |
+
|
| 385 |
+
const runBtn = document.getElementById('run-btn');
|
| 386 |
+
if(runBtn) runBtn.click();
|
| 387 |
+
};
|
| 388 |
|
| 389 |
+
// Close function
|
| 390 |
+
window.closeErrorModal = function() {
|
| 391 |
+
const modal = document.getElementById('custom-quota-modal');
|
| 392 |
+
if (modal) modal.remove();
|
| 393 |
+
};
|
|
|
|
| 394 |
|
| 395 |
+
// 3. SHOW MODAL FUNCTION
|
| 396 |
const showQuotaModal = () => {
|
| 397 |
if (document.getElementById('custom-quota-modal')) return;
|
| 398 |
+
|
| 399 |
+
const modalHtml = `
|
| 400 |
+
<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;">
|
| 401 |
+
<div class="ip-reset-guide-container">
|
| 402 |
+
<div class="guide-header">
|
| 403 |
+
<svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
| 404 |
+
<defs><lineargradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color: #667eea; stop-opacity: 1;"></stop><stop offset="100%" style="stop-color: #764ba2; stop-opacity: 1;"></stop></lineargradient></defs>
|
| 405 |
+
<circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle>
|
| 406 |
+
<circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle>
|
| 407 |
+
<path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
|
| 408 |
+
<circle cx="65" cy="35" r="8" fill="#fee140"></circle>
|
| 409 |
+
<path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
|
| 410 |
+
</svg>
|
| 411 |
+
<div>
|
| 412 |
+
<h2>محدودیت سرور گرافیکی</h2>
|
| 413 |
+
<p>نیازمند تغییر نقطه دستیابی</p>
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
|
| 417 |
+
<div class="guide-content">
|
| 418 |
+
<div class="info-card">
|
| 419 |
+
<div class="info-card-header">
|
| 420 |
+
<svg class="info-card-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#667eea" opacity="0.2"></path><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
| 421 |
+
<span class="info-card-title">راه حل سریع</span>
|
| 422 |
+
</div>
|
| 423 |
+
<p>طبق ویدیو آموزشی پایین بین نقطه دستیابی جابجا شوید تلاش مجدد بزنید تا تصاویر مجدداً تولید بشه.</p>
|
| 424 |
+
</div>
|
| 425 |
+
|
| 426 |
+
<div class="summary-section">
|
| 427 |
+
<div class="summary-header">
|
| 428 |
+
<svg class="summary-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#56ab2f" opacity="0.2"></circle><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" stroke="#56ab2f" stroke-width="2"></path><path d="M9 12l2 2 4-4" stroke="#56ab2f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
| 429 |
+
<span class="summary-title">خلاصه راهنما</span>
|
| 430 |
+
</div>
|
| 431 |
+
<div class="summary-text">این پیام مربوط به سرور جهانی است. برای رفع آن: اینترنت سیمکارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی نقطه دستیابی رو تغییر دهید.</div>
|
| 432 |
+
</div>
|
| 433 |
+
|
| 434 |
+
<div class="video-button-container">
|
| 435 |
+
<button onclick="parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*')" class="elegant-video-button">
|
| 436 |
+
<svg class="elegant-video-button-icon" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg>
|
| 437 |
+
<span>دیدن ویدیو آموزشی استفاده نامحدود</span>
|
| 438 |
+
</button>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
|
| 442 |
+
<div class="guide-actions">
|
| 443 |
+
<button class="action-button back-button" onclick="window.closeErrorModal()">
|
| 444 |
+
<svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
| 445 |
+
<span>بازگشت</span>
|
| 446 |
+
</button>
|
| 447 |
+
<button class="action-button retry-button" onclick="window.retryGeneration()">
|
| 448 |
+
<svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23 4v6h-6M1 20v-6h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
| 449 |
+
<span>تلاش مجدد</span>
|
| 450 |
+
</button>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
`;
|
| 455 |
+
|
| 456 |
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
| 457 |
+
|
| 458 |
+
// Auto close after 10 seconds
|
| 459 |
+
setTimeout(() => {
|
| 460 |
+
window.closeErrorModal();
|
| 461 |
+
}, 10000);
|
| 462 |
};
|
| 463 |
+
|
| 464 |
+
// 4. SCANNER (For HuggingFace GPU Quota)
|
| 465 |
setInterval(() => {
|
| 466 |
+
const potentialErrors = document.querySelectorAll('.toast-body, .error, .toast-wrap, .eta-bar, div[class*="error"]');
|
| 467 |
+
|
| 468 |
+
potentialErrors.forEach(el => {
|
| 469 |
+
const text = el.innerText || "";
|
| 470 |
+
if (text.toLowerCase().includes('quota') || text.toLowerCase().includes('exceeded')) {
|
| 471 |
+
|
| 472 |
+
showQuotaModal();
|
| 473 |
+
|
| 474 |
+
// Immediately hide the Gradio error
|
| 475 |
+
el.style.display = 'none';
|
| 476 |
+
el.style.opacity = '0';
|
| 477 |
+
el.innerText = '';
|
| 478 |
+
|
| 479 |
+
const parentWrap = el.closest('.toast-wrap');
|
| 480 |
+
if(parentWrap) parentWrap.style.display = 'none';
|
| 481 |
}
|
| 482 |
});
|
| 483 |
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
});
|
| 485 |
</script>
|
| 486 |
+
"""
|
| 487 |
+
|
| 488 |
+
# --- CSS Updated (Larger & Auto Scroll) ---
|
| 489 |
+
css_code = """
|
| 490 |
<style>
|
|
|
|
| 491 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap');
|
| 492 |
+
|
| 493 |
+
:root, .dark, body, .gradio-container {
|
| 494 |
+
--body-background-fill: #f5f7fa !important;
|
| 495 |
+
--body-text-color: #1f2937 !important;
|
| 496 |
+
--background-fill-primary: #ffffff !important;
|
| 497 |
+
--background-fill-secondary: #f3f4f6 !important;
|
| 498 |
+
--border-color-primary: #e5e7eb !important;
|
| 499 |
+
--block-background-fill: #ffffff !important;
|
| 500 |
+
--block-label-text-color: #374151 !important;
|
| 501 |
+
--block-title-text-color: #111827 !important;
|
| 502 |
+
--input-background-fill: #ffffff !important;
|
| 503 |
+
color-scheme: light !important;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
/* --- IP Reset Guide CSS --- */
|
| 507 |
+
:root {
|
| 508 |
+
--guide-bg: rgba(255, 255, 255, 0.98);
|
| 509 |
+
--guide-border: rgba(102, 126, 234, 0.2);
|
| 510 |
+
--guide-text-title: #2d3748;
|
| 511 |
+
--guide-text-body: #4a5568;
|
| 512 |
+
--guide-accent: #667eea;
|
| 513 |
+
--primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 514 |
+
--success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
|
| 515 |
+
--radius-md-guide: 12px;
|
| 516 |
+
--radius-lg-guide: 16px;
|
| 517 |
+
--shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
|
| 518 |
+
--shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
|
| 519 |
+
--shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
@keyframes float {
|
| 523 |
+
0%, 100% { transform: translateY(0px); }
|
| 524 |
+
50% { transform: translateY(-10px); }
|
| 525 |
+
}
|
| 526 |
+
@keyframes slideInUp {
|
| 527 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 528 |
+
to { opacity: 1; transform: translateY(0); }
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.ip-reset-guide-container {
|
| 532 |
+
text-align: right;
|
| 533 |
+
direction: rtl;
|
| 534 |
+
background: var(--guide-bg);
|
| 535 |
+
backdrop-filter: blur(10px);
|
| 536 |
+
padding: 20px; /* Increased Padding */
|
| 537 |
+
border-radius: var(--radius-lg-guide);
|
| 538 |
+
box-shadow: var(--shadow-xl);
|
| 539 |
+
border: 1px solid var(--guide-border);
|
| 540 |
+
animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) both;
|
| 541 |
+
width: 90%;
|
| 542 |
+
max-width: 420px; /* Increased width slightly */
|
| 543 |
+
max-height: 90vh; /* Prevent overflow on small screens */
|
| 544 |
+
overflow-y: auto; /* Enable scroll if needed */
|
| 545 |
+
position: relative;
|
| 546 |
+
box-sizing: border-box;
|
| 547 |
+
font-family: 'Vazirmatn', sans-serif !important;
|
| 548 |
+
}
|
| 549 |
+
.ip-reset-guide-container::before {
|
| 550 |
+
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--primary-gradient-guide);
|
| 551 |
+
}
|
| 552 |
+
.guide-header { display: flex; align-items: center; margin-bottom: 15px; }
|
| 553 |
+
.guide-header-icon { width: 45px; height: 45px; margin-left: 15px; animation: float 3s ease-in-out infinite; flex-shrink: 0; }
|
| 554 |
+
.guide-header h2 { font-size: 1.2rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
|
| 555 |
+
.guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 3px; margin-bottom: 0; }
|
| 556 |
+
|
| 557 |
+
.guide-content { font-size: 0.9rem; color: var(--guide-text-body); line-height: 1.6; }
|
| 558 |
+
|
| 559 |
+
.info-card { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: var(--radius-md-guide); padding: 12px; margin: 12px 0; position: relative; overflow: hidden; }
|
| 560 |
+
.info-card p { font-size: 0.85rem; line-height: 1.6; margin: 0; }
|
| 561 |
+
.info-card::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--primary-gradient-guide); }
|
| 562 |
+
.info-card-header { display: flex; align-items: center; margin-bottom: 8px; }
|
| 563 |
+
.info-card-icon { width: 18px; height: 18px; margin-left: 8px; }
|
| 564 |
+
.info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 0.95rem; }
|
| 565 |
+
|
| 566 |
+
.summary-section { margin-top: 12px; padding: 12px; border-radius: var(--radius-md-guide); background: linear-gradient(135deg, #56ab2f15 0%, #a8e06315 100%); border: 1px solid rgba(86, 171, 47, 0.2); position: relative; overflow: hidden; }
|
| 567 |
+
.summary-section::before { content: ''; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: var(--success-gradient-guide); }
|
| 568 |
+
.summary-header { display: flex; align-items: center; margin-bottom: 8px; }
|
| 569 |
+
.summary-icon { width: 18px; height: 18px; margin-left: 8px; }
|
| 570 |
+
.summary-title { font-weight: 600; color: #2f5a33; font-size: 0.95rem; }
|
| 571 |
+
.summary-text { color: #2f5a33; font-size: 0.85rem; line-height: 1.6; }
|
| 572 |
+
|
| 573 |
+
/* Tutorial Button */
|
| 574 |
+
.video-button-container { text-align: center; margin: 20px 0 15px 0; width: 100%; }
|
| 575 |
+
.elegant-video-button {
|
| 576 |
+
display: inline-flex !important;
|
| 577 |
+
align-items: center;
|
| 578 |
+
justify-content: center;
|
| 579 |
+
padding: 10px 20px !important;
|
| 580 |
+
background-color: #fff !important;
|
| 581 |
+
color: var(--guide-accent) !important;
|
| 582 |
+
border: 1px solid #e2e8f0 !important;
|
| 583 |
+
text-decoration: none;
|
| 584 |
+
border-radius: 50px !important;
|
| 585 |
+
font-weight: 600 !important;
|
| 586 |
+
font-size: 0.9rem !important;
|
| 587 |
+
cursor: pointer !important;
|
| 588 |
+
font-family: inherit;
|
| 589 |
+
transition: all 0.3s ease !important;
|
| 590 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05) !important;
|
| 591 |
+
width: auto !important;
|
| 592 |
+
}
|
| 593 |
+
.elegant-video-button:hover {
|
| 594 |
+
background: var(--primary-gradient-guide) !important;
|
| 595 |
+
color: white !important;
|
| 596 |
+
border-color: transparent !important;
|
| 597 |
+
transform: translateY(-2px);
|
| 598 |
+
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3) !important;
|
| 599 |
+
}
|
| 600 |
+
.elegant-video-button-icon { width: 18px; height: 18px; margin-left: 8px; fill: currentColor; }
|
| 601 |
+
|
| 602 |
+
/* Action Buttons */
|
| 603 |
+
.guide-actions {
|
| 604 |
+
display: flex !important;
|
| 605 |
+
gap: 12px !important;
|
| 606 |
+
margin-top: 20px;
|
| 607 |
+
padding-top: 20px;
|
| 608 |
+
border-top: 1px solid #e2e8f0;
|
| 609 |
+
width: 100% !important;
|
| 610 |
+
}
|
| 611 |
+
.action-button {
|
| 612 |
+
padding: 12px 15px !important;
|
| 613 |
+
border: none !important;
|
| 614 |
+
border-radius: 12px !important;
|
| 615 |
+
font-size: 0.95rem !important;
|
| 616 |
+
font-weight: 600 !important;
|
| 617 |
+
cursor: pointer !important;
|
| 618 |
+
flex: 1 !important;
|
| 619 |
+
transition: all 0.3s ease !important;
|
| 620 |
+
display: flex !important;
|
| 621 |
+
align-items: center;
|
| 622 |
+
justify-content: center;
|
| 623 |
+
font-family: inherit;
|
| 624 |
+
height: 48px !important;
|
| 625 |
+
}
|
| 626 |
+
.action-button-icon { width: 20px; height: 20px; margin-right: 0; margin-left: 8px; }
|
| 627 |
+
|
| 628 |
+
.back-button {
|
| 629 |
+
background: white !important;
|
| 630 |
+
color: var(--guide-text-body) !important;
|
| 631 |
+
border: 2px solid #e2e8f0 !important;
|
| 632 |
+
}
|
| 633 |
+
.back-button:hover {
|
| 634 |
+
background: #f7fafc !important;
|
| 635 |
+
border-color: var(--guide-accent) !important;
|
| 636 |
+
transform: translateY(-2px);
|
| 637 |
+
box-shadow: var(--shadow-md) !important;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.retry-button {
|
| 641 |
+
background: var(--primary-gradient-guide) !important;
|
| 642 |
+
color: white !important;
|
| 643 |
+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
|
| 644 |
+
}
|
| 645 |
+
.retry-button:hover {
|
| 646 |
+
transform: translateY(-2px);
|
| 647 |
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
/* --- Main App CSS --- */
|
| 651 |
+
body {
|
| 652 |
+
font-family: 'Vazirmatn', sans-serif !important;
|
| 653 |
+
background-color: #f5f7fa !important;
|
| 654 |
+
margin: 0;
|
| 655 |
+
padding: 10px;
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
#col-container {
|
| 659 |
+
margin: 0 auto;
|
| 660 |
+
max-width: 980px;
|
| 661 |
+
direction: rtl;
|
| 662 |
+
text-align: right;
|
| 663 |
+
padding: 30px;
|
| 664 |
+
background: #ffffff !important;
|
| 665 |
+
border-radius: 24px;
|
| 666 |
+
box-shadow: 0 10px 40px -10px rgba(0,0,0,0.08);
|
| 667 |
+
border: 1px solid rgba(255,255,255,0.8);
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
#main-title h1 {
|
| 671 |
+
font-size: 2.4em !important;
|
| 672 |
+
text-align: center;
|
| 673 |
+
color: #1a202c !important;
|
| 674 |
+
margin-bottom: 15px;
|
| 675 |
+
font-weight: 800;
|
| 676 |
+
background: -webkit-linear-gradient(45deg, #2563eb, #1e40af);
|
| 677 |
+
-webkit-background-clip: text;
|
| 678 |
+
-webkit-text-fill-color: transparent;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
#main-description {
|
| 682 |
text-align: center;
|
| 683 |
+
font-size: 1.15em;
|
| 684 |
+
color: #4b5563 !important;
|
| 685 |
+
margin-bottom: 40px;
|
| 686 |
+
line-height: 1.6;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
.gr-input-label, span.label-wrap, label span {
|
| 690 |
+
font-weight: 700 !important;
|
| 691 |
+
color: #374151 !important;
|
| 692 |
+
font-size: 0.95em !important;
|
| 693 |
+
margin-bottom: 8px !important;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
textarea, input[type="text"] {
|
| 697 |
+
border: 2px solid #e2e8f0 !important;
|
| 698 |
+
border-radius: 12px !important;
|
| 699 |
+
background-color: #ffffff !important;
|
| 700 |
+
color: #111827 !important;
|
| 701 |
+
padding: 12px !important;
|
| 702 |
+
transition: all 0.3s ease;
|
| 703 |
+
font-family: 'Vazirmatn', sans-serif !important;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
textarea:focus, input[type="text"]:focus {
|
| 707 |
+
border-color: #3b82f6 !important;
|
| 708 |
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1) !important;
|
| 709 |
+
outline: none;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
.gr-dropdown {
|
| 713 |
+
background: #ffffff !important;
|
| 714 |
+
border-radius: 12px !important;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.primary-btn, button.primary {
|
| 718 |
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
|
| 719 |
+
border: none !important;
|
| 720 |
+
color: white !important;
|
| 721 |
+
font-weight: 700 !important;
|
| 722 |
+
font-size: 1.1em !important;
|
| 723 |
+
padding: 14px 28px !important;
|
| 724 |
+
border-radius: 14px !important;
|
| 725 |
+
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3) !important;
|
| 726 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
| 727 |
+
cursor: pointer !important;
|
| 728 |
+
width: 100%;
|
| 729 |
margin-top: 15px;
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
.primary-btn:hover, button.primary:hover {
|
| 733 |
+
transform: translateY(-2px);
|
| 734 |
+
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.45) !important;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.primary-btn:active, button.primary:active {
|
| 738 |
+
transform: translateY(1px);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
#download-btn {
|
| 742 |
+
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
| 743 |
+
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
|
| 744 |
+
}
|
| 745 |
+
#download-btn:hover {
|
| 746 |
+
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.45) !important;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.gradio-container .prose table,
|
| 750 |
+
.gradio-container table {
|
| 751 |
+
background-color: #ffffff !important;
|
| 752 |
+
color: #111827 !important;
|
| 753 |
+
border: 1px solid #e5e7eb !important;
|
| 754 |
+
border-radius: 12px !important;
|
| 755 |
+
overflow: hidden !important;
|
| 756 |
+
width: 100% !important;
|
| 757 |
+
margin-top: 20px !important;
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
.gradio-container thead th {
|
| 761 |
+
background-color: #f3f4f6 !important;
|
| 762 |
+
color: #374151 !important;
|
| 763 |
+
font-weight: 700 !important;
|
| 764 |
+
border-bottom: 2px solid #e5e7eb !important;
|
| 765 |
+
padding: 12px !important;
|
| 766 |
+
text-align: right !important;
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
.gradio-container tbody tr {
|
| 770 |
+
background-color: #ffffff !important;
|
| 771 |
+
border-bottom: 1px solid #f3f4f6 !important;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
.gradio-container tbody tr:hover {
|
| 775 |
+
background-color: #f9fafb !important;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
.gradio-container tbody td {
|
| 779 |
+
background-color: #ffffff !important;
|
| 780 |
+
color: #374151 !important;
|
| 781 |
+
padding: 10px !important;
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
.gradio-container tbody td span,
|
| 785 |
+
.gradio-container tbody td p {
|
| 786 |
+
color: #374151 !important;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
footer { display: none !important; }
|
| 790 |
+
.flagging { display: none !important; }
|
| 791 |
+
|
| 792 |
+
/* Force toast transparency */
|
| 793 |
+
.toast-body {
|
| 794 |
+
direction: rtl !important;
|
| 795 |
+
text-align: right !important;
|
| 796 |
+
background: transparent !important;
|
| 797 |
+
box-shadow: none !important;
|
| 798 |
+
border: none !important;
|
| 799 |
+
padding: 0 !important;
|
| 800 |
+
max-width: 100% !important;
|
| 801 |
+
width: auto !important;
|
| 802 |
+
}
|
| 803 |
+
.toast-wrap {
|
| 804 |
+
background: transparent !important;
|
| 805 |
+
border: none !important;
|
| 806 |
box-shadow: none !important;
|
|
|
|
| 807 |
}
|
| 808 |
|
| 809 |
+
@media (prefers-color-scheme: dark) {
|
| 810 |
+
body, .gradio-container, .prose, table, tr, td, th {
|
| 811 |
+
background-color: #ffffff !important;
|
| 812 |
+
color: #333333 !important;
|
| 813 |
+
}
|
| 814 |
+
}
|
| 815 |
</style>
|
| 816 |
"""
|
| 817 |
|
| 818 |
+
# ادغام CSS و JS
|
| 819 |
+
combined_html = css_code + js_global_content
|
| 820 |
+
|
| 821 |
# استفاده از gr.Blocks
|
| 822 |
+
with gr.Blocks() as demo:
|
| 823 |
+
# تزریق کدها به عنوان HTML
|
| 824 |
+
gr.HTML(combined_html)
|
| 825 |
|
| 826 |
with gr.Column(elem_id="col-container"):
|
| 827 |
gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
|
| 828 |
+
gr.Markdown(
|
| 829 |
+
"با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
|
| 830 |
+
elem_id="main-description"
|
| 831 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
|
| 833 |
with gr.Row(equal_height=True):
|
| 834 |
with gr.Column():
|
| 835 |
input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
|
| 836 |
+
|
| 837 |
+
prompt = gr.Text(
|
| 838 |
+
label="دستور ویرایش (به فارسی)",
|
| 839 |
+
show_label=True,
|
| 840 |
+
placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
|
| 841 |
+
rtl=True,
|
| 842 |
+
lines=3
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
status_box = gr.HTML(label="وضعیت")
|
| 846 |
+
|
| 847 |
+
# Checkbox مخفی برای انتقال وضعیت VIP از جاوااسکریپت به پایتون
|
| 848 |
+
is_vip_state = gr.Checkbox(visible=False, value=False, label="VIP State")
|
| 849 |
+
|
| 850 |
+
# IMPORTANT: Added elem_id="run-btn" here for JS targeting
|
| 851 |
run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn")
|
| 852 |
|
| 853 |
with gr.Column():
|
| 854 |
output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
|
| 855 |
+
|
| 856 |
download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
|
|
|
|
| 857 |
|
| 858 |
+
with gr.Row():
|
| 859 |
+
lora_adapter = gr.Dropdown(
|
| 860 |
+
label="انتخاب سبک ویرایش (LoRA)",
|
| 861 |
+
choices=list(LORA_MAPPING.keys()),
|
| 862 |
+
value="تبدیل عکس به انیمه"
|
| 863 |
+
)
|
| 864 |
+
|
| 865 |
+
with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
|
| 866 |
+
aspect_ratio_selection = gr.Dropdown(
|
| 867 |
+
label="ابعاد تصویر خروجی",
|
| 868 |
+
choices=ASPECT_RATIOS_LIST,
|
| 869 |
+
value="خودکار (پیشفرض)",
|
| 870 |
+
interactive=True
|
| 871 |
+
)
|
| 872 |
+
|
| 873 |
with gr.Row(visible=False) as custom_dims_row:
|
| 874 |
+
custom_width = gr.Slider(
|
| 875 |
+
label="عرض دلخواه (Width)",
|
| 876 |
+
minimum=256, maximum=2048, step=8, value=1024
|
| 877 |
+
)
|
| 878 |
+
custom_height = gr.Slider(
|
| 879 |
+
label="ارتفاع دلخواه (Height)",
|
| 880 |
+
minimum=256, maximum=2048, step=8, value=1024
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
|
| 884 |
randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
|
| 885 |
+
guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
|
| 886 |
+
steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
|
| 887 |
|
| 888 |
+
# اصلاح تابع نمایش ردیف اسلایدرها
|
| 889 |
def toggle_row(choice):
|
| 890 |
+
if choice == "شخصیسازی (Custom)":
|
| 891 |
+
return gr.update(visible=True)
|
| 892 |
+
return gr.update(visible=False)
|
| 893 |
+
|
| 894 |
+
aspect_ratio_selection.change(
|
| 895 |
+
fn=toggle_row,
|
| 896 |
+
inputs=aspect_ratio_selection,
|
| 897 |
+
outputs=custom_dims_row
|
| 898 |
+
)
|
| 899 |
|
| 900 |
gr.Examples(
|
| 901 |
+
examples=[
|
| 902 |
+
["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
|
| 903 |
+
["examples/5.jpg", "سایهها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
|
| 904 |
+
["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
|
| 905 |
+
["examples/2.jpeg", "دوربین را ۴۵ درجه به سمت چپ بچرخان.", "تغییر زاویه دید"],
|
| 906 |
+
["examples/7.jpg", "منبع نور را از سمت راست عقب قرار بده.", "نورپردازی چند زاویهای"],
|
| 907 |
+
["examples/10.jpeg", "کیفیت تصویر را افزایش بده (Upscale).", "افزایش کیفیت (Upscale)"],
|
| 908 |
+
["examples/7.jpg", "منبع نور را از پایین بتابان.", "نورپردازی چند زاویهای"],
|
| 909 |
+
["examples/2.jpeg", "زاویه دوربین را به نمای بالا گوشه راست تغییر بده.", "تغییر زاویه دید"],
|
| 910 |
+
["examples/9.jpg", "دوربین کمی به جلو حرکت میکند در حالی که نور خورشید از میان ابرها میتابد و درخششی نرم اطراف شبح شخصیت در مه ایجاد میکند. سبک سینمایی واقعی.", "صحنه بعدی (سینمایی)"],
|
| 911 |
+
["examples/8.jpg", "جزئیات پوست سوژه را برجستهتر و طبیعیتر کن.", "روتوش پوست"],
|
| 912 |
+
["examples/6.jpg", "دوربین را به نمای پایین به بالا تغییر بده.", "تغییر زاویه دید"],
|
| 913 |
+
],
|
| 914 |
+
inputs=[input_image, prompt, lora_adapter],
|
| 915 |
+
outputs=[output_image, seed, status_box],
|
| 916 |
+
fn=infer_example,
|
| 917 |
+
cache_examples=False,
|
| 918 |
+
label="نمونهها (برای تست کلیک کنید)"
|
| 919 |
)
|
| 920 |
|
| 921 |
+
# جاوااسکریپت برای تزریق متغیر سراسری JS به ورودی Checkbox پایتون
|
| 922 |
+
# این تابع مقدار window.userIsVip را میخواند و به عنوان آخرین آرگومان به تابع پایتون میفرستد
|
| 923 |
+
inject_vip_js = """
|
| 924 |
+
(img, p, lora, seed, rand, scale, steps, aspect, w, h, vip_dummy) => {
|
| 925 |
+
return [img, p, lora, seed, rand, scale, steps, aspect, w, h, window.userIsVip];
|
| 926 |
+
}
|
| 927 |
+
"""
|
| 928 |
+
|
| 929 |
run_button.click(
|
| 930 |
fn=infer,
|
| 931 |
+
inputs=[input_image, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, aspect_ratio_selection, custom_width, custom_height, is_vip_state],
|
| 932 |
outputs=[output_image, seed, status_box],
|
| 933 |
+
js=inject_vip_js,
|
| 934 |
api_name="predict"
|
| 935 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 936 |
|
| 937 |
+
download_button.click(
|
|
|
|
| 938 |
fn=None,
|
| 939 |
+
inputs=[output_image],
|
| 940 |
+
outputs=None,
|
| 941 |
+
js=js_download_func
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
|
| 944 |
if __name__ == "__main__":
|
| 945 |
demo.queue(max_size=30).launch(show_error=True)
|