console.log("main.js loaded"); document.addEventListener('DOMContentLoaded', () => { // File Upload Elements const sourceFileInput = document.getElementById('source-file'); const targetFileInput = document.getElementById('target-file'); const sourcePreview = document.getElementById('source-preview'); const targetPreviewImage = document.getElementById('target-preview-image'); const targetPreviewVideo = document.getElementById('target-preview-video'); // Settings Elements const keepFpsCheckbox = document.getElementById('keep-fps'); const keepAudioCheckbox = document.getElementById('keep-audio'); const manyFacesCheckbox = document.getElementById('many-faces'); // General many_faces const mapFacesCheckbox = document.getElementById('map-faces-checkbox'); // Specific for face mapping UI const mouthMaskCheckbox = document.getElementById('mouth-mask'); // Add other settings elements here // Status Element const statusMessage = document.getElementById('status-message'); // Action Elements const startProcessingButton = document.getElementById('start-processing'); const livePreviewButton = document.getElementById('live-preview'); const processedPreviewImage = document.getElementById('processed-preview'); const outputArea = document.getElementById('output-area'); const downloadLink = document.getElementById('download-link'); // Face Mapper Elements const faceMapperContainer = document.getElementById('face-mapper-container'); const faceMapperArea = document.getElementById('face-mapper-area'); const submitFaceMappingsButton = document.getElementById('submit-face-mappings'); const faceMapperStatus = document.getElementById('face-mapper-status'); // WebApp state (mirroring some crucial Globals for UI logic) let webAppGlobals = { target_path_web: null, // Store the uploaded target file's path for UI checks source_target_map_from_backend: [], // To hold face data from /get_target_faces_for_mapping currentFaceMappings: [] // To store { target_id, target_image_b64, source_file, source_b64_preview } }; // Initially hide output area and face mapper if(outputArea) outputArea.style.display = 'none'; if(faceMapperContainer) faceMapperContainer.style.display = 'none'; if(submitFaceMappingsButton) submitFaceMappingsButton.style.display = 'none'; // Function to handle file preview (generic for source and target main previews) function previewFile(file, imagePreviewElement, videoPreviewElement) { const reader = new FileReader(); reader.onload = (e) => { if (file.type.startsWith('image/')) { imagePreviewElement.src = e.target.result; imagePreviewElement.style.display = 'block'; if (videoPreviewElement) videoPreviewElement.style.display = 'none'; } else if (file.type.startsWith('video/')) { if (videoPreviewElement) { videoPreviewElement.src = e.target.result; videoPreviewElement.style.display = 'block'; } imagePreviewElement.style.display = 'none'; } }; reader.readAsDataURL(file); } // Source File Upload sourceFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; previewFile(file, sourcePreview, null); // Source is always an image const formData = new FormData(); formData.append('file', file); statusMessage.textContent = 'Uploading source...'; fetch('/upload/source', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.error) { console.error('Source upload error:', data.error); statusMessage.textContent = `Error: ${data.error}`; } else { console.log('Source uploaded:', data); statusMessage.textContent = 'Source uploaded successfully.'; // Optionally, use data.filepath if server sends a path to a served file } }) .catch(error => { console.error('Fetch error for source upload:', error); statusMessage.textContent = 'Upload failed. Check console.'; }); }); // Target File Upload targetFileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; previewFile(file, targetPreviewImage, targetPreviewVideo); // Show preview in main target area const formData = new FormData(); formData.append('file', file); statusMessage.textContent = 'Uploading target...'; fetch('/upload/target', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.error) { console.error('Target upload error:', data.error); statusMessage.textContent = `Error: ${data.error}`; webAppGlobals.target_path_web = null; } else { console.log('Target uploaded:', data); statusMessage.textContent = 'Target uploaded successfully.'; webAppGlobals.target_path_web = data.filepath; // Store the path from backend // If map faces is checked, try to load faces if (mapFacesCheckbox && mapFacesCheckbox.checked) { fetchAndDisplayTargetFaces(); } } }) .catch(error => { console.error('Fetch error for target upload:', error); statusMessage.textContent = 'Upload failed. Check console.'; webAppGlobals.target_path_web = null; }); }); // Settings Update Logic function sendSettings() { const settings = { keep_fps: keepFpsCheckbox ? keepFpsCheckbox.checked : undefined, keep_audio: keepAudioCheckbox ? keepAudioCheckbox.checked : undefined, many_faces: manyFacesCheckbox ? manyFacesCheckbox.checked : undefined, // General many_faces map_faces: mapFacesCheckbox ? mapFacesCheckbox.checked : undefined, // map_faces for backend processing mouth_mask: mouthMaskCheckbox ? mouthMaskCheckbox.checked : undefined, // Add other settings here based on their IDs }; // Clean undefined values Object.keys(settings).forEach(key => settings[key] === undefined && delete settings[key]); console.log('Sending settings:', settings); statusMessage.textContent = 'Updating settings...'; fetch('/update_settings', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(settings) }) .then(response => response.json()) .then(data => { if (data.error) { console.error('Settings update error:', data.error); statusMessage.textContent = `Error: ${data.error}`; } else { console.log('Settings updated:', data); statusMessage.textContent = 'Settings updated.'; } }) .catch(error => { console.error('Fetch error for settings update:', error); statusMessage.textContent = 'Settings update failed. Check console.'; }); } // Add event listeners to general settings checkboxes [keepFpsCheckbox, keepAudioCheckbox, manyFacesCheckbox, mouthMaskCheckbox].forEach(checkbox => { if (checkbox) { checkbox.addEventListener('change', sendSettings); } }); // Special handling for mapFacesCheckbox as it affects UI and backend settings if (mapFacesCheckbox) { mapFacesCheckbox.addEventListener('change', () => { sendSettings(); // Update backend about the map_faces state for processing if (mapFacesCheckbox.checked && webAppGlobals.target_path_web) { faceMapperContainer.style.display = 'block'; fetchAndDisplayTargetFaces(); } else { if (faceMapperContainer) faceMapperContainer.style.display = 'none'; if (faceMapperArea) faceMapperArea.innerHTML = ''; // Clear existing faces if (submitFaceMappingsButton) submitFaceMappingsButton.style.display = 'none'; if (faceMapperStatus) faceMapperStatus.textContent = 'Upload a target image and check "Map Specific Faces" to begin.'; webAppGlobals.currentFaceMappings = []; // Clear mappings } }); } // Initial load of settings (optional, requires backend endpoint /get_settings) // fetch('/get_settings') // .then(response => response.json()) // .then(settings => { // keepFpsCheckbox.checked = settings.keep_fps || false; // keepAudioCheckbox.checked = settings.keep_audio || false; // manyFacesCheckbox.checked = settings.many_faces || false; // mouthMaskCheckbox.checked = settings.mouth_mask || false; // // set other checkboxes // statusMessage.textContent = 'Settings loaded.'; // }) // .catch(error => { // console.error('Error fetching initial settings:', error); // statusMessage.textContent = 'Could not load initial settings.'; // }); // Function to fetch and display target faces for mapping function fetchAndDisplayTargetFaces() { if (!mapFacesCheckbox || !mapFacesCheckbox.checked || !webAppGlobals.target_path_web) { if (faceMapperStatus) faceMapperStatus.textContent = 'Target image not uploaded or "Map Specific Faces" not checked.'; return; } if (faceMapperStatus) faceMapperStatus.textContent = "Loading target faces..."; if (faceMapperContainer) faceMapperContainer.style.display = 'block'; // Show container while loading fetch('/get_target_faces_for_mapping') .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error ${response.status}`) }); } return response.json(); }) .then(targetFaces => { if (!faceMapperArea || !submitFaceMappingsButton || !faceMapperStatus) return; faceMapperArea.innerHTML = ''; // Clear previous faces webAppGlobals.currentFaceMappings = []; // Reset mappings if (targetFaces.error) { faceMapperStatus.textContent = `Error: ${targetFaces.error}`; submitFaceMappingsButton.style.display = 'none'; return; } if (targetFaces.length === 0) { faceMapperStatus.textContent = "No faces found in the target image for mapping."; submitFaceMappingsButton.style.display = 'none'; return; } targetFaces.forEach(face => { const faceDiv = document.createElement('div'); faceDiv.className = 'face-map-item'; // For styling faceDiv.style = "border:1px solid #ccc; padding:10px; text-align:center; margin-bottom:10px;"; faceDiv.innerHTML = `

Target ID: ${face.id}

`; const imgEl = document.createElement('img'); imgEl.src = 'data:image/jpeg;base64,' + face.image_b64; imgEl.style = "max-width:100px; max-height:100px; display:block; margin:auto;"; faceDiv.appendChild(imgEl); const sourceInput = document.createElement('input'); sourceInput.type = 'file'; sourceInput.accept = 'image/*'; sourceInput.id = `source-for-target-${face.id}`; sourceInput.dataset.targetId = face.id; sourceInput.style = "margin-top:10px;"; faceDiv.appendChild(sourceInput); const sourcePreview = document.createElement('img'); sourcePreview.id = `source-preview-for-target-${face.id}`; sourcePreview.style = "max-width:80px; max-height:80px; display:none; margin-top:5px; margin:auto;"; faceDiv.appendChild(sourcePreview); faceMapperArea.appendChild(faceDiv); // Initialize this target face in our mapping array webAppGlobals.currentFaceMappings.push({ target_id: face.id, target_image_b64: face.image_b64, source_file: null, source_b64_preview: null // Will hold base64 for preview from file reader }); // Add event listener for the file input sourceInput.addEventListener('change', (event) => { const file = event.target.files[0]; const targetId = event.target.dataset.targetId; const mappingIndex = webAppGlobals.currentFaceMappings.findIndex(m => m.target_id == targetId); if (file && mappingIndex !== -1) { webAppGlobals.currentFaceMappings[mappingIndex].source_file = file; // Preview for this source const reader = new FileReader(); reader.onload = (e) => { sourcePreview.src = e.target.result; sourcePreview.style.display = 'block'; webAppGlobals.currentFaceMappings[mappingIndex].source_b64_preview = e.target.result; }; reader.readAsDataURL(file); } else if (mappingIndex !== -1) { webAppGlobals.currentFaceMappings[mappingIndex].source_file = null; webAppGlobals.currentFaceMappings[mappingIndex].source_b64_preview = null; sourcePreview.src = '#'; sourcePreview.style.display = 'none'; } }); }); submitFaceMappingsButton.style.display = 'block'; faceMapperStatus.textContent = "Please select a source image for each target face."; }) .catch(error => { console.error('Error fetching/displaying target faces:', error); if (faceMapperStatus) faceMapperStatus.textContent = `Error loading faces: ${error.message || 'Unknown error'}`; if (submitFaceMappingsButton) submitFaceMappingsButton.style.display = 'none'; }); } if (submitFaceMappingsButton) { submitFaceMappingsButton.addEventListener('click', (event) => { event.preventDefault(); // Prevent any default form submission behavior if (faceMapperStatus) faceMapperStatus.textContent = "Submitting mappings..."; const formData = new FormData(); const targetIdsWithSource = []; webAppGlobals.currentFaceMappings.forEach(mapping => { if (mapping.source_file) { formData.append(`source_file_${mapping.target_id}`, mapping.source_file, mapping.source_file.name); targetIdsWithSource.push(mapping.target_id); } }); if (targetIdsWithSource.length === 0) { if (faceMapperStatus) faceMapperStatus.textContent = "No source images selected to map."; // Potentially clear backend maps if no sources are provided? Or backend handles this. // For now, we can choose to send an empty list, or not send at all. // Let's send an empty list to indicate an explicit "clear" or "submit with no new sources". // The backend will then call simplify_maps() which would clear simple_map. } formData.append('target_ids_json', JSON.stringify(targetIdsWithSource)); fetch('/submit_face_mappings', { method: 'POST', body: formData // FormData will set Content-Type to multipart/form-data automatically }) .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error ${response.status}`) }); } return response.json(); }) .then(data => { console.log('Mappings submission response:', data); if (faceMapperStatus) faceMapperStatus.textContent = data.message || "Mappings submitted successfully."; // Optionally hide the face mapper container or update UI // For now, user can manually uncheck "Map Specific Faces" to hide it. // Or, if processing is started, it will also clear. // Consider if mapFacesCheckbox should be set to true in Globals on backend now. // The backend /submit_face_mappings sets Globals.map_faces = True. // We should ensure the checkbox reflects this state if it's not already. if (mapFacesCheckbox && !mapFacesCheckbox.checked && targetIdsWithSource.length > 0) { // If user submitted mappings, but then unchecked "Map Faces" before submission finished, // we might want to re-check it for them, or let sendSettings handle it. // For simplicity, backend sets Globals.map_faces = true. UI should reflect this. // mapFacesCheckbox.checked = true; // This might trigger its change event again. // Better to let sendSettings in mapFacesCheckbox handler manage consistency. } if (targetIdsWithSource.length > 0) { statusMessage.textContent = "Face mappings ready. You can now start processing or live preview with these mappings."; } }) .catch(error => { console.error('Error submitting face mappings:', error); if (faceMapperStatus) faceMapperStatus.textContent = `Error: ${error.message || 'Failed to submit mappings.'}`; }); }); } // Start Processing Logic if (startProcessingButton) { startProcessingButton.addEventListener('click', () => { // When starting processing, clear any live feed from the preview area if (processedPreviewImage) { processedPreviewImage.src = "#"; // Clear src processedPreviewImage.style.display = 'block'; // Or 'none' if you prefer to hide it } // Potentially call /stop_video_feed if live feed was active and using a global camera object that needs release // For now, just clearing the src is the main action. statusMessage.textContent = 'Processing... Please wait.'; statusMessage.textContent = 'Processing... Please wait.'; if(outputArea) outputArea.style.display = 'none'; // Hide previous output // Ensure settings are sent before starting, or rely on them being up-to-date // For simplicity, we assume settings are current from checkbox listeners. // Alternatively, call sendSettings() here and chain the fetch. fetch('/start_processing', { method: 'POST', // No body needed if settings are read from Globals on backend }) .then(response => response.json()) .then(data => { if (data.error) { console.error('Processing error:', data.error); statusMessage.textContent = `Error: ${data.error}`; if(outputArea) outputArea.style.display = 'none'; } else { console.log('Processing complete:', data); statusMessage.textContent = 'Processing complete!'; if (downloadLink && data.download_url) { downloadLink.href = data.download_url; // Backend provides full URL for download downloadLink.textContent = `Download ${data.output_filename || 'processed file'}`; if(outputArea) outputArea.style.display = 'block'; } else { if(outputArea) outputArea.style.display = 'none'; } } }) .catch(error => { console.error('Fetch error for start processing:', error); statusMessage.textContent = 'Processing request failed. Check console.'; if(outputArea) outputArea.style.display = 'none'; }); }); } // Live Preview Logic if (livePreviewButton && processedPreviewImage) { let isLiveFeedActive = false; // State to toggle button livePreviewButton.addEventListener('click', () => { if (!isLiveFeedActive) { processedPreviewImage.src = '/video_feed'; processedPreviewImage.style.display = 'block'; // Make sure it's visible statusMessage.textContent = 'Live feed started. Navigate away or click "Stop Live Feed" to stop.'; livePreviewButton.textContent = 'Stop Live Feed'; isLiveFeedActive = true; if(outputArea) outputArea.style.display = 'none'; // Hide download area } else { // Stop the feed processedPreviewImage.src = '#'; // Clear the image source // Optionally, set a placeholder: processedPreviewImage.src = "placeholder.jpg"; statusMessage.textContent = 'Live feed stopped.'; livePreviewButton.textContent = 'Live Preview'; isLiveFeedActive = false; // Inform the backend to release the camera, if the backend supports it // This is important if the camera is a shared global resource on the server. fetch('/stop_video_feed', { method: 'POST' }) .then(response => response.json()) .then(data => console.log('Stop video feed response:', data)) .catch(error => console.error('Error stopping video feed:', error)); } }); } });