Spaces:
Running
Running
| import gradio as gr | |
| import threading | |
| import os | |
| import shutil | |
| import tempfile | |
| import time | |
| import json | |
| from util import process_image_edit, process_local_image_edit, download_and_check_result_nsfw | |
| from nfsw import NSFWDetector | |
| # i18n | |
| def load_translations(): | |
| translations = {} | |
| import os | |
| i18n_dir = "i18n/" | |
| for lang_file in os.listdir(i18n_dir): | |
| if lang_file.endswith('.json'): | |
| lang = lang_file[:-5] # Remove .json extension | |
| try: | |
| with open(os.path.join(i18n_dir, lang_file), "r", encoding="utf-8") as f: | |
| translations[lang] = json.load(f) | |
| except Exception as e: | |
| print(f"Failed to load {lang_file}: {e}") | |
| return translations | |
| translations = load_translations() | |
| def t(key, lang="en"): | |
| return translations.get(lang, {}).get(key, key) | |
| # Configuration parameters | |
| TIP_TRY_N = 4 # Show like button tip after 12 tries | |
| FREE_TRY_N = 8 # Free phase: first 15 tries without restrictions | |
| SLOW_TRY_N = 15 # Slow phase start: 25 tries | |
| SLOW2_TRY_N = 20 # Slow phase start: 32 tries | |
| RATE_LIMIT_60 = 25 # Full restriction: blocked after 40 tries | |
| # Time window configuration (minutes) | |
| PHASE_1_WINDOW = 3 # 15-25 tries: 5 minutes | |
| PHASE_2_WINDOW = 10 # 25-32 tries: 10 minutes | |
| PHASE_3_WINDOW = 20 # 32-40 tries: 20 minutes | |
| MAX_IMAGES_PER_WINDOW = 2 # Max images per time window | |
| IP_Dict = {} | |
| # IP generation statistics and time window tracking | |
| IP_Generation_Count = {} # Record total generation count for each IP | |
| IP_Rate_Limit_Track = {} # Record generation count and timestamp in current time window for each IP | |
| IP_Country_Cache = {} # Cache IP country information to avoid repeated queries | |
| # Country usage statistics | |
| Country_Usage_Stats = {} # Track usage count by country | |
| Total_Request_Count = 0 # Total request counter for periodic printing | |
| PRINT_STATS_INTERVAL = 10 # Print stats every N requests | |
| # Async IP query tracking | |
| IP_Query_Results = {} # Track async query results | |
| # Restricted countries list (these countries have lower usage limits) | |
| RESTRICTED_COUNTRIES = ["印度", "巴基斯坦"] | |
| RESTRICTED_COUNTRY_LIMIT = 5 # Max usage for restricted countries | |
| country_dict = { | |
| "zh": ["中国", "香港"], | |
| "fi": ["芬兰"], | |
| "en": ["美国", "澳大利亚", "英国", "加拿大", "新西兰", "爱尔兰"], | |
| "es": ["西班牙", "墨西哥", "阿根廷", "哥伦比亚", "智利", "秘鲁"], | |
| "pt": ["葡萄牙", "巴西"], | |
| "fr": ["法国", "摩纳哥"], | |
| "de": ["德国", "奥地利", ], | |
| "it": ["意大利", "圣马力诺", "梵蒂冈"], | |
| "ja": ["日本"], | |
| "ru": ["俄罗斯"], | |
| "uk": ["乌克兰"], | |
| "ar": ["沙特阿拉伯", "埃及", "阿拉伯联合酋长国", "摩洛哥"], | |
| "nl":["荷兰"], | |
| "no":["挪威"], | |
| "sv":["瑞典"], | |
| "id":["印度尼西亚"], | |
| "vi": ["越南"], | |
| "he": ["以色列"], | |
| "tr": ["土耳其"], | |
| "da": ["丹麦"], | |
| } | |
| def query_ip_country(client_ip): | |
| """ | |
| Query IP address geo information with robust error handling | |
| Features: | |
| - 3 second timeout limit | |
| - Comprehensive error handling | |
| - Automatic fallback to default values | |
| - Cache mechanism to avoid repeated queries | |
| Returns: | |
| dict: {"country": str, "region": str, "city": str} | |
| """ | |
| # Check cache first - no API call for subsequent visits | |
| if client_ip in IP_Country_Cache: | |
| print(f"Using cached IP data for {client_ip}") | |
| return IP_Country_Cache[client_ip] | |
| # Validate IP address | |
| if not client_ip or client_ip in ["127.0.0.1", "localhost", "::1"]: | |
| print(f"Invalid or local IP address: {client_ip}, using default") | |
| default_geo = {"country": "Unknown", "region": "Unknown", "city": "Unknown"} | |
| IP_Country_Cache[client_ip] = default_geo | |
| return default_geo | |
| # First time visit - query API with robust error handling | |
| print(f"Querying IP geolocation for {client_ip}...") | |
| try: | |
| import requests | |
| from requests.exceptions import Timeout, ConnectionError, RequestException | |
| api_url = f"https://api.vore.top/api/IPdata?ip={client_ip}" | |
| # Make request with 3 second timeout | |
| response = requests.get(api_url, timeout=3) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get("code") == 200 and "ipdata" in data: | |
| ipdata = data["ipdata"] | |
| geo_info = { | |
| "country": ipdata.get("info1", "Unknown"), | |
| "region": ipdata.get("info2", "Unknown"), | |
| "city": ipdata.get("info3", "Unknown") | |
| } | |
| IP_Country_Cache[client_ip] = geo_info | |
| print(f"Successfully detected location for {client_ip}: {geo_info['country']}") | |
| return geo_info | |
| else: | |
| print(f"API returned invalid data for {client_ip}: {data}") | |
| else: | |
| print(f"API request failed with status {response.status_code} for {client_ip}") | |
| except Timeout: | |
| print(f"Timeout (>3s) querying IP location for {client_ip}, using default") | |
| except ConnectionError: | |
| print(f"Network connection error for IP {client_ip}, using default") | |
| except RequestException as e: | |
| print(f"Request error for IP {client_ip}: {e}, using default") | |
| except Exception as e: | |
| print(f"Unexpected error querying IP {client_ip}: {e}, using default") | |
| # All failures lead here - cache default and return | |
| default_geo = {"country": "Unknown", "region": "Unknown", "city": "Unknown"} | |
| IP_Country_Cache[client_ip] = default_geo | |
| print(f"Cached default location for {client_ip}") | |
| return default_geo | |
| def query_ip_country_async(client_ip): | |
| """ | |
| Async version that returns immediately with default, then updates cache in background | |
| Returns: | |
| tuple: (immediate_lang, geo_info_or_none) | |
| """ | |
| # If already cached, return immediately | |
| if client_ip in IP_Country_Cache: | |
| geo_info = IP_Country_Cache[client_ip] | |
| lang = get_lang_from_country(geo_info["country"]) | |
| return lang, geo_info | |
| # Return default immediately, query in background | |
| return "en", None | |
| def get_lang_from_country(country): | |
| """ | |
| Map country name to language code with comprehensive validation | |
| Features: | |
| - Handles invalid/empty input | |
| - Case-insensitive matching | |
| - Detailed logging | |
| - Always returns valid language code | |
| Args: | |
| country (str): Country name | |
| Returns: | |
| str: Language code (always valid, defaults to "en") | |
| """ | |
| # Input validation | |
| if not country or not isinstance(country, str) or country.strip() == "": | |
| print(f"Invalid country provided: '{country}', defaulting to English") | |
| return "en" | |
| # Normalize country name | |
| country = country.strip() | |
| if country.lower() == "unknown": | |
| print(f"Unknown country, defaulting to English") | |
| return "en" | |
| try: | |
| # Search in country dictionary with case-sensitive match first | |
| for lang, countries in country_dict.items(): | |
| if country in countries: | |
| print(f"Matched country '{country}' to language '{lang}'") | |
| return lang | |
| # If no exact match, try case-insensitive match | |
| country_lower = country.lower() | |
| for lang, countries in country_dict.items(): | |
| for country_variant in countries: | |
| if country_variant.lower() == country_lower: | |
| print(f"Case-insensitive match: country '{country}' to language '{lang}'") | |
| return lang | |
| # No match found | |
| print(f"Country '{country}' not found in country_dict, defaulting to English") | |
| return "en" | |
| except Exception as e: | |
| print(f"Error matching country '{country}': {e}, defaulting to English") | |
| return "en" | |
| def get_lang_from_ip(client_ip): | |
| """ | |
| Get language based on IP geolocation with comprehensive error handling | |
| Features: | |
| - Validates input IP address | |
| - Handles all possible exceptions | |
| - Always returns a valid language code | |
| - Defaults to English on any failure | |
| - Includes detailed logging | |
| Args: | |
| client_ip (str): Client IP address | |
| Returns: | |
| str: Language code (always valid, defaults to "en") | |
| """ | |
| # Input validation | |
| if not client_ip or not isinstance(client_ip, str): | |
| print(f"Invalid IP address provided: {client_ip}, defaulting to English") | |
| return "en" | |
| try: | |
| # Query geolocation info (has its own error handling and 3s timeout) | |
| geo_info = query_ip_country(client_ip) | |
| if not geo_info or not isinstance(geo_info, dict): | |
| print(f"No geolocation data for {client_ip}, defaulting to English") | |
| return "en" | |
| # Extract country with fallback | |
| country = geo_info.get("country", "Unknown") | |
| if not country or country == "Unknown": | |
| print(f"Unknown country for IP {client_ip}, defaulting to English") | |
| return "en" | |
| # Map country to language | |
| detected_lang = get_lang_from_country(country) | |
| # Validate language code | |
| if not detected_lang or not isinstance(detected_lang, str) or len(detected_lang) != 2: | |
| print(f"Invalid language code '{detected_lang}' for {client_ip}, defaulting to English") | |
| return "en" | |
| print(f"IP {client_ip} -> Country: {country} -> Language: {detected_lang}") | |
| return detected_lang | |
| except Exception as e: | |
| print(f"Unexpected error getting language from IP {client_ip}: {e}, defaulting to English") | |
| return "en" # Always return a valid language code | |
| def is_restricted_country_ip(client_ip): | |
| """ | |
| Check if IP is from a restricted country | |
| Returns: | |
| bool: True if from restricted country | |
| """ | |
| geo_info = query_ip_country(client_ip) | |
| country = geo_info["country"] | |
| return country in RESTRICTED_COUNTRIES | |
| def get_ip_max_limit(client_ip): | |
| """ | |
| Get max usage limit for IP based on country | |
| Returns: | |
| int: Max usage limit | |
| """ | |
| if is_restricted_country_ip(client_ip): | |
| return RESTRICTED_COUNTRY_LIMIT | |
| else: | |
| return RATE_LIMIT_60 | |
| def get_ip_generation_count(client_ip): | |
| """ | |
| Get IP generation count | |
| """ | |
| if client_ip not in IP_Generation_Count: | |
| IP_Generation_Count[client_ip] = 0 | |
| return IP_Generation_Count[client_ip] | |
| def increment_ip_generation_count(client_ip): | |
| """ | |
| Increment IP generation count | |
| """ | |
| if client_ip not in IP_Generation_Count: | |
| IP_Generation_Count[client_ip] = 0 | |
| IP_Generation_Count[client_ip] += 1 | |
| return IP_Generation_Count[client_ip] | |
| def get_ip_phase(client_ip): | |
| """ | |
| Get current phase for IP | |
| Returns: | |
| str: 'free', 'rate_limit_1', 'rate_limit_2', 'rate_limit_3', 'blocked' | |
| """ | |
| count = get_ip_generation_count(client_ip) | |
| max_limit = get_ip_max_limit(client_ip) | |
| # For restricted countries, check if they've reached their limit | |
| if is_restricted_country_ip(client_ip): | |
| if count >= max_limit: | |
| return 'blocked' | |
| elif count >= max_limit - 2: # Last 2 attempts | |
| return 'rate_limit_3' | |
| elif count >= max_limit - 3: # 3rd attempt from end | |
| return 'rate_limit_2' | |
| elif count >= max_limit - 4: # 4th attempt from end | |
| return 'rate_limit_1' | |
| else: | |
| return 'free' | |
| # For normal countries, use standard limits | |
| if count < FREE_TRY_N: | |
| return 'free' | |
| elif count < SLOW_TRY_N: | |
| return 'rate_limit_1' # NSFW blur + 5 minutes 2 images | |
| elif count < SLOW2_TRY_N: | |
| return 'rate_limit_2' # NSFW blur + 10 minutes 2 images | |
| elif count < max_limit: | |
| return 'rate_limit_3' # NSFW blur + 20 minutes 2 images | |
| else: | |
| return 'blocked' # Generation blocked | |
| def check_rate_limit_for_phase(client_ip, phase): | |
| """ | |
| Check rate limit for specific phase | |
| Returns: | |
| tuple: (is_limited, wait_time_minutes, current_count) | |
| """ | |
| if phase not in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']: | |
| return False, 0, 0 | |
| # Determine time window | |
| if phase == 'rate_limit_1': | |
| window_minutes = PHASE_1_WINDOW | |
| elif phase == 'rate_limit_2': | |
| window_minutes = PHASE_2_WINDOW | |
| else: # rate_limit_3 | |
| window_minutes = PHASE_3_WINDOW | |
| current_time = time.time() | |
| window_key = f"{client_ip}_{phase}" | |
| # Clean expired records | |
| if window_key in IP_Rate_Limit_Track: | |
| track_data = IP_Rate_Limit_Track[window_key] | |
| # Check if within current time window | |
| if current_time - track_data['start_time'] > window_minutes * 60: | |
| # Time window expired, reset | |
| IP_Rate_Limit_Track[window_key] = { | |
| 'count': 0, | |
| 'start_time': current_time, | |
| 'last_generation': current_time | |
| } | |
| else: | |
| # Initialize | |
| IP_Rate_Limit_Track[window_key] = { | |
| 'count': 0, | |
| 'start_time': current_time, | |
| 'last_generation': current_time | |
| } | |
| track_data = IP_Rate_Limit_Track[window_key] | |
| # Check if exceeded limit | |
| if track_data['count'] >= MAX_IMAGES_PER_WINDOW: | |
| # Calculate remaining wait time | |
| elapsed = current_time - track_data['start_time'] | |
| wait_time = (window_minutes * 60) - elapsed | |
| wait_minutes = max(0, wait_time / 60) | |
| return True, wait_minutes, track_data['count'] | |
| return False, 0, track_data['count'] | |
| def update_country_stats(client_ip): | |
| """ | |
| Update country usage statistics and print periodically | |
| """ | |
| global Total_Request_Count, Country_Usage_Stats | |
| # Get country info | |
| geo_info = IP_Country_Cache.get(client_ip, {"country": "Unknown", "region": "Unknown", "city": "Unknown"}) | |
| country = geo_info["country"] | |
| # Update country stats | |
| if country not in Country_Usage_Stats: | |
| Country_Usage_Stats[country] = 0 | |
| Country_Usage_Stats[country] += 1 | |
| # Increment total request counter | |
| Total_Request_Count += 1 | |
| # Print stats every N requests | |
| if Total_Request_Count % PRINT_STATS_INTERVAL == 0: | |
| print("\n" + "="*60) | |
| print(f"📊 国家使用统计 (总请求数: {Total_Request_Count})") | |
| print("="*60) | |
| # Sort by usage count (descending) | |
| sorted_stats = sorted(Country_Usage_Stats.items(), key=lambda x: x[1], reverse=True) | |
| for country_name, count in sorted_stats: | |
| percentage = (count / Total_Request_Count) * 100 | |
| print(f" {country_name}: {count} 次 ({percentage:.1f}%)") | |
| print("="*60 + "\n") | |
| def record_generation_attempt(client_ip, phase): | |
| """ | |
| Record generation attempt | |
| """ | |
| # Increment total count | |
| increment_ip_generation_count(client_ip) | |
| # Update country statistics | |
| update_country_stats(client_ip) | |
| # Record time window count | |
| if phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']: | |
| window_key = f"{client_ip}_{phase}" | |
| current_time = time.time() | |
| if window_key in IP_Rate_Limit_Track: | |
| IP_Rate_Limit_Track[window_key]['count'] += 1 | |
| IP_Rate_Limit_Track[window_key]['last_generation'] = current_time | |
| else: | |
| IP_Rate_Limit_Track[window_key] = { | |
| 'count': 1, | |
| 'start_time': current_time, | |
| 'last_generation': current_time | |
| } | |
| def apply_gaussian_blur_to_image_url(image_url, blur_strength=50): | |
| """ | |
| Apply Gaussian blur to image URL | |
| Args: | |
| image_url (str): Original image URL | |
| blur_strength (int): Blur strength, default 50 (heavy blur) | |
| Returns: | |
| PIL.Image: Blurred PIL Image object | |
| """ | |
| try: | |
| import requests | |
| from PIL import Image, ImageFilter | |
| import io | |
| # Download image | |
| response = requests.get(image_url, timeout=30) | |
| if response.status_code != 200: | |
| return None | |
| # Convert to PIL Image | |
| image_data = io.BytesIO(response.content) | |
| image = Image.open(image_data) | |
| # Apply heavy Gaussian blur | |
| blurred_image = image.filter(ImageFilter.GaussianBlur(radius=blur_strength)) | |
| return blurred_image | |
| except Exception as e: | |
| print(f"⚠️ Failed to apply Gaussian blur: {e}") | |
| return None | |
| # Initialize NSFW detector (download from Hugging Face) | |
| try: | |
| nsfw_detector = NSFWDetector() # Auto download falconsai_yolov9_nsfw_model_quantized.pt from Hugging Face | |
| print("✅ NSFW detector initialized successfully") | |
| except Exception as e: | |
| print(f"❌ NSFW detector initialization failed: {e}") | |
| nsfw_detector = None | |
| def edit_image_interface(input_image, prompt, lang, request: gr.Request, progress=gr.Progress()): | |
| """ | |
| Interface function for processing image editing with phase-based limitations | |
| """ | |
| try: | |
| # Extract user IP | |
| client_ip = request.client.host | |
| x_forwarded_for = dict(request.headers).get('x-forwarded-for') | |
| if x_forwarded_for: | |
| client_ip = x_forwarded_for | |
| if client_ip not in IP_Dict: | |
| IP_Dict[client_ip] = 0 | |
| IP_Dict[client_ip] += 1 | |
| if input_image is None: | |
| return None, t("error_upload_first", lang), gr.update(visible=False) | |
| if not prompt or prompt.strip() == "": | |
| return None, t("error_enter_prompt", lang), gr.update(visible=False) | |
| # Check if prompt length is greater than 3 characters | |
| if len(prompt.strip()) <= 3: | |
| return None, t("error_prompt_too_short", lang), gr.update(visible=False) | |
| except Exception as e: | |
| print(f"⚠️ Request preprocessing error: {e}") | |
| return None, t("error_request_processing", lang), gr.update(visible=False) | |
| # Get user current phase | |
| current_phase = get_ip_phase(client_ip) | |
| current_count = get_ip_generation_count(client_ip) | |
| geo_info = IP_Country_Cache.get(client_ip, {"country": "Unknown", "region": "Unknown", "city": "Unknown"}) | |
| print(f"📊 User phase info - IP: {client_ip}, Location: {geo_info['country']}/{geo_info['region']}/{geo_info['city']}, Phase: {current_phase}, Count: {current_count}") | |
| # Check if user reached the like button tip threshold | |
| show_like_tip = (current_count >= TIP_TRY_N) | |
| # Check if completely blocked | |
| if current_phase == 'blocked': | |
| # Generate blocked limit button | |
| blocked_button_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 200px; | |
| box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🚀 Unlimited Generation</a> | |
| </div> | |
| """ | |
| # Use same message for all users to avoid discrimination perception | |
| blocked_message = t("error_free_limit_reached", lang) | |
| return None, blocked_message, gr.update(value=blocked_button_html, visible=True) | |
| # Check rate limit (applies to rate_limit phases) | |
| if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']: | |
| is_limited, wait_minutes, window_count = check_rate_limit_for_phase(client_ip, current_phase) | |
| if is_limited: | |
| wait_minutes_int = int(wait_minutes) + 1 | |
| # Generate rate limit button | |
| rate_limit_button_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 200px; | |
| box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>⏰ Skip Wait - Unlimited Generation</a> | |
| </div> | |
| """ | |
| return None, t("error_free_limit_wait", lang).format(wait_minutes_int=wait_minutes_int), gr.update(value=rate_limit_button_html, visible=True) | |
| # Handle NSFW detection based on phase | |
| is_nsfw_task = False # Track if this task involves NSFW content | |
| # Skip NSFW detection in free phase | |
| if current_phase != 'free' and nsfw_detector is not None and input_image is not None: | |
| try: | |
| nsfw_result = nsfw_detector.predict_pil_label_only(input_image) | |
| if nsfw_result.lower() == "nsfw": | |
| is_nsfw_task = True | |
| print(f"🔍 Input NSFW detected in {current_phase} phase: ❌❌❌ {nsfw_result} - IP: {client_ip} (will blur result)") | |
| else: | |
| print(f"🔍 Input NSFW check passed: ✅✅✅ {nsfw_result} - IP: {client_ip}") | |
| except Exception as e: | |
| print(f"⚠️ Input NSFW detection failed: {e}") | |
| # Allow continuation when detection fails | |
| result_url = None | |
| status_message = "" | |
| def progress_callback(message): | |
| try: | |
| nonlocal status_message | |
| status_message = message | |
| # Add error handling to prevent progress update failure | |
| if progress is not None: | |
| # Enhanced progress display with better formatting | |
| if "Queue:" in message or "tasks ahead" in message: | |
| # Queue status - show with different progress value to indicate waiting | |
| progress(0.1, desc=message) | |
| elif "Processing" in message or "AI is processing" in message: | |
| # Processing status | |
| progress(0.7, desc=message) | |
| elif "Generating" in message or "Almost done" in message: | |
| # Generation status | |
| progress(0.9, desc=message) | |
| else: | |
| # Default status | |
| progress(0.5, desc=message) | |
| except Exception as e: | |
| print(f"⚠️ Progress update failed: {e}") | |
| try: | |
| # Record generation attempt (before actual generation to ensure correct count) | |
| record_generation_attempt(client_ip, current_phase) | |
| updated_count = get_ip_generation_count(client_ip) | |
| print(f"✅ Processing started - IP: {client_ip}, phase: {current_phase}, total count: {updated_count}, prompt: {prompt.strip()}", flush=True) | |
| # Call image editing processing function | |
| input_image_url, result_url, message, task_uuid = process_image_edit(input_image, prompt.strip(), None, progress_callback) | |
| if result_url: | |
| print(f"✅ Processing completed successfully - IP: {client_ip}, result_url: {result_url}, task_uuid: {task_uuid}", flush=True) | |
| # Detect result image NSFW content (only in rate limit phases) | |
| if nsfw_detector is not None and current_phase != 'free': | |
| try: | |
| if progress is not None: | |
| progress(0.9, desc=t("status_checking_result", lang)) | |
| is_nsfw, nsfw_error = download_and_check_result_nsfw(result_url, nsfw_detector) | |
| if nsfw_error: | |
| print(f"⚠️ Result image NSFW detection error - IP: {client_ip}, error: {nsfw_error}") | |
| elif is_nsfw: | |
| is_nsfw_task = True # Mark task as NSFW | |
| print(f"🔍 Result image NSFW detected in {current_phase} phase: ❌❌❌ - IP: {client_ip} (will blur result)") | |
| else: | |
| print(f"🔍 Result image NSFW check passed: ✅✅✅ - IP: {client_ip}") | |
| except Exception as e: | |
| print(f"⚠️ Result image NSFW detection exception - IP: {client_ip}, error: {str(e)}") | |
| # Apply blur if this is an NSFW task in rate limit phases | |
| should_blur = False | |
| if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3'] and is_nsfw_task: | |
| should_blur = True | |
| # Apply blur processing | |
| if should_blur: | |
| if progress is not None: | |
| progress(0.95, desc=t("status_applying_filter", lang)) | |
| blurred_image = apply_gaussian_blur_to_image_url(result_url) | |
| if blurred_image is not None: | |
| final_result = blurred_image # Return PIL Image object | |
| final_message = t("warning_content_filter", lang) | |
| print(f"🔒 Applied Gaussian blur for NSFW content - IP: {client_ip}") | |
| else: | |
| # Blur failed, return original URL with warning | |
| final_result = result_url | |
| final_message = t("warning_content_review", lang) | |
| # Generate NSFW button for blurred content | |
| nsfw_action_buttons_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 200px; | |
| box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🔥 Unlimited Creative Generation</a> | |
| </div> | |
| """ | |
| return final_result, final_message, gr.update(value=nsfw_action_buttons_html, visible=True) | |
| else: | |
| final_result = result_url | |
| final_message = t("status_completed_message", lang).format(message=message) | |
| try: | |
| if progress is not None: | |
| progress(1.0, desc=t("status_processing_completed", lang)) | |
| except Exception as e: | |
| print(f"⚠️ Final progress update failed: {e}") | |
| # Generate action buttons HTML like Trump AI Voice | |
| action_buttons_html = "" | |
| if task_uuid: | |
| # Create image-to-video URL with input image, end image, and prompt | |
| from urllib.parse import quote | |
| # Use result URL as end_image, original upload URL as input_image | |
| encoded_prompt = quote(prompt.strip()) | |
| image_to_video_url = f"https://omnicreator.net/image-to-video?input_image={input_image_url}&end_image={result_url}&prompt={encoded_prompt}" | |
| action_buttons_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 160px; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🚀 Unlimited Generation</a> | |
| <a href='{image_to_video_url}' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 160px; | |
| box-shadow: 0 4px 15px rgba(17, 153, 142, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🎥 Turn Image to Video</a> | |
| </div> | |
| """ | |
| # Add popup script if needed (using different approach) | |
| if show_like_tip: | |
| action_buttons_html += """ | |
| <div style='display: flex; justify-content: center; margin: 15px 0 5px 0; padding: 0px;'> | |
| <div style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 12px 24px; | |
| background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); | |
| color: white; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| font-size: 14px; | |
| text-align: center; | |
| max-width: 400px; | |
| box-shadow: 0 3px 12px rgba(255, 107, 107, 0.3); | |
| border: none; | |
| '>👉 Click the ❤️ Like button to unlock more free trial attempts!</div> | |
| </div> | |
| """ | |
| return final_result, final_message, gr.update(value=action_buttons_html, visible=True) | |
| else: | |
| print(f"❌ Processing failed - IP: {client_ip}, error: {message}", flush=True) | |
| return None, t("error_processing_failed", lang).format(message=message), gr.update(visible=False) | |
| except Exception as e: | |
| print(f"❌ Processing exception - IP: {client_ip}, error: {str(e)}") | |
| return None, t("error_processing_exception", lang).format(error=str(e)), gr.update(visible=False) | |
| def local_edit_interface(image_dict, prompt, reference_image, lang, request: gr.Request, progress=gr.Progress()): | |
| """ | |
| Handle local editing requests (with phase-based limitations) | |
| """ | |
| try: | |
| # Extract user IP | |
| client_ip = request.client.host | |
| x_forwarded_for = dict(request.headers).get('x-forwarded-for') | |
| if x_forwarded_for: | |
| client_ip = x_forwarded_for | |
| if client_ip not in IP_Dict: | |
| IP_Dict[client_ip] = 0 | |
| IP_Dict[client_ip] += 1 | |
| if image_dict is None: | |
| return None, t("error_upload_and_draw", lang), gr.update(visible=False) | |
| # Handle different input formats for ImageEditor | |
| if isinstance(image_dict, dict): | |
| # ImageEditor dict format | |
| if "background" not in image_dict or "layers" not in image_dict: | |
| return None, t("error_draw_on_image", lang), gr.update(visible=False) | |
| base_image = image_dict["background"] | |
| layers = image_dict["layers"] | |
| # Special handling: if background is None but composite exists, use composite | |
| if base_image is None and "composite" in image_dict and image_dict["composite"] is not None: | |
| print("🔧 Background is None, using composite instead") | |
| base_image = image_dict["composite"] | |
| else: | |
| # Simple case: Direct PIL Image (from example) | |
| base_image = image_dict | |
| layers = [] | |
| # Check for special example case - bypass mask requirement | |
| is_example_case = prompt and prompt.startswith("EXAMPLE_PANDA_CAT_") | |
| # Debug: check current state | |
| if is_example_case: | |
| print(f"🔍 Example case detected - base_image is None: {base_image is None}") | |
| # Special handling for example case: load image directly from file | |
| if is_example_case and base_image is None: | |
| try: | |
| from PIL import Image | |
| import os | |
| main_path = "datas/panda01.jpeg" | |
| print(f"🔍 Trying to load: {main_path}, exists: {os.path.exists(main_path)}") | |
| if os.path.exists(main_path): | |
| base_image = Image.open(main_path) | |
| print(f"✅ Successfully loaded example image: {base_image.size}") | |
| else: | |
| return None, f"❌ Example image not found: {main_path}", gr.update(visible=False) | |
| except Exception as e: | |
| return None, f"❌ Failed to load example image: {str(e)}", gr.update(visible=False) | |
| # Additional check for base_image | |
| if base_image is None: | |
| if is_example_case: | |
| print(f"❌ Example case but base_image still None!") | |
| return None, t("error_no_image_found", lang), gr.update(visible=False) | |
| if not layers and not is_example_case: | |
| return None, t("error_draw_on_image", lang), gr.update(visible=False) | |
| if not prompt or prompt.strip() == "": | |
| return None, t("error_enter_prompt", lang), gr.update(visible=False) | |
| # Check prompt length | |
| if len(prompt.strip()) <= 3: | |
| return None, t("error_prompt_too_short", lang), gr.update(visible=False) | |
| except Exception as e: | |
| print(f"⚠️ Local edit request preprocessing error: {e}") | |
| return None, t("error_request_processing", lang), gr.update(visible=False) | |
| # Get user current phase | |
| current_phase = get_ip_phase(client_ip) | |
| current_count = get_ip_generation_count(client_ip) | |
| geo_info = IP_Country_Cache.get(client_ip, {"country": "Unknown", "region": "Unknown", "city": "Unknown"}) | |
| print(f"📊 Local edit user phase info - IP: {client_ip}, Location: {geo_info['country']}/{geo_info['region']}/{geo_info['city']}, Phase: {current_phase}, Count: {current_count}") | |
| # Check if user reached the like button tip threshold | |
| show_like_tip = (current_count >= TIP_TRY_N) | |
| # Check if completely blocked | |
| if current_phase == 'blocked': | |
| # Generate blocked limit button | |
| blocked_button_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 200px; | |
| box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🚀 Unlimited Generation</a> | |
| </div> | |
| """ | |
| # Use same message for all users to avoid discrimination perception | |
| blocked_message = t("error_free_limit_reached", lang) | |
| return None, blocked_message, gr.update(value=blocked_button_html, visible=True) | |
| # Check rate limit (applies to rate_limit phases) | |
| if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3']: | |
| is_limited, wait_minutes, window_count = check_rate_limit_for_phase(client_ip, current_phase) | |
| if is_limited: | |
| wait_minutes_int = int(wait_minutes) + 1 | |
| # Generate rate limit button | |
| rate_limit_button_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 200px; | |
| box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>⏰ Skip Wait - Unlimited Generation</a> | |
| </div> | |
| """ | |
| return None, t("error_free_limit_wait", lang).format(wait_minutes_int=wait_minutes_int), gr.update(value=rate_limit_button_html, visible=True) | |
| # Handle NSFW detection based on phase | |
| is_nsfw_task = False # Track if this task involves NSFW content | |
| # Skip NSFW detection in free phase | |
| if current_phase != 'free' and nsfw_detector is not None and base_image is not None: | |
| try: | |
| nsfw_result = nsfw_detector.predict_pil_label_only(base_image) | |
| if nsfw_result.lower() == "nsfw": | |
| is_nsfw_task = True | |
| print(f"🔍 Local edit input NSFW detected in {current_phase} phase: ❌❌❌ {nsfw_result} - IP: {client_ip} (will blur result)") | |
| else: | |
| print(f"🔍 Local edit input NSFW check passed: ✅✅✅ {nsfw_result} - IP: {client_ip}") | |
| except Exception as e: | |
| print(f"⚠️ Local edit input NSFW detection failed: {e}") | |
| # Allow continuation when detection fails | |
| result_url = None | |
| status_message = "" | |
| def progress_callback(message): | |
| try: | |
| nonlocal status_message | |
| status_message = message | |
| # Add error handling to prevent progress update failure | |
| if progress is not None: | |
| # Enhanced progress display with better formatting for local editing | |
| if "Queue:" in message or "tasks ahead" in message: | |
| # Queue status - show with different progress value to indicate waiting | |
| progress(0.1, desc=message) | |
| elif "Processing" in message or "AI is processing" in message: | |
| # Processing status | |
| progress(0.7, desc=message) | |
| elif "Generating" in message or "Almost done" in message: | |
| # Generation status | |
| progress(0.9, desc=message) | |
| else: | |
| # Default status | |
| progress(0.5, desc=message) | |
| except Exception as e: | |
| print(f"⚠️ Local edit progress update failed: {e}") | |
| try: | |
| # Record generation attempt (before actual generation to ensure correct count) | |
| record_generation_attempt(client_ip, current_phase) | |
| updated_count = get_ip_generation_count(client_ip) | |
| print(f"✅ Local editing started - IP: {client_ip}, phase: {current_phase}, total count: {updated_count}, prompt: {prompt.strip()}", flush=True) | |
| # Clean prompt for API call | |
| clean_prompt = prompt.strip() | |
| if clean_prompt.startswith("EXAMPLE_PANDA_CAT_"): | |
| clean_prompt = clean_prompt[18:] # Remove the prefix | |
| # Call local image editing processing function | |
| if is_example_case: | |
| # For example case, pass special flag to use local mask file | |
| input_image_url, result_url, message, task_uuid = process_local_image_edit(base_image, layers, clean_prompt, reference_image, progress_callback, use_example_mask="datas/panda01m.jpeg") | |
| else: | |
| # Normal case | |
| input_image_url, result_url, message, task_uuid = process_local_image_edit(base_image, layers, clean_prompt, reference_image, progress_callback) | |
| if result_url: | |
| print(f"✅ Local editing completed successfully - IP: {client_ip}, result_url: {result_url}, task_uuid: {task_uuid}", flush=True) | |
| # Detect result image NSFW content (only in rate limit phases) | |
| if nsfw_detector is not None and current_phase != 'free': | |
| try: | |
| if progress is not None: | |
| progress(0.9, desc=t("status_checking_result", lang)) | |
| is_nsfw, nsfw_error = download_and_check_result_nsfw(result_url, nsfw_detector) | |
| if nsfw_error: | |
| print(f"⚠️ Local edit result image NSFW detection error - IP: {client_ip}, error: {nsfw_error}") | |
| elif is_nsfw: | |
| is_nsfw_task = True # Mark task as NSFW | |
| print(f"🔍 Local edit result image NSFW detected in {current_phase} phase: ❌❌❌ - IP: {client_ip} (will blur result)") | |
| else: | |
| print(f"🔍 Local edit result image NSFW check passed: ✅✅✅ - IP: {client_ip}") | |
| except Exception as e: | |
| print(f"⚠️ Local edit result image NSFW detection exception - IP: {client_ip}, error: {str(e)}") | |
| # Apply blur if this is an NSFW task in rate limit phases | |
| should_blur = False | |
| if current_phase in ['rate_limit_1', 'rate_limit_2', 'rate_limit_3'] and is_nsfw_task: | |
| should_blur = True | |
| # Apply blur processing | |
| if should_blur: | |
| if progress is not None: | |
| progress(0.95, desc=t("status_applying_filter", lang)) | |
| blurred_image = apply_gaussian_blur_to_image_url(result_url) | |
| if blurred_image is not None: | |
| final_result = blurred_image # Return PIL Image object | |
| final_message = t("warning_content_filter", lang) | |
| print(f"🔒 Local edit applied Gaussian blur for NSFW content - IP: {client_ip}") | |
| else: | |
| # Blur failed, return original URL with warning | |
| final_result = result_url | |
| final_message = t("warning_content_review", lang) | |
| # Generate NSFW button for blurred content | |
| nsfw_action_buttons_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/#generator' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 200px; | |
| box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🔥 Unlimited Creative Generation</a> | |
| </div> | |
| """ | |
| return final_result, final_message, gr.update(value=nsfw_action_buttons_html, visible=True) | |
| else: | |
| final_result = result_url | |
| final_message = t("status_completed_message", lang).format(message=message) | |
| try: | |
| if progress is not None: | |
| progress(1.0, desc=t("status_processing_completed", lang)) | |
| except Exception as e: | |
| print(f"⚠️ Local edit final progress update failed: {e}") | |
| # Generate action buttons HTML like Trump AI Voice | |
| action_buttons_html = "" | |
| if task_uuid: | |
| # Create image-to-video URL with input image, end image, and prompt | |
| from urllib.parse import quote | |
| # Use result URL as end_image, original upload URL as input_image | |
| encoded_prompt = quote(clean_prompt) | |
| image_to_video_url = f"https://omnicreator.net/image-to-video?input_image={input_image_url}&end_image={result_url}&prompt={encoded_prompt}" | |
| action_buttons_html = f""" | |
| <div style='display: flex; justify-content: center; gap: 15px; margin: 10px 0 5px 0; padding: 0px;'> | |
| <a href='https://omnicreator.net/local-inpaint' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 160px; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🚀 Unlimited Generation</a> | |
| <a href='{image_to_video_url}' target='_blank' style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 16px 32px; | |
| background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-align: center; | |
| min-width: 160px; | |
| box-shadow: 0 4px 15px rgba(17, 153, 142, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| '>🎥 Turn Image to Video</a> | |
| </div> | |
| """ | |
| # Add popup script if needed (using different approach) | |
| if show_like_tip: | |
| action_buttons_html += """ | |
| <div style='display: flex; justify-content: center; margin: 15px 0 5px 0; padding: 0px;'> | |
| <div style=' | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 12px 24px; | |
| background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); | |
| color: white; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| font-size: 14px; | |
| text-align: center; | |
| max-width: 400px; | |
| box-shadow: 0 3px 12px rgba(255, 107, 107, 0.3); | |
| border: none; | |
| '>👉 Click the ❤️ Like button to unlock more free trial attempts!</div> | |
| </div> | |
| """ | |
| return final_result, final_message, gr.update(value=action_buttons_html, visible=True) | |
| else: | |
| print(f"❌ Local editing processing failed - IP: {client_ip}, error: {message}", flush=True) | |
| return None, t("error_processing_failed", lang).format(message=message), gr.update(visible=False) | |
| except Exception as e: | |
| print(f"❌ Local editing exception - IP: {client_ip}, error: {str(e)}") | |
| return None, t("error_processing_exception", lang).format(error=str(e)), gr.update(visible=False) | |
| # Create Gradio interface | |
| def create_app(): | |
| with gr.Blocks( | |
| title="AI Image Editor", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .main-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .news-banner-row { | |
| margin: 10px auto 15px auto; | |
| padding: 0 10px; | |
| max-width: 1200px; | |
| width: 100% !important; | |
| } | |
| .news-banner-row .gr-row { | |
| display: flex !important; | |
| align-items: center !important; | |
| width: 100% !important; | |
| } | |
| .news-banner-row .gr-column:first-child { | |
| flex: 1 !important; /* 占据所有剩余空间 */ | |
| display: flex !important; | |
| justify-content: center !important; /* 在其空间内居中 */ | |
| } | |
| .banner-lang-selector { | |
| margin-left: auto !important; | |
| display: flex !important; | |
| justify-content: flex-end !important; | |
| align-items: center !important; | |
| position: relative !important; | |
| z-index: 10 !important; | |
| } | |
| .banner-lang-selector .gr-dropdown { | |
| background: white !important; | |
| border: 1px solid #ddd !important; | |
| border-radius: 8px !important; | |
| padding: 8px 16px !important; | |
| font-size: 14px !important; | |
| font-weight: 500 !important; | |
| color: #333 !important; | |
| cursor: pointer !important; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; | |
| min-width: 140px !important; | |
| max-width: 160px !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .banner-lang-selector .gr-dropdown:hover { | |
| border-color: #999 !important; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important; | |
| } | |
| @media (max-width: 768px) { | |
| .news-banner-row { | |
| padding: 0 15px !important; | |
| } | |
| .news-banner-row .gr-row { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| gap: 10px !important; | |
| position: static !important; | |
| } | |
| .news-banner-row .gr-column:first-child { | |
| position: static !important; | |
| pointer-events: auto !important; | |
| } | |
| .banner-lang-selector { | |
| margin-left: 0 !important; | |
| justify-content: center !important; | |
| } | |
| } | |
| .upload-area { | |
| border: 2px dashed #ccc; | |
| border-radius: 10px; | |
| padding: 20px; | |
| text-align: center; | |
| } | |
| .result-area { | |
| margin-top: 20px; | |
| padding: 20px; | |
| border-radius: 10px; | |
| background-color: #f8f9fa; | |
| } | |
| .use-as-input-btn { | |
| margin-top: 10px; | |
| width: 100%; | |
| } | |
| """, | |
| # Improve concurrency performance configuration | |
| head=""" | |
| <script> | |
| // Reduce client-side state update frequency, avoid excessive SSE connections | |
| if (window.gradio) { | |
| window.gradio.update_frequency = 2000; // Update every 2 seconds | |
| } | |
| </script> | |
| """ | |
| ) as app: | |
| lang_state = gr.State("en") | |
| # Main title - centered | |
| header_title = gr.HTML(f""" | |
| <div style="text-align: center; margin: 20px auto 10px auto; max-width: 800px;"> | |
| <h1 style="color: #2c3e50; margin: 0; font-size: 3.5em; font-weight: 800; letter-spacing: 3px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);"> | |
| {t('header_title', 'en')} | |
| </h1> | |
| </div> | |
| """) | |
| # 🌟 NEW: Multi-Image Editing Announcement Banner with language selector | |
| with gr.Row(elem_classes=["news-banner-row"]): | |
| with gr.Column(scale=1, min_width=400): | |
| news_banner = gr.HTML(f""" | |
| <style> | |
| @keyframes breathe {{ | |
| 0%, 100% {{ transform: scale(1); }} | |
| 50% {{ transform: scale(1.02); }} | |
| }} | |
| .breathing-banner {{ | |
| animation: breathe 3s ease-in-out infinite; | |
| }} | |
| </style> | |
| <div class="breathing-banner" style=" | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| margin: 0 auto; | |
| padding: 8px 40px; | |
| border-radius: 20px; | |
| max-width: 600px; | |
| box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); | |
| text-align: center; | |
| width: fit-content; | |
| "> | |
| <span style="color: white; font-weight: 600; font-size: 1.0em;"> | |
| {t('news_banner_prefix', 'en')} | |
| <a href="https://huggingface.co/spaces/Selfit/Multi-Image-Edit" target="_blank" style=" | |
| color: white; | |
| text-decoration: none; | |
| border-bottom: 1px solid rgba(255,255,255,0.5); | |
| transition: all 0.3s ease; | |
| " onmouseover="this.style.borderBottom='1px solid white'" | |
| onmouseout="this.style.borderBottom='1px solid rgba(255,255,255,0.5)'"> | |
| {t('news_banner_link', 'en')} | |
| </a> | |
| </span> | |
| </div> | |
| """) | |
| with gr.Column(scale=0, min_width=160, elem_classes=["banner-lang-selector"]): | |
| lang_dropdown = gr.Dropdown( | |
| choices=[ | |
| ("English", "en"), | |
| ("中文", "zh"), | |
| ("Suomi", "fi"), | |
| ("Español", "es"), | |
| ("Português", "pt"), | |
| ("Français", "fr"), | |
| ("Deutsch", "de"), | |
| ("Italiano", "it"), | |
| ("日本語", "ja"), | |
| ("Русский", "ru"), | |
| ("Українська", "uk"), | |
| ("العربية", "ar"), | |
| ("Nederlands", "nl"), | |
| ("Norsk", "no"), | |
| ("Svenska", "sv"), | |
| ("Indonesian", "id"), | |
| ("Tiếng Việt", "vi"), | |
| ("עברית", "he"), | |
| ("Türkçe", "tr"), | |
| ("Dansk", "da") | |
| ], | |
| value="en", | |
| label="🌐", | |
| show_label=True, | |
| interactive=True, | |
| container=False | |
| ) | |
| with gr.Tabs() as tabs: | |
| with gr.Tab(t("global_editor_tab", "en")) as global_tab: | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| upload_image_header = gr.Markdown(t("upload_image_header", "en")) | |
| input_image = gr.Image( | |
| label=t("upload_image_label", "en"), | |
| type="pil", | |
| height=512, | |
| elem_classes=["upload-area"] | |
| ) | |
| editing_instructions_header = gr.Markdown(t("editing_instructions_header", "en")) | |
| prompt_input = gr.Textbox( | |
| label=t("prompt_input_label", "en"), | |
| placeholder=t("prompt_input_placeholder", "en"), | |
| lines=3, | |
| max_lines=5 | |
| ) | |
| edit_button = gr.Button( | |
| t("start_editing_button", "en"), | |
| variant="primary", | |
| size="lg" | |
| ) | |
| with gr.Column(scale=1): | |
| editing_result_header = gr.Markdown(t("editing_result_header", "en")) | |
| output_image = gr.Image( | |
| label=t("output_image_label", "en"), | |
| height=320, | |
| elem_classes=["result-area"] | |
| ) | |
| use_as_input_btn = gr.Button( | |
| t("use_as_input_button", "en"), | |
| variant="secondary", | |
| size="sm", | |
| elem_classes=["use-as-input-btn"] | |
| ) | |
| status_output = gr.Textbox( | |
| label=t("status_output_label", "en"), | |
| lines=2, | |
| max_lines=3, | |
| interactive=False | |
| ) | |
| action_buttons = gr.HTML(visible=False) | |
| prompt_examples_header = gr.Markdown(t("prompt_examples_header", "en")) | |
| with gr.Row(): | |
| example_prompts = [ | |
| "Set the background to a grand opera stage with red curtains", | |
| "Change the outfit into a traditional Chinese hanfu with flowing sleeves", | |
| "Give the character blue dragon-like eyes with glowing pupils", | |
| "Change lighting to soft dreamy pastel glow", | |
| "Change pose to sitting cross-legged on the ground" | |
| ] | |
| for prompt in example_prompts: | |
| gr.Button( | |
| prompt, | |
| size="sm" | |
| ).click( | |
| lambda p=prompt: p, | |
| outputs=prompt_input | |
| ) | |
| edit_button.click( | |
| fn=edit_image_interface, | |
| inputs=[input_image, prompt_input, lang_state], | |
| outputs=[output_image, status_output, action_buttons], | |
| show_progress=True, | |
| concurrency_limit=20 | |
| ) | |
| def simple_use_as_input(output_img): | |
| if output_img is not None: | |
| return output_img | |
| return None | |
| use_as_input_btn.click( | |
| fn=simple_use_as_input, | |
| inputs=[output_image], | |
| outputs=[input_image] | |
| ) | |
| with gr.Tab(t("local_inpaint_tab", "en")) as local_tab: | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| upload_and_draw_mask_header = gr.Markdown(t("upload_and_draw_mask_header", "en")) | |
| local_input_image = gr.ImageEditor( | |
| label=t("upload_and_draw_mask_label", "en"), | |
| type="pil", | |
| height=512, | |
| brush=gr.Brush(colors=["#ff0000"], default_size=180), | |
| elem_classes=["upload-area"] | |
| ) | |
| reference_image_header = gr.Markdown(t("reference_image_header", "en")) | |
| local_reference_image = gr.Image( | |
| label=t("reference_image_label", "en"), | |
| type="pil", | |
| height=256 | |
| ) | |
| local_editing_instructions_header = gr.Markdown(t("editing_instructions_header", "en")) | |
| local_prompt_input = gr.Textbox( | |
| label=t("local_prompt_input_label", "en"), | |
| placeholder=t("local_prompt_input_placeholder", "en"), | |
| lines=3, | |
| max_lines=5 | |
| ) | |
| local_edit_button = gr.Button( | |
| t("start_local_editing_button", "en"), | |
| variant="primary", | |
| size="lg" | |
| ) | |
| with gr.Column(scale=1): | |
| local_editing_result_header = gr.Markdown(t("editing_result_header", "en")) | |
| local_output_image = gr.Image( | |
| label=t("local_output_image_label", "en"), | |
| height=320, | |
| elem_classes=["result-area"] | |
| ) | |
| local_use_as_input_btn = gr.Button( | |
| t("use_as_input_button", "en"), | |
| variant="secondary", | |
| size="sm", | |
| elem_classes=["use-as-input-btn"] | |
| ) | |
| local_status_output = gr.Textbox( | |
| label=t("status_output_label", "en"), | |
| lines=2, | |
| max_lines=3, | |
| interactive=False | |
| ) | |
| local_action_buttons = gr.HTML(visible=False) | |
| local_edit_button.click( | |
| fn=local_edit_interface, | |
| inputs=[local_input_image, local_prompt_input, local_reference_image, lang_state], | |
| outputs=[local_output_image, local_status_output, local_action_buttons], | |
| show_progress=True, | |
| concurrency_limit=20 | |
| ) | |
| def simple_local_use_as_input(output_img): | |
| if output_img is not None: | |
| return { | |
| "background": output_img, | |
| "layers": [], | |
| "composite": output_img | |
| } | |
| return None | |
| local_use_as_input_btn.click( | |
| fn=simple_local_use_as_input, | |
| inputs=[local_output_image], | |
| outputs=[local_input_image] | |
| ) | |
| # Local inpaint example | |
| local_inpaint_example_header = gr.Markdown(t("local_inpaint_example_header", "en")) | |
| def load_local_example(): | |
| """Load panda to cat transformation example - simplified, mask handled in backend""" | |
| try: | |
| from PIL import Image | |
| import os | |
| # Check file paths | |
| main_path = "datas/panda01.jpeg" | |
| ref_path = "datas/cat01.webp" | |
| # Load main image | |
| if not os.path.exists(main_path): | |
| return None, None, "EXAMPLE_PANDA_CAT_let the cat ride on the panda" | |
| main_img = Image.open(main_path) | |
| # Load reference image | |
| if not os.path.exists(ref_path): | |
| ref_img = None | |
| else: | |
| ref_img = Image.open(ref_path) | |
| # ImageEditor format | |
| editor_data = { | |
| "background": main_img, | |
| "layers": [], | |
| "composite": main_img | |
| } | |
| # Special prompt to indicate this is the example case | |
| prompt = "EXAMPLE_PANDA_CAT_let the cat ride on the panda" | |
| # Return just the PIL image instead of dict format to avoid UI state issues | |
| return main_img, ref_img, prompt | |
| except Exception as e: | |
| return None, None, "EXAMPLE_PANDA_CAT_Transform the panda head into a cute cat head, keeping the body" | |
| # Example display | |
| panda_to_cat_example_header = gr.Markdown(t("panda_to_cat_example_header", "en")) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # Preview images for local example | |
| with gr.Row(): | |
| try: | |
| gr.Image("datas/panda01.jpeg", label=t("main_image_label", "en"), height=120, width=120, show_label=True, interactive=False) | |
| gr.Image("datas/panda01m.jpeg", label=t("mask_label", "en"), height=120, width=120, show_label=True, interactive=False) | |
| gr.Image("datas/cat01.webp", label=t("reference_label", "en"), height=120, width=120, show_label=True, interactive=False) | |
| except: | |
| gr.Markdown("*Preview images not available*") | |
| panda_example_note = gr.Markdown(t("panda_example_note", "en")) | |
| with gr.Column(scale=1): | |
| load_panda_example_button = gr.Button( | |
| t("load_panda_example_button", "en"), | |
| size="lg", | |
| variant="secondary" | |
| ) | |
| load_panda_example_button.click( | |
| fn=load_local_example, | |
| outputs=[local_input_image, local_reference_image, local_prompt_input] | |
| ) | |
| # Add a refresh button to fix UI state issues | |
| refresh_editor_button = gr.Button( | |
| t("refresh_editor_button", "en"), | |
| size="sm", | |
| variant="secondary" | |
| ) | |
| refresh_editor_button.click( | |
| fn=lambda: gr.update(), | |
| outputs=[local_input_image] | |
| ) | |
| # SEO Content Section | |
| seo_html = gr.HTML() | |
| def get_seo_html(lang): | |
| return f""" | |
| <div style="width: 100%; margin: 50px 0; padding: 0 20px;"> | |
| <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 20px; margin: 40px 0;"> | |
| <h2 style="margin: 0 0 20px 0; font-size: 2.2em; font-weight: 700;"> | |
| 🎨 {t('seo_unlimited_title', lang)} | |
| </h2> | |
| <p style="margin: 0 0 25px 0; font-size: 1.2em; opacity: 0.95; line-height: 1.6;"> | |
| {t('seo_unlimited_desc', lang)} | |
| </p> | |
| <div style="display: flex; justify-content: center; gap: 25px; flex-wrap: wrap; margin: 30px 0;"> | |
| <a href="https://omnicreator.net/#generator" target="_blank" style=" | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px 40px; | |
| background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%); | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 15px; | |
| font-weight: 700; | |
| font-size: 18px; | |
| text-align: center; | |
| min-width: 250px; | |
| box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4); | |
| transition: all 0.3s ease; | |
| border: none; | |
| transform: scale(1); | |
| " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'"> | |
| 🚀 {t('seo_unlimited_button', lang)} | |
| </a> | |
| </div> | |
| <p style="color: rgba(255,255,255,0.9); font-size: 1em; margin: 20px 0 0 0;"> | |
| {t('seo_unlimited_footer', lang)} | |
| </p> | |
| </div> | |
| <div style="text-align: center; margin: 25px auto; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); padding: 35px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1);"> | |
| <h2 style="color: #2c3e50; margin: 0 0 20px 0; font-size: 1.9em; font-weight: 700;"> | |
| ⭐ {t('seo_professional_title', lang)} | |
| </h2> | |
| <p style="color: #555; font-size: 1.1em; line-height: 1.6; margin: 0 0 20px 0; padding: 0 20px;"> | |
| {t('seo_professional_desc', lang)} | |
| </p> | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; margin: 40px 0;"> | |
| <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #e74c3c;"> | |
| <h3 style="color: #e74c3c; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;"> | |
| 🎯 {t('seo_feature1_title', lang)} | |
| </h3> | |
| <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;"> | |
| {t('seo_feature1_desc', lang)} | |
| </p> | |
| </div> | |
| <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #3498db;"> | |
| <h3 style="color: #3498db; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;"> | |
| 🔓 {t('seo_feature2_title', lang)} | |
| </h3> | |
| <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;"> | |
| {t('seo_feature2_desc', lang)} | |
| </p> | |
| </div> | |
| <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #27ae60;"> | |
| <h3 style="color: #27ae60; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;"> | |
| ⚡ {t('seo_feature3_title', lang)} | |
| </h3> | |
| <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;"> | |
| {t('seo_feature3_desc', lang)} | |
| </p> | |
| </div> | |
| <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #9b59b6;"> | |
| <h3 style="color: #9b59b6; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;"> | |
| 🎨 {t('seo_feature4_title', lang)} | |
| </h3> | |
| <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;"> | |
| {t('seo_feature4_desc', lang)} | |
| </p> | |
| </div> | |
| <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #f39c12;"> | |
| <h3 style="color: #f39c12; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;"> | |
| 💎 {t('seo_feature5_title', lang)} | |
| </h3> | |
| <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;"> | |
| {t('seo_feature5_desc', lang)} | |
| </p> | |
| </div> | |
| <div style="background: white; padding: 30px; border-radius: 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); border-left: 5px solid #34495e;"> | |
| <h3 style="color: #34495e; margin: 0 0 15px 0; font-size: 1.4em; font-weight: 600;"> | |
| 🌍 {t('seo_feature6_title', lang)} | |
| </h3> | |
| <p style="color: #666; margin: 0; line-height: 1.6; font-size: 1em;"> | |
| {t('seo_feature6_desc', lang)} | |
| </p> | |
| </div> | |
| </div> | |
| <div style="background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%); padding: 30px; border-radius: 15px; margin: 40px 0;"> | |
| <h3 style="color: #8b5cf6; text-align: center; margin: 0 0 25px 0; font-size: 1.5em; font-weight: 700;"> | |
| 💡 {t('seo_protips_title', lang)} | |
| </h3> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 18px;"> | |
| <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;"> | |
| <strong style="color: #8b5cf6; font-size: 1.1em;">📝 {t('seo_protip1_title', lang)}</strong> | |
| <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">{t('seo_protip1_desc', lang)}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;"> | |
| <strong style="color: #8b5cf6; font-size: 1.1em;">🎯 {t('seo_protip2_title', lang)}</strong> | |
| <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">{t('seo_protip2_desc', lang)}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;"> | |
| <strong style="color: #8b5cf6; font-size: 1.1em;">⚡ {t('seo_protip3_title', lang)}</strong> | |
| <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">{t('seo_protip3_desc', lang)}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.85); padding: 18px; border-radius: 12px;"> | |
| <strong style="color: #8b5cf6; font-size: 1.1em;">🖼 {t('seo_protip4_title', lang)}</strong> | |
| <p style="color: #555; margin: 5px 0 0 0; line-height: 1.5;">{t('seo_protip4_desc', lang)}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="text-align: center; margin: 25px auto; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); padding: 35px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1);"> | |
| <h2 style="color: #2c3e50; margin: 0 0 20px 0; font-size: 1.8em; font-weight: 700;"> | |
| 🚀 {t('seo_needs_title', lang)} | |
| </h2> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 25px 0; text-align: left;"> | |
| <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;"> | |
| <h4 style="color: #e74c3c; margin: 0 0 10px 0;">🎨 {t('seo_needs_art_title', lang)}</h4> | |
| <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;"> | |
| <li>{t('seo_needs_art_item1', lang)}</li> | |
| <li>{t('seo_needs_art_item2', lang)}</li> | |
| <li>{t('seo_needs_art_item3', lang)}</li> | |
| <li>{t('seo_needs_art_item4', lang)}</li> | |
| </ul> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;"> | |
| <h4 style="color: #3498db; margin: 0 0 10px 0;">📸 {t('seo_needs_photo_title', lang)}</h4> | |
| <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;"> | |
| <li>{t('seo_needs_photo_item1', lang)}</li> | |
| <li>{t('seo_needs_photo_item2', lang)}</li> | |
| <li>{t('seo_needs_photo_item3', lang)}</li> | |
| <li>{t('seo_needs_photo_item4', lang)}</li> | |
| </ul> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;"> | |
| <h4 style="color: #27ae60; margin: 0 0 10px 0;">🛍️ {t('seo_needs_ecom_title', lang)}</h4> | |
| <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;"> | |
| <li>{t('seo_needs_ecom_item1', lang)}</li> | |
| <li>{t('seo_needs_ecom_item2', lang)}</li> | |
| <li>{t('seo_needs_ecom_item3', lang)}</li> | |
| <li>{t('seo_needs_ecom_item4', lang)}</li> | |
| </ul> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.8); padding: 20px; border-radius: 12px;"> | |
| <h4 style="color: #9b59b6; margin: 0 0 10px 0;">📱 {t('seo_needs_social_title', lang)}</h4> | |
| <ul style="color: #555; margin: 0; padding-left: 18px; line-height: 1.6;"> | |
| <li>{t('seo_needs_social_item1', lang)}</li> | |
| <li>{t('seo_needs_social_item2', lang)}</li> | |
| <li>{t('seo_needs_social_item3', lang)}</li> | |
| <li>{t('seo_needs_social_item4', lang)}</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| all_ui_components = [ | |
| header_title, news_banner, | |
| global_tab, upload_image_header, input_image, editing_instructions_header, prompt_input, edit_button, | |
| editing_result_header, output_image, use_as_input_btn, status_output, prompt_examples_header, | |
| local_tab, upload_and_draw_mask_header, local_input_image, reference_image_header, local_reference_image, | |
| local_editing_instructions_header, local_prompt_input, local_edit_button, local_editing_result_header, | |
| local_output_image, local_use_as_input_btn, local_status_output, local_inpaint_example_header, | |
| panda_to_cat_example_header, panda_example_note, load_panda_example_button, refresh_editor_button, | |
| seo_html, | |
| ] | |
| def update_ui_lang(lang): | |
| return { | |
| header_title: gr.update(value=f""" | |
| <div style="text-align: center; margin: 20px auto 10px auto; max-width: 800px;"> | |
| <h1 style="color: #2c3e50; margin: 0; font-size: 3.5em; font-weight: 800; letter-spacing: 3px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);"> | |
| {t('header_title', lang)} | |
| </h1> | |
| </div>"""), | |
| news_banner: gr.update(value=f""" | |
| <style> | |
| @keyframes breathe {{ 0%, 100% {{ transform: scale(1); }} 50% {{ transform: scale(1.02); }} }} | |
| .breathing-banner {{ animation: breathe 3s ease-in-out infinite; }} | |
| </style> | |
| <div class="breathing-banner" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin: 5px auto 5px auto; padding: 6px 40px; border-radius: 20px; max-width: 700px; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); text-align: center;"> | |
| <span style="color: white; font-weight: 600; font-size: 1.0em;"> | |
| {t('news_banner_prefix', lang)} | |
| <a href="https://huggingface.co/spaces/Selfit/Multi-Image-Edit" target="_blank" style="color: white; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,0.5); transition: all 0.3s ease;" onmouseover="this.style.borderBottom='1px solid white'" onmouseout="this.style.borderBottom='1px solid rgba(255,255,255,0.5)'"> | |
| {t('news_banner_link', lang)} | |
| </a> | |
| </span> | |
| </div>"""), | |
| global_tab: gr.update(label=t("global_editor_tab", lang)), | |
| upload_image_header: gr.update(value=t("upload_image_header", lang)), | |
| input_image: gr.update(label=t("upload_image_label", lang)), | |
| editing_instructions_header: gr.update(value=t("editing_instructions_header", lang)), | |
| prompt_input: gr.update(label=t("prompt_input_label", lang), placeholder=t("prompt_input_placeholder", lang)), | |
| edit_button: gr.update(value=t("start_editing_button", lang)), | |
| editing_result_header: gr.update(value=t("editing_result_header", lang)), | |
| output_image: gr.update(label=t("output_image_label", lang)), | |
| use_as_input_btn: gr.update(value=t("use_as_input_button", lang)), | |
| status_output: gr.update(label=t("status_output_label", lang)), | |
| prompt_examples_header: gr.update(value=t("prompt_examples_header", lang)), | |
| local_tab: gr.update(label=t("local_inpaint_tab", lang)), | |
| upload_and_draw_mask_header: gr.update(value=t("upload_and_draw_mask_header", lang)), | |
| local_input_image: gr.update(label=t("upload_and_draw_mask_label", lang)), | |
| reference_image_header: gr.update(value=t("reference_image_header", lang)), | |
| local_reference_image: gr.update(label=t("reference_image_label", lang)), | |
| local_editing_instructions_header: gr.update(value=t("editing_instructions_header", lang)), | |
| local_prompt_input: gr.update(label=t("local_prompt_input_label", lang), placeholder=t("local_prompt_input_placeholder", lang)), | |
| local_edit_button: gr.update(value=t("start_local_editing_button", lang)), | |
| local_editing_result_header: gr.update(value=t("editing_result_header", lang)), | |
| local_output_image: gr.update(label=t("local_output_image_label", lang)), | |
| local_use_as_input_btn: gr.update(value=t("use_as_input_button", lang)), | |
| local_status_output: gr.update(label=t("status_output_label", lang)), | |
| local_inpaint_example_header: gr.update(value=t("local_inpaint_example_header", lang)), | |
| panda_to_cat_example_header: gr.update(value=t("panda_to_cat_example_header", lang)), | |
| panda_example_note: gr.update(value=t("panda_example_note", lang)), | |
| load_panda_example_button: gr.update(value=t("load_panda_example_button", lang)), | |
| refresh_editor_button: gr.update(value=t("refresh_editor_button", lang)), | |
| seo_html: gr.update(value=get_seo_html(lang)), | |
| } | |
| def on_lang_change(lang): | |
| return lang, *update_ui_lang(lang).values() | |
| lang_dropdown.change( | |
| on_lang_change, | |
| inputs=[lang_dropdown], | |
| outputs=[lang_state] + all_ui_components | |
| ) | |
| # IP query state for async loading | |
| ip_query_state = gr.State({"status": "pending", "ip": None, "lang": "en"}) | |
| def on_load_immediate(request: gr.Request): | |
| """ | |
| Load page with language based on robust IP detection | |
| Features: | |
| - Multiple fallback layers for IP extraction | |
| - Comprehensive error handling | |
| - Always returns valid language (defaults to English) | |
| - Detailed logging for debugging | |
| """ | |
| # Extract client IP with multiple fallback methods | |
| client_ip = None | |
| try: | |
| # Primary method: direct client host | |
| client_ip = request.client.host | |
| # Secondary method: check forwarded headers | |
| headers = dict(request.headers) if hasattr(request, 'headers') else {} | |
| x_forwarded_for = headers.get('x-forwarded-for') or headers.get('X-Forwarded-For') | |
| if x_forwarded_for: | |
| # Take first IP from comma-separated list | |
| client_ip = x_forwarded_for.split(',')[0].strip() | |
| # Alternative headers | |
| if not client_ip or client_ip in ["127.0.0.1", "localhost"]: | |
| client_ip = headers.get('x-real-ip') or headers.get('X-Real-IP') or client_ip | |
| except Exception as e: | |
| print(f"Error extracting client IP: {e}, using default") | |
| client_ip = "unknown" | |
| # Validate extracted IP | |
| if not client_ip: | |
| client_ip = "unknown" | |
| print(f"Loading page for IP: {client_ip}") | |
| # Determine language with robust error handling | |
| try: | |
| # Check if IP is already cached (second+ visit) | |
| if client_ip in IP_Country_Cache: | |
| # Use cached data - very fast | |
| cached_lang = get_lang_from_ip(client_ip) | |
| # Validate cached language | |
| if cached_lang and len(cached_lang) == 2: | |
| print(f"Using cached language: {cached_lang} for IP: {client_ip}") | |
| query_state = {"ip": client_ip, "cached": True} | |
| return cached_lang, cached_lang, query_state, *update_ui_lang(cached_lang).values() | |
| # First visit: Query IP and determine language (max 3s timeout built-in) | |
| print(f"First visit - detecting language for IP: {client_ip}") | |
| detected_lang = get_lang_from_ip(client_ip) | |
| # Double-check the detected language is valid | |
| if not detected_lang or len(detected_lang) != 2: | |
| print(f"Invalid detected language '{detected_lang}', using English") | |
| detected_lang = "en" | |
| print(f"First visit - Final language: {detected_lang} for IP: {client_ip}") | |
| query_state = {"ip": client_ip, "cached": False} | |
| return detected_lang, detected_lang, query_state, *update_ui_lang(detected_lang).values() | |
| except Exception as e: | |
| # Ultimate fallback - always works | |
| print(f"Critical error in language detection for {client_ip}: {e}") | |
| print("Using English as ultimate fallback") | |
| query_state = {"ip": client_ip or "unknown", "cached": False, "error": str(e)} | |
| return "en", "en", query_state, *update_ui_lang("en").values() | |
| app.load( | |
| on_load_immediate, | |
| inputs=None, | |
| outputs=[lang_state, lang_dropdown, ip_query_state] + all_ui_components, | |
| ) | |
| return app | |
| if __name__ == "__main__": | |
| app = create_app() | |
| # Improve queue configuration to handle high concurrency and prevent SSE connection issues | |
| app.queue( | |
| default_concurrency_limit=20, # Default concurrency limit | |
| max_size=50, # Maximum queue size | |
| api_open=False # Close API access to reduce resource consumption | |
| ) | |
| app.launch( | |
| server_name="0.0.0.0", | |
| show_error=True, # Show detailed error information | |
| quiet=False, # Keep log output | |
| max_threads=40, # Increase thread pool size | |
| height=800, | |
| favicon_path=None # Reduce resource loading | |
| ) |