"""Senioren-Beruhigungssätze für die Web-Suche — englische Master-Quelle. DIES IST DIE EINZIGE STELLE zum Pflegen der Sätze. Zur Laufzeit werden sie einmal je Sprache übersetzt und gecacht (die Standardsprache wird beim Serverstart vorgewärmt → sofortige Ausgabe ohne Verzögerung). Zwei Pools: - OPENING: sofort beim Such-Start ("Einen Moment, ich schaue kurz nach.") - PATIENCE: bei längerer Recherche nachgeschoben ("Bitte noch etwas Geduld …") Beide werden ZUFÄLLIG gewählt, damit schnelles Feedback kommt und es nicht roboterhaft eintönig klingt. Siehe Docs/weg2-tool-calling.md §5.4. """ import asyncio import logging import random import re import httpx logger = logging.getLogger(__name__) ENDPOINT = "https://openrouter.ai/api/v1/chat/completions" # --- Master-Sätze (Englisch — einzige Quelle der Wahrheit) ------------------ OPENING_PHRASES = [ "One moment, let me look that up for you.", "Just a second, I'll check that for you right away.", "Let me find that out for you — one moment.", "Good question — let me check the latest on that.", "Hold on just a moment, I'm looking it up.", "Let me quickly find the current information for you.", "One little moment while I check that for you.", "I'll go and look that up for you right now.", "Let me see what the latest is — just a moment.", "Give me a second, I'll find that out for you.", "I'm checking that for you now, one moment please.", "Let me look that up so I can tell you exactly.", ] PATIENCE_PHRASES = [ "Thank you for your patience, I'm almost there.", "Just a little longer, please — I'm still looking.", "Bear with me a moment, I'm nearly done.", "Almost ready — thank you for waiting.", "I'm still checking, just a few more seconds.", "Please hold on a little longer, I'm getting there.", "Nearly finished — thank you for your patience.", "Just a moment more, I want to get this right for you.", "I'm still looking it up, please stay with me.", "It's taking a little longer, but I'm almost done.", "Thank you for waiting — I'll have it for you shortly.", "Hang on just a bit longer, I'm nearly there.", "I haven't forgotten you — I'm still checking.", "Almost there now, thank you for being so patient.", "Just finishing up — one more moment, please.", "I'm making sure I get it right for you — nearly done.", ] _LANG_DISPLAY = { "de": "German", "en": "English", "fr": "French", "es": "Spanish", "it": "Italian", "nl": "Dutch", "pt": "Portuguese", "pl": "Polish", "ar": "Arabic", "ru": "Russian", "zh": "Chinese", } def _parse_numbered(text: str, n: int) -> list[str] | None: """Liest eine nummerierte Liste; liefert genau n Zeilen oder None (→ Fallback).""" out = [] for raw in (text or "").splitlines(): s = re.sub(r"^\s*\d+[.)]\s*", "", raw.strip()).strip().strip('"').strip("„“”") if s: out.append(s) return out[:n] if len(out) >= n else None def make_openrouter_translator(api_key: str, model: str): """Liefert eine async translate_pool(phrases, language) -> list[str] | None.""" api_key = (api_key or "").strip() model = (model or "").strip() async def translate_pool(phrases: list[str], language: str) -> list[str] | None: if not api_key or not model: return None target = _LANG_DISPLAY.get(language, language) numbered = "\n".join(f"{i + 1}. {p}" for i, p in enumerate(phrases)) prompt = ( f"Translate each numbered phrase into {target}. These are short, warm, " f"reassuring things a voice assistant says to an elderly person while it " f"looks something up. Keep them natural and spoken. ALWAYS use the polite, " f"formal form of address (German 'Sie', not 'du'; French 'vous'; Spanish " f"'usted'; Italian 'Lei'; Dutch 'u') consistently. Same count, same order, " f"no quotes, no extra words. Output ONLY a numbered list of the translations." f"\n\n{numbered}" ) payload = {"model": model, "temperature": 0.4, "messages": [{"role": "user", "content": prompt}]} headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.post(ENDPOINT, json=payload, headers=headers) resp.raise_for_status() text = resp.json()["choices"][0]["message"]["content"] return _parse_numbered(text, len(phrases)) return translate_pool class FillerPhrases: """Zufällige, in die Zielsprache übersetzte Beruhigungssätze (einmal je Sprache gecacht).""" def __init__(self, translate_pool=None): self._translate_pool = translate_pool self._cache: dict[str, dict] = { "en": {"opening": OPENING_PHRASES, "patience": PATIENCE_PHRASES}} self._locks: dict[str, asyncio.Lock] = {} async def warm(self, language: str | None) -> None: """Vorab übersetzen (z. B. Standardsprache beim Serverstart).""" await self._ensure(language) async def _ensure(self, language: str | None) -> str: lang = (language or "en").lower() if lang in self._cache: return lang if self._translate_pool is None: return "en" # ohne Übersetzer: englische Master-Sätze lock = self._locks.setdefault(lang, asyncio.Lock()) async with lock: if lang in self._cache: return lang opening = patience = None try: opening = await self._translate_pool(OPENING_PHRASES, lang) patience = await self._translate_pool(PATIENCE_PHRASES, lang) except Exception as exc: # noqa: BLE001 — best effort, Fallback Englisch logger.warning("Filler-Übersetzung (%s) fehlgeschlagen: %s", lang, exc) self._cache[lang] = {"opening": opening or OPENING_PHRASES, "patience": patience or PATIENCE_PHRASES} return lang async def opening(self, language: str | None) -> str: lang = await self._ensure(language) return random.choice(self._cache[lang]["opening"]) async def patience(self, language: str | None) -> str: lang = await self._ensure(language) return random.choice(self._cache[lang]["patience"])