File size: 9,284 Bytes
ecb9f70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""FastAPI application β€” entry point for the Bodhi HTTP server."""

from __future__ import annotations

import os
from contextlib import asynccontextmanager

# Set TensorFlow environment variables BEFORE any imports
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")  # Suppress TF warnings
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "-1")  # Force CPU usage
os.environ.setdefault("TF_ENABLE_ONEDNN_OPTS", "0")  # Disable oneDNN warnings

import redis
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

load_dotenv()

# Configure logging for Bodhi debug output
import logging
import warnings

# Suppress TensorFlow and related warnings
warnings.filterwarnings('ignore', category=FutureWarning)
warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', message='.*CUDA.*')
warnings.filterwarnings('ignore', message='.*GPU.*')
warnings.filterwarnings('ignore', message='.*CuDNN.*')

logging.basicConfig(level=logging.INFO)
logging.getLogger("bodhi").setLevel(logging.DEBUG)

# Suppress TensorFlow logs
logging.getLogger("tensorflow").setLevel(logging.ERROR)
logging.getLogger("absl").setLevel(logging.ERROR)


@asynccontextmanager
async def lifespan(app: FastAPI):
    from src.storage import BodhiStorage
    from src.cache import BodhiCache
    from src.services.llm import create_llm
    from src.graph import build_interview_graph, create_durable_checkpointer
    from src.api.auth import assert_auth_configured
    from loguru import logger

    # Fail fast if auth is neither configured nor explicitly bypassed.
    assert_auth_configured()

    db_url = os.getenv("DATABASE_URL", "")
    if not db_url:
        raise RuntimeError("DATABASE_URL is required for the API server")

    storage = BodhiStorage(db_url)
    storage.init_tables()
    app.state.storage = storage

    redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
    try:
        logger.info(f"Attempting to connect to Redis at: {redis_url}")
        cache = BodhiCache(redis_url)
        logger.info("βœ“ Redis connection successful and verified")
    except redis.ConnectionError as e:
        logger.error(f"βœ— Redis connection failed: {e}")
        logger.error(f"  Redis URL: {redis_url}")
        logger.error(f"  Please ensure Redis server is running:")
        logger.error(f"    - Windows: Start Redis service or run 'redis-server'")
        logger.error(f"    - Check if port 6379 is accessible")
        logger.error(f"    - Verify firewall settings")
        cache = None
    except Exception as e:
        logger.error(f"βœ— Redis initialization error: {type(e).__name__}: {e}")
        logger.error(f"  Redis URL: {redis_url}")
        import traceback
        logger.error(traceback.format_exc())
        cache = None
    app.state.cache = cache

    google_key = os.getenv("GOOGLE_API_KEY", "")
    llm = create_llm(api_key=google_key, model="gemini-3.1-flash-lite-preview")
    app.state.llm = llm
    # Durable interview state (falls back to in-memory if deps/DB unavailable).
    checkpointer = create_durable_checkpointer(db_url)
    app.state.graph = build_interview_graph(llm, checkpointer=checkpointer)
    app.state.sarvam_key = os.getenv("SARVAM_API_KEY", "")
    app.state.deepgram_key = os.getenv("DEEPGRAM_API_KEY", "")
    app.state.tts_sample_rate = int(os.getenv("SARVAM_TTS_SAMPLE_RATE", "22050"))

    # ── Initialize Proctoring CV Models ──────────────────────────────────────
    # Each model loads independently so one failure (e.g. emotion model download)
    # doesn't disable the entire proctoring pipeline. The whole block can be
    # disabled with PROCTORING_ENABLED=false β€” useful when the native CV stack
    # (mediapipe/tensorflow) segfaults in a given environment, which would
    # otherwise crash the worker (a SIGSEGV can't be caught by try/except).
    _proctoring_enabled = os.getenv("PROCTORING_ENABLED", "true").strip().lower() in (
        "1", "true", "yes",
    )
    if not _proctoring_enabled:
        logger.warning("Proctoring disabled via PROCTORING_ENABLED=false")
        for _attr in ("face_detector", "gaze_analyzer", "object_detector", "emotion_analyzer"):
            setattr(app.state, _attr, None)
        _factories = {}
    else:
        logger.info("Loading proctoring CV models...")
        try:
            from src.proctoring_backend.services.proctoring.face_detection import FaceDetector
            from src.proctoring_backend.services.proctoring.gaze_analysis import GazeAnalyzer
            from src.proctoring_backend.services.proctoring.object_detection import ObjectDetector
            from src.proctoring_backend.services.proctoring.emotion_analysis import EmotionAnalyzer
            _factories = {
                "face_detector": FaceDetector,
                "gaze_analyzer": GazeAnalyzer,
                "object_detector": ObjectDetector,
                "emotion_analyzer": EmotionAnalyzer,
            }
        except Exception as e:
            logger.warning(f"⚠ Proctoring dependencies unavailable: {e}")
            _factories = {}

    for _attr in ("face_detector", "gaze_analyzer", "object_detector", "emotion_analyzer"):
        _factory = _factories.get(_attr)
        if _factory is None:
            setattr(app.state, _attr, None)
            continue
        try:
            setattr(app.state, _attr, _factory())
            logger.info(f"βœ“ {_attr} loaded")
        except Exception as e:
            logger.warning(f"⚠ {_attr} failed to load: {e}")
            setattr(app.state, _attr, None)

    _core_ok = all(
        getattr(app.state, a, None) is not None
        for a in ("face_detector", "gaze_analyzer", "object_detector")
    )
    logger.info(
        "Proctoring core models %s; emotion %s",
        "ready" if _core_ok else "UNAVAILABLE",
        "ready" if getattr(app.state, "emotion_analyzer", None) else "disabled",
    )

    # ── Initialize Behavioral Models ─────────────────────────────────────────
    if not _proctoring_enabled:
        logger.warning("Behavioral models skipped (PROCTORING_ENABLED=false)")
    else:
        logger.info("Loading behavioral analysis models...")
        try:
            from src.behavioral_analysis.services.speech_service import load_models as load_speech_models
            from src.behavioral_analysis.services.posture_service import load_models as load_posture_models
            load_speech_models()
            load_posture_models()
            logger.info("βœ“ Behavioral models loaded successfully")
        except Exception as e:
            logger.warning(f"⚠ Behavioral models failed to load: {e}")

    yield

    storage.close()


app = FastAPI(
    title="Bodhi API",
    description="Voice-first AI Mock Interviewer β€” HTTP API",
    version="1.0.0",
    lifespan=lifespan,
)

# Allowed origins are read from BODHI_ALLOWED_ORIGINS (comma-separated).
# A wildcard "*" combined with allow_credentials=True is invalid per the CORS
# spec and is rejected by browsers, so we require an explicit allowlist.
_origins_env = os.getenv("BODHI_ALLOWED_ORIGINS", "").strip()
_allowed_origins = (
    [o.strip() for o in _origins_env.split(",") if o.strip()]
    if _origins_env
    else ["http://localhost:3000", "http://localhost:5173"]
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=_allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=[
        "X-Bodhi-Text",
        "X-Bodhi-Phase",
        "X-Bodhi-End",
        "X-Bodhi-Session",
        "X-Bodhi-Transcript",
        "X-Bodhi-Curriculum",
        "X-Bodhi-Sentiment",
    ],
)

# Per-client rate limiting (no-op if slowapi is absent or disabled via env).
from src.api.ratelimit import setup_rate_limiting

setup_rate_limiting(app)

from src.api.roles import router as roles_router
from src.api.companies import router as companies_router
from src.api.documents import router as documents_router
from src.api.interviews import router as interviews_router
from src.api.audio import router as audio_router
from src.api.proctoring import router as proctoring_router
from src.api.resumes import router as resumes_router
from src.api.users import router as users_router

app.include_router(roles_router)
app.include_router(companies_router)
app.include_router(documents_router)
app.include_router(interviews_router)
app.include_router(audio_router)
app.include_router(proctoring_router)
app.include_router(resumes_router)
app.include_router(users_router)


@app.get("/health")
@app.get("/api/health")
async def health():
    models = {
        name: getattr(app.state, name, None) is not None
        for name in ("face_detector", "gaze_analyzer", "object_detector", "emotion_analyzer")
    }
    # Core proctoring needs face + gaze + object; emotion is an optional enhancer.
    proctoring_enabled = all(
        models[m] for m in ("face_detector", "gaze_analyzer", "object_detector")
    )
    return {
        "status": "ok",
        "proctoring_enabled": proctoring_enabled,
        "proctoring_models": models,
    }