#!/usr/bin/env python3 """ Vibeon Translator API - Main Application Production-ready translation API with database integration """ import sys import os import warnings from pathlib import Path # Suppress numpy binary incompatibility and transformers deprecation warnings warnings.filterwarnings("ignore", message=".*numpy.dtype size changed.*") warnings.filterwarnings("ignore", message=".*numpy.ufunc size changed.*") warnings.filterwarnings("ignore", category=FutureWarning, module="transformers") warnings.filterwarnings("ignore", message=".*torch.utils._pytree._register_pytree_node.*") # Add the current directory to Python path current_dir = Path(__file__).parent sys.path.insert(0, str(current_dir)) from fastapi import FastAPI, Request, status from fastapi.middleware.cors import CORSMiddleware from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from contextlib import asynccontextmanager import logging from typing import Optional from config.settings import settings from utils.logger import setup_logging, RequestIdLoggingMiddleware from utils.memory_optimizer import memory_optimizer from database import init_db from starlette.middleware import Middleware # type: ignore from starlette.middleware.sessions import SessionMiddleware # type: ignore from utils.request_limits import SimpleRateLimiter, MaxUploadSizeLimiter # Optional observability try: import sentry_sdk # type: ignore from sentry_sdk.integrations.asgi import SentryAsgiMiddleware # type: ignore SENTRY_AVAILABLE = True except Exception: SENTRY_AVAILABLE = False try: from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, generate_latest, Counter, Histogram # type: ignore PROM_AVAILABLE = True except Exception: PROM_AVAILABLE = False # Setup logging setup_logging( log_level=settings.log_level, log_file=settings.log_file ) # Suppress specific warnings import warnings warnings.filterwarnings("ignore", message=".*bcrypt.*__about__.*") logging.getLogger("passlib.handlers.bcrypt").setLevel(logging.ERROR) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan handler for startup and shutdown""" # Startup logger.info("Starting Vibeon Translator API...") try: # Initialize database logger.info("Initializing database...") init_db() logger.info("Database initialized successfully") except Exception as e: logger.error(f"Database initialization failed: {e}") logger.warning("API starting without database connection") # Create upload directories for directory in [settings.upload_dir, settings.temp_dir, settings.cache_dir, settings.model_cache_dir]: Path(directory).mkdir(parents=True, exist_ok=True) # Initialize memory optimizer logger.info("Initializing memory optimizer...") memory_optimizer.auto_cleanup_if_needed() # Pre-load translation models to avoid first-request delay logger.info("Pre-loading translation models...") try: from services.translation_service import unified_translation_service await unified_translation_service.initialize() logger.info("Translation models pre-loaded successfully") except Exception as e: logger.warning(f"Translation model pre-loading failed (will load on demand): {e}") # Pre-load STT (Whisper) model to avoid 1.5min delay on first request logger.info("Pre-loading STT (Whisper) model...") try: from services.stt_service import unified_stt_service # Preload base model in background to avoid blocking startup # Use asyncio.create_task to run in background (lifespan is async) import asyncio asyncio.create_task(unified_stt_service.preload_whisper_model()) logger.info("STT model pre-loading initiated in background (will be ready in 1-2 minutes)") except Exception as e: logger.warning(f"STT model pre-loading failed (will load on demand): {e}") # Pre-load TTS engines to avoid delays logger.info("Pre-loading TTS engines...") try: from services.tts_service import unified_tts_service # TTS engines are already initialized, just verify logger.info(f"TTS engines ready: {len(unified_tts_service.engines)} engines available") except Exception as e: logger.warning(f"TTS engine verification failed: {e}") logger.info(f"Vibeon Translator API started on {settings.api_host}:{settings.api_port}") yield # Shutdown logger.info("Shutting down Vibeon Translator API...") try: # Cleanup memory memory_optimizer.cleanup_models(keep_essential=False) logger.info("Memory cleanup completed") from database import close_db # Optional try: close_db() logger.info("Database connections closed") except Exception as e: logger.error(f"Error closing database: {e}") except Exception: # No close handler available pass logger.info("Vibeon Translator API stopped") # Create FastAPI app with lifespan app = FastAPI( title="Vibeon Translator API", description="Smart translation API for text, audio, and documents using AI models", version="3.0.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan ) # Add validation error handler for debugging @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Log and return validation errors""" logger.error(f"Validation error for {request.url.path}: {exc.errors()}") return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors(), "body": exc.body}, ) # Request ID & structured logging try: app.add_middleware(RequestIdLoggingMiddleware) except Exception: pass # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=[o for o in settings.cors_origins if o != "*"] if getattr(settings, "environment", "development") == "production" else settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["*"] ) # Basic rate limiting and upload size limits (tunable) try: app.add_middleware(MaxUploadSizeLimiter, max_bytes=getattr(settings, "max_upload_bytes", 15 * 1024 * 1024)) app.add_middleware(SimpleRateLimiter, max_requests=getattr(settings, "rate_limit_requests", 300), window_seconds=getattr(settings, "rate_limit_window", 60)) except Exception: pass # Optional Security headers for production try: if getattr(settings, "environment", "development") == "production": try: from starlette.middleware.security import SecurityMiddleware # type: ignore app.add_middleware( SecurityMiddleware, content_security_policy="default-src 'self'", content_security_policy_report_only=False, referrer_policy="same-origin", force_https=getattr(settings, "force_https", False), strict_transport_security="max-age=63072000; includeSubDomains; preload", x_content_type_options="nosniff", x_frame_options="DENY", ) except Exception: pass except Exception: pass # Optional Sentry initialization if SENTRY_AVAILABLE: sentry_dsn: Optional[str] = getattr(settings, "sentry_dsn", None) if sentry_dsn: try: sentry_sdk.init(dsn=sentry_dsn, traces_sample_rate=0.1, environment=getattr(settings, "environment", "development")) app.add_middleware(SentryAsgiMiddleware) logging.getLogger(__name__).info("Sentry initialized") except Exception as _s: logging.getLogger(__name__).warning(f"Sentry init failed: {_s}") # Optional Prometheus metrics if PROM_AVAILABLE: registry = CollectorRegistry() request_counter = Counter( "translator_http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"], registry=registry, ) request_latency = Histogram( "translator_request_latency_seconds", "Request latency", ["endpoint"], registry=registry, ) from starlette.requests import Request from starlette.responses import Response import time as _time @app.middleware("http") async def _metrics_middleware(request: Request, call_next): start = _time.time() response: Response = await call_next(request) try: endpoint = request.url.path request_counter.labels(request.method, endpoint, str(response.status_code)).inc() request_latency.labels(endpoint).observe(max(_time.time() - start, 0.0)) except Exception: pass return response @app.get("/metrics") async def metrics(): data = generate_latest(registry) from fastapi import Response as FResponse return FResponse(content=data, media_type=CONTENT_TYPE_LATEST) # Import and include routers (load independently so one failure doesn't block others) def _include_router_safely(module_path: str, attr: str = "router", prefix: str = "/api"): try: module = __import__(module_path, fromlist=[attr]) router = getattr(module, attr) app.include_router(router, prefix=prefix) logger.info(f"Loaded router: {module_path}") return True except (ImportError, OSError, Exception) as e: logger.error(f"Failed to load router {module_path}: {e}") return False loaded = [] loaded.append(_include_router_safely("api.routes.auth")) loaded.append(_include_router_safely("api.routes.real_translation")) loaded.append(_include_router_safely("api.routes.social_translation")) loaded.append(_include_router_safely("api.routes.vibeon_integration")) loaded.append(_include_router_safely("api.routes.tts_safe")) loaded.append(_include_router_safely("api.routes.languages")) loaded.append(_include_router_safely("api.routes.ui_translations")) # UI translations API loaded.append(_include_router_safely("api.routes.voice_services")) loaded.append(_include_router_safely("api.routes.multimodal")) # router prefix is already /multimodal → /api/multimodal/... loaded.append(_include_router_safely("api.routes.kinyarwanda_numbers")) # router prefix is already /numbers → /api/numbers/... logger.info(f"Routers loaded: {sum(1 for x in loaded if x)} / {len(loaded)}") # Root endpoint @app.get("/") async def root(): """API root endpoint""" return { "service": "Vibeon Translator API", "version": "3.0.0", "status": "running", "environment": settings.environment, "endpoints": { "docs": "/docs", "redoc": "/redoc", "health": "/health", "auth": "/api/auth", "translate": "/api/translate", "social": "/api/social", "vibeon": "/api/vibeon", "voice": "/api/voice" }, "features": { "text_translation": True, "audio_translation": True, "document_translation": True, "batch_translation": True, "realtime_translation": True, "quality_assessment": True, "multi_model_support": True, "database_persistence": True } } # Health check endpoint @app.get("/health") async def health_check(): """Comprehensive health check""" health_status = { "status": "healthy", "service": "Vibeon Translator API", "version": "3.0.0", "timestamp": str(Path(__file__).stat().st_mtime) } # Check database connection try: from database import engine from sqlalchemy import text with engine.connect() as conn: conn.execute(text("SELECT 1")) health_status["database"] = "connected" except Exception as e: health_status["database"] = f"disconnected: {str(e)}" health_status["status"] = "degraded" # Check translation service try: from services.translation_service import unified_translation_service as real_translation_service if real_translation_service.initialized: health_status["translator"] = "ready" else: health_status["translator"] = "not_initialized" health_status["status"] = "degraded" except Exception as e: health_status["translator"] = f"error: {str(e)}" health_status["status"] = "degraded" # Check cache service try: from services.cache_service import cache_service cache_stats = cache_service.get_stats() health_status["cache"] = "ready" if cache_stats["enabled"] else "disabled" except Exception as e: health_status["cache"] = f"error: {str(e)}" return health_status # Database info endpoint @app.get("/api/database/info") async def database_info(): """Get database information""" try: from database import engine from sqlalchemy import inspect inspector = inspect(engine) tables = inspector.get_table_names() return { "database": settings.db_name, "host": settings.db_host, "port": settings.db_port, "tables": tables, "table_count": len(tables) } except Exception as e: return { "error": str(e), "database": settings.db_name } @app.get("/memory/status") async def get_memory_status(): """Get current memory usage and optimization status""" try: from utils.memory_optimizer import get_memory_status return get_memory_status() except Exception as e: return {"error": str(e)} @app.post("/memory/cleanup") async def cleanup_memory(): """Manually trigger memory cleanup""" try: from utils.memory_optimizer import cleanup_memory cleanup_memory() return {"status": "success", "message": "Memory cleanup completed"} except Exception as e: return {"error": str(e)} if __name__ == "__main__": import uvicorn logger.info("="*60) logger.info("Starting Vibeon Translator API") logger.info(f"Environment: {settings.environment}") logger.info(f"Host: {settings.api_host}") logger.info(f"Port: {settings.api_port}") logger.info(f"Database: {settings.db_name} @ {settings.db_host}:{settings.db_port}") logger.info(f"Docs URL: http://{settings.api_host}:{settings.api_port}/docs") logger.info("="*60) uvicorn.run( app, host=settings.api_host, port=settings.api_port, reload=settings.debug, log_level=settings.log_level.lower() )