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:
@@ -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
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren