From 1c11cb6a2f1bb0932f41370879b223633fe6b13d Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 22:02:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(bridge):=20GPS=E2=86=92Ortsname=20im=20Sta?= =?UTF-8?q?ndort-Pr=C3=A4fix=20(Reverse-Geocode,=20keine=20Tokens)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das lokale 8B kann Koordinaten nicht zuverlässig in eine Stadt uebersetzen (riet "Berlin" fuer Oldenburg). Claude kann's, aber das kostet Tokens. Loesung: die Bridge loest lat/lon EINMAL via Nominatim (OpenStreetMap, gratis, keyless) auf und schreibt den Namen in den Praefix: [Stefans aktueller Standort: Oldenburg, Niedersachsen (GPS 53.12, 8.22). ...] So muss KEIN Modell mehr raten — local UND Claude lesen den Ort direkt ab, Token-Ersparnis bleibt voll erhalten. - _persist_location merkt sich den letzten Ort + triggert den Geocode async (im Hintergrund, gecacht pro ~1km-Raster, nur bei Bewegung → Nominatim- Rate-Limit locker eingehalten). - _build_core_text nutzt frische ODER letzte bekannte Position (Praefix faellt beim passiven Weiterreden ohne frisches GPS nicht mehr weg) + den Ortsnamen. - NOMINATIM_URL env (Default oeffentlicher Dienst) → spaeter self-hostbar. Reiner Bridge-Fix, kein APK/Brain. Co-Authored-By: Claude Opus 4.8 --- bridge/aria_bridge.py | 78 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index f723676..5e28c08 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1175,8 +1175,12 @@ class ARIABridge: "lat": float(lat), "lon": float(lon), }) + # Fuer den Standort-Praefix merken (auch fuer Turns ohne frisches GPS). + self._last_location = {"lat": float(lat), "lon": float(lon)} except Exception: return + # Ortsname im Hintergrund aufloesen (Nominatim, gecacht) — kein Modell. + self._maybe_reverse_geocode(float(lat), float(lon)) # Fire-and-forget: Brain-on-demand-Tick. Wenn Brain nicht antwortet # oder langsam ist, blockt das nicht den GPS-Pfad. try: @@ -1184,6 +1188,57 @@ class ARIABridge: except Exception: pass + def _maybe_reverse_geocode(self, lat: float, lon: float) -> None: + """Loest lat/lon → Ortsname auf, aber nur bei nennenswerter Bewegung + (~1 km Raster) und im Hintergrund. Ergebnis landet in + self._last_place_name; der Standort-Praefix liest es dann nur ab.""" + try: + key = (round(lat, 2), round(lon, 2)) # ~1.1 km — reicht fuer "Stadt" + except Exception: + return + if key == getattr(self, "_geocoded_key", None): + return # diese Gegend schon aufgeloest + # Neue Gegend: alten Namen verwerfen, sonst zeigt der Praefix die falsche + # Stadt bis der neue Geocode da ist. + self._geocoded_key = key + self._last_place_name = "" + try: + asyncio.create_task(self._do_reverse_geocode(lat, lon, key)) + except Exception: + pass + + async def _do_reverse_geocode(self, lat: float, lon: float, key) -> None: + """Nominatim-Reverse-Geocode (keyless, gratis). Setzt self._last_place_name. + Fehler/Timeout → still (Praefix faellt dann auf reine Koordinaten zurueck).""" + base = os.environ.get("NOMINATIM_URL", "https://nominatim.openstreetmap.org").rstrip("/") + url = (f"{base}/reverse?lat={lat:.5f}&lon={lon:.5f}" + f"&format=jsonv2&zoom=10&accept-language=de") + + def _fetch(): + try: + req = urllib.request.Request(url, headers={ + # Nominatim verlangt einen aussagekraeftigen User-Agent. + "User-Agent": "ARIA-Assistant/1.0 (personal voice assistant)", + }) + with urllib.request.urlopen(req, timeout=8) as r: + return json.loads(r.read().decode("utf-8", "ignore")) + except Exception as exc: + logger.debug("[geo] reverse-geocode fehlgeschlagen: %s", exc) + return None + + data = await asyncio.get_event_loop().run_in_executor(None, _fetch) + if not isinstance(data, dict): + return + addr = data.get("address") or {} + place = (addr.get("city") or addr.get("town") or addr.get("village") + or addr.get("municipality") or addr.get("county") or "") + state = addr.get("state") or "" + name = f"{place}, {state}" if (place and state) else (place or data.get("name") or "") + # Nur uebernehmen wenn Stefan nicht schon weitergezogen ist (key aktuell). + if name and getattr(self, "_geocoded_key", None) == key: + self._last_place_name = name + logger.info("[geo] %.5f,%.5f → %s", lat, lon, name) + async def _trigger_brain_check_now(self) -> None: """Brain-Endpoint POST /triggers/check-now anstossen.""" brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080") @@ -1557,14 +1612,25 @@ class ARIABridge: "gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur, " "Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.]" ) - if location and isinstance(location, dict): - lat = location.get("lat") - lon = location.get("lon") or location.get("lng") + # Standort: frische GPS aus der Nachricht, sonst der zuletzt bekannte + # (damit's beim passiven Weiterreden ohne frisches GPS nicht wegfaellt). + loc = location if (isinstance(location, dict) and location.get("lat") is not None) \ + else getattr(self, "_last_location", None) + if isinstance(loc, dict): + lat = loc.get("lat") + lon = loc.get("lon") or loc.get("lng") if lat is not None and lon is not None: + # Ortsname kommt vom Reverse-Geocode (Nominatim, gecacht) — so + # muss KEIN Modell Koordinaten raten (das lokale 8B tippt sonst + # daneben, z.B. "Berlin" fuer Oldenburg). Noch nicht aufgeloest + # → nur Koordinaten (Claude kann die zur Not selbst deuten). + place = getattr(self, "_last_place_name", "") + where = f"{place} " if place else "" parts.append( - f"[Stefans aktuelle GPS-Position: {float(lat):.6f}, {float(lon):.6f}. " - f"Nutze die nur wenn die Frage sich auf seinen Standort bezieht. " - f"Erwaehne sie nicht von dir aus, ausser er fragt explizit danach.]" + f"[Stefans aktueller Standort: {where}(GPS {float(lat):.5f}, " + f"{float(lon):.5f}). Nutze das nur wenn die Frage sich auf seinen " + f"Standort bezieht. Erwaehne ihn nicht von dir aus, ausser er " + f"fragt explizit danach.]" ) if parts: return " ".join(parts) + " " + text