# ============================================================ # main.py # ------------------------------------------------------------ # Titan API Gateway — FastAPI application entry point. # # V2 Changes: # ★ APScheduler lifespan — auto-publishes Immersion story # every hour (00:05, 01:05, 02:05, … UTC) — 24 stories/day. # No manual cron job needed. # ★ Added admin_router (/admin/refresh-hot-cache). # # Responsibilities: # 1. Create the FastAPI app instance with metadata. # 2. Configure CORS + SharedArrayBuffer security headers. # 3. Include all feature routers (Cinema, Immersion, Chat, Translate, AI Tasks, Admin). # 4. Start/stop the Immersion story scheduler via lifespan. # 5. Provide a public /health endpoint for HF Space monitoring. # 6. Launch uvicorn when executed directly. # # Hugging Face Spaces notes: # - HF expects the app to bind on 0.0.0.0:7860. # - Set INTERNAL_BASE_URL=http://localhost:7860 in HF Secrets. # - Set TITAN_INTERNAL_KEY in HF Secrets (same key sent as the # X-Titan-Key header — see core/security.py for the current # auth module; this replaced the old hardcoded "Titan2026_Admin" # string and the stale TITAN_API_KEY name this comment used to say). # - Titan Academy (see api/academy.py) additionally needs # AGENTROUTER_API_KEY in HF Secrets for its Opus calls. # ============================================================ import logging import os import time import asyncio from contextlib import asynccontextmanager from pathlib import Path import uvicorn from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse, FileResponse from fastapi.staticfiles import StaticFiles from api.cinema import router as cinema_router from api.immersion import router as immersion_router from api.academy import router as academy_router # ★ Titan Academy — standalone module, see api/academy.py # ★ استيراد الراوترات (Chat, Translate, AI, Admin, Live) from api.chat import chat_router, translate_router, ai_router, admin_router, live_router, image_router, messaging_router, story_companion_router # 8 routers from api.scheduler import start_scheduler, stop_scheduler # ============================================================ # Logging # ============================================================ logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)-8s | %(name)s — %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger("titan.main") # Paths exempt from COEP/COOP headers — HF health checker hits these # from a different origin; require-corp would cause silent failures. _HEALTH_PATHS = {"/", "/health"} # ============================================================ # Lifespan — scheduler start/stop # ============================================================ # ============================================================ # Generated Images — served directly from /tmp/titan_images/ # URL: GET /images/{filename} # Auto-cleanup: files older than 2 hours are deleted # ============================================================ IMAGES_DIR = Path("/tmp/titan_images") IMAGES_DIR.mkdir(parents=True, exist_ok=True) IMAGE_TTL_SECONDS = 7200 # 2 hours async def _image_cleanup_loop(): """Deletes generated images older than IMAGE_TTL_SECONDS every 30 minutes.""" while True: await asyncio.sleep(1800) # every 30 minutes try: now = time.time() deleted = 0 for f in IMAGES_DIR.iterdir(): if f.is_file() and (now - f.stat().st_mtime) > IMAGE_TTL_SECONDS: f.unlink(missing_ok=True) deleted += 1 if deleted: logger.info("🗑️ Image cleanup — deleted %d expired files", deleted) except Exception as exc: logger.warning("Image cleanup error: %s", exc) @asynccontextmanager async def lifespan(app: FastAPI): """Start Immersion auto-scheduler on startup, stop on shutdown.""" logger.info("🚀 Titan Gateway starting up…") start_scheduler() asyncio.create_task(_image_cleanup_loop()) yield logger.info("🛑 Titan Gateway shutting down…") stop_scheduler() # ============================================================ # FastAPI application # ============================================================ app = FastAPI( title="Titan API Gateway", description=( "Master API Gateway for Titan applications. " "Provides AI-powered transcription (Groq Whisper) and translation (Google Gemini/Gemma) " "services with built-in key rotation, rate-limit handling, and zero disk I/O. " # ★ CORRECTED: was "auto-published every 2 hours via APScheduler" — no longer # true as of scheduler.py V6.0 (auto-scheduling was deliberately disabled; # this line was stale and misleading anyone reading /docs). "Titan Immersion stories and Titan Academy units are generated via manual/internal " "trigger endpoints, not an automatic schedule. " "All endpoints require the `X-Titan-Key` header for authentication." ), version="23.0.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, ) # ============================================================ # Security Headers Middleware # Required for FFmpeg.wasm SharedArrayBuffer support. # ============================================================ @app.middleware("http") async def add_security_headers(request: Request, call_next): response = await call_next(request) if request.url.path not in _HEALTH_PATHS: response.headers["Cross-Origin-Opener-Policy"] = "same-origin" response.headers["Cross-Origin-Embedder-Policy"] = "require-corp" return response # ============================================================ # CORS # ============================================================ ALLOWED_ORIGINS: list[str] = os.environ.get("CORS_ORIGINS", "*").split(",") app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["X-Request-ID"], ) # ============================================================ # Routers # ============================================================ app.include_router(cinema_router) app.include_router(immersion_router) app.include_router(academy_router) # ★ Titan Academy: POST /api/v1/academy/generate-next-unit (manual/internal — see # api/scheduler.py's own V6.0 note on why story auto-scheduling is disabled; # Academy follows the same manual-trigger philosophy, not APScheduler) app.include_router(chat_router) # POST /api/v1/chat/ask & /ask/sync app.include_router(translate_router) # POST /api/v1/translate app.include_router(ai_router) # POST /api/v1/ai/task app.include_router(admin_router) # ★ POST /admin/refresh-hot-cache app.include_router(live_router) # ★ WebSocket /ws/live/{model_id} app.include_router(image_router) # ★ POST /api/v1/image/generate | /enhance app.include_router(messaging_router) # ★ Titan Connect: POST /transcribe, GET/POST /tts/* app.include_router(story_companion_router) # ★ Titan Immersion: POST /api/v1/immersion/story-chat (AI narrator) # ============================================================ # Generated Image Serving # GET /images/{filename} → serves from /tmp/titan_images/ # ============================================================ @app.get("/images/{filename}", tags=["Images"]) async def serve_generated_image(filename: str): """Serves a locally generated image — valid for 2 hours after creation.""" if "/" in filename or "\\" in filename or ".." in filename: from fastapi import HTTPException raise HTTPException(status_code=400, detail="Invalid filename") filepath = IMAGES_DIR / filename if not filepath.exists(): from fastapi import HTTPException raise HTTPException(status_code=404, detail="Image not found or expired") ext = filepath.suffix.lower().lstrip(".") media_types = {"webp": "image/webp", "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg"} return FileResponse( path=str(filepath), media_type=media_types.get(ext, "image/webp"), headers={"Cache-Control": "public, max-age=7200"}, ) # ============================================================ # Health check (no auth — used by HF Spaces + load balancers) # ============================================================ @app.get( "/health", tags=["System"], summary="Gateway health check", ) async def health_check() -> dict: return { "status": "ok", "gateway": "Titan API Gateway", "version": "23.0.0", } # ============================================================ # Root → Health Check (To satisfy Hugging Face) # ============================================================ @app.get("/", tags=["System"]) async def root() -> dict: return { "status": "ok", "message": "Titan Gateway V23.0 is Alive and Ready!", "docs_url": "/docs", "health": "All systems operational" } # ============================================================ # Entry point # ============================================================ if __name__ == "__main__": uvicorn.run( "main:app", host="0.0.0.0", port=7860, reload=os.environ.get("RELOAD", "true").lower() == "true", log_level="info", workers=1, )