arindae's picture
serve on hf app page
d90a0af verified
Raw
History Blame Contribute Delete
4.37 kB
"""FastAPI service — model-serving API consumed by the Django application."""
from __future__ import annotations
import os
import httpx
from fastapi import FastAPI, Header, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from scalar_fastapi import get_scalar_api_reference
from pydantic import BaseModel, Field
import providers
from languages import as_list
readme_path = os.path.join(os.path.dirname(__file__), "..", "README.md")
readme_content = ""
if os.path.exists(readme_path):
with open(readme_path, "r", encoding="utf-8") as f:
raw_text = f.read()
if raw_text.startswith("---"):
parts = raw_text.split("---", 2)
readme_content = parts[2] if len(parts) >= 3 else raw_text
else:
readme_content = raw_text
app = FastAPI(
title="Translator Model API",
description=readme_content,
version="1.3.0",
)
_origins = os.environ.get("CORS_ORIGINS", "*").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in _origins],
allow_methods=["*"],
allow_headers=["*"],
)
MAX_INPUT_CHARS = int(os.environ.get("MAX_INPUT_CHARS", "5000"))
class TranslateRequest(BaseModel):
text: str = Field(..., description="Text to translate.")
source: str = Field(..., description="Source FLORES-200 code, e.g. 'eng_Latn'.")
target: str = Field(..., description="Target FLORES-200 code, e.g. 'kor_Hang'.")
engine: str | None = Field(
None, description="Engine id; defaults to first available."
)
class TranslateResponse(BaseModel):
translation: str
source: str
target: str
engine: str
@app.get("/api/health")
def health() -> dict:
return {"status": "ok", "default_engine": providers.default_id()}
@app.get("/api/engines")
def engines() -> dict:
return {
"engines": [vars(i) for i in providers.all_infos()],
"default": providers.default_id(),
}
@app.get("/api/languages")
def languages() -> dict:
return {"languages": as_list()}
@app.post("/api/translate", response_model=TranslateResponse)
def translate_endpoint(
req: TranslateRequest,
x_gemini_key: str | None = Header(default=None),
x_groq_key: str | None = Header(default=None),
) -> TranslateResponse:
if not req.text.strip():
return TranslateResponse(
translation="",
source=req.source,
target=req.target,
engine=req.engine or "",
)
if len(req.text) > MAX_INPUT_CHARS:
raise HTTPException(400, f"Input too long (max {MAX_INPUT_CHARS} chars).")
engine_id = req.engine or providers.default_id()
if not engine_id:
raise HTTPException(503, "No translation engine is configured.")
provider = providers.get(engine_id)
if provider is None:
raise HTTPException(400, f"Unknown engine: {engine_id!r}")
# Client-supplied key (bring-your-own-key) for API engines.
client_keys = {"gemini": x_gemini_key, "groq": x_groq_key}
api_key = client_keys.get(provider.key_field) if provider.key_field else None
if provider.kind == "api":
if not (provider.is_available() or api_key):
raise HTTPException(
503, provider.setup_hint or "Add an API key in Settings."
)
elif not provider.is_available():
raise HTTPException(
503, provider.setup_hint or f"Engine '{engine_id}' is not available."
)
try:
result = provider.translate(req.text, req.source, req.target, api_key=api_key)
except ValueError as exc:
raise HTTPException(400, str(exc)) from exc
except httpx.HTTPStatusError as exc:
raise HTTPException(
502, f"Upstream API error: {exc.response.status_code}"
) from exc
except Exception as exc: # noqa: BLE001 — surface engine errors to the client
raise HTTPException(500, f"Translation failed: {exc}") from exc
return TranslateResponse(
translation=result, source=req.source, target=req.target, engine=engine_id
)
@app.get("/scalar", include_in_schema=False)
async def scalar_html():
return get_scalar_api_reference(
openapi_url=app.openapi_url,
title=app.title,
)
@app.get("/", include_in_schema=False)
def root():
return RedirectResponse(url="/scalar")