Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AudioScribe - AI Audio Transcription</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .dropzone { | |
| border: 2px dashed #9CA3AF; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #3B82F6; | |
| background-color: rgba(59, 130, 246, 0.05); | |
| } | |
| .waveform { | |
| background: linear-gradient(to right, #3B82F6, #8B5CF6); | |
| height: 100px; | |
| border-radius: 8px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .waveform-bar { | |
| position: absolute; | |
| bottom: 0; | |
| width: 4px; | |
| background-color: white; | |
| opacity: 0.7; | |
| border-radius: 2px; | |
| } | |
| .speaker-1 { | |
| border-left: 4px solid #3B82F6; | |
| } | |
| .speaker-2 { | |
| border-left: 4px solid #10B981; | |
| } | |
| .speaker-3 { | |
| border-left: 4px solid #F59E0B; | |
| } | |
| .tag-emphasis { | |
| font-weight: bold; | |
| color: #3B82F6; | |
| } | |
| .tag-pause { | |
| color: #6B7280; | |
| font-style: italic; | |
| } | |
| .tag-emotion { | |
| background-color: #FEE2E2; | |
| color: #B91C1C; | |
| border-radius: 4px; | |
| padding: 0 4px; | |
| } | |
| .tag-laugh { | |
| color: #10B981; | |
| } | |
| .sidebar { | |
| transition: all 0.3s ease; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| } | |
| .animate-pulse { | |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| .progress-indicator { | |
| width: 0; | |
| height: 2px; | |
| background-color: #3B82F6; | |
| transition: width 0.1s linear; | |
| } | |
| #audioPlayer { | |
| width: 100%; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 text-gray-900 font-sans"> | |
| <div class="flex h-screen overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div class="sidebar bg-white w-64 border-r border-gray-200 flex flex-col"> | |
| <div class="p-4 border-b border-gray-200"> | |
| <h1 class="text-xl font-bold text-indigo-600 flex items-center"> | |
| <i class="fas fa-microphone-alt mr-2"></i> AudioScribe | |
| </h1> | |
| </div> | |
| <div class="p-4"> | |
| <button id="newProjectBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-md flex items-center justify-center mb-4"> | |
| <i class="fas fa-plus mr-2"></i> New Project | |
| </button> | |
| <div class="mb-6"> | |
| <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-2">Projects</h3> | |
| <ul id="projectList" class="space-y-1"> | |
| <!-- Projects will be added here dynamically --> | |
| </ul> | |
| </div> | |
| <div> | |
| <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-2">Settings</h3> | |
| <ul class="space-y-1"> | |
| <li class="px-2 py-1 rounded hover:bg-gray-100 cursor-pointer flex items-center"> | |
| <i class="fas fa-cog text-gray-400 mr-2"></i> Preferences | |
| </li> | |
| <li class="px-2 py-1 rounded hover:bg-gray-100 cursor-pointer flex items-center"> | |
| <i class="fas fa-key text-gray-400 mr-2"></i> API Keys | |
| </li> | |
| <li class="px-2 py-1 rounded hover:bg-gray-100 cursor-pointer flex items-center"> | |
| <i class="fas fa-info-circle text-gray-400 mr-2"></i> About | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="mt-auto p-4 border-t border-gray-200"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600"> | |
| <i class="fas fa-user"></i> | |
| </div> | |
| <div class="ml-2"> | |
| <p class="text-sm font-medium">John Doe</p> | |
| <p class="text-xs text-gray-500">Free Plan</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <!-- Top Bar --> | |
| <div class="bg-white border-b border-gray-200 p-4 flex items-center justify-between"> | |
| <div class="flex items-center"> | |
| <button id="sidebarToggle" class="mr-4 text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <h2 id="projectTitle" class="text-lg font-semibold">Untitled Project</h2> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <button id="saveBtn" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-save mr-1"></i> Save | |
| </button> | |
| <div class="relative"> | |
| <button id="exportBtn" class="px-3 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded-md text-sm flex items-center"> | |
| <i class="fas fa-file-export mr-1"></i> Export | |
| </button> | |
| <div id="exportDropdown" class="hidden absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg z-10"> | |
| <div class="py-1"> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" id="exportTxt">Text (.txt)</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" id="exportDocx">Word (.docx)</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" id="exportSrt">Subtitles (.srt)</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" id="exportJson">JSON (.json)</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" id="exportAudio">Audio (.wav)</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content Area --> | |
| <div class="flex-1 overflow-auto p-6"> | |
| <div id="dropzone" class="dropzone rounded-lg p-12 text-center mb-8"> | |
| <div class="mx-auto w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-600 mb-4"> | |
| <i class="fas fa-microphone-alt text-2xl"></i> | |
| </div> | |
| <h3 class="text-lg font-medium text-gray-900 mb-2">Drag & drop your audio files here</h3> | |
| <p class="text-gray-500 mb-4">or</p> | |
| <label for="fileInput" class="cursor-pointer inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none"> | |
| <i class="fas fa-folder-open mr-2"></i> Browse Files | |
| </label> | |
| <input id="fileInput" type="file" accept="audio/*" class="hidden"> | |
| <p class="text-sm text-gray-500 mt-4">Supports WAV, MP3, and other common audio formats</p> | |
| </div> | |
| <!-- Processing Section (hidden by default) --> | |
| <div id="processingSection" class="hidden"> | |
| <div class="bg-white rounded-lg shadow-sm p-6 mb-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-medium">Processing Files</h3> | |
| <span id="progressStatus" class="text-sm text-gray-500">0% completed</span> | |
| </div> | |
| <div class="progress-indicator mb-2"></div> | |
| <div class="space-y-4"> | |
| <!-- Current File Progress --> | |
| <div> | |
| <div class="flex items-center justify-between mb-1"> | |
| <span id="currentFileName" class="text-sm font-medium">No file selected</span> | |
| <span id="fileProgress" class="text-xs text-gray-500">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2"> | |
| <div id="fileProgressBar" class="bg-indigo-600 h-2 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="mt-2 text-xs text-gray-500 flex justify-between"> | |
| <span>Audio analysis</span> | |
| <span>Noise reduction</span> | |
| <span>Transcription</span> | |
| <span>Tagging</span> | |
| </div> | |
| </div> | |
| <!-- Queued Files --> | |
| <div id="queuedFiles" class="border-t border-gray-200 pt-4"> | |
| <!-- Files will be added here dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Transcription Preview --> | |
| <div class="bg-white rounded-lg shadow-sm p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-medium">Transcription Preview</h3> | |
| <div class="flex space-x-2"> | |
| <button id="playBtn" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-headphones mr-1"></i> Play | |
| </button> | |
| <button id="editBtn" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-md text-sm flex items-center"> | |
| <i class="fas fa-edit mr-1"></i> Edit | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <!-- Waveform Visualization --> | |
| <div> | |
| <div class="waveform mb-4" id="waveform"> | |
| <!-- Waveform bars will be added here dynamically --> | |
| </div> | |
| <audio id="audioPlayer" controls></audio> | |
| <div class="flex items-center justify-between mb-2 mt-2"> | |
| <div class="flex items-center space-x-2"> | |
| <button id="playPauseBtn" class="p-2 rounded-full bg-gray-100 hover:bg-gray-200"> | |
| <i class="fas fa-play text-gray-700"></i> | |
| </button> | |
| <span id="timeDisplay" class="text-sm text-gray-500">00:00 / 00:00</span> | |
| </div> | |
| <div class="flex items-center space-x-1"> | |
| <button id="noiseReductionBtn" class="p-2 rounded-full bg-gray-100 hover:bg-gray-200" title="Noise Reduction"> | |
| <i class="fas fa-volume-mute text-gray-700"></i> | |
| </button> | |
| <button id="diarizationBtn" class="p-2 rounded-full bg-gray-100 hover:bg-gray-200" title="Diarization Settings"> | |
| <i class="fas fa-users text-gray-700"></i> | |
| </button> | |
| <button id="emotionBtn" class="p-2 rounded-full bg-gray-100 hover:bg-gray-200" title="Emotion Detection"> | |
| <i class="fas fa-smile text-gray-700"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Transcription Text --> | |
| <div id="transcriptionText" class="bg-gray-50 p-4 rounded-md max-h-96 overflow-y-auto"> | |
| <div class="space-y-4"> | |
| <div class="animate-pulse flex items-center text-gray-500"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> | |
| <span>Waiting for audio to process...</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Processing Settings --> | |
| <div class="bg-white rounded-lg shadow-sm p-6 mt-6"> | |
| <h3 class="text-lg font-medium mb-4">Processing Settings</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <div class="border border-gray-200 rounded-md p-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="font-medium">Noise Reduction</h4> | |
| <label class="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" id="noiseReductionToggle" class="sr-only peer" checked> | |
| <div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div> | |
| </label> | |
| </div> | |
| <p class="text-sm text-gray-500">Clean up background noise and enhance voice clarity</p> | |
| </div> | |
| <div class="border border-gray-200 rounded-md p-4"> | |
| <div class="mb-2"> | |
| <h4 class="font-medium">Transcription Model</h4> | |
| </div> | |
| <select id="modelSelect" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"> | |
| <option value="fast">Whisper Small (Fastest)</option> | |
| <option value="balanced" selected>Whisper Medium (Balanced)</option> | |
| <option value="accurate">Whisper Large (Most Accurate)</option> | |
| </select> | |
| </div> | |
| <div class="border border-gray-200 rounded-md p-4"> | |
| <div class="mb-2"> | |
| <h4 class="font-medium">Speaker Diarization</h4> | |
| </div> | |
| <select id="diarizationSelect" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"> | |
| <option value="basic">Basic (2-3 speakers)</option> | |
| <option value="advanced" selected>Advanced (up to 5 speakers)</option> | |
| <option value="precision">High Precision (1-2 speakers)</option> | |
| </select> | |
| </div> | |
| <div class="border border-gray-200 rounded-md p-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="font-medium">Emotion Detection</h4> | |
| <label class="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" id="emotionToggle" class="sr-only peer" checked> | |
| <div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div> | |
| </label> | |
| </div> | |
| <p class="text-sm text-gray-500">Detect emotional tone and vocal inflections</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // App state | |
| const state = { | |
| currentProject: { | |
| id: Date.now(), | |
| name: 'Untitled Project', | |
| audioFile: null, | |
| transcription: [], | |
| settings: { | |
| noiseReduction: true, | |
| model: 'balanced', | |
| diarization: 'advanced', | |
| emotionDetection: true | |
| }, | |
| processed: false | |
| }, | |
| projects: [], | |
| isProcessing: false, | |
| currentAudioTime: 0, | |
| isPlaying: false, | |
| recognition: null | |
| }; | |
| // DOM elements | |
| const elements = { | |
| dropzone: document.getElementById('dropzone'), | |
| fileInput: document.getElementById('fileInput'), | |
| processingSection: document.getElementById('processingSection'), | |
| sidebarToggle: document.getElementById('sidebarToggle'), | |
| exportBtn: document.getElementById('exportBtn'), | |
| exportDropdown: document.getElementById('exportDropdown'), | |
| newProjectBtn: document.getElementById('newProjectBtn'), | |
| projectTitle: document.getElementById('projectTitle'), | |
| projectList: document.getElementById('projectList'), | |
| progressStatus: document.getElementById('progressStatus'), | |
| currentFileName: document.getElementById('currentFileName'), | |
| fileProgress: document.getElementById('fileProgress'), | |
| fileProgressBar: document.getElementById('fileProgressBar'), | |
| queuedFiles: document.getElementById('queuedFiles'), | |
| playBtn: document.getElementById('playBtn'), | |
| editBtn: document.getElementById('editBtn'), | |
| waveform: document.getElementById('waveform'), | |
| audioPlayer: document.getElementById('audioPlayer'), | |
| playPauseBtn: document.getElementById('playPauseBtn'), | |
| timeDisplay: document.getElementById('timeDisplay'), | |
| noiseReductionBtn: document.getElementById('noiseReductionBtn'), | |
| diarizationBtn: document.getElementById('diarizationBtn'), | |
| emotionBtn: document.getElementById('emotionBtn'), | |
| transcriptionText: document.getElementById('transcriptionText'), | |
| noiseReductionToggle: document.getElementById('noiseReductionToggle'), | |
| modelSelect: document.getElementById('modelSelect'), | |
| diarizationSelect: document.getElementById('diarizationSelect'), | |
| emotionToggle: document.getElementById('emotionToggle'), | |
| saveBtn: document.getElementById('saveBtn'), | |
| exportTxt: document.getElementById('exportTxt'), | |
| exportDocx: document.getElementById('exportDocx'), | |
| exportSrt: document.getElementById('exportSrt'), | |
| exportJson: document.getElementById('exportJson'), | |
| exportAudio: document.getElementById('exportAudio') | |
| }; | |
| // Initialize the app | |
| function init() { | |
| setupEventListeners(); | |
| updateUI(); | |
| checkSpeechRecognitionSupport(); | |
| loadProjects(); | |
| } | |
| // Check if speech recognition is supported | |
| function checkSpeechRecognitionSupport() { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SpeechRecognition) { | |
| alert('Speech recognition is not supported in your browser. This app will not be able to transcribe audio.'); | |
| return false; | |
| } | |
| state.recognition = new SpeechRecognition(); | |
| state.recognition.continuous = true; | |
| state.recognition.interimResults = true; | |
| state.recognition.onresult = handleRecognitionResult; | |
| state.recognition.onerror = handleRecognitionError; | |
| state.recognition.onend = handleRecognitionEnd; | |
| return true; | |
| } | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Sidebar toggle | |
| elements.sidebarToggle.addEventListener('click', toggleSidebar); | |
| // Export dropdown | |
| elements.exportBtn.addEventListener('click', toggleExportDropdown); | |
| document.addEventListener('click', closeExportDropdown); | |
| // Drag and drop | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| elements.dropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| elements.dropzone.addEventListener(eventName, highlightDropzone, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| elements.dropzone.addEventListener(eventName, unhighlightDropzone, false); | |
| }); | |
| elements.dropzone.addEventListener('drop', handleDrop, false); | |
| elements.fileInput.addEventListener('change', handleFileSelect); | |
| // Click on dropzone triggers file input | |
| elements.dropzone.addEventListener('click', () => { | |
| elements.fileInput.click(); | |
| }); | |
| // New project button | |
| elements.newProjectBtn.addEventListener('click', createNewProject); | |
| // Audio player controls | |
| elements.playPauseBtn.addEventListener('click', togglePlayPause); | |
| elements.audioPlayer.addEventListener('timeupdate', updateTimeDisplay); | |
| elements.audioPlayer.addEventListener('play', () => { | |
| state.isPlaying = true; | |
| updatePlayPauseButton(); | |
| }); | |
| elements.audioPlayer.addEventListener('pause', () => { | |
| state.isPlaying = false; | |
| updatePlayPauseButton(); | |
| }); | |
| elements.audioPlayer.addEventListener('ended', () => { | |
| state.isPlaying = false; | |
| updatePlayPauseButton(); | |
| }); | |
| // Processing settings | |
| elements.noiseReductionToggle.addEventListener('change', updateSettings); | |
| elements.modelSelect.addEventListener('change', updateSettings); | |
| elements.diarizationSelect.addEventListener('change', updateSettings); | |
| elements.emotionToggle.addEventListener('change', updateSettings); | |
| // Save button | |
| elements.saveBtn.addEventListener('click', saveProject); | |
| // Export buttons | |
| elements.exportTxt.addEventListener('click', exportAsTxt); | |
| elements.exportDocx.addEventListener('click', exportAsDocx); | |
| elements.exportSrt.addEventListener('click', exportAsSrt); | |
| elements.exportJson.addEventListener('click', exportAsJson); | |
| elements.exportAudio.addEventListener('click', exportAudio); | |
| } | |
| // Toggle sidebar | |
| function toggleSidebar() { | |
| document.querySelector('.sidebar').classList.toggle('hidden'); | |
| } | |
| // Toggle export dropdown | |
| function toggleExportDropdown(e) { | |
| e.stopPropagation(); | |
| elements.exportDropdown.classList.toggle('hidden'); | |
| } | |
| // Close export dropdown | |
| function closeExportDropdown() { | |
| elements.exportDropdown.classList.add('hidden'); | |
| } | |
| // Prevent default behavior for drag and drop | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| // Highlight dropzone | |
| function highlightDropzone() { | |
| elements.dropzone.classList.add('active'); | |
| } | |
| // Unhighlight dropzone | |
| function unhighlightDropzone() { | |
| elements.dropzone.classList.remove('active'); | |
| } | |
| // Handle dropped files | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| // Handle selected files | |
| function handleFileSelect(e) { | |
| const files = e.target.files; | |
| handleFiles(files); | |
| } | |
| // Process files | |
| function handleFiles(files) { | |
| if (files.length === 0) return; | |
| const audioFile = files[0]; | |
| state.currentProject.audioFile = audioFile; | |
| state.currentProject.name = audioFile.name.replace(/\.[^/.]+$/, ""); // Remove extension | |
| updateUI(); | |
| // Show processing section | |
| elements.dropzone.classList.add('hidden'); | |
| elements.processingSection.classList.remove('hidden'); | |
| // Start processing | |
| processAudioFile(audioFile); | |
| } | |
| // Process audio file | |
| function processAudioFile(file) { | |
| state.isProcessing = true; | |
| elements.currentFileName.textContent = file.name; | |
| elements.fileProgress.textContent = '0%'; | |
| elements.fileProgressBar.style.width = '0%'; | |
| elements.progressStatus.textContent = 'Starting processing...'; | |
| // Create audio player source | |
| const audioURL = URL.createObjectURL(file); | |
| elements.audioPlayer.src = audioURL; | |
| // Generate waveform visualization | |
| generateWaveform(); | |
| // Simulate processing (in a real app, this would be actual audio processing) | |
| simulateProcessing(); | |
| // Start speech recognition | |
| startSpeechRecognition(); | |
| } | |
| // Simulate processing with progress updates | |
| function simulateProcessing() { | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 5; | |
| if (progress >= 100) { | |
| progress = 100; | |
| clearInterval(interval); | |
| state.isProcessing = false; | |
| state.currentProject.processed = true; | |
| elements.progressStatus.textContent = 'Processing complete!'; | |
| updateUI(); | |
| } | |
| updateProgress(progress); | |
| }, 300); | |
| } | |
| // Update progress indicators | |
| function updateProgress(progress) { | |
| elements.fileProgress.textContent = `${Math.round(progress)}%`; | |
| elements.fileProgressBar.style.width = `${progress}%`; | |
| elements.progressStatus.textContent = `${Math.round(progress)}% completed`; | |
| } | |
| // Generate waveform visualization | |
| function generateWaveform() { | |
| elements.waveform.innerHTML = ''; | |
| const barCount = 100; | |
| for (let i = 0; i < barCount; i++) { | |
| const bar = document.createElement('div'); | |
| bar.className = 'waveform-bar'; | |
| bar.style.left = `${(i / barCount) * 100}%`; | |
| bar.style.height = `${Math.random() * 80 + 20}%`; | |
| elements.waveform.appendChild(bar); | |
| } | |
| } | |
| // Start speech recognition | |
| function startSpeechRecognition() { | |
| if (!state.recognition) return; | |
| // In a real app, we would process the audio file with the Web Speech API | |
| // For this demo, we'll simulate transcription with sample data | |
| setTimeout(() => { | |
| const sampleTranscription = [ | |
| { | |
| speaker: 1, | |
| text: "Hello everyone, welcome to today's meeting. Really glad you could all make it. [pause] We have a lot to cover today.", | |
| time: 0, | |
| tags: [ | |
| { type: 'emphasis', text: 'Really', start: 28, end: 34 }, | |
| { type: 'pause', text: '[pause]', start: 52, end: 59 } | |
| ] | |
| }, | |
| { | |
| speaker: 2, | |
| text: "Thanks for having us! [laughs] I'm excited to discuss the new project updates. [emotional tone: happy]", | |
| time: 6, | |
| tags: [ | |
| { type: 'laugh', text: '[laughs]', start: 19, end: 27 }, | |
| { type: 'emotion', text: '[emotional tone: happy]', start: 64, end: 86 } | |
| ] | |
| }, | |
| { | |
| speaker: 1, | |
| text: "Let's start with the quarterly results. Revenue is up by 15% compared to last quarter, which is [emotional tone: excited] fantastic news!", | |
| time: 12, | |
| tags: [ | |
| { type: 'emphasis', text: 'Revenue', start: 32, end: 39 }, | |
| { type: 'emotion', text: '[emotional tone: excited]', start: 84, end: 107 } | |
| ] | |
| } | |
| ]; | |
| state.currentProject.transcription = sampleTranscription; | |
| renderTranscription(); | |
| }, 2000); | |
| } | |
| // Handle recognition result (not fully implemented in this demo) | |
| function handleRecognitionResult(event) { | |
| // In a real app, this would process the recognition results | |
| } | |
| // Handle recognition error | |
| function handleRecognitionError(event) { | |
| console.error('Speech recognition error', event.error); | |
| } | |
| // Handle recognition end | |
| function handleRecognitionEnd() { | |
| if (state.isProcessing) { | |
| state.recognition.start(); // Restart if still processing | |
| } | |
| } | |
| // Render transcription | |
| function renderTranscription() { | |
| elements.transcriptionText.innerHTML = ''; | |
| if (state.currentProject.transcription.length === 0) { | |
| elements.transcriptionText.innerHTML = ` | |
| <div class="text-gray-500 text-center py-4"> | |
| No transcription available yet. | |
| </div> | |
| `; | |
| return; | |
| } | |
| const container = document.createElement('div'); | |
| container.className = 'space-y-4'; | |
| state.currentProject.transcription.forEach((segment, index) => { | |
| const segmentDiv = document.createElement('div'); | |
| segmentDiv.className = `speaker-${segment.speaker} pl-3`; | |
| const speakerDiv = document.createElement('div'); | |
| speakerDiv.className = 'flex items-center mb-1'; | |
| const speakerColor = segment.speaker === 1 ? 'indigo' : segment.speaker === 2 ? 'green' : 'yellow'; | |
| speakerDiv.innerHTML = ` | |
| <span class="font-medium text-${speakerColor}-600">Speaker ${segment.speaker}</span> | |
| <button class="ml-2 text-gray-400 hover:text-gray-600"> | |
| <i class="fas fa-pencil-alt text-xs"></i> | |
| </button> | |
| `; | |
| const textDiv = document.createElement('p'); | |
| textDiv.className = 'text-gray-800'; | |
| // Process text with tags | |
| let processedText = segment.text; | |
| if (segment.tags && segment.tags.length > 0) { | |
| // Sort tags by start position in reverse order to avoid offset issues when inserting HTML | |
| const sortedTags = [...segment.tags].sort((a, b) => b.start - a.start); | |
| sortedTags.forEach(tag => { | |
| const before = processedText.substring(0, tag.start); | |
| const after = processedText.substring(tag.end); | |
| const tagClass = | |
| tag.type === 'emphasis' ? 'tag-emphasis' : | |
| tag.type === 'pause' ? 'tag-pause' : | |
| tag.type === 'emotion' ? 'tag-emotion' : | |
| tag.type === 'laugh' ? 'tag-laugh' : ''; | |
| processedText = `${before}<span class="${tagClass}">${tag.text}</span>${after}`; | |
| }); | |
| } | |
| textDiv.innerHTML = processedText; | |
| segmentDiv.appendChild(speakerDiv); | |
| segmentDiv.appendChild(textDiv); | |
| container.appendChild(segmentDiv); | |
| }); | |
| elements.transcriptionText.appendChild(container); | |
| } | |
| // Toggle play/pause | |
| function togglePlayPause() { | |
| if (state.isPlaying) { | |
| elements.audioPlayer.pause(); | |
| } else { | |
| elements.audioPlayer.play(); | |
| } | |
| state.isPlaying = !state.isPlaying; | |
| updatePlayPauseButton(); | |
| } | |
| // Update play/pause button | |
| function updatePlayPauseButton() { | |
| const icon = elements.playPauseBtn.querySelector('i'); | |
| if (state.isPlaying) { | |
| icon.classList.remove('fa-play'); | |
| icon.classList.add('fa-pause'); | |
| } else { | |
| icon.classList.remove('fa-pause'); | |
| icon.classList.add('fa-play'); | |
| } | |
| } | |
| // Update time display | |
| function updateTimeDisplay() { | |
| const currentTime = elements.audioPlayer.currentTime; | |
| const duration = elements.audioPlayer.duration || 1; | |
| state.currentAudioTime = currentTime; | |
| const currentMinutes = Math.floor(currentTime / 60); | |
| const currentSeconds = Math.floor(currentTime % 60); | |
| const durationMinutes = Math.floor(duration / 60); | |
| const durationSeconds = Math.floor(duration % 60); | |
| elements.timeDisplay.textContent = | |
| `${currentMinutes.toString().padStart(2, '0')}:${currentSeconds.toString().padStart(2, '0')} / ` + | |
| `${durationMinutes.toString().padStart(2, '0')}:${durationSeconds.toString().padStart(2, '0')}`; | |
| // Update waveform visualization (simplified) | |
| const progressPercent = (currentTime / duration) * 100; | |
| document.querySelector('.progress-indicator').style.width = `${progressPercent}%`; | |
| } | |
| // Create new project | |
| function createNewProject() { | |
| // Save current project if it has content | |
| if (state.currentProject.audioFile || state.currentProject.transcription.length > 0) { | |
| saveProject(); | |
| } | |
| // Reset state for new project | |
| state.currentProject = { | |
| id: Date.now(), | |
| name: 'Untitled Project', | |
| audioFile: null, | |
| transcription: [], | |
| settings: { | |
| noiseReduction: true, | |
| model: 'balanced', | |
| diarization: 'advanced', | |
| emotionDetection: true | |
| }, | |
| processed: false | |
| }; | |
| // Reset UI | |
| elements.dropzone.classList.remove('hidden'); | |
| elements.processingSection.classList.add('hidden'); | |
| elements.fileInput.value = ''; | |
| elements.projectTitle.textContent = 'Untitled Project'; | |
| // Stop any ongoing processing | |
| state.isProcessing = false; | |
| if (state.recognition) { | |
| state.recognition.stop(); | |
| } | |
| } | |
| // Update settings from UI | |
| function updateSettings() { | |
| state.currentProject.settings = { | |
| noiseReduction: elements.noiseReductionToggle.checked, | |
| model: elements.modelSelect.value, | |
| diarization: elements.diarizationSelect.value, | |
| emotionDetection: elements.emotionToggle.checked | |
| }; | |
| } | |
| // Save project | |
| function saveProject() { | |
| if (!state.currentProject.audioFile && state.currentProject.transcription.length === 0) { | |
| alert('Nothing to save! Please upload an audio file first.'); | |
| return; | |
| } | |
| // Check if this project already exists in the projects array | |
| const existingIndex = state.projects.findIndex(p => p.id === state.currentProject.id); | |
| if (existingIndex >= 0) { | |
| // Update existing project | |
| state.projects[existingIndex] = {...state.currentProject}; | |
| } else { | |
| // Add new project | |
| state.projects.push({...state.currentProject}); | |
| } | |
| // Update UI | |
| updateProjectsList(); | |
| alert('Project saved successfully!'); | |
| } | |
| // Load projects (simulated - in a real app this would be from storage) | |
| function loadProjects() { | |
| // Sample projects for demo | |
| state.projects = [ | |
| { | |
| id: 1, | |
| name: 'Interview_001', | |
| audioFile: { name: 'interview_001.wav' }, | |
| transcription: [], | |
| settings: { | |
| noiseReduction: true, | |
| model: 'balanced', | |
| diarization: 'advanced', | |
| emotionDetection: true | |
| }, | |
| processed: true | |
| }, | |
| { | |
| id: 2, | |
| name: 'Meeting_2023', | |
| audioFile: { name: 'meeting_2023.wav' }, | |
| transcription: [], | |
| settings: { | |
| noiseReduction: false, | |
| model: 'fast', | |
| diarization: 'basic', | |
| emotionDetection: false | |
| }, | |
| processed: true | |
| }, | |
| { | |
| id: 3, | |
| name: 'Podcast_Episode', | |
| audioFile: { name: 'podcast_episode.wav' }, | |
| transcription: [], | |
| settings: { | |
| noiseReduction: true, | |
| model: 'accurate', | |
| diarization: 'precision', | |
| emotionDetection: true | |
| }, | |
| processed: false | |
| } | |
| ]; | |
| updateProjectsList(); | |
| } | |
| // Update projects list in sidebar | |
| function updateProjectsList() { | |
| elements.projectList.innerHTML = ''; | |
| state.projects.forEach(project => { | |
| const li = document.createElement('li'); | |
| li.className = 'px-2 py-1 rounded hover:bg-gray-100 cursor-pointer flex items-center'; | |
| li.innerHTML = ` | |
| <i class="fas fa-file-audio text-gray-400 mr-2"></i> ${project.name} | |
| `; | |
| li.addEventListener('click', () => loadProject(project.id)); | |
| elements.projectList.appendChild(li); | |
| }); | |
| } | |
| // Load project | |
| function loadProject(id) { | |
| const project = state.projects.find(p => p.id === id); | |
| if (!project) return; | |
| state.currentProject = {...project}; | |
| updateUI(); | |
| if (project.audioFile) { | |
| elements.dropzone.classList.add('hidden'); | |
| elements.processingSection.classList.remove('hidden'); | |
| elements.currentFileName.textContent = project.audioFile.name; | |
| // In a real app, we would load the actual audio file and transcription | |
| if (project.processed) { | |
| elements.fileProgress.textContent = '100%'; | |
| elements.fileProgressBar.style.width = '100%'; | |
| elements.progressStatus.textContent = 'Processing complete!'; | |
| // Simulate loading the audio and transcription | |
| setTimeout(() => { | |
| renderTranscription(); | |
| generateWaveform(); | |
| }, 500); | |
| } else { | |
| // Simulate processing if not already processed | |
| simulateProcessing(); | |
| } | |
| } | |
| } | |
| // Export as text | |
| function exportAsTxt(e) { | |
| e.preventDefault(); | |
| if (!state.currentProject.processed) { | |
| alert('Please process the audio first!'); | |
| return; | |
| } | |
| let textContent = `Transcription for ${state.currentProject.name}\n\n`; | |
| state.currentProject.transcription.forEach(segment => { | |
| textContent += `Speaker ${segment.speaker}: ${segment.text}\n\n`; | |
| }); | |
| downloadFile(textContent, `${state.currentProject.name}.txt`, 'text/plain'); | |
| } | |
| // Export as Word (simulated) | |
| function exportAsDocx(e) { | |
| e.preventDefault(); | |
| alert('In a real application, this would export as a Word document.'); | |
| } | |
| // Export as subtitles | |
| function exportAsSrt(e) { | |
| e.preventDefault(); | |
| if (!state.currentProject.processed) { | |
| alert('Please process the audio first!'); | |
| return; | |
| } | |
| let srtContent = ''; | |
| let counter = 1; | |
| state.currentProject.transcription.forEach(segment => { | |
| const startTime = formatTimeForSrt(segment.time); | |
| const endTime = formatTimeForSrt(segment.time + 5); // Assuming 5 seconds per segment for demo | |
| srtContent += `${counter++}\n`; | |
| srtContent += `${startTime} --> ${endTime}\n`; | |
| srtContent += `${segment.text}\n\n`; | |
| }); | |
| downloadFile(srtContent, `${state.currentProject.name}.srt`, 'text/plain'); | |
| } | |
| // Format time for SRT | |
| function formatTimeForSrt(seconds) { | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = Math.floor(seconds % 60); | |
| const millis = Math.floor((seconds % 1) * 1000); | |
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${millis.toString().padStart(3, '0')}`; | |
| } | |
| // Export as JSON | |
| function exportAsJson(e) { | |
| e.preventDefault(); | |
| const jsonContent = JSON.stringify(state.currentProject, null, 2); | |
| downloadFile(jsonContent, `${state.currentProject.name}.json`, 'application/json'); | |
| } | |
| // Export audio | |
| function exportAudio(e) { | |
| e.preventDefault(); | |
| if (!state.currentProject.audioFile) { | |
| alert('No audio file to export!'); | |
| return; | |
| } | |
| // In a real app, we would use the actual audio file | |
| alert('In a real application, this would export the audio file.'); | |
| } | |
| // Download helper function | |
| function downloadFile(content, filename, mimeType) { | |
| const blob = new Blob([content], { type: mimeType }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| } | |
| // Update UI based on state | |
| function updateUI() { | |
| elements.projectTitle.textContent = state.currentProject.name; | |
| // Update settings toggles | |
| elements.noiseReductionToggle.checked = state.currentProject.settings.noiseReduction; | |
| elements.modelSelect.value = state.currentProject.settings.model; | |
| elements.diarizationSelect.value = state.currentProject.settings.diarization; | |
| elements.emotionToggle.checked = state.currentProject.settings.emotionDetection; | |
| } | |
| // Initialize the app | |
| init(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MagicBullets/tts" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |