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> |