Spaces:
Running
Running
| """ | |
| FastAPI wrapper around the SigLIP text encoder for HuggingFace Spaces. | |
| POST /encode {query} -> {vector:[768]} (bearer-token auth) | |
| GET /health -> {status:"ok"} (open, for the keep-warm ping) | |
| The model + tokenizer load once at startup (lifespan) and stay resident, so every | |
| request after wake is warm. The 283 MB model is baked into the Docker image, so a | |
| post-sleep restart doesn't re-download. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import secrets | |
| from contextlib import asynccontextmanager | |
| from fastapi import Depends, FastAPI, Header, HTTPException | |
| from pydantic import BaseModel | |
| from text_encoder import EMBED_DIM, encode, warmup | |
| ENCODER_TOKEN = os.environ.get("SIGLIP_ENCODER_TOKEN", "") | |
| async def lifespan(_app: FastAPI): | |
| warmup() | |
| yield | |
| app = FastAPI(lifespan=lifespan) | |
| class EncodeRequest(BaseModel): | |
| query: str | |
| class EncodeResponse(BaseModel): | |
| vector: list[float] | |
| def require_token(authorization: str = Header(default="")) -> None: | |
| if not ENCODER_TOKEN: | |
| raise HTTPException(status_code=500, detail="encoder token not configured") | |
| # Constant-time compare so the endpoint isn't a timing oracle for the token. | |
| if not secrets.compare_digest(authorization, f"Bearer {ENCODER_TOKEN}"): | |
| raise HTTPException(status_code=401, detail="unauthorized") | |
| def health() -> dict[str, str]: | |
| return {"status": "ok"} | |
| def encode_query(body: EncodeRequest) -> EncodeResponse: | |
| query = body.query.strip() | |
| if not query: | |
| raise HTTPException(status_code=422, detail="empty query") | |
| vec = encode(query) | |
| if len(vec) != EMBED_DIM: | |
| raise HTTPException(status_code=500, detail=f"expected {EMBED_DIM} dims, got {len(vec)}") | |
| return EncodeResponse(vector=vec) | |