import asyncio import os from contextlib import asynccontextmanager # CPU thread tuning — must happen BEFORE torch/onnxruntime import. # HF free tier = 2 vCPU; we want to use both but not oversubscribe. os.environ.setdefault("OMP_NUM_THREADS", "2") os.environ.setdefault("MKL_NUM_THREADS", "2") os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from src.core.config import MAX_CONCURRENT_INFERENCES, USE_ASYNC_UPLOADS from src.core.logging import log, init_logging_session, close_logging_session from src.api import danger, explorer, search, system, upload from src.api import people# Phase 3 from src.api import jobs as jobs_api # explicit alias @asynccontextmanager async def lifespan(app: FastAPI): await init_logging_session() log("INFO", "server.startup", message="Loading AI models...") from src.services.ai_manager import AIModelManager loop = asyncio.get_event_loop() app.state.ai = await loop.run_in_executor(None, AIModelManager) # Split semaphores: face detection and object embedding can overlap # without fighting for the same CPU cores. app.state.ai_semaphore = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES) app.state.face_semaphore = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES) app.state.object_semaphore = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES) # Phase 3: start background job worker if async uploads are enabled worker_task = None if USE_ASYNC_UPLOADS: from src.services.jobs import run_worker worker_task = asyncio.create_task(run_worker(app.state)) log("INFO", "server.worker_started", message="Async upload worker running") log("INFO", "server.ready", message="All models loaded. API ready.") yield # Graceful shutdown if worker_task: worker_task.cancel() try: await worker_task except asyncio.CancelledError: pass log("INFO", "server.shutdown", message="API shutting down.") await close_logging_session() app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["https://photofinderv2.vercel.app"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) os.makedirs("temp_uploads", exist_ok=True) # Existing routers app.include_router(system.router) app.include_router(upload.router) app.include_router(search.router) app.include_router(explorer.router) app.include_router(danger.router) # Phase 3 routers app.include_router(people.router) app.include_router(jobs_api.router)