File size: 4,373 Bytes
034506e
 
 
 
 
 
 
 
 
d90a0af
012abcf
034506e
 
 
 
 
d90a0af
 
 
 
 
 
 
 
 
 
 
 
 
034506e
 
d90a0af
 
034506e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
012abcf
 
 
034506e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
012abcf
 
 
 
034506e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
012abcf
 
 
034506e
012abcf
 
 
034506e
 
 
 
 
 
012abcf
 
 
034506e
 
 
 
 
 
012abcf
 
 
 
 
 
 
 
d90a0af
 
 
 
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
"""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")