delete me

This commit is contained in:
DanielS
2026-05-29 14:14:04 +02:00
parent bd1bd52c6c
commit 5462784c1f
621 changed files with 2047 additions and 750 deletions

View File

@@ -36,10 +36,17 @@ let isWebcamActive = false;
let webcamStream = null;
let simulatedStreamInterval = null;
let currentFrameBase64 = null;
let activeCameraDeviceId = null; // stores selected camera device ID
let isDetecting = false;
let autoDetectEnabled = true;
let trainingPollInterval = null;
// Learn/Anlern-Modus State
let learnBackgroundFrame = null; // stores ImageData or Image object for canvas subtraction
let learnSelectedClass = null; // selected article key for training
let learnSessionImages = []; // list of filenames in the current session
let isSerialCapturing = false;
// Scanner Emulator State
let autoScanEnabled = true;
let stableClass = null;
@@ -56,6 +63,9 @@ let shoppingCart = [];
const videoEl = document.getElementById("webcam");
const canvasEl = document.getElementById("camera-canvas");
const ctx = canvasEl.getContext("2d");
const learnCanvasEl = document.getElementById("learn-camera-canvas");
const learnCtx = learnCanvasEl ? learnCanvasEl.getContext("2d") : null;
const toggleCameraBtn = document.getElementById("toggle-camera-btn");
const triggerScaleBtn = document.getElementById("trigger-scale-btn");
const autoDetectToggle = document.getElementById("auto-detect-toggle");
@@ -86,19 +96,121 @@ function switchTab(tabName) {
if (tabName === 'articles') {
loadArticlesDatabase();
}
if (tabName === 'learn') {
checkBackgroundCalibration();
loadLearnArticles();
updateLearnBadge();
}
}
// --- CAMERA & DETECTION LOGIC ---
async function startWebcam() {
async function initCameraSelection() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
const selectCheckout = document.getElementById("camera-select");
const selectLearn = document.getElementById("learn-camera-select");
if (selectCheckout && selectLearn) {
selectCheckout.innerHTML = "";
selectLearn.innerHTML = "";
if (videoDevices.length === 0) {
const opt = document.createElement("option");
opt.value = "";
opt.textContent = "Keine Kamera gefunden";
selectCheckout.appendChild(opt);
selectLearn.appendChild(opt.cloneNode(true));
return;
}
videoDevices.forEach((device, idx) => {
const label = device.label || `Kamera ${idx + 1}`;
const opt = document.createElement("option");
opt.value = device.deviceId;
opt.textContent = label;
selectCheckout.appendChild(opt);
selectLearn.appendChild(opt.cloneNode(true));
});
// Sync values to current active camera
if (activeCameraDeviceId) {
selectCheckout.value = activeCameraDeviceId;
selectLearn.value = activeCameraDeviceId;
} else {
activeCameraDeviceId = videoDevices[0].deviceId;
selectCheckout.value = activeCameraDeviceId;
selectLearn.value = activeCameraDeviceId;
}
// Hook up change handlers
selectCheckout.onchange = (e) => switchCameraDevice(e.target.value);
selectLearn.onchange = (e) => switchCameraDevice(e.target.value);
}
} catch (err) {
console.error("Fehler beim Abrufen der Kameras:", err);
}
}
async function switchCameraDevice(deviceId) {
activeCameraDeviceId = deviceId;
// Sync dropdown values across tabs
const selectCheckout = document.getElementById("camera-select");
const selectLearn = document.getElementById("learn-camera-select");
if (selectCheckout) selectCheckout.value = deviceId;
if (selectLearn) selectLearn.value = deviceId;
if (isWebcamActive) {
// Stop current tracks and restart webcam stream with the new device
if (webcamStream) {
webcamStream.getTracks().forEach(track => track.stop());
webcamStream = null;
}
await startWebcam(deviceId);
}
}
async function startWebcam(deviceId = null) {
try {
const targetDeviceId = deviceId || activeCameraDeviceId;
const videoConstraints = {
width: 640,
height: 480
};
if (targetDeviceId) {
videoConstraints.deviceId = { exact: targetDeviceId };
} else {
videoConstraints.facingMode = "environment";
}
webcamStream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480, facingMode: "environment" }
video: videoConstraints
});
videoEl.srcObject = webcamStream;
videoEl.style.display = "none";
isWebcamActive = true;
// Show camera selector dropdowns
const selectCheckout = document.getElementById("camera-select");
const selectLearn = document.getElementById("learn-camera-select");
if (selectCheckout) selectCheckout.style.display = "inline-block";
if (selectLearn) selectLearn.style.display = "inline-block";
cameraTypeBadge.textContent = "LIVE-WEBCAM";
cameraTypeBadge.style.background = "var(--color-primary)";
const learnStreamBadge = document.getElementById("learn-camera-type-badge");
if (learnStreamBadge) {
learnStreamBadge.textContent = "LIVE-WEBCAM";
learnStreamBadge.style.background = "var(--color-primary)";
}
toggleCameraBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/></svg>
Kamera deaktivieren
@@ -106,6 +218,9 @@ async function startWebcam() {
document.getElementById("stream-status").textContent = "LIVE";
document.getElementById("stream-status").classList.add("live");
// Initialize options list
await initCameraSelection();
// Stop simulated stream if active
if (simulatedStreamInterval) {
clearInterval(simulatedStreamInterval);
@@ -128,25 +243,42 @@ function stopWebcam() {
videoEl.srcObject = null;
videoEl.style.display = "none";
isWebcamActive = false;
// Hide camera selectors when webcam is inactive
const selectCheckout = document.getElementById("camera-select");
const selectLearn = document.getElementById("learn-camera-select");
if (selectCheckout) selectCheckout.style.display = "none";
if (selectLearn) selectLearn.style.display = "none";
toggleCameraBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
Webcam aktivieren
`;
startSimulatedStream();
}
function processWebcamFrame() {
if (!isWebcamActive) return;
// Draw current video frame to canvas
ctx.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
// Get frame as base64
currentFrameBase64 = canvasEl.toDataURL("image/jpeg", 0.7);
// Auto-detect if enabled
if (autoDetectEnabled && !isDetecting) {
runDetection();
if (activeTab === 'checkout') {
// Draw current video frame to canvas
ctx.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
// Get frame as base64
currentFrameBase64 = canvasEl.toDataURL("image/jpeg", 0.7);
// Auto-detect if enabled
if (autoDetectEnabled && !isDetecting) {
runDetection();
}
} else if (activeTab === 'learn') {
if (learnCanvasEl && learnCtx) {
// Draw current video frame to learn canvas
learnCtx.drawImage(videoEl, 0, 0, learnCanvasEl.width, learnCanvasEl.height);
// Get frame as base64
currentFrameBase64 = learnCanvasEl.toDataURL("image/jpeg", 0.7);
// Draw dynamic green bounding box
drawLearnFrameGreenBox();
}
}
setTimeout(() => {
@@ -155,10 +287,22 @@ function processWebcamFrame() {
}
function startSimulatedStream() {
cameraTypeBadge.textContent = "SIMULATIONS-MODUS";
cameraTypeBadge.style.background = "var(--color-bg-dark)";
document.getElementById("stream-status").textContent = "Simuliert";
document.getElementById("stream-status").classList.add("live");
const streamBadge = document.getElementById("camera-type-badge");
const learnStreamBadge = document.getElementById("learn-camera-type-badge");
if (streamBadge) {
streamBadge.textContent = "SIMULATIONS-MODUS";
streamBadge.style.background = "var(--color-bg-dark)";
}
if (learnStreamBadge) {
learnStreamBadge.textContent = "SIMULATIONS-MODUS";
learnStreamBadge.style.background = "var(--color-bg-dark)";
}
const streamStatus = document.getElementById("stream-status");
if (streamStatus) {
streamStatus.textContent = "Simuliert";
streamStatus.classList.add("live");
}
const loadSimulatedFrame = async () => {
try {
@@ -167,11 +311,21 @@ function startSimulatedStream() {
if (data.status === "success" && data.image) {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, canvasEl.width, canvasEl.height);
currentFrameBase64 = data.image;
if (autoDetectEnabled && !isDetecting) {
runDetection();
if (activeTab === 'checkout') {
ctx.drawImage(img, 0, 0, canvasEl.width, canvasEl.height);
currentFrameBase64 = data.image;
if (autoDetectEnabled && !isDetecting) {
runDetection();
}
} else if (activeTab === 'learn') {
if (learnCanvasEl && learnCtx) {
learnCtx.drawImage(img, 0, 0, learnCanvasEl.width, learnCanvasEl.height);
currentFrameBase64 = data.image;
// Draw dynamic green bounding box
drawLearnFrameGreenBox();
}
}
};
img.src = data.image;
@@ -248,14 +402,14 @@ function drawDetections(predictions) {
function updateQuickButtons(predictions) {
quickSelectButtons.innerHTML = "";
// Filter predictions with confidence > 0.10
const candidates = predictions.filter(p => p.confidence > 0.10);
// Filter predictions with confidence > 0.85
const candidates = predictions.filter(p => p.confidence > 0.85);
if (candidates.length === 0) {
quickSelectButtons.innerHTML = `
<div class="empty-state">
<p>Keine Objekte erkannt</p>
<span>Confidence Schwellenwert unter 10%</span>
<span>Confidence Schwellenwert unter 85%</span>
</div>
`;
return;
@@ -446,6 +600,23 @@ async function startTraining() {
}
}
// Cancel active model training
async function cancelTraining() {
const cancelBtn = document.getElementById("cancel-training-btn");
if (cancelBtn) cancelBtn.disabled = true;
try {
const response = await fetch("/api/cancel_training", { method: "POST" });
const data = await response.json();
alert(data.message);
} catch (err) {
console.error("Failed to cancel training:", err);
alert("Fehler beim Abbrechen des Trainings.");
} finally {
if (cancelBtn) cancelBtn.disabled = false;
}
}
// Poll training API
function startTrainingPolling() {
if (trainingPollInterval) return;
@@ -497,6 +668,19 @@ function updateTrainingStatusUI(data) {
statusBox.classList.add("active-pulse");
}
// Toggle start and cancel buttons based on status
const startBtn = document.getElementById("start-training-btn");
const cancelBtn = document.getElementById("cancel-training-btn");
if (startBtn && cancelBtn) {
if (data.status === "training") {
startBtn.style.display = "none";
cancelBtn.style.display = "inline-block";
} else {
startBtn.style.display = "inline-block";
cancelBtn.style.display = "none";
}
}
// Plot Chart if loss history is available
if (data.train_loss && data.train_loss.length > 0) {
updateChart(data.train_loss, data.val_loss || []);
@@ -900,7 +1084,7 @@ async function processStabilityAndScan(predictions) {
const progressWrapper = document.getElementById("stability-progress-wrapper");
const progressFill = document.getElementById("stability-progress-fill");
const STABLE_CONF_THRESHOLD = 0.75;
const STABLE_CONF_THRESHOLD = 0.85;
if (topPrediction && topPrediction.confidence >= STABLE_CONF_THRESHOLD) {
const detectedClass = topPrediction.class;
@@ -986,6 +1170,518 @@ async function triggerManualScan() {
}
}
// --- LEARN ARTICLE TAB (ANLERN-MODUS) LOGIC ---
let learnCurrentBBox = null; // stores {xmin, ymin, xmax, ymax}
function computeBBoxFromDiff(currData, bgData, width, height, threshold = 25) {
let minX = width, minY = height, maxX = 0, maxY = 0;
let changedCount = 0;
for (let y = 0; y < height; y += 4) { // sample every 4th pixel for speed
for (let x = 0; x < width; x += 4) {
const idx = (y * width + x) * 4;
const rDiff = Math.abs(currData[idx] - bgData[idx]);
const gDiff = Math.abs(currData[idx+1] - bgData[idx+1]);
const bDiff = Math.abs(currData[idx+2] - bgData[idx+2]);
if (rDiff + gDiff + bDiff > threshold * 3) {
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
changedCount++;
}
}
}
// If too few pixels changed, it's probably noise or empty
if (changedCount < 100) return null;
// Add padding
const padding = 15;
minX = Math.max(0, minX - padding);
minY = Math.max(0, minY - padding);
maxX = Math.min(width, maxX + padding);
maxY = Math.min(height, maxY + padding);
return { xmin: minX, ymin: minY, xmax: maxX, ymax: maxY };
}
function drawLearnFrameGreenBox() {
if (!learnCanvasEl || !learnCtx || !currentFrameBase64) return;
if (learnBackgroundFrame) {
try {
const width = learnCanvasEl.width;
const height = learnCanvasEl.height;
// Get current frame image data
const currImgData = learnCtx.getImageData(0, 0, width, height);
// Calculate bbox
const bbox = computeBBoxFromDiff(currImgData.data, learnBackgroundFrame.data, width, height);
learnCurrentBBox = bbox;
if (bbox) {
// Draw green bounding box outline
learnCtx.lineWidth = 3;
learnCtx.strokeStyle = "#10b981"; // Green
learnCtx.strokeRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin);
// Draw label box background
learnCtx.fillStyle = "rgba(16, 185, 129, 0.85)";
const labelText = "Ware erkannt (Auto-Crop)";
learnCtx.font = "bold 12px sans-serif";
const textWidth = learnCtx.measureText(labelText).width;
learnCtx.fillRect(bbox.xmin - 1, bbox.ymin - 22, textWidth + 12, 22);
// Label text
learnCtx.fillStyle = "#ffffff";
learnCtx.fillText(labelText, bbox.xmin + 5, bbox.ymin - 6);
}
} catch (e) {
console.error("Error drawing green box:", e);
}
} else {
learnCurrentBBox = null;
}
}
async function calibrateBackground() {
if (!currentFrameBase64) {
alert("Warte auf Kamerabild...");
return;
}
try {
const response = await fetch("/api/calibrate_background", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: currentFrameBase64 })
});
const data = await response.json();
if (data.status === "success") {
// Save background frame locally as ImageData for real-time diffing
const width = learnCanvasEl.width;
const height = learnCanvasEl.height;
learnBackgroundFrame = learnCtx.getImageData(0, 0, width, height);
// Update UI indicators
const statusIndicator = document.getElementById("learn-calibration-indicator");
if (statusIndicator) {
statusIndicator.textContent = "Kalibriert";
statusIndicator.style.color = "var(--color-success)";
statusIndicator.classList.add("live");
}
const checkbox = document.getElementById("learn-bg-ok-indicator");
if (checkbox) {
checkbox.checked = true;
}
alert(data.message);
} else {
alert("Kalibrierung fehlgeschlagen: " + data.message);
}
} catch (err) {
console.error("Calibration error:", err);
alert("Serverfehler bei Kalibrierung.");
}
}
async function checkBackgroundCalibration() {
try {
const response = await fetch("/api/has_background");
const data = await response.json();
const statusIndicator = document.getElementById("learn-calibration-indicator");
const checkbox = document.getElementById("learn-bg-ok-indicator");
if (data.has_background) {
if (statusIndicator) {
statusIndicator.textContent = "Kalibriert";
statusIndicator.style.color = "var(--color-success)";
statusIndicator.classList.add("live");
}
if (checkbox) {
checkbox.checked = true;
}
// Load background image from backend and draw it on a hidden canvas to get ImageData
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const hiddenCanvas = document.createElement("canvas");
hiddenCanvas.width = learnCanvasEl.width;
hiddenCanvas.height = learnCanvasEl.height;
const hiddenCtx = hiddenCanvas.getContext("2d");
hiddenCtx.drawImage(img, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
learnBackgroundFrame = hiddenCtx.getImageData(0, 0, hiddenCanvas.width, hiddenCanvas.height);
};
img.src = "/api/background_image?t=" + Date.now(); // Cache busting
} else {
learnBackgroundFrame = null;
if (statusIndicator) {
statusIndicator.textContent = "Nicht kalibriert";
statusIndicator.style.color = "var(--text-muted)";
statusIndicator.classList.remove("live");
}
if (checkbox) {
checkbox.checked = false;
}
}
} catch (err) {
console.error("Failed to check background calibration:", err);
}
}
async function resetBackgroundCalibration() {
if (!confirm("Hintergrundkalibrierung wirklich zurücksetzen?")) return;
try {
const response = await fetch("/api/reset_background", { method: "POST" });
const data = await response.json();
if (data.status === "success") {
learnBackgroundFrame = null;
const statusIndicator = document.getElementById("learn-calibration-indicator");
if (statusIndicator) {
statusIndicator.textContent = "Nicht kalibriert";
statusIndicator.style.color = "var(--text-muted)";
statusIndicator.classList.remove("live");
}
const checkbox = document.getElementById("learn-bg-ok-indicator");
if (checkbox) {
checkbox.checked = false;
}
alert(data.message);
}
} catch (err) {
console.error("Reset calibration error:", err);
}
}
function loadLearnArticles() {
const listContainer = document.getElementById("learn-articles-list");
if (!listContainer) return;
listContainer.innerHTML = "";
// Check if database loaded, if not, wait
if (Object.keys(articlesDatabase).length === 0) {
setTimeout(loadLearnArticles, 200);
return;
}
Object.keys(articlesDatabase).forEach(key => {
const art = articlesDatabase[key];
const item = document.createElement("div");
item.className = "learn-article-item";
item.id = `learn-item-${key}`;
if (learnSelectedClass === key) {
item.classList.add("selected");
}
item.onclick = () => selectLearnArticle(key);
item.innerHTML = `
<span>${art.name}</span>
<span class="item-plu">EAN: ${art.ean || 'Keine'}</span>
`;
listContainer.appendChild(item);
});
}
function filterLearnArticles() {
const query = document.getElementById("learn-search-input").value.toLowerCase().trim();
const items = document.querySelectorAll(".learn-article-item");
items.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(query)) {
item.style.display = "flex";
} else {
item.style.display = "none";
}
});
}
function selectLearnArticle(classKey) {
learnSelectedClass = classKey;
document.querySelectorAll(".learn-article-item").forEach(el => el.classList.remove("selected"));
const selectedEl = document.getElementById(`learn-item-${classKey}`);
if (selectedEl) {
selectedEl.classList.add("selected");
}
const display = document.getElementById("learn-selected-article-display");
if (display) {
const name = articlesDatabase[classKey]?.name || classKey;
display.textContent = name;
}
}
function updateLearnBadge() {
fetch("/api/capture_count")
.then(r => r.json())
.then(data => {
const badge = document.getElementById("learn-session-count-badge");
if (badge) badge.textContent = `Eigene Bilder Gesamt: ${data.count}`;
const displayCheckout = document.getElementById("capture-count-display");
if (displayCheckout) displayCheckout.textContent = `Eigene Bilder: ${data.count}`;
})
.catch(err => console.error(err));
}
async function startSerialCapture() {
if (!learnSelectedClass) {
alert("Bitte wählen Sie zuerst einen Artikel aus der Liste aus.");
return;
}
if (!learnBackgroundFrame) {
alert("Bitte führen Sie zuerst die Hintergrund-Kalibrierung durch (Hintergrund speichern).");
return;
}
if (isSerialCapturing) return;
isSerialCapturing = true;
// Clear session gallery UI and list
const galleryGrid = document.getElementById("learn-gallery-grid");
galleryGrid.innerHTML = "";
learnSessionImages = [];
const startBtn = document.getElementById("learn-start-capture-btn");
startBtn.disabled = true;
startBtn.innerHTML = `<span>Aufnahme läuft...</span>`;
const progressText = document.getElementById("learn-progress-text");
const progressFill = document.getElementById("learn-progress-fill");
let count = 0;
const total = 10;
const captureNext = async () => {
// Sound feedback
playScannerBeep();
// Capture frame
const frameData = learnCanvasEl.toDataURL("image/jpeg", 0.95);
const currentBbox = learnCurrentBBox ? { ...learnCurrentBBox } : null;
try {
const response = await fetch("/api/capture_training_image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
image: frameData,
class_name: learnSelectedClass,
bbox: currentBbox
})
});
const data = await response.json();
if (data.status === "success" && data.filename) {
learnSessionImages.push(data.filename);
addPhotoToGallery(data.filename);
}
} catch (err) {
console.error("Serial capture step failed:", err);
}
count++;
if (progressText) progressText.textContent = `${count} / ${total} Bilder`;
if (progressFill) progressFill.style.width = `${(count / total) * 100}%`;
if (count >= total) {
clearInterval(captureInterval);
isSerialCapturing = false;
startBtn.disabled = false;
startBtn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>
START (SERIENAUFNAHME)
`;
updateLearnBadge();
alert("Serienaufnahme abgeschlossen! Sie können fehlerhafte Bilder unten in der Galerie löschen.");
}
};
// Start interval
await captureNext(); // First capture immediately
const captureInterval = setInterval(captureNext, 1000);
}
function addPhotoToGallery(filename) {
const galleryGrid = document.getElementById("learn-gallery-grid");
const emptyState = document.getElementById("learn-gallery-empty");
if (emptyState) emptyState.remove();
const wrapper = document.createElement("div");
wrapper.className = "gallery-item-wrapper";
wrapper.id = `gallery-item-${filename.replace(".", "_")}`;
wrapper.innerHTML = `
<img class="gallery-item-img" src="/api/captured_images/${filename}" alt="${filename}">
<button class="gallery-item-delete-btn" onclick="deleteSessionImage('${filename}')" title="Bild löschen">X</button>
`;
galleryGrid.appendChild(wrapper);
}
async function deleteSessionImage(filename) {
if (!confirm("Dieses Bild aus der Session löschen?")) return;
try {
const response = await fetch("/api/delete_captured_image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: filename })
});
const data = await response.json();
if (data.status === "success") {
// Remove from UI
const wrapper = document.getElementById(`gallery-item-${filename.replace(".", "_")}`);
if (wrapper) wrapper.remove();
// Remove from list
learnSessionImages = learnSessionImages.filter(f => f !== filename);
// If empty, show empty state
const galleryGrid = document.getElementById("learn-gallery-grid");
if (galleryGrid && galleryGrid.children.length === 0) {
galleryGrid.innerHTML = `
<div class="empty-state" id="learn-gallery-empty" style="grid-column: 1 / -1; padding: 40px 0;">
<p>Noch keine Aufnahmen in dieser Session</p>
<span>Wähle ein Produkt und starte die Serienaufnahme</span>
</div>
`;
}
updateLearnBadge();
console.log(data.message);
} else {
alert("Löschen fehlgeschlagen: " + data.message);
}
} catch (err) {
console.error("Delete image error:", err);
alert("Serverfehler beim Löschen des Bildes.");
}
}
// --- RESET ALL CAPTURED TRAINING DATA ---
async function resetAllCapturedData() {
const confirmed = confirm(
"⚠️ Alle selbst erfassten Trainingsbilder löschen?\n\n" +
"Dies entfernt alle user_capture_* Bilder und Labels sowie die Hintergrundkalibrierung.\n" +
"Das vortrainierte Basis-Modell bleibt erhalten.\n\n" +
"Dieser Vorgang kann NICHT rückgängig gemacht werden!"
);
if (!confirmed) return;
const resetBtnLearn = document.getElementById("reset-all-captures-btn");
const resetBtnDash = document.getElementById("dashboard-reset-btn");
if (resetBtnLearn) { resetBtnLearn.disabled = true; resetBtnLearn.textContent = "Wird gelöscht..."; }
if (resetBtnDash) { resetBtnDash.disabled = true; resetBtnDash.textContent = "Wird gelöscht..."; }
try {
const response = await fetch("/api/reset_dataset", { method: "POST" });
const data = await response.json();
if (data.status === "success") {
// 1. Clear gallery UI
const galleryGrid = document.getElementById("learn-gallery-grid");
if (galleryGrid) {
galleryGrid.innerHTML = `
<div class="empty-state" id="learn-gallery-empty" style="grid-column: 1 / -1; padding: 40px 0;">
<p>Noch keine Aufnahmen in dieser Session</p>
<span>Wähle ein Produkt und starte die Serienaufnahme</span>
</div>
`;
}
learnSessionImages = [];
// 2. Reset background calibration state
learnBackgroundFrame = null;
learnCurrentBBox = null;
const statusIndicator = document.getElementById("learn-calibration-indicator");
if (statusIndicator) {
statusIndicator.textContent = "Nicht kalibriert";
statusIndicator.style.color = "var(--text-muted)";
statusIndicator.classList.remove("live");
}
const checkbox = document.getElementById("learn-bg-ok-indicator");
if (checkbox) checkbox.checked = false;
// 3. Update count badges
const badge = document.getElementById("learn-session-count-badge");
if (badge) badge.textContent = "Eigene Bilder Gesamt: 0";
const countDisplay = document.getElementById("capture-count-display");
if (countDisplay) countDisplay.textContent = "Eigene Bilder: 0";
alert("✅ " + data.message);
} else {
alert("Fehler beim Zurücksetzen: " + (data.detail || data.message));
}
} catch (err) {
console.error("Reset error:", err);
alert("Serverfehler beim Zurücksetzen der Trainingsdaten.");
} finally {
if (resetBtnLearn) {
resetBtnLearn.disabled = false;
resetBtnLearn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Alle Trainingsbilder löschen
`;
}
if (resetBtnDash) {
resetBtnDash.disabled = false;
resetBtnDash.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Eigene Bilder & Kalibrierung zurücksetzen
`;
}
}
}
// --- RESET TRAINED AI MODEL ---
async function resetAIModel() {
const confirmed = confirm(
"⚠️ Trainiertes KI-Modell wirklich zurücksetzen?\n\n" +
"Dies löscht das trainierte PyTorch- und ONNX-Modell vom Server.\n" +
"Das System läuft danach im Simulations-Modus, bis Sie ein neues Modell trainieren.\n\n" +
"Dieser Vorgang kann NICHT rückgängig gemacht werden!"
);
if (!confirmed) return;
const resetBtn = document.getElementById("dashboard-reset-model-btn");
if (resetBtn) {
resetBtn.disabled = true;
resetBtn.textContent = "Zurücksetzen...";
}
try {
const response = await fetch("/api/reset_model", { method: "POST" });
const data = await response.json();
if (response.ok && data.status === "success") {
alert("✅ " + data.message);
// Refresh dashboard training status if polling is active
if (activeTab === 'dashboard') {
const statusResponse = await fetch("/api/train_status");
const statusData = await statusResponse.json();
updateTrainingStatusUI(statusData);
}
} else {
alert("Fehler beim Zurücksetzen des Modells: " + (data.detail || data.message));
}
} catch (err) {
console.error("Reset model error:", err);
alert("Serverfehler beim Zurücksetzen des KI-Modells.");
} finally {
if (resetBtn) {
resetBtn.disabled = false;
resetBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
Trainiertes KI-Modell zurücksetzen
`;
}
}
}
// --- INTERACTIVE EVENT LISTENERS ---
toggleCameraBtn.addEventListener("click", () => {
if (isWebcamActive) {
@@ -1022,4 +1718,8 @@ window.addEventListener("DOMContentLoaded", () => {
// Initialize capture classes
initCapturePanel();
// Check background calibration on startup
checkBackgroundCalibration();
updateLearnBadge();
});

View File

@@ -27,6 +27,10 @@
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="2" y1="12" x2="22" y2="12"/></svg>
Kassensystem
</button>
<button class="nav-btn" id="tab-btn-learn" onclick="switchTab('learn')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M8 11h8"/><path d="M12 7v8"/></svg>
Artikel anlernen
</button>
<button class="nav-btn" id="tab-btn-articles" onclick="switchTab('articles')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><circle cx="10" cy="9" r="1"/></svg>
Artikel-Verwaltung
@@ -71,6 +75,9 @@
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
Webcam aktivieren
</button>
<select id="camera-select" class="btn btn-secondary" style="background: var(--color-bg-input); border-color: var(--border-glass); color: var(--text-primary); outline: none; padding: 10px 14px; max-width: 180px; display: none;">
<option value="">Standard-Kamera</option>
</select>
<button id="trigger-scale-btn" class="btn btn-primary btn-glow">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M12 18h.01"/></svg>
Waage auslösen (Trigger)
@@ -108,7 +115,7 @@
<div class="card quick-select-card">
<div class="card-header">
<h2 class="card-title">KI Schnellwahltasten</h2>
<p class="card-subtitle">Vorschläge über Confidence-Schwellenwert &gt; 10%</p>
<p class="card-subtitle">Vorschläge über Confidence-Schwellenwert &gt; 85%</p>
</div>
<div class="quick-buttons-container" id="quick-selection-buttons">
<div class="empty-state">
@@ -179,6 +186,123 @@
</div>
</section>
<!-- LEARN ARTICLE TAB (ANLERN-MODUS) -->
<section id="tab-learn" class="tab-content">
<div class="checkout-grid">
<!-- Left: Camera Preview & Calibration -->
<div class="card camera-card">
<div class="card-header">
<div>
<h2 class="card-title">Live-Kamerabild (Von oben)</h2>
<p class="card-subtitle">Kalibriere die Null-Linie und richte das Produkt aus</p>
</div>
<span class="status-indicator" id="learn-calibration-indicator">Nicht kalibriert</span>
</div>
<div class="camera-viewport-container">
<canvas id="learn-camera-canvas" width="640" height="480"></canvas>
<div class="scale-target-overlay">
<div class="scale-target-box"></div>
</div>
<div class="stream-badge" id="learn-camera-type-badge">SIMULATION-MODUS</div>
</div>
<div class="camera-controls" style="justify-content: space-between;">
<div style="display: flex; gap: 12px; align-items: center;">
<button id="learn-calibrate-btn" class="btn btn-secondary" onclick="calibrateBackground()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
Hintergrund speichern
</button>
<button id="learn-reset-calibrate-btn" class="btn btn-danger" style="padding: 10px 14px;" onclick="resetBackgroundCalibration()" title="Hintergrundkalibrierung löschen">
Reset
</button>
<select id="learn-camera-select" class="btn btn-secondary" style="background: var(--color-bg-input); border-color: var(--border-glass); color: var(--text-primary); outline: none; padding: 10px 14px; max-width: 180px; display: none;">
<option value="">Standard-Kamera</option>
</select>
</div>
<label class="toggle-control">
<input type="checkbox" id="learn-bg-ok-indicator" disabled>
<span class="toggle-slider"></span>
<span class="toggle-label">Hintergrund OK</span>
</label>
</div>
</div>
<!-- Right: Product Selection & Controls -->
<div class="checkout-sidebar">
<!-- Product Selection -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Artikel-Details</h2>
<p class="card-subtitle">Wähle den anzulernenden Artikel aus</p>
</div>
<div class="card-body" style="display: flex; flex-direction: column; gap: 16px;">
<div class="control-group" style="margin-bottom: 0;">
<label for="learn-search-input">Suche / PLU-Nummer</label>
<input type="text" id="learn-search-input" class="input-field" placeholder="z.B. 4011 oder Banane..." oninput="filterLearnArticles()" style="width: 100%; background: var(--color-bg-input); border: 1px solid var(--border-glass); color: var(--text-primary); padding: 10px 14px; border-radius: var(--border-radius-md); font-size: 14px;">
</div>
<div class="learn-articles-list" id="learn-articles-list" style="max-height: 180px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--border-glass); padding: 8px; border-radius: var(--border-radius-md); background: rgba(0,0,0,0.15);">
<!-- Populated by JS -->
</div>
<div style="display: flex; flex-direction: column; gap: 4px; padding-top: 8px; border-top: 1px solid var(--border-glass);">
<span style="font-size: 12px; color: var(--text-secondary);">Ausgewählter Artikel:</span>
<strong id="learn-selected-article-display" style="font-size: 16px; color: var(--color-primary);">Kein Artikel ausgewählt</strong>
</div>
</div>
</div>
<!-- Recording Controls -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Aufnahme-Steuerung</h2>
<p class="card-subtitle">Serienaufnahme starten und Artikel drehen</p>
</div>
<div class="card-body" style="display: flex; flex-direction: column; gap: 16px;">
<button id="learn-start-capture-btn" class="btn btn-accent btn-glow" style="width: 100%; height: 50px; font-size: 15px;" onclick="startSerialCapture()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>
START (SERIENAUFNAHME)
</button>
<div class="progress-info" style="display: flex; justify-content: space-between; font-size: 13px; color: var(--text-secondary);">
<span>Fortschritt:</span>
<span id="learn-progress-text">0 / 10 Bilder</span>
</div>
<div class="progress-bar-container" style="height: 10px;">
<div class="progress-bar-fill" id="learn-progress-fill" style="width: 0%; background: var(--color-accent); box-shadow: 0 0 10px var(--color-accent-glow);"></div>
</div>
<p style="font-size: 12px; color: var(--text-muted); line-height: 1.4;">
Das System nimmt im Sekundentakt automatisch 10 Bilder auf. Stupsen Sie das Produkt zwischen den Signaltönen leicht an, um verschiedene Ansichten zu erfassen.
</p>
</div>
</div>
</div>
</div>
<!-- Gallery Section -->
<div class="card" style="margin-top: 24px;">
<div class="card-header">
<div>
<h2 class="card-title">Bilder-Galerie (Aktuelle Session)</h2>
<p class="card-subtitle">Hier werden die 10 aufgenommenen Bilder angezeigt. Lösche fehlerhafte Bilder per Klick auf das X.</p>
</div>
<div style="display: flex; gap: 12px; align-items: center;">
<span class="capture-count-badge" id="learn-session-count-badge">Session Bilder: 0</span>
<button id="reset-all-captures-btn" class="btn btn-danger" onclick="resetAllCapturedData()" style="display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 13px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Alle Trainingsbilder löschen
</button>
</div>
</div>
<div class="card-body">
<div class="gallery-grid" id="learn-gallery-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 16px;">
<div class="empty-state" id="learn-gallery-empty" style="grid-column: 1 / -1; padding: 40px 0;">
<p>Noch keine Aufnahmen in dieser Session</p>
<span>Wähle ein Produkt und starte die Serienaufnahme</span>
</div>
</div>
</div>
</div>
</section>
<!-- DEVELOPER DASHBOARD TAB -->
<section id="tab-dashboard" class="tab-content">
<div class="dashboard-grid">
@@ -217,6 +341,49 @@
</div>
</div>
<!-- Step 0: Reset Captured Training Data -->
<div class="card" style="border: 1px solid rgba(239, 68, 68, 0.3); background: rgba(239, 68, 68, 0.04);">
<div class="card-header">
<div>
<h2 class="card-title" style="color: var(--color-danger);">⚠ KI Gefahrenzone & Reset-Optionen</h2>
<p class="card-subtitle">Trainingsdaten löschen oder das trainierte KI-Modell zurücksetzen</p>
</div>
</div>
<div class="card-body" style="display: flex; flex-direction: column; gap: 20px;">
<!-- Option 1: Dataset reset -->
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 style="margin: 0; font-size: 14px; color: var(--text-primary);">1. Trainingsbilder & Kalibrierung</h3>
<p style="font-size: 13px; color: var(--text-secondary); margin: 0; line-height: 1.5;">
Löscht alle selbst erfassten Trainingsbilder und setzt den kalibrierten Hintergrund zurück.
</p>
<div class="action-row" style="margin-top: 4px;">
<button id="dashboard-reset-btn" class="btn btn-danger" onclick="resetAllCapturedData()" style="display: flex; align-items: center; gap: 8px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Eigene Bilder & Kalibrierung zurücksetzen
</button>
<span class="action-desc" style="color: var(--color-danger); font-weight: 500;">Nicht umkehrbar!</span>
</div>
</div>
<div style="border-top: 1px solid rgba(239, 68, 68, 0.2); width: 100%;"></div>
<!-- Option 2: Model reset -->
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 style="margin: 0; font-size: 14px; color: var(--text-primary);">2. KI-Modell zurücksetzen</h3>
<p style="font-size: 13px; color: var(--text-secondary); margin: 0; line-height: 1.5;">
Löscht das trainierte PyTorch- und ONNX-Modell. Das System fällt danach in den Simulations-Modus zurück, bis ein neues Modell trainiert wird.
</p>
<div class="action-row" style="margin-top: 4px;">
<button id="dashboard-reset-model-btn" class="btn btn-danger" onclick="resetAIModel()" style="display: flex; align-items: center; gap: 8px; background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.3)); border: 1px solid rgba(239, 68, 68, 0.5);">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
Trainiertes KI-Modell zurücksetzen
</button>
<span class="action-desc" style="color: var(--color-danger); font-weight: 500;">Setzt Inferenz auf Simulation zurück.</span>
</div>
</div>
</div>
</div>
<!-- Step 3: Export & Hardware-Performance -->
<div class="card">
<div class="card-header">
@@ -272,14 +439,17 @@
<div class="control-group">
<label for="param-batch">Batch Size</label>
<select id="param-batch">
<option value="4">4</option>
<option value="8" selected>8</option>
<option value="16">16</option>
<option value="4" selected>4 (CPU empfohlen)</option>
<option value="8">8 (GPU)</option>
<option value="16">16 (GPU)</option>
</select>
</div>
<button class="btn btn-primary" id="start-training-btn" onclick="startTraining()">
Training starten
</button>
<button class="btn btn-danger" id="cancel-training-btn" onclick="cancelTraining()" style="display: none;">
Abbrechen
</button>
</div>
<!-- Training Status -->
@@ -373,7 +543,7 @@
<!-- Footer -->
<footer class="app-footer">
<p>Lizenz-Fokus: 100% kommerziell nutzbar, BSD & MIT Open-Source Lizenzen. ONNX Runtime Inference.</p>
<p>Made by Hephex Industries</p>
</footer>
</div>

View File

@@ -1073,3 +1073,105 @@ input:checked + .toggle-slider::before {
color: var(--text-muted);
font-size: 11px;
}
/* Learn Tab specific styles */
.learn-articles-list {
scrollbar-width: thin;
scrollbar-color: var(--border-glass) transparent;
}
.learn-article-item {
padding: 8px 12px;
border-radius: var(--border-radius-sm);
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border-glass);
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s ease;
}
.learn-article-item:hover {
background: rgba(99, 102, 241, 0.06);
border-color: rgba(99, 102, 241, 0.3);
color: var(--text-primary);
}
.learn-article-item.selected {
background: rgba(99, 102, 241, 0.15);
border-color: var(--color-primary);
color: var(--text-primary);
box-shadow: 0 0 10px rgba(99, 102, 241, 0.15);
font-weight: 500;
}
.learn-article-item .item-plu {
font-family: monospace;
font-size: 11px;
background: rgba(255, 255, 255, 0.05);
padding: 2px 6px;
border-radius: 4px;
color: var(--text-muted);
}
.learn-article-item.selected .item-plu {
background: rgba(99, 102, 241, 0.3);
color: #a5b4fc;
}
.gallery-item-wrapper {
position: relative;
border-radius: var(--border-radius-md);
border: 1px solid var(--border-glass);
background: rgba(0, 0, 0, 0.3);
aspect-ratio: 4/3;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.gallery-item-wrapper:hover {
transform: scale(1.03);
border-color: var(--color-accent);
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.2);
}
.gallery-item-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.gallery-item-delete-btn {
position: absolute;
top: 6px;
right: 6px;
width: 22px;
height: 22px;
background: rgba(239, 68, 68, 0.9);
border: none;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
font-size: 11px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
.gallery-item-wrapper:hover .gallery-item-delete-btn {
opacity: 1;
}
.gallery-item-delete-btn:hover {
background: var(--color-danger);
transform: scale(1.1);
}