Kimang18's picture
add application files and dependencies based on project audio-annotator
433f1c5
// 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();
};