tts / index.html
MagicBullets's picture
Add 2 files
ff13098 verified
<!DOCTYPE html>
<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>