// 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(); };