Kimang18 commited on
Commit
433f1c5
·
1 Parent(s): 2510959

add application files and dependencies based on project audio-annotator

Browse files
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+
17
+ # create the uploads directory inside the container
18
+ RUN mkdir -p /app/uploaded_audio
19
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any
2
+ import os
3
+ import shutil
4
+ import uuid
5
+ from fastapi import FastAPI, Request, UploadFile, File, HTTPException
6
+ from fastapi.responses import HTMLResponse, JSONResponse
7
+ from fastapi.templating import Jinja2Templates
8
+ from fastapi.staticfiles import StaticFiles
9
+ from starlette.middleware.sessions import SessionMiddleware
10
+ from starlette.responses import FileResponse
11
+ from starlette.background import BackgroundTask
12
+ from pydantic import BaseModel
13
+ from datasets import Dataset, Audio
14
+
15
+
16
+ # --- Pydantic Models for Data Validation/Serialization ---
17
+ class SaveAnnotationRequest(BaseModel):
18
+ """Model for the POST request payload to save transcription."""
19
+ index: int
20
+ transcription: str
21
+ speaker: str
22
+
23
+ class AudioDataResponse(BaseModel):
24
+ """Model for the GET response when loading an audio row."""
25
+ index: int
26
+ filename: str
27
+ transcription: str
28
+ speaker: str
29
+ max_index: int
30
+
31
+
32
+ # --- Configuration and Global State ---
33
+
34
+ # Directory to save user-uploaded audio files
35
+ UPLOAD_DIR = "./uploaded_audio"
36
+ # Data structure to hold the annotation state: {f'{user_id}': [{"filename": str, "speaker": str, "transcription": str}]}
37
+ ANNOTATION_DATA: Dict[str, List[Dict[str, Any]]] = {}
38
+ # Index of the audio file currently being displayed/annotated
39
+ current_index: Dict[str, int] = {}
40
+
41
+ # Ensure the upload directory exists
42
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
43
+
44
+
45
+ # --- FastAPI Setup ---
46
+ app = FastAPI(title="Audio Annotation Tool with File Upload")
47
+ app.mount("/static", StaticFiles(directory="./static", html=True), name="static")
48
+ app.add_middleware(SessionMiddleware, secret_key="audio-annotator-application")
49
+ templates = Jinja2Templates(directory="./templates")
50
+
51
+ # --- Utility Functions ---
52
+
53
+
54
+ def load_data_for_index(user_id: int, index: int) -> Dict[str, Any]:
55
+ """Helper to safely fetch data for a given index."""
56
+ if not ANNOTATION_DATA[user_id]:
57
+ return {"index": -1, "filename": "", "transcription": "No files uploaded yet.", "speaker": "No files uploaded yet.", "max_index": 0}
58
+ if 0 <= index < len(ANNOTATION_DATA[user_id]):
59
+ item = ANNOTATION_DATA[user_id][index]
60
+ return {
61
+ "index": index,
62
+ "filename": item['filename'],
63
+ "transcription": item['transcription'],
64
+ "speaker": item['speaker'],
65
+ "max_index": len(ANNOTATION_DATA[user_id])
66
+ }
67
+ else:
68
+ # Wrap around if needed, or handle boundary cases
69
+ raise IndexError("Index out of bounds.")
70
+
71
+ def get_user_directory(user_id):
72
+ return os.path.join(UPLOAD_DIR, f'{user_id}')
73
+
74
+ def serve_index_html(request: Request):
75
+ try:
76
+ user_id = request.session.get('_id', None)
77
+ if user_id is None:
78
+ user_id = str(uuid.uuid4())
79
+ request.session['_id'] = user_id
80
+ # Ensure the upload directory exists
81
+ # print(user_id)
82
+ user_dir = get_user_directory(user_id)
83
+ os.makedirs(user_dir, exist_ok=True)
84
+ ANNOTATION_DATA[user_id] = []
85
+ current_index[user_id] = -1
86
+
87
+ return templates.TemplateResponse("index.html", context={"request": request})
88
+ except FileNotFoundError:
89
+ return HTMLResponse(content="<h1>Server Error: index.html not found.</h1>", status_code=500)
90
+
91
+
92
+ # --- Routes ---
93
+
94
+
95
+ @app.get("/")
96
+ async def index(request: Request):
97
+ """Serves the main application page by reading index.html."""
98
+ return serve_index_html(request)
99
+
100
+
101
+ @app.get("/annotate")
102
+ async def annotate(request: Request):
103
+ """Serves the main application page by reading index.html."""
104
+ return serve_index_html(request)
105
+
106
+
107
+ @app.post("/upload_audio")
108
+ async def upload_audio(request: Request, audio_files: List[UploadFile] = File(...)):
109
+ """Handles multiple audio file uploads from the client."""
110
+ global ANNOTATION_DATA, current_index
111
+ new_files_count = 0
112
+ user_id = request.session.get('_id', None)
113
+
114
+ # Reset index if this is the first upload
115
+ if not ANNOTATION_DATA[user_id]:
116
+ current_index[user_id] = 0
117
+
118
+ for file in audio_files:
119
+ # Construct the full path
120
+ user_dir = get_user_directory(user_id)
121
+ file_path = os.path.join(user_dir, file.filename)
122
+
123
+ # Save the file to disk
124
+ try:
125
+ with open(file_path, "wb") as buffer:
126
+ # Read the file chunk by chunk to handle large files
127
+ shutil.copyfileobj(file.file, buffer)
128
+
129
+ # Update the annotation data structure
130
+ ANNOTATION_DATA[user_id].append({
131
+ "filename": file.filename,
132
+ "transcription": "", # Initialize transcription as empty,
133
+ "speaker": "" # Initialize speaker as empty
134
+ })
135
+ new_files_count += 1
136
+ except Exception as e:
137
+ print(f"Error saving file {file.filename}: {e}")
138
+ raise HTTPException(status_code=500, detail=f"Failed to save file: {file.filename}")
139
+
140
+ return JSONResponse({
141
+ "message": f"Successfully uploaded {new_files_count} files.",
142
+ "total_files": len(ANNOTATION_DATA[user_id])
143
+ })
144
+
145
+
146
+ @app.post("/save_annotation")
147
+ async def save_annotation(request: Request, data: SaveAnnotationRequest):
148
+ """Saves the transcription for the current index."""
149
+ try:
150
+ user_id = request.session.get('_id', None)
151
+ index_to_save = data.index
152
+
153
+ if 0 <= index_to_save < len(ANNOTATION_DATA[user_id]):
154
+ # Update the transcription text
155
+ ANNOTATION_DATA[user_id][index_to_save]["transcription"] = data.transcription
156
+ ANNOTATION_DATA[user_id][index_to_save]["speaker"] = data.speaker
157
+ return JSONResponse({"success": True, "message": f"Row {index_to_save + 1} saved."})
158
+ else:
159
+ raise HTTPException(status_code=400, detail="Invalid index for saving.")
160
+
161
+ except Exception as e:
162
+ print(f"Error during save: {e}")
163
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
164
+
165
+
166
+ @app.get("/load_audio_data/{direction}")
167
+ async def load_audio_data(request: Request, direction: str):
168
+ """Loads the audio data and increments/decrements the current_index."""
169
+ global current_index
170
+ user_id = request.session.get('_id', None)
171
+
172
+ if not ANNOTATION_DATA[user_id]:
173
+ return JSONResponse(load_data_for_index(user_id, -1))
174
+
175
+ new_index = current_index[user_id]
176
+ max_len = len(ANNOTATION_DATA[user_id])
177
+
178
+ if direction == 'next':
179
+ new_index = (current_index[user_id] + 1) % max_len
180
+ elif direction == 'prev':
181
+ # Handles wrapping from 0 back to the last index
182
+ new_index = (current_index[user_id] - 1 + max_len) % max_len
183
+ else:
184
+ # 'current' direction is used for initial load or after upload
185
+ pass
186
+
187
+ try:
188
+ data = load_data_for_index(user_id, new_index)
189
+ # Only update the global index if navigation was successful
190
+ current_index[user_id] = new_index
191
+ return JSONResponse(data)
192
+ except IndexError:
193
+ raise HTTPException(status_code=404, detail="No more audio files to load.")
194
+
195
+
196
+ @app.get("/audio_file/{filename}")
197
+ async def serve_audio_file(request: Request, filename: str):
198
+ """Streams the requested audio file from the upload directory."""
199
+ user_id = request.session.get('_id', None)
200
+ user_dir = get_user_directory(user_id)
201
+ file_path = os.path.join(user_dir, filename)
202
+
203
+ if os.path.exists(file_path):
204
+ # FileResponse sends the file directly, optimized for binary streams
205
+ return FileResponse(file_path, media_type="audio/wav") # Assume WAV for simplicity, use relevant type if required
206
+
207
+ raise HTTPException(status_code=404, detail="Audio file not found.")
208
+
209
+
210
+ @app.get("/download_annotations")
211
+ async def download_annotations(request: Request):
212
+ """Returns the entire annotated dataset as a downloadable JSON file."""
213
+ global current_index, ANNOTATION_DATA
214
+ user_id = request.session.get('_id', None)
215
+
216
+ if not ANNOTATION_DATA[user_id]:
217
+ raise HTTPException(status_code=404, detail="No annotations available to download.")
218
+
219
+ user_dir = get_user_directory(user_id)
220
+ # Convert the dataset to Dataset
221
+ data = {"audio": [], "transcription": [], "speaker": []}
222
+ for item in ANNOTATION_DATA[user_id]:
223
+ data['audio'].append(os.path.join(user_dir, item['filename']))
224
+ data['transcription'].append(item['transcription'])
225
+ data['speaker'].append(item['speaker'])
226
+ # print(data)
227
+ ds = Dataset.from_dict(data).cast_column('audio', Audio(sampling_rate=16000))
228
+ dataset_dir = os.path.join(user_dir, 'dataset')
229
+ ds.save_to_disk(dataset_dir)
230
+
231
+ # Write the content to a temporary file
232
+ zip_dir = os.path.join(user_dir, 'final')
233
+ shutil.make_archive(zip_dir, 'zip', dataset_dir)
234
+ # Create a temporary file path
235
+ temp_file = f'{zip_dir}.zip'
236
+
237
+ def cleanup_file():
238
+ try:
239
+ shutil.rmtree(user_dir)
240
+ os.makedirs(user_dir)
241
+ except Exception as e:
242
+ print(f"Error deleting directory {user_dir}: {e}")
243
+ ANNOTATION_DATA[user_id] = []
244
+ current_index[user_id] = -1
245
+
246
+ # Return the file, which will be deleted after being sent
247
+ return FileResponse(
248
+ path=temp_file,
249
+ filename='annotated_data.zip',
250
+ media_type="application/zip",
251
+ background=BackgroundTask(cleanup_file)
252
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ itsdangerous
5
+ jinja2
6
+ datasets[audio]
7
+ numpy
static/css/style.css ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for the table for better mobile viewing */
2
+ @media (max-width: 768px) {
3
+ .responsive-table tr {
4
+ display: block;
5
+ margin-bottom: 0.75rem;
6
+ border-bottom: 2px solid #e5e7eb;
7
+ }
8
+ .responsive-table th, .responsive-table td {
9
+ display: block;
10
+ text-align: right;
11
+ padding: 0.5rem 1rem;
12
+ }
13
+ .responsive-table td::before {
14
+ content: attr(data-label);
15
+ float: left;
16
+ font-weight: 600;
17
+ color: #4b5563;
18
+ }
19
+ .responsive-table thead {
20
+ display: none;
21
+ }
22
+ }
23
+ /* Ensure the body uses the Inter font, centers content, and stacks vertically */
24
+ body {
25
+ font-family: 'Inter', sans-serif;
26
+ background-color: #f3f4f6;
27
+ display: flex;
28
+ flex-direction: column; /* FIX: Stack children vertically */
29
+ justify-content: flex-start;
30
+ align-items: center; /* FIX: Center content horizontally */
31
+ min-height: 100vh;
32
+ padding-top: 2rem;
33
+ padding-bottom: 2rem;
34
+ }
35
+ [contenteditable="true"] {
36
+ border: 1px solid #c7d2fe;
37
+ background-color: #f0f4ff;
38
+ padding: 0.75rem;
39
+ cursor: text;
40
+ min-height: 5rem;
41
+ border-radius: 0.5rem;
42
+ transition: border-color 0.15s ease-in-out;
43
+ resize: vertical; /* Allow vertical resizing for long transcriptions */
44
+ overflow: auto;
45
+ }
46
+ [contenteditable="true"]:focus {
47
+ outline: none;
48
+ border-color: #4f46e5;
49
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.5);
50
+ }
51
+ /* New CSS for Contenteditable Placeholder */
52
+ [contenteditable][data-placeholder]:empty::before {
53
+ content: attr(data-placeholder);
54
+ color: #9ca3af; /* A light grey color */
55
+ pointer-events: none; /* Allows the user to click the div to type */
56
+ display: block;
57
+ }
static/js/script.js ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // Global variables for tracking state
3
+ let currentDataIndex = -1;
4
+ let maxDataIndex = 0;
5
+ let isUploading = false;
6
+
7
+ const BASE_URL = window.location.origin;
8
+
9
+ // Elements
10
+ const uploadSection = document.getElementById('upload-section');
11
+ const annotationSection = document.getElementById('annotation-section');
12
+ const uploadButton = document.getElementById('upload-button');
13
+ const nextButton = document.getElementById('next-button');
14
+ const prevButton = document.getElementById('prev-button');
15
+ const downloadButton = document.getElementById('download-button');
16
+ const counter = document.getElementById('counter');
17
+ const filenameDisplay = document.getElementById('filename-display');
18
+ const audioPlayer = document.getElementById('audio-player');
19
+ const transcriptionText = document.getElementById('transcription-text');
20
+ const speakerText = document.getElementById('speaker-text');
21
+ const statusMessage = document.getElementById('status-message');
22
+ const audioFilesInput = document.getElementById('audio-files');
23
+
24
+ // --- Routing and Navigation Functions ---
25
+
26
+ /** Changes the URL path and pushes state to history without reloading. */
27
+ function navigateTo(path) {
28
+ window.history.pushState(null, '', path);
29
+ router();
30
+ }
31
+
32
+ /** Controls the view shown based on the current URL path and server state. */
33
+ async function router() {
34
+ // Normalize path to handle both '/' and '/index' as root
35
+ const path = window.location.pathname.replace(/\/+$/, ''); // Remove trailing slash
36
+
37
+ // 1. Reset visibility for all sections
38
+ uploadSection.classList.add('hidden');
39
+ annotationSection.classList.add('hidden');
40
+
41
+ const isAnnotationPath = path === '/annotate';
42
+ console.log(isAnnotationPath);
43
+
44
+ if (isAnnotationPath) {
45
+ // Route: /annotate
46
+ annotationSection.classList.remove('hidden');
47
+
48
+ // Attempt to load data from the server. This determines if a session exists.
49
+ const initialData = await loadAudio('current');
50
+
51
+ // If the backend returns index: -1, it means no files are loaded.
52
+ if (initialData && initialData.index === -1) {
53
+ navigateTo('/');
54
+ }
55
+ } else {
56
+ // Route: / or /index
57
+ // Always show upload form initially, but check if we should redirect to annotation
58
+
59
+ // Check server state to see if any files are currently loaded
60
+ const checkData = await loadAudio('current', { suppressNavigation: true });
61
+
62
+ if (checkData && checkData.index !== -1) {
63
+ // Files exist on the server, redirect to annotation screen
64
+ navigateTo('/annotate');
65
+ } else {
66
+ // No files loaded, stay on upload page
67
+ uploadSection.classList.remove('hidden');
68
+ }
69
+ }
70
+ }
71
+
72
+ // --- Core Application Functions ---
73
+
74
+ /** Sets the status message with a specified color/style. */
75
+ function setStatus(message, type = 'info') {
76
+ let color = 'text-gray-600';
77
+ if (type === 'success') color = 'text-green-600';
78
+ else if (type === 'error') color = 'text-red-600';
79
+ else if (type === 'warn') color = 'text-yellow-600';
80
+ else if (type === 'loading') color = 'text-indigo-600';
81
+
82
+ statusMessage.className = `text-sm font-semibold mt-2 h-5 ${color}`;
83
+ statusMessage.textContent = message;
84
+ }
85
+
86
+ /** Disables/enables navigation buttons. */
87
+ function toggleNavigationButtons(disabled) {
88
+ nextButton.disabled = disabled;
89
+ prevButton.disabled = disabled;
90
+ downloadButton.disabled = disabled;
91
+ audioPlayer.disabled = disabled;
92
+ transcriptionText.contentEditable = disabled ? 'false' : 'true';
93
+ speakerText.contentEditable = disabled ? 'false' : 'true';
94
+ }
95
+
96
+ /** 1. Handles file upload to the server. */
97
+ async function uploadFiles() {
98
+ if (isUploading) return;
99
+ const files = audioFilesInput.files;
100
+
101
+ if (files.length === 0) {
102
+ setStatus("Please select at least one audio file.", 'warn');
103
+ return;
104
+ }
105
+
106
+ isUploading = true;
107
+ uploadButton.disabled = true;
108
+ setStatus(`Uploading ${files.length} file(s)...`, 'loading');
109
+
110
+ try {
111
+ const formData = new FormData();
112
+ for (let i = 0; i < files.length; i++) {
113
+ formData.append("audio_files", files[i]);
114
+ }
115
+
116
+ const response = await fetch(`${BASE_URL}/upload_audio`, {
117
+ method: 'POST',
118
+ body: formData
119
+ });
120
+
121
+ if (!response.ok) {
122
+ throw new Error(`Upload failed with status: ${response.status}`);
123
+ }
124
+ const result = await response.json();
125
+
126
+ setStatus(result.message, 'success');
127
+
128
+ // Switch UI and load the first file
129
+ // uploadSection.classList.add('hidden');
130
+ // annotationSection.classList.remove('hidden');
131
+ // await loadAudio('current');
132
+ navigateTo('/annotate');
133
+
134
+ } catch (error) {
135
+ console.error("Upload error:", error);
136
+ setStatus("An error occurred during upload. Check console for details.", 'error');
137
+ } finally {
138
+ isUploading = false;
139
+ uploadButton.disabled = false;
140
+ }
141
+ }
142
+
143
+ /** 2. Saves the current transcription before navigating. */
144
+ async function saveCurrentAnnotation() {
145
+ if (currentDataIndex < 0 || maxDataIndex === 0) return;
146
+
147
+ const textToSave = transcriptionText.textContent.trim();
148
+ const nameToSave = speakerText.textContent.trim();
149
+ setStatus(`Saving File ${currentDataIndex + 1}...`, 'loading');
150
+
151
+ try {
152
+ const response = await fetch(`${BASE_URL}/save_annotation`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify({
156
+ index: currentDataIndex,
157
+ transcription: textToSave,
158
+ speaker: nameToSave
159
+ })
160
+ });
161
+
162
+ if (!response.ok) {
163
+ const errorDetails = await response.json();
164
+ throw new Error(errorDetails.detail || "Failed to save annotation.");
165
+ }
166
+
167
+ setStatus(`File ${currentDataIndex + 1} saved.`, 'success');
168
+ } catch (error) {
169
+ console.error("Save failed:", error);
170
+ setStatus(`Save failed for File ${currentDataIndex + 1}.`, 'error');
171
+ }
172
+ }
173
+
174
+
175
+ /** 3. Loads audio data based on direction ('next', 'prev', 'current'). */
176
+ async function loadAudio(direction, options = {}) {
177
+ const { suppressNavigation = false } = options;
178
+
179
+ if (!suppressNavigation) {
180
+ toggleNavigationButtons(true);
181
+
182
+ // 1. Save the current state before navigating
183
+ if (direction !== 'current') {
184
+ await saveCurrentAnnotation();
185
+ } else {
186
+ // Clear initial state message for fresh load
187
+ setStatus("Loading audio data...", 'loading');
188
+ }
189
+ }
190
+
191
+ try {
192
+ // The server determines the correct index based on 'direction' and its internal state
193
+ const response = await fetch(`${BASE_URL}/load_audio_data/${direction}`);
194
+
195
+ if (!response.ok) {
196
+ throw new Error(`HTTP error! status: ${response.status}`);
197
+ }
198
+ const data = await response.json();
199
+
200
+ if (data.index === -1) {
201
+ // Handle the initial state before files are uploaded
202
+ counter.textContent = "No files loaded.";
203
+ filenameDisplay.textContent = "";
204
+ transcriptionText.textContent = "Please upload audio files to begin annotation.";
205
+ transcriptionText.contentEditable = 'false';
206
+ speakerText.textContent = "Please upload audio files to begin annotation.";
207
+ speakerText.contentEditable = 'false';
208
+ toggleNavigationButtons(true);
209
+ return data; // Return data for router to check
210
+ }
211
+
212
+ // Update global state
213
+ currentDataIndex = data.index;
214
+ maxDataIndex = data.max_index;
215
+
216
+ // 2. Update text fields and counter
217
+ transcriptionText.textContent = data.transcription;
218
+ speakerText.textContent = data.speaker;
219
+ counter.textContent = `File ${currentDataIndex + 1} of ${maxDataIndex}`;
220
+ filenameDisplay.textContent = `File: ${data.filename}`;
221
+
222
+ // 3. Update the audio player source
223
+ const audioSourceUrl = `${BASE_URL}/audio_file/${data.filename}`;
224
+ audioPlayer.src = audioSourceUrl;
225
+ audioPlayer.load();
226
+
227
+ // Clear status unless a save operation just happened
228
+ if (!statusMessage.textContent.includes("saved")) {
229
+ statusMessage.textContent = "";
230
+ }
231
+
232
+ // Attempt to play the audio immediately
233
+ try {
234
+ await audioPlayer.play();
235
+ } catch (e) {
236
+ // Fail gracefully if autoplay is blocked
237
+ console.log("Autoplay prevented by browser.", e);
238
+ }
239
+
240
+ toggleNavigationButtons(false);
241
+ return data;
242
+
243
+ } catch (error) {
244
+ console.error("Navigation error:", error);
245
+ setStatus("Error loading audio. Please try reloading or uploading files.", 'error');
246
+ transcriptionText.textContent = "Error loading data.";
247
+ speakerText.textContent = "Error loading data.";
248
+ toggleNavigationButtons(true);
249
+ return { index: -1 }; // Return -1 on catastrophic error
250
+ }
251
+ }
252
+
253
+ /** 4. Triggers the download of the annotated dataset. */
254
+ async function downloadAnnotations() {
255
+ downloadButton.disabled = true;
256
+ setStatus("Preparing annotated data for download...", 'loading');
257
+
258
+ try {
259
+ const response = await fetch(`${BASE_URL}/download_annotations`);
260
+ if (!response.ok) {
261
+ throw new Error(`Download failed with status: ${response.status}`);
262
+ }
263
+
264
+ // Get the blob and trigger download
265
+ const blob = await response.blob();
266
+ const url = window.URL.createObjectURL(blob);
267
+ const a = document.createElement('a');
268
+ a.style.display = 'none';
269
+ a.href = url;
270
+ // const date = new Date().toISOString().slice(0, 10);
271
+ // a.download = `annotations_${date}.json`;
272
+ a.download = `annotated_data.zip`;
273
+ document.body.appendChild(a);
274
+ a.click();
275
+ window.URL.revokeObjectURL(url);
276
+
277
+ setStatus("Download complete! Session cleared", 'success');
278
+
279
+ // Navigate back to the upload page to start fresh
280
+ navigateTo('/');
281
+
282
+ } catch (error) {
283
+ console.error("Download error:", error);
284
+ setStatus("Error downloading annotations.", 'error');
285
+ } finally {
286
+ downloadButton.disabled = false;
287
+ }
288
+ }
289
+
290
+
291
+ // Load initial state and set up routing listeners
292
+ window.onload = () => {
293
+ // Handle back/forward buttonn clicks by rerunning the router
294
+ window.onpopstate = router;
295
+
296
+ // Initial route call
297
+ router();
298
+ };
static/logo_white_v2.jpg ADDED
templates/index.html ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Audio Annotator</title>
7
+ <!-- Load Tailwind CSS --><script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" type="text/css" href="/static/css/style.css">
9
+ </head>
10
+ <body>
11
+ <div class="w-full max-w-4xl p-4 md:p-8 bg-white shadow-2xl rounded-xl relative"> <!-- Added 'relative' here -->
12
+ <!-- Logo Added Here -->
13
+ <img id="logo" src="/static/logo_white_v2.jpg" alt="App Logo" class="absolute top-4 left-4 h-24">
14
+
15
+ <h1 class="text-3xl font-extrabold text-gray-800 mt-6 mb-6 text-center"> <!-- Adjusted mt-6 for logo clearance -->Audio Transcription Tool
16
+ </h1>
17
+
18
+ <!-- File Upload Section -->
19
+ <div id="upload-section" class="mb-8 border-2 border-dashed border-gray-300 p-6 rounded-lg bg-gray-50">
20
+ <h2 class="text-xl font-semibold mb-3 text-center text-gray-700">Upload Your Audio Files (WAV/MP3)</h2>
21
+ <input type="file" id="audio-files" multiple accept="audio/*" class="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-100 file:text-indigo-700 hover:file:bg-indigo-200"/>
22
+ <button
23
+ id="upload-button"
24
+ onclick="uploadFiles()"
25
+ class="mt-4 w-full py-2 bg-indigo-600 text-white font-bold rounded-lg shadow-md hover:bg-indigo-700 transition duration-150 disabled:bg-indigo-400"
26
+ >
27
+ 🚀 Start Annotation
28
+ </button>
29
+ </div>
30
+
31
+ <!-- Annotation Section (Hidden until files are loaded) -->
32
+ <div id="annotation-section" class="hidden">
33
+ <div class="mb-4 text-center">
34
+ <span id="counter" class="text-lg font-medium text-indigo-600">No files loaded.</span>
35
+ <span id="filename-display" class="text-sm text-gray-500 block"></span>
36
+ <div id="status-message" class="text-sm font-semibold mt-2 h-5"></div>
37
+ </div>
38
+
39
+ <!-- Data Table --><div class="shadow-md rounded-lg mb-6">
40
+ <table class="min-w-full divide-y divide-gray-200 responsive-table">
41
+ <thead class="bg-indigo-50">
42
+ <tr>
43
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-1/2">
44
+ Speaker (Type Here)
45
+ </th>
46
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-1/2">
47
+ Transcription (Type Here)
48
+ </th>
49
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-1/2">
50
+ Audio Playback
51
+ </th>
52
+ </tr>
53
+ </thead>
54
+ <tbody class="bg-white divide-y divide-gray-200">
55
+ <tr id="data-row">
56
+ <!-- Speaker's name Text Area --><td data-label="Speaker" class="px-6 py-4 whitespace-normal text-sm font-medium text-gray-900">
57
+ <div id="speaker-text"
58
+ contenteditable="true"
59
+ class="w-full text-base"
60
+ data-placeholder="Enter speaker's name here...">
61
+ </div>
62
+ <!-- Transcription Text Area --><td data-label="Transcription" class="px-6 py-4 whitespace-normal text-sm font-medium text-gray-900">
63
+ <div id="transcription-text"
64
+ contenteditable="true"
65
+ class="w-full text-base"
66
+ data-placeholder="Enter your transcription here...">
67
+ </div>
68
+ </td>
69
+ <!-- Audio Player Widget --><td data-label="Audio" class="px-6 py-4 text-sm text-gray-500 flex items-center justify-center">
70
+ <audio id="audio-player" controls class="w-full max-w-xs md:max-w-none" disabled>
71
+ Your browser does not support the audio element.
72
+ </audio>
73
+ </td>
74
+ </tr>
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+
79
+ <div class="flex justify-center space-x-4 flex-wrap"> <!-- Control Buttons -->
80
+ <!-- Previous Button --><button
81
+ id="prev-button"
82
+ onclick="loadAudio('prev')"
83
+ class="mt-3 px-6 py-3 bg-red-500 text-white font-bold rounded-lg shadow-md hover:bg-red-600 transition duration-150 focus:outline-none focus:ring-4 focus:ring-red-500 focus:ring-opacity-50 disabled:bg-gray-400"
84
+ disabled
85
+ >
86
+ &laquo; Previous Audio
87
+ </button>
88
+
89
+ <!-- Next Button --><button
90
+ id="next-button"
91
+ onclick="loadAudio('next')"
92
+ class="mt-3 px-6 py-3 bg-green-600 text-white font-bold rounded-lg shadow-md hover:bg-green-700 transition duration-150 focus:outline-none focus:ring-4 focus:ring-green-500 focus:ring-opacity-50 disabled:bg-gray-400"
93
+ disabled
94
+ >
95
+ Next Audio &raquo;
96
+ </button>
97
+
98
+ <!-- Download Button --><button
99
+ id="download-button"
100
+ onclick="downloadAnnotations()"
101
+ class="mt-3 px-6 py-3 bg-indigo-600 text-white font-bold rounded-lg shadow-md hover:bg-indigo-700 transition duration-150 focus:outline-none focus:ring-4 focus:ring-indigo-500 focus:ring-opacity-50 disabled:bg-gray-400"
102
+ disabled
103
+ >
104
+ ⬇️ Download Annotated Dataset
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ <footer class="mt-8 mb-4 text-center text-gray-500 text-sm w-full max-w-4xl">
110
+ Thanks Huggingface Spaces 🤗
111
+ </footer>
112
+
113
+ <script src="/static/js/script.js"></script>
114
+ </body>
115
+ </html>