|
<!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/[email protected]/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-latest.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 { |
|
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; |
|
} |
|
</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">Upload Audio File</h3> |
|
|
|
<form id="upload-form" enctype="multipart/form-data"> |
|
|
|
<div id="demo-mode-section" class="mb-6 hidden"> |
|
<h4 class="text-lg font-medium text-gray-900 mb-4">Select Demo Audio File</h4> |
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> |
|
<div class="demo-file-option border-2 border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-500 transition-colors" data-demo-id="yuri_kizaki"> |
|
<div class="flex items-start"> |
|
<div class="flex-shrink-0"> |
|
<i class="fas fa-microphone text-2xl text-blue-600"></i> |
|
</div> |
|
<div class="ml-3"> |
|
<h5 class="text-sm font-medium text-gray-900">Yuri Kizaki - Japanese Audio</h5> |
|
<p class="text-sm text-gray-500 mt-1">Audio message about website communication enhancement</p> |
|
<div class="flex items-center mt-2"> |
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">Japanese</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="demo-file-option border-2 border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-500 transition-colors" data-demo-id="film_podcast"> |
|
<div class="flex items-start"> |
|
<div class="flex-shrink-0"> |
|
<i class="fas fa-podcast text-2xl text-green-600"></i> |
|
</div> |
|
<div class="ml-3"> |
|
<h5 class="text-sm font-medium text-gray-900">French Film Podcast</h5> |
|
<p class="text-sm text-gray-500 mt-1">Discussion about recent movies including Social Network</p> |
|
<div class="flex items-center mt-2"> |
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">French</span> |
|
</div> |
|
</div> |
|
</div> |
|
</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 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 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"> |
|
<i class="fas fa-spinner text-2xl text-blue-500"></i> |
|
</div> |
|
<p class="mt-2 text-gray-600">Loading system information...</p> |
|
</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'); |
|
} 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'); |
|
} |
|
} |
|
|
|
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.querySelectorAll('.demo-file-option').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; |
|
document.getElementById('selected-demo-file').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(duration) { |
|
const canvas = document.getElementById('waveform-canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
canvas.width = canvas.offsetWidth * window.devicePixelRatio; |
|
canvas.height = 80 * window.devicePixelRatio; |
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio); |
|
|
|
|
|
ctx.clearRect(0, 0, canvas.offsetWidth, 80); |
|
|
|
|
|
const samples = 200; |
|
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 * 60; |
|
const x = i * barWidth; |
|
const y = (80 - 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); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
function generateWaveformFromAudio(audioElement) { |
|
try { |
|
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
const source = audioContext.createMediaElementSource(audioElement); |
|
const analyser = audioContext.createAnalyser(); |
|
|
|
source.connect(analyser); |
|
analyser.connect(audioContext.destination); |
|
|
|
analyser.fftSize = 512; |
|
const bufferLength = analyser.frequencyBinCount; |
|
const dataArray = new Uint8Array(bufferLength); |
|
|
|
const canvas = document.getElementById('waveform-canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
function draw() { |
|
analyser.getByteFrequencyData(dataArray); |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
ctx.fillStyle = '#3B82F6'; |
|
|
|
const barWidth = canvas.offsetWidth / bufferLength; |
|
|
|
for (let i = 0; i < bufferLength; i++) { |
|
const barHeight = (dataArray[i] / 255) * 60; |
|
const x = i * barWidth; |
|
const y = (80 - barHeight) / 2; |
|
|
|
ctx.fillRect(x, y, barWidth - 1, barHeight); |
|
} |
|
|
|
if (!audioElement.paused) { |
|
requestAnimationFrame(draw); |
|
} |
|
} |
|
|
|
|
|
generateDemoWaveform(audioElement.duration || 30); |
|
|
|
|
|
audioElement.addEventListener('play', () => { |
|
if (audioContext.state === 'suspended') { |
|
audioContext.resume(); |
|
} |
|
draw(); |
|
}); |
|
|
|
} catch (error) { |
|
console.log('Web Audio API not available, showing static waveform'); |
|
generateDemoWaveform(audioElement.duration || 30); |
|
} |
|
} |
|
|
|
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('selected-demo-file').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('selected-demo-file').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 mr-2"></i>Starting...'; |
|
|
|
|
|
const endpoint = isDemoMode ? '/api/demo-process' : '/api/upload'; |
|
const response = await fetch(endpoint, { |
|
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}`); |
|
const status = await response.json(); |
|
|
|
updateProgress(status); |
|
|
|
if (status.status === 'complete') { |
|
clearInterval(progressInterval); |
|
const resultsResponse = await fetch(`/api/results/${currentTaskId}`); |
|
const results = await resultsResponse.json(); |
|
showResults(results.results); |
|
} 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'); |
|
|
|
|
|
populateTranscript(results.segments); |
|
|
|
|
|
populateVisualizations(results.segments); |
|
|
|
|
|
populateSummary(results.summary); |
|
|
|
|
|
setupDownloadButtons(); |
|
} |
|
|
|
function populateVisualizations(segments) { |
|
|
|
createLanguageChart(segments); |
|
|
|
|
|
createSpeakerTimeline(segments); |
|
|
|
} |
|
|
|
function createLanguageChart(segments) { |
|
const languages = {}; |
|
const languageDurations = {}; |
|
|
|
segments.forEach(seg => { |
|
const lang = seg.language.toUpperCase(); |
|
const duration = seg.end_time - seg.start_time; |
|
|
|
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))]; |
|
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']; |
|
|
|
const data = speakers.map((speaker, index) => { |
|
const speakerSegments = segments.filter(seg => seg.speaker === speaker); |
|
|
|
return { |
|
x: speakerSegments.map(seg => seg.start_time), |
|
y: speakerSegments.map(() => speaker), |
|
mode: 'markers', |
|
type: 'scatter', |
|
marker: { |
|
size: speakerSegments.map(seg => (seg.end_time - seg.start_time) * 5), |
|
color: colors[index % colors.length], |
|
opacity: 0.7 |
|
}, |
|
name: speaker, |
|
text: speakerSegments.map(seg => `${seg.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.toUpperCase()})</span> |
|
</div> |
|
<p class="text-gray-800 leading-relaxed">${segment.text}</p> |
|
</div> |
|
|
|
${segment.translated_text && segment.translated_text !== segment.text && segment.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)}</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}</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}</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">${summary.processing_time}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"> |
|
<i class="fas fa-spinner text-2xl text-blue-500 animate-spin"></i> |
|
<p class="mt-2 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(); |
|
</script> |
|
</body> |
|
</html> |