landvex: Fixar och tester klara för alla komponenter

- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
This commit is contained in:
Bernt
2026-07-05 06:41:32 +00:00
parent f4f853d94b
commit aee0f09db8
19583 changed files with 1450867 additions and 1153 deletions
+391
View File
@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Landvex — Vision Training</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#060a12;--s1:#0b1220;--s2:#0f1929;--s3:#152236;--border: rgba(0,0,0,0.08);--text:#dce8f5;--t2:rgba(220,232,245,0.55);--accent:#1d6fa4;--accent3:#4aafef}
html,body{min-height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text',sans-serif;background:var(--bg);color:var(--text)}
#app{max-width:960px;margin:0 auto;padding:16px}
h1{font-size:22px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px}
.sub{font-size:12px;color:var(--t2);margin-bottom:24px}
#drop-zone{border:2px dashed rgba(0,0,0,0.15);border-radius:12px;padding:40px;text-align:center;cursor:pointer;transition:border-color 0.2s;margin-bottom:20px}
#drop-zone:hover,#drop-zone.drag{border-color:var(--accent3)}
#drop-zone p{color:var(--t2);font-size:13px}
#preview-wrap{display:none;position:relative;margin-bottom:16px}
#preview{max-width:100%;border-radius:10px;display:block}
#canvas{position:absolute;top:0;left:0;cursor:crosshair}
#labels-panel{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:16px}
.label-btn{padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--s2);cursor:pointer;font-size:12px;font-weight:600;color:var(--t2);transition:all 0.12s;display:flex;align-items:center;gap:7px}
.label-btn:hover{background:var(--s3);color:var(--text)}
.label-btn.active{border-color:currentColor}
.label-btn[data-cat="electrical_hazard"].active{color:#f87171;background:rgba(248,113,113,0.1)}
.label-btn[data-cat="cable_disorder"].active{color:#fb923c;background:rgba(251,146,60,0.1)}
.label-btn[data-cat="graffiti"].active{color:#a78bfa;background:rgba(167,139,250,0.1)}
.label-btn[data-cat="pothole"].active{color:#fbbf24;background:rgba(251,191,36,0.1)}
.label-btn[data-cat="litter"].active{color:#94a3b8;background:rgba(148,163,184,0.1)}
.label-btn[data-cat="vacancy"].active{color:#f87171;background:rgba(248,113,113,0.1)}
.label-btn[data-cat="construction"].active{color:#4ade80;background:rgba(74,222,128,0.1)}
.label-btn[data-cat="commercial_active"].active{color:#38bdf8;background:rgba(56,189,248,0.1)}
.label-btn[data-cat="good_condition"].active{color:#4ade80;background:rgba(74,222,128,0.1)}
.label-btn[data-cat="infrastructure_ok"].active{color:#4ade80;background:rgba(74,222,128,0.1)}
#scores{display:grid;grid-template-columns:1fr;gap:8px;margin-bottom:16px}
@media(min-width:560px){#scores{grid-template-columns:1fr 1fr 1fr}}
.score-group{background:var(--s2);border-radius:8px;padding:10px 12px}
.score-group label{font-size:9px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--t2);display:block;margin-bottom:6px}
.score-group input[type=range]{width:100%;accent-color:var(--accent3)}
.score-val{font-size:14px;font-weight:800;font-family:monospace;color:var(--accent3)}
#boxes-list{margin-bottom:16px;font-size:11px;color:var(--t2)}
.box-item{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--s2);border-radius:5px;margin-bottom:4px}
.box-del{color:#f87171;cursor:pointer;font-size:14px;background:none;border:none;padding:0 4px}
.btn{padding:9px 20px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:700}
.btn-primary{background:var(--accent);color: #1d1d1f}
.btn-secondary{background:var(--s2);color:var(--text);border:1px solid var(--border)}
#actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
#output{background:var(--s2);border-radius:8px;padding:14px;font-size:11px;font-family:monospace;white-space:pre-wrap;color:#4ade80;display:none;max-height:300px;overflow-y:auto}
#file-input{display:none}
.contradiction-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:16px;background:var(--s2);border-radius:8px;padding:10px 12px}
.contradiction-row label{font-size:11px;font-weight:600;color:var(--t2);flex:1}
.contradiction-row select{background:var(--s3);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11px}
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": [
"Organization",
"Corporation"
],
"@id": "https://landvex.com/#organization",
"name": "Landvex",
"alternateName": [
"LandveX",
"Landvex AB",
"LandveX AB"
],
"url": "https://landvex.com",
"logo": {
"@type": "ImageObject",
"url": "https://landvex.com/apple-touch-icon.png",
"width": 180,
"height": 180
},
"foundingDate": "2024",
"foundingLocation": {
"@type": "Place",
"addressLocality": "Tyresö",
"addressRegion": "Stockholm",
"addressCountry": "SE"
},
"legalName": "Landvex Inc",
"address": {
"@type": "PostalAddress",
"addressLocality": "Houston",
"addressRegion": "TX",
"addressCountry": "US"
},
"location": [
{
"@type": "Place",
"name": "Houston, Texas, USA (US HQ)",
"address": {
"@type": "PostalAddress",
"addressLocality": "Houston",
"addressRegion": "TX",
"addressCountry": "US"
}
},
{
"@type": "Place",
"name": "Tyresö, Sweden (EU HQ)",
"address": {
"@type": "PostalAddress",
"addressLocality": "Tyresö",
"addressRegion": "Stockholm",
"addressCountry": "SE"
}
}
],
"taxID": "pending-EIN",
"description": "Landvex is a decision intelligence company that identifies contradictions between official narratives and observed physical reality. Using the quiXzoom field observation network and the AMOS AI analysis engine, Landvex delivers infrastructure risk indexes, urban intelligence scores, and contradiction reports to infrastructure owners, municipalities, enterprises, and investors globally.",
"disambiguatingDescription": "Landvex (LandveX) is a decision intelligence company with US headquarters in Houston, Texas (Landvex Inc.) and European headquarters in Tyresö, Sweden (Landvex AB, org.nr 559141-7042). Landvex delivers control intelligence — infrastructure risk scores, urban intelligence, and contradiction analysis — to municipalities, enterprises, infrastructure operators and investors. Landvex is not a UK meat wholesaler, food distributor, logistics company, package tracker, or land investment platform.",
"knowsAbout": [
"control intelligence",
"decision intelligence",
"infrastructure risk assessment",
"urban intelligence",
"field data collection",
"contradiction detection",
"physical world analytics",
"geospatial analytics"
],
"brand": {
"@type": "Brand",
"name": "Landvex",
"slogan": "Control intelligence for the physical world."
},
"sameAs": [
"https://landvex.com",
"https://www.linkedin.com/company/landvex",
"https://x.com/landvex",
"https://twitter.com/landvex",
"https://www.crunchbase.com/organization/landvex",
"https://github.com/landvex",
"https://www.instagram.com/landvex"
],
"contactPoint": {
"@type": "ContactPoint",
"email": "contact@landvex.com",
"contactType": "sales"
},
"slogan": "Where reported reality conflicts with observed reality."
}
</script>
</head>
<body>
<div id="app">
<h1>Landvex Vision Training</h1>
<p class="sub">Label images for quiXzoom infrastructure intelligence training. Draw bounding boxes, select categories, export JSON.</p>
<div id="drop-zone" onclick="document.getElementById('file-input').click()">
<p>Drop image here or click to upload</p>
<p style="font-size:11px;margin-top:6px;color:rgba(220,232,245,0.3)">JPEG / PNG · max 20MB</p>
</div>
<input type="file" id="file-input" accept="image/*">
<div id="preview-wrap">
<img loading="lazy" loading="lazy" loading="lazy" loading="lazy" id="preview" alt="Label preview"/>
<canvas id="canvas"></canvas>
</div>
<div id="labels-panel">
<button class="label-btn" data-cat="electrical_hazard" onclick="toggleLabel(this)"> Electrical hazard</button>
<button class="label-btn" data-cat="cable_disorder" onclick="toggleLabel(this)"> Cable disorder</button>
<button class="label-btn" data-cat="graffiti" onclick="toggleLabel(this)"> Graffiti</button>
<button class="label-btn" data-cat="pothole" onclick="toggleLabel(this)"> Pothole</button>
<button class="label-btn" data-cat="litter" onclick="toggleLabel(this)"> Litter/Waste</button>
<button class="label-btn" data-cat="vacancy" onclick="toggleLabel(this)"> Vacancy</button>
<button class="label-btn" data-cat="construction" onclick="toggleLabel(this)"> Construction</button>
<button class="label-btn" data-cat="commercial_active" onclick="toggleLabel(this)"> Commercial active</button>
<button class="label-btn" data-cat="good_condition" onclick="toggleLabel(this)"> Good condition</button>
<button class="label-btn" data-cat="infrastructure_ok" onclick="toggleLabel(this)"> Infra OK</button>
</div>
<div id="scores">
<div class="score-group">
<label>Infrastructure Risk <span class="score-val" id="risk-val">5</span>/10</label>
<input type="range" min="0" max="10" value="5" step="0.5" id="risk-slider" oninput="document.getElementById('risk-val').textContent=this.value">
</div>
<div class="score-group">
<label>Condition Score <span class="score-val" id="cond-val">5</span>/10</label>
<input type="range" min="0" max="10" value="5" step="0.5" id="cond-slider" oninput="document.getElementById('cond-val').textContent=this.value">
</div>
<div class="score-group">
<label>Contradiction Potential <span class="score-val" id="contra-val">0.0</span></label>
<input type="range" min="0" max="1" value="0" step="0.1" id="contra-slider" oninput="document.getElementById('contra-val').textContent=parseFloat(this.value).toFixed(1)">
</div>
</div>
<div class="contradiction-row">
<label>Official data says:</label>
<select id="official-status">
<option value="approved">Infrastructure approved</option>
<option value="low_risk">Low risk</option>
<option value="moderate">Moderate risk</option>
<option value="high_risk">High risk</option>
<option value="unknown">Unknown</option>
</select>
<label>Vacancy status:</label>
<select id="vacancy-status">
<option value="occupied">Occupied</option>
<option value="vacant">Vacant</option>
<option value="uncertain">Uncertain</option>
</select>
</div>
<div id="boxes-list"></div>
<div id="actions">
<button class="btn btn-primary" onclick="exportLabel()">Export JSON</button>
<button class="btn btn-secondary" onclick="clearAll()">Clear</button>
<input type="text" id="lat-input" placeholder="lat" style="flex:1;min-width:70px;padding:8px;background:var(--s2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px">
<input type="text" id="lng-input" placeholder="lng" style="flex:1;min-width:70px;padding:8px;background:var(--s2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px">
<input type="text" id="city-input" placeholder="city" style="flex:1;min-width:70px;padding:8px;background:var(--s2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px">
<input type="text" id="district-input" placeholder="district" style="flex:1;min-width:80px;padding:8px;background:var(--s2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px">
</div>
<pre id="output"></pre>
</div>
<script>
let img=null, boxes=[], drawing=false, startX=0, startY=0;
let activeLabels=new Set();
let currentBox=null;
const canvas=document.getElementById('canvas');
const ctx=canvas.getContext('2d');
const preview=document.getElementById('preview');
const wrap=document.getElementById('preview-wrap');
// File input
document.getElementById('file-input').addEventListener('change',e=>{
const f=e.target.files[0]; if(!f)return;
const r=new FileReader();
r.onload=ev=>loadImage(ev.target.result);
r.readAsDataURL(f);
});
// Drag & drop
const dz=document.getElementById('drop-zone');
dz.addEventListener('dragover',e=>{e.preventDefault();dz.classList.add('drag')});
dz.addEventListener('dragleave',()=>dz.classList.remove('drag'));
dz.addEventListener('drop',e=>{
e.preventDefault();dz.classList.remove('drag');
const f=e.dataTransfer.files[0]; if(!f)return;
const r=new FileReader();
r.onload=ev=>loadImage(ev.target.result);
r.readAsDataURL(f);
});
function loadImage(src){
img=new Image();
img.onload=()=>{
preview.src=src;
wrap.style.display='block';
dz.style.display='none';
setTimeout(()=>{
canvas.width=preview.offsetWidth;
canvas.height=preview.offsetHeight;
redraw();
},100);
};
img.src=src;
}
// Bounding box drawing
canvas.addEventListener('mousedown',e=>{
const r=canvas.getBoundingClientRect();
startX=e.clientX-r.left; startY=e.clientY-r.top;
drawing=true; currentBox={x:startX,y:startY,w:0,h:0,labels:[...activeLabels]};
});
canvas.addEventListener('mousemove',e=>{
if(!drawing)return;
const r=canvas.getBoundingClientRect();
currentBox.w=(e.clientX-r.left)-startX;
currentBox.h=(e.clientY-r.top)-startY;
redraw();
});
canvas.addEventListener('mouseup',()=>{
if(!drawing)return; drawing=false;
if(Math.abs(currentBox.w)>10&&Math.abs(currentBox.h)>10){
boxes.push({...currentBox});
updateBoxList();
}
currentBox=null; redraw();
});
function redraw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
boxes.forEach((b,i)=>{
ctx.strokeStyle='#4aafef'; ctx.lineWidth=2;
ctx.strokeRect(b.x,b.y,b.w,b.h);
ctx.fillStyle='rgba(74,175,239,0.15)';
ctx.fillRect(b.x,b.y,b.w,b.h);
ctx.fillStyle='#4aafef'; ctx.font='10px monospace';
ctx.fillText(b.labels.join(',')||'unlabeled',b.x+3,b.y+12);
});
if(currentBox){
ctx.strokeStyle='#fbbf24'; ctx.lineWidth=1.5; ctx.setLineDash([4,3]);
ctx.strokeRect(currentBox.x,currentBox.y,currentBox.w,currentBox.h);
ctx.setLineDash([]);
}
}
function updateBoxList(){
const el=document.getElementById('boxes-list');
el.innerHTML=boxes.map((b,i)=>`
<div class="box-item">
<span>Box ${i+1}: ${b.labels.join(', ')||'unlabeled'} (${Math.abs(Math.round(b.w))}×${Math.abs(Math.round(b.h))}px)</span>
<button class="box-del" onclick="deleteBox(${i})">×</button>
</div>`).join('');
}
function deleteBox(i){ boxes.splice(i,1); updateBoxList(); redraw(); }
function toggleLabel(btn){
const cat=btn.dataset.cat;
if(activeLabels.has(cat)){activeLabels.delete(cat);btn.classList.remove('active');}
else{activeLabels.add(cat);btn.classList.add('active');}
}
function exportLabel(){
const scaleX=img?img.naturalWidth/canvas.width:1;
const scaleY=img?img.naturalHeight/canvas.height:1;
const tags=[...document.querySelectorAll('.label-btn.active')].map(b=>b.dataset.cat);
const out={
observation_id:`qx-label-${Date.now()}`,
lat:parseFloat(document.getElementById('lat-input').value)||null,
lng:parseFloat(document.getElementById('lng-input').value)||null,
city:document.getElementById('city-input').value||'unknown',
district:document.getElementById('district-input').value||'unknown',
timestamp:new Date().toISOString(),
manual_labels:{
tags,
infrastructure_risk:parseFloat(document.getElementById('risk-slider').value),
condition_score:parseFloat(document.getElementById('cond-slider').value),
contradiction_potential:parseFloat(document.getElementById('contra-slider').value),
vacancy_status:document.getElementById('vacancy-status').value,
official_status:document.getElementById('official-status').value,
},
bounding_boxes:boxes.map((b,i)=>({
id:i+1,
labels:b.labels,
bbox_normalized:[
Math.round(Math.min(b.x,b.x+b.w)*scaleX),
Math.round(Math.min(b.y,b.y+b.h)*scaleY),
Math.round(Math.abs(b.w)*scaleX),
Math.round(Math.abs(b.h)*scaleY)
]
})),
source_first:{type:'manual_label',labeled_by:'human',pipeline:'landvex-vision-v1'}
};
const el=document.getElementById('output');
el.style.display='block';
el.textContent=JSON.stringify(out,null,2);
// Auto-download
const blob=new Blob([JSON.stringify(out,null,2)],{type:'application/json'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download=`label-${out.observation_id}.json`;
a.click();
}
function clearAll(){
boxes=[]; activeLabels.clear();
document.querySelectorAll('.label-btn.active').forEach(b=>b.classList.remove('active'));
document.getElementById('risk-slider').value=5;
document.getElementById('cond-slider').value=5;
document.getElementById('contra-slider').value=0;
document.getElementById('risk-val').textContent='5';
document.getElementById('cond-val').textContent='5';
document.getElementById('contra-val').textContent='0.0';
document.getElementById('output').style.display='none';
updateBoxList(); redraw();
}
</script>
</body>
</html>