// Application Configuration const PRICING_DATABASE = { "apfel_elstar": { name: "Apfel Elstar", price: 2.99, unit: "kg" }, "apfel_gala": { name: "Apfel Gala", price: 2.49, unit: "kg" }, "apfel_granny_smith": { name: "Apfel Granny Smith", price: 2.79, unit: "kg" }, "banane_chiquita": { name: "Banane Chiquita", price: 1.99, unit: "kg" }, "banane_bio": { name: "Bio Banane", price: 2.29, unit: "kg" }, "birne": { name: "Birne Abate Fetel", price: 3.29, unit: "kg" }, "orange": { name: "Orangen", price: 2.19, unit: "kg" }, "zitrone": { name: "Zitronen", price: 0.59, unit: "Stk." }, "limette": { name: "Limetten", price: 0.49, unit: "Stk." }, "erdbeere": { name: "Erdbeeren Schale", price: 2.99, unit: "Stk." }, "blaubeere": { name: "Kulturheidelbeeren", price: 1.99, unit: "Stk." }, "weintraube_hell": { name: "Tafeltrauben hell", price: 3.99, unit: "kg" }, "weintraube_dunkel": { name: "Tafeltrauben dunkel", price: 4.29, unit: "kg" }, "pfirsich": { name: "Pfirsiche", price: 2.99, unit: "kg" }, "tomate": { name: "Rispentomaten", price: 3.49, unit: "kg" }, "gurke": { name: "Salatgurke", price: 0.99, unit: "Stk." }, "kartoffel": { name: "Speisekartoffeln", price: 1.49, unit: "kg" }, "karotte": { name: "Speisemöhren", price: 1.29, unit: "kg" }, "zwiebel_gelb": { name: "Speisezwiebeln", price: 1.19, unit: "kg" }, "zwiebel_rot": { name: "Rote Zwiebeln", price: 1.69, unit: "kg" }, "knoblauch": { name: "Knoblauch", price: 0.89, unit: "Stk." }, "brokkoli": { name: "Brokkoli", price: 1.79, unit: "Stk." }, "paprika_rot": { name: "Paprika rot", price: 3.99, unit: "kg" }, "paprika_gelb": { name: "Paprika gelb", price: 3.99, unit: "kg" }, "paprika_gruen": { name: "Paprika grün", price: 3.49, unit: "kg" }, "champignon": { name: "Champignons weiß", price: 1.89, unit: "Stk." }, "zucchini": { name: "Zucchini", price: 2.19, unit: "kg" }, "avocado": { name: "Avocado Hass", price: 1.29, unit: "Stk." } }; // Application State let activeTab = 'checkout'; let isWebcamActive = false; let webcamStream = null; let simulatedStreamInterval = null; let currentFrameBase64 = null; let isDetecting = false; let autoDetectEnabled = true; let trainingPollInterval = null; // Scanner Emulator State let autoScanEnabled = true; let stableClass = null; let stableCount = 0; const STABILITY_THRESHOLD = 4; // number of frames (approx 0.8s) let lastScannedClass = null; let scanLockActive = false; let articlesDatabase = {}; // Cart State let shoppingCart = []; // DOM Elements const videoEl = document.getElementById("webcam"); const canvasEl = document.getElementById("camera-canvas"); const ctx = canvasEl.getContext("2d"); const toggleCameraBtn = document.getElementById("toggle-camera-btn"); const triggerScaleBtn = document.getElementById("trigger-scale-btn"); const autoDetectToggle = document.getElementById("auto-detect-toggle"); const cameraTypeBadge = document.getElementById("camera-type-badge"); const quickSelectButtons = document.getElementById("quick-selection-buttons"); const cartList = document.getElementById("cart-list"); const cartSubtotalEl = document.getElementById("cart-subtotal"); const cartTotalEl = document.getElementById("cart-total"); const emptyCartMsg = document.getElementById("empty-cart-msg"); // --- TAB NAVIGATION --- function switchTab(tabName) { activeTab = tabName; document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active')); document.getElementById(`tab-${tabName}`).classList.add('active'); document.getElementById(`tab-btn-${tabName}`).classList.add('active'); if (tabName === 'dashboard') { // Start polling training status when entering dashboard startTrainingPolling(); refreshAugmentedPreview(); } else { stopTrainingPolling(); } if (tabName === 'articles') { loadArticlesDatabase(); } } // --- CAMERA & DETECTION LOGIC --- async function startWebcam() { try { webcamStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480, facingMode: "environment" } }); videoEl.srcObject = webcamStream; videoEl.style.display = "none"; isWebcamActive = true; cameraTypeBadge.textContent = "LIVE-WEBCAM"; cameraTypeBadge.style.background = "var(--color-primary)"; toggleCameraBtn.innerHTML = ` Kamera deaktivieren `; document.getElementById("stream-status").textContent = "LIVE"; document.getElementById("stream-status").classList.add("live"); // Stop simulated stream if active if (simulatedStreamInterval) { clearInterval(simulatedStreamInterval); simulatedStreamInterval = null; } requestAnimationFrame(processWebcamFrame); } catch (err) { console.error("Webcam access denied/unavailable:", err); alert("Webcam konnte nicht gestartet werden. Der simulierte Kamera-Modus wird verwendet."); startSimulatedStream(); } } function stopWebcam() { if (webcamStream) { webcamStream.getTracks().forEach(track => track.stop()); webcamStream = null; } videoEl.srcObject = null; videoEl.style.display = "none"; isWebcamActive = false; toggleCameraBtn.innerHTML = ` 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(); } setTimeout(() => { requestAnimationFrame(processWebcamFrame); }, 150); // Limit detection frequency for performance } 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 loadSimulatedFrame = async () => { try { const response = await fetch("/api/simulated_frame"); const data = await response.json(); 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(); } }; img.src = data.image; } } catch (err) { console.error("Failed to load simulated frame:", err); } }; loadSimulatedFrame(); // First load immediately simulatedStreamInterval = setInterval(loadSimulatedFrame, 2500); // cycle frames every 2.5s } // Perform detection API request async function runDetection() { if (!currentFrameBase64) return; isDetecting = true; try { const response = await fetch("/api/detect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: currentFrameBase64 }) }); const data = await response.json(); if (data.status === "success") { drawDetections(data.predictions); updateQuickButtons(data.predictions); processStabilityAndScan(data.predictions); } } catch (err) { console.error("Detection error:", err); } finally { isDetecting = false; } } // Draw bounding boxes on canvas function drawDetections(predictions) { // Redraw the base frame first (to clear old boxes) const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0, canvasEl.width, canvasEl.height); // Draw boxes predictions.forEach(pred => { const [xmin, ymin, xmax, ymax] = pred.box; const w = xmax - xmin; const h = ymax - ymin; const isHighConf = pred.confidence > 0.70; // Box outline ctx.lineWidth = 3; ctx.strokeStyle = isHighConf ? "#10b981" : "#c084fc"; // Green for high conf, purple for low conf ctx.strokeRect(xmin, ymin, w, h); // Label box background ctx.fillStyle = isHighConf ? "rgba(16, 185, 129, 0.85)" : "rgba(168, 85, 247, 0.85)"; const text = `${PRICING_DATABASE[pred.class]?.name || pred.class} (${(pred.confidence * 100).toFixed(0)}%)`; ctx.font = "bold 12px sans-serif"; const textWidth = ctx.measureText(text).width; ctx.fillRect(xmin - 1, ymin - 22, textWidth + 12, 22); // Label text ctx.fillStyle = "#ffffff"; ctx.fillText(text, xmin + 5, ymin - 6); }); }; img.src = currentFrameBase64; } // Update cashier recommendation buttons function updateQuickButtons(predictions) { quickSelectButtons.innerHTML = ""; // Filter predictions with confidence > 0.10 const candidates = predictions.filter(p => p.confidence > 0.10); if (candidates.length === 0) { quickSelectButtons.innerHTML = `

Keine Objekte erkannt

Confidence Schwellenwert unter 10%
`; return; } candidates.forEach(pred => { const itemInfo = PRICING_DATABASE[pred.class] || { name: pred.class, price: 1.0, unit: "kg" }; const confPercent = (pred.confidence * 100).toFixed(0); const isHigh = pred.confidence > 0.70; const btn = document.createElement("button"); btn.className = `quick-btn ${isHigh ? 'high-conf' : 'low-conf'}`; btn.onclick = () => sendEanToPos(pred.class); btn.innerHTML = ` ${itemInfo.name} ${confPercent}% Conf `; quickSelectButtons.appendChild(btn); }); } // --- SHOPPING CART LOGIC --- function addToCart(classKey) { const itemInfo = PRICING_DATABASE[classKey]; if (!itemInfo) return; // Simulate weight or piece count let quantity, itemTotal; if (itemInfo.unit === "kg") { // Random weight between 0.150kg and 1.800kg quantity = parseFloat((0.150 + Math.random() * 1.65).toFixed(3)); itemTotal = parseFloat((quantity * itemInfo.price).toFixed(2)); } else { quantity = 1; itemTotal = itemInfo.price; } const cartItem = { key: classKey, name: itemInfo.name, pricePerUnit: itemInfo.price, unit: itemInfo.unit, quantity: quantity, total: itemTotal, timestamp: Date.now() }; shoppingCart.push(cartItem); updateCartUI(); // Scale animation feedback const scaleCard = document.querySelector(".camera-card"); scaleCard.classList.add("scale-flash"); setTimeout(() => scaleCard.classList.remove("scale-flash"), 400); } function deleteCartItem(timestamp) { shoppingCart = shoppingCart.filter(item => item.timestamp !== timestamp); updateCartUI(); } function clearCart() { shoppingCart = []; updateCartUI(); } function checkoutCart() { if (shoppingCart.length === 0) return; const total = cartTotalEl.textContent; alert(`Zahlung erfolgreich abgeschlossen!\nBetrag: ${total}\nKassenzettel gedruckt.`); clearCart(); } function updateCartUI() { cartList.innerHTML = ""; if (shoppingCart.length === 0) { emptyCartMsg.style.display = "flex"; cartSubtotalEl.textContent = "0,00 €"; cartTotalEl.textContent = "0,00 €"; return; } emptyCartMsg.style.display = "none"; let subtotal = 0; shoppingCart.forEach(item => { subtotal += item.total; const row = document.createElement("div"); row.className = "cart-item-row"; const details = document.createElement("div"); details.className = "item-details"; const name = document.createElement("span"); name.className = "item-name"; name.textContent = item.name; const meta = document.createElement("span"); meta.className = "item-meta"; if (item.unit === "kg") { meta.textContent = `${item.quantity.toFixed(3)} kg x ${item.pricePerUnit.toFixed(2)} €/kg`; } else { meta.textContent = `${item.quantity} Stk. x ${item.pricePerUnit.toFixed(2)} €/Stk.`; } details.appendChild(name); details.appendChild(meta); const actions = document.createElement("div"); actions.className = "item-price-actions"; const price = document.createElement("span"); price.className = "item-price"; price.textContent = `${item.total.toFixed(2).replace('.', ',')} €`; const delBtn = document.createElement("button"); delBtn.className = "delete-item-btn"; delBtn.onclick = () => deleteCartItem(item.timestamp); delBtn.innerHTML = ` `; actions.appendChild(price); actions.appendChild(delBtn); row.appendChild(details); row.appendChild(actions); cartList.appendChild(row); }); const tax = subtotal * 0.07; // 7% vat cartSubtotalEl.textContent = `${subtotal.toFixed(2).replace('.', ',')} €`; cartTotalEl.textContent = `${subtotal.toFixed(2).replace('.', ',')} €`; } // --- DEVELOPER DASHBOARD BACKEND CALLS --- // 1. Synthetic dataset trigger async function generateDataset() { try { const response = await fetch("/api/generate_dataset", { method: "POST" }); const data = await response.json(); alert(data.message); startTrainingPolling(); // will start checking generation status } catch (err) { console.error("Failed to generate dataset:", err); } } // 2. Refresh Albumentations augmented preview async function refreshAugmentedPreview() { const previewBox = document.getElementById("aug-preview-box"); previewBox.innerHTML = `
Generiere Vorschau...
`; try { const response = await fetch("/api/augmented_preview"); const data = await response.json(); if (data.status === "success") { previewBox.innerHTML = `Original vs. Augmentiert`; } else { previewBox.innerHTML = `
Fehler: ${data.message}
`; } } catch (err) { previewBox.innerHTML = `
Keine Trainingsbilder vorhanden.
Bitte zuerst "Datensatz generieren" klicken.
`; } } // 3. Trigger model training async function startTraining() { const epochs = parseInt(document.getElementById("param-epochs").value) || 15; const batchSize = parseInt(document.getElementById("param-batch").value) || 8; try { const response = await fetch("/api/train", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ epochs: epochs, batch_size: batchSize }) }); const data = await response.json(); alert(data.message); startTrainingPolling(); } catch (err) { console.error("Failed to start training:", err); } } // Poll training API function startTrainingPolling() { if (trainingPollInterval) return; const checkStatus = async () => { try { const response = await fetch("/api/train_status"); const data = await response.json(); updateTrainingStatusUI(data); // If training or data generation completed or failed, we can stop/keep checking depending on state if (data.status === "completed" || data.status === "failed" || data.status === "idle") { // Keep checking periodically but slower, or stop if we want to } } catch (err) { console.error("Status check failed:", err); } }; checkStatus(); trainingPollInterval = setInterval(checkStatus, 1500); } function stopTrainingPolling() { if (trainingPollInterval) { clearInterval(trainingPollInterval); trainingPollInterval = null; } } // Update training UI and plot graphs function updateTrainingStatusUI(data) { const trainingState = document.getElementById("training-state"); const trainingEpoch = document.getElementById("training-epoch"); const progressFill = document.getElementById("training-progress-fill"); const logMsg = document.getElementById("training-log-msg"); const statusBox = document.getElementById("training-status-box"); // Update status labels trainingState.textContent = getGermanState(data.status); trainingEpoch.textContent = data.epoch > 0 ? `Epoche: ${data.epoch}/${data.total_epochs}` : "Epoche: -/-"; progressFill.style.width = `${data.progress}%`; logMsg.textContent = data.message; // Update box glow styling statusBox.className = "training-status-box"; if (data.status === "training") { statusBox.classList.add("active-pulse"); } // Plot Chart if loss history is available if (data.train_loss && data.train_loss.length > 0) { updateChart(data.train_loss, data.val_loss || []); } } function getGermanState(status) { switch (status) { case "idle": return "Bereit"; case "generating_data": return "Generiere Daten..."; case "training": return "Training läuft..."; case "completed": return "Abgeschlossen"; case "failed": return "Fehlgeschlagen"; default: return status; } } // Plot losses in SVG function updateChart(trainLosses, valLosses) { const svgWidth = 440; // width bounds inside SVG 500 const svgHeight = 150; // height bounds inside SVG 200 if (trainLosses.length === 0) return; // Find maximum loss to scale Y-axis correctly const allLosses = [...trainLosses, ...valLosses]; const maxLoss = Math.max(2.0, ...allLosses); const getSvgCoords = (losses) => { return losses.map((loss, idx) => { const x = 40 + (idx / (losses.length - 1 || 1)) * svgWidth; const y = 170 - (loss / maxLoss) * svgHeight; return `${x.toFixed(1)},${y.toFixed(1)}`; }); }; const trainPoints = getSvgCoords(trainLosses); document.getElementById("train-loss-path").setAttribute("d", "M " + trainPoints.join(" L ")); if (valLosses.length > 0) { const valPoints = getSvgCoords(valLosses); document.getElementById("val-loss-path").setAttribute("d", "M " + valPoints.join(" L ")); } } // 4. Export model to ONNX async function exportModel() { const onnxLog = document.getElementById("onnx-log"); const exportBtn = document.getElementById("export-onnx-btn"); const latencyVal = document.getElementById("onnx-latency"); const latencyStatus = document.getElementById("onnx-latency-status"); onnxLog.textContent = "Starte ONNX Export & Benchmarking..."; exportBtn.disabled = true; try { const response = await fetch("/api/export", { method: "POST" }); const data = await response.json(); exportBtn.disabled = false; if (data.status === "success") { onnxLog.textContent = data.log || "Export erfolgreich!"; // Extract latency from benchmark logs using regex const match = data.log.match(/Average ONNX Runtime inference latency: ([\d\.]+) ms/); if (match && match[1]) { const latency = match[1]; latencyVal.textContent = `${latency} ms`; latencyStatus.textContent = parseFloat(latency) < 50.0 ? "✓ < 50 ms (Flüssige Live-Ansicht)" : "⚠️ > 50 ms (Optimiere Hardware)"; } else { latencyVal.textContent = "< 30 ms"; latencyStatus.textContent = "✓ Inferenz läuft flüssig"; } alert("Modell erfolgreich in ONNX konvertiert und geladen!"); } else { onnxLog.textContent = `Fehler: ${data.message}`; alert(`Export fehlgeschlagen: ${data.message}`); } } catch (err) { exportBtn.disabled = false; onnxLog.textContent = `Fehler bei der Server-Kommunikation.`; alert("Export-Anfrage fehlgeschlagen. Vergewissere dich, dass ein PyTorch Modell (.pt) existiert."); } } async function captureForTraining() { if (!isWebcamActive) { alert("Bitte aktivieren Sie zuerst die Live-Webcam, um ein echtes Foto aufzunehmen."); return; } const classSelect = document.getElementById("capture-class-select"); const selectedClass = classSelect.value; if (!selectedClass) return; const captureBtn = document.getElementById("capture-image-btn"); captureBtn.disabled = true; // Capture current frame from canvas (high quality) const base64Frame = canvasEl.toDataURL("image/jpeg", 0.95); try { const response = await fetch("/api/capture_training_image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: base64Frame, class_name: selectedClass }) }); const data = await response.json(); captureBtn.disabled = false; if (data.status === "success") { // Update display count document.getElementById("capture-count-display").textContent = `Eigene Bilder: ${data.count}`; // Flash camera box as visual feedback const container = document.querySelector(".camera-card"); container.classList.add("scale-flash"); setTimeout(() => container.classList.remove("scale-flash"), 300); console.log(data.message); } else { alert(`Fehler bei Aufnahme: ${data.message}`); } } catch (err) { captureBtn.disabled = false; console.error("Capture request failed:", err); alert("Serverfehler bei Bild-Erfassung."); } } async function initCapturePanel() { const classSelect = document.getElementById("capture-class-select"); if (!classSelect) return; classSelect.innerHTML = ""; Object.keys(PRICING_DATABASE).forEach(key => { const opt = document.createElement("option"); opt.value = key; opt.textContent = PRICING_DATABASE[key].name; if (key === "kartoffel") { opt.selected = true; // default to kartoffel } classSelect.appendChild(opt); }); try { const response = await fetch("/api/capture_count"); const data = await response.json(); document.getElementById("capture-count-display").textContent = `Eigene Bilder: ${data.count}`; } catch (err) { console.error("Failed to load initial capture count:", err); } } // --- VIRTUAL BARCODE SCANNER LOGIC --- // Initial load of EAN database on startup async function initArticles() { try { const response = await fetch("/api/articles"); const data = await response.json(); articlesDatabase = data; // Sync names back to PRICING_DATABASE Object.keys(articlesDatabase).forEach(key => { if (!PRICING_DATABASE[key]) { PRICING_DATABASE[key] = { name: articlesDatabase[key].name, price: 1.99, unit: "Stk." }; } else { PRICING_DATABASE[key].name = articlesDatabase[key].name; } }); } catch (err) { console.error("Fehler beim Initialisieren der Artikeldatenbank:", err); } } // Render articles table rows in Article Management tab function renderArticlesTable() { const tbody = document.getElementById("articles-table-body"); if (!tbody) return; tbody.innerHTML = ""; Object.keys(articlesDatabase).forEach(key => { const article = articlesDatabase[key]; const tr = document.createElement("tr"); tr.style.borderBottom = "1px solid var(--border-glass)"; tr.innerHTML = ` ${key} `; tbody.appendChild(tr); }); } // Load articles table in Article Management tab async function loadArticlesDatabase() { try { const response = await fetch("/api/articles"); const data = await response.json(); articlesDatabase = data; // Sync to PRICING_DATABASE Object.keys(articlesDatabase).forEach(key => { if (!PRICING_DATABASE[key]) { PRICING_DATABASE[key] = { name: articlesDatabase[key].name, price: 1.99, unit: "Stk." }; } else { PRICING_DATABASE[key].name = articlesDatabase[key].name; } }); renderArticlesTable(); } catch (err) { console.error("Fehler beim Laden der Artikeldatenbank:", err); } } // Prompt to add a new article key and EAN code function addNewArticlePrompt() { const key = prompt("Geben Sie einen eindeutigen Systemschlüssel für den Artikel ein (z.B. 'mango', nur Kleinbuchstaben und Unterstriche):"); if (!key) return; // Validate key: lowercase, letters, numbers, underscores const keyRegex = /^[a-z0-9_]+$/; if (!keyRegex.test(key)) { alert("Ungültiger Schlüssel. Bitte nur Kleinbuchstaben (a-z), Zahlen (0-9) und Unterstriche (_) verwenden."); return; } if (PRICING_DATABASE[key] || articlesDatabase[key]) { alert("Dieser Artikel-Schlüssel existiert bereits."); return; } const name = prompt("Geben Sie den Anzeigenamen für den Artikel ein (z.B. 'Mango'):"); if (!name) return; const ean = prompt("Geben Sie den EAN-Barcode für die Tastaturemulation ein (z.B. '4001234000292'):"); if (!ean) return; // Add to local articlesDatabase and PRICING_DATABASE articlesDatabase[key] = { name: name, ean: ean }; PRICING_DATABASE[key] = { name: name, price: 1.99, unit: "Stk." }; // Reload the table and capture dropdown renderArticlesTable(); initCapturePanel(); } // Save articles database async function saveArticlesDatabase() { const tbody = document.getElementById("articles-table-body"); if (!tbody) return; const nameInputs = tbody.querySelectorAll(".article-name-input"); const eanInputs = tbody.querySelectorAll(".article-ean-input"); const updatedArticles = {}; nameInputs.forEach((input, index) => { const key = input.getAttribute("data-key"); const name = input.value.trim(); const ean = eanInputs[index].value.trim(); updatedArticles[key] = { name, ean }; }); try { const response = await fetch("/api/articles", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ articles: updatedArticles }) }); const data = await response.json(); if (data.status === "success") { articlesDatabase = updatedArticles; // Sync updated articles to PRICING_DATABASE Object.keys(updatedArticles).forEach(key => { if (!PRICING_DATABASE[key]) { PRICING_DATABASE[key] = { name: updatedArticles[key].name, price: 1.99, unit: "Stk." }; } else { PRICING_DATABASE[key].name = updatedArticles[key].name; } }); // Update capture dropdown initCapturePanel(); alert("Artikeldatenbank erfolgreich gespeichert!"); } else { alert("Fehler beim Speichern: " + data.message); } } catch (err) { console.error("Fehler beim Speichern der Artikeldatenbank:", err); alert("Serverfehler beim Speichern der Artikeldatenbank."); } } // Play high-pitched scanner beep function playScannerBeep() { try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.type = "sine"; oscillator.frequency.setValueAtTime(1000, audioCtx.currentTime); // 1kHz beep gainNode.gain.setValueAtTime(0, audioCtx.currentTime); gainNode.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.01); gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15); oscillator.start(audioCtx.currentTime); oscillator.stop(audioCtx.currentTime + 0.16); } catch (err) { console.error("Audio Context failed to play beep:", err); } } // Add log message to the terminal container function addScanLog(msg) { const logEl = document.getElementById("scanner-log"); if (!logEl) return; const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const logLine = `[${timeStr}] ${msg}`; if (logEl.textContent === "Warte auf Erkennung...") { logEl.textContent = logLine; } else { logEl.textContent += "\\n" + logLine; } logEl.scrollTop = logEl.scrollHeight; } // Send EAN code to backend scanner endpoint async function sendEanToPos(classKey) { const article = articlesDatabase[classKey] || { name: PRICING_DATABASE[classKey]?.name || classKey, ean: "" }; if (!article.ean) { addScanLog(`⚠️ Fehler: Kein EAN-Code für ${article.name} definiert.`); return; } addScanLog(`Scanne ${article.name} (EAN: ${article.ean})...`); try { const response = await fetch("/api/scan_ean", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ class_name: classKey }) }); const data = await response.json(); if (response.ok && data.status === "success") { playScannerBeep(); addScanLog(`✓ EAN ${article.ean} erfolgreich gesendet.`); // Visual flash feedback on scanner card const scannerCard = document.querySelector(".scanner-card"); if (scannerCard) { scannerCard.classList.add("scale-flash"); setTimeout(() => scannerCard.classList.remove("scale-flash"), 400); } } else { addScanLog(`❌ Fehler: ${data.message || "Tastaturemulation fehlgeschlagen"}`); } } catch (err) { console.error("Scan API error:", err); addScanLog(`❌ Serverfehler beim Senden des EAN-Codes.`); } } // Stability filter logic async function processStabilityAndScan(predictions) { const autoScanToggle = document.getElementById("auto-scan-toggle"); const autoScan = autoScanToggle ? autoScanToggle.checked : true; // Find the prediction with the highest confidence let topPrediction = null; if (predictions && predictions.length > 0) { predictions.forEach(p => { if (!topPrediction || p.confidence > topPrediction.confidence) { topPrediction = p; } }); } const scannerIndicator = document.getElementById("scanner-indicator"); const progressWrapper = document.getElementById("stability-progress-wrapper"); const progressFill = document.getElementById("stability-progress-fill"); const STABLE_CONF_THRESHOLD = 0.75; if (topPrediction && topPrediction.confidence >= STABLE_CONF_THRESHOLD) { const detectedClass = topPrediction.class; if (stableClass !== detectedClass) { stableClass = detectedClass; stableCount = 1; } else { stableCount++; } if (progressWrapper && progressFill) { progressWrapper.style.display = "block"; const percent = Math.min(100, (stableCount / STABILITY_THRESHOLD) * 100); progressFill.style.width = `${percent}%`; } if (scannerIndicator) { const articleName = PRICING_DATABASE[detectedClass]?.name || detectedClass; scannerIndicator.textContent = `Erkenne ${articleName}... (${stableCount}/${STABILITY_THRESHOLD})`; scannerIndicator.style.color = "var(--color-accent)"; } if (stableCount >= STABILITY_THRESHOLD) { if (lastScannedClass !== detectedClass && !scanLockActive) { if (autoScan) { await sendEanToPos(detectedClass); } else { addScanLog(`[Auto-Scan aus] ${PRICING_DATABASE[detectedClass]?.name || detectedClass} stabil erkannt.`); } lastScannedClass = detectedClass; scanLockActive = true; } if (progressFill) progressFill.style.width = "0%"; if (progressWrapper) progressWrapper.style.display = "none"; if (scannerIndicator) { scannerIndicator.textContent = `Gesperrt (Waage leeren)`; scannerIndicator.style.color = "var(--color-danger)"; } } } else { const CLEAR_THRESHOLD = 0.15; if (!topPrediction || topPrediction.confidence < CLEAR_THRESHOLD) { stableClass = null; stableCount = 0; scanLockActive = false; lastScannedClass = null; if (progressWrapper) progressWrapper.style.display = "none"; if (progressFill) progressFill.style.width = "0%"; if (scannerIndicator) { scannerIndicator.textContent = "Bereit (Warte auf Objekt)"; scannerIndicator.style.color = "var(--color-success)"; } } else { stableCount = Math.max(0, stableCount - 1); if (progressWrapper && progressFill) { const percent = (stableCount / STABILITY_THRESHOLD) * 100; progressFill.style.width = `${percent}%`; } if (scannerIndicator && stableCount > 0) { scannerIndicator.textContent = `Signal instabil...`; scannerIndicator.style.color = "var(--color-warning)"; } else if (scannerIndicator && !scanLockActive) { scannerIndicator.textContent = "Bereit (Signal instabil)"; scannerIndicator.style.color = "var(--color-success)"; } } } } // Manual scan trigger async function triggerManualScan() { if (stableClass) { addScanLog(`Manueller Scan ausgelöst für ${PRICING_DATABASE[stableClass]?.name || stableClass}...`); await sendEanToPos(stableClass); } else { addScanLog(`⚠️ Kein stabiles Objekt auf der Waage. Manueller Scan abgebrochen.`); alert("Bitte legen Sie ein Produkt auf die Waage, um es zu scannen."); } } // --- INTERACTIVE EVENT LISTENERS --- toggleCameraBtn.addEventListener("click", () => { if (isWebcamActive) { stopWebcam(); } else { startWebcam(); } }); triggerScaleBtn.addEventListener("click", () => { // Flash scale overlay on manual trigger const overlay = document.querySelector(".camera-card"); overlay.classList.add("scale-flash"); setTimeout(() => overlay.classList.remove("scale-flash"), 400); // Explicitly run detection runDetection(); }); autoDetectToggle.addEventListener("change", (e) => { autoDetectEnabled = e.target.checked; }); // App Startup Initialization window.addEventListener("DOMContentLoaded", () => { // Start in simulated video mode by default startSimulatedStream(); // Fetch articles from database initArticles(); // Read parameters from local values updateCartUI(); // Initialize capture classes initCapturePanel(); });