File size: 2,723 Bytes
f1ef7e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import threading
from contextlib import asynccontextmanager
from pathlib import Path

from dotenv import load_dotenv

# Load the repo-root .env for local dev BEFORE importing anything that reads
# env at import time (database.py builds the engine from DATABASE_URL). On the
# Space there is no .env; the real environment (Space secrets) is used and
# always takes precedence -- load_dotenv never overrides existing vars.
load_dotenv(Path(__file__).resolve().parents[2] / ".env")

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.database import init_db
from app.routers import calls, sentiment, artifacts
from app import worker


def _boot_model_prewarm():
    """Pre-download the acoustic weights so the first job doesn't pay for it.
    Best-effort, off-thread; only when acoustic is enabled."""
    if os.environ.get("ENABLE_ACOUSTIC", "0").strip().lower() in ("", "0", "false", "no"):
        return
    try:
        from app.pipeline_bridge import install
        install()
        from acoustic import resolve_model_dir
        resolve_model_dir()
        print("[startup] acoustic weights ready")
    except Exception as e:
        print(f"[startup] acoustic pre-warm skipped: {e}")


@asynccontextmanager
async def lifespan(app):
    try:
        init_db()
    except Exception as e:
        print(f"[startup] init_db skipped (db unreachable?): {e}")
    if os.environ.get("START_WORKER", "1").strip().lower() not in ("", "0", "false", "no"):
        worker.start()
    threading.Thread(target=_boot_model_prewarm, name="model-prewarm",
                     daemon=True).start()
    yield


app = FastAPI(
    title="AI/ML Capstone API",
    description="Fresh-call ingest, pipeline worker, and artifact serving",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS: the Vercel frontend calls this API cross-origin. FRONTEND_ORIGIN pins
# the allowed origin(s) in production (comma-separated); "*" for local dev.
# Credentials are disabled under wildcard (the spec forbids "*" + credentials);
# the frontend uses plain fetch with no cookies, so this is fine either way.
_origins = os.environ.get("FRONTEND_ORIGIN", "*")
_wild = _origins.strip() == "*"
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"] if _wild else [o.strip() for o in _origins.split(",")],
    allow_credentials=not _wild,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(calls.router)
app.include_router(sentiment.router)
app.include_router(artifacts.router)


@app.get("/")
def read_root():
    return {"status": "online", "service": "backend-orchestration"}


@app.get("/health")
def health_check():
    return {"status": "ok", "service": "capstone_api"}