Spaces:
Paused
Paused
Deploy Sensorflow Studio (part 6)
Browse files- static/app.js +495 -49
- static/index.html +69 -5
- static/style.css +208 -0
- tests/test_pipeline/test_auto_label_integrity.py +124 -0
static/app.js
CHANGED
|
@@ -2,6 +2,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2 |
// API base URL
|
| 3 |
const API_BASE = '';
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
// Stage Navigation
|
| 6 |
const navButtons = document.querySelectorAll('.nav-btn');
|
| 7 |
const panels = document.querySelectorAll('.stage-panel');
|
|
@@ -30,11 +78,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 30 |
let pipelineState = {};
|
| 31 |
let currentSequenceId = 'seq_001';
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
async function refreshPipelineStatus() {
|
| 34 |
try {
|
| 35 |
-
|
|
|
|
| 36 |
const data = await res.json();
|
| 37 |
pipelineState = data;
|
|
|
|
| 38 |
updateGateBadges();
|
| 39 |
} catch (e) { /* backend offline */ }
|
| 40 |
}
|
|
@@ -59,9 +123,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 59 |
|
| 60 |
if (meta.completionOnly) {
|
| 61 |
if (verdict === true) {
|
| 62 |
-
badge.textContent = '
|
| 63 |
-
badge.style.background = '#00ffaa33';
|
| 64 |
-
badge.style.color = '#00ffaa';
|
| 65 |
} else {
|
| 66 |
badge.textContent = '';
|
| 67 |
}
|
|
@@ -77,10 +141,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 77 |
badge.style.background = '#ff444433';
|
| 78 |
badge.style.color = '#ff4444';
|
| 79 |
} else if (completed === true) {
|
| 80 |
-
|
| 81 |
-
badge.
|
| 82 |
-
badge.style.
|
| 83 |
-
badge.style.color = '#00ffaa';
|
| 84 |
} else {
|
| 85 |
badge.textContent = '';
|
| 86 |
}
|
|
@@ -126,7 +189,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 126 |
const currentStage = document.querySelector('.nav-btn.active')?.dataset.stage;
|
| 127 |
const meta = STAGE_METADATA[currentStage];
|
| 128 |
if (meta && meta.gateKey && pipelineState[meta.gateKey] === false) {
|
| 129 |
-
|
| 130 |
return;
|
| 131 |
}
|
| 132 |
switchStage(nextBtn.dataset.next);
|
|
@@ -175,11 +238,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 175 |
source_path: datasetSourcePath.value
|
| 176 |
})
|
| 177 |
});
|
|
|
|
| 178 |
const data = await res.json();
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
} catch (e) {
|
| 181 |
console.error(e);
|
| 182 |
-
|
| 183 |
}
|
| 184 |
});
|
| 185 |
|
|
@@ -188,9 +257,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 188 |
try {
|
| 189 |
const res = await fetch(`${API_BASE}/api/precheck`);
|
| 190 |
const data = await res.json();
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
} catch (e) {
|
| 193 |
-
|
| 194 |
}
|
| 195 |
});
|
| 196 |
|
|
@@ -238,8 +309,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 238 |
})
|
| 239 |
});
|
| 240 |
const data = await res.json();
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
pollTraining();
|
| 244 |
trainInterval = setInterval(pollTraining, 1500);
|
| 245 |
} catch (e) {
|
|
@@ -289,7 +371,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 289 |
}
|
| 290 |
|
| 291 |
if (!status.running) {
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
resetTrainingUI();
|
| 294 |
}
|
| 295 |
} catch (e) {
|
|
@@ -398,8 +496,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 398 |
})
|
| 399 |
});
|
| 400 |
const data = await res.json();
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
selectInferImage.innerHTML = '';
|
| 404 |
if (data.images && data.images.length > 0) {
|
| 405 |
data.images.forEach(img => {
|
|
@@ -408,15 +515,37 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 408 |
opt.textContent = img;
|
| 409 |
selectInferImage.appendChild(opt);
|
| 410 |
});
|
| 411 |
-
// Trigger image load
|
| 412 |
loadImage(data.images[0]);
|
| 413 |
} else {
|
| 414 |
selectInferImage.innerHTML = '<option value="">No images found</option>';
|
| 415 |
}
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
} catch (e) {
|
| 418 |
console.error(e);
|
| 419 |
-
|
| 420 |
} finally {
|
| 421 |
btnRunInfer.textContent = '⚡ Run Auto-Labeler';
|
| 422 |
btnRunInfer.disabled = false;
|
|
@@ -447,14 +576,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 447 |
try {
|
| 448 |
const res = await fetch(`${API_BASE}/api/grade`);
|
| 449 |
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
-
graderScore.textContent = data.quality_score.toFixed(1);
|
| 452 |
-
statTotalImgs.textContent = data.total_images;
|
| 453 |
-
statTotalPreds.textContent = data.total_predictions;
|
| 454 |
|
| 455 |
-
// Render issues list
|
| 456 |
graderIssuesList.innerHTML = '';
|
| 457 |
-
if (data.
|
|
|
|
|
|
|
| 458 |
data.issues.forEach(issue => {
|
| 459 |
const el = document.createElement('div');
|
| 460 |
el.className = `issue-card ${issue.severity.toLowerCase()}`;
|
|
@@ -466,11 +605,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 466 |
graderIssuesList.appendChild(el);
|
| 467 |
});
|
| 468 |
} else {
|
| 469 |
-
graderIssuesList.innerHTML = '<div class="empty-state">
|
| 470 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
} catch (e) {
|
| 472 |
console.error(e);
|
| 473 |
-
|
| 474 |
} finally {
|
| 475 |
btnRunGrader.textContent = 'Run Quality Diagnostics';
|
| 476 |
btnRunGrader.disabled = false;
|
|
@@ -617,11 +768,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 617 |
currentMitlData = data.data;
|
| 618 |
originalCocText = currentMitlData.coc_trace;
|
| 619 |
renderMitlData();
|
| 620 |
-
|
| 621 |
}
|
| 622 |
} catch (e) {
|
| 623 |
console.error(e);
|
| 624 |
-
|
| 625 |
}
|
| 626 |
}
|
| 627 |
|
|
@@ -758,7 +909,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 758 |
}
|
| 759 |
} catch (e) {
|
| 760 |
console.error(e);
|
| 761 |
-
|
|
|
|
|
|
|
| 762 |
} finally {
|
| 763 |
btnPreprocessDataset.textContent = 'Load Catalog Metadata';
|
| 764 |
btnPreprocessDataset.disabled = false;
|
|
@@ -839,12 +992,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 839 |
});
|
| 840 |
const data = await res.json();
|
| 841 |
if (data.status === 'ok') {
|
| 842 |
-
|
|
|
|
|
|
|
|
|
|
| 843 |
pipelineWizardModal.classList.add('hidden');
|
| 844 |
}
|
| 845 |
} catch (e) {
|
| 846 |
console.error(e);
|
| 847 |
-
|
| 848 |
} finally {
|
| 849 |
btnWizardSubmit.textContent = 'Save Pipeline Configuration';
|
| 850 |
btnWizardSubmit.disabled = false;
|
|
@@ -947,7 +1103,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 947 |
// Add Box Handler
|
| 948 |
btnMitlAddBox.addEventListener('click', () => {
|
| 949 |
if (!currentMitlData) {
|
| 950 |
-
|
| 951 |
return;
|
| 952 |
}
|
| 953 |
const nextId = currentMitlData.annotations.length > 0
|
|
@@ -966,7 +1122,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 966 |
// Save Human Edits
|
| 967 |
btnMitlSave.addEventListener('click', async () => {
|
| 968 |
if (!currentMitlData) {
|
| 969 |
-
|
| 970 |
return;
|
| 971 |
}
|
| 972 |
|
|
@@ -994,7 +1150,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 994 |
});
|
| 995 |
|
| 996 |
if (parseError) {
|
| 997 |
-
|
| 998 |
return;
|
| 999 |
}
|
| 1000 |
|
|
@@ -1014,18 +1170,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1014 |
// Update local memory
|
| 1015 |
currentMitlData.annotations = updatedAnns;
|
| 1016 |
currentMitlData.coc_trace = mitlCocTrace.value;
|
| 1017 |
-
|
| 1018 |
}
|
| 1019 |
} catch (e) {
|
| 1020 |
console.error(e);
|
| 1021 |
-
|
| 1022 |
}
|
| 1023 |
});
|
| 1024 |
|
| 1025 |
// Ask Gemma 4 critique
|
| 1026 |
btnMitlAudit.addEventListener('click', async () => {
|
| 1027 |
if (!currentMitlData) {
|
| 1028 |
-
|
| 1029 |
return;
|
| 1030 |
}
|
| 1031 |
|
|
@@ -1312,7 +1468,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1312 |
}
|
| 1313 |
} catch (e) {
|
| 1314 |
console.error(e);
|
| 1315 |
-
|
| 1316 |
checkbox.checked = !checkbox.checked;
|
| 1317 |
bullet.style.transform = checkbox.checked ? 'translateX(22px)' : 'none';
|
| 1318 |
updateSliderStyle();
|
|
@@ -1827,9 +1983,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1827 |
}),
|
| 1828 |
});
|
| 1829 |
ssamState.selectedRow.manual_annotation = ssamDrawerAnnotation.value;
|
| 1830 |
-
|
| 1831 |
} catch (e) {
|
| 1832 |
-
|
| 1833 |
} finally {
|
| 1834 |
ssamDrawerSave.textContent = 'Save Annotation';
|
| 1835 |
ssamDrawerSave.disabled = false;
|
|
@@ -2077,6 +2233,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2077 |
const statusEl = document.getElementById('perception-status');
|
| 2078 |
statusEl.textContent = 'Running SAM auto-label...';
|
| 2079 |
try {
|
|
|
|
| 2080 |
const res = await fetch(`${API_BASE}/api/perception/auto-label`, {
|
| 2081 |
method: 'POST',
|
| 2082 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2084,17 +2241,63 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2084 |
sequence_id: currentSequenceId,
|
| 2085 |
sam_checkpoint: document.getElementById('perception-sam-checkpoint')?.value || 'models/sam_vit_b.pth',
|
| 2086 |
device: document.getElementById('perception-device')?.value || 'cpu',
|
| 2087 |
-
no_sam:
|
| 2088 |
}),
|
| 2089 |
});
|
| 2090 |
const data = await res.json();
|
| 2091 |
-
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
|
| 2095 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2096 |
} else {
|
| 2097 |
-
statusEl.textContent =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2098 |
}
|
| 2099 |
await refreshPipelineStatus();
|
| 2100 |
await refreshAllOutputBrowsers();
|
|
@@ -2328,4 +2531,247 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2328 |
}
|
| 2329 |
});
|
| 2330 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2331 |
});
|
|
|
|
| 2 |
// API base URL
|
| 3 |
const API_BASE = '';
|
| 4 |
|
| 5 |
+
// Non-blocking status signaling (replaces window.alert for routine feedback)
|
| 6 |
+
const toastStack = document.getElementById('toast-stack');
|
| 7 |
+
const TOAST_ICONS = { success: '✓', error: '✕', warning: '!', info: 'ℹ' };
|
| 8 |
+
const TOAST_DEFAULT_MS = { success: 3800, info: 4200, warning: 5200, error: 6500 };
|
| 9 |
+
|
| 10 |
+
function notify(message, type = 'info', options = {}) {
|
| 11 |
+
if (!toastStack || message == null || message === '') return;
|
| 12 |
+
const kind = ['success', 'error', 'warning', 'info'].includes(type) ? type : 'info';
|
| 13 |
+
const duration = options.duration ?? TOAST_DEFAULT_MS[kind];
|
| 14 |
+
|
| 15 |
+
const el = document.createElement('div');
|
| 16 |
+
el.className = `toast toast-${kind}`;
|
| 17 |
+
el.setAttribute('role', kind === 'error' ? 'alert' : 'status');
|
| 18 |
+
|
| 19 |
+
const icon = document.createElement('span');
|
| 20 |
+
icon.className = 'toast-icon';
|
| 21 |
+
icon.textContent = TOAST_ICONS[kind];
|
| 22 |
+
icon.setAttribute('aria-hidden', 'true');
|
| 23 |
+
|
| 24 |
+
const body = document.createElement('div');
|
| 25 |
+
body.className = 'toast-body';
|
| 26 |
+
body.textContent = String(message);
|
| 27 |
+
|
| 28 |
+
const close = document.createElement('button');
|
| 29 |
+
close.type = 'button';
|
| 30 |
+
close.className = 'toast-close';
|
| 31 |
+
close.setAttribute('aria-label', 'Dismiss');
|
| 32 |
+
close.textContent = '×';
|
| 33 |
+
|
| 34 |
+
let hideTimer = null;
|
| 35 |
+
const dismiss = () => {
|
| 36 |
+
if (hideTimer) clearTimeout(hideTimer);
|
| 37 |
+
el.classList.add('toast-out');
|
| 38 |
+
setTimeout(() => el.remove(), 180);
|
| 39 |
+
};
|
| 40 |
+
close.addEventListener('click', dismiss);
|
| 41 |
+
|
| 42 |
+
el.appendChild(icon);
|
| 43 |
+
el.appendChild(body);
|
| 44 |
+
el.appendChild(close);
|
| 45 |
+
toastStack.appendChild(el);
|
| 46 |
+
|
| 47 |
+
if (duration > 0) {
|
| 48 |
+
hideTimer = setTimeout(dismiss, duration);
|
| 49 |
+
}
|
| 50 |
+
return dismiss;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
// Stage Navigation
|
| 54 |
const navButtons = document.querySelectorAll('.nav-btn');
|
| 55 |
const panels = document.querySelectorAll('.stage-panel');
|
|
|
|
| 78 |
let pipelineState = {};
|
| 79 |
let currentSequenceId = 'seq_001';
|
| 80 |
|
| 81 |
+
async function syncActiveSequence() {
|
| 82 |
+
try {
|
| 83 |
+
const cfg = await fetch(`${API_BASE}/api/config`).then((r) => r.json());
|
| 84 |
+
if (cfg?.sequence_id) {
|
| 85 |
+
currentSequenceId = cfg.sequence_id;
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
} catch (e) { /* ignore */ }
|
| 89 |
+
try {
|
| 90 |
+
const st = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${encodeURIComponent(currentSequenceId)}`).then((r) => r.json());
|
| 91 |
+
if (st?.active_sequence_id) currentSequenceId = st.active_sequence_id;
|
| 92 |
+
} catch (e) { /* ignore */ }
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
async function refreshPipelineStatus() {
|
| 96 |
try {
|
| 97 |
+
await syncActiveSequence();
|
| 98 |
+
const res = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${encodeURIComponent(currentSequenceId)}`);
|
| 99 |
const data = await res.json();
|
| 100 |
pipelineState = data;
|
| 101 |
+
if (data.active_sequence_id) currentSequenceId = data.active_sequence_id;
|
| 102 |
updateGateBadges();
|
| 103 |
} catch (e) { /* backend offline */ }
|
| 104 |
}
|
|
|
|
| 123 |
|
| 124 |
if (meta.completionOnly) {
|
| 125 |
if (verdict === true) {
|
| 126 |
+
badge.textContent = pipelineState.demo_stub ? 'STUB' : 'DONE';
|
| 127 |
+
badge.style.background = pipelineState.demo_stub ? '#fbbf2433' : '#00ffaa33';
|
| 128 |
+
badge.style.color = pipelineState.demo_stub ? '#fbbf24' : '#00ffaa';
|
| 129 |
} else {
|
| 130 |
badge.textContent = '';
|
| 131 |
}
|
|
|
|
| 141 |
badge.style.background = '#ff444433';
|
| 142 |
badge.style.color = '#ff4444';
|
| 143 |
} else if (completed === true) {
|
| 144 |
+
badge.textContent = 'RAN';
|
| 145 |
+
badge.style.background = '#3b82f633';
|
| 146 |
+
badge.style.color = '#93c5fd';
|
|
|
|
| 147 |
} else {
|
| 148 |
badge.textContent = '';
|
| 149 |
}
|
|
|
|
| 189 |
const currentStage = document.querySelector('.nav-btn.active')?.dataset.stage;
|
| 190 |
const meta = STAGE_METADATA[currentStage];
|
| 191 |
if (meta && meta.gateKey && pipelineState[meta.gateKey] === false) {
|
| 192 |
+
notify('Launch gate not passed. Complete the quality gate first.', 'warning');
|
| 193 |
return;
|
| 194 |
}
|
| 195 |
switchStage(nextBtn.dataset.next);
|
|
|
|
| 238 |
source_path: datasetSourcePath.value
|
| 239 |
})
|
| 240 |
});
|
| 241 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 242 |
const data = await res.json();
|
| 243 |
+
notify('Configuration saved (YAML/paths not yet executed).', 'success');
|
| 244 |
+
const cfgStatus = document.getElementById('config-exec-status');
|
| 245 |
+
if (cfgStatus) {
|
| 246 |
+
cfgStatus.textContent = 'CONFIGURATION SAVED / not yet used by an execution — run Load & Preprocess.';
|
| 247 |
+
cfgStatus.dataset.modified = '1';
|
| 248 |
+
}
|
| 249 |
} catch (e) {
|
| 250 |
console.error(e);
|
| 251 |
+
notify('Failed to save dataset configuration.', 'error');
|
| 252 |
}
|
| 253 |
});
|
| 254 |
|
|
|
|
| 257 |
try {
|
| 258 |
const res = await fetch(`${API_BASE}/api/precheck`);
|
| 259 |
const data = await res.json();
|
| 260 |
+
const precheckType = data.verified ? 'success' : 'warning';
|
| 261 |
+
notify(`Pre-check: ${data.status}\n${data.message || ''}`, precheckType);
|
| 262 |
+
refreshScriptStatus(data.scripts);
|
| 263 |
} catch (e) {
|
| 264 |
+
notify('Failed to complete pre-check.', 'error');
|
| 265 |
}
|
| 266 |
});
|
| 267 |
|
|
|
|
| 309 |
})
|
| 310 |
});
|
| 311 |
const data = await res.json();
|
| 312 |
+
if (!res.ok) {
|
| 313 |
+
terminalLogs.textContent += `Error: ${JSON.stringify(data)}\n`;
|
| 314 |
+
resetTrainingUI();
|
| 315 |
+
notify(data.detail?.message || data.detail || 'Failed to start training', 'error');
|
| 316 |
+
return;
|
| 317 |
+
}
|
| 318 |
+
renderEvidenceCard('evidence-training', {
|
| 319 |
+
status: 'RUNNING',
|
| 320 |
+
execution_id: data.execution_id,
|
| 321 |
+
process_id: data.process_id,
|
| 322 |
+
command: data.command,
|
| 323 |
+
message: 'Training process spawned',
|
| 324 |
+
});
|
| 325 |
pollTraining();
|
| 326 |
trainInterval = setInterval(pollTraining, 1500);
|
| 327 |
} catch (e) {
|
|
|
|
| 371 |
}
|
| 372 |
|
| 373 |
if (!status.running) {
|
| 374 |
+
const verdict = status.status || (status.exit_code === 0 ? 'SUCCEEDED' : 'FAILED');
|
| 375 |
+
terminalLogs.textContent += `\n--- Process finished: ${verdict} exit_code=${status.exit_code} ---\n`;
|
| 376 |
+
renderEvidenceCard('evidence-training', {
|
| 377 |
+
status: verdict,
|
| 378 |
+
execution_id: status.execution_id,
|
| 379 |
+
exit_code: status.exit_code,
|
| 380 |
+
process_id: status.process_id,
|
| 381 |
+
command: status.command,
|
| 382 |
+
metrics: {
|
| 383 |
+
epochs_observed: status.epochs_observed,
|
| 384 |
+
epochs_requested: status.epochs_requested,
|
| 385 |
+
checkpoint: status.checkpoint,
|
| 386 |
+
losses_parsed: (status.losses || []).length,
|
| 387 |
+
},
|
| 388 |
+
message: `Training ${verdict}`,
|
| 389 |
+
});
|
| 390 |
+
refreshExecutionConsole();
|
| 391 |
resetTrainingUI();
|
| 392 |
}
|
| 393 |
} catch (e) {
|
|
|
|
| 496 |
})
|
| 497 |
});
|
| 498 |
const data = await res.json();
|
| 499 |
+
if (!res.ok) {
|
| 500 |
+
const detail = data.detail?.message || data.detail || JSON.stringify(data);
|
| 501 |
+
notify(`Inference failed: ${detail}`, 'error');
|
| 502 |
+
renderEvidenceCard('evidence-inference', {
|
| 503 |
+
status: 'FAILED',
|
| 504 |
+
execution_id: data.detail?.execution_id || data.execution_id,
|
| 505 |
+
message: detail,
|
| 506 |
+
});
|
| 507 |
+
return;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
selectInferImage.innerHTML = '';
|
| 511 |
if (data.images && data.images.length > 0) {
|
| 512 |
data.images.forEach(img => {
|
|
|
|
| 515 |
opt.textContent = img;
|
| 516 |
selectInferImage.appendChild(opt);
|
| 517 |
});
|
|
|
|
| 518 |
loadImage(data.images[0]);
|
| 519 |
} else {
|
| 520 |
selectInferImage.innerHTML = '<option value="">No images found</option>';
|
| 521 |
}
|
| 522 |
+
renderEvidenceCard('evidence-inference', {
|
| 523 |
+
status: data.status,
|
| 524 |
+
execution_id: data.execution_id,
|
| 525 |
+
duration_ms: data.duration_ms,
|
| 526 |
+
model: data.model,
|
| 527 |
+
checkpoint: data.checkpoint,
|
| 528 |
+
metrics: {
|
| 529 |
+
discovered: data.records_discovered,
|
| 530 |
+
processed: data.records_processed,
|
| 531 |
+
succeeded: data.records_succeeded,
|
| 532 |
+
failed: data.records_failed,
|
| 533 |
+
inference_calls: data.inference_calls,
|
| 534 |
+
predictions: data.predictions_generated,
|
| 535 |
+
output_dir: data.output_dir,
|
| 536 |
+
exit_code: data.exit_code,
|
| 537 |
+
},
|
| 538 |
+
message: data.message,
|
| 539 |
+
});
|
| 540 |
+
refreshExecutionConsole();
|
| 541 |
+
if (data.status === 'SUCCEEDED' || data.status === 'PARTIAL_SUCCESS') {
|
| 542 |
+
notify(`Inference ${data.status}: ${data.records_succeeded}/${data.records_discovered} images`, 'success');
|
| 543 |
+
} else {
|
| 544 |
+
notify(`Inference ${data.status}: ${data.message || 'see evidence'}`, 'warning');
|
| 545 |
+
}
|
| 546 |
} catch (e) {
|
| 547 |
console.error(e);
|
| 548 |
+
notify('Inference execution failed.', 'error');
|
| 549 |
} finally {
|
| 550 |
btnRunInfer.textContent = '⚡ Run Auto-Labeler';
|
| 551 |
btnRunInfer.disabled = false;
|
|
|
|
| 576 |
try {
|
| 577 |
const res = await fetch(`${API_BASE}/api/grade`);
|
| 578 |
const data = await res.json();
|
| 579 |
+
if (!res.ok) {
|
| 580 |
+
notify(data.detail?.message || data.detail || 'Grader failed', 'error');
|
| 581 |
+
renderEvidenceCard('evidence-grader', {
|
| 582 |
+
status: 'FAILED',
|
| 583 |
+
execution_id: data.detail?.execution_id || data.execution_id,
|
| 584 |
+
message: data.detail?.message || data.detail,
|
| 585 |
+
});
|
| 586 |
+
return;
|
| 587 |
+
}
|
| 588 |
|
| 589 |
+
graderScore.textContent = data.quality_score != null ? Number(data.quality_score).toFixed(1) : '—';
|
| 590 |
+
statTotalImgs.textContent = data.total_images ?? '—';
|
| 591 |
+
statTotalPreds.textContent = data.total_predictions ?? '—';
|
| 592 |
|
|
|
|
| 593 |
graderIssuesList.innerHTML = '';
|
| 594 |
+
if (data.status === 'NOT_EXECUTED') {
|
| 595 |
+
graderIssuesList.innerHTML = '<div class="empty-state">NOT_EXECUTED — no predictions artifact. This is not a perfect score.</div>';
|
| 596 |
+
} else if (data.issues && data.issues.length > 0) {
|
| 597 |
data.issues.forEach(issue => {
|
| 598 |
const el = document.createElement('div');
|
| 599 |
el.className = `issue-card ${issue.severity.toLowerCase()}`;
|
|
|
|
| 605 |
graderIssuesList.appendChild(el);
|
| 606 |
});
|
| 607 |
} else {
|
| 608 |
+
graderIssuesList.innerHTML = '<div class="empty-state">No quality anomalies reported by grader output (not a claim of ground-truth perfection).</div>';
|
| 609 |
}
|
| 610 |
+
renderEvidenceCard('evidence-grader', {
|
| 611 |
+
status: data.status,
|
| 612 |
+
execution_id: data.execution_id,
|
| 613 |
+
duration_ms: data.duration_ms,
|
| 614 |
+
metrics: data.metrics || {
|
| 615 |
+
total_images: data.total_images,
|
| 616 |
+
total_predictions: data.total_predictions,
|
| 617 |
+
quality_score: data.quality_score,
|
| 618 |
+
},
|
| 619 |
+
message: data.message,
|
| 620 |
+
});
|
| 621 |
+
refreshExecutionConsole();
|
| 622 |
} catch (e) {
|
| 623 |
console.error(e);
|
| 624 |
+
notify('Failed to run quality diagnostics.', 'error');
|
| 625 |
} finally {
|
| 626 |
btnRunGrader.textContent = 'Run Quality Diagnostics';
|
| 627 |
btnRunGrader.disabled = false;
|
|
|
|
| 768 |
currentMitlData = data.data;
|
| 769 |
originalCocText = currentMitlData.coc_trace;
|
| 770 |
renderMitlData();
|
| 771 |
+
notify(`Dataset loaded: ${currentMitlData.dataset}`, 'success');
|
| 772 |
}
|
| 773 |
} catch (e) {
|
| 774 |
console.error(e);
|
| 775 |
+
notify('Failed to load NVIDIA dataset.', 'error');
|
| 776 |
}
|
| 777 |
}
|
| 778 |
|
|
|
|
| 909 |
}
|
| 910 |
} catch (e) {
|
| 911 |
console.error(e);
|
| 912 |
+
const statusEl = document.getElementById('source-browse-status');
|
| 913 |
+
if (statusEl) statusEl.textContent = 'Catalog metadata load failed.';
|
| 914 |
+
notify('Catalog metadata load failed.', 'error');
|
| 915 |
} finally {
|
| 916 |
btnPreprocessDataset.textContent = 'Load Catalog Metadata';
|
| 917 |
btnPreprocessDataset.disabled = false;
|
|
|
|
| 992 |
});
|
| 993 |
const data = await res.json();
|
| 994 |
if (data.status === 'ok') {
|
| 995 |
+
notify(
|
| 996 |
+
`Pipeline configuration saved\nAnnotation: ${annotationTool}\nTraining: ${trainingFramework}\nValidation: ${validationMethod}`,
|
| 997 |
+
'success'
|
| 998 |
+
);
|
| 999 |
pipelineWizardModal.classList.add('hidden');
|
| 1000 |
}
|
| 1001 |
} catch (e) {
|
| 1002 |
console.error(e);
|
| 1003 |
+
notify('Failed to save pipeline configuration settings.', 'error');
|
| 1004 |
} finally {
|
| 1005 |
btnWizardSubmit.textContent = 'Save Pipeline Configuration';
|
| 1006 |
btnWizardSubmit.disabled = false;
|
|
|
|
| 1103 |
// Add Box Handler
|
| 1104 |
btnMitlAddBox.addEventListener('click', () => {
|
| 1105 |
if (!currentMitlData) {
|
| 1106 |
+
notify('Please load a dataset first.', 'warning');
|
| 1107 |
return;
|
| 1108 |
}
|
| 1109 |
const nextId = currentMitlData.annotations.length > 0
|
|
|
|
| 1122 |
// Save Human Edits
|
| 1123 |
btnMitlSave.addEventListener('click', async () => {
|
| 1124 |
if (!currentMitlData) {
|
| 1125 |
+
notify('No dataset currently loaded to save.', 'warning');
|
| 1126 |
return;
|
| 1127 |
}
|
| 1128 |
|
|
|
|
| 1150 |
});
|
| 1151 |
|
| 1152 |
if (parseError) {
|
| 1153 |
+
notify('Invalid bounding box format. Use coordinates [x, y, w, h].', 'error');
|
| 1154 |
return;
|
| 1155 |
}
|
| 1156 |
|
|
|
|
| 1170 |
// Update local memory
|
| 1171 |
currentMitlData.annotations = updatedAnns;
|
| 1172 |
currentMitlData.coc_trace = mitlCocTrace.value;
|
| 1173 |
+
notify('Triage results saved to runs/mitl_annotations.json.', 'success');
|
| 1174 |
}
|
| 1175 |
} catch (e) {
|
| 1176 |
console.error(e);
|
| 1177 |
+
notify('Failed to save human edits.', 'error');
|
| 1178 |
}
|
| 1179 |
});
|
| 1180 |
|
| 1181 |
// Ask Gemma 4 critique
|
| 1182 |
btnMitlAudit.addEventListener('click', async () => {
|
| 1183 |
if (!currentMitlData) {
|
| 1184 |
+
notify('No dataset currently loaded to audit.', 'warning');
|
| 1185 |
return;
|
| 1186 |
}
|
| 1187 |
|
|
|
|
| 1468 |
}
|
| 1469 |
} catch (e) {
|
| 1470 |
console.error(e);
|
| 1471 |
+
notify('Failed to toggle MCP server state.', 'error');
|
| 1472 |
checkbox.checked = !checkbox.checked;
|
| 1473 |
bullet.style.transform = checkbox.checked ? 'translateX(22px)' : 'none';
|
| 1474 |
updateSliderStyle();
|
|
|
|
| 1983 |
}),
|
| 1984 |
});
|
| 1985 |
ssamState.selectedRow.manual_annotation = ssamDrawerAnnotation.value;
|
| 1986 |
+
notify(`Annotation saved for ${ssamState.selectedRow.street_name}`, 'success');
|
| 1987 |
} catch (e) {
|
| 1988 |
+
notify('Failed to save annotation.', 'error');
|
| 1989 |
} finally {
|
| 1990 |
ssamDrawerSave.textContent = 'Save Annotation';
|
| 1991 |
ssamDrawerSave.disabled = false;
|
|
|
|
| 2233 |
const statusEl = document.getElementById('perception-status');
|
| 2234 |
statusEl.textContent = 'Running SAM auto-label...';
|
| 2235 |
try {
|
| 2236 |
+
await syncActiveSequence();
|
| 2237 |
const res = await fetch(`${API_BASE}/api/perception/auto-label`, {
|
| 2238 |
method: 'POST',
|
| 2239 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 2241 |
sequence_id: currentSequenceId,
|
| 2242 |
sam_checkpoint: document.getElementById('perception-sam-checkpoint')?.value || 'models/sam_vit_b.pth',
|
| 2243 |
device: document.getElementById('perception-device')?.value || 'cpu',
|
| 2244 |
+
no_sam: !!document.getElementById('perception-no-sam')?.checked,
|
| 2245 |
}),
|
| 2246 |
});
|
| 2247 |
const data = await res.json();
|
| 2248 |
+
const payload = (data && typeof data.detail === 'object') ? data.detail : data;
|
| 2249 |
+
const status = payload.status || (res.ok ? 'SUCCEEDED' : 'FAILED');
|
| 2250 |
+
const nextSteps = payload.next_steps || [];
|
| 2251 |
+
const stubPrefix = payload.demo_stub ? 'demo stub · ' : '';
|
| 2252 |
+
|
| 2253 |
+
if (!res.ok || status === 'FAILED') {
|
| 2254 |
+
statusEl.textContent = [
|
| 2255 |
+
`${status}: ${payload.message || 'Auto-label could not run.'}`,
|
| 2256 |
+
payload.checkpoint?.exists === false ? 'SAM checkpoint missing.' : '',
|
| 2257 |
+
payload.frames_expected === 0 ? 'No ingested frames for this sequence.' : '',
|
| 2258 |
+
nextSteps.length ? `Next: ${nextSteps[0]}` : '',
|
| 2259 |
+
].filter(Boolean).join('\n');
|
| 2260 |
+
renderEvidenceCard('evidence-perception', {
|
| 2261 |
+
status: 'FAILED',
|
| 2262 |
+
execution_id: payload.execution_id,
|
| 2263 |
+
duration_ms: payload.duration_ms,
|
| 2264 |
+
model: payload.model,
|
| 2265 |
+
checkpoint: payload.checkpoint,
|
| 2266 |
+
metrics: {
|
| 2267 |
+
frames_processed: payload.frames_processed,
|
| 2268 |
+
frames_expected: payload.frames_expected,
|
| 2269 |
+
predictions_generated: payload.predictions_generated,
|
| 2270 |
+
sam_ran: payload.sam_ran,
|
| 2271 |
+
},
|
| 2272 |
+
message: payload.message,
|
| 2273 |
+
next_steps: nextSteps,
|
| 2274 |
+
events: payload.events,
|
| 2275 |
+
});
|
| 2276 |
} else {
|
| 2277 |
+
statusEl.textContent = [
|
| 2278 |
+
`${status}: ${stubPrefix}${payload.frames_processed}/${payload.frames_expected} frames`,
|
| 2279 |
+
payload.message || '',
|
| 2280 |
+
nextSteps.length && status !== 'SUCCEEDED' ? `Next: ${nextSteps[0]}` : '',
|
| 2281 |
+
].filter(Boolean).join('\n');
|
| 2282 |
+
renderEvidenceCard('evidence-perception', {
|
| 2283 |
+
status,
|
| 2284 |
+
execution_id: payload.execution_id,
|
| 2285 |
+
duration_ms: payload.duration_ms,
|
| 2286 |
+
model: payload.model,
|
| 2287 |
+
checkpoint: payload.checkpoint,
|
| 2288 |
+
metrics: {
|
| 2289 |
+
frames_processed: payload.frames_processed,
|
| 2290 |
+
frames_expected: payload.frames_expected,
|
| 2291 |
+
predictions_generated: payload.predictions_generated,
|
| 2292 |
+
inference_calls: payload.inference_calls,
|
| 2293 |
+
sam_ran: payload.sam_ran,
|
| 2294 |
+
output_dir: payload.proposals_dir,
|
| 2295 |
+
},
|
| 2296 |
+
message: payload.message,
|
| 2297 |
+
next_steps: nextSteps,
|
| 2298 |
+
events: payload.events,
|
| 2299 |
+
});
|
| 2300 |
+
refreshExecutionConsole();
|
| 2301 |
}
|
| 2302 |
await refreshPipelineStatus();
|
| 2303 |
await refreshAllOutputBrowsers();
|
|
|
|
| 2531 |
}
|
| 2532 |
});
|
| 2533 |
}
|
| 2534 |
+
|
| 2535 |
+
// --- Execution integrity: evidence cards, console, strict mode, load ---
|
| 2536 |
+
function esc(s) {
|
| 2537 |
+
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 2538 |
+
}
|
| 2539 |
+
|
| 2540 |
+
function renderEvidenceCard(elementId, evidence) {
|
| 2541 |
+
const el = document.getElementById(elementId);
|
| 2542 |
+
if (!el || !evidence) return;
|
| 2543 |
+
const status = evidence.status || 'NOT_EXECUTED';
|
| 2544 |
+
const metrics = evidence.metrics || {};
|
| 2545 |
+
const metricRows = Object.entries(metrics).map(([k, v]) => {
|
| 2546 |
+
const val = typeof v === 'object' ? JSON.stringify(v) : v;
|
| 2547 |
+
return `<div class="ev-row"><span>${esc(k)}</span><strong>${esc(val)}</strong></div>`;
|
| 2548 |
+
}).join('');
|
| 2549 |
+
const events = (evidence.events || []).slice(-8).map(ev =>
|
| 2550 |
+
`<li><code>${esc(ev.ts || '')}</code> ${esc(ev.kind || '')}: ${esc(ev.message || '')}</li>`
|
| 2551 |
+
).join('');
|
| 2552 |
+
const ckpt = evidence.checkpoint
|
| 2553 |
+
? `<div class="ev-row"><span>checkpoint</span><strong>${esc(evidence.checkpoint.path || evidence.checkpoint)} ${evidence.checkpoint.exists === false ? '(missing)' : ''}</strong></div>`
|
| 2554 |
+
: '';
|
| 2555 |
+
const nextSteps = (evidence.next_steps || []).map((s) => `<li>${esc(s)}</li>`).join('');
|
| 2556 |
+
el.innerHTML = `
|
| 2557 |
+
<h4>What actually happened?</h4>
|
| 2558 |
+
<div class="ev-status ev-status-${esc(status)}">${esc(status)}</div>
|
| 2559 |
+
<div class="ev-row"><span>execution_id</span><strong class="font-mono">${esc(evidence.execution_id || '—')}</strong></div>
|
| 2560 |
+
${evidence.duration_ms != null ? `<div class="ev-row"><span>duration_ms</span><strong>${esc(evidence.duration_ms)}</strong></div>` : ''}
|
| 2561 |
+
${evidence.model ? `<div class="ev-row"><span>model</span><strong>${esc(evidence.model)}</strong></div>` : ''}
|
| 2562 |
+
${ckpt}
|
| 2563 |
+
${evidence.process_id != null ? `<div class="ev-row"><span>process_id</span><strong>${esc(evidence.process_id)}</strong></div>` : ''}
|
| 2564 |
+
${evidence.exit_code != null ? `<div class="ev-row"><span>exit_code</span><strong>${esc(evidence.exit_code)}</strong></div>` : ''}
|
| 2565 |
+
${evidence.command ? `<div class="ev-row"><span>command</span><strong class="font-mono">${esc(Array.isArray(evidence.command) ? evidence.command.join(' ') : evidence.command)}</strong></div>` : ''}
|
| 2566 |
+
${metricRows}
|
| 2567 |
+
${evidence.message ? `<p class="ev-msg">${esc(evidence.message)}</p>` : ''}
|
| 2568 |
+
${nextSteps ? `<div class="ev-next"><strong>What to do next</strong><ul>${nextSteps}</ul></div>` : ''}
|
| 2569 |
+
${evidence.execution_id ? `<a class="ev-log-link" href="/api/executions/${encodeURIComponent(evidence.execution_id)}/log" target="_blank" rel="noopener">Open execution log</a>` : ''}
|
| 2570 |
+
${events ? `<details class="ev-timeline"><summary>Event timeline</summary><ul>${events}</ul></details>` : ''}
|
| 2571 |
+
`;
|
| 2572 |
+
}
|
| 2573 |
+
|
| 2574 |
+
function refreshScriptStatus(scripts) {
|
| 2575 |
+
const map = {
|
| 2576 |
+
'train.py': 'script-status-train',
|
| 2577 |
+
'infer.py': 'script-status-infer',
|
| 2578 |
+
'autograder.py': 'script-status-grader',
|
| 2579 |
+
};
|
| 2580 |
+
Object.entries(map).forEach(([name, id]) => {
|
| 2581 |
+
const el = document.getElementById(id);
|
| 2582 |
+
const r = scripts?.[name];
|
| 2583 |
+
if (!el || !r) return;
|
| 2584 |
+
if (!r.exists) {
|
| 2585 |
+
el.textContent = `Missing (${name})`;
|
| 2586 |
+
el.className = 'val font-mono';
|
| 2587 |
+
el.style.color = '#ff8888';
|
| 2588 |
+
} else if (r.syntax_valid && r.dry_run_ok) {
|
| 2589 |
+
el.textContent = `Verified (${name})${r.last_status ? ' · last ' + r.last_status : ''}`;
|
| 2590 |
+
el.className = 'val font-mono green';
|
| 2591 |
+
el.style.color = '';
|
| 2592 |
+
} else {
|
| 2593 |
+
el.textContent = `Found but unverified (${name})`;
|
| 2594 |
+
el.className = 'val font-mono';
|
| 2595 |
+
el.style.color = '#fbbf24';
|
| 2596 |
+
}
|
| 2597 |
+
});
|
| 2598 |
+
}
|
| 2599 |
+
|
| 2600 |
+
async function refreshHealth() {
|
| 2601 |
+
try {
|
| 2602 |
+
const res = await fetch(`${API_BASE}/api/health`);
|
| 2603 |
+
const data = await res.json();
|
| 2604 |
+
const chip = document.getElementById('health-status-chip');
|
| 2605 |
+
if (chip) {
|
| 2606 |
+
chip.textContent = [
|
| 2607 |
+
data.filesystem_writable ? 'FS writable' : 'FS FAIL',
|
| 2608 |
+
data.torch_installed ? 'torch' : 'no-torch',
|
| 2609 |
+
data.ultralytics_installed ? 'yolo' : 'no-yolo',
|
| 2610 |
+
data.gpu?.available ? `GPU:${data.gpu.name}` : 'CPU',
|
| 2611 |
+
data.last_successful_execution_id ? `last:${data.last_successful_execution_id}` : 'no-success-yet',
|
| 2612 |
+
].join(' · ');
|
| 2613 |
+
}
|
| 2614 |
+
if (data.scripts) refreshScriptStatus(data.scripts);
|
| 2615 |
+
const tog = document.getElementById('toggle-strict-mode');
|
| 2616 |
+
if (tog) tog.checked = !!data.strict_mode;
|
| 2617 |
+
} catch (e) {
|
| 2618 |
+
const chip = document.getElementById('health-status-chip');
|
| 2619 |
+
if (chip) chip.textContent = 'Backend unreachable';
|
| 2620 |
+
}
|
| 2621 |
+
}
|
| 2622 |
+
|
| 2623 |
+
async function refreshExecutionConsole() {
|
| 2624 |
+
const list = document.getElementById('exec-console-list');
|
| 2625 |
+
if (!list) return;
|
| 2626 |
+
try {
|
| 2627 |
+
const res = await fetch(`${API_BASE}/api/executions?limit=40`);
|
| 2628 |
+
const data = await res.json();
|
| 2629 |
+
const rows = data.executions || [];
|
| 2630 |
+
if (!rows.length) {
|
| 2631 |
+
list.innerHTML = '<p class="evidence-empty">No executions recorded yet.</p>';
|
| 2632 |
+
return;
|
| 2633 |
+
}
|
| 2634 |
+
list.innerHTML = rows.map(e => `
|
| 2635 |
+
<button type="button" class="exec-row" data-id="${esc(e.execution_id)}">
|
| 2636 |
+
<span class="ev-status ev-status-${esc(e.status)}">${esc(e.status)}</span>
|
| 2637 |
+
<span class="font-mono">${esc(e.execution_id)}</span>
|
| 2638 |
+
<span>${esc(e.operation)}</span>
|
| 2639 |
+
<span>${esc(e.duration_ms != null ? e.duration_ms + 'ms' : '—')}</span>
|
| 2640 |
+
<span>${e.verified ? 'verified' : 'unverified'}</span>
|
| 2641 |
+
</button>
|
| 2642 |
+
`).join('');
|
| 2643 |
+
list.querySelectorAll('.exec-row').forEach(btn => {
|
| 2644 |
+
btn.addEventListener('click', async () => {
|
| 2645 |
+
const detail = document.getElementById('exec-console-detail');
|
| 2646 |
+
const r = await fetch(`${API_BASE}/api/executions/${encodeURIComponent(btn.dataset.id)}`);
|
| 2647 |
+
const full = await r.json();
|
| 2648 |
+
detail.classList.remove('hidden');
|
| 2649 |
+
detail.innerHTML = `<pre>${esc(JSON.stringify(full, null, 2))}</pre>`;
|
| 2650 |
+
});
|
| 2651 |
+
});
|
| 2652 |
+
} catch (e) {
|
| 2653 |
+
list.innerHTML = `<p class="evidence-empty">Failed to load executions: ${esc(e.message)}</p>`;
|
| 2654 |
+
}
|
| 2655 |
+
}
|
| 2656 |
+
|
| 2657 |
+
const btnLoadPreprocess = document.getElementById('btn-load-preprocess');
|
| 2658 |
+
if (btnLoadPreprocess) {
|
| 2659 |
+
btnLoadPreprocess.addEventListener('click', async () => {
|
| 2660 |
+
btnLoadPreprocess.disabled = true;
|
| 2661 |
+
btnLoadPreprocess.textContent = 'Loading…';
|
| 2662 |
+
const statusEl = document.getElementById('source-browse-status');
|
| 2663 |
+
try {
|
| 2664 |
+
const res = await fetch(`${API_BASE}/api/dataset/load`, {
|
| 2665 |
+
method: 'POST',
|
| 2666 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2667 |
+
body: JSON.stringify({
|
| 2668 |
+
source_path: datasetSourcePath?.value || 'data',
|
| 2669 |
+
yaml_path: dataYamlPath?.value || 'coco8.yaml',
|
| 2670 |
+
dataset_type: selectDatasetType?.value || 'local',
|
| 2671 |
+
}),
|
| 2672 |
+
});
|
| 2673 |
+
const data = await res.json();
|
| 2674 |
+
const m = data.metrics || {};
|
| 2675 |
+
if (statusEl) {
|
| 2676 |
+
statusEl.textContent = `${data.status}: discovered=${m.images_discovered ?? 0} readable=${m.images_readable ?? 0} corrupt=${m.images_corrupt ?? 0} → ${data.manifest_path || ''}`;
|
| 2677 |
+
}
|
| 2678 |
+
renderEvidenceCard('evidence-dataset', {
|
| 2679 |
+
status: data.status,
|
| 2680 |
+
execution_id: data.execution_id,
|
| 2681 |
+
duration_ms: data.duration_ms,
|
| 2682 |
+
metrics: {
|
| 2683 |
+
...m,
|
| 2684 |
+
reconciliation: data.reconciliation?.state,
|
| 2685 |
+
manifest: data.manifest_path,
|
| 2686 |
+
},
|
| 2687 |
+
message: data.message,
|
| 2688 |
+
events: data.events,
|
| 2689 |
+
});
|
| 2690 |
+
const badge = document.getElementById('ds-meta-badge');
|
| 2691 |
+
const loadedEl = document.getElementById('ds-kpi-loaded-rows');
|
| 2692 |
+
const pctEl = document.getElementById('ds-kpi-ingest-pct');
|
| 2693 |
+
if (loadedEl) loadedEl.textContent = m.images_readable ?? 0;
|
| 2694 |
+
if (pctEl) pctEl.textContent = m.loaded_pct_of_discovered != null ? `${m.loaded_pct_of_discovered}% of discovered` : '—';
|
| 2695 |
+
if (badge) {
|
| 2696 |
+
if (data.status === 'FAILED' || (m.images_readable ?? 0) === 0) {
|
| 2697 |
+
badge.textContent = 'FAILED — 0 loaded';
|
| 2698 |
+
badge.style.background = 'rgba(255, 68, 68, 0.12)';
|
| 2699 |
+
badge.style.color = '#ff8888';
|
| 2700 |
+
} else {
|
| 2701 |
+
badge.textContent = `${m.images_readable} loaded (evidence)`;
|
| 2702 |
+
badge.style.background = 'rgba(0, 255, 170, 0.15)';
|
| 2703 |
+
badge.style.color = '#00ffaa';
|
| 2704 |
+
}
|
| 2705 |
+
}
|
| 2706 |
+
const cfgStatus = document.getElementById('config-exec-status');
|
| 2707 |
+
if (cfgStatus) {
|
| 2708 |
+
cfgStatus.textContent = `used_by_execution YES · last execution_id ${data.execution_id}`;
|
| 2709 |
+
cfgStatus.dataset.modified = '0';
|
| 2710 |
+
}
|
| 2711 |
+
refreshExecutionConsole();
|
| 2712 |
+
if (data.status === 'FAILED' || data.status === 'VALIDATION_FAILED') {
|
| 2713 |
+
notify(data.message || 'Load failed', 'error');
|
| 2714 |
+
} else if (data.status === 'PARTIAL_SUCCESS') {
|
| 2715 |
+
notify(data.message || 'Partial load', 'warning');
|
| 2716 |
+
} else {
|
| 2717 |
+
notify(data.message || 'Load complete', 'success');
|
| 2718 |
+
}
|
| 2719 |
+
} catch (e) {
|
| 2720 |
+
if (statusEl) statusEl.textContent = `Load failed: ${e.message}`;
|
| 2721 |
+
notify('Load & Preprocess failed.', 'error');
|
| 2722 |
+
} finally {
|
| 2723 |
+
btnLoadPreprocess.disabled = false;
|
| 2724 |
+
btnLoadPreprocess.textContent = 'Load & Preprocess';
|
| 2725 |
+
}
|
| 2726 |
+
});
|
| 2727 |
+
}
|
| 2728 |
+
|
| 2729 |
+
const btnYamlValidate = document.getElementById('btn-yaml-validate');
|
| 2730 |
+
if (btnYamlValidate) {
|
| 2731 |
+
btnYamlValidate.addEventListener('click', async () => {
|
| 2732 |
+
try {
|
| 2733 |
+
const res = await fetch(`${API_BASE}/api/yaml/validate`, {
|
| 2734 |
+
method: 'POST',
|
| 2735 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2736 |
+
body: JSON.stringify({ path: dataYamlPath?.value || 'coco8.yaml' }),
|
| 2737 |
+
});
|
| 2738 |
+
const data = await res.json();
|
| 2739 |
+
notify(`${data.status}: classes=${data.class_count} images=${JSON.stringify(data.image_counts)}`, data.status === 'SUCCEEDED' ? 'success' : 'warning');
|
| 2740 |
+
const cfgStatus = document.getElementById('config-exec-status');
|
| 2741 |
+
if (cfgStatus) {
|
| 2742 |
+
cfgStatus.textContent = `YAML hash ${data.content_hash || '—'} · ${data.status} · execution ${data.execution_id || '—'}`;
|
| 2743 |
+
}
|
| 2744 |
+
refreshExecutionConsole();
|
| 2745 |
+
} catch (e) {
|
| 2746 |
+
notify(`YAML validate failed: ${e.message}`, 'error');
|
| 2747 |
+
}
|
| 2748 |
+
});
|
| 2749 |
+
}
|
| 2750 |
+
|
| 2751 |
+
document.getElementById('btn-open-exec-console')?.addEventListener('click', () => {
|
| 2752 |
+
document.getElementById('exec-console-modal')?.classList.remove('hidden');
|
| 2753 |
+
refreshExecutionConsole();
|
| 2754 |
+
});
|
| 2755 |
+
document.getElementById('btn-close-exec-console')?.addEventListener('click', () => {
|
| 2756 |
+
document.getElementById('exec-console-modal')?.classList.add('hidden');
|
| 2757 |
+
});
|
| 2758 |
+
document.getElementById('btn-refresh-executions')?.addEventListener('click', refreshExecutionConsole);
|
| 2759 |
+
document.getElementById('btn-health-check')?.addEventListener('click', async () => {
|
| 2760 |
+
await refreshHealth();
|
| 2761 |
+
notify('Health refreshed — see Workspace Status chip.', 'info');
|
| 2762 |
+
});
|
| 2763 |
+
document.getElementById('toggle-strict-mode')?.addEventListener('change', async (e) => {
|
| 2764 |
+
try {
|
| 2765 |
+
await fetch(`${API_BASE}/api/strict-mode`, {
|
| 2766 |
+
method: 'POST',
|
| 2767 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2768 |
+
body: JSON.stringify({ enabled: e.target.checked }),
|
| 2769 |
+
});
|
| 2770 |
+
notify(e.target.checked ? 'Strict Execution Mode ON' : 'Strict Execution Mode OFF', 'info');
|
| 2771 |
+
} catch (err) {
|
| 2772 |
+
notify('Failed to toggle strict mode', 'error');
|
| 2773 |
+
}
|
| 2774 |
+
});
|
| 2775 |
+
|
| 2776 |
+
refreshHealth();
|
| 2777 |
});
|
static/index.html
CHANGED
|
@@ -102,7 +102,12 @@
|
|
| 102 |
<h2 id="stage-title">Dataset Configuration</h2>
|
| 103 |
<p id="stage-desc">Prepare and validate your image source path and annotation files.</p>
|
| 104 |
</div>
|
| 105 |
-
<div class="controls-area">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
<button class="btn btn-secondary" id="btn-validate">Run Pre-Check</button>
|
| 107 |
<button class="btn btn-primary" id="btn-global-action">Proceed to Model</button>
|
| 108 |
</div>
|
|
@@ -141,11 +146,18 @@
|
|
| 141 |
</div>
|
| 142 |
<div class="actions-row" style="display: flex; gap: 12px; flex-wrap: wrap;">
|
| 143 |
<button class="btn btn-primary" id="btn-save-dataset">Save Config</button>
|
|
|
|
| 144 |
<button class="btn btn-secondary" id="btn-preprocess-dataset">Load Catalog Metadata</button>
|
| 145 |
<button class="btn btn-secondary" id="btn-browse-source" title="List and preview images under Images Path">Validate & Browse Images Path</button>
|
|
|
|
| 146 |
</div>
|
|
|
|
| 147 |
<div id="source-browse-status" class="status-box" style="margin-top:12px;"></div>
|
| 148 |
<div id="source-browse-gallery" class="output-browse-gallery" style="margin-top:12px;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
</div>
|
| 150 |
|
| 151 |
<div class="card card-glass">
|
|
@@ -157,15 +169,19 @@
|
|
| 157 |
</div>
|
| 158 |
<div class="info-row">
|
| 159 |
<span class="lbl">Train script:</span>
|
| 160 |
-
<span class="val font-mono
|
| 161 |
</div>
|
| 162 |
<div class="info-row">
|
| 163 |
<span class="lbl">Infer script:</span>
|
| 164 |
-
<span class="val font-mono
|
| 165 |
</div>
|
| 166 |
<div class="info-row">
|
| 167 |
<span class="lbl">Auto-Grader script:</span>
|
| 168 |
-
<span class="val font-mono
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
</div>
|
|
@@ -346,7 +362,11 @@
|
|
| 346 |
<p class="text-muted">Generate 2D SAM masks and lift to 3D bounding box proposals.</p>
|
| 347 |
<div class="form-group">
|
| 348 |
<label for="perception-sam-checkpoint">SAM Checkpoint</label>
|
| 349 |
-
<input type="text" id="perception-sam-checkpoint" value="models/sam_vit_b.pth" class="input-field"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
</div>
|
| 351 |
<div class="form-group">
|
| 352 |
<label for="perception-device">Device</label>
|
|
@@ -355,8 +375,15 @@
|
|
| 355 |
<option value="cuda">CUDA</option>
|
| 356 |
</select>
|
| 357 |
</div>
|
|
|
|
|
|
|
|
|
|
| 358 |
<button class="btn btn-primary" id="btn-perception-run">Run Auto-Label</button>
|
| 359 |
<div id="perception-status" class="status-box" style="margin-top:16px;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
</div>
|
| 361 |
<div class="card card-glass pipeline-output-browser" style="margin-top:16px;" data-browser="perception">
|
| 362 |
<h3>Pipeline Outputs — proposals & frames</h3>
|
|
@@ -392,6 +419,10 @@
|
|
| 392 |
<button class="btn btn-primary" id="btn-tracking-run">Run Temporal Tracker</button>
|
| 393 |
<div id="tracking-status" class="status-box" style="margin-top:16px;"></div>
|
| 394 |
<div id="tracking-tracks-table" style="margin-top:16px;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
</div>
|
| 396 |
</section>
|
| 397 |
|
|
@@ -403,6 +434,10 @@
|
|
| 403 |
<button class="btn btn-primary" id="btn-quality-gate-run">Run Quality Gate</button>
|
| 404 |
<div id="quality-gate-status" class="status-box" style="margin-top:16px;"></div>
|
| 405 |
<div id="quality-gate-metrics" style="margin-top:16px;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
</div>
|
| 407 |
</section>
|
| 408 |
|
|
@@ -522,6 +557,10 @@ ollama run gemma4:26b
|
|
| 522 |
<div class="progress-bar-fill" id="train-progress-bar" style="width: 0%"></div>
|
| 523 |
</div>
|
| 524 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
</div>
|
| 526 |
|
| 527 |
<div class="card card-glass">
|
|
@@ -562,6 +601,10 @@ ollama run gemma4:26b
|
|
| 562 |
<span class="range-val" id="val-infer-iou">0.45</span>
|
| 563 |
</div>
|
| 564 |
<button class="btn btn-primary btn-large" id="btn-run-infer">⚡ Run Auto-Labeler</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
</div>
|
| 566 |
|
| 567 |
<div class="card card-glass">
|
|
@@ -603,6 +646,10 @@ ollama run gemma4:26b
|
|
| 603 |
</div>
|
| 604 |
</div>
|
| 605 |
<button class="btn btn-primary width-full" id="btn-run-grader" style="margin-top: 15px;">Run Quality Diagnostics</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
</div>
|
| 607 |
|
| 608 |
<div class="card card-glass">
|
|
@@ -1071,6 +1118,23 @@ ollama run gemma4:26b
|
|
| 1071 |
</div>
|
| 1072 |
</div>
|
| 1073 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
<script src="app.js"></script>
|
| 1075 |
</body>
|
| 1076 |
</html>
|
|
|
|
| 102 |
<h2 id="stage-title">Dataset Configuration</h2>
|
| 103 |
<p id="stage-desc">Prepare and validate your image source path and annotation files.</p>
|
| 104 |
</div>
|
| 105 |
+
<div class="controls-area" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
| 106 |
+
<label class="strict-mode-toggle" title="Refuse SUCCEEDED unless process, exit, outputs, metrics, and execution_id are verified">
|
| 107 |
+
<input type="checkbox" id="toggle-strict-mode"> Strict Execution
|
| 108 |
+
</label>
|
| 109 |
+
<button class="btn btn-secondary" id="btn-open-exec-console" title="Global execution history">Execution Console</button>
|
| 110 |
+
<button class="btn btn-secondary" id="btn-health-check" title="Meaningful backend health">Health</button>
|
| 111 |
<button class="btn btn-secondary" id="btn-validate">Run Pre-Check</button>
|
| 112 |
<button class="btn btn-primary" id="btn-global-action">Proceed to Model</button>
|
| 113 |
</div>
|
|
|
|
| 146 |
</div>
|
| 147 |
<div class="actions-row" style="display: flex; gap: 12px; flex-wrap: wrap;">
|
| 148 |
<button class="btn btn-primary" id="btn-save-dataset">Save Config</button>
|
| 149 |
+
<button class="btn btn-primary" id="btn-load-preprocess" title="Real discovery/decode/validation with ledger evidence">Load & Preprocess</button>
|
| 150 |
<button class="btn btn-secondary" id="btn-preprocess-dataset">Load Catalog Metadata</button>
|
| 151 |
<button class="btn btn-secondary" id="btn-browse-source" title="List and preview images under Images Path">Validate & Browse Images Path</button>
|
| 152 |
+
<button class="btn btn-secondary" id="btn-yaml-validate" title="Semantic YAML path/class validation">Validate YAML</button>
|
| 153 |
</div>
|
| 154 |
+
<div id="config-exec-status" class="config-exec-status" style="margin-top:10px;font-size:12px;color:var(--text-muted);"></div>
|
| 155 |
<div id="source-browse-status" class="status-box" style="margin-top:12px;"></div>
|
| 156 |
<div id="source-browse-gallery" class="output-browse-gallery" style="margin-top:12px;"></div>
|
| 157 |
+
<div class="evidence-card" id="evidence-dataset" data-stage="dataset">
|
| 158 |
+
<h4>What actually happened?</h4>
|
| 159 |
+
<p class="evidence-empty">No execution yet — Load & Preprocess to generate verifiable evidence.</p>
|
| 160 |
+
</div>
|
| 161 |
</div>
|
| 162 |
|
| 163 |
<div class="card card-glass">
|
|
|
|
| 169 |
</div>
|
| 170 |
<div class="info-row">
|
| 171 |
<span class="lbl">Train script:</span>
|
| 172 |
+
<span class="val font-mono" id="script-status-train">Verifying…</span>
|
| 173 |
</div>
|
| 174 |
<div class="info-row">
|
| 175 |
<span class="lbl">Infer script:</span>
|
| 176 |
+
<span class="val font-mono" id="script-status-infer">Verifying…</span>
|
| 177 |
</div>
|
| 178 |
<div class="info-row">
|
| 179 |
<span class="lbl">Auto-Grader script:</span>
|
| 180 |
+
<span class="val font-mono" id="script-status-grader">Verifying…</span>
|
| 181 |
+
</div>
|
| 182 |
+
<div class="info-row">
|
| 183 |
+
<span class="lbl">Backend health:</span>
|
| 184 |
+
<span class="val font-mono" id="health-status-chip">—</span>
|
| 185 |
</div>
|
| 186 |
</div>
|
| 187 |
</div>
|
|
|
|
| 362 |
<p class="text-muted">Generate 2D SAM masks and lift to 3D bounding box proposals.</p>
|
| 363 |
<div class="form-group">
|
| 364 |
<label for="perception-sam-checkpoint">SAM Checkpoint</label>
|
| 365 |
+
<input type="text" id="perception-sam-checkpoint" value="models/sam_vit_b.pth" class="input-field"
|
| 366 |
+
placeholder="models/sam_vit_b.pth — download ViT-B weights locally (not bundled on HF Space CPU)">
|
| 367 |
+
<p class="text-muted" style="font-size:12px;margin-top:4px;">
|
| 368 |
+
Default path <code>models/sam_vit_b.pth</code>. On Hugging Face Space, upload weights or run locally with GPU.
|
| 369 |
+
</p>
|
| 370 |
</div>
|
| 371 |
<div class="form-group">
|
| 372 |
<label for="perception-device">Device</label>
|
|
|
|
| 375 |
<option value="cuda">CUDA</option>
|
| 376 |
</select>
|
| 377 |
</div>
|
| 378 |
+
<div class="form-group">
|
| 379 |
+
<label><input type="checkbox" id="perception-no-sam"> Skip SAM (fallback / NOT a model SUCCESS)</label>
|
| 380 |
+
</div>
|
| 381 |
<button class="btn btn-primary" id="btn-perception-run">Run Auto-Label</button>
|
| 382 |
<div id="perception-status" class="status-box" style="margin-top:16px;"></div>
|
| 383 |
+
<div class="evidence-card" id="evidence-perception" data-stage="perception">
|
| 384 |
+
<h4>What actually happened?</h4>
|
| 385 |
+
<p class="evidence-empty">No auto-label execution yet.</p>
|
| 386 |
+
</div>
|
| 387 |
</div>
|
| 388 |
<div class="card card-glass pipeline-output-browser" style="margin-top:16px;" data-browser="perception">
|
| 389 |
<h3>Pipeline Outputs — proposals & frames</h3>
|
|
|
|
| 419 |
<button class="btn btn-primary" id="btn-tracking-run">Run Temporal Tracker</button>
|
| 420 |
<div id="tracking-status" class="status-box" style="margin-top:16px;"></div>
|
| 421 |
<div id="tracking-tracks-table" style="margin-top:16px;"></div>
|
| 422 |
+
<div class="evidence-card evidence-stub" id="evidence-tracking" data-stage="tracking">
|
| 423 |
+
<h4>What actually happened?</h4>
|
| 424 |
+
<p class="evidence-empty">Ledger wiring pending — status will show NOT_EXECUTED until this stage records evidence.</p>
|
| 425 |
+
</div>
|
| 426 |
</div>
|
| 427 |
</section>
|
| 428 |
|
|
|
|
| 434 |
<button class="btn btn-primary" id="btn-quality-gate-run">Run Quality Gate</button>
|
| 435 |
<div id="quality-gate-status" class="status-box" style="margin-top:16px;"></div>
|
| 436 |
<div id="quality-gate-metrics" style="margin-top:16px;"></div>
|
| 437 |
+
<div class="evidence-card evidence-stub" id="evidence-quality-gate" data-stage="quality-gate">
|
| 438 |
+
<h4>What actually happened?</h4>
|
| 439 |
+
<p class="evidence-empty">Evidence panel stub — gate results are live, full ledger wiring deferred.</p>
|
| 440 |
+
</div>
|
| 441 |
</div>
|
| 442 |
</section>
|
| 443 |
|
|
|
|
| 557 |
<div class="progress-bar-fill" id="train-progress-bar" style="width: 0%"></div>
|
| 558 |
</div>
|
| 559 |
</div>
|
| 560 |
+
<div class="evidence-card" id="evidence-training" data-stage="training">
|
| 561 |
+
<h4>What actually happened?</h4>
|
| 562 |
+
<p class="evidence-empty">No training execution yet.</p>
|
| 563 |
+
</div>
|
| 564 |
</div>
|
| 565 |
|
| 566 |
<div class="card card-glass">
|
|
|
|
| 601 |
<span class="range-val" id="val-infer-iou">0.45</span>
|
| 602 |
</div>
|
| 603 |
<button class="btn btn-primary btn-large" id="btn-run-infer">⚡ Run Auto-Labeler</button>
|
| 604 |
+
<div class="evidence-card" id="evidence-inference" data-stage="inference">
|
| 605 |
+
<h4>What actually happened?</h4>
|
| 606 |
+
<p class="evidence-empty">No inference execution yet.</p>
|
| 607 |
+
</div>
|
| 608 |
</div>
|
| 609 |
|
| 610 |
<div class="card card-glass">
|
|
|
|
| 646 |
</div>
|
| 647 |
</div>
|
| 648 |
<button class="btn btn-primary width-full" id="btn-run-grader" style="margin-top: 15px;">Run Quality Diagnostics</button>
|
| 649 |
+
<div class="evidence-card" id="evidence-grader" data-stage="grader">
|
| 650 |
+
<h4>What actually happened?</h4>
|
| 651 |
+
<p class="evidence-empty">No grader execution yet.</p>
|
| 652 |
+
</div>
|
| 653 |
</div>
|
| 654 |
|
| 655 |
<div class="card card-glass">
|
|
|
|
| 1118 |
</div>
|
| 1119 |
</div>
|
| 1120 |
|
| 1121 |
+
<div id="toast-stack" class="toast-stack" aria-live="polite" aria-relevant="additions"></div>
|
| 1122 |
+
|
| 1123 |
+
<div id="exec-console-modal" class="modal-overlay hidden" role="dialog" aria-label="Execution Console">
|
| 1124 |
+
<div class="modal-card exec-console-card">
|
| 1125 |
+
<div class="modal-header" style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
|
| 1126 |
+
<h3 style="margin:0;">Global Execution Console</h3>
|
| 1127 |
+
<div style="display:flex;gap:8px;">
|
| 1128 |
+
<button type="button" class="btn btn-secondary" id="btn-refresh-executions">Refresh</button>
|
| 1129 |
+
<button type="button" class="btn btn-secondary" id="btn-close-exec-console">Close</button>
|
| 1130 |
+
</div>
|
| 1131 |
+
</div>
|
| 1132 |
+
<p style="font-size:12px;color:var(--text-muted);margin:8px 0 12px;">Persistent ledger under <code>runs/executions/</code>. UI PASS badges are not evidence.</p>
|
| 1133 |
+
<div id="exec-console-list" class="exec-console-list"></div>
|
| 1134 |
+
<div id="exec-console-detail" class="exec-console-detail hidden"></div>
|
| 1135 |
+
</div>
|
| 1136 |
+
</div>
|
| 1137 |
+
|
| 1138 |
<script src="app.js"></script>
|
| 1139 |
</body>
|
| 1140 |
</html>
|
static/style.css
CHANGED
|
@@ -669,6 +669,117 @@ body {
|
|
| 669 |
color: var(--secondary);
|
| 670 |
}
|
| 671 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
/* Animations */
|
| 673 |
@keyframes fadeIn {
|
| 674 |
from { opacity: 0; transform: translateY(8px); }
|
|
@@ -1442,3 +1553,100 @@ body {
|
|
| 1442 |
color: var(--text-muted);
|
| 1443 |
font-size: 10px;
|
| 1444 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
color: var(--secondary);
|
| 670 |
}
|
| 671 |
|
| 672 |
+
.status-msg.success-msg {
|
| 673 |
+
background: rgba(16, 185, 129, 0.12);
|
| 674 |
+
border-color: rgba(16, 185, 129, 0.28);
|
| 675 |
+
color: var(--success);
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
.status-msg.error-msg {
|
| 679 |
+
background: rgba(239, 68, 68, 0.12);
|
| 680 |
+
border-color: rgba(239, 68, 68, 0.28);
|
| 681 |
+
color: var(--danger);
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
/* Non-blocking toast notifications (replaces window.alert) */
|
| 685 |
+
.toast-stack {
|
| 686 |
+
position: fixed;
|
| 687 |
+
bottom: 24px;
|
| 688 |
+
left: 50%;
|
| 689 |
+
transform: translateX(-50%);
|
| 690 |
+
display: flex;
|
| 691 |
+
flex-direction: column-reverse;
|
| 692 |
+
gap: 8px;
|
| 693 |
+
z-index: 2000;
|
| 694 |
+
width: min(420px, calc(100vw - 32px));
|
| 695 |
+
pointer-events: none;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
.toast {
|
| 699 |
+
pointer-events: auto;
|
| 700 |
+
display: flex;
|
| 701 |
+
align-items: flex-start;
|
| 702 |
+
gap: 10px;
|
| 703 |
+
padding: 12px 14px;
|
| 704 |
+
border-radius: 8px;
|
| 705 |
+
background: rgba(15, 20, 34, 0.96);
|
| 706 |
+
border: 1px solid var(--border);
|
| 707 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
| 708 |
+
color: var(--text);
|
| 709 |
+
font-size: 13px;
|
| 710 |
+
line-height: 1.45;
|
| 711 |
+
white-space: pre-line;
|
| 712 |
+
animation: toastIn 0.2s ease;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.toast.toast-out {
|
| 716 |
+
animation: toastOut 0.18s ease forwards;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.toast-icon {
|
| 720 |
+
flex-shrink: 0;
|
| 721 |
+
width: 18px;
|
| 722 |
+
text-align: center;
|
| 723 |
+
font-size: 14px;
|
| 724 |
+
line-height: 1.3;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.toast-body {
|
| 728 |
+
flex: 1;
|
| 729 |
+
min-width: 0;
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
.toast-close {
|
| 733 |
+
flex-shrink: 0;
|
| 734 |
+
background: transparent;
|
| 735 |
+
border: none;
|
| 736 |
+
color: var(--text-muted);
|
| 737 |
+
cursor: pointer;
|
| 738 |
+
font-size: 16px;
|
| 739 |
+
line-height: 1;
|
| 740 |
+
padding: 0 2px;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.toast-close:hover {
|
| 744 |
+
color: var(--text);
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.toast-success {
|
| 748 |
+
border-color: rgba(16, 185, 129, 0.35);
|
| 749 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(16, 185, 129, 0.08);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.toast-success .toast-icon { color: var(--success); }
|
| 753 |
+
|
| 754 |
+
.toast-error {
|
| 755 |
+
border-color: rgba(239, 68, 68, 0.4);
|
| 756 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(239, 68, 68, 0.08);
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.toast-error .toast-icon { color: var(--danger); }
|
| 760 |
+
|
| 761 |
+
.toast-warning {
|
| 762 |
+
border-color: rgba(245, 158, 11, 0.4);
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
.toast-warning .toast-icon { color: var(--warning); }
|
| 766 |
+
|
| 767 |
+
.toast-info {
|
| 768 |
+
border-color: rgba(59, 130, 246, 0.35);
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.toast-info .toast-icon { color: var(--secondary); }
|
| 772 |
+
|
| 773 |
+
@keyframes toastIn {
|
| 774 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 775 |
+
to { opacity: 1; transform: translateY(0); }
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
@keyframes toastOut {
|
| 779 |
+
from { opacity: 1; transform: translateY(0); }
|
| 780 |
+
to { opacity: 0; transform: translateY(8px); }
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
/* Animations */
|
| 784 |
@keyframes fadeIn {
|
| 785 |
from { opacity: 0; transform: translateY(8px); }
|
|
|
|
| 1553 |
color: var(--text-muted);
|
| 1554 |
font-size: 10px;
|
| 1555 |
}
|
| 1556 |
+
|
| 1557 |
+
/* Execution integrity */
|
| 1558 |
+
.strict-mode-toggle {
|
| 1559 |
+
display: inline-flex;
|
| 1560 |
+
align-items: center;
|
| 1561 |
+
gap: 6px;
|
| 1562 |
+
font-size: 12px;
|
| 1563 |
+
color: var(--text-muted);
|
| 1564 |
+
cursor: pointer;
|
| 1565 |
+
user-select: none;
|
| 1566 |
+
}
|
| 1567 |
+
.evidence-card {
|
| 1568 |
+
margin-top: 16px;
|
| 1569 |
+
padding: 14px;
|
| 1570 |
+
border: 1px solid var(--border);
|
| 1571 |
+
border-radius: 8px;
|
| 1572 |
+
background: rgba(4, 6, 11, 0.55);
|
| 1573 |
+
}
|
| 1574 |
+
.evidence-card h4 {
|
| 1575 |
+
margin: 0 0 10px 0;
|
| 1576 |
+
font-size: 13px;
|
| 1577 |
+
color: var(--secondary);
|
| 1578 |
+
}
|
| 1579 |
+
.evidence-empty {
|
| 1580 |
+
font-size: 12px;
|
| 1581 |
+
color: var(--text-muted);
|
| 1582 |
+
margin: 0;
|
| 1583 |
+
}
|
| 1584 |
+
.ev-status {
|
| 1585 |
+
display: inline-block;
|
| 1586 |
+
font-size: 11px;
|
| 1587 |
+
font-weight: 700;
|
| 1588 |
+
letter-spacing: 0.4px;
|
| 1589 |
+
padding: 3px 8px;
|
| 1590 |
+
border-radius: 4px;
|
| 1591 |
+
margin-bottom: 10px;
|
| 1592 |
+
font-family: var(--font-mono);
|
| 1593 |
+
}
|
| 1594 |
+
.ev-status-SUCCEEDED { background: #00ffaa22; color: #00ffaa; }
|
| 1595 |
+
.ev-status-PARTIAL_SUCCESS { background: #fbbf2422; color: #fbbf24; }
|
| 1596 |
+
.ev-status-FAILED, .ev-status-VALIDATION_FAILED { background: #ff444422; color: #ff8888; }
|
| 1597 |
+
.ev-status-RUNNING, .ev-status-QUEUED { background: #3b82f622; color: #93c5fd; }
|
| 1598 |
+
.ev-status-NOT_EXECUTED, .ev-status-UNVERIFIED, .ev-status-CANCELLED { background: #64748b33; color: #94a3b8; }
|
| 1599 |
+
.ev-row {
|
| 1600 |
+
display: flex;
|
| 1601 |
+
justify-content: space-between;
|
| 1602 |
+
gap: 12px;
|
| 1603 |
+
font-size: 12px;
|
| 1604 |
+
padding: 3px 0;
|
| 1605 |
+
border-bottom: 1px solid rgba(255,255,255,0.04);
|
| 1606 |
+
}
|
| 1607 |
+
.ev-row span { color: var(--text-muted); }
|
| 1608 |
+
.ev-row strong { color: var(--text); text-align: right; word-break: break-all; }
|
| 1609 |
+
.ev-msg { font-size: 12px; color: var(--text-muted); margin: 10px 0 0; }
|
| 1610 |
+
.ev-next { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
|
| 1611 |
+
.ev-next ul { margin: 6px 0 0 18px; padding: 0; }
|
| 1612 |
+
.ev-next li { margin-bottom: 4px; }
|
| 1613 |
+
.ev-log-link { display: inline-block; margin-top: 10px; font-size: 12px; color: #38bdf8; }
|
| 1614 |
+
.ev-timeline { margin-top: 10px; font-size: 11px; color: var(--text-muted); }
|
| 1615 |
+
.ev-timeline ul { margin: 6px 0 0 16px; padding: 0; }
|
| 1616 |
+
.exec-console-card {
|
| 1617 |
+
width: min(920px, 94vw);
|
| 1618 |
+
max-height: 85vh;
|
| 1619 |
+
overflow: auto;
|
| 1620 |
+
background: var(--bg-sidebar);
|
| 1621 |
+
border: 1px solid var(--border);
|
| 1622 |
+
border-radius: 12px;
|
| 1623 |
+
padding: 18px;
|
| 1624 |
+
}
|
| 1625 |
+
.exec-console-list { display: flex; flex-direction: column; gap: 6px; }
|
| 1626 |
+
.exec-row {
|
| 1627 |
+
display: grid;
|
| 1628 |
+
grid-template-columns: 140px 160px 1fr 80px 90px;
|
| 1629 |
+
gap: 8px;
|
| 1630 |
+
align-items: center;
|
| 1631 |
+
text-align: left;
|
| 1632 |
+
padding: 8px 10px;
|
| 1633 |
+
background: rgba(4,6,11,0.5);
|
| 1634 |
+
border: 1px solid var(--border);
|
| 1635 |
+
border-radius: 6px;
|
| 1636 |
+
color: var(--text);
|
| 1637 |
+
cursor: pointer;
|
| 1638 |
+
font-size: 12px;
|
| 1639 |
+
}
|
| 1640 |
+
.exec-row:hover { border-color: var(--border-active); }
|
| 1641 |
+
.exec-console-detail pre {
|
| 1642 |
+
margin-top: 12px;
|
| 1643 |
+
max-height: 320px;
|
| 1644 |
+
overflow: auto;
|
| 1645 |
+
background: #04060b;
|
| 1646 |
+
border: 1px solid var(--border);
|
| 1647 |
+
border-radius: 8px;
|
| 1648 |
+
padding: 12px;
|
| 1649 |
+
font-size: 11px;
|
| 1650 |
+
color: #00ffaa;
|
| 1651 |
+
}
|
| 1652 |
+
.config-exec-status { font-family: var(--font-mono); }
|
tests/test_pipeline/test_auto_label_integrity.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Auto-label pre-checks, status semantics, and UI-facing error payloads."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
from fastapi.testclient import TestClient
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 13 |
+
if str(ROOT) not in sys.path:
|
| 14 |
+
sys.path.insert(0, str(ROOT))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _write_tiny_jpeg(path: Path) -> None:
|
| 18 |
+
path.write_bytes(
|
| 19 |
+
bytes.fromhex(
|
| 20 |
+
"ffd8ffe000104a46494600010100000100010000ffdb004300080606070605080707"
|
| 21 |
+
"070909080a0c140d0c0b0b0c1912130f141d1a1f1e1d1a1c1c20242e2720222c231c"
|
| 22 |
+
"1c2837292c30313434341f27393d38323c2e333432ffdb0043010909090c0b0c180d"
|
| 23 |
+
"0d1832211c2132323232323232323232323232323232323232323232323232323232"
|
| 24 |
+
"323232323232323232323232323232323232323232ffc00011080001000103011100"
|
| 25 |
+
"021101031101ffc40014000100000000000000000000000000000000ffc400141001"
|
| 26 |
+
"00000000000000000000000000000000ffda000c0301000210031000003f00bf80ffd9"
|
| 27 |
+
)
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.fixture()
|
| 32 |
+
def client(tmp_path, monkeypatch):
|
| 33 |
+
monkeypatch.chdir(tmp_path)
|
| 34 |
+
from sensorflow import execution_ledger as ledger
|
| 35 |
+
|
| 36 |
+
monkeypatch.setattr(ledger, "LEDGER_ROOT", tmp_path / "runs" / "executions")
|
| 37 |
+
monkeypatch.setattr(ledger, "INDEX_PATH", tmp_path / "runs" / "executions" / "index.json")
|
| 38 |
+
monkeypatch.setattr(ledger, "STRICT_MODE_PATH", tmp_path / "runs" / "studio_strict_mode.json")
|
| 39 |
+
|
| 40 |
+
import importlib
|
| 41 |
+
import app_backend
|
| 42 |
+
|
| 43 |
+
importlib.reload(app_backend)
|
| 44 |
+
return TestClient(app_backend.app)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _ingest_local(client: TestClient, tmp_path: Path, seq_id: str = "seq_local", n: int = 5):
|
| 48 |
+
frames_dir = tmp_path / "frames"
|
| 49 |
+
frames_dir.mkdir()
|
| 50 |
+
for i in range(n):
|
| 51 |
+
_write_tiny_jpeg(frames_dir / f"f{i}.jpg")
|
| 52 |
+
res = client.post(
|
| 53 |
+
"/api/dataset/ingest",
|
| 54 |
+
json={
|
| 55 |
+
"vendors": ["local"],
|
| 56 |
+
"sequence_id": seq_id,
|
| 57 |
+
"source_path": str(frames_dir),
|
| 58 |
+
"max_frames": 10000,
|
| 59 |
+
},
|
| 60 |
+
)
|
| 61 |
+
assert res.status_code == 200
|
| 62 |
+
assert res.json()["frames"] == n
|
| 63 |
+
return res
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_missing_checkpoint_returns_failed_not_partial(client, tmp_path):
|
| 67 |
+
_ingest_local(client, tmp_path, "seq_ckpt", 3)
|
| 68 |
+
res = client.post(
|
| 69 |
+
"/api/perception/auto-label",
|
| 70 |
+
json={
|
| 71 |
+
"sequence_id": "seq_ckpt",
|
| 72 |
+
"sam_checkpoint": str(tmp_path / "missing_sam.pth"),
|
| 73 |
+
"no_sam": False,
|
| 74 |
+
},
|
| 75 |
+
)
|
| 76 |
+
assert res.status_code == 400
|
| 77 |
+
detail = res.json()["detail"]
|
| 78 |
+
assert detail["status"] == "FAILED"
|
| 79 |
+
assert detail["checkpoint"]["exists"] is False
|
| 80 |
+
assert detail["frames_expected"] == 3
|
| 81 |
+
assert detail["frames_processed"] == 0
|
| 82 |
+
assert detail.get("next_steps")
|
| 83 |
+
assert "FAILED" == detail["status"]
|
| 84 |
+
assert "PARTIAL_SUCCESS" != detail["status"]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def test_empty_manifest_returns_failed(client, tmp_path):
|
| 88 |
+
manifest_dir = tmp_path / "runs" / "pipeline" / "seq_empty"
|
| 89 |
+
manifest_dir.mkdir(parents=True)
|
| 90 |
+
(manifest_dir / "manifest.json").write_text(
|
| 91 |
+
json.dumps(
|
| 92 |
+
{
|
| 93 |
+
"sequence_id": "seq_empty",
|
| 94 |
+
"vendor": "local",
|
| 95 |
+
"frames": [],
|
| 96 |
+
"taxonomy_manifest": {"demo_stub": False},
|
| 97 |
+
}
|
| 98 |
+
)
|
| 99 |
+
)
|
| 100 |
+
res = client.post(
|
| 101 |
+
"/api/perception/auto-label",
|
| 102 |
+
json={"sequence_id": "seq_empty", "no_sam": True},
|
| 103 |
+
)
|
| 104 |
+
assert res.status_code == 400
|
| 105 |
+
detail = res.json()["detail"]
|
| 106 |
+
assert detail["status"] == "FAILED"
|
| 107 |
+
assert detail["frames_expected"] == 0
|
| 108 |
+
assert "ingest" in detail["message"].lower() or "0 ingested" in detail["message"].lower()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def test_ingest_then_no_sam_auto_label_has_frames(client, tmp_path):
|
| 112 |
+
_ingest_local(client, tmp_path, "seq_ok", 4)
|
| 113 |
+
res = client.post(
|
| 114 |
+
"/api/perception/auto-label",
|
| 115 |
+
json={"sequence_id": "seq_ok", "no_sam": True},
|
| 116 |
+
)
|
| 117 |
+
assert res.status_code == 200
|
| 118 |
+
body = res.json()
|
| 119 |
+
assert body["frames_expected"] == 4
|
| 120 |
+
assert body["frames_processed"] == 4
|
| 121 |
+
assert body["status"] == "NOT_EXECUTED"
|
| 122 |
+
assert body["sam_ran"] is False
|
| 123 |
+
assert isinstance(body.get("message"), str)
|
| 124 |
+
assert "{" not in body["message"][:20]
|