fix: Viewport-Bias entfernt + Timestamp-Sonnenstand + hoehere Aufloesung

Kernproblem: Claude wurde durch Viewport-Position beeinflusst
(User schaut auf Sueddeutschland → Claude sagt Bayern)

Fixes:
- Viewport-Info wird NICHT mehr an VLM-Prompt uebergeben
- Stattdessen: Dateiname-Timestamp extrahiert (20231010_155544 → Datum)
- Sonnenstand fuer 3 Breitengrade berechnet und als Kontext mitgegeben
- Prompt: explizite Anweisung, sich NUR auf Bildinhalt zu stuetzen
- Bildaufloesung von 1500px auf 2500px erhoeht (mehr Details)
- Besseres Logging: Koordinaten, Gewaesser, Region
Dieser Commit ist enthalten in:
Claude Dev
2026-03-26 11:03:59 +01:00
Ursprung 51e85b2572
Commit 86db66a787

Datei anzeigen

@@ -19,7 +19,7 @@ router = APIRouter()
_CLAUDE_BIN = "/usr/bin/claude"
_TIMEOUT = 90
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
_MAX_IMAGE_DIMENSION = 1500
_MAX_IMAGE_DIMENSION = 2500
_ALLOWED_TYPES = {"image/png", "image/jpeg", "image/webp"}
# Semaphore: max 1 gleichzeitige Analyse
@@ -272,24 +272,76 @@ def _resize_image(input_path: str, output_path: str):
img.save(output_path, "PNG", optimize=True)
async def _run_claude(image_path: str, viewport_info: str = "") -> dict:
def _extract_filename_timestamp(filename: str) -> str | None:
"""Extrahiert Datum/Uhrzeit aus Dateinamen wie 20231010_155544.jpg."""
import re
# Pattern: YYYYMMDD_HHMMSS oder YYYY-MM-DD_HH-MM-SS
m = re.search(r'(\d{4})(\d{2})(\d{2})[_-](\d{2})(\d{2})(\d{2})', filename or "")
if m:
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}T{m.group(4)}:{m.group(5)}:{m.group(6)}"
m = re.search(r'(\d{4})-(\d{2})-(\d{2})[_T](\d{2})-(\d{2})-(\d{2})', filename or "")
if m:
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}T{m.group(4)}:{m.group(5)}:{m.group(6)}"
return None
async def _run_claude(image_path: str, viewport_info: str = "", filename: str = "", exif_data: dict = None) -> dict:
"""Ruft Claude Code headless auf um ein Bild zu analysieren."""
# Kontext aus Metadaten aufbauen (NICHT Viewport — das erzeugt Bias!)
context_parts = []
# Timestamp aus Dateiname oder EXIF
ts = None
if exif_data and exif_data.get("timestamp"):
ts = exif_data["timestamp"]
context_parts.append(f"EXIF-Zeitstempel: {ts}")
else:
ts = _extract_filename_timestamp(filename)
if ts:
context_parts.append(f"Dateiname-Zeitstempel: {ts} (Lokalzeit, Zeitzone unbekannt)")
# Sonnenstand berechnen falls Timestamp vorhanden (fuer verschiedene Breitengrade)
if ts:
from data_geoint import _calc_sun_position
from datetime import datetime, timezone
try:
dt_str = ts.replace("Z", "+00:00")
if "+" not in dt_str and len(dt_str) <= 19:
dt_str += "+00:00" # UTC annehmen wenn keine TZ
dt = datetime.fromisoformat(dt_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# Sonnenstand fuer verschiedene Breitengrade berechnen (Hilfe bei Eingrenzung)
sun_info = []
for test_lat, label in [(48.0, "Sueddeutschland"), (51.0, "NRW/Mitteldeutschland"), (53.5, "Norddeutschland")]:
sun = _calc_sun_position(test_lat, 8.0, dt)
sun_info.append(f"{label} (Lat {test_lat}): Elevation {sun['elevation']}°, Azimut {sun['azimuth']}°")
context_parts.append("Sonnenstand zum Aufnahmezeitpunkt: " + "; ".join(sun_info))
except Exception:
pass
# EXIF-GPS als starker Hinweis
if exif_data and exif_data.get("has_gps"):
context_parts.append(f"EXIF-GPS: {exif_data['latitude']}, {exif_data['longitude']} (verifizierte Position!)")
context = ""
if viewport_info:
context = f"Kontext: Der Nutzer schaut gerade auf {viewport_info}. "
if context_parts:
context = "Metadaten: " + " | ".join(context_parts) + ". "
prompt = (
f"Lies die Bilddatei {image_path} mit dem Read-Tool. "
f"{context}"
"PRIMAERZIEL: Bestimme den Aufnahmeort so praezise wie moeglich (Reverse Geolocation). "
"WICHTIG: Schaetze die Position AUSSCHLIESSLICH anhand des Bildinhalts. "
"Lass dich NICHT von Viewport-Positionen oder Vermutungen beeinflussen. "
"Schaetze in estimated_coordinates konkrete Lat/Lon-Werte und einen Unsicherheitsradius. "
"Nutze ALLE visuellen Hinweise: "
"1) Gewaesser: Identifiziere den konkreten Fluss/See (Rhein, Donau, Elbe, etc.) anhand von Breite, Wasserfarbe, Uferform, Kiesbaenke, Stroemungsrichtung, Auenlandschaft. "
"2) Vegetation: Baumart, Jahreszeit, Reifegrad. Schilf+Kies+Auwald am breiten Fluss = typisch Rhein/Niederrhein. "
"1) Gewaesser: Identifiziere den konkreten Fluss/See anhand von Breite, Wasserfarbe, Uferform, Kiesbaenke, Stroemung. "
"Ein breiter Fluss (>200m) mit hellen Kiesbaenken und Schilfzonen in Mitteleuropa ist wahrscheinlich Rhein, Main oder Elbe. "
"2) Vegetation: Baumart, Jahreszeit, Reifegrad. "
"3) Architektur, Schilder, Fahrzeuge, Infrastruktur im Hintergrund. "
"4) Bodenfarbe, Gestein, Gelaendeform. Rheinischer Kies ist hell, Donauschlick dunkel. "
"5) Sonne/Schatten: Richtung und Laenge fuer Breitengrad-Schaetzung. "
"Identifiziere ausserdem Objekte und militaerisch relevante Strukturen mit OSM-Tags. "
"4) Bodenfarbe und Gestein. Heller Rheinkies vs. dunkler Donauschotter. "
"5) Schatten: Richtung und Laenge. Vergleiche mit den Sonnenstand-Daten falls vorhanden. "
"Fuelle identified_features (Gewaessername, Region, Landmarken) und landscape_clues komplett. "
"Antworte ausschliesslich im vorgegebenen JSON-Format."
)
@@ -386,14 +438,19 @@ async def analyze_image(
if exif_data.get("has_gps"):
logger.info(f"EXIF GPS: {exif_data['latitude']}, {exif_data['longitude']}")
# Claude Code aufrufen
result = await _run_claude(resized_path, viewport_info)
# Claude Code aufrufen (Dateiname + EXIF als Kontext, NICHT Viewport)
result = await _run_claude(resized_path, filename=file.filename, exif_data=exif_data)
# EXIF-Daten zum Ergebnis hinzufuegen
result["exif"] = exif_data
# Ergebnis loggen
ec = result.get("estimated_coordinates", {})
idf = result.get("identified_features", {})
logger.info(
f"VLM-Analyse abgeschlossen: {len(result.get('objects', []))} Objekte erkannt"
f"VLM-Analyse abgeschlossen: {len(result.get('objects', []))} Objekte, "
f"Koordinaten: {ec.get('latitude')}/{ec.get('longitude')} (r={ec.get('confidence_radius_km')}km), "
f"Gewaesser: {idf.get('water_body')}, Region: {idf.get('specific_region')}"
)
return result