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"
|
_CLAUDE_BIN = "/usr/bin/claude"
|
||||||
_TIMEOUT = 90
|
_TIMEOUT = 90
|
||||||
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||||
_MAX_IMAGE_DIMENSION = 1500
|
_MAX_IMAGE_DIMENSION = 2500
|
||||||
_ALLOWED_TYPES = {"image/png", "image/jpeg", "image/webp"}
|
_ALLOWED_TYPES = {"image/png", "image/jpeg", "image/webp"}
|
||||||
|
|
||||||
# Semaphore: max 1 gleichzeitige Analyse
|
# 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)
|
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."""
|
"""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 = ""
|
context = ""
|
||||||
if viewport_info:
|
if context_parts:
|
||||||
context = f"Kontext: Der Nutzer schaut gerade auf {viewport_info}. "
|
context = "Metadaten: " + " | ".join(context_parts) + ". "
|
||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
f"Lies die Bilddatei {image_path} mit dem Read-Tool. "
|
f"Lies die Bilddatei {image_path} mit dem Read-Tool. "
|
||||||
f"{context}"
|
f"{context}"
|
||||||
"PRIMAERZIEL: Bestimme den Aufnahmeort so praezise wie moeglich (Reverse Geolocation). "
|
"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. "
|
"Schaetze in estimated_coordinates konkrete Lat/Lon-Werte und einen Unsicherheitsradius. "
|
||||||
"Nutze ALLE visuellen Hinweise: "
|
"Nutze ALLE visuellen Hinweise: "
|
||||||
"1) Gewaesser: Identifiziere den konkreten Fluss/See (Rhein, Donau, Elbe, etc.) anhand von Breite, Wasserfarbe, Uferform, Kiesbaenke, Stroemungsrichtung, Auenlandschaft. "
|
"1) Gewaesser: Identifiziere den konkreten Fluss/See anhand von Breite, Wasserfarbe, Uferform, Kiesbaenke, Stroemung. "
|
||||||
"2) Vegetation: Baumart, Jahreszeit, Reifegrad. Schilf+Kies+Auwald am breiten Fluss = typisch Rhein/Niederrhein. "
|
"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. "
|
"3) Architektur, Schilder, Fahrzeuge, Infrastruktur im Hintergrund. "
|
||||||
"4) Bodenfarbe, Gestein, Gelaendeform. Rheinischer Kies ist hell, Donauschlick dunkel. "
|
"4) Bodenfarbe und Gestein. Heller Rheinkies vs. dunkler Donauschotter. "
|
||||||
"5) Sonne/Schatten: Richtung und Laenge fuer Breitengrad-Schaetzung. "
|
"5) Schatten: Richtung und Laenge. Vergleiche mit den Sonnenstand-Daten falls vorhanden. "
|
||||||
"Identifiziere ausserdem Objekte und militaerisch relevante Strukturen mit OSM-Tags. "
|
|
||||||
"Fuelle identified_features (Gewaessername, Region, Landmarken) und landscape_clues komplett. "
|
"Fuelle identified_features (Gewaessername, Region, Landmarken) und landscape_clues komplett. "
|
||||||
"Antworte ausschliesslich im vorgegebenen JSON-Format."
|
"Antworte ausschliesslich im vorgegebenen JSON-Format."
|
||||||
)
|
)
|
||||||
@@ -386,14 +438,19 @@ async def analyze_image(
|
|||||||
if exif_data.get("has_gps"):
|
if exif_data.get("has_gps"):
|
||||||
logger.info(f"EXIF GPS: {exif_data['latitude']}, {exif_data['longitude']}")
|
logger.info(f"EXIF GPS: {exif_data['latitude']}, {exif_data['longitude']}")
|
||||||
|
|
||||||
# Claude Code aufrufen
|
# Claude Code aufrufen (Dateiname + EXIF als Kontext, NICHT Viewport)
|
||||||
result = await _run_claude(resized_path, viewport_info)
|
result = await _run_claude(resized_path, filename=file.filename, exif_data=exif_data)
|
||||||
|
|
||||||
# EXIF-Daten zum Ergebnis hinzufuegen
|
# EXIF-Daten zum Ergebnis hinzufuegen
|
||||||
result["exif"] = exif_data
|
result["exif"] = exif_data
|
||||||
|
|
||||||
|
# Ergebnis loggen
|
||||||
|
ec = result.get("estimated_coordinates", {})
|
||||||
|
idf = result.get("identified_features", {})
|
||||||
logger.info(
|
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
|
return result
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren