2026-06-29 22:34:15 +02:00
|
|
|
"""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.",
|
2026-06-30 13:53:46 +02:00
|
|
|
"Let me check the current details for you — it won't take long.",
|
|
|
|
|
"I'll find the up-to-date answer for you, one moment.",
|
|
|
|
|
"Allow me a brief moment to look into that.",
|
|
|
|
|
"Let me gather the latest information for you.",
|
|
|
|
|
"I'm on it — just give me a short moment.",
|
|
|
|
|
"Let me make sure I have the current facts for you.",
|
|
|
|
|
"One moment please, I'm fetching the latest for you.",
|
|
|
|
|
"Let me quickly check what's current on that.",
|
|
|
|
|
"I'll look into that for you right away, just a moment.",
|
|
|
|
|
"Bear with me a second while I find that out.",
|
|
|
|
|
"Let me get you the most recent information on that.",
|
|
|
|
|
"Just a brief moment — I'm checking the latest for you.",
|
|
|
|
|
"I'll dig that up for you now, one moment please.",
|
|
|
|
|
"Let me find the current answer so I can tell you properly.",
|
|
|
|
|
"Let me take a quick look and find that out for you.",
|
2026-06-29 22:34:15 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
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"])
|