vyluong's picture
PoC deployment
832e106 verified
/**
* PrecisionVoice - Frontend Application Logic
* Handles file upload, transcription requests, and result display.
*/
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const elements = {
// Upload
dropZone: document.getElementById('drop-zone'),
fileInput: document.getElementById('file-input'),
fileInfo: document.getElementById('file-info'),
fileName: document.getElementById('file-name'),
fileSize: document.getElementById('file-size'),
clearBtn: document.getElementById('clear-btn'),
transcribeBtn: document.getElementById('transcribe-btn'),
// Sections
uploadSection: document.getElementById('upload-section'),
processingSection: document.getElementById('processing-section'),
resultsSection: document.getElementById('results-section'),
errorSection: document.getElementById('error-section'),
// Processing
processingStatus: document.getElementById('processing-status'),
progressFill: document.getElementById('progress-fill'),
processingTimer: document.getElementById('processing-timer'),
// Results
speakerCount: document.getElementById('speaker-count'),
durationInfo: document.getElementById('duration-info'),
processingTime: document.getElementById('processing-time'),
transcriptContainer: document.getElementById('transcript-container'),
downloadTxt: document.getElementById('download-txt'),
downloadSrt: document.getElementById('download-srt'),
newUploadBtn: document.getElementById('new-upload-btn'),
// Error
errorMessage: document.getElementById('error-message'),
retryBtn: document.getElementById('retry-btn')
};
let selectedFile = null;
// =====================
// Event Listeners
// =====================
// Click to upload
elements.dropZone.addEventListener('click', () => {
elements.fileInput.click();
});
// File input change
elements.fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelection(e.target.files[0]);
}
});
// Drag and drop
elements.dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
elements.dropZone.classList.add('dragover');
});
elements.dropZone.addEventListener('dragleave', () => {
elements.dropZone.classList.remove('dragover');
});
elements.dropZone.addEventListener('drop', (e) => {
e.preventDefault();
elements.dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFileSelection(e.dataTransfer.files[0]);
}
});
// Clear file
elements.clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
clearFileSelection();
});
// Transcribe button
elements.transcribeBtn.addEventListener('click', () => {
if (selectedFile) {
startTranscription();
}
});
// New upload button
elements.newUploadBtn.addEventListener('click', resetToUpload);
// Retry button
elements.retryBtn.addEventListener('click', resetToUpload);
// =====================
// File Handling
// =====================
function handleFileSelection(file) {
const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/mp4', 'audio/x-m4a',
'audio/ogg', 'audio/flac', 'audio/webm', 'video/webm'];
const allowedExtensions = ['mp3', 'wav', 'm4a', 'ogg', 'flac', 'webm'];
// Check file extension
const ext = file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(ext)) {
showError(`Unsupported file type: .${ext}. Supported: ${allowedExtensions.join(', ')}`);
return;
}
// Check file size (100MB limit)
const maxSize = 100 * 1024 * 1024;
if (file.size > maxSize) {
showError(`File too large. Maximum size: 100MB`);
return;
}
selectedFile = file;
// Update UI
elements.fileName.textContent = file.name;
elements.fileSize.textContent = formatFileSize(file.size);
elements.fileInfo.classList.remove('hidden');
elements.transcribeBtn.disabled = false;
// Hide drop zone text
elements.dropZone.style.display = 'none';
}
function clearFileSelection() {
selectedFile = null;
elements.fileInput.value = '';
elements.fileInfo.classList.add('hidden');
elements.transcribeBtn.disabled = true;
elements.dropZone.style.display = 'block';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// =====================
// Transcription
// =====================
async function startTranscription() {
if (!selectedFile) return;
// Show processing UI
showSection('processing');
updateProgress(100, 'Processing audio... (Check server logs for details)');
// Reset and start timer
let seconds = 0;
elements.processingTimer.textContent = '00:00';
const timerInterval = setInterval(() => {
seconds++;
const m = Math.floor(seconds / 60);
const s = seconds % 60;
elements.processingTimer.textContent = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}, 1000);
try {
const formData = new FormData();
formData.append('file', selectedFile);
const response = await fetch('/api/transcribe', {
method: 'POST',
body: formData
});
clearInterval(timerInterval);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Processing failed');
}
const result = await response.json();
displayResults(result);
} catch (error) {
clearInterval(timerInterval);
console.error('Processing error:', error);
showError(error.message || 'An error occurred during processing');
}
}
function updateProgress(percent, status) {
elements.progressFill.style.width = `${percent}%`;
if (status) {
elements.processingStatus.textContent = status;
}
}
// =====================
// Results Display
// =====================
function displayResults(result) {
// Update metadata
elements.speakerCount.textContent = `${result.num_speakers} speaker${result.num_speakers !== 1 ? 's' : ''}`;
elements.durationInfo.textContent = formatDuration(result.duration);
elements.processingTime.textContent = `${result.processing_time}s`;
// Set download links
elements.downloadTxt.href = result.download_txt;
elements.downloadSrt.href = result.download_srt;
// Render transcript segments
renderTranscript(result.segments);
// Show results section
showSection('results');
}
function renderTranscript(segments) {
elements.transcriptContainer.innerHTML = '';
const speakerColors = {};
let colorIndex = 0;
segments.forEach((segment) => {
// Assign color to speaker
if (!(segment.speaker in speakerColors)) {
colorIndex++;
speakerColors[segment.speaker] = `speaker-${Math.min(colorIndex, 5)}`;
}
const segmentEl = document.createElement('div');
segmentEl.className = `segment ${speakerColors[segment.speaker]}`;
segmentEl.innerHTML = `
<div class="segment-header">
<span class="segment-speaker">${escapeHtml(segment.speaker)}</span>
<span class="segment-time">${formatTime(segment.start)} - ${formatTime(segment.end)}</span>
</div>
<p class="segment-text">${escapeHtml(segment.text)}</p>
`;
elements.transcriptContainer.appendChild(segmentEl);
});
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) {
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
return `${m}:${s.toString().padStart(2, '0')}`;
}
function formatDuration(seconds) {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// =====================
// UI State Management
// =====================
function showSection(section) {
elements.uploadSection.classList.add('hidden');
elements.processingSection.classList.add('hidden');
elements.resultsSection.classList.add('hidden');
elements.errorSection.classList.add('hidden');
switch (section) {
case 'upload':
elements.uploadSection.classList.remove('hidden');
break;
case 'processing':
elements.processingSection.classList.remove('hidden');
break;
case 'results':
elements.resultsSection.classList.remove('hidden');
break;
case 'error':
elements.errorSection.classList.remove('hidden');
break;
}
}
function showError(message) {
elements.errorMessage.textContent = message;
showSection('error');
}
function resetToUpload() {
clearFileSelection();
showSection('upload');
updateProgress(0, 'Uploading file...');
}
});