import os
import gradio as gr
import numpy as np
import spaces
import torch
import random
from PIL import Image, ImageFilter
from typing import Iterable
from gradio.themes import Soft
from gradio.themes.utils import colors, fonts, sizes
from deep_translator import GoogleTranslator
from transformers import pipeline
from datetime import date
import json
# --- تعریف تم ---
colors.steel_blue = colors.Color(
name="steel_blue",
c50="#EBF3F8",
c100="#D3E5F0",
c200="#A8CCE1",
c300="#7DB3D2",
c400="#529AC3",
c500="#4682B4",
c600="#3E72A0",
c700="#36638C",
c800="#2E5378",
c900="#264364",
c950="#1E3450",
)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# --- تنظیمات سیستم اعتبار ---
USAGE_LIMIT = 5
DATA_FILE = "usage_data.json"
PREMIUM_PAGE_ID = '1149636'
# --- مدیریت ذخیرهسازی دادهها ---
def load_usage_data():
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'r') as f:
return json.load(f)
except:
return {}
return {}
def save_usage_data(data):
try:
with open(DATA_FILE, 'w') as f:
json.dump(data, f)
except Exception as e:
print(f"Error saving data: {e}")
# بارگذاری اولیه دادهها
usage_data_cache = load_usage_data()
# --- بارگذاری مدل تشخیص محتوای نامناسب (Image Safety) ---
print("Loading Safety Checker...")
safety_classifier = pipeline("image-classification", model="Falconsai/nsfw_image_detection", device=-1)
def is_image_nsfw(image):
if image is None: return False
try:
results = safety_classifier(image)
for result in results:
if result['label'] == 'nsfw' and result['score'] > 0.75:
return True
return False
except Exception as e:
print(f"Safety check error: {e}")
return False
# --- بارگذاری مدل اصلی ---
from diffusers import FlowMatchEulerDiscreteScheduler
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
dtype = torch.bfloat16
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Loading pipeline...")
pipe = QwenImageEditPlusPipeline.from_pretrained(
"Qwen/Qwen-Image-Edit-2509",
transformer=QwenImageTransformer2DModel.from_pretrained(
"linoyts/Qwen-Image-Edit-Rapid-AIO",
subfolder='transformer',
torch_dtype=dtype,
device_map='cuda'
),
torch_dtype=dtype
).to(device)
# بارگذاری LoRA ها
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")
pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles")
pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Light_restoration", weight_name="移除光影.safetensors", adapter_name="light-restoration")
pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Relight", weight_name="Qwen-Edit-Relight.safetensors", adapter_name="relight")
pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multi-Angle-Lighting", weight_name="多角度灯光-251116.safetensors", adapter_name="multi-angle-lighting")
pipe.load_lora_weights("tlennon-ie/qwen-edit-skin", weight_name="qwen-edit-skin_1.1_000002750.safetensors", adapter_name="edit-skin")
pipe.load_lora_weights("lovis93/next-scene-qwen-image-lora-2509", weight_name="next-scene_lora-v2-3000.safetensors", adapter_name="next-scene")
pipe.load_lora_weights("vafipas663/Qwen-Edit-2509-Upscale-LoRA", weight_name="qwen-edit-enhance_64-v3_000001000.safetensors", adapter_name="upscale-image")
pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
MAX_SEED = np.iinfo(np.int32).max
LORA_MAPPING = {
"تبدیل عکس به انیمه": "anime",
"تغییر زاویه دید": "multiple-angles",
"اصلاح نور و سایه": "light-restoration",
"نورپردازی مجدد (Relight)": "relight",
"نورپردازی چند زاویهای": "multi-angle-lighting",
"روتوش پوست": "edit-skin",
"صحنه بعدی (سینمایی)": "next-scene",
"افزایش کیفیت (Upscale)": "upscale-image"
}
ASPECT_RATIOS_LIST = [
"خودکار (پیشفرض)",
"۱:۱ (مربع - 1024x1024)",
"۱۶:۹ (افقی - 1344x768)",
"۹:۱۶ (عمودی - 768x1344)",
"شخصیسازی (Custom)"
]
ASPECT_RATIOS_MAP = {
"خودکار (پیشفرض)": "Auto",
"۱:۱ (مربع - 1024x1024)": (1024, 1024),
"۱۶:۹ (افقی - 1344x768)": (1344, 768),
"۹:۱۶ (عمودی - 768x1344)": (768, 1344),
"شخصیسازی (Custom)": "Custom"
}
# --- لیست کلمات ممنوعه پیشرفته (Strict Mode) ---
BANNED_WORDS = [
# General NSFW
"nsfw", "nude", "naked", "sex", "porn", "erotic", "xxx", "18+", "adult",
"explicit", "uncensored", "sexual", "lewd", "sensual", "lust", "horny",
# Anatomy (Explicit)
"breast", "breasts", "nipple", "nipples", "vagina", "pussy", "cunt",
"penis", "dick", "cock", "genital", "genitals", "groin", "pubic",
"ass", "butt", "buttocks", "anus", "anal", "rectum",
# Acts / Concepts
"intercourse", "masturbation", "orgasm", "blowjob", "bj", "cum", "sperm",
"ejaculation", "penetration", "fucking", "sucking", "licking",
# Clothing / Lingerie
"lingerie", "bikini", "swimwear", "underwear", "panties", "bra", "thong",
"topless", "bottomless", "undressed", "unclothed", "skimpy", "transparent",
"see-through",
# Fetish / Styles
"fetish", "bdsm", "bondage", "latex", "hentai", "ecchi", "ahegao",
"exhibitionism", "voyeur", "harem",
# Violence / Gore (Optional but recommended for strict safety)
"gore", "bloody", "blood", "kill", "murder", "dead", "torture", "abuse"
]
def check_text_safety(text):
"""
بررسی وجود کلمات ممنوعه در متن انگلیسی.
از متد whole word matching ساده استفاده میکند تا کلماتی مثل 'assassin' اشتباهاً حذف نشوند.
"""
if not text: return True
text_lower = text.lower()
# نرمالسازی متن برای جستجوی دقیقتر
# اضافه کردن فاصله به اول و آخر متن برای تشخیص کلمات اول و آخر جمله
padded_text = f" {text_lower} "
# حذف علائم نگارشی برای جلوگیری از دور زدن فیلتر (مثلاً sex.)
for char in [".", ",", "!", "?", "-", "_", "(", ")", "[", "]", "{", "}"]:
padded_text = padded_text.replace(char, " ")
for word in BANNED_WORDS:
# جستجو به صورت کلمه مستقل
if f" {word} " in padded_text:
print(f"Safety Triggered: Found banned word '{word}'")
return False
return True
def translate_prompt(text):
if not text:
return ""
try:
translated = GoogleTranslator(source='auto', target='en').translate(text)
return translated
except Exception as e:
print(f"Translation Error: {e}")
return text
def update_dimensions_on_upload(image):
if image is None:
return 1024, 1024
original_width, original_height = image.size
if original_width > original_height:
new_width = 1024
aspect_ratio = original_height / original_width
new_height = int(new_width * aspect_ratio)
else:
new_height = 1024
aspect_ratio = original_width / original_height
new_width = int(new_height * aspect_ratio)
new_width = (new_width // 8) * 8
new_height = (new_height // 8) * 8
return new_width, new_height
def update_sliders_visibility(choice):
if choice == "شخصیسازی (Custom)":
return gr.update(visible=True), gr.update(visible=True)
else:
return gr.update(visible=False), gr.update(visible=False)
def get_error_html(message):
return f"""
⛔
{message}
"""
def get_success_html(message):
return f"""
✅
{message}
"""
def get_quota_exceeded_html():
return """
💎
اعتبار رایگان امروز تمام شد
شما از ۵ تصویر رایگان امروز استفاده کردهاید.
برای ساخت تصاویر نامحدود و حرفهای، لطفا نسخه خود را ارتقا دهید.
"""
def get_user_record(fingerprint):
"""خواندن رکورد کاربر از دیتابیس بدون اعمال تغییرات"""
global usage_data_cache
if not fingerprint: return None
# Reload to get fresh data
usage_data_cache = load_usage_data()
today_str = date.today().isoformat()
user_record = usage_data_cache.get(fingerprint)
if not user_record or user_record.get("last_reset") != today_str:
return {"count": 0, "last_reset": today_str}
return user_record
def consume_quota(fingerprint):
"""کسر اعتبار کاربر"""
global usage_data_cache
today_str = date.today().isoformat()
usage_data_cache = load_usage_data()
user_record = usage_data_cache.get(fingerprint)
if not user_record or user_record.get("last_reset") != today_str:
user_record = {"count": 0, "last_reset": today_str}
user_record["count"] += 1
usage_data_cache[fingerprint] = user_record
save_usage_data(usage_data_cache)
return user_record["count"]
# --- تابع بررسی اولیه (بدون GPU) برای نمایش/مخفی کردن دکمهها ---
def check_initial_quota(fingerprint, subscription_status):
"""
این تابع به محض لود شدن صفحه توسط جاوا اسکریپت صدا زده میشود.
اگر اعتبار تمام شده باشد، دکمه ساخت را مخفی و دکمه ارتقا را نشان میدهد.
"""
if not fingerprint:
return gr.update(visible=True), gr.update(visible=False), None
if subscription_status == 'paid':
return gr.update(visible=True), gr.update(visible=False), None
user_record = get_user_record(fingerprint)
current_usage = user_record["count"] if user_record else 0
if current_usage >= USAGE_LIMIT:
# اعتبار تمام شده
return gr.update(visible=False), gr.update(visible=True), get_quota_exceeded_html()
else:
# اعتبار دارد
return gr.update(visible=True), gr.update(visible=False), None
@spaces.GPU(duration=30)
def infer(
input_image,
prompt,
lora_adapter_persian,
seed,
randomize_seed,
guidance_scale,
steps,
aspect_ratio_selection,
custom_width,
custom_height,
fingerprint,
subscription_status,
progress=gr.Progress(track_tqdm=True)
):
# بررسی نهایی اعتبار قبل از شروع پردازش سنگین
if subscription_status != 'paid':
user_record = get_user_record(fingerprint)
if user_record and user_record["count"] >= USAGE_LIMIT:
return (
None, seed, get_quota_exceeded_html(),
gr.update(visible=False), gr.update(visible=True)
)
# --- بررسیهای اولیه (تصویر و متن) قبل از کسر اعتبار ---
if input_image is None:
return None, seed, get_error_html("لطفاً ابتدا یک تصویر بارگذاری کنید."), gr.update(visible=True), gr.update(visible=False)
if is_image_nsfw(input_image):
return None, seed, get_error_html("تصویر ورودی دارای محتوای نامناسب است و پردازش نمیشود."), gr.update(visible=True), gr.update(visible=False)
# ترجمه و بررسی متن (مهم: قبل از کسر اعتبار)
english_prompt = translate_prompt(prompt)
if not check_text_safety(english_prompt):
return None, seed, get_error_html("متن درخواست شامل کلمات غیرمجاز، جنسی یا خشونتآمیز است. لطفاً متن را اصلاح کنید."), gr.update(visible=True), gr.update(visible=False)
# اگر همه چیز امن بود، حالا اعتبار را کسر میکنیم
if subscription_status != 'paid':
consume_quota(fingerprint)
adapter_internal_name = LORA_MAPPING.get(lora_adapter_persian)
if adapter_internal_name:
pipe.set_adapters([adapter_internal_name], adapter_weights=[1.0])
if randomize_seed:
seed = random.randint(0, MAX_SEED)
generator = torch.Generator(device=device).manual_seed(seed)
# اضافه کردن کلمات منفی ایمنی به صورت پیشفرض به پرامپت منفی
safety_negative = "nsfw, nude, naked, porn, sexual, xxx, breast, nipple, genital, vagina, penis, ass, lingerie, bikini, swimwear, underwear, fetish, topless, gore, violence, blood, explicit"
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"
final_negative_prompt = f"{safety_negative}, {base_negative}"
original_image = input_image.convert("RGB")
selection_value = ASPECT_RATIOS_MAP.get(aspect_ratio_selection)
if selection_value == "Custom":
width = (int(custom_width) // 8) * 8
height = (int(custom_height) // 8) * 8
elif selection_value == "Auto" or selection_value is None:
width, height = update_dimensions_on_upload(original_image)
else:
width, height = selection_value
try:
result = pipe(
image=original_image,
prompt=english_prompt,
negative_prompt=final_negative_prompt,
height=height,
width=width,
num_inference_steps=steps,
generator=generator,
true_cfg_scale=guidance_scale,
).images[0]
# بررسی نهایی تصویر تولید شده
if is_image_nsfw(result):
return None, seed, get_error_html("تصویر تولید شده حاوی محتوای نامناسب بود و حذف شد."), gr.update(visible=True), gr.update(visible=False)
# محاسبه اعتبار باقیمانده برای نمایش
user_record = get_user_record(fingerprint)
remaining = USAGE_LIMIT - user_record["count"] if user_record else 0
success_msg = f"تصویر با موفقیت ویرایش شد."
if subscription_status != 'paid':
success_msg += f" (اعتبار باقیمانده امروز: {remaining})"
# اگر با این درخواست اعتبار تمام شد، دکمهها را آپدیت میکنیم
if subscription_status != 'paid' and remaining <= 0:
return result, seed, get_success_html(success_msg), gr.update(visible=False), gr.update(visible=True)
return result, seed, get_success_html(success_msg), gr.update(visible=True), gr.update(visible=False)
except Exception as e:
error_str = str(e)
# -------------------------------------------------------------
# مدیریت خطای GPU QUOTA
# -------------------------------------------------------------
if "quota" in error_str.lower() or "exceeded" in error_str.lower():
raise e
return None, seed, get_error_html(f"خطا در پردازش: {error_str}"), gr.update(visible=True), gr.update(visible=False)
@spaces.GPU(duration=30)
def infer_example(input_image, prompt, lora_adapter):
# مثالها اعتبار کم نمیکنند
res, s, status, btn1, btn2 = infer(input_image, prompt, lora_adapter, 0, True, 1.0, 4, "خودکار (پیشفرض)", 1024, 1024, "example_user", "paid")
return res, s, status
# --- جاوااسکریپت برای دکمه دانلود ---
js_download_func = """
async (image) => {
if (!image) {
alert("لطفاً ابتدا تصویر را تولید کنید.");
return;
}
let fileUrl = image.url;
if (fileUrl && !fileUrl.startsWith('http')) {
fileUrl = window.location.origin + fileUrl;
} else if (!fileUrl && image.path) {
fileUrl = window.location.origin + "/file=" + image.path;
}
window.parent.postMessage({
type: 'DOWNLOAD_REQUEST',
url: fileUrl
}, '*');
}
"""
# --- جاوااسکریپت برای دکمه ارتقا ---
js_upgrade_func = """
() => {
window.parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*');
}
"""
# --- جاوااسکریپت سراسری ---
js_global_content = """
"""
# --- CSS Updated ---
css_code = """
"""
# ادغام CSS و JS
combined_html = css_code + js_global_content
# استفاده از gr.Blocks
with gr.Blocks() as demo:
# تزریق کدها به عنوان HTML
gr.HTML(combined_html)
# اینپوتهای مخفی برای انتقال دیتای کاربر از JS به Python
fingerprint_box = gr.Textbox(elem_id="fingerprint_storage", visible=True)
status_box_input = gr.Textbox(elem_id="status_storage", visible=True)
with gr.Column(elem_id="col-container"):
gr.Markdown("# **ویرایشگر هوشمند آلفا**", elem_id="main-title")
gr.Markdown(
"با هوش مصنوعی آلفا تصاویر تونو به مدل های مختلف ویرایش کنید.",
elem_id="main-description"
)
# محل قرارگیری بج (نشانگر) وضعیت اشتراک
gr.HTML('
')
with gr.Row(equal_height=True):
with gr.Column():
input_image = gr.Image(label="بارگذاری تصویر", type="pil", height=320)
prompt = gr.Text(
label="دستور ویرایش (به فارسی)",
show_label=True,
placeholder="مثال: تصویر را به سبک انیمه تبدیل کن...",
rtl=True,
lines=3
)
status_box = gr.HTML(label="وضعیت")
# --- دکمهها ---
# دکمه ساخت تصویر (پیشفرض نمایش)
run_button = gr.Button("✨ شروع پردازش و ساخت تصویر", variant="primary", elem_classes="primary-btn", elem_id="run-btn", visible=True)
# دکمه ارتقا (پیشفرض مخفی)
upgrade_button = gr.Button("💎 خرید نسخه نامحدود (کلیک کنید)", variant="primary", elem_classes="upgrade-btn", elem_id="upgrade-btn", visible=False)
with gr.Column():
output_image = gr.Image(label="تصویر نهایی", interactive=False, format="png", height=380)
download_button = gr.Button("📥 دانلود و ذخیره تصویر", variant="secondary", elem_id="download-btn", elem_classes="primary-btn")
with gr.Row():
lora_adapter = gr.Dropdown(
label="انتخاب سبک ویرایش (LoRA)",
choices=list(LORA_MAPPING.keys()),
value="تبدیل عکس به انیمه"
)
with gr.Accordion("تنظیمات پیشرفته", open=False, visible=True):
aspect_ratio_selection = gr.Dropdown(
label="ابعاد تصویر خروجی",
choices=ASPECT_RATIOS_LIST,
value="خودکار (پیشفرض)",
interactive=True
)
with gr.Row(visible=False) as custom_dims_row:
custom_width = gr.Slider(
label="عرض دلخواه (Width)",
minimum=256, maximum=2048, step=8, value=1024
)
custom_height = gr.Slider(
label="ارتفاع دلخواه (Height)",
minimum=256, maximum=2048, step=8, value=1024
)
seed = gr.Slider(label="دانه تصادفی (Seed)", minimum=0, maximum=MAX_SEED, step=1, value=0)
randomize_seed = gr.Checkbox(label="استفاده از Seed تصادفی", value=True)
guidance_scale = gr.Slider(label="میزان وفاداری به متن (Guidance Scale)", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
steps = gr.Slider(label="تعداد مراحل پردازش (Steps)", minimum=1, maximum=50, step=1, value=4)
# اصلاح تابع نمایش ردیف اسلایدرها
def toggle_row(choice):
if choice == "شخصیسازی (Custom)":
return gr.update(visible=True)
return gr.update(visible=False)
aspect_ratio_selection.change(
fn=toggle_row,
inputs=aspect_ratio_selection,
outputs=custom_dims_row
)
gr.Examples(
examples=[
["examples/1.jpg", "تبدیل به انیمه کن.", "تبدیل عکس به انیمه"],
["examples/5.jpg", "سایهها را حذف کن و نورپردازی نرم به تصویر بده.", "اصلاح نور و سایه"],
["examples/4.jpg", "از فیلتر ساعت طلایی با پخش نور ملایم استفاده کن.", "نورپردازی مجدد (Relight)"],
["examples/2.jpeg", "دوربین را ۴۵ درجه به سمت چپ بچرخان.", "تغییر زاویه دید"],
["examples/7.jpg", "منبع نور را از سمت راست عقب قرار بده.", "نورپردازی چند زاویهای"],
["examples/10.jpeg", "کیفیت تصویر را افزایش بده (Upscale).", "افزایش کیفیت (Upscale)"],
["examples/7.jpg", "منبع نور را از پایین بتابان.", "نورپردازی چند زاویهای"],
["examples/2.jpeg", "زاویه دوربین را به نمای بالا گوشه راست تغییر بده.", "تغییر زاویه دید"],
["examples/9.jpg", "دوربین کمی به جلو حرکت میکند در حالی که نور خورشید از میان ابرها میتابد و درخششی نرم اطراف شبح شخصیت در مه ایجاد میکند. سبک سینمایی واقعی.", "صحنه بعدی (سینمایی)"],
["examples/8.jpg", "جزئیات پوست سوژه را برجستهتر و طبیعیتر کن.", "روتوش پوست"],
["examples/6.jpg", "دوربین را به نمای پایین به بالا تغییر بده.", "تغییر زاویه دید"],
],
inputs=[input_image, prompt, lora_adapter],
outputs=[output_image, seed, status_box],
fn=infer_example,
cache_examples=False,
label="نمونهها (برای تست کلیک کنید)"
)
# --- سیستم هوشمند: بررسی اعتبار در لحظه لود شدن ---
fingerprint_box.change(
fn=check_initial_quota,
inputs=[fingerprint_box, status_box_input],
outputs=[run_button, upgrade_button, status_box]
)
# اجرای اصلی
run_button.click(
fn=infer,
inputs=[
input_image, prompt, lora_adapter, seed, randomize_seed,
guidance_scale, steps, aspect_ratio_selection,
custom_width, custom_height,
fingerprint_box, status_box_input
],
outputs=[output_image, seed, status_box, run_button, upgrade_button],
api_name="predict"
)
# عملکرد دکمه ارتقا (فراخوانی JS)
upgrade_button.click(
fn=None,
js=js_upgrade_func
)
download_button.click(
fn=None,
inputs=[output_image],
outputs=None,
js=js_download_func
)
if __name__ == "__main__":
demo.queue(max_size=30).launch(show_error=True)