Spaces:
Sleeping
Sleeping
| // Global variables for tracking state | |
| let currentDataIndex = -1; | |
| let maxDataIndex = 0; | |
| let isUploading = false; | |
| const BASE_URL = window.location.origin; | |
| // Elements | |
| const uploadSection = document.getElementById('upload-section'); | |
| const annotationSection = document.getElementById('annotation-section'); | |
| const uploadButton = document.getElementById('upload-button'); | |
| const nextButton = document.getElementById('next-button'); | |
| const prevButton = document.getElementById('prev-button'); | |
| const downloadButton = document.getElementById('download-button'); | |
| const counter = document.getElementById('counter'); | |
| const filenameDisplay = document.getElementById('filename-display'); | |
| const audioPlayer = document.getElementById('audio-player'); | |
| const transcriptionText = document.getElementById('transcription-text'); | |
| const speakerText = document.getElementById('speaker-text'); | |
| const statusMessage = document.getElementById('status-message'); | |
| const audioFilesInput = document.getElementById('audio-files'); | |
| // --- Routing and Navigation Functions --- | |
| /** Changes the URL path and pushes state to history without reloading. */ | |
| function navigateTo(path) { | |
| window.history.pushState(null, '', path); | |
| router(); | |
| } | |
| /** Controls the view shown based on the current URL path and server state. */ | |
| async function router() { | |
| // Normalize path to handle both '/' and '/index' as root | |
| const path = window.location.pathname.replace(/\/+$/, ''); // Remove trailing slash | |
| // 1. Reset visibility for all sections | |
| uploadSection.classList.add('hidden'); | |
| annotationSection.classList.add('hidden'); | |
| const isAnnotationPath = path === '/annotate'; | |
| console.log(isAnnotationPath); | |
| if (isAnnotationPath) { | |
| // Route: /annotate | |
| annotationSection.classList.remove('hidden'); | |
| // Attempt to load data from the server. This determines if a session exists. | |
| const initialData = await loadAudio('current'); | |
| // If the backend returns index: -1, it means no files are loaded. | |
| if (initialData && initialData.index === -1) { | |
| navigateTo('/'); | |
| } | |
| } else { | |
| // Route: / or /index | |
| // Always show upload form initially, but check if we should redirect to annotation | |
| // Check server state to see if any files are currently loaded | |
| const checkData = await loadAudio('current', { suppressNavigation: true }); | |
| if (checkData && checkData.index !== -1) { | |
| // Files exist on the server, redirect to annotation screen | |
| navigateTo('/annotate'); | |
| } else { | |
| // No files loaded, stay on upload page | |
| uploadSection.classList.remove('hidden'); | |
| } | |
| } | |
| } | |
| // --- Core Application Functions --- | |
| /** Sets the status message with a specified color/style. */ | |
| function setStatus(message, type = 'info') { | |
| let color = 'text-gray-600'; | |
| if (type === 'success') color = 'text-green-600'; | |
| else if (type === 'error') color = 'text-red-600'; | |
| else if (type === 'warn') color = 'text-yellow-600'; | |
| else if (type === 'loading') color = 'text-indigo-600'; | |
| statusMessage.className = `text-sm font-semibold mt-2 h-5 ${color}`; | |
| statusMessage.textContent = message; | |
| } | |
| /** Disables/enables navigation buttons. */ | |
| function toggleNavigationButtons(disabled) { | |
| nextButton.disabled = disabled; | |
| prevButton.disabled = disabled; | |
| downloadButton.disabled = disabled; | |
| audioPlayer.disabled = disabled; | |
| transcriptionText.contentEditable = disabled ? 'false' : 'true'; | |
| speakerText.contentEditable = disabled ? 'false' : 'true'; | |
| } | |
| /** 1. Handles file upload to the server. */ | |
| async function uploadFiles() { | |
| if (isUploading) return; | |
| const files = audioFilesInput.files; | |
| if (files.length === 0) { | |
| setStatus("Please select at least one audio file.", 'warn'); | |
| return; | |
| } | |
| isUploading = true; | |
| uploadButton.disabled = true; | |
| setStatus(`Uploading ${files.length} file(s)...`, 'loading'); | |
| try { | |
| const formData = new FormData(); | |
| for (let i = 0; i < files.length; i++) { | |
| formData.append("audio_files", files[i]); | |
| } | |
| const response = await fetch(`${BASE_URL}/upload_audio`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Upload failed with status: ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| setStatus(result.message, 'success'); | |
| // Switch UI and load the first file | |
| // uploadSection.classList.add('hidden'); | |
| // annotationSection.classList.remove('hidden'); | |
| // await loadAudio('current'); | |
| navigateTo('/annotate'); | |
| } catch (error) { | |
| console.error("Upload error:", error); | |
| setStatus("An error occurred during upload. Check console for details.", 'error'); | |
| } finally { | |
| isUploading = false; | |
| uploadButton.disabled = false; | |
| } | |
| } | |
| /** 2. Saves the current transcription before navigating. */ | |
| async function saveCurrentAnnotation() { | |
| if (currentDataIndex < 0 || maxDataIndex === 0) return; | |
| const textToSave = transcriptionText.textContent.trim(); | |
| const nameToSave = speakerText.textContent.trim(); | |
| setStatus(`Saving File ${currentDataIndex + 1}...`, 'loading'); | |
| try { | |
| const response = await fetch(`${BASE_URL}/save_annotation`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| index: currentDataIndex, | |
| transcription: textToSave, | |
| speaker: nameToSave | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorDetails = await response.json(); | |
| throw new Error(errorDetails.detail || "Failed to save annotation."); | |
| } | |
| setStatus(`File ${currentDataIndex + 1} saved.`, 'success'); | |
| } catch (error) { | |
| console.error("Save failed:", error); | |
| setStatus(`Save failed for File ${currentDataIndex + 1}.`, 'error'); | |
| } | |
| } | |
| /** 3. Loads audio data based on direction ('next', 'prev', 'current'). */ | |
| async function loadAudio(direction, options = {}) { | |
| const { suppressNavigation = false } = options; | |
| if (!suppressNavigation) { | |
| toggleNavigationButtons(true); | |
| // 1. Save the current state before navigating | |
| if (direction !== 'current') { | |
| await saveCurrentAnnotation(); | |
| } else { | |
| // Clear initial state message for fresh load | |
| setStatus("Loading audio data...", 'loading'); | |
| } | |
| } | |
| try { | |
| // The server determines the correct index based on 'direction' and its internal state | |
| const response = await fetch(`${BASE_URL}/load_audio_data/${direction}`); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.index === -1) { | |
| // Handle the initial state before files are uploaded | |
| counter.textContent = "No files loaded."; | |
| filenameDisplay.textContent = ""; | |
| transcriptionText.textContent = "Please upload audio files to begin annotation."; | |
| transcriptionText.contentEditable = 'false'; | |
| speakerText.textContent = "Please upload audio files to begin annotation."; | |
| speakerText.contentEditable = 'false'; | |
| toggleNavigationButtons(true); | |
| return data; // Return data for router to check | |
| } | |
| // Update global state | |
| currentDataIndex = data.index; | |
| maxDataIndex = data.max_index; | |
| // 2. Update text fields and counter | |
| transcriptionText.textContent = data.transcription; | |
| speakerText.textContent = data.speaker; | |
| counter.textContent = `File ${currentDataIndex + 1} of ${maxDataIndex}`; | |
| filenameDisplay.textContent = `File: ${data.filename}`; | |
| // 3. Update the audio player source | |
| const audioSourceUrl = `${BASE_URL}/audio_file/${data.filename}`; | |
| audioPlayer.src = audioSourceUrl; | |
| audioPlayer.load(); | |
| // Clear status unless a save operation just happened | |
| if (!statusMessage.textContent.includes("saved")) { | |
| statusMessage.textContent = ""; | |
| } | |
| // Attempt to play the audio immediately | |
| try { | |
| await audioPlayer.play(); | |
| } catch (e) { | |
| // Fail gracefully if autoplay is blocked | |
| console.log("Autoplay prevented by browser.", e); | |
| } | |
| toggleNavigationButtons(false); | |
| return data; | |
| } catch (error) { | |
| console.error("Navigation error:", error); | |
| setStatus("Error loading audio. Please try reloading or uploading files.", 'error'); | |
| transcriptionText.textContent = "Error loading data."; | |
| speakerText.textContent = "Error loading data."; | |
| toggleNavigationButtons(true); | |
| return { index: -1 }; // Return -1 on catastrophic error | |
| } | |
| } | |
| /** 4. Triggers the download of the annotated dataset. */ | |
| async function downloadAnnotations() { | |
| downloadButton.disabled = true; | |
| setStatus("Preparing annotated data for download...", 'loading'); | |
| try { | |
| const response = await fetch(`${BASE_URL}/download_annotations`); | |
| if (!response.ok) { | |
| throw new Error(`Download failed with status: ${response.status}`); | |
| } | |
| // Get the blob and trigger download | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| // const date = new Date().toISOString().slice(0, 10); | |
| // a.download = `annotations_${date}.json`; | |
| a.download = `annotated_data.zip`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| setStatus("Download complete! Session cleared", 'success'); | |
| // Navigate back to the upload page to start fresh | |
| navigateTo('/'); | |
| } catch (error) { | |
| console.error("Download error:", error); | |
| setStatus("Error downloading annotations.", 'error'); | |
| } finally { | |
| downloadButton.disabled = false; | |
| } | |
| } | |
| // Load initial state and set up routing listeners | |
| window.onload = () => { | |
| // Handle back/forward buttonn clicks by rerunning the router | |
| window.onpopstate = router; | |
| // Initial route call | |
| router(); | |
| }; | |