fix: VLM-Bildanalyse Bugfixes (Prompt, UI, Logging)
- Fix 1: Claude-Prompt weist generische Natur-Objekte ab (forest, mountain etc.) Objects-Array nur fuer OSINT-relevante Infrastruktur, Rest in terrain/landscape_clues - Fix 2: Freundliche UI-Meldungen bei leeren Objects, 0 Treffern, gefilterten Tags Button ausgegraut + Hinweis wenn keine Infrastruktur erkannt - Fix 3: generate_queries gibt JSON statt HTTP 400 bei leeren Fragments - Fix 4: Verbose Objekt-Match-Logging auf debug reduziert - Fix 5: Verwaiste static/js/ui/overpass.js geloescht Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -346,7 +346,7 @@ async def _run_claude(image_path: str, viewport_info: str = "", filename: str =
|
|||||||
"4) Bodenfarbe und Gestein. Heller Rheinkies vs. dunkler Donauschotter. "
|
"4) Bodenfarbe und Gestein. Heller Rheinkies vs. dunkler Donauschotter. "
|
||||||
"5) Schatten: Richtung und Laenge. Vergleiche mit den Sonnenstand-Daten falls vorhanden. "
|
"5) Schatten: Richtung und Laenge. Vergleiche mit den Sonnenstand-Daten falls vorhanden. "
|
||||||
"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."
|
"OBJEKT-REGELN: Das objects-Array darf NUR konkrete OSINT-relevante Infrastruktur enthalten: Flughaefen, Militaerbasen, Haefen, Kraftwerke, Bruecken, Radaranlagen, Raffinerien, Gefaengnisse, Botschaften, Bahnhoefe, Sendemasten, Staudaemme, Bunker, Hangars, Landebahnen etc. Generische Natur (forest, mountain, farmland, vegetation, residential_area, river, lake, field) gehoert in terrain/landscape_clues, NICHT in objects. Wenn das Bild keine OSINT-relevante Infrastruktur zeigt: objects als leeres Array [] zurueckgeben. Antworte ausschliesslich im vorgegebenen JSON-Format."
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -603,7 +603,7 @@ async def generate_queries(req: QueryGenRequest):
|
|||||||
for obj in req.objects:
|
for obj in req.objects:
|
||||||
obj_type = obj.get("type", "").lower().replace(" ", "_")
|
obj_type = obj.get("type", "").lower().replace(" ", "_")
|
||||||
osm_tags = obj.get("osm_tags", [])
|
osm_tags = obj.get("osm_tags", [])
|
||||||
logger.info(f"VLM Objekt: type={obj_type}, osm_tags={osm_tags}")
|
logger.debug(f"VLM Objekt: type={obj_type}, osm_tags={osm_tags}")
|
||||||
|
|
||||||
# Direkte Zuordnung
|
# Direkte Zuordnung
|
||||||
if obj_type in _OBJECT_TO_OVERPASS and obj_type not in used_types:
|
if obj_type in _OBJECT_TO_OVERPASS and obj_type not in used_types:
|
||||||
@@ -611,7 +611,7 @@ async def generate_queries(req: QueryGenRequest):
|
|||||||
fragments.append(f' node{tag}{bbox_str};')
|
fragments.append(f' node{tag}{bbox_str};')
|
||||||
fragments.append(f' way{tag}{bbox_str};')
|
fragments.append(f' way{tag}{bbox_str};')
|
||||||
used_types.add(obj_type)
|
used_types.add(obj_type)
|
||||||
logger.info(f" -> Mapping-Match: {obj_type} -> {tag}")
|
logger.debug(f" -> Mapping-Match: {obj_type} -> {tag}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Fallback: OSM-Tags aus VLM-Ergebnis verwenden
|
# Fallback: OSM-Tags aus VLM-Ergebnis verwenden
|
||||||
@@ -620,20 +620,20 @@ async def generate_queries(req: QueryGenRequest):
|
|||||||
if "=" in tag_str and tag_str not in used_types:
|
if "=" in tag_str and tag_str not in used_types:
|
||||||
key, val = tag_str.split("=", 1)
|
key, val = tag_str.split("=", 1)
|
||||||
if key in _GENERIC_KEYS:
|
if key in _GENERIC_KEYS:
|
||||||
logger.info(f" -> Tag gefiltert (generisch): {tag_str}")
|
logger.debug(f" -> Tag gefiltert (generisch): {tag_str}")
|
||||||
continue
|
continue
|
||||||
frag = f'["{key}"="{val}"]'
|
frag = f'["{key}"="{val}"]'
|
||||||
fragments.append(f' node{frag}{bbox_str};')
|
fragments.append(f' node{frag}{bbox_str};')
|
||||||
fragments.append(f' way{frag}{bbox_str};')
|
fragments.append(f' way{frag}{bbox_str};')
|
||||||
used_types.add(tag_str)
|
used_types.add(tag_str)
|
||||||
logger.info(f" -> OSM-Tag verwendet: {tag_str}")
|
logger.debug(f" -> OSM-Tag verwendet: {tag_str}")
|
||||||
matched = True
|
matched = True
|
||||||
break
|
break
|
||||||
if not matched and obj_type not in _OBJECT_TO_OVERPASS:
|
if not matched and obj_type not in _OBJECT_TO_OVERPASS:
|
||||||
logger.warning(f" -> KEIN MATCH fuer: {obj_type} (tags: {osm_tags})")
|
logger.debug(f" -> KEIN MATCH fuer: {obj_type} (tags: {osm_tags})")
|
||||||
|
|
||||||
if not fragments:
|
if not fragments:
|
||||||
raise HTTPException(400, "Keine passenden OSM-Tags fuer die erkannten Objekte gefunden")
|
return {"query": "", "tag_count": 0, "object_count": len(req.objects), "no_infrastructure": True}
|
||||||
|
|
||||||
query = "[out:json][timeout:30];\n(\n" + "\n".join(fragments) + "\n);\nout center body;"
|
query = "[out:json][timeout:30];\n(\n" + "\n".join(fragments) + "\n);\nout center body;"
|
||||||
|
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
/**
|
|
||||||
* Overpass UI: Query-Editor-Panel mit Template-Browser und BBox-Integration.
|
|
||||||
*/
|
|
||||||
const OverpassUI = {
|
|
||||||
_panel: null,
|
|
||||||
_editor: null,
|
|
||||||
_templates: [],
|
|
||||||
_categories: [],
|
|
||||||
_activeCategory: null,
|
|
||||||
_isLoading: false,
|
|
||||||
_useBbox: true,
|
|
||||||
|
|
||||||
init: function() {
|
|
||||||
this._createPanel();
|
|
||||||
this._loadTemplates();
|
|
||||||
},
|
|
||||||
|
|
||||||
show: function() {
|
|
||||||
if (!this._panel) this.init();
|
|
||||||
this._panel.style.display = 'block';
|
|
||||||
if (!OverpassLayer._points) OverpassLayer.start(Globe.viewer);
|
|
||||||
},
|
|
||||||
|
|
||||||
hide: function() {
|
|
||||||
if (this._panel) this._panel.style.display = 'none';
|
|
||||||
},
|
|
||||||
|
|
||||||
_createPanel: function() {
|
|
||||||
var panel = document.getElementById('overpass-panel');
|
|
||||||
if (!panel) return;
|
|
||||||
this._panel = panel;
|
|
||||||
|
|
||||||
panel.innerHTML =
|
|
||||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
|
|
||||||
'<div style="font-size:11px;font-weight:700;letter-spacing:2px;color:var(--accent)">OVERPASS QUERY</div>' +
|
|
||||||
'<button onclick="OverpassUI.hide();var cb=document.getElementById(\'layer-overpass\');if(cb){cb.checked=false;}OverpassLayer.stop();" ' +
|
|
||||||
'style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:18px;line-height:1" title="Schliessen">×</button>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div id="overpass-cat-tabs" class="overpass-cat-tabs"></div>' +
|
|
||||||
'<div id="overpass-template-list" style="max-height:160px;overflow-y:auto;margin-bottom:10px"></div>' +
|
|
||||||
'<div class="panel-divider"></div>' +
|
|
||||||
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:4px">OVERPASSQL EDITOR</div>' +
|
|
||||||
'<textarea id="overpass-editor" rows="6" spellcheck="false" placeholder="[out:json][timeout:25];\n(\n node["amenity"]({{bbox}});\n);\nout center body;"></textarea>' +
|
|
||||||
'<div style="display:flex;align-items:center;gap:8px;margin:10px 0">' +
|
|
||||||
'<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text);cursor:pointer;flex:1">' +
|
|
||||||
'<input type="checkbox" id="overpass-bbox" checked onchange="OverpassUI._useBbox=this.checked" style="accent-color:var(--accent);width:14px;height:14px">' +
|
|
||||||
'Viewport als BBox' +
|
|
||||||
'</label>' +
|
|
||||||
'</div>' +
|
|
||||||
'<button class="overpass-exec-btn" id="overpass-exec-btn" onclick="OverpassUI._executeQuery()">AUSFUEHREN</button>' +
|
|
||||||
'<div id="overpass-result" style="margin-top:10px;font-size:11px;color:var(--text-dim);display:none"></div>' +
|
|
||||||
'<button id="overpass-clear-btn" onclick="OverpassUI._clearResults()" ' +
|
|
||||||
'style="display:none;width:100%;padding:6px;margin-top:6px;background:none;border:1px solid var(--border);color:var(--text-dim);' +
|
|
||||||
'font-family:var(--font-mono);font-size:11px;border-radius:4px;cursor:pointer;letter-spacing:1px">ERGEBNISSE LOESCHEN</button>';
|
|
||||||
|
|
||||||
this._editor = document.getElementById('overpass-editor');
|
|
||||||
},
|
|
||||||
|
|
||||||
_loadTemplates: function() {
|
|
||||||
var self = this;
|
|
||||||
fetch('/api/overpass/templates')
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
self._categories = data.categories || [];
|
|
||||||
if (self._categories.length > 0) {
|
|
||||||
self._activeCategory = self._categories[0].id;
|
|
||||||
self._renderCategoryTabs();
|
|
||||||
self._renderTemplateList();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(e) { console.warn('Overpass Templates:', e); });
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderCategoryTabs: function() {
|
|
||||||
var el = document.getElementById('overpass-cat-tabs');
|
|
||||||
if (!el) return;
|
|
||||||
var self = this;
|
|
||||||
var html = '';
|
|
||||||
this._categories.forEach(function(cat) {
|
|
||||||
var active = cat.id === self._activeCategory ? ' active' : '';
|
|
||||||
html += '<button class="overpass-cat-tab' + active + '" data-cat="' + cat.id + '" ' +
|
|
||||||
'onclick="OverpassUI._selectCategory(\'' + cat.id + '\')">' + cat.label + '</button>';
|
|
||||||
});
|
|
||||||
el.innerHTML = html;
|
|
||||||
},
|
|
||||||
|
|
||||||
_selectCategory: function(catId) {
|
|
||||||
this._activeCategory = catId;
|
|
||||||
this._renderCategoryTabs();
|
|
||||||
this._renderTemplateList();
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderTemplateList: function() {
|
|
||||||
var el = document.getElementById('overpass-template-list');
|
|
||||||
if (!el) return;
|
|
||||||
var self = this;
|
|
||||||
var cat = this._categories.find(function(c) { return c.id === self._activeCategory; });
|
|
||||||
if (!cat) { el.innerHTML = ''; return; }
|
|
||||||
|
|
||||||
var html = '';
|
|
||||||
cat.templates.forEach(function(t) {
|
|
||||||
html += '<button class="overpass-template-btn" onclick="OverpassUI._applyTemplate(\'' + t.id + '\')" title="' + t.description + '">' +
|
|
||||||
'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + t.color + ';margin-right:8px"></span>' +
|
|
||||||
t.name + '</button>';
|
|
||||||
});
|
|
||||||
el.innerHTML = html;
|
|
||||||
},
|
|
||||||
|
|
||||||
_applyTemplate: function(templateId) {
|
|
||||||
var tpl = null;
|
|
||||||
for (var i = 0; i < this._categories.length; i++) {
|
|
||||||
var found = this._categories[i].templates.find(function(t) { return t.id === templateId; });
|
|
||||||
if (found) { tpl = found; break; }
|
|
||||||
}
|
|
||||||
if (tpl && this._editor) {
|
|
||||||
this._editor.value = tpl.query;
|
|
||||||
OverpassLayer.setColor(tpl.color);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_getBboxFromViewport: function() {
|
|
||||||
if (!Globe.viewer) return null;
|
|
||||||
var rect = Globe.viewer.camera.computeViewRectangle();
|
|
||||||
if (!rect) return null;
|
|
||||||
return [
|
|
||||||
Cesium.Math.toDegrees(rect.south),
|
|
||||||
Cesium.Math.toDegrees(rect.west),
|
|
||||||
Cesium.Math.toDegrees(rect.north),
|
|
||||||
Cesium.Math.toDegrees(rect.east),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
_executeQuery: function() {
|
|
||||||
if (this._isLoading || !this._editor) return;
|
|
||||||
var query = this._editor.value.trim();
|
|
||||||
if (!query) return;
|
|
||||||
|
|
||||||
var bbox = null;
|
|
||||||
if (this._useBbox) {
|
|
||||||
bbox = this._getBboxFromViewport();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isLoading = true;
|
|
||||||
var btn = document.getElementById('overpass-exec-btn');
|
|
||||||
if (btn) { btn.disabled = true; btn.textContent = 'LADE...'; }
|
|
||||||
var resultEl = document.getElementById('overpass-result');
|
|
||||||
if (resultEl) { resultEl.style.display = 'block'; resultEl.textContent = 'Anfrage wird gesendet...'; resultEl.style.color = 'var(--text-dim)'; }
|
|
||||||
var startTime = Date.now();
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
fetch('/api/overpass/query', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ query: query, bbox: bbox }),
|
|
||||||
})
|
|
||||||
.then(function(r) {
|
|
||||||
if (r.status === 429) {
|
|
||||||
return r.json().then(function(d) { throw new Error(d.detail || 'Rate-Limit erreicht'); });
|
|
||||||
}
|
|
||||||
if (!r.ok) {
|
|
||||||
return r.json().then(function(d) { throw new Error(d.detail || 'Fehler ' + r.status); });
|
|
||||||
}
|
|
||||||
return r.json();
|
|
||||||
})
|
|
||||||
.then(function(data) {
|
|
||||||
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
||||||
OverpassLayer.render(data);
|
|
||||||
if (resultEl) {
|
|
||||||
var text = data.total + ' Objekte (' + elapsed + 's)';
|
|
||||||
if (data.cached) text += ' [Cache]';
|
|
||||||
if (data.truncated) text += ' [max. ' + data.total + ' angezeigt]';
|
|
||||||
resultEl.textContent = text;
|
|
||||||
resultEl.style.color = 'var(--accent)';
|
|
||||||
}
|
|
||||||
var clearBtn = document.getElementById('overpass-clear-btn');
|
|
||||||
if (clearBtn) clearBtn.style.display = 'block';
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
if (resultEl) {
|
|
||||||
resultEl.textContent = 'Fehler: ' + e.message;
|
|
||||||
resultEl.style.color = '#ff5252';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(function() {
|
|
||||||
self._isLoading = false;
|
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'AUSFUEHREN'; }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_clearResults: function() {
|
|
||||||
OverpassLayer.clear();
|
|
||||||
var resultEl = document.getElementById('overpass-result');
|
|
||||||
if (resultEl) { resultEl.style.display = 'none'; resultEl.style.color = 'var(--text-dim)'; }
|
|
||||||
var clearBtn = document.getElementById('overpass-clear-btn');
|
|
||||||
if (clearBtn) clearBtn.style.display = 'none';
|
|
||||||
},
|
|
||||||
|
|
||||||
executeQueryDirect: function(query, bbox, color) {
|
|
||||||
if (this._editor) this._editor.value = query;
|
|
||||||
if (color) OverpassLayer.setColor(color);
|
|
||||||
this._useBbox = !!bbox;
|
|
||||||
var bboxCb = document.getElementById('overpass-bbox');
|
|
||||||
if (bboxCb) bboxCb.checked = this._useBbox;
|
|
||||||
this.show();
|
|
||||||
this._executeQuery();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -62,6 +62,7 @@ const VlmUI = {
|
|||||||
'</label>' +
|
'</label>' +
|
||||||
'<button class="overpass-exec-btn" id="vlm-search-btn" onclick="VlmUI._confirmAndSearch()" style="margin-top:8px">AUF GLOBE SUCHEN</button>' +
|
'<button class="overpass-exec-btn" id="vlm-search-btn" onclick="VlmUI._confirmAndSearch()" style="margin-top:8px">AUF GLOBE SUCHEN</button>' +
|
||||||
'<div id="vlm-search-result" style="margin-top:8px;font-size:11px;color:var(--text-dim);display:none"></div>' +
|
'<div id="vlm-search-result" style="margin-top:8px;font-size:11px;color:var(--text-dim);display:none"></div>' +
|
||||||
|
'<div id="vlm-no-infra" style="display:none;font-size:11px;margin-top:6px;padding:8px;border-radius:4px;background:rgba(255,152,0,0.1);border:1px solid rgba(255,152,0,0.3);color:#ff9800">Keine OSINT-relevante Infrastruktur erkannt. Standortschaetzung und Landschaftsmerkmale sind weiterhin verfuegbar.</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
// Reset
|
// Reset
|
||||||
'<button id="vlm-reset-btn" onclick="VlmUI._reset()" ' +
|
'<button id="vlm-reset-btn" onclick="VlmUI._reset()" ' +
|
||||||
@@ -180,6 +181,17 @@ const VlmUI = {
|
|||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
self._currentAnalysis = data;
|
self._currentAnalysis = data;
|
||||||
|
|
||||||
|
// Fix 2c: Bei leeren objects Button disablen
|
||||||
|
var noInfraEl = document.getElementById('vlm-no-infra');
|
||||||
|
var searchBtn = document.getElementById('vlm-search-btn');
|
||||||
|
if (!data.objects || data.objects.length === 0) {
|
||||||
|
if (searchBtn) { searchBtn.disabled = true; searchBtn.style.opacity = '0.4'; }
|
||||||
|
if (noInfraEl) noInfraEl.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
if (searchBtn) { searchBtn.disabled = false; searchBtn.style.opacity = '1'; }
|
||||||
|
if (noInfraEl) noInfraEl.style.display = 'none';
|
||||||
|
}
|
||||||
self._showResults(data);
|
self._showResults(data);
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
@@ -441,6 +453,17 @@ const VlmUI = {
|
|||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
|
// Fix 2a: Backend meldet keine passenden Tags
|
||||||
|
if (data.no_infrastructure) {
|
||||||
|
if (searchResult) {
|
||||||
|
var prefix = hasExifGps ? 'EXIF GPS-Marker gesetzt | ' : '';
|
||||||
|
searchResult.style.display = 'block';
|
||||||
|
searchResult.textContent = prefix + 'Keine passenden OSM-Tags fuer die erkannten Objekte. Die Objekte sind zu generisch fuer eine Overpass-Suche.';
|
||||||
|
searchResult.style.color = '#ff9800';
|
||||||
|
}
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = 'AUF GLOBE SUCHEN'; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
var src = hasExifGps ? 'EXIF BBox' : (data.region ? 'Region: ' + data.region : 'weltweit');
|
var src = hasExifGps ? 'EXIF BBox' : (data.region ? 'Region: ' + data.region : 'weltweit');
|
||||||
if (searchResult) searchResult.textContent = statusParts.join(' | ') + (statusParts.length ? ' | ' : '') + 'Query generiert (' + data.tag_count + ' Tags, ' + src + '). Suche OSM-Daten...';
|
if (searchResult) searchResult.textContent = statusParts.join(' | ') + (statusParts.length ? ' | ' : '') + 'Query generiert (' + data.tag_count + ' Tags, ' + src + '). Suche OSM-Daten...';
|
||||||
|
|
||||||
@@ -487,6 +510,11 @@ const VlmUI = {
|
|||||||
}
|
}
|
||||||
var countEl = document.getElementById('count-vlm');
|
var countEl = document.getElementById('count-vlm');
|
||||||
if (countEl) countEl.textContent = data.total;
|
if (countEl) countEl.textContent = data.total;
|
||||||
|
// Fix 2b: 0 Treffer erklaerend melden
|
||||||
|
if (data.total === 0 && searchResult) {
|
||||||
|
searchResult.textContent = (hasExifGps ? 'EXIF GPS-Marker gesetzt | ' : '') + 'Keine passenden Orte in OSM gefunden. Region moeglicherweise zu ungenau oder Objekte nicht in OpenStreetMap erfasst.';
|
||||||
|
searchResult.style.color = '#ff9800';
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
if (searchResult) {
|
if (searchResult) {
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren