Fyfgfgfg/Web-Project-screenshot
0
1import os2import json3import logging4import asyncio5import re6import socket7import gc8from io import BytesIO9from datetime import datetime, date10from contextlib import asynccontextmanager11 12from PIL import Image13import gradio as gr14from fastapi import FastAPI, Request15from fastapi.responses import JSONResponse, PlainTextResponse16import uvicorn17import aiohttp18 19from telegram import Update20from telegram.ext import (21 Application as TGApp,22 CommandHandler, MessageHandler, filters, ContextTypes23)24from telegram.constants import ChatAction25from telegram.request import HTTPXRequest26from playwright.async_api import async_playwright27 28from amazon_aod import capture_aod_screenshot29 30# =====================================================31# === LOGGING ===32# =====================================================33logging.basicConfig(34 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',35 level=logging.INFO36)37logger = logging.getLogger(__name__)38 39# =====================================================40# === CONFIGURATION ===41# =====================================================42SERVICE_TOKEN = os.getenv('SERVICE_TOKEN')43PORT = int(os.getenv('PORT', 7860))44 45SCREENSHOT_WIDTH = 124046SCREENSHOT_HEIGHT = 64947SCREENSHOT_TIMEOUT = 6048SCREENSHOT_MAX_RETRIES = 249 50# =====================================================51# === GLOBAL STATE ===52# =====================================================53browser = None54browser_context = None55playwright_instance = None56msg_app = None57 58# =====================================================59# === ACCESS CONTROL SYSTEM ===60# =====================================================61authorized_users = {}62admin_ids = []63contact_username = "admin"64 65 66def load_users():67 global authorized_users, admin_ids, contact_username68 possible_paths = [69 os.path.join(os.path.dirname(os.path.abspath(__file__)), 'users.json'),70 '/app/users.json',71 'users.json'72 ]73 for filepath in possible_paths:74 try:75 with open(filepath, 'r') as f:76 data = json.load(f)77 authorized_users = data.get('authorized_users', {})78 admin_ids = data.get('admin_ids', [])79 contact_username = data.get('contact_username', 'admin')80 logger.info(f"Loaded {len(authorized_users)} users from {filepath}")81 return True82 except FileNotFoundError:83 continue84 except Exception as e:85 logger.error(f"Error loading users from {filepath}: {e}")86 continue87 logger.error("users.json not found!")88 return False89 90 91def is_user_authorized(user_id: int) -> dict:92 user_id_str = str(user_id)93 if user_id_str not in authorized_users:94 return {'authorized': False, 'reason': 'not_registered', 'info': None}95 user_info = authorized_users[user_id_str]96 expiry_str = user_info.get('expiry', '2000-01-01')97 try:98 expiry_date = datetime.strptime(expiry_str, '%Y-%m-%d').date()99 except ValueError:100 return {'authorized': False, 'reason': 'invalid_expiry', 'info': user_info}101 today = date.today()102 if today > expiry_date:103 return {104 'authorized': False, 'reason': 'expired', 'info': user_info,105 'expiry_date': expiry_str, 'days_expired': (today - expiry_date).days106 }107 return {108 'authorized': True, 'reason': 'active', 'info': user_info,109 'expiry_date': expiry_str, 'days_remaining': (expiry_date - today).days110 }111 112 113def is_admin(user_id: int) -> bool:114 return user_id in admin_ids115 116 117def get_denial_message(auth_result: dict) -> str:118 reason = auth_result.get('reason', 'unknown')119 if reason == 'not_registered':120 return (121 "๐ซ *Access Denied*\n\n"122 "You don't have access to this bot.\n\n"123 "This is a premium bot available to paid subscribers only.\n\n"124 f"๐ฉ Contact @{contact_username} to get access.\n\n"125 "๐ *Plans Available:*\nโข Monthly subscription\nโข Lifetime access\n\n"126 "Send your payment and get instant activation!"127 )128 elif reason == 'expired':129 expiry = auth_result.get('expiry_date', 'Unknown')130 days = auth_result.get('days_expired', 0)131 username = auth_result.get('info', {}).get('username', 'User')132 return (133 "โฐ *Subscription Expired*\n\n"134 f"Hey @{username}, your subscription expired on *{expiry}* "135 f"({days} day{'s' if days != 1 else ''} ago).\n\n"136 f"๐ฉ Contact @{contact_username} to renew.\n\n"137 "Renew now to continue! ๐"138 )139 elif reason == 'invalid_expiry':140 return f"โ ๏ธ *Account Error*\n\nPlease contact @{contact_username} to fix this."141 else:142 return f"๐ซ *Access Denied*\n\n๐ฉ Contact @{contact_username} for access."143 144 145load_users()146 147# =====================================================148# === BROWSER MANAGEMENT ===149# =====================================================150async def init_browser():151 global browser, browser_context, playwright_instance152 for attempt in range(3):153 try:154 logger.info(f"Initializing browser (attempt {attempt + 1}/3)...")155 if not playwright_instance:156 playwright_instance = await async_playwright().start()157 browser = await playwright_instance.chromium.launch(158 headless=True,159 args=[160 '--no-sandbox', '--disable-setuid-sandbox',161 '--disable-dev-shm-usage', '--disable-gpu',162 '--disable-software-rasterizer', '--disable-extensions',163 '--disable-blink-features=AutomationControlled',164 '--disable-web-security',165 '--disable-features=IsolateOrigins,site-per-process',166 '--no-first-run', '--no-zygote',167 '--force-color-profile=srgb', '--disable-lcd-text',168 ]169 )170 browser_context = await browser.new_context(171 viewport={'width': SCREENSHOT_WIDTH, 'height': SCREENSHOT_HEIGHT + 250},172 user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',173 ignore_https_errors=True174 )175 test_page = await browser_context.new_page()176 await test_page.goto('about:blank', timeout=5000)177 await test_page.close()178 logger.info("Browser initialized successfully!")179 return True180 except Exception as e:181 logger.error(f"Browser init failed (attempt {attempt + 1}/3): {e}")182 for obj, name in [(browser_context, 'ctx'), (browser, 'browser')]:183 if obj:184 try: await obj.close()185 except: pass186 browser_context = None187 browser = None188 if attempt < 2:189 await asyncio.sleep(2)190 return False191 192 193async def close_browser():194 global browser, browser_context, playwright_instance195 for obj in [browser_context, browser]:196 if obj:197 try: await obj.close()198 except: pass199 if playwright_instance:200 try: await playwright_instance.stop()201 except: pass202 logger.info("Browser cleanup completed")203 204 205# =====================================================206# === SCREENSHOT LOGIC ===207# =====================================================208def extract_urls(text):209 return re.findall(r'https?://[^\s<>"{}|\\^`\[\]]+', text)210 211 212def get_url_type(url):213 url_lower = url.lower()214 if any(d in url_lower for d in ['fkrt.cc', 'fkrt.to', 'fkrt.site', 'fkrt.co']):215 return 'flipkart'216 elif any(d in url_lower for d in ['amazon.in', 'amazon.com', 'amzn.to', 'amzn.in', 'a.co', 'amzn.eu', 'amzn.asia']):217 return 'amazon'218 return 'default'219 220 221async def capture_screenshot(url, timeout=SCREENSHOT_TIMEOUT, max_retries=SCREENSHOT_MAX_RETRIES):222 global browser_context223 if not browser_context:224 return None225 226 url_type = get_url_type(url)227 logger.info(f"URL type: {url_type} for {url}")228 229 # Amazon AOD primary method230 if url_type == 'amazon' and browser:231 try:232 aod_bytes = await asyncio.wait_for(233 capture_aod_screenshot(url, browser), timeout=25234 )235 if aod_bytes is not None:236 logger.info("Amazon AOD screenshot success")237 return aod_bytes238 logger.info("Amazon AOD returned None, falling back")239 except asyncio.TimeoutError:240 logger.warning("Amazon AOD timed out, falling back")241 except Exception as e:242 logger.warning(f"Amazon AOD error: {e}, falling back")243 244 # Standard screenshot245 for attempt in range(max_retries):246 page = None247 try:248 page = await browser_context.new_page()249 await page.set_extra_http_headers({250 'Accept-Language': 'en-US,en;q=0.9',251 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'252 })253 254 async def route_handler(route):255 if route.request.resource_type in ["font", "media"]:256 await route.abort()257 else:258 await route.continue_()259 260 await page.route("**/*", route_handler)261 262 for strategy in ['commit', 'domcontentloaded', 'load']:263 try:264 await page.goto(url, wait_until=strategy, timeout=timeout * 1000)265 break266 except Exception as e:267 if strategy == 'load':268 raise269 continue270 271 await page.wait_for_timeout(2000)272 await page.unroute("**/*")273 await page.wait_for_timeout(1500)274 275 if url_type == 'default':276 try:277 await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 2)")278 await page.wait_for_timeout(1000)279 await page.evaluate("window.scrollTo(0, 0)")280 await page.wait_for_timeout(500)281 except: pass282 elif url_type == 'amazon':283 try:284 await page.evaluate("window.scrollTo(0, 300)")285 await page.wait_for_timeout(1500)286 await page.evaluate("window.scrollTo(0, 0)")287 await page.wait_for_timeout(800)288 except: pass289 290 if url_type == 'flipkart':291 screenshot_bytes = await page.screenshot(292 full_page=False, type='jpeg', quality=85, animations='disabled',293 clip={'x': 0, 'y': 100, 'width': SCREENSHOT_WIDTH, 'height': 540}294 )295 elif url_type == 'amazon':296 screenshot_bytes = await page.screenshot(297 full_page=False, type='jpeg', quality=85, animations='disabled',298 clip={'x': 0, 'y': 250, 'width': SCREENSHOT_WIDTH, 'height': SCREENSHOT_HEIGHT}299 )300 else:301 screenshot_bytes = await page.screenshot(302 full_page=False, type='jpeg', quality=85, animations='disabled'303 )304 305 logger.info(f"Screenshot captured for: {url}")306 return screenshot_bytes307 308 except Exception as e:309 logger.error(f"Screenshot attempt {attempt + 1}/{max_retries} failed: {e}")310 if attempt < max_retries - 1:311 await asyncio.sleep(2)312 else:313 return None314 finally:315 if page:316 try: await page.close()317 except: pass318 return None319 320 321# =====================================================322# === TELEGRAM COMMAND HANDLERS ===323# =====================================================324async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):325 user_id = update.effective_user.id326 auth = is_user_authorized(user_id)327 if not auth['authorized']:328 await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')329 return330 331 days_info = ""332 if auth.get('days_remaining') is not None:333 days = auth['days_remaining']334 plan = auth.get('info', {}).get('plan', 'unknown')335 if plan == 'lifetime':336 days_info = "โพ๏ธ Lifetime Access"337 elif days <= 7:338 days_info = f"โ ๏ธ {days} day{'s' if days != 1 else ''} remaining!"339 else:340 days_info = f"โ
{days} days remaining"341 342 await update.message.reply_text(343 f"๐ข *Bot is Active!*\n๐ *Your Status:* {days_info}\n\n"344 "๐ Welcome! Send me any message containing URLs, "345 "and I'll send you screenshots!\n\n"346 "*Features:*\nโข Extract links from any message\nโข Works with forwarded messages\n"347 "โข Smart cropping for Amazon & Flipkart\nโข Amazon AOD panel screenshots\n\n"348 "Just send or forward any message with links! ๐",349 parse_mode='Markdown'350 )351 352 353async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):354 auth = is_user_authorized(update.effective_user.id)355 if not auth['authorized']:356 await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')357 return358 await update.message.reply_text(359 "๐ *How to use:*\n\n"360 "1. Send any message containing URLs\n"361 "2. I'll extract the links automatically\n"362 "3. Wait for screenshots (30-60 seconds)\n\n"363 "*Smart Cropping:*\n"364 "โข ๐ Amazon: AOD panel (576ร239, primary)\n"365 "โข ๐ฆ Amazon fallback: 1240ร649\n"366 "โข ๐๏ธ Flipkart: 1240ร540\n"367 "โข ๐ Other sites: 1240ร649\n\n"368 "*Commands:*\n/start /help /status /myaccount",369 parse_mode='Markdown'370 )371 372 373async def cmd_myaccount(update: Update, context: ContextTypes.DEFAULT_TYPE):374 user_id = update.effective_user.id375 auth = is_user_authorized(user_id)376 if not auth['authorized']:377 await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')378 return379 info = auth.get('info', {})380 plan = info.get('plan', 'Unknown')381 expiry = auth.get('expiry_date', 'Unknown')382 added = info.get('added_on', 'Unknown')383 days = auth.get('days_remaining', 0)384 username = info.get('username', 'Unknown')385 386 if plan == 'lifetime': status_emoji, days_text = "๐", "Lifetime - Never expires"387 elif days <= 3: status_emoji, days_text = "๐ด", f"{days}d remaining - RENEW SOON!"388 elif days <= 7: status_emoji, days_text = "๐ก", f"{days}d remaining"389 else: status_emoji, days_text = "๐ข", f"{days}d remaining"390 391 await update.message.reply_text(392 f"๐ค *My Account*\n\n๐ @{username}\n๐ `{user_id}`\n"393 f"๐ *{plan.title()}*\n๐
Since: {added}\nโฐ Expires: {expiry}\n"394 f"{status_emoji} {days_text}\n\nRenew? Contact @{contact_username}",395 parse_mode='Markdown'396 )397 398 399async def cmd_admin_users(update: Update, context: ContextTypes.DEFAULT_TYPE):400 if not is_admin(update.effective_user.id):401 await update.message.reply_text("๐ซ Admins only.")402 return403 if not authorized_users:404 await update.message.reply_text("๐ญ No users.")405 return406 today = date.today()407 msg = "๐ฅ *Users:*\n\n"408 for uid, info in authorized_users.items():409 username = info.get('username', '?')410 plan = info.get('plan', '?')411 expiry_str = info.get('expiry', '?')412 try:413 ed = datetime.strptime(expiry_str, '%Y-%m-%d').date()414 dl = (ed - today).days415 if dl < 0: s = f"๐ด Exp {abs(dl)}d"416 elif plan == 'lifetime': s = "๐"417 else: s = f"๐ข {dl}d"418 except: s = "โ ๏ธ"419 msg += f"โข `{uid}` @{username} {plan} {s}\n"420 msg += f"\nTotal: {len(authorized_users)}"421 await update.message.reply_text(msg, parse_mode='Markdown')422 423 424async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):425 auth = is_user_authorized(update.effective_user.id)426 if not auth['authorized']:427 await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')428 return429 if browser and browser_context:430 await update.message.reply_text(431 "๐ข Active\n๐ธ Browser: Ready\n"432 "โข Amazon AOD: 576ร239\nโข Amazon fallback: 1240ร649\n"433 "โข Flipkart: 1240ร540\nโข Default: 1240ร649"434 )435 else:436 await update.message.reply_text("๐ก Browser initializing, try again shortly.")437 438 439async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):440 user_id = update.effective_user.id441 auth = is_user_authorized(user_id)442 if not auth['authorized']:443 await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')444 return445 if not browser or not browser_context:446 await update.message.reply_text("โณ Service initializing. Try again shortly.")447 return448 449 message_text = update.message.text or update.message.caption or ""450 urls = extract_urls(message_text)451 if not urls:452 await update.message.reply_text("๐ No links found. Send a message with URLs.")453 return454 455 confirm_msg = await update.message.reply_text(456 f"๐ Found {len(urls)} link(s)! Generating screenshots..."457 )458 459 successful = 0460 failed = 0461 for idx, url in enumerate(urls, 1):462 try:463 await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.TYPING)464 465 if len(urls) > 1:466 try:467 await confirm_msg.edit_text(f"โณ Processing {idx}/{len(urls)}...")468 except: pass469 470 screenshot_bytes = await capture_screenshot(url)471 if not screenshot_bytes:472 failed += 1473 await update.message.reply_text(f"โ Failed for:\n{url}")474 continue475 476 caption = message_text[:1024] if len(urls) == 1 else f"๐ธ {idx}/{len(urls)}\n๐ {url[:900]}"477 await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.UPLOAD_PHOTO)478 await update.message.reply_photo(photo=BytesIO(screenshot_bytes), caption=caption)479 successful += 1480 481 if idx < len(urls):482 await asyncio.sleep(1)483 except Exception as e:484 failed += 1485 logger.error(f"Error URL {idx}: {e}", exc_info=True)486 try:487 await update.message.reply_text(f"โ Error for link {idx}: {str(e)[:100]}")488 except: pass489 490 try: await confirm_msg.delete()491 except: pass492 493 summary = f"โ
Done! Success: {successful}"494 if failed > 0:495 summary += f", Failed: {failed}"496 try: await update.message.reply_text(summary)497 except: pass498 499 500async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):501 logger.error(f"Update error: {context.error}", exc_info=context.error)502 503 504# =====================================================505# === GRADIO WEB UI ===506# =====================================================507async def gradio_screenshot(url_text):508 if not url_text or not url_text.strip():509 return None, "Please enter a URL"510 if not browser or not browser_context:511 return None, "Browser still initializing..."512 try:513 url = url_text.strip()514 if not url.startswith(('http://', 'https://')):515 url = 'https://' + url516 img_bytes = await capture_screenshot(url)517 if img_bytes:518 return Image.open(BytesIO(img_bytes)), "Screenshot captured!"519 return None, "Failed to capture. Site may be blocking access."520 except Exception as e:521 return None, f"Error: {str(e)[:200]}"522 523 524async def gradio_get_status():525 b = "Ready" if (browser and browser_context) else "Starting..."526 m = "Connected" if msg_app else "Connecting..."527 return f"Browser: {b}\nMessaging: {m}\nUsers: {len(authorized_users)}"528 529 530with gr.Blocks(title="URL Preview Generator", theme=gr.themes.Soft()) as demo:531 gr.Markdown("# ๐ธ URL Preview Generator\nCapture screenshots of any webpage.")532 533 with gr.Row():534 url_input = gr.Textbox(label="URL", placeholder="https://example.com", lines=1, scale=4)535 capture_btn = gr.Button("๐ธ Capture", variant="primary", scale=1)536 537 output_image = gr.Image(label="Result", type="pil", height=400)538 result_text = gr.Textbox(label="Status", interactive=False, lines=1)539 540 with gr.Accordion("Service Status", open=False):541 status_box = gr.Textbox(label="Status", interactive=False, lines=3)542 gr.Button("Refresh").click(fn=gradio_get_status, outputs=[status_box])543 544 capture_btn.click(fn=gradio_screenshot, inputs=[url_input], outputs=[output_image, result_text])545 url_input.submit(fn=gradio_screenshot, inputs=[url_input], outputs=[output_image, result_text])546 demo.load(fn=gradio_get_status, outputs=[status_box])547 548 549# =====================================================550# === MESSAGING INIT (background) ===551# =====================================================552async def _init_bot_background():553 global msg_app554 555 if not SERVICE_TOKEN:556 logger.warning("No SERVICE_TOKEN set")557 return558 559 logger.info("Waiting for network...")560 await asyncio.sleep(8)561 562 # DNS563 for attempt in range(1, 31):564 try:565 socket.getaddrinfo("api.telegram.org", 443)566 logger.info(f"DNS ready on attempt {attempt}")567 break568 except socket.gaierror:569 logger.warning(f"DNS not ready ({attempt}/30)")570 await asyncio.sleep(10)571 else:572 logger.error("DNS never resolved")573 return574 575 # Connectivity test576 for attempt in range(1, 6):577 try:578 logger.info(f"Testing API (attempt {attempt}/5)...")579 async with aiohttp.ClientSession() as session:580 async with session.get(581 f"https://api.telegram.org/bot{SERVICE_TOKEN}/getMe",582 timeout=aiohttp.ClientTimeout(total=120)583 ) as resp:584 data = await resp.json()585 if data.get("ok"):586 logger.info("API reachable!")587 break588 except Exception as e:589 logger.warning(f"API test failed: {e}")590 await asyncio.sleep(10)591 else:592 logger.error("Cannot reach API")593 return594 595 # Build app596 for attempt in range(1, 6):597 try:598 logger.info(f"Initializing app (attempt {attempt}/5)...")599 req = HTTPXRequest(600 connect_timeout=120.0, read_timeout=120.0,601 write_timeout=120.0, pool_timeout=120.0,602 connection_pool_size=8,603 )604 msg_app = (605 TGApp.builder()606 .token(SERVICE_TOKEN)607 .request(req)608 .get_updates_request(HTTPXRequest(609 connect_timeout=120.0, read_timeout=120.0,610 write_timeout=120.0, pool_timeout=120.0,611 connection_pool_size=8,612 ))613 .build()614 )615 616 msg_app.add_handler(CommandHandler("start", cmd_start))617 msg_app.add_handler(CommandHandler("help", cmd_help))618 msg_app.add_handler(CommandHandler("status", cmd_status))619 msg_app.add_handler(CommandHandler("myaccount", cmd_myaccount))620 msg_app.add_handler(CommandHandler("users", cmd_admin_users))621 msg_app.add_handler(MessageHandler(622 (filters.TEXT | filters.CAPTION) & ~filters.COMMAND, handle_message623 ))624 msg_app.add_error_handler(error_handler)625 626 await asyncio.wait_for(msg_app.initialize(), timeout=180)627 logger.info("App initialized")628 await asyncio.wait_for(msg_app.start(), timeout=60)629 logger.info("App started")630 631 # Set callback632 cb_url = os.getenv('RENDER_EXTERNAL_URL')633 if not cb_url:634 host = os.getenv('SPACE_HOST')635 if host:636 cb_url = f"https://{host}"637 if cb_url:638 full = f"{cb_url}/callback/{SERVICE_TOKEN}"639 await asyncio.wait_for(640 msg_app.bot.set_webhook(url=full, allowed_updates=Update.ALL_TYPES, drop_pending_updates=True),641 timeout=120642 )643 logger.info(f"Callback set: {full}")644 else:645 logger.warning("No callback URL found")646 647 logger.info("Messaging service ready!")648 return649 650 except asyncio.TimeoutError:651 logger.error(f"Attempt {attempt}/5 timed out")652 except Exception as e:653 logger.error(f"Attempt {attempt}/5 failed: {e}")654 655 if msg_app:656 try: await msg_app.shutdown()657 except: pass658 msg_app = None659 if attempt < 5:660 await asyncio.sleep(15 * attempt)661 662 logger.error("Messaging init failed after all attempts")663 664 665# =====================================================666# === FASTAPI APP ===667# =====================================================668@asynccontextmanager669async def lifespan(app):670 print("=" * 50)671 print(" URL Preview Generator")672 print("=" * 50)673 674 logger.info("Initializing browser...")675 await init_browser()676 asyncio.create_task(_init_bot_background())677 print("Server starting...")678 679 yield680 681 logger.info("Shutting down...")682 await close_browser()683 if msg_app:684 try:685 await msg_app.stop()686 await msg_app.shutdown()687 except: pass688 689 690fastapi_app = FastAPI(title="URL Preview Generator", lifespan=lifespan)691 692 693@fastapi_app.get("/health")694async def health():695 return PlainTextResponse(696 f"OK - Browser:{'Ready' if browser else 'Starting'} Service:{'Active' if msg_app else 'Starting'}"697 )698 699 700@fastapi_app.post("/callback/{token}")701async def callback(token: str, request: Request):702 if token != SERVICE_TOKEN:703 return JSONResponse(status_code=403, content={"error": "forbidden"})704 if not msg_app:705 return JSONResponse(status_code=503, content={"error": "starting"})706 try:707 data = await request.json()708 update = Update.de_json(data, msg_app.bot)709 await msg_app.process_update(update)710 return JSONResponse(status_code=200, content={"ok": True})711 except Exception as e:712 logger.error(f"Callback error: {e}")713 return JSONResponse(status_code=500, content={"error": str(e)})714 715 716# Mount Gradio717app = gr.mount_gradio_app(fastapi_app, demo, path="/")718 719# =====================================================720# === MAIN ===721# =====================================================722if __name__ == '__main__':723 uvicorn.run(app, host='0.0.0.0', port=PORT)