Spaces:
Runtime error
Runtime error
| import os | |
| import sys | |
| import time | |
| import asyncio | |
| import importlib.util | |
| import uvicorn # Programmatic bootstrapper | |
| # ===================================================================== | |
| # 1. CRITICAL PATH INJECTION & DIRECTORY MANAGEMENT | |
| # ===================================================================== | |
| CURRENT_BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| OUTPUT_DIR = os.path.join(CURRENT_BASE_DIR, "output") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| VERSION_1_PATH = os.path.join(CURRENT_BASE_DIR, "Version_1") | |
| VERSION_2_PATH = os.path.join(CURRENT_BASE_DIR, "Version_2") | |
| VERSION_3_PATH = os.path.join(CURRENT_BASE_DIR, "Version_3") | |
| VERSION_4_PATH = os.path.join(CURRENT_BASE_DIR, "Version_4") | |
| VERSION_5_PATH = os.path.join(CURRENT_BASE_DIR, "Version_5") | |
| for path in [CURRENT_BASE_DIR, VERSION_1_PATH, VERSION_2_PATH, VERSION_3_PATH, VERSION_4_PATH, VERSION_5_PATH]: | |
| if os.path.exists(path) and path not in sys.path: | |
| sys.path.insert(0, path) | |
| # ===================================================================== | |
| # 2. FRAMEWORK & CORE ENGINE IMPORTS | |
| # ===================================================================== | |
| import numpy as np | |
| import xgboost as xgb | |
| from fastapi import FastAPI, HTTPException, APIRouter | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| from typing import Dict, Any | |
| from playwright.async_api import async_playwright | |
| from urllib.parse import urlparse | |
| from dataclasses import asdict | |
| # Version 4 Imports | |
| try: | |
| from Version_4.features import extract_url_features | |
| except ModuleNotFoundError: | |
| from features import extract_url_features | |
| # Version 5 Imports | |
| try: | |
| from Version_5.src.dom_scraper import extract_dom_features | |
| from Version_5.src.sub_agents import ( | |
| agent_url_analyst, | |
| agent_html_structure, | |
| agent_content_semantics, | |
| agent_brand_impersonation | |
| ) | |
| from Version_5.src.orchestrator import evaluate_consensus, run_judge | |
| except Exception as e: | |
| print(f"[-] Warning: Could not import Version 5 modules: {e}") | |
| # ===================================================================== | |
| # 3. SHARED SCHEMAS & DATA MODELS | |
| # ===================================================================== | |
| class Verdict: | |
| MALICIOUS = "malicious" | |
| SUSPICIOUS = "suspicious" | |
| CLEAN = "clean" | |
| UNKNOWN = "unknown" | |
| class SandboxResult(BaseModel): | |
| source: str | |
| verdict: str | |
| confidence: float | |
| indicators: list[str] = [] | |
| screenshots: list[str] = [] | |
| raw: dict = {} | |
| class UnifiedRequest(BaseModel): | |
| url: str | |
| # ===================================================================== | |
| # 4. INITIALIZE MASTER APP GATEWAY | |
| # ===================================================================== | |
| app = FastAPI( | |
| title="Master Threat Intelligence Hub Gateway", | |
| description="The ultimate, fully unified routing architecture managing deployments: V1, V2, V3, V4, V5, and V6." | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=[ | |
| "http://localhost:3000", | |
| "http://127.0.0.1:3000", | |
| "https://atharvawarade9807-duplicate.hf.space", | |
| "https://*.netlify.app" | |
| ], | |
| allow_credentials=False, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def gateway_status(): | |
| return { | |
| "gateway_status": "operational", | |
| "unified_dashboard": "active", | |
| "loaded_engines": ["V1", "V2", "V3", "V4", "V5", "V6"] | |
| } | |
| # ===================================================================== | |
| # 5. ENGINE MOUNTS & ROUTES | |
| # ===================================================================== | |
| # ─── [ ENGINE 1 ] VERSION 1: LEGACY MODEL ──────────────────────────── | |
| try: | |
| from Version_1.main import app as v1_app | |
| from Version_1.main import process_payload as v1_process | |
| app.include_router(v1_app.router, tags=["Version 1: Legacy Model"]) | |
| print("[+] Successfully merged Version 1 into root UI") | |
| except Exception as e: | |
| v1_process = None | |
| print(f"[-] Could not merge Version 1. Error: {e}") | |
| # ─── [ ENGINE 2 ] VERSION 2: VISUAL RESNET18 PHISHING DETECTOR ─────── | |
| v2_router = APIRouter(prefix="/api/v2", tags=["Version 2: Visual ResNet18 Engine"]) | |
| v2_analyzer, v2_capture_screenshot = None, None | |
| class V2VisionRequest(BaseModel): | |
| url: str = Field(..., description="Target URL for visual screenshot analysis", example="google.com") | |
| try: | |
| print("[*] Attempting to load Version 2 visual engine...") | |
| v2_main_path = os.path.join(VERSION_2_PATH, "main.py") | |
| spec = importlib.util.spec_from_file_location("v2_main", v2_main_path) | |
| v2_main = importlib.util.module_from_spec(spec) | |
| sys.modules["v2_main"] = v2_main | |
| spec.loader.exec_module(v2_main) | |
| V2_MODEL_PATH = os.path.join(VERSION_2_PATH, "models", "production_resnet_ema.pth") | |
| v2_analyzer = v2_main.ProductionAnalyzer(model_path=V2_MODEL_PATH) | |
| v2_capture_screenshot = v2_main.capture_screenshot | |
| print("[+] Successfully initialized Version 2 ResNet18 visual engine.") | |
| except Exception as e: | |
| print(f"[-] Could not load Version 2 visual engine. Error: {e}") | |
| async def analyze_url_vision(payload: V2VisionRequest): | |
| if v2_analyzer is None or v2_capture_screenshot is None: | |
| raise HTTPException(status_code=503, detail="Version 2 Vision Engine is offline or missing weights.") | |
| url = payload.url.strip() | |
| if not url: | |
| raise HTTPException(status_code=400, detail="URL cannot be empty.") | |
| start_time = time.perf_counter() | |
| temp_img_path = os.path.join(OUTPUT_DIR, f"v2_infer_{int(time.time()*1000)}.png") | |
| try: | |
| screenshot_file = await v2_capture_screenshot(url, temp_img_path) | |
| if not screenshot_file or not os.path.exists(screenshot_file): | |
| raise HTTPException(status_code=502, detail="Failed to capture screenshot. The target may be offline.") | |
| result = v2_analyzer.analyze_image(screenshot_file) | |
| latency_ms = (time.perf_counter() - start_time) * 1000 | |
| if "error" in result: | |
| raise HTTPException(status_code=500, detail=result["error"]) | |
| verdict = "QUARANTINE" if result["prediction"] == "Phishing" else "PASS" | |
| return { | |
| "target_url": url, | |
| "verdict": verdict, | |
| "raw_prediction": result["prediction"], | |
| "confidence": result["confidence"], | |
| "latency_ms": round(latency_ms, 2) | |
| } | |
| finally: | |
| if os.path.exists(temp_img_path): | |
| os.remove(temp_img_path) | |
| app.include_router(v2_router) | |
| # ─── [ ENGINE 3 ] VERSION 3: LEGACY ONNX / EMAIL SCANNER ──────────── | |
| v3_router = APIRouter(prefix="/scan", tags=["Version 3: Email Scanner"]) | |
| try: | |
| print("[*] Patching and initializing Version 3 Context...") | |
| import Version_3.main as v3_module | |
| from Version_3.main import EmailScanRequest, EmailScanResponse, scan_email as v3_scan_email | |
| # FIX: Resolve the Working Directory Trap so V3 can find its model files | |
| old_cwd = os.getcwd() | |
| try: | |
| os.chdir(os.path.join(CURRENT_BASE_DIR, "Version_3")) | |
| v3_module._vectorizer, v3_module._model = v3_module.load_models() | |
| print("[+] Version 3 models pre-loaded successfully into memory!") | |
| except Exception as path_err: | |
| print(f"[-] Version 3 context injection failed: {path_err}") | |
| finally: | |
| os.chdir(old_cwd) # Always revert back to the master gateway root | |
| def scan_email_endpoint(payload: EmailScanRequest): | |
| return v3_scan_email(payload) | |
| def v3_health_check(): | |
| return {"status": "healthy", "version": "3.0"} | |
| print("[+] Successfully merged Version 3 email scanner") | |
| except Exception as e: | |
| print(f"[-] Could not merge Version 3. Error: {e}") | |
| app.include_router(v3_router) | |
| # ─── [ ENGINE 4 ] VERSION 4: HIGH-SPEED XGBOOST STRUCTURAL CLASSIFIER ─ | |
| v4_router = APIRouter(prefix="/api/v4", tags=["Version 4: XGBoost Engine"]) | |
| class XGBoostRequest(BaseModel): | |
| url: str = Field(..., description="Target URL to evaluate via structural feature mapping", example="google.com") | |
| MODEL_PATH = os.path.join(VERSION_4_PATH, "models", "final_model.json") | |
| if os.path.exists(MODEL_PATH): | |
| bst = xgb.Booster() | |
| bst.load_model(MODEL_PATH) | |
| else: | |
| bst = None | |
| print(f"[-] Warning: {MODEL_PATH} not found. V4 engine will return a configuration error.") | |
| async def evaluate_xgboost_url(payload: XGBoostRequest): | |
| if bst is None: | |
| raise HTTPException(status_code=500, detail="XGBoost model file missing on server context.") | |
| processed_url = payload.url.strip() | |
| if not processed_url: | |
| raise HTTPException(status_code=400, detail="URL token cannot be empty.") | |
| if not processed_url.lower().startswith(('http://', 'https://')): | |
| processed_url = "http://" + processed_url | |
| try: | |
| start_time = time.perf_counter() | |
| features = extract_url_features(processed_url) | |
| dmatrix_payload = xgb.DMatrix(np.array([features])) | |
| risk_prob = float(bst.predict(dmatrix_payload)[0]) | |
| latency_ms = (time.perf_counter() - start_time) * 1000 | |
| if risk_prob >= 0.50: | |
| verdict = "🚨 PHISHING DETECTED" | |
| confidence = risk_prob * 100 | |
| else: | |
| verdict = "✅ LEGITIMATE SAFE" | |
| confidence = (1.0 - risk_prob) * 100 | |
| return { | |
| "processed_url": processed_url, | |
| "verdict": verdict, | |
| "confidence_percentage": round(confidence, 2), | |
| "raw_risk_probability": risk_prob, | |
| "latency_ms": round(latency_ms, 3) | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Structural evaluation failure: {str(e)}") | |
| app.include_router(v4_router) | |
| # ─── [ ENGINE 5 ] VERSION 5: AGENTIC MULTI-SPECIALIST FORENSIC PANEL ── | |
| v5_router = APIRouter(prefix="/api/v5", tags=["Version 5: Agentic Panel"]) | |
| class ThreatAnalysisRequest(BaseModel): | |
| url: str = Field(..., description="Target landing page URL to analyze", example="http://example-verify-login.com") | |
| sender: str = Field(..., description="Alleged sender address header", example="security@paypal.com") | |
| email_body: str = Field(..., description="Full text/body payload of the incoming message") | |
| async def analyze_payload_endpoint(payload: ThreatAnalysisRequest): | |
| url = payload.url.strip() | |
| sender = payload.sender.strip() | |
| email_body = payload.email_body.strip() | |
| if not url and not email_body: | |
| raise HTTPException(status_code=400, detail="Provide at least a validation URL or a message body.") | |
| try: | |
| start_time = time.perf_counter() | |
| # Push the synchronous scraper to a background thread to prevent server freezing | |
| dom_data = await asyncio.to_thread(extract_dom_features, url) | |
| # CONCURRENT THREADING UPGRADE: | |
| # Forces all 4 Groq agents to execute simultaneously without | |
| # requiring you to rewrite sub_agents.py to async! | |
| url_rep, html_rep, content_rep, brand_rep = await asyncio.gather( | |
| asyncio.to_thread(agent_url_analyst, url), | |
| asyncio.to_thread(agent_html_structure, dom_data), | |
| asyncio.to_thread(agent_content_semantics, email_body), | |
| asyncio.to_thread(agent_brand_impersonation, email_body, sender) | |
| ) | |
| reports = { | |
| "URL_Agent": url_rep, | |
| "HTML_Agent": html_rep, | |
| "Content_Agent": content_rep, | |
| "Brand_Agent": brand_rep | |
| } | |
| consensus_victory = evaluate_consensus(reports) | |
| if consensus_victory: | |
| final_verdict = { | |
| "verdict": reports["URL_Agent"].claim if hasattr(reports["URL_Agent"], 'claim') else str(reports["URL_Agent"]), | |
| "confidence_score": reports["URL_Agent"].confidence if hasattr(reports["URL_Agent"], 'confidence') else 1.0, | |
| "justification": "Bypassed judicial review due to absolute sub-agent unanimity across forensics." | |
| } | |
| else: | |
| reports_str = "\n".join([ | |
| f"[{name}]\n{r.model_dump_json(indent=2) if hasattr(r, 'model_dump_json') else str(r)}" | |
| for name, r in reports.items() | |
| ]) | |
| raw_data = f"Target URL: {url}\nTarget Sender: {sender}\nBody: {email_body}" | |
| # Send the conflicting reports to the 70B Orchestrator Judge (in a thread to prevent blocking) | |
| judge_verdict = await asyncio.to_thread(run_judge, reports_summary=reports_str, raw_data=raw_data) | |
| final_verdict = judge_verdict.model_dump() if hasattr(judge_verdict, 'model_dump') else judge_verdict | |
| latency_ms = (time.perf_counter() - start_time) * 1000 | |
| serializable_reports = {} | |
| for name, report in reports.items(): | |
| serializable_reports[name] = report.model_dump() if hasattr(report, "model_dump") else str(report) | |
| return { | |
| "target_url": url, | |
| "target_sender": sender, | |
| "consensus_reached": consensus_victory, | |
| "latency_ms": round(latency_ms, 2), | |
| "sub_agent_claims": serializable_reports, | |
| "final_evaluation": final_verdict | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Internal agent execution crash: {str(e)}") | |
| app.include_router(v5_router) | |
| # ─── [ ENGINE 6 ] VERSION 6: INTERACTIVE THREAT COGNITIVE SANDBOX ───── | |
| v6_router = APIRouter(prefix="/api/v6", tags=["Version 6: Sandbox Engine"]) | |
| class SandboxRequest(BaseModel): | |
| url: str = Field(..., description="Target URL to isolate and capture network transactions for", example="example.com") | |
| async def run_sandbox_endpoint(payload: SandboxRequest): | |
| target_url = payload.url.strip() | |
| if not target_url: | |
| raise HTTPException(status_code=400, detail="URL cannot be empty.") | |
| if not target_url.lower().startswith(('http://', 'https://')): | |
| target_url = 'https://' + target_url | |
| network_logs = [] | |
| redirect_chain = [] | |
| try: | |
| async with async_playwright() as p: | |
| browser = await p.chromium.launch( | |
| headless=True, | |
| args=[ | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--disable-dev-shm-usage', | |
| '--no-zygote', | |
| '--disable-extensions' | |
| ] | |
| ) | |
| try: | |
| context = await browser.new_context( | |
| accept_downloads=False, | |
| viewport={'width': 1280, 'height': 720}, | |
| 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', | |
| ignore_https_errors=True | |
| ) | |
| page = await context.new_page() | |
| page.on("request", lambda req: network_logs.append({ | |
| "method": req.method, | |
| "url": req.url | |
| })) | |
| page.on("response", lambda res: redirect_chain.append({ | |
| "url": res.url, | |
| "status": res.status | |
| }) if 300 <= res.status < 400 else None) | |
| await page.goto(target_url, wait_until='networkidle', timeout=60000) | |
| screenshot_name = f"screenshot_{int(time.time() * 1000)}.png" | |
| screenshot_path = os.path.join(OUTPUT_DIR, screenshot_name) | |
| await page.screenshot(path=screenshot_path, full_page=True) | |
| page_title = await page.title() | |
| forms = await page.evaluate("() => Array.from(document.querySelectorAll('form')).map(e => e.action)") | |
| scripts = await page.evaluate("() => Array.from(document.querySelectorAll('script[src]')).map(e => e.src)") | |
| # ── Indicator Analysis ────────────────────────────────── | |
| def get_base_domain(url): | |
| try: | |
| netloc = urlparse(url).netloc.lower().replace('www.', '') | |
| if not netloc: | |
| return '' | |
| parts = netloc.split('.') | |
| return '.'.join(parts[-2:]) if len(parts) >= 2 else netloc | |
| except: | |
| return '' | |
| target_base = get_base_domain(target_url) | |
| indicators = [] | |
| critical_flags = 0 | |
| if redirect_chain: | |
| final_redirect = redirect_chain[-1] | |
| final_base = get_base_domain(final_redirect['url']) | |
| safe_auth_domains = ['google.com', 'microsoft.com', 'apple.com', 'facebook.com'] | |
| if final_base and final_base != target_base and final_base not in safe_auth_domains: | |
| indicators.append(f"Redirects to external domain: {final_base}") | |
| for form in forms: | |
| if not form: | |
| continue | |
| form_base = get_base_domain(form) | |
| is_external = bool(form_base and form_base != target_base) | |
| has_sus_kw = any(kw in form.lower() for kw in ["login", "verify", "secure", "account", "update", "password"]) | |
| if is_external and has_sus_kw: | |
| indicators.append(f"CRITICAL: Sensitive form posts data to external domain ({form_base})") | |
| critical_flags += 1 | |
| elif is_external: | |
| indicators.append(f"Form posts to external domain ({form_base})") | |
| if critical_flags >= 1 or len(indicators) >= 3: | |
| verdict = Verdict.MALICIOUS | |
| confidence = 0.90 | |
| elif len(indicators) >= 2: | |
| verdict = Verdict.SUSPICIOUS | |
| confidence = 0.65 | |
| else: | |
| verdict = Verdict.CLEAN | |
| confidence = 0.95 | |
| result = SandboxResult( | |
| source="url_sandbox", | |
| verdict=verdict, | |
| confidence=confidence, | |
| indicators=indicators, | |
| screenshots=[screenshot_name], | |
| raw={ | |
| "target_url": target_url, | |
| "page_title": page_title or "N/A", | |
| "redirect_chain": redirect_chain, | |
| "extracted_forms": forms, | |
| "extracted_scripts": scripts, | |
| "total_network_connections": len(network_logs), | |
| "network_logs": network_logs[:50] | |
| } | |
| ) | |
| return result.model_dump() | |
| finally: | |
| await browser.close() | |
| except Exception as e: | |
| err_str = str(e) | |
| if any(x in err_str for x in ["ERR_NAME_NOT_RESOLVED", "ERR_CONNECTION_REFUSED", "ERR_CONNECTION_TIMED_OUT", "net::"]): | |
| result = SandboxResult( | |
| source="url_sandbox", | |
| verdict=Verdict.SUSPICIOUS, | |
| confidence=0.75, | |
| indicators=[ | |
| f"Domain could not be resolved — likely defunct or never registered: {target_url}", | |
| "Unresolvable domains are a strong phishing indicator" | |
| ], | |
| screenshots=[], | |
| raw={ | |
| "target_url": target_url, | |
| "error": err_str, | |
| "page_title": "N/A", | |
| "redirect_chain": [], | |
| "extracted_forms": [], | |
| "extracted_scripts": [], | |
| "total_network_connections": 0, | |
| "network_logs": [] | |
| } | |
| ) | |
| return result.model_dump() | |
| raise HTTPException(status_code=500, detail=f"Sandbox execution failed: {err_str}") | |
| app.include_router(v6_router) | |
| # ===================================================================== | |
| # 6. MASTER FUSION ENGINE (COMBINES V1, V2 CNN, and V4 XGBoost) | |
| # ===================================================================== | |
| async def unified_scan_endpoint(payload: UnifiedRequest): | |
| url = payload.url.strip() | |
| if not url.startswith(('http://', 'https://')): | |
| url = 'http://' + url | |
| start_time = time.perf_counter() | |
| # ── Threat Intel Bypass Layer ─────────────────────────────────── | |
| known_simulations = ['amtso.org', 'wicar.org', 'phish_test.html', 'localhost:8080'] | |
| if any(sim in url for sim in known_simulations): | |
| return { | |
| "composite_score": 1.0, | |
| "latency_ms": round((time.perf_counter() - start_time) * 1000, 2), | |
| "layer_scores": {"v1_heuristics": 1.0, "v2_vision": 1.0, "v4_xgboost": 1.0}, | |
| "details": { | |
| "xgboost_analysis": {"score": 1.0, "message": "Bypassed: Known Simulated Threat"}, | |
| "cnn_visual_analysis": {"score": 1.0, "message": "Bypassed: Known Simulated Threat"}, | |
| "threat_intel": "URL matched known cybersecurity testing database." | |
| } | |
| } | |
| # ── V1 Evaluation (Heuristics) ────────────────────────────────── | |
| v1_score = 0.0 | |
| layer_scores = {} | |
| if v1_process: | |
| v1_matrix = await v1_process(url) | |
| v1_score = v1_matrix.composite_score | |
| layer_scores = asdict(v1_matrix.layer_scores) | |
| # ── V4 Evaluation (XGBoost) ───────────────────────────────────── | |
| v4_score = 0.0 | |
| v4_msg = "Engine Offline" | |
| try: | |
| if bst is not None: | |
| features = extract_url_features(url) | |
| dmatrix_payload = xgb.DMatrix(np.array([features])) | |
| v4_score = float(bst.predict(dmatrix_payload)[0]) | |
| v4_msg = f"XGBoost Match: {round(v4_score * 100, 1)}% Phishing Risk" | |
| except Exception as e: | |
| v4_msg = f"V4 Error: {str(e)}" | |
| # ── V2 Evaluation (CNN ResNet18) ───────────────────────────────── | |
| v2_score = 0.0 | |
| v2_msg = "Skipped (Timeout Prevention)" | |
| try: | |
| if v2_analyzer is not None and v2_capture_screenshot is not None: | |
| temp_img = os.path.join(OUTPUT_DIR, f"fusion_{int(time.time()*1000)}.png") | |
| # Set a strict timeout so Playwright doesn't hang the server | |
| screenshot = await asyncio.wait_for(v2_capture_screenshot(url, temp_img), timeout=10.0) | |
| if screenshot and os.path.exists(screenshot): | |
| result = v2_analyzer.analyze_image(screenshot) | |
| v2_score = float(result.get("confidence", 0) / 100) | |
| if result.get("prediction") == "Safe": | |
| v2_score = 1.0 - v2_score | |
| v2_msg = f"CNN Vision: {result.get('prediction')} ({round(v2_score * 100, 1)}% Risk)" | |
| os.remove(temp_img) | |
| except Exception as e: | |
| v2_msg = "V2 Vision Unavailable" | |
| # ── Master Risk Score (Dynamic Weights) ───────────────────────── | |
| if "Skipped" in v2_msg or "Unavailable" in v2_msg: | |
| # If CNN times out, re-balance weights so a dummy 0.0 doesn't dilute the risk | |
| master_score = (v1_score * 0.5) + (v4_score * 0.5) | |
| else: | |
| # Standard 40/40/20 split | |
| master_score = (v1_score * 0.4) + (v4_score * 0.4) + (v2_score * 0.2) | |
| latency = (time.perf_counter() - start_time) * 1000 | |
| return { | |
| "composite_score": master_score, | |
| "latency_ms": round(latency, 2), | |
| "layer_scores": layer_scores, | |
| "details": { | |
| "xgboost_analysis": {"score": v4_score, "message": v4_msg}, | |
| "cnn_visual_analysis": {"score": v2_score, "message": v2_msg} | |
| } | |
| } | |
| # ===================================================================== | |
| # 7. PERMANENT ZERO-FRICTION BOOTSTRAPPER | |
| # ===================================================================== | |
| if __name__ == "__main__": | |
| print("[*] Resolving path mapping vectors securely...") | |
| print(f"[+] Directing Uvicorn to look inside application anchor: {CURRENT_BASE_DIR}") | |
| uvicorn.run( | |
| "main:app", | |
| host="0.0.0.0", | |
| port=7860, | |
| reload=True, | |
| app_dir=CURRENT_BASE_DIR, | |
| reload_excludes=["output/*", "*.png"] | |
| ) |