/** * ArtiAgent Web UI - Unified Frontend Logic * Supports both DefectFill & DefectDiffu * ========================================== */ // ============================================================================= // Global State // ============================================================================= const state = { currentModel: 'defectfill', activeTab: 'training', uploadedImages: [], currentMaskImage: null, drawnRect: null, drawnRectNatural: null, isDrawing: false, trainingPollInterval: null, checkpoints: {}, generationResults: [], savedMasks: new Set(), ddConfig: {}, }; // ============================================================================= // Initialization // ============================================================================= document.addEventListener('DOMContentLoaded', () => { initTabs(); initModelSelector(); initTrainingWizard(); initGenerationForm(); initDefectDiffu(); initCanvas(); loadCheckpoints(); loadSystemInfo(); loadDDConfig(); setupDomainExamples(); }); // ============================================================================= // Navigation & Tabs // ============================================================================= function initTabs() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { const target = btn.dataset.tab; state.activeTab = target; tabBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`tab-${target}`).classList.add('active'); }); }); } function initModelSelector() { const modelBtns = document.querySelectorAll('.model-btn'); modelBtns.forEach(btn => { btn.addEventListener('click', () => { const model = btn.dataset.model; state.currentModel = model; modelBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); const defectfillTabs = document.getElementById('defectfill-tabs'); const defectdiffuTabs = document.getElementById('defectdiffu-tabs'); if (model === 'defectfill') { defectfillTabs.classList.remove('hidden'); defectdiffuTabs.classList.add('hidden'); } else { defectfillTabs.classList.add('hidden'); defectdiffuTabs.classList.remove('hidden'); } }); }); } // ============================================================================= // Training Wizard (DefectFill) // ============================================================================= function initTrainingWizard() { const btnStep1 = document.getElementById('btn-step1'); btnStep1.addEventListener('click', () => { const objClass = document.getElementById('train-object-class').value.trim(); const defectType = document.getElementById('train-defect-type').value.trim(); if (!objClass || !defectType) { showAlert('step1-alert', 'Please enter both product name and defect type.', 'danger'); return; } document.getElementById('step2').classList.remove('hidden'); document.getElementById('step2').scrollIntoView({ behavior: 'smooth' }); checkGoodImages(objClass); }); const goodUploadZone = document.getElementById('good-upload-zone'); const goodFileInput = document.getElementById('good-images'); goodUploadZone.addEventListener('click', () => goodFileInput.click()); goodUploadZone.addEventListener('dragover', (e) => { e.preventDefault(); goodUploadZone.classList.add('dragover'); }); goodUploadZone.addEventListener('dragleave', () => goodUploadZone.classList.remove('dragover')); goodUploadZone.addEventListener('drop', (e) => { e.preventDefault(); goodUploadZone.classList.remove('dragover'); handleGoodFileSelect(e.dataTransfer.files); }); goodFileInput.addEventListener('change', (e) => handleGoodFileSelect(e.target.files)); document.getElementById('btn-upload-good').addEventListener('click', () => { const files = goodFileInput.files; if (files.length === 0) { showAlert('step2-alert', 'Please select files first.', 'warning'); return; } handleGoodFileSelect(files); }); document.getElementById('btn-use-existing-good').addEventListener('click', () => { document.getElementById('step3').classList.remove('hidden'); document.getElementById('step3').scrollIntoView({ behavior: 'smooth' }); }); document.getElementById('btn-proceed-good').addEventListener('click', () => { document.getElementById('step3').classList.remove('hidden'); document.getElementById('step3').scrollIntoView({ behavior: 'smooth' }); }); const defectUploadZone = document.getElementById('defect-upload-zone'); const defectFileInput = document.getElementById('defect-images'); defectUploadZone.addEventListener('click', () => defectFileInput.click()); defectUploadZone.addEventListener('dragover', (e) => { e.preventDefault(); defectUploadZone.classList.add('dragover'); }); defectUploadZone.addEventListener('dragleave', () => defectUploadZone.classList.remove('dragover')); defectUploadZone.addEventListener('drop', (e) => { e.preventDefault(); defectUploadZone.classList.remove('dragover'); handleFileSelect(e.dataTransfer.files); }); defectFileInput.addEventListener('change', (e) => handleFileSelect(e.target.files)); document.getElementById('btn-step3-upload').addEventListener('click', () => { const files = defectFileInput.files; if (files.length === 0) { showAlert('step3-alert', 'Please select files first.', 'warning'); return; } handleFileSelect(files); }); document.getElementById('btn-align-yes').addEventListener('click', () => { document.getElementById('align-reference-section').classList.remove('hidden'); }); document.getElementById('btn-align-no').addEventListener('click', skipAlignment); const refUploadZone = document.getElementById('reference-upload-zone'); const refFileInput = document.getElementById('reference-image'); refUploadZone.addEventListener('click', () => refFileInput.click()); refFileInput.addEventListener('change', (e) => { if (e.target.files && e.target.files.length > 0) { renderReferenceImage(e.target.files[0]); } }); document.getElementById('btn-align-proceed').addEventListener('click', runAlignment); document.getElementById('btn-save-mask').addEventListener('click', saveMask); document.getElementById('btn-skip-mask').addEventListener('click', skipMask); document.getElementById('btn-prev-image').addEventListener('click', prevMaskImage); document.getElementById('btn-next-image').addEventListener('click', nextMaskImage); document.getElementById('btn-clear-rect').addEventListener('click', clearRect); document.getElementById('btn-done-masks').addEventListener('click', finishMaskStep); document.getElementById('btn-save-domain').addEventListener('click', saveDomainInstructions); document.getElementById('btn-load-domain-example').addEventListener('click', loadDomainExample); document.getElementById('btn-start-training').addEventListener('click', startTraining); document.getElementById('btn-stop-training').addEventListener('click', stopTraining); } function handleFileSelect(files) { const objClass = document.getElementById('train-object-class').value.trim(); const defectType = document.getElementById('train-defect-type').value.trim(); if (!objClass || !defectType) { showAlert('step3-alert', 'Please complete Step 1 first.', 'warning'); return; } if (files.length < 1) { showAlert('step3-alert', 'At least 1 images required for few-shot training.', 'warning'); return; } const formData = new FormData(); formData.append('object_class', objClass); formData.append('defect_type', defectType); for (const file of files) { if (file.type.startsWith('image/')) { formData.append('images', file); } } const btn = document.getElementById('btn-step3-upload'); btn.disabled = true; btn.innerHTML = ' Uploading...'; fetch('/api/training/upload-images', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { btn.disabled = false; btn.innerHTML = '📤 Upload Images'; if (data.success) { state.uploadedImages = data.saved; renderUploadedImages(); showAlert('step3-alert', `Uploaded ${data.count} images successfully.`, 'success'); document.getElementById('step3-5').classList.remove('hidden'); document.getElementById('step3-5').scrollIntoView({ behavior: 'smooth' }); } else { showAlert('step3-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; btn.innerHTML = '📤 Upload Images'; showAlert('step3-alert', 'Upload failed: ' + err.message, 'danger'); }); } function checkProductExists(objClass, defectType) { fetch(`/api/check-product-exists?object_class=${encodeURIComponent(objClass)}`) .then(r => r.json()) .then(data => { if (data.success && data.exists) { showDuplicateProductModal(objClass, data.existing_defect_types, data.sample_image_url, defectType); } else { proceedToStep2(); } }) .catch(err => { console.error('Error checking product existence:', err); proceedToStep2(); }); } function showDuplicateProductModal(productName, existingDefectTypes, sampleImageUrl, defectType) { const modal = document.getElementById('duplicate-product-modal'); document.getElementById('dup-modal-product-name').textContent = productName; document.getElementById('dup-modal-defect-types').textContent = existingDefectTypes.length > 0 ? existingDefectTypes.join(', ') : 'None'; const imgContainer = document.getElementById('dup-modal-image-container'); const img = document.getElementById('dup-modal-sample-image'); if (sampleImageUrl) { img.src = sampleImageUrl; imgContainer.classList.remove('hidden'); } else { imgContainer.classList.add('hidden'); } modal.classList.remove('hidden'); document.getElementById('btn-add-defect-type').onclick = () => { modal.classList.add('hidden'); proceedToStep2(); }; document.getElementById('btn-new-product').onclick = () => { modal.classList.add('hidden'); promptForNewProductName(productName); }; document.getElementById('btn-close-dup-modal').onclick = () => { modal.classList.add('hidden'); }; modal.onclick = (e) => { if (e.target === modal) modal.classList.add('hidden'); }; } function promptForNewProductName(originalName) { const newName = prompt( `Product "${originalName}" already exists.\n\nPlease enter a new, unique product name:` ); if (!newName || !newName.trim()) { showAlert('step1-alert', 'A new product name is required to proceed.', 'warning'); return; } const trimmed = newName.trim(); fetch(`/api/check-product-exists?object_class=${encodeURIComponent(trimmed)}`) .then(r => r.json()) .then(data => { if (data.success && data.exists) { showAlert('step1-alert', `Product name "${trimmed}" also already exists. Please choose another name.`, 'danger'); setTimeout(() => promptForNewProductName(originalName), 300); } else { document.getElementById('train-object-class').value = trimmed; showAlert('step1-alert', `Product name updated to "${trimmed}". Proceeding...`, 'success'); proceedToStep2(); } }) .catch(err => { showAlert('step1-alert', 'Error checking new name: ' + err.message, 'danger'); }); } function checkGoodImages(objectClass) { fetch(`/api/training/good-images/count?object_class=${encodeURIComponent(objectClass)}`) .then(r => r.json()) .then(data => { const statusDiv = document.getElementById('good-images-status'); const useExistingBtn = document.getElementById('btn-use-existing-good'); const proceedBtn = document.getElementById('btn-proceed-good'); if (data.success && data.count > 0) { statusDiv.className = 'alert alert-info'; statusDiv.innerHTML = `${data.count} good image(s) already exist for "${escapeHtml(objectClass)}".
You can upload more or proceed with existing ones.`; statusDiv.classList.remove('hidden'); useExistingBtn.classList.remove('hidden'); proceedBtn.classList.remove('hidden'); } else { statusDiv.className = 'alert alert-warning'; statusDiv.innerHTML = `No good images found for "${escapeHtml(objectClass)}".
Please upload at least 1 clean (non-defective) image.`; statusDiv.classList.remove('hidden'); useExistingBtn.classList.add('hidden'); proceedBtn.classList.add('hidden'); } }) .catch(err => { showAlert('step2-alert', 'Failed to check good images: ' + err.message, 'danger'); }); } function handleGoodFileSelect(files) { const objClass = document.getElementById('train-object-class').value.trim(); if (!objClass) { showAlert('step2-alert', 'Please complete Step 1 first.', 'warning'); return; } const formData = new FormData(); formData.append('object_class', objClass); for (const file of files) { if (file.type.startsWith('image/')) { formData.append('images', file); } } const btn = document.getElementById('btn-upload-good'); btn.disabled = true; btn.innerHTML = ' Uploading...'; fetch('/api/training/upload-good-images', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { btn.disabled = false; btn.innerHTML = '📤 Upload Good Images'; if (data.success) { renderGoodImages(data.saved); showAlert('step2-alert', `Uploaded ${data.count} good images. Total in folder: ${data.total_in_folder}`, 'success'); document.getElementById('btn-proceed-good').classList.remove('hidden'); document.getElementById('btn-use-existing-good').classList.remove('hidden'); } else { showAlert('step2-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; btn.innerHTML = '📤 Upload Good Images'; showAlert('step2-alert', 'Upload failed: ' + err.message, 'danger'); }); } function renderGoodImages(images) { const gallery = document.getElementById('good-images-gallery'); images.forEach(img => { const div = document.createElement('div'); div.className = 'image-item'; div.innerHTML = `${img.filename}`; gallery.appendChild(div); }); } function proceedToStep2() { document.getElementById('step2').classList.remove('hidden'); document.getElementById('step2').scrollIntoView({ behavior: 'smooth' }); } function renderUploadedImages() { const gallery = document.getElementById('defect-upload-gallery'); gallery.innerHTML = ''; state.uploadedImages.forEach((img, idx) => { const div = document.createElement('div'); div.className = 'image-item'; div.innerHTML = ` ${img.filename}
`; gallery.appendChild(div); }); updateMaskCounter(); } function renderReferenceImage(file) { const gallery = document.getElementById('reference-gallery'); gallery.innerHTML = ''; const div = document.createElement('div'); div.className = 'image-item'; const url = URL.createObjectURL(file); div.innerHTML = `Reference`; gallery.appendChild(div); } function skipAlignment() { const objClass = document.getElementById('train-object-class').value.trim(); const defectType = document.getElementById('train-defect-type').value.trim(); const btn = document.getElementById('btn-align-no'); btn.disabled = true; fetch('/api/training/skip-align', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ object_class: objClass }) }) .then(r => r.json()) .then(data => { btn.disabled = false; if (data.success) { if (data.images) { const defectImages = data.images.filter(img => img.path.includes(`/defective/${encodeURIComponent(defectType)}`) ); if (defectImages.length > 0) { state.uploadedImages = defectImages; renderUploadedImages(); } } showAlert('step3-5-alert', `Moved ${data.moved_count} images to final location.`, 'success'); proceedToStep4(); } else { showAlert('step3-5-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; showAlert('step3-5-alert', 'Error: ' + err.message, 'danger'); }); } function runAlignment() { const objClass = document.getElementById('train-object-class').value.trim(); const defectType = document.getElementById('train-defect-type').value.trim(); const refInput = document.getElementById('reference-image'); if (!refInput.files || refInput.files.length === 0) { showAlert('step3-5-alert', 'Please upload a reference image first.', 'warning'); return; } const formData = new FormData(); formData.append('object_class', objClass); formData.append('reference', refInput.files[0]); const btn = document.getElementById('btn-align-proceed'); const spinner = document.getElementById('align-spinner'); btn.disabled = true; spinner.classList.remove('hidden'); fetch('/api/training/align', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { btn.disabled = false; spinner.classList.add('hidden'); if (data.success) { if (data.images) { const defectImages = data.images.filter(img => img.path.includes(`/defective/${encodeURIComponent(defectType)}`) ); if (defectImages.length > 0) { state.uploadedImages = defectImages; renderUploadedImages(); } } let msg = `Aligned ${data.aligned_count} images successfully.`; if (data.error_count > 0) { msg += ` ${data.error_count} failed and were copied as-is.`; } showAlert('step3-5-alert', msg, 'success'); proceedToStep4(); } else { showAlert('step3-5-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; spinner.classList.add('hidden'); showAlert('step3-5-alert', 'Alignment failed: ' + err.message, 'danger'); }); } function proceedToStep4() { document.getElementById('step4').classList.remove('hidden'); document.getElementById('step4').scrollIntoView({ behavior: 'smooth' }); initMaskCanvas(0); } // ============================================================================= // Canvas Mask Drawing // ============================================================================= function initCanvas() { // Canvas is initialized per-image } function initMaskCanvas(index) { if (index < 0 || index >= state.uploadedImages.length) return; state.currentMaskImage = index; state.drawnRect = null; state.drawnRectNatural = null; const imgData = state.uploadedImages[index]; const canvas = document.getElementById('mask-canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { const maxW = 800; const maxH = 600; let w = img.width; let h = img.height; if (w > maxW || h > maxH) { const ratio = Math.min(maxW / w, maxH / h); w = Math.floor(w * ratio); h = Math.floor(h * ratio); } canvas.width = w; canvas.height = h; canvas.dataset.naturalWidth = img.width; canvas.dataset.naturalHeight = img.height; ctx.drawImage(img, 0, 0, w, h); updateMaskCounter(); document.getElementById('step4-canvas').classList.remove('hidden'); }; img.src = imgData.url; let startX, startY; function getCanvasCoords(e) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }; } canvas.onmousedown = (e) => { const pos = getCanvasCoords(e); startX = pos.x; startY = pos.y; state.isDrawing = true; state.drawnRect = null; state.drawnRectNatural = null; }; canvas.onmousemove = (e) => { if (!state.isDrawing) return; const pos = getCanvasCoords(e); const x = pos.x; const y = pos.y; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.strokeRect(startX, startY, x - startX, y - startY); ctx.fillStyle = 'rgba(37, 99, 235, 0.2)'; ctx.fillRect(startX, startY, x - startX, y - startY); }; canvas.onmouseup = (e) => { if (!state.isDrawing) return; state.isDrawing = false; const pos = getCanvasCoords(e); const endX = pos.x; const endY = pos.y; const x = Math.min(startX, endX); const y = Math.min(startY, endY); const width = Math.abs(endX - startX); const height = Math.abs(endY - startY); if (width > 10 && height > 10) { state.drawnRect = { x, y, width, height }; const scaleX = img.width / canvas.width; const scaleY = img.height / canvas.height; state.drawnRectNatural = { x: Math.round(x * scaleX), y: Math.round(y * scaleY), width: Math.round(width * scaleX), height: Math.round(height * scaleY) }; } ctx.setLineDash([]); }; canvas.onmouseleave = () => { if (state.isDrawing) { state.isDrawing = false; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); if (state.drawnRect) { ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 2; ctx.strokeRect(state.drawnRect.x, state.drawnRect.y, state.drawnRect.width, state.drawnRect.height); } } }; } function toggleInputMode() { const mode = document.querySelector('input[name="input_mode"]:checked').value; const singleInput = document.getElementById('gen-clean-image'); const folderInput = document.getElementById('gen-clean-folder'); const singleDiv = document.getElementById('single-image-input'); const folderDiv = document.getElementById('folder-input'); const hint = document.getElementById('input-hint'); if (mode === 'folder') { singleDiv.classList.add('hidden'); folderDiv.classList.remove('hidden'); singleInput.value = ''; hint.textContent = 'Select a folder containing clean (non-defective) product images. All images will be processed.'; } else { singleDiv.classList.remove('hidden'); folderDiv.classList.add('hidden'); folderInput.value = ''; hint.textContent = 'Upload a clean (non-defective) image of the product.'; } } function toggleDDInputMode() { const mode = document.querySelector('input[name="dd_input_mode"]:checked').value; const singleDiv = document.getElementById('dd-single-image-input'); const folderDiv = document.getElementById('dd-folder-input'); const singleInput = document.getElementById('dd-clean-image'); const folderInput = document.getElementById('dd-clean-folder'); const hint = document.getElementById('dd-input-hint'); if (mode === 'folder') { singleDiv.classList.add('hidden'); folderDiv.classList.remove('hidden'); singleInput.value = ''; hint.textContent = 'Select a folder containing clean (non-defective) product images. All images will be processed.'; } else { singleDiv.classList.remove('hidden'); folderDiv.classList.add('hidden'); folderInput.value = ''; hint.textContent = 'Upload a clean (non-defective) image of the product.'; } } function clearRect() { state.drawnRect = null; state.drawnRectNatural = null; const canvas = document.getElementById('mask-canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }; if (state.uploadedImages[state.currentMaskImage]) { img.src = state.uploadedImages[state.currentMaskImage].url; } } function saveMask() { if (!state.drawnRectNatural) { showAlert('step4-alert', 'Please draw a rectangle on the image first.', 'warning'); return; } const imgData = state.uploadedImages[state.currentMaskImage]; fetch('/api/training/save-mask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_path: imgData.path, rect: state.drawnRectNatural }) }) .then(r => r.json()) .then(data => { if (data.success) { state.savedMasks.add(state.currentMaskImage); const badge = document.getElementById(`mask-badge-${state.currentMaskImage}`); if (badge) badge.classList.remove('hidden'); updateMaskCounter(); const totalSaved = state.savedMasks.size; const remaining = state.uploadedImages.length - totalSaved; if (totalSaved >= 3) { showAlert('step4-alert', `Mask saved! Please click the Done button to continue to Step 5 (Remaining unmasked images: ${remaining})`, 'success', 0); } else { showAlert('step4-alert', 'Mask saved!', 'success', 1200); } if (state.currentMaskImage < state.uploadedImages.length - 1) { setTimeout(() => nextMaskImage(), 500); } } else { showAlert('step4-alert', data.error, 'danger'); } }) .catch(err => { showAlert('step4-alert', 'Error: ' + err.message, 'danger'); }); } function skipMask() { if (state.currentMaskImage < state.uploadedImages.length - 1) { nextMaskImage(); } } function prevMaskImage() { if (state.currentMaskImage > 0) { initMaskCanvas(state.currentMaskImage - 1); } } function nextMaskImage() { if (state.currentMaskImage < state.uploadedImages.length - 1) { initMaskCanvas(state.currentMaskImage + 1); } } function updateMaskCounter() { const counter = document.getElementById('mask-counter'); const total = state.uploadedImages.length; const masked = state.savedMasks.size; const current = state.currentMaskImage !== null ? state.currentMaskImage + 1 : 0; const filename = state.uploadedImages[state.currentMaskImage]?.filename || ''; counter.textContent = `Image ${current} of ${total}: ${filename} | Masks saved: ${masked}/${total}`; } function finishMaskStep() { if (state.savedMasks.size < 1) { showAlert('step4-alert', `You must save masks for at least 1 images. Currently saved: ${state.savedMasks.size}.`, 'warning'); return; } document.getElementById('step5').classList.remove('hidden'); document.getElementById('step5').scrollIntoView({ behavior: 'smooth' }); document.getElementById('step4-alert').classList.add('hidden'); } // ============================================================================= // Domain Instructions // ============================================================================= function setupDomainExamples() { const examples = { triac: `If the image is a standard TO-220 package (like a Triac): - The component consists of three metallic pins/leads, a black plastic mold body, a metal mounting tab with a mounting hole, and printed silkscreen text. - CRITICAL CONSTRAINT: You MUST exclusively propose defects located on the metallic pins/leads. Do NOT propose any defects on the black plastic body, the metal mounting tab, the mounting hole, or the silkscreen text. - Acceptable pin/lead defects include: pin bending (crooked leads), lead deformation, surface scratches on the metallic pins, or contamination/solder dross on the lead surfaces. - Use "3 pin" as target_entity rather than specific individual pins. - The description must clearly detail exactly which pin is affected and how it is deformed or damaged (e.g., "The right pin is bent sharply outwards near the mid-section"). - The defect_coverage_ratio should be 0.4 - 0.60.`, xray_pcb: `If the image is an X-ray or CT scan of a PCB/IC package: - The die appears as a large dark or light rectangular region, usually in the center. - Solder balls/BGA appear as an array of circular dots. - Defects in X-ray appear as grayscale anomalies (darker or brighter regions), NOT as circuit traces, lines, or wire patterns. - A "xray_die" defect should look like: a natural void, crack, or delamination INSIDE the die material — blobby, irregular, or circular grayscale spots. - A "xray_solder" defect should look like: missing solder, solder bridges, or voids in solder balls. - NEVER describe the defect as "circuit traces", "wiring", "grid lines", or "PCB patterns". The defect must look like a manufacturing flaw, not an overlaid schematic. - The defect_coverage_ratio should be 1.0.`, vcsel: `If the image is a VCSEL laser diode: - The component has a metallic can package (TO-can), a glass lens cap on top, and electrode bars. - Defects should focus on: lens cap cracks, scratches on the glass dome, electrode bar discontinuity, or metallic can surface damage. - Use "lens cap" for lens-related defects and "electrode bars" for electrode-related defects. - Avoid targeting the emission aperture directly with large coverage masks.`, generic: `If the image shows an electronic component: - Identify the main functional areas: metallic leads/pins, plastic or ceramic body, and any optical or contact surfaces. - Propose defects that are realistic manufacturing flaws: scratches, cracks, contamination, bent leads, missing material, or discoloration. - Use specific terminology for metallic parts (e.g., "metallic pins", "copper leads", "gold-plated contacts") rather than generic terms like "leg" or "metal part". - The defect must be visually distinct and not confused with normal component features.` }; window.domainExamples = examples; } function loadDomainExample() { const select = document.getElementById('domain-example-select'); const key = select.value; if (window.domainExamples[key]) { document.getElementById('domain-instructions').value = window.domainExamples[key]; } } function saveDomainInstructions() { const objClass = document.getElementById('train-object-class').value.trim() || 'default'; const instructions = document.getElementById('domain-instructions').value.trim(); fetch('/api/domain-instructions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ object_class: objClass, instructions: instructions }) }) .then(r => r.json()) .then(data => { if (data.success) { showAlert('step5-alert', data.message, 'success'); document.getElementById('step6').classList.remove('hidden'); document.getElementById('step6').scrollIntoView({ behavior: 'smooth' }); } else { showAlert('step5-alert', data.error, 'danger'); } }) .catch(err => { showAlert('step5-alert', 'Error: ' + err.message, 'danger'); }); } // ============================================================================= // Training Execution (DefectFill) // ============================================================================= function startTraining() { const objClass = document.getElementById('train-object-class').value.trim(); const defectType = document.getElementById('train-defect-type').value.trim(); const config = { object_class: objClass, defect_type: defectType, lora_rank: parseInt(document.getElementById('cfg-lora-rank').value) || 8, lora_alpha: parseInt(document.getElementById('cfg-lora-alpha').value) || 16, max_train_steps: parseInt(document.getElementById('cfg-max-steps').value) || 1500, batch_size: parseInt(document.getElementById('cfg-batch-size').value) || 2, gradient_accumulation_steps: parseInt(document.getElementById('cfg-grad-accum').value) || 2, lambda_defect: parseFloat(document.getElementById('cfg-lambda-defect').value) || 0.5, lambda_obj: parseFloat(document.getElementById('cfg-lambda-obj').value) || 0.2, lambda_attn: parseFloat(document.getElementById('cfg-lambda-attn').value) || 0.05, alpha: parseFloat(document.getElementById('cfg-alpha').value) || 0.3 }; const btn = document.getElementById('btn-start-training'); btn.disabled = true; btn.innerHTML = ' Starting Training...'; fetch('/api/training/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }) .then(r => r.json()) .then(data => { if (data.success) { document.getElementById('training-monitor').classList.remove('hidden'); document.getElementById('training-monitor').scrollIntoView({ behavior: 'smooth', block: 'start' }); showAlert('step6-alert', 'Training started! Monitor logs below.', 'success'); startLogPolling(); } else { btn.disabled = false; btn.innerHTML = '🚀 Start Training'; showAlert('step6-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; btn.innerHTML = '🚀 Start Training'; showAlert('step6-alert', 'Error: ' + err.message, 'danger'); }); } function stopTraining() { fetch('/api/training/stop', { method: 'POST' }) .then(r => r.json()) .then(data => { showAlert('step6-alert', data.message || data.error, data.success ? 'warning' : 'danger'); }); } function startLogPolling() { if (state.trainingPollInterval) clearInterval(state.trainingPollInterval); const logViewer = document.getElementById('training-log'); const statusBadge = document.getElementById('training-status-badge'); const progressBar = document.getElementById('training-progress'); const btnStart = document.getElementById('btn-start-training'); const btnStop = document.getElementById('btn-stop-training'); btnStop.classList.remove('hidden'); state.trainingPollInterval = setInterval(() => { fetch('/api/training/status') .then(r => r.json()) .then(data => { if (!data.success) return; const status = data.status; statusBadge.className = 'status-badge'; if (status.running) { statusBadge.classList.add('running'); statusBadge.innerHTML = ' Running'; } else if (status.error) { statusBadge.classList.add('error'); statusBadge.innerHTML = ' Error'; } else if (status.completed) { statusBadge.classList.add('completed'); statusBadge.innerHTML = ' Completed'; } else { statusBadge.classList.add('idle'); statusBadge.innerHTML = ' Idle'; } if (status.log_lines && status.log_lines.length > 0) { logViewer.innerHTML = status.log_lines.map(line => { let cls = 'log-line'; if (line.includes('Error') || line.includes('FAILED')) cls += ' log-error'; else if (line.includes('complete') || line.includes('saved')) cls += ' log-success'; else if (line.includes('Warning')) cls += ' log-warn'; return `
${escapeHtml(line)}
`; }).join(''); logViewer.scrollTop = logViewer.scrollHeight; } const progress = estimateProgress(status.log_lines); progressBar.style.width = progress + '%'; if (!status.running && status.start_time) { clearInterval(state.trainingPollInterval); btnStart.disabled = false; btnStart.innerHTML = '🚀 Start Training'; btnStop.classList.add('hidden'); if (status.completed) { showAlert('step6-alert', 'Training completed successfully! You can now generate defects.', 'success'); loadCheckpoints(); } else if (status.error) { showAlert('step6-alert', 'Training failed: ' + status.error, 'danger'); } } }); }, 2000); } function estimateProgress(logLines) { if (!logLines || logLines.length === 0) return 0; const lastLines = logLines.slice(-20).join(' '); const match = lastLines.match(/(\d+)\s*\/\s*(\d+)/); if (match) { const current = parseInt(match[1]); const total = parseInt(match[2]); if (total > 0) return Math.min(100, (current / total) * 100); } if (lastLines.includes('complete') || lastLines.includes('saved')) return 100; return 0; } // ============================================================================= // Generation (DefectFill) // ============================================================================= function initGenerationForm() { document.getElementById('gen-form').addEventListener('submit', (e) => { e.preventDefault(); generateDefect(); }); document.getElementById('btn-refresh-checkpoints').addEventListener('click', loadCheckpoints); initDetectSimilarProduct(); } function initDetectSimilarProduct() { const detectUploadZone = document.getElementById('detect-upload-zone'); const detectFileInput = document.getElementById('detect-product-image'); const btnDetect = document.getElementById('btn-detect-product'); const preview = document.getElementById('detect-image-preview'); if (!detectUploadZone || !btnDetect) return; detectUploadZone.addEventListener('click', () => detectFileInput.click()); detectFileInput.addEventListener('change', (e) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; preview.innerHTML = ``; preview.classList.remove('hidden'); } }); btnDetect.addEventListener('click', () => { const files = detectFileInput.files; if (!files || files.length === 0) { showAlert('detect-product-alert', 'Please upload 1 clean product image first.', 'warning'); return; } const formData = new FormData(); formData.append('image', files[0]); btnDetect.disabled = true; btnDetect.innerHTML = ' Detecting similarity...'; fetch('/api/detect-similar-product', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { btnDetect.disabled = false; btnDetect.innerHTML = '🔍 Verify Product Model'; if (data.success) { const res = data.result; if (res.is_trained) { const select = document.getElementById('gen-object-class'); select.value = res.matched_product; select.dispatchEvent(new Event('change')); showAlert( 'detect-product-alert', `✅ Match Found! Recognized as ${escapeHtml(res.matched_product)} (Similarity: ${(res.confidence * 100).toFixed(1)}%). Object Class dropdown updated automatically.`, 'success' ); } else { const alertHtml = `
⚠️ Product Not Trained
${escapeHtml(res.message)}
`; const alertDiv = document.getElementById('detect-product-alert'); alertDiv.className = 'alert alert-warning mt-2'; alertDiv.innerHTML = alertHtml; alertDiv.classList.remove('hidden'); } } else { showAlert('detect-product-alert', data.error, 'danger'); } }) .catch(err => { btnDetect.disabled = false; btnDetect.innerHTML = '🔍 Verify Product Model'; showAlert('detect-product-alert', 'Detection error: ' + err.message, 'danger'); }); }); } function switchToTrainingTab() { const trainingTabBtn = document.querySelector('.tab-btn[data-tab="training"]'); if (trainingTabBtn) { trainingTabBtn.click(); window.scrollTo({ top: 0, behavior: 'smooth' }); } } function loadCheckpoints() { fetch('/api/checkpoints') .then(r => r.json()) .then(data => { if (data.success) { state.checkpoints = data.checkpoints; renderCheckpointSelector(); } }) .catch(console.error); } function renderCheckpointSelector() { const select = document.getElementById('gen-object-class'); const defectSelect = document.getElementById('gen-defect-type'); const currentObj = select.value; select.innerHTML = ''; Object.keys(state.checkpoints).forEach(objClass => { const opt = document.createElement('option'); opt.value = objClass; opt.textContent = objClass; select.appendChild(opt); }); if (currentObj && state.checkpoints[currentObj]) { select.value = currentObj; updateDefectTypeOptions(currentObj); } select.addEventListener('change', (e) => { updateDefectTypeOptions(e.target.value); }); } function updateDefectTypeOptions(objClass) { const defectSelect = document.getElementById('gen-defect-type'); defectSelect.innerHTML = ''; if (state.checkpoints[objClass]) { state.checkpoints[objClass].forEach(dt => { const opt = document.createElement('option'); opt.value = dt; opt.textContent = dt; defectSelect.appendChild(opt); }); } } function generateDefect() { const form = document.getElementById('gen-form'); const formData = new FormData(form); const objClass = formData.get('object_class'); if (!objClass) { showAlert('gen-alert', 'Please select a trained product.', 'warning'); return; } const btn = document.getElementById('btn-generate'); btn.disabled = true; btn.innerHTML = ' Generating...'; fetch('/api/generate', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.success) { startGenerationPolling(data.job_id, 'defectfill'); } else { btn.disabled = false; btn.innerHTML = '✨ Generate Synthetic Defects'; showAlert('gen-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; btn.innerHTML = '✨ Generate Synthetic Defects'; showAlert('gen-alert', 'Generation failed: ' + err.message, 'danger'); }); } // ============================================================================= // DefectDiffu // ============================================================================= function initDefectDiffu() { // Load config on init loadDDConfig(); // Save config button document.getElementById('btn-save-dd-config').addEventListener('click', saveDDConfig); // Generation form document.getElementById('dd-gen-form').addEventListener('submit', (e) => { e.preventDefault(); generateDefectDiffu(); }); } function loadDDConfig() { fetch('/api/defectdiffu/config') .then(r => r.json()) .then(data => { if (data.success && data.config) { state.ddConfig = data.config; document.getElementById('dd-ckpt-path').value = data.config.ckpt_path || ''; document.getElementById('dd-vae-path').value = data.config.vae_path || ''; document.getElementById('dd-vlm-model').value = data.config.vlm_model || 'gemma3:12b'; } }) .catch(console.error); } function saveDDConfig() { const ckptPath = document.getElementById('dd-ckpt-path').value.trim(); const vaePath = document.getElementById('dd-vae-path').value.trim(); const vlmModel = document.getElementById('dd-vlm-model').value.trim() || 'gemma3:12b'; if (!ckptPath || !vaePath) { showAlert('dd-config-alert', 'Both checkpoint path and VAE path are required.', 'warning'); return; } fetch('/api/defectdiffu/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ckpt_path: ckptPath, vae_path: vaePath, vlm_model: vlmModel }) }) .then(r => r.json()) .then(data => { if (data.success) { state.ddConfig = { ckpt_path: ckptPath, vae_path: vaePath, vlm_model: vlmModel }; showAlert('dd-config-alert', data.message, 'success'); } else { showAlert('dd-config-alert', data.error, 'danger'); } }) .catch(err => { showAlert('dd-config-alert', 'Error saving config: ' + err.message, 'danger'); }); } function generateDefectDiffu() { const form = document.getElementById('dd-gen-form'); const formData = new FormData(form); const productDesc = formData.get('product_desc'); if (!productDesc) { showAlert('dd-gen-alert', 'Product description is required.', 'warning'); return; } // Check config is set if (!state.ddConfig.ckpt_path || !state.ddConfig.vae_path) { showAlert('dd-gen-alert', 'Please configure DefectDiffu model paths first.', 'warning'); document.getElementById('dd-config-card').scrollIntoView({ behavior: 'smooth' }); return; } const btn = document.getElementById('btn-dd-generate'); btn.disabled = true; btn.innerHTML = ' Generating...'; fetch('/api/defectdiffu/generate', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.success) { startGenerationPolling(data.job_id, 'defectdiffu'); } else { btn.disabled = false; btn.innerHTML = '✨ Generate with DefectDiffu'; showAlert('dd-gen-alert', data.error, 'danger'); } }) .catch(err => { btn.disabled = false; btn.innerHTML = '✨ Generate with DefectDiffu'; showAlert('dd-gen-alert', 'Generation failed: ' + err.message, 'danger'); }); } // ============================================================================= // Generation Polling (Unified for both models) // ============================================================================= function startGenerationPolling(jobId, mode) { const btn = mode === 'defectfill' ? document.getElementById('btn-generate') : document.getElementById('btn-dd-generate'); const resultsContainer = mode === 'defectfill' ? document.getElementById('gen-results') : document.getElementById('dd-gen-results'); const summaryDiv = mode === 'defectfill' ? document.getElementById('gen-summary') : document.getElementById('dd-gen-summary'); const alertId = mode === 'defectfill' ? 'gen-alert' : 'dd-gen-alert'; resultsContainer.innerHTML = `

⏳ Generating Synthetic Defects...

Initializing...
Waiting for generation to start...
`; const pollInterval = setInterval(() => { fetch(`/api/generation-status/${jobId}`) .then(r => r.json()) .then(data => { if (!data.success) { clearInterval(pollInterval); return; } const job = data.job; let progress = 0; if (job.unit_total && job.unit_current !== undefined) { progress = Math.min(100, (job.unit_current / job.unit_total) * 100); } else { progress = Math.min(100, (job.step / job.total_steps) * 100); } document.getElementById(`gen-progress-bar-${mode}`).style.width = progress + '%'; document.getElementById(`gen-step-text-${mode}`).textContent = `Step ${job.step}/${job.total_steps}: ${job.step_text}`; const defectCounter = document.getElementById(`gen-defect-counter-${mode}`); let counterText = ''; if (job.image_total > 0) { counterText += `Image ${job.image_current}/${job.image_total}`; } if (job.defect_total > 0) { counterText += (counterText ? ' | ' : '') + `Defect ${job.defect_current}/${job.defect_total}`; } defectCounter.textContent = counterText; const logViewer = document.getElementById(`gen-log-viewer-${mode}`); const line = document.createElement('div'); line.className = 'log-line'; line.textContent = `[${new Date().toLocaleTimeString()}] ${job.step_text}`; logViewer.appendChild(line); logViewer.scrollTop = logViewer.scrollHeight; if (job.status === 'completed') { clearInterval(pollInterval); btn.disabled = false; btn.innerHTML = mode === 'defectfill' ? '✨ Generate Synthetic Defects' : '✨ Generate with DefectDiffu'; renderGenerationResults(job.result, mode); showAlert(alertId, `Generated ${job.result.output_images?.length || 0} images in ${formatElapsedTime(job.result.elapsed_time)}`, 'success'); } else if (job.status === 'error') { clearInterval(pollInterval); btn.disabled = false; btn.innerHTML = mode === 'defectfill' ? '✨ Generate Synthetic Defects' : '✨ Generate with DefectDiffu'; showAlert(alertId, 'Generation failed: ' + job.error, 'danger'); } }) .catch(console.error); }, 10000); } function renderGenerationResults(data, mode = 'defectfill') { const container = mode === 'defectfill' ? document.getElementById('gen-results') : document.getElementById('dd-gen-results'); const summary = mode === 'defectfill' ? document.getElementById('gen-summary') : document.getElementById('dd-gen-summary'); container.innerHTML = ''; const grouped = data.grouped_results; let totalImages = 0; if (grouped) { grouped.forEach(g => { totalImages += g.defects.length * 4; }); } if (grouped && grouped.length > 0) { summary.innerHTML = `
Generation Complete
Experiment: ${data.experiment_id || 'N/A'} | Product: ${data.product_type || 'N/A'} | Time: ${formatElapsedTime(data.elapsed_time)} | ${totalImages} image(s) generated
`; grouped.forEach((group, groupIdx) => { const groupId = `result-group-${mode}-${groupIdx}`; const groupEl = document.createElement('div'); groupEl.className = 'result-group'; const header = document.createElement('div'); header.className = 'result-group-header'; header.onclick = () => toggleResultGroup(groupId); header.innerHTML = ` Source ${groupIdx + 1}: ${escapeHtml(group.source_name)} ${group.defect_count} defect(s) `; const content = document.createElement('div'); content.className = 'result-group-content'; content.id = groupId; group.defects.forEach((defect, defectIdx) => { const defectId = `${groupId}-defect-${defectIdx}`; const defectGroup = document.createElement('div'); defectGroup.className = 'result-defect-group'; const defectHeader = document.createElement('div'); defectHeader.className = 'result-defect-header'; defectHeader.onclick = (e) => { e.stopPropagation(); toggleResultGroup(defectId); }; defectHeader.innerHTML = ` ${escapeHtml(defect.defect_label)} ${escapeHtml(defect.defect_type)} `; const defectContent = document.createElement('div'); defectContent.className = 'result-defect-content'; defectContent.id = defectId; const trioGrid = document.createElement('div'); trioGrid.className = 'result-trio-grid'; const imageTypes = [ { key: 'clean_image', label: 'Clean' }, { key: 'mask_image', label: 'Mask' }, { key: 'raw_patch_image', label: 'Raw Patch' }, { key: 'output_image', label: 'Output' } ]; imageTypes.forEach(type => { const imgData = defect[type.key]; if (imgData && imgData.url) { const card = document.createElement('div'); card.className = 'result-trio-card'; card.innerHTML = `
${type.label}
${imgData.filename}
${escapeHtml(imgData.filename)}
Download `; trioGrid.appendChild(card); } }); defectContent.appendChild(trioGrid); defectGroup.appendChild(defectHeader); defectGroup.appendChild(defectContent); content.appendChild(defectGroup); }); groupEl.appendChild(header); groupEl.appendChild(content); container.appendChild(groupEl); }); } else if (data.output_images && data.output_images.length > 0) { summary.innerHTML = `
Generation Complete
Experiment: ${data.experiment_id || 'N/A'} | Product: ${data.product_type || 'N/A'} | Time: ${formatElapsedTime(data.elapsed_time)}
`; const grid = document.createElement('div'); grid.className = 'result-grid'; data.output_images.forEach(img => { const card = document.createElement('div'); card.className = 'result-card'; card.innerHTML = ` ${img.filename}

${img.defect_type}

${img.filename}

Download
`; grid.appendChild(card); }); container.appendChild(grid); } else { summary.innerHTML = `
No images were generated. Check the defect type and product description.
`; } } function toggleResultGroup(groupId) { const content = document.getElementById(groupId); const icon = document.getElementById(`icon-${groupId}`); const header = content.previousElementSibling; if (content.classList.contains('expanded')) { content.classList.remove('expanded'); icon.textContent = '▶'; header.classList.remove('active'); } else { content.classList.add('expanded'); icon.textContent = '▼'; header.classList.add('active'); } } // ============================================================================= // Utilities // ============================================================================= function showAlert(elementId, message, type, autoHideMs = null) { const el = document.getElementById(elementId); if (!el) return; if (el._hideTimeout) { clearTimeout(el._hideTimeout); el._hideTimeout = null; } el.className = `alert alert-${type}`; el.innerHTML = message; el.classList.remove('hidden'); if (type === 'success') { const delay = autoHideMs !== null ? autoHideMs : 0; if (delay > 0) { el._hideTimeout = setTimeout(() => { el.classList.add('hidden'); }, delay); } } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function previewImage(url) { const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` `; overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); document.body.appendChild(overlay); } function loadSystemInfo() { fetch('/api/system/info') .then(r => r.json()) .then(data => { if (data.success) { const info = data.info; const badge = document.getElementById('cuda-badge'); if (info.cuda_available) { badge.textContent = `CUDA: ${info.cuda_device_name}`; badge.style.background = 'var(--success)'; } else { badge.textContent = 'CPU Mode'; badge.style.background = 'var(--warning)'; } } }) .catch(console.error); } function formatElapsedTime(seconds) { if (!seconds && seconds !== 0) return '0s'; const mins = Math.floor(seconds / 60); const secs = (seconds % 60).toFixed(1); if (mins > 0) { return `${mins}m ${secs}s`; } return `${secs}s`; }