| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Multilingual Audio Intelligence System</title> |
| <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
| <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script> |
| <style> |
| .upload-area { |
| border: 2px dashed #cbd5e1; |
| transition: all 0.3s ease; |
| } |
| .upload-area:hover { |
| border-color: #3b82f6; |
| background-color: #f8fafc; |
| } |
| .upload-area.dragover { |
| border-color: #2563eb; |
| background-color: #eff6ff; |
| } |
| .progress-bar { |
| background: linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%); |
| } |
| .tab-content { |
| display: none; |
| } |
| .tab-content.active { |
| display: block; |
| } |
| .page-section { |
| display: none; |
| } |
| .page-section.active { |
| display: block; |
| } |
| .loading-spinner { |
| animation: spin 1s linear infinite; |
| } |
| @keyframes spin { |
| from { transform: rotate(0deg); } |
| to { transform: rotate(360deg); } |
| } |
| .hero-pattern { |
| background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.15) 1px, transparent 0); |
| background-size: 20px 20px; |
| } |
| |
| |
| .scrollbar-hide { |
| -ms-overflow-style: none; |
| scrollbar-width: none; |
| } |
| .scrollbar-hide::-webkit-scrollbar { |
| display: none; |
| } |
| |
| .demo-file-option { |
| transition: all 0.2s ease; |
| } |
| |
| .demo-file-option:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| } |
| |
| .demo-file-option.selected { |
| border-color: #3b82f6; |
| background-color: #eff6ff; |
| } |
| |
| .scroll-indicator { |
| transition: all 0.2s ease; |
| } |
| |
| .scroll-indicator.active { |
| background-color: #3b82f6; |
| transform: scale(1.2); |
| } |
| |
| |
| #demo-files-container { |
| scroll-snap-type: x mandatory; |
| } |
| |
| .demo-file-option { |
| scroll-snap-align: start; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| |
| <header class="bg-white shadow-sm border-b"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div class="flex justify-between items-center py-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <h1 class="text-2xl font-bold text-gray-900 cursor-pointer" id="home-link">Audio Intelligence System</h1> |
| </div> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <button id="demo-mode-btn" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
| <i class="fas fa-play-circle mr-2"></i> |
| Demo Mode |
| </button> |
| <button id="processing-mode-btn" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
| <i class="fas fa-cog mr-2"></i> |
| Full Processing |
| </button> |
| |
| |
| |
| |
| |
| |
| <span id="server-status" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"> |
| ⬀ Checking... |
| </span> |
| <button id="system-info-btn" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-info-circle"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </header> |
|
|
| <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> |
| |
| <div id="home-section" class="page-section active"> |
| |
| <div class="relative bg-white overflow-hidden rounded-lg shadow-lg mb-8"> |
| <div class="hero-pattern absolute inset-0"></div> |
| <div class="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8"> |
| <div class="text-center"> |
| <h1 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl"> |
| Multilingual Audio Intelligence |
| </h1> |
| <p class="mt-6 max-w-3xl mx-auto text-xl text-gray-500 leading-relaxed"> |
| Advanced AI-powered speaker diarization, transcription, and translation system. |
| Transform any audio into structured, actionable insights with speaker attribution and cross-lingual understanding. |
| </p> |
| <div class="mt-10 flex justify-center space-x-4"> |
| <button id="get-started-btn" class="inline-flex items-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"> |
| <i class="fas fa-rocket mr-2"></i> |
| Get Started |
| </button> |
| <button id="try-demo-btn" class="inline-flex items-center px-8 py-3 border border-gray-300 text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"> |
| <i class="fas fa-play mr-2"></i> |
| Try Demo |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mb-12"> |
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="p-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-users text-2xl text-blue-600"></i> |
| </div> |
| <div class="ml-4"> |
| <h3 class="text-lg font-medium text-gray-900">Speaker Diarization</h3> |
| <p class="text-sm text-gray-500 mt-1">Identify who spoke when with 95%+ accuracy</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="p-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-language text-2xl text-green-600"></i> |
| </div> |
| <div class="ml-4"> |
| <h3 class="text-lg font-medium text-gray-900">Multilingual Recognition</h3> |
| <p class="text-sm text-gray-500 mt-1">Support for 99+ languages with auto-detection</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="p-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-exchange-alt text-2xl text-purple-600"></i> |
| </div> |
| <div class="ml-4"> |
| <h3 class="text-lg font-medium text-gray-900">Neural Translation</h3> |
| <p class="text-sm text-gray-500 mt-1">High-quality translation to multiple languages</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="p-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-chart-line text-2xl text-red-600"></i> |
| </div> |
| <div class="ml-4"> |
| <h3 class="text-lg font-medium text-gray-900">Interactive Visualization</h3> |
| <p class="text-sm text-gray-500 mt-1">Real-time waveform analysis and insights</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="p-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-download text-2xl text-yellow-600"></i> |
| </div> |
| <div class="ml-4"> |
| <h3 class="text-lg font-medium text-gray-900">Multiple Formats</h3> |
| <p class="text-sm text-gray-500 mt-1">Export as JSON, SRT, TXT, or CSV</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="p-6"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-bolt text-2xl text-orange-600"></i> |
| </div> |
| <div class="ml-4"> |
| <h3 class="text-lg font-medium text-gray-900">Fast Processing</h3> |
| <p class="text-sm text-gray-500 mt-1">14x real-time processing speed</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="px-4 py-5 sm:p-6"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Technical Specifications</h3> |
| <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> |
| <div> |
| <h4 class="text-sm font-medium text-gray-700 mb-2">Supported Audio Formats</h4> |
| <div class="flex flex-wrap gap-2"> |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">WAV</span> |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">MP3</span> |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">OGG</span> |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">FLAC</span> |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">M4A</span> |
| </div> |
| </div> |
| <div> |
| <h4 class="text-sm font-medium text-gray-700 mb-2">Performance</h4> |
| <ul class="text-sm text-gray-600 space-y-1"> |
| <li>β’ Processing: 2-14x real-time</li> |
| <li>β’ Maximum file size: 100MB</li> |
| <li>β’ Recommended duration: Under 30 minutes</li> |
| <li>β’ CPU optimized (no GPU required)</li> |
| </ul> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="processing-section" class="page-section"> |
| <div class="px-4 py-6 sm:px-0"> |
| <div class="text-center mb-8"> |
| <h2 class="text-3xl font-extrabold text-gray-900 sm:text-4xl"> |
| Process Audio File |
| </h2> |
| <p class="mt-4 max-w-2xl mx-auto text-xl text-gray-500"> |
| Upload your audio file and select processing options to get comprehensive analysis. |
| </p> |
| <div class="mt-4"> |
| <span id="processing-mode-indicator" class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"> |
| <i class="fas fa-cog mr-2"></i> |
| Full Processing Mode |
| </span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="px-4 sm:px-0"> |
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="px-4 py-5 sm:p-6"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Select Audio File</h3> |
| |
| <form id="upload-form" enctype="multipart/form-data"> |
| |
| <div id="demo-mode-section" class="mb-6 hidden"> |
| |
| |
| <div class="relative"> |
| |
| <div class="flex justify-between items-center mb-2 sm:hidden"> |
| <button type="button" id="scroll-left" class="p-2 text-gray-500 hover:text-gray-700 disabled:opacity-50" disabled> |
| <i class="fas fa-chevron-left"></i> |
| </button> |
| <button type="button" id="scroll-right" class="p-2 text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-chevron-right"></i> |
| </button> |
| </div> |
| |
| |
| <div id="demo-files-container" class="flex gap-4 overflow-x-auto pb-4 scrollbar-hide" style="scroll-behavior: smooth;"> |
| |
| </div> |
| |
| |
| |
| |
| |
| |
| |
| |
| </div> |
| |
| <input type="hidden" id="selected-demo-file" name="demo_file_id" value=""> |
| </div> |
|
|
| |
| <div id="file-upload-section" class="mb-6"> |
| <div class="upload-area rounded-lg p-6 text-center mb-6" id="upload-area"> |
| <input type="file" id="file-input" name="file" class="hidden" accept=".wav,.mp3,.ogg,.flac,.m4a"> |
| <div id="upload-prompt"> |
| <i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-4"></i> |
| <p class="text-lg text-gray-600 mb-2">Click to upload or drag and drop</p> |
| <p class="text-sm text-gray-500">WAV, MP3, OGG, FLAC, or M4A files up to 100MB</p> |
| </div> |
| <div id="file-info" class="hidden"> |
| <i class="fas fa-file-audio text-4xl text-blue-500 mb-4"></i> |
| <p id="file-name" class="text-lg text-gray-800 mb-2"></p> |
| <p id="file-size" class="text-sm text-gray-500"></p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="audio-preview" class="mb-6 hidden"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Audio Preview</label> |
| <div class="bg-gray-50 p-4 rounded-lg border"> |
| <audio id="audio-player" controls class="w-full mb-4"> |
| Your browser does not support the audio element. |
| </audio> |
| |
| <div id="waveform-container" class="mt-4"> |
| <canvas id="waveform-canvas" class="w-full h-20 bg-gray-100 rounded"></canvas> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="config-options" class="grid grid-cols-1 gap-6 sm:grid-cols-2 mb-6"> |
| <div> |
| <label for="whisper-model" class="block text-sm font-medium text-gray-700">Model Size</label> |
| <select id="whisper-model" name="whisper_model" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"> |
| <option value="tiny">Tiny (Fast, Lower Accuracy)</option> |
| <option value="small" selected>Small (Balanced)</option> |
| <option value="medium">Medium (Better Accuracy)</option> |
| <option value="large">Large (Best Accuracy, Slower)</option> |
| </select> |
| </div> |
| <div> |
| <label for="target-language" class="block text-sm font-medium text-gray-700">Target Language</label> |
| <select id="target-language" name="target_language" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"> |
| <option value="en" selected>English</option> |
| <option value="es">Spanish</option> |
| <option value="fr">French</option> |
| <option value="de">German</option> |
| <option value="it">Italian</option> |
| <option value="pt">Portuguese</option> |
| <option value="zh">Chinese</option> |
| <option value="ja">Japanese</option> |
| <option value="ko">Korean</option> |
| <option value="ar">Arabic</option> |
| </select> |
| </div> |
| </div> |
|
|
| |
| <div id="process-btn-container" class="flex justify-center"> |
| <button type="submit" id="process-btn" class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"> |
| <i class="fas fa-play mr-2"></i> |
| Process Audio |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="progress-section" class="px-4 sm:px-0 mt-6 hidden"> |
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="px-4 py-5 sm:p-6"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Processing Status</h3> |
| <div class="mb-4"> |
| <div class="flex justify-between text-sm text-gray-600 mb-1"> |
| <span id="progress-text">Initializing...</span> |
| <span id="progress-percent">0%</span> |
| </div> |
| <div class="bg-gray-200 rounded-full h-2"> |
| <div id="progress-bar" class="progress-bar h-2 rounded-full transition-all duration-300" style="width: 0%"></div> |
| </div> |
| </div> |
| <p id="progress-detail" class="text-sm text-gray-500">Please wait while we process your audio file...</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="results-section" class="px-4 sm:px-0 mt-6 hidden"> |
| <div class="bg-white overflow-hidden shadow rounded-lg"> |
| <div class="px-4 py-5 sm:p-6"> |
| <div class="flex justify-between items-center mb-6"> |
| <h3 class="text-lg font-medium text-gray-900">Analysis Results</h3> |
| <div class="flex space-x-2"> |
| <button id="download-json" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
| <i class="fas fa-download mr-2"></i>JSON |
| </button> |
| <button id="download-srt" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
| <i class="fas fa-download mr-2"></i>SRT |
| </button> |
| <button id="download-txt" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> |
| <i class="fas fa-download mr-2"></i>Text |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="border-b border-gray-200 mb-6"> |
| <nav class="-mb-px flex space-x-8"> |
| <button class="tab-btn whitespace-nowrap py-2 px-1 border-b-2 border-blue-500 font-medium text-sm text-blue-600" data-tab="transcript"> |
| Transcript & Translation |
| </button> |
| <button class="tab-btn whitespace-nowrap py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="visualization"> |
| Analytics & Insights |
| </button> |
| <button class="tab-btn whitespace-nowrap py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-tab="summary"> |
| Summary |
| </button> |
| </nav> |
| </div> |
|
|
| |
| <div id="transcript-tab" class="tab-content active"> |
| <div id="transcript-content"> |
| |
| </div> |
| </div> |
|
|
| <div id="visualization-tab" class="tab-content"> |
| <div class="grid grid-cols-1 gap-6"> |
| <div id="language-chart" style="width:100%;height:300px;"></div> |
| <div id="speaker-timeline" style="width:100%;height:300px;"></div> |
| </div> |
| </div> |
|
|
| <div id="summary-tab" class="tab-content"> |
| <div id="summary-content"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| |
| <div id="system-info-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden"> |
| <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"> |
| <div class="mt-3"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-lg font-medium text-gray-900">System Information</h3> |
| <button id="close-modal" class="text-gray-400 hover:text-gray-600"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div id="system-info-content"> |
| <div class="loading text-center py-4"> |
| <div class="inline-block"> |
| <i class="fas fa-spinner fa-spin text-2xl text-blue-500"></i> |
| </div> |
| <p class="mt-2 text-gray-600">Loading system information...</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let currentTaskId = null; |
| let progressInterval = null; |
| let isDemoMode = false; |
| |
| |
| const homeSection = document.getElementById('home-section'); |
| const processingSection = document.getElementById('processing-section'); |
| const uploadArea = document.getElementById('upload-area'); |
| const fileInput = document.getElementById('file-input'); |
| const uploadForm = document.getElementById('upload-form'); |
| const processBtn = document.getElementById('process-btn'); |
| const progressSection = document.getElementById('progress-section'); |
| const resultsSection = document.getElementById('results-section'); |
| const systemInfoBtn = document.getElementById('system-info-btn'); |
| const systemInfoModal = document.getElementById('system-info-modal'); |
| const closeModal = document.getElementById('close-modal'); |
| |
| |
| const homeLink = document.getElementById('home-link'); |
| const getStartedBtn = document.getElementById('get-started-btn'); |
| const tryDemoBtn = document.getElementById('try-demo-btn'); |
| const demoModeBtn = document.getElementById('demo-mode-btn'); |
| const processingModeBtn = document.getElementById('processing-mode-btn'); |
| const processingModeIndicator = document.getElementById('processing-mode-indicator'); |
| |
| async function updateServerStatus() { |
| const el = document.getElementById("server-status"); |
| try { |
| const res = await fetch("/health"); |
| if (!res.ok) throw new Error("Bad response"); |
| el.textContent = "⬀ Live"; |
| el.className = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"; |
| } catch (err) { |
| |
| fetch("/").catch(() => { |
| el.textContent = "⬀ Server Down"; |
| el.className = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"; |
| }); |
| |
| el.textContent = "⬀ Error"; |
| el.className = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"; |
| } |
| } |
| |
| |
| document.addEventListener("DOMContentLoaded", updateServerStatus); |
| |
| |
| function showHome() { |
| homeSection.classList.add('active'); |
| processingSection.classList.remove('active'); |
| resetProcessing(); |
| } |
| |
| function showProcessing(demoMode = false) { |
| homeSection.classList.remove('active'); |
| processingSection.classList.add('active'); |
| isDemoMode = demoMode; |
| updateProcessingMode(); |
| resetProcessing(); |
| } |
| |
| function updateProcessingMode() { |
| if (isDemoMode) { |
| processingModeIndicator.innerHTML = '<i class="fas fa-play-circle mr-2"></i>Demo Mode'; |
| processingModeIndicator.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800'; |
| demoModeBtn.className = 'inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500'; |
| processingModeBtn.className = 'inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'; |
| |
| |
| document.getElementById('demo-mode-section').classList.remove('hidden'); |
| document.getElementById('file-upload-section').classList.add('hidden'); |
| document.getElementById('config-options').classList.add('hidden'); |
| |
| |
| document.getElementById('process-btn-container').classList.add('hidden'); |
| |
| |
| loadDemoFiles(); |
| } else { |
| processingModeIndicator.innerHTML = '<i class="fas fa-cog mr-2"></i>Full Processing Mode'; |
| processingModeIndicator.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800'; |
| demoModeBtn.className = 'inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'; |
| processingModeBtn.className = 'inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'; |
| |
| |
| document.getElementById('demo-mode-section').classList.add('hidden'); |
| document.getElementById('file-upload-section').classList.remove('hidden'); |
| document.getElementById('config-options').classList.remove('hidden'); |
| |
| |
| document.getElementById('process-btn-container').classList.remove('hidden'); |
| } |
| } |
| |
| function resetProcessing() { |
| progressSection.classList.add('hidden'); |
| resultsSection.classList.add('hidden'); |
| if (progressInterval) { |
| clearInterval(progressInterval); |
| progressInterval = null; |
| } |
| currentTaskId = null; |
| |
| |
| document.getElementById('upload-prompt').classList.remove('hidden'); |
| document.getElementById('file-info').classList.add('hidden'); |
| document.getElementById('audio-preview').classList.add('hidden'); |
| |
| |
| document.querySelectorAll('.demo-file-option').forEach(opt => { |
| opt.classList.remove('border-blue-500', 'bg-blue-50'); |
| opt.classList.add('border-gray-200'); |
| }); |
| document.getElementById('selected-demo-file').value = ''; |
| |
| uploadForm.reset(); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const demoOptions = document.querySelectorAll('.demo-file-option'); |
| demoOptions.forEach(option => { |
| option.addEventListener('click', () => { |
| |
| document.querySelectorAll('.demo-file-option').forEach(opt => { |
| opt.classList.remove('border-blue-500', 'bg-blue-50'); |
| opt.classList.add('border-gray-200'); |
| }); |
| |
| |
| option.classList.add('border-blue-500', 'bg-blue-50'); |
| option.classList.remove('border-gray-200'); |
| |
| |
| const demoId = option.dataset.demoId; |
| const selectedDemoFile = document.getElementById('selected-demo-file'); |
| if (selectedDemoFile) { |
| selectedDemoFile.value = demoId; |
| } |
| |
| |
| loadDemoAudioPreview(demoId); |
| }); |
| }); |
| }); |
| |
| async function loadDemoAudioPreview(demoId) { |
| try { |
| |
| const audioPreview = document.getElementById('audio-preview'); |
| const audioPlayer = document.getElementById('audio-player'); |
| |
| |
| const demoConfig = { |
| 'yuri_kizaki': { |
| name: 'Yuri Kizaki - Japanese Audio', |
| filename: 'Yuri_Kizaki.mp3', |
| duration: 23.0 |
| }, |
| 'film_podcast': { |
| name: 'French Film Podcast', |
| filename: 'Film_Podcast.mp3', |
| duration: 25.0 |
| } |
| }; |
| |
| if (demoConfig[demoId]) { |
| |
| try { |
| |
| audioPlayer.src = `/demo_audio/${demoConfig[demoId].filename}`; |
| audioPlayer.load(); |
| |
| |
| audioPlayer.addEventListener('loadedmetadata', () => { |
| generateWaveformFromAudio(audioPlayer); |
| }); |
| |
| } catch (e) { |
| console.log('Demo audio file not directly accessible, will be processed on server'); |
| } |
| |
| |
| |
| audioPreview.classList.remove('hidden'); |
| } |
| } catch (error) { |
| console.error('Error loading demo preview:', error); |
| } |
| } |
| |
| function generateDemoWaveform(canvasElement, fileName = 'Audio Preview') { |
| |
| let canvas; |
| if (typeof canvasElement === 'string' || typeof canvasElement === 'number') { |
| |
| canvas = document.getElementById('waveform-canvas'); |
| } else { |
| |
| canvas = canvasElement || document.getElementById('waveform-canvas'); |
| } |
| const ctx = canvas.getContext('2d'); |
| |
| |
| const canvasHeight = canvas.offsetHeight || 80; |
| canvas.width = canvas.offsetWidth * window.devicePixelRatio; |
| canvas.height = canvasHeight * window.devicePixelRatio; |
| ctx.scale(window.devicePixelRatio, window.devicePixelRatio); |
| |
| |
| ctx.clearRect(0, 0, canvas.offsetWidth, canvasHeight); |
| |
| |
| const samples = 100; |
| const barWidth = canvas.offsetWidth / samples; |
| |
| ctx.fillStyle = '#3B82F6'; |
| |
| for (let i = 0; i < samples; i++) { |
| |
| const amplitude = Math.sin(i * 0.1) * Math.random() * 0.8 + 0.2; |
| const height = amplitude * (canvasHeight * 0.8); |
| const x = i * barWidth; |
| const y = (canvasHeight - height) / 2; |
| |
| ctx.fillRect(x, y, barWidth - 1, height); |
| } |
| } |
| |
| function handleFileSelect() { |
| const file = fileInput.files[0]; |
| if (file) { |
| document.getElementById('upload-prompt').classList.add('hidden'); |
| document.getElementById('file-info').classList.remove('hidden'); |
| document.getElementById('file-name').textContent = file.name; |
| document.getElementById('file-size').textContent = formatFileSize(file.size); |
| |
| |
| const audioPreview = document.getElementById('audio-preview'); |
| const audioPlayer = document.getElementById('audio-player'); |
| if (file.type.startsWith('audio/')) { |
| const url = URL.createObjectURL(file); |
| audioPlayer.src = url; |
| audioPreview.classList.remove('hidden'); |
| |
| |
| audioPlayer.addEventListener('loadedmetadata', () => { |
| generateWaveformFromAudio(audioPlayer); |
| }); |
| |
| |
| const canvas = document.getElementById('waveform-canvas'); |
| if (canvas) { |
| generateDemoWaveform(canvas, file.name); |
| } |
| } |
| } |
| } |
| |
| function generateWaveformFromAudio(audioElement, targetCanvas = null, audioSource = null) { |
| console.log('π¨ Generating waveform visualization...'); |
| |
| |
| const canvas = targetCanvas || |
| document.getElementById('demo-waveform-canvas') || |
| document.getElementById('waveform-canvas'); |
| |
| if (!canvas) { |
| console.warn('β οΈ No canvas element found for waveform'); |
| return; |
| } |
| |
| |
| canvas.width = canvas.offsetWidth * (window.devicePixelRatio || 1); |
| canvas.height = (canvas.offsetHeight || 80) * (window.devicePixelRatio || 1); |
| const ctx = canvas.getContext('2d'); |
| ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1); |
| |
| |
| generateDemoWaveform(canvas, 'Audio Preview'); |
| |
| |
| if (audioElement && audioElement.src) { |
| console.log('π Attempting to generate real waveform from audio data...'); |
| |
| try { |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| |
| |
| fetch(audioElement.src) |
| .then(response => response.arrayBuffer()) |
| .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer)) |
| .then(audioBuffer => { |
| console.log('β
Audio decoded successfully, drawing real waveform'); |
| drawWaveformFromBuffer(audioBuffer, canvas); |
| |
| |
| setupLiveWaveform(audioElement, canvas); |
| }) |
| .catch(err => { |
| console.warn("β οΈ Could not decode audio, using static fallback", err); |
| }); |
| |
| } catch (error) { |
| console.warn('β οΈ Web Audio API not available, using static fallback', error); |
| } |
| } |
| |
| function drawWaveformFromBuffer(audioBuffer, canvas) { |
| const ctx = canvas.getContext('2d'); |
| const rawData = audioBuffer.getChannelData(0); |
| const samples = 100; |
| const blockSize = Math.floor(rawData.length / samples); |
| const filteredData = []; |
| |
| |
| for (let i = 0; i < samples; i++) { |
| let sum = 0; |
| for (let j = 0; j < blockSize; j++) { |
| const sample = rawData[i * blockSize + j]; |
| sum += Math.abs(sample); |
| } |
| filteredData.push(sum / blockSize); |
| } |
| |
| |
| ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight); |
| ctx.fillStyle = '#3B82F6'; |
| |
| const barWidth = canvas.offsetWidth / samples; |
| const maxHeight = canvas.offsetHeight * 0.9; |
| |
| filteredData.forEach((val, i) => { |
| const barHeight = val * maxHeight; |
| const x = i * barWidth; |
| const y = (canvas.offsetHeight - barHeight) / 2; |
| ctx.fillRect(x, y, barWidth - 1, barHeight); |
| }); |
| } |
| |
| function setupLiveWaveform(audioElement, canvas) { |
| |
| audioElement.addEventListener('play', () => { |
| console.log('π΅ Starting live waveform visualization...'); |
| |
| try { |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| |
| if (audioContext.state === 'suspended') { |
| audioContext.resume(); |
| } |
| |
| const source = audioContext.createMediaElementSource(audioElement); |
| const analyser = audioContext.createAnalyser(); |
| |
| source.connect(analyser); |
| analyser.connect(audioContext.destination); |
| |
| analyser.fftSize = 256; |
| const bufferLength = analyser.frequencyBinCount; |
| const dataArray = new Uint8Array(bufferLength); |
| |
| const ctx = canvas.getContext('2d'); |
| |
| function drawLiveWaveform() { |
| if (audioElement.paused) return; |
| |
| analyser.getByteFrequencyData(dataArray); |
| |
| ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight); |
| ctx.fillStyle = '#10B981'; |
| |
| const barWidth = canvas.offsetWidth / bufferLength; |
| const maxHeight = canvas.offsetHeight * 0.8; |
| |
| for (let i = 0; i < bufferLength; i++) { |
| const barHeight = (dataArray[i] / 255) * maxHeight; |
| const x = i * barWidth; |
| const y = (canvas.offsetHeight - barHeight) / 2; |
| |
| ctx.fillRect(x, y, barWidth - 1, barHeight); |
| } |
| |
| requestAnimationFrame(drawLiveWaveform); |
| } |
| |
| drawLiveWaveform(); |
| |
| } catch (error) { |
| console.warn('β οΈ Live waveform not available:', error); |
| } |
| }); |
| |
| |
| audioElement.addEventListener('pause', () => { |
| setTimeout(() => { |
| if (audioElement.paused) { |
| generateWaveformFromAudio(audioElement, canvas); |
| } |
| }, 100); |
| }); |
| } |
| } |
| |
| 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]; |
| } |
| |
| |
| homeLink.addEventListener('click', showHome); |
| getStartedBtn.addEventListener('click', () => showProcessing(false)); |
| tryDemoBtn.addEventListener('click', () => showProcessing(true)); |
| demoModeBtn.addEventListener('click', () => showProcessing(true)); |
| processingModeBtn.addEventListener('click', () => showProcessing(false)); |
| |
| |
| uploadArea.addEventListener('click', () => fileInput.click()); |
| uploadArea.addEventListener('dragover', handleDragOver); |
| uploadArea.addEventListener('dragleave', handleDragLeave); |
| uploadArea.addEventListener('drop', handleDrop); |
| fileInput.addEventListener('change', handleFileSelect); |
| |
| function handleDragOver(e) { |
| e.preventDefault(); |
| uploadArea.classList.add('dragover'); |
| } |
| |
| function handleDragLeave(e) { |
| e.preventDefault(); |
| uploadArea.classList.remove('dragover'); |
| } |
| |
| function handleDrop(e) { |
| e.preventDefault(); |
| uploadArea.classList.remove('dragover'); |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| fileInput.files = files; |
| handleFileSelect(); |
| } |
| } |
| |
| |
| uploadForm.addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| |
| |
| if (isDemoMode) { |
| const selectedDemo = document.getElementById('demo-selector').value; |
| if (!selectedDemo) { |
| alert('Please select a demo audio file.'); |
| return; |
| } |
| } else { |
| if (!fileInput.files[0]) { |
| alert('Please select a file to upload.'); |
| return; |
| } |
| } |
| |
| const formData = new FormData(); |
| |
| |
| if (isDemoMode) { |
| formData.append('demo_file_id', document.getElementById('demo-selector').value); |
| formData.append('whisper_model', document.getElementById('whisper-model').value); |
| formData.append('target_language', document.getElementById('target-language').value); |
| } else { |
| formData.append('file', fileInput.files[0]); |
| formData.append('whisper_model', document.getElementById('whisper-model').value); |
| formData.append('target_language', document.getElementById('target-language').value); |
| } |
| |
| try { |
| processBtn.disabled = true; |
| processBtn.innerHTML = '<i class="fas fa-spinner loading-spinner mr-2"></i>Starting...'; |
| |
| |
| let response; |
| if (isDemoMode) { |
| |
| const selector = document.getElementById('demo-selector'); |
| if (!selector || !selector.value) { |
| alert('Please select a demo audio file first.'); |
| return; |
| } |
| const demoId = selector.value; |
| response = await fetch(`/api/process-demo/${demoId}`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| } |
| }); |
| } else { |
| |
| response = await fetch('/api/upload', { |
| method: 'POST', |
| body: formData |
| }); |
| } |
| |
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
| |
| const result = await response.json(); |
| |
| if (result.status === 'complete') { |
| |
| showResults(result.results); |
| } else { |
| |
| currentTaskId = result.task_id; |
| showProgress(); |
| startProgressPolling(); |
| } |
| |
| } catch (error) { |
| console.error('Upload error:', error); |
| alert('Error processing request: ' + error.message); |
| } finally { |
| processBtn.disabled = false; |
| processBtn.innerHTML = '<i class="fas fa-play mr-2"></i>Process Audio'; |
| } |
| }); |
| |
| function showProgress() { |
| progressSection.classList.remove('hidden'); |
| resultsSection.classList.add('hidden'); |
| } |
| |
| function startProgressPolling() { |
| if (!currentTaskId) return; |
| |
| progressInterval = setInterval(async () => { |
| try { |
| const response = await fetch(`/api/status/${currentTaskId}`); |
| |
| if (!response.ok) { |
| throw new Error(`Status fetch failed: ${response.status}`); |
| } |
| |
| const status = await response.json(); |
| |
| if (!status) { |
| console.warn('β οΈ Empty status response'); |
| return; |
| } |
| |
| updateProgress(status); |
| |
| if (status.status === 'complete') { |
| clearInterval(progressInterval); |
| const resultsResponse = await fetch(`/api/results/${currentTaskId}`); |
| |
| if (!resultsResponse.ok) { |
| throw new Error(`Results fetch failed: ${resultsResponse.status}`); |
| } |
| |
| const results = await resultsResponse.json(); |
| |
| if (results && results.results) { |
| showResults(results.results); |
| } else if (results) { |
| |
| showResults(results); |
| } else { |
| console.error('β Invalid results format:', results); |
| alert('Error: No results available'); |
| progressSection.classList.add('hidden'); |
| } |
| } else if (status.status === 'error') { |
| clearInterval(progressInterval); |
| alert('Processing error: ' + status.error); |
| progressSection.classList.add('hidden'); |
| } |
| } catch (error) { |
| console.error('Status polling error:', error); |
| } |
| }, 1000); |
| } |
| |
| function updateProgress(status) { |
| const progressBar = document.getElementById('progress-bar'); |
| const progressText = document.getElementById('progress-text'); |
| const progressPercent = document.getElementById('progress-percent'); |
| const progressDetail = document.getElementById('progress-detail'); |
| |
| const progress = status.progress || 0; |
| progressBar.style.width = `${progress}%`; |
| progressPercent.textContent = `${progress}%`; |
| |
| const statusMessages = { |
| 'initializing': 'Initializing processing pipeline...', |
| 'processing': 'Analyzing audio and identifying speakers...', |
| 'generating_outputs': 'Generating transcripts and translations...', |
| 'complete': 'Processing complete!' |
| }; |
| |
| progressText.textContent = statusMessages[status.status] || 'Processing...'; |
| progressDetail.textContent = isDemoMode ? |
| 'Demo mode - results will be shown shortly.' : |
| 'This may take a few minutes depending on audio length.'; |
| } |
| |
| function showResults(results) { |
| progressSection.classList.add('hidden'); |
| resultsSection.classList.remove('hidden'); |
| |
| console.log('π― Processing results:', results); |
| |
| |
| let segments, summary; |
| |
| if (results.segments && results.summary) { |
| |
| segments = results.segments; |
| summary = results.summary; |
| } else if (results.outputs && results.outputs.json) { |
| |
| try { |
| const jsonData = JSON.parse(results.outputs.json); |
| segments = jsonData.segments || []; |
| summary = jsonData.statistics || results.processing_stats || {}; |
| } catch (e) { |
| console.error('β Failed to parse JSON output:', e); |
| segments = []; |
| summary = {}; |
| } |
| } else if (results.processed_segments) { |
| |
| segments = results.processed_segments.map(seg => { |
| |
| if (typeof seg === 'string' && seg.startsWith('ProcessedSegment(')) { |
| |
| const match = seg.match(/ProcessedSegment\(start_time=([\d.]+), end_time=([\d.]+), speaker_id='([^']+)', original_text='([^']+)', original_language='([^']+)', translated_text='([^']+)'/); |
| if (match) { |
| return { |
| speaker: match[3], |
| start_time: parseFloat(match[1]), |
| end_time: parseFloat(match[2]), |
| text: match[4], |
| translated_text: match[6], |
| language: match[5] |
| }; |
| } |
| } |
| |
| |
| return { |
| speaker: seg.speaker_id || 'Unknown', |
| start_time: seg.start_time, |
| end_time: seg.end_time, |
| text: seg.original_text || seg.text, |
| translated_text: seg.translated_text, |
| language: seg.original_language || seg.language |
| }; |
| }); |
| summary = results.processing_stats || {}; |
| } else { |
| console.error('β Unknown results format:', results); |
| alert('Error: Unable to display results - unknown format'); |
| return; |
| } |
| |
| console.log('β
Processed segments:', segments.length); |
| console.log('β
Summary data:', summary); |
| |
| |
| populateTranscript(segments); |
| |
| |
| populateVisualizations(segments); |
| |
| |
| populateSummary(summary); |
| |
| |
| setupDownloadButtons(); |
| |
| |
| if (!isDemoMode) { |
| scheduleDelayedCleanup(); |
| } |
| } |
| |
| function populateVisualizations(segments) { |
| |
| createLanguageChart(segments); |
| |
| |
| createSpeakerTimeline(segments); |
| |
| } |
| |
| function createLanguageChart(segments) { |
| const languages = {}; |
| const languageDurations = {}; |
| |
| segments.forEach(seg => { |
| const lang = (seg.language || seg.original_language || 'unknown').toUpperCase(); |
| const duration = (seg.end_time || 0) - (seg.start_time || 0); |
| |
| languages[lang] = (languages[lang] || 0) + 1; |
| languageDurations[lang] = (languageDurations[lang] || 0) + duration; |
| }); |
| |
| const data = [{ |
| values: Object.values(languages), |
| labels: Object.keys(languages), |
| type: 'pie', |
| marker: { |
| colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'] |
| }, |
| textinfo: 'label+percent', |
| textposition: 'auto' |
| }]; |
| |
| const layout = { |
| title: { |
| text: 'π Language Distribution', |
| font: { size: 18, family: 'Arial, sans-serif' } |
| }, |
| showlegend: true, |
| height: 300, |
| margin: { t: 50, b: 20, l: 20, r: 20 } |
| }; |
| |
| Plotly.newPlot('language-chart', data, layout, {responsive: true}); |
| } |
| |
| function createSpeakerTimeline(segments) { |
| const speakers = [...new Set(segments.map(seg => seg.speaker || seg.speaker_id || 'Unknown'))]; |
| const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']; |
| |
| const data = speakers.map((speaker, index) => { |
| const speakerSegments = segments.filter(seg => (seg.speaker || seg.speaker_id || 'Unknown') === speaker); |
| |
| return { |
| x: speakerSegments.map(seg => seg.start_time || 0), |
| y: speakerSegments.map(() => speaker), |
| mode: 'markers', |
| type: 'scatter', |
| marker: { |
| size: speakerSegments.map(seg => ((seg.end_time || 0) - (seg.start_time || 0)) * 5), |
| color: colors[index % colors.length], |
| opacity: 0.7 |
| }, |
| name: speaker, |
| text: speakerSegments.map(seg => `${(seg.text || seg.original_text || '').substring(0, 50)}...`), |
| hovertemplate: '%{text}<br>Time: %{x:.1f}s<extra></extra>' |
| }; |
| }); |
| |
| const layout = { |
| title: { |
| text: 'π₯ Speaker Activity Timeline', |
| font: { size: 18, family: 'Arial, sans-serif' } |
| }, |
| xaxis: { title: 'Time (seconds)' }, |
| yaxis: { title: 'Speakers' }, |
| height: 300, |
| margin: { t: 50, b: 50, l: 100, r: 20 } |
| }; |
| |
| Plotly.newPlot('speaker-timeline', data, layout, {responsive: true}); |
| } |
| |
| function populateTranscript(segments) { |
| const transcriptContent = document.getElementById('transcript-content'); |
| transcriptContent.innerHTML = ''; |
| |
| segments.forEach((segment, index) => { |
| const segmentDiv = document.createElement('div'); |
| segmentDiv.className = 'mb-6 p-4 border border-gray-200 rounded-lg bg-white shadow-sm'; |
| |
| segmentDiv.innerHTML = ` |
| <div class="flex justify-between items-start mb-3"> |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"> |
| ${segment.speaker} |
| </span> |
| <span class="text-sm text-gray-500"> |
| ${formatTime(segment.start_time)} - ${formatTime(segment.end_time)} |
| </span> |
| </div> |
| |
| <div class="space-y-3"> |
| <div class="bg-gray-50 p-3 rounded-lg"> |
| <div class="flex items-center mb-2"> |
| <i class="fas fa-microphone text-gray-600 mr-2"></i> |
| <span class="text-sm font-medium text-gray-700">Original (${(segment.language || segment.original_language || 'Unknown').toUpperCase()})</span> |
| </div> |
| <p class="text-gray-800 leading-relaxed">${segment.text}</p> |
| </div> |
| |
| ${segment.translated_text && segment.translated_text !== segment.text && (segment.language || segment.original_language) !== 'en' ? ` |
| <div class="bg-blue-50 p-3 rounded-lg"> |
| <div class="flex items-center mb-2"> |
| <i class="fas fa-language text-blue-600 mr-2"></i> |
| <span class="text-sm font-medium text-blue-700">English Translation</span> |
| </div> |
| <p class="text-blue-800 leading-relaxed italic">${segment.translated_text}</p> |
| </div> |
| ` : ''} |
| </div> |
| `; |
| |
| transcriptContent.appendChild(segmentDiv); |
| }); |
| } |
| |
| function populateSummary(summary) { |
| const summaryContent = document.getElementById('summary-content'); |
| summaryContent.innerHTML = ` |
| <div class="grid grid-cols-2 gap-4"> |
| <div class="bg-gray-50 p-4 rounded-lg"> |
| <h4 class="text-sm font-medium text-gray-700">Total Duration</h4> |
| <p class="text-2xl font-bold text-gray-900">${formatTime(summary.total_duration || 0)}</p> |
| </div> |
| <div class="bg-gray-50 p-4 rounded-lg"> |
| <h4 class="text-sm font-medium text-gray-700">Speakers Detected</h4> |
| <p class="text-2xl font-bold text-gray-900">${summary.num_speakers || 0}</p> |
| </div> |
| <div class="bg-gray-50 p-4 rounded-lg"> |
| <h4 class="text-sm font-medium text-gray-700">Speech Segments</h4> |
| <p class="text-2xl font-bold text-gray-900">${summary.num_segments || 0}</p> |
| </div> |
| <div class="bg-gray-50 p-4 rounded-lg"> |
| <h4 class="text-sm font-medium text-gray-700">Processing Time</h4> |
| <p class="text-2xl font-bold text-gray-900">${Math.round(summary.processing_time || 0)}s</p> |
| </div> |
| </div> |
| <div class="mt-4"> |
| <h4 class="text-sm font-medium text-gray-700 mb-2">Languages Detected</h4> |
| <div class="flex flex-wrap gap-2"> |
| ${(summary.languages || []).map(lang => |
| `<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">${lang}</span>` |
| ).join('')} |
| </div> |
| </div> |
| `; |
| } |
| |
| function formatTime(seconds) { |
| const minutes = Math.floor(seconds / 60); |
| const secs = Math.floor(seconds % 60); |
| return `${minutes}:${secs.toString().padStart(2, '0')}`; |
| } |
| |
| function setupDownloadButtons() { |
| document.getElementById('download-json').onclick = () => downloadFile('json'); |
| document.getElementById('download-srt').onclick = () => downloadFile('srt'); |
| document.getElementById('download-txt').onclick = () => downloadFile('txt'); |
| } |
| |
| function downloadFile(format) { |
| if (currentTaskId) { |
| window.open(`/api/download/${currentTaskId}/${format}`, '_blank'); |
| } |
| } |
| |
| |
| document.querySelectorAll('.tab-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const tabName = e.target.dataset.tab; |
| |
| |
| document.querySelectorAll('.tab-btn').forEach(b => { |
| b.classList.remove('border-blue-500', 'text-blue-600'); |
| b.classList.add('border-transparent', 'text-gray-500'); |
| }); |
| e.target.classList.add('border-blue-500', 'text-blue-600'); |
| e.target.classList.remove('border-transparent', 'text-gray-500'); |
| |
| |
| document.querySelectorAll('.tab-content').forEach(content => { |
| content.classList.remove('active'); |
| }); |
| document.getElementById(`${tabName}-tab`).classList.add('active'); |
| }); |
| }); |
| |
| |
| systemInfoBtn.addEventListener('click', async () => { |
| systemInfoModal.classList.remove('hidden'); |
| |
| const content = document.getElementById('system-info-content'); |
| content.innerHTML = ` |
| <div class="loading text-center py-4 flex flex-col items-center"> |
| <div class="mb-2"> |
| <i class="fas fa-spinner text-2xl text-blue-500 animate-spin"></i> |
| </div> |
| <p class="text-gray-600">Loading system information...</p> |
| </div> |
| `; |
| |
| |
| |
| |
| |
| |
| |
| |
| try { |
| const response = await fetch('/api/system-info'); |
| const info = await response.json(); |
| |
| const statusColors = { |
| green: "bg-green-100 text-green-800", |
| yellow: "bg-yellow-100 text-yellow-800", |
| red: "bg-red-100 text-red-800", |
| gray: "bg-gray-100 text-gray-800" |
| }; |
| |
| const colorClass = statusColors[info.statusColor] || statusColors.gray; |
| |
| content.innerHTML = ` |
| <div class="space-y-3"> |
| <div> |
| <span class="font-medium">Status:</span> |
| <span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}"> |
| ⬀ ${info.status} |
| </span> |
| </div> |
| <div> |
| <span class="font-medium">Version:</span> |
| <span class="ml-2 text-gray-600">${info.version}</span> |
| </div> |
| <div> |
| <span class="font-medium">Features:</span> |
| <div class="mt-2 flex flex-wrap gap-1"> |
| ${info.features.map(feature => |
| `<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800">${feature}</span>` |
| ).join('')} |
| </div> |
| </div> |
| </div> |
| `; |
| } catch (error) { |
| content.innerHTML = `<p class="text-red-600">Error loading system information</p>`; |
| } |
| }); |
| |
| closeModal.addEventListener('click', () => { |
| systemInfoModal.classList.add('hidden'); |
| }); |
| |
| |
| systemInfoModal.addEventListener('click', (e) => { |
| if (e.target === systemInfoModal) { |
| systemInfoModal.classList.add('hidden'); |
| } |
| }); |
| |
| |
| updateProcessingMode(); |
| |
| |
| if (isDemoMode) { |
| loadDemoFiles(); |
| } |
| |
| |
| let demoFiles = []; |
| |
| |
| function createFallbackDemoFiles() { |
| demoFiles = [ |
| { |
| id: "yuri_kizaki", |
| name: "Yuri Kizaki", |
| filename: "Yuri_Kizaki.mp3", |
| language: "ja", |
| description: "Japanese audio message about website communication", |
| duration: "00:01:45", |
| available: true, |
| download_status: "ready" |
| }, |
| { |
| id: "film_podcast", |
| name: "Film Podcast", |
| filename: "Film_Podcast.mp3", |
| language: "fr", |
| description: "French podcast discussing various films and cinema", |
| duration: "00:03:32", |
| available: true, |
| download_status: "ready" |
| }, |
| { |
| id: "tamil_interview", |
| name: "Tamil Wikipedia Interview", |
| filename: "Tamil_Wikipedia_Interview.ogg", |
| language: "ta", |
| description: "Discussion on Tamil Wikipedia and collaborative knowledge sharing", |
| duration: "00:36:17", |
| available: true, |
| download_status: "ready" |
| }, |
| { |
| id: "car_trouble", |
| name: "Car Trouble", |
| filename: "Car_Trouble.mp3", |
| language: "hi", |
| description: "Conversation about waiting for a mechanic and basic assistance", |
| duration: "00:02:45", |
| available: true, |
| download_status: "ready" |
| } |
| ]; |
| populateDemoFiles(); |
| |
| |
| setTimeout(() => { |
| selectDemoFile(demoFiles[0].id); |
| const firstOption = document.querySelector(`[data-demo-id="${demoFiles[0].id}"]`); |
| if (firstOption) { |
| firstOption.classList.add('border-blue-500', 'bg-blue-50'); |
| firstOption.classList.remove('border-gray-200'); |
| } |
| }, 100); |
| } |
| |
| |
| function getIconForLanguage(language) { |
| const icons = { |
| 'ja': 'fas fa-microphone', |
| 'fr': 'fas fa-podcast', |
| 'ta': 'fas fa-headphones', |
| 'hi': 'fas fa-volume-up' |
| }; |
| return icons[language] || 'fas fa-music'; |
| } |
| |
| |
| function getStatusClass(status) { |
| const classes = { |
| 'pending': 'bg-gray-100 text-gray-800', |
| 'downloading': 'bg-yellow-100 text-yellow-800', |
| 'completed': 'bg-green-100 text-green-800', |
| 'ready': 'bg-green-100 text-green-800', |
| 'failed': 'bg-red-100 text-red-800' |
| }; |
| return classes[status] || 'bg-gray-100 text-gray-800'; |
| } |
| |
| |
| function getStatusText(status) { |
| const texts = { |
| 'pending': 'Pending', |
| 'downloading': 'Downloading...', |
| 'completed': 'Available', |
| 'ready': 'Ready', |
| 'failed': 'Failed' |
| }; |
| return texts[status] || 'Unknown'; |
| } |
| |
| |
| function selectDemoFile(demoId) { |
| document.getElementById('selected-demo-file').value = demoId; |
| console.log('Selected demo file:', demoId); |
| } |
| |
| |
| function updateScrollIndicators() { |
| const container = document.getElementById('demo-files-container'); |
| const indicators = document.querySelectorAll('.scroll-indicator'); |
| const scrollLeft = container.scrollLeft; |
| const maxScroll = container.scrollWidth - container.clientWidth; |
| |
| |
| const leftBtn = document.getElementById('scroll-left'); |
| const rightBtn = document.getElementById('scroll-right'); |
| |
| if (leftBtn) leftBtn.disabled = scrollLeft <= 0; |
| if (rightBtn) rightBtn.disabled = scrollLeft >= maxScroll; |
| |
| |
| const scrollPercentage = maxScroll > 0 ? scrollLeft / maxScroll : 0; |
| const activeIndex = Math.floor(scrollPercentage * (indicators.length - 1)); |
| |
| indicators.forEach((indicator, index) => { |
| indicator.classList.toggle('active', index === activeIndex); |
| }); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const container = document.getElementById('demo-files-container'); |
| if (container) { |
| container.addEventListener('scroll', updateScrollIndicators); |
| } |
| |
| |
| const leftBtn = document.getElementById('scroll-left'); |
| const rightBtn = document.getElementById('scroll-right'); |
| |
| if (leftBtn) { |
| leftBtn.addEventListener('click', () => { |
| container.scrollBy({ left: -300, behavior: 'smooth' }); |
| }); |
| } |
| |
| if (rightBtn) { |
| rightBtn.addEventListener('click', () => { |
| container.scrollBy({ left: 300, behavior: 'smooth' }); |
| }); |
| } |
| }); |
| |
| |
| const demoModeToggle = document.getElementById('demo-mode-toggle'); |
| if (demoModeToggle) { |
| demoModeToggle.addEventListener('change', function() { |
| if (this.checked) { |
| loadDemoFiles(); |
| } |
| }); |
| |
| |
| if (demoModeToggle.checked) { |
| loadDemoFiles(); |
| } |
| } |
| |
| |
| async function loadDemoFiles() { |
| console.log('π Loading demo files from API...'); |
| try { |
| const response = await fetch('/api/demo-files'); |
| console.log('π‘ API Response status:', response.status); |
| |
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| console.log('π API returned demo files:', data); |
| |
| |
| if (data.demo_files && Array.isArray(data.demo_files)) { |
| demoFiles = data.demo_files; |
| console.log('β
Demo files loaded from API:', demoFiles.length); |
| console.log('π Demo files details:', demoFiles); |
| populateDemoFiles(); |
| } else if (Array.isArray(data)) { |
| demoFiles = data; |
| console.log('β
Demo files loaded as direct array:', demoFiles.length); |
| populateDemoFiles(); |
| } else { |
| console.warn('β οΈ Unexpected API response format, using fallback'); |
| createFallbackDemoFiles(); |
| } |
| } catch (error) { |
| console.error('β Failed to load demo files:', error); |
| console.error('Error details:', error.message); |
| createFallbackDemoFiles(); |
| } |
| } |
| |
| |
| function populateDemoFiles() { |
| console.log('ποΈ Starting populateDemoFiles...'); |
| console.log('π Demo files to populate:', demoFiles); |
| |
| const container = document.getElementById('demo-files-container'); |
| console.log('π― Container element:', container); |
| |
| if (!container) { |
| console.error('β Demo files container not found! Expected element with id="demo-files-container"'); |
| return; |
| } |
| |
| console.log('β
Container found, clearing existing content...'); |
| container.innerHTML = ''; |
| |
| if (demoFiles.length === 0) { |
| console.warn('β οΈ No demo files to display'); |
| container.innerHTML = '<p class="text-gray-500 text-center py-8">No demo files available</p>'; |
| return; |
| } |
| |
| console.log(`π§ Creating single demo file selector for ${demoFiles.length} files...`); |
| console.log('π Available demo files:', demoFiles.map(f => ({ id: f.id, name: f.name }))); |
| |
| |
| const demoContainer = document.createElement('div'); |
| demoContainer.className = 'w-full'; |
| |
| |
| const selectorHTML = ` |
| <div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 border border-blue-200 w-full"> |
| <div class="flex items-center space-x-4 mb-4"> |
| <div class="flex-shrink-0"> |
| <div class="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center"> |
| <i class="fas fa-play text-white text-lg"></i> |
| </div> |
| </div> |
| <div class="flex-1"> |
| <label for="demo-selector" class="block text-sm font-medium text-gray-700 mb-2"> |
| Choose a sample: |
| </label> |
| <select id="demo-selector" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> |
| ${demoFiles.map(file => |
| `<option value="${file.id}" data-name="${file.name}" data-filename="${file.filename || ''}" data-description="${file.description || ''}" data-language="${file.language || 'Unknown'}" data-duration="${file.duration || 'Unknown'}"> |
| ${file.name} |
| </option>` |
| ).join('')} |
| </select> |
| </div> |
| </div> |
| |
| <!-- Demo file details (will be updated when selection changes) --> |
| <div id="demo-details" class="bg-white rounded-lg p-4 border border-gray-200"> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> |
| <div> |
| <span class="font-medium text-gray-600">Language:</span> |
| <span id="demo-language" class="ml-2 text-gray-800">${demoFiles[0]?.language || 'Unknown'}</span> |
| </div> |
| <div> |
| <span class="font-medium text-gray-600">Duration:</span> |
| <span id="demo-duration" class="ml-2 text-gray-800">${demoFiles[0]?.duration || 'Unknown'}</span> |
| </div> |
| <div> |
| <span class="font-medium text-gray-600">Status:</span> |
| <span class="ml-2 px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs">Ready</span> |
| </div> |
| </div> |
| <div class="mt-3"> |
| <span class="font-medium text-gray-600">Description:</span> |
| <p id="demo-description" class="mt-1 text-gray-700">${demoFiles[0]?.description || 'Demo audio file for testing'}</p> |
| </div> |
| </div> |
| |
| <!-- Audio Preview and Processing --> |
| <div class="mt-4 space-y-4"> |
| <!-- Audio Preview --> |
| <div class="bg-white rounded-lg p-4 border border-gray-200"> |
| <h4 class="text-sm font-medium text-gray-700 mb-3"> |
| <i class="fas fa-headphones mr-2"></i>Audio Preview |
| </h4> |
| <audio id="demo-audio-player" controls class="w-full mb-3"> |
| <source id="demo-audio-source" type="audio/mpeg"> |
| Your browser does not support the audio element. |
| </audio> |
| <!-- Waveform Visualization --> |
| <div id="demo-waveform-container" class="mt-3"> |
| <canvas id="demo-waveform-canvas" class="w-full h-16 bg-gray-50 rounded border"></canvas> |
| </div> |
| </div> |
| |
| <!-- Demo Results Section --> |
| <div class="flex justify-center"> |
| <button onclick="loadDemoResults()" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"> |
| <i class="fas fa-eye mr-2"></i>View Processing Results |
| </button> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| demoContainer.innerHTML = selectorHTML; |
| container.appendChild(demoContainer); |
| |
| |
| const selector = document.getElementById('demo-selector'); |
| if (selector) { |
| selector.addEventListener('change', function() { |
| const selectedOption = this.options[this.selectedIndex]; |
| updateDemoDetails(selectedOption); |
| loadDemoAudio(this.value, selectedOption.dataset.filename || selectedOption.dataset.name); |
| }); |
| |
| |
| if (selector.options.length > 0) { |
| const firstOption = selector.options[0]; |
| loadDemoAudio(selector.value, firstOption.dataset.name); |
| } |
| } |
| |
| console.log('β
Demo files populated successfully'); |
| } |
| |
| |
| function updateDemoDetails(selectedOption) { |
| const languageEl = document.getElementById('demo-language'); |
| const durationEl = document.getElementById('demo-duration'); |
| const descriptionEl = document.getElementById('demo-description'); |
| |
| if (languageEl) languageEl.textContent = selectedOption.dataset.language || 'Unknown'; |
| if (durationEl) durationEl.textContent = selectedOption.dataset.duration || 'Unknown'; |
| if (descriptionEl) descriptionEl.textContent = selectedOption.dataset.description || 'Demo audio file for testing'; |
| |
| console.log('β
Updated demo details for:', selectedOption.dataset.name); |
| } |
| |
| |
| function loadDemoAudio(demoId, fileName) { |
| console.log('π΅ Loading demo audio:', demoId, fileName); |
| |
| const audioPlayer = document.getElementById('demo-audio-player'); |
| const audioSource = document.getElementById('demo-audio-source'); |
| const waveformCanvas = document.getElementById('demo-waveform-canvas'); |
| |
| if (!audioPlayer || !audioSource || !waveformCanvas) { |
| console.warn('β οΈ Demo audio elements not found'); |
| return; |
| } |
| |
| |
| let actualFileName = fileName; |
| |
| |
| if (demoFiles && demoFiles.length > 0) { |
| const demoFile = demoFiles.find(file => file.id === demoId); |
| if (demoFile && demoFile.filename) { |
| actualFileName = demoFile.filename; |
| } |
| } else { |
| |
| const filenameMap = { |
| 'yuri_kizaki': 'Yuri_Kizaki.mp3', |
| 'film_podcast': 'Film_Podcast.mp3', |
| 'car_trouble': 'Car_Trouble.mp3', |
| 'tamil_interview': 'Tamil_Wikipedia_Interview.ogg' |
| }; |
| |
| if (filenameMap[demoId]) { |
| actualFileName = filenameMap[demoId]; |
| } |
| } |
| |
| console.log(`π΅ Mapped ${demoId} -> ${actualFileName}`); |
| |
| |
| const audioPath = `/demo_audio/${actualFileName}`; |
| |
| console.log(`π Loading audio from: ${audioPath}`); |
| |
| |
| audioSource.src = audioPath; |
| audioPlayer.load(); |
| |
| |
| const onCanPlay = function() { |
| console.log('β
Demo audio loaded successfully'); |
| generateWaveformFromAudio(audioPlayer, waveformCanvas, audioSource); |
| audioPlayer.removeEventListener('canplaythrough', onCanPlay); |
| audioPlayer.removeEventListener('error', onError); |
| }; |
| |
| const onError = function() { |
| console.warn(`β Failed to load audio: ${audioPath}`); |
| console.log(`β οΈ Generating placeholder waveform for: ${actualFileName}`); |
| generateDemoWaveform(waveformCanvas, actualFileName); |
| audioPlayer.removeEventListener('canplaythrough', onCanPlay); |
| audioPlayer.removeEventListener('error', onError); |
| }; |
| |
| audioPlayer.addEventListener('canplaythrough', onCanPlay); |
| audioPlayer.addEventListener('error', onError); |
| } |
| |
| |
| |
| |
| |
| |
| async function loadDemoResults() { |
| const selector = document.getElementById('demo-selector'); |
| if (!selector || !selector.value) { |
| alert('Please select a demo audio file first.'); |
| return; |
| } |
| |
| const demoId = selector.value; |
| console.log('π― Loading demo results for:', demoId); |
| |
| try { |
| |
| showProgress(); |
| const progressBar = document.querySelector('.progress-bar-fill'); |
| if (progressBar) progressBar.style.width = '50%'; |
| |
| |
| const response = await fetch(`/api/process-demo/${demoId}`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| } |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| } |
| |
| const result = await response.json(); |
| console.log('π Demo results received:', result); |
| |
| |
| if (progressBar) progressBar.style.width = '100%'; |
| |
| setTimeout(() => { |
| if (result.status === 'complete') { |
| showResults(result.results); |
| } else { |
| throw new Error('Demo processing failed: ' + (result.error || 'Unknown error')); |
| } |
| }, 500); |
| |
| } catch (error) { |
| console.error('β Demo results error:', error); |
| alert('Error loading demo results: ' + error.message); |
| |
| |
| const progressSection = document.getElementById('progress-section'); |
| if (progressSection) progressSection.classList.add('hidden'); |
| } |
| } |
| |
| |
| function processAudio() { |
| console.log('π― Processing audio...'); |
| |
| |
| if (isDemoMode) { |
| const selector = document.getElementById('demo-selector'); |
| if (!selector) { |
| alert('Demo selector not found'); |
| return; |
| } |
| |
| const selectedId = selector.value; |
| const selectedOption = selector.options[selector.selectedIndex]; |
| const fileName = selectedOption.dataset.name; |
| |
| console.log('π― Processing demo file:', selectedId, fileName); |
| } |
| |
| |
| const uploadForm = document.getElementById('upload-form'); |
| if (uploadForm) { |
| uploadForm.dispatchEvent(new Event('submit')); |
| } else { |
| alert('Upload form not found'); |
| } |
| } |
| |
| console.log('Demo files population completed'); |
| |
| |
| function getStatusClass(status) { |
| switch(status) { |
| case 'ready': return 'bg-green-100 text-green-800'; |
| case 'processing': return 'bg-yellow-100 text-yellow-800'; |
| case 'downloading': return 'bg-blue-100 text-blue-800'; |
| case 'error': return 'bg-red-100 text-red-800'; |
| default: return 'bg-gray-100 text-gray-800'; |
| } |
| } |
| |
| function getStatusText(status) { |
| switch(status) { |
| case 'ready': return 'β
Ready'; |
| case 'processing': return 'β³ Processing'; |
| case 'downloading': return 'β¬οΈ Downloading'; |
| case 'error': return 'β Error'; |
| default: return 'βͺ Unknown'; |
| } |
| } |
| |
| function getIconForLanguage(language) { |
| const lang = language.toLowerCase(); |
| if (lang.includes('japanese') || lang.includes('ja')) return 'fas fa-flag'; |
| if (lang.includes('french') || lang.includes('fr')) return 'fas fa-flag'; |
| if (lang.includes('tamil') || lang.includes('ta')) return 'fas fa-flag'; |
| if (lang.includes('hindi') || lang.includes('hi')) return 'fas fa-flag'; |
| return 'fas fa-globe'; |
| } |
| |
| |
| function triggerCleanup() { |
| |
| if (isDemoMode) { |
| console.log('π― Skipping cleanup in demo mode'); |
| return; |
| } |
| |
| console.log('π§Ή Triggering session cleanup...'); |
| fetch('/api/cleanup', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| } |
| }).then(response => { |
| if (response.ok) { |
| console.log('β
Session cleanup completed'); |
| } else { |
| console.warn('β οΈ Session cleanup failed'); |
| } |
| }).catch(error => { |
| console.warn('β οΈ Session cleanup error:', error); |
| }); |
| } |
| |
| |
| window.addEventListener('beforeunload', function(event) { |
| |
| if (!isDemoMode && currentTaskId) { |
| triggerCleanup(); |
| } |
| }); |
| |
| |
| let cleanupScheduled = false; |
| function scheduleDelayedCleanup() { |
| if (cleanupScheduled) return; |
| cleanupScheduled = true; |
| |
| |
| setTimeout(function() { |
| if (!isDemoMode) { |
| console.log('π Scheduled cleanup after results display'); |
| triggerCleanup(); |
| } |
| cleanupScheduled = false; |
| }, 10 * 60 * 1000); |
| } |
| |
| |
| setInterval(function() { |
| |
| fetch('/api/session-info') |
| .then(response => response.json()) |
| .then(data => { |
| console.log('π Session info:', data); |
| |
| const now = Date.now() / 1000; |
| if (data.last_activity && (now - data.last_activity) > 7200) { |
| console.log('π Auto-cleanup due to long inactivity'); |
| triggerCleanup(); |
| } |
| }) |
| .catch(error => { |
| console.warn('β οΈ Failed to get session info:', error); |
| }); |
| }, 60 * 60 * 1000); |
| |
| |
| function manualCleanup() { |
| triggerCleanup(); |
| alert('π§Ή Session cleanup requested. Your uploaded files have been removed from the server.'); |
| } |
| |
| function setupLiveWaveformVisualization() { |
| console.log('π― Setting up live waveform visualization'); |
| |
| |
| const demoAudioPlayer = document.getElementById('demo-audio-player'); |
| const demoCanvas = document.getElementById('demo-waveform-canvas'); |
| |
| if (demoAudioPlayer && demoCanvas) { |
| console.log('π΅ Setting up demo audio visualization'); |
| setupAudioVisualization(demoAudioPlayer, demoCanvas, 'demo'); |
| } else { |
| console.log('β οΈ Demo audio elements not found'); |
| } |
| |
| |
| const audioElements = document.querySelectorAll('audio'); |
| const canvasElements = document.querySelectorAll('canvas[id*="waveform"]'); |
| |
| audioElements.forEach((audio, index) => { |
| if (audio.id !== 'demo-audio-player') { |
| const canvas = canvasElements[index] || document.getElementById('waveform-canvas'); |
| if (canvas) { |
| console.log('π΅ Setting up full mode audio visualization'); |
| setupAudioVisualization(audio, canvas, 'full'); |
| } |
| } |
| }); |
| } |
| |
| function setupAudioVisualization(audioElement, canvas, mode) { |
| console.log(`π§ Setting up audio visualization for ${mode} mode`); |
| |
| let animationId = null; |
| let audioContext = null; |
| let analyser = null; |
| let dataArray = null; |
| let source = null; |
| |
| |
| const existingListeners = audioElement._visualizationListeners; |
| if (existingListeners) { |
| audioElement.removeEventListener('play', existingListeners.play); |
| audioElement.removeEventListener('pause', existingListeners.pause); |
| audioElement.removeEventListener('ended', existingListeners.ended); |
| } |
| |
| |
| const playListener = async () => { |
| try { |
| console.log(`π΅ ${mode} audio started playing`); |
| |
| if (!audioContext) { |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| console.log('π― Created new AudioContext'); |
| } |
| |
| if (!source) { |
| source = audioContext.createMediaElementSource(audioElement); |
| analyser = audioContext.createAnalyser(); |
| analyser.fftSize = 256; |
| analyser.smoothingTimeConstant = 0.8; |
| |
| source.connect(analyser); |
| analyser.connect(audioContext.destination); |
| |
| const bufferLength = analyser.frequencyBinCount; |
| dataArray = new Uint8Array(bufferLength); |
| console.log('π Connected audio source to analyser'); |
| } |
| |
| if (audioContext.state === 'suspended') { |
| await audioContext.resume(); |
| console.log('βΆοΈ Resumed AudioContext'); |
| } |
| |
| startLiveVisualization(); |
| console.log(`β
Live visualization started for ${mode} mode`); |
| } catch (error) { |
| console.warn('β οΈ Web Audio API not available for live visualization:', error); |
| |
| drawStaticWaveform(); |
| } |
| }; |
| |
| const pauseListener = () => { |
| console.log(`βΈοΈ ${mode} audio paused`); |
| stopLiveVisualization(); |
| }; |
| |
| const endedListener = () => { |
| console.log(`βΉοΈ ${mode} audio ended`); |
| stopLiveVisualization(); |
| |
| |
| |
| |
| }; |
| |
| |
| audioElement.addEventListener('play', playListener); |
| audioElement.addEventListener('pause', pauseListener); |
| audioElement.addEventListener('ended', endedListener); |
| |
| |
| audioElement._visualizationListeners = { |
| play: playListener, |
| pause: pauseListener, |
| ended: endedListener |
| }; |
| |
| |
| drawStaticWaveform(); |
| |
| function drawStaticWaveform() { |
| if (!canvas) return; |
| |
| const ctx = canvas.getContext('2d'); |
| const canvasWidth = canvas.offsetWidth || 800; |
| const canvasHeight = canvas.offsetHeight || 64; |
| |
| |
| canvas.width = canvasWidth * window.devicePixelRatio; |
| canvas.height = canvasHeight * window.devicePixelRatio; |
| ctx.scale(window.devicePixelRatio, window.devicePixelRatio); |
| |
| |
| ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
| |
| |
| const barCount = 100; |
| const barWidth = canvasWidth / barCount; |
| |
| ctx.fillStyle = '#3B82F6'; |
| |
| for (let i = 0; i < barCount; i++) { |
| |
| const normalizedIndex = i / barCount; |
| const amplitude = Math.sin(normalizedIndex * Math.PI * 4) * 0.3 + |
| Math.sin(normalizedIndex * Math.PI * 8) * 0.2 + |
| Math.random() * 0.1; |
| const barHeight = Math.max(2, Math.abs(amplitude) * canvasHeight * 0.8); |
| |
| const x = i * barWidth; |
| const y = (canvasHeight - barHeight) / 2; |
| |
| ctx.fillRect(x, y, barWidth - 1, barHeight); |
| } |
| |
| console.log(`π Drew static waveform on ${mode} canvas`); |
| } |
| |
| function startLiveVisualization() { |
| if (!analyser || !dataArray) { |
| console.warn('β οΈ Analyser or dataArray not available for live visualization'); |
| return; |
| } |
| |
| const ctx = canvas.getContext('2d'); |
| const canvasWidth = canvas.offsetWidth || 800; |
| const canvasHeight = canvas.offsetHeight || 64; |
| |
| |
| canvas.width = canvasWidth * window.devicePixelRatio; |
| canvas.height = canvasHeight * window.devicePixelRatio; |
| ctx.scale(window.devicePixelRatio, window.devicePixelRatio); |
| |
| console.log(`π¬ Starting live animation for ${mode} canvas (${canvasWidth}x${canvasHeight})`); |
| |
| function animate() { |
| if (!analyser || !dataArray) return; |
| |
| analyser.getByteFrequencyData(dataArray); |
| |
| |
| ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
| |
| |
| const barCount = 100; |
| const barWidth = canvasWidth / barCount; |
| |
| ctx.fillStyle = '#10B981'; |
| |
| for (let i = 0; i < barCount; i++) { |
| const dataIndex = Math.floor((i / barCount) * dataArray.length); |
| const barHeight = Math.max(2, (dataArray[dataIndex] / 255) * canvasHeight * 0.8); |
| |
| const x = i * barWidth; |
| const y = (canvasHeight - barHeight) / 2; |
| |
| ctx.fillRect(x, y, barWidth - 1, barHeight); |
| } |
| |
| animationId = requestAnimationFrame(animate); |
| } |
| |
| animate(); |
| } |
| |
| function stopLiveVisualization() { |
| if (animationId) { |
| cancelAnimationFrame(animationId); |
| animationId = null; |
| console.log(`βΉοΈ Stopped live visualization for ${mode} mode`); |
| } |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| console.log('π DOM loaded, setting up waveform visualization'); |
| setupLiveWaveformVisualization(); |
| |
| |
| const observer = new MutationObserver((mutations) => { |
| mutations.forEach((mutation) => { |
| mutation.addedNodes.forEach((node) => { |
| if (node.nodeType === 1) { |
| const audioElements = node.querySelectorAll ? node.querySelectorAll('audio') : []; |
| const canvasElements = node.querySelectorAll ? node.querySelectorAll('canvas[id*="waveform"]') : []; |
| |
| if (node.tagName === 'AUDIO' || audioElements.length > 0 || canvasElements.length > 0) { |
| console.log('π New audio/canvas elements detected, reinitializing visualization'); |
| setTimeout(setupLiveWaveformVisualization, 500); |
| } |
| } |
| }); |
| }); |
| }); |
| |
| observer.observe(document.body, { |
| childList: true, |
| subtree: true |
| }); |
| }); |
| |
| </script> |
| </body> |
| </html> |