"""FastAPI: source text → translated text via Claude Sonnet 4. Supports auto-detection of source language and a configurable tone. """ import json import logging import os from typing import Literal from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from openai import AsyncOpenAI from pydantic import BaseModel, Field logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="Translator") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) MODEL = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-20250514") _client: AsyncOpenAI | None = None def _get_client() -> AsyncOpenAI: global _client if _client is None: _client = AsyncOpenAI( base_url=os.getenv("LITELLM_BASE_URL", "http://host.docker.internal:4000/v1"), api_key=os.getenv("LITELLM_API_KEY", "sk-dummy"), ) return _client # Display name (Czech) and English label for the LLM prompt. LANGUAGES = [ {"code": "auto", "cs": "Automaticky rozpoznat", "en": "auto-detect"}, {"code": "cs", "cs": "Čeština", "en": "Czech"}, {"code": "en", "cs": "Angličtina", "en": "English"}, {"code": "sk", "cs": "Slovenština", "en": "Slovak"}, {"code": "de", "cs": "Němčina", "en": "German"}, {"code": "pl", "cs": "Polština", "en": "Polish"}, {"code": "uk", "cs": "Ukrajinština", "en": "Ukrainian"}, {"code": "ru", "cs": "Ruština", "en": "Russian"}, {"code": "fr", "cs": "Francouzština", "en": "French"}, {"code": "it", "cs": "Italština", "en": "Italian"}, {"code": "es", "cs": "Španělština", "en": "Spanish"}, {"code": "pt", "cs": "Portugalština", "en": "Portuguese"}, {"code": "nl", "cs": "Nizozemština", "en": "Dutch"}, {"code": "hu", "cs": "Maďarština", "en": "Hungarian"}, {"code": "ro", "cs": "Rumunština", "en": "Romanian"}, {"code": "bg", "cs": "Bulharština", "en": "Bulgarian"}, {"code": "tr", "cs": "Turečtina", "en": "Turkish"}, {"code": "zh", "cs": "Čínština", "en": "Chinese (Simplified)"}, {"code": "ja", "cs": "Japonština", "en": "Japanese"}, {"code": "ar", "cs": "Arabština", "en": "Arabic"}, ] LANG_BY_CODE = {l["code"]: l for l in LANGUAGES} TONES = { "formal": "formal, professional, polished business register", "casual": "casual, friendly, conversational", "technical": "technical, precise, preserving exact technical terms", "marketing": "marketing copy — engaging, persuasive, brand-voice", "legal": "legal / contractual — precise, neutral, preserving legal terms of art", } class TranslateRequest(BaseModel): text: str = Field(..., min_length=1, max_length=20000) source_lang: str = "auto" target_lang: str = "en" tone: Literal["formal", "casual", "technical", "marketing", "legal"] = "formal" @app.get("/") async def root(): return FileResponse("static/index.html") @app.get("/api/languages") async def languages(): return {"languages": LANGUAGES} @app.post("/api/translate") async def translate(req: TranslateRequest): target = LANG_BY_CODE.get(req.target_lang) if not target or target["code"] == "auto": raise HTTPException(400, "Vyberte cílový jazyk") source_label = ( "auto-detect the source language" if req.source_lang == "auto" else f"the source language is {LANG_BY_CODE.get(req.source_lang, {}).get('en', req.source_lang)}" ) system = f"""You are a professional translator producing high-quality business translations. Translate the user's input into **{target['en']}**. {source_label.capitalize()}. Tone: **{TONES[req.tone]}**. Strict rules: - Output ONLY the translated text. No quotes, no preamble, no explanation, no language tag. - Preserve formatting: line breaks, lists, paragraphs, code blocks, URLs. - Keep proper names, brand names, product codes, and acronyms unchanged unless they have a well-established translation in the target language. - Numbers, dates, and currencies: convert format conventions to the target language (e.g. decimal comma vs dot, date format) but do NOT convert values. - If the input is already in the target language, return it unchanged. - Never add information that isn't in the source.""" try: resp = await _get_client().chat.completions.create( model=MODEL, messages=[ {"role": "system", "content": system}, {"role": "user", "content": req.text}, ], temperature=0.2, max_tokens=8000, ) except Exception as exc: logger.exception("LLM call failed") raise HTTPException(500, f"Překlad selhal: {exc}") translated = (resp.choices[0].message.content or "").strip() # Strip accidental markdown code fences if translated.startswith("```") and translated.endswith("```"): translated = translated.removeprefix("```").removesuffix("```").strip() return { "translated": translated, "source_lang": req.source_lang, "target_lang": req.target_lang, "tone": req.tone, } @app.get("/health") async def health(): return {"status": "ok"} app.mount("/static", StaticFiles(directory="static"), name="static")