Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Audio Similarity Matcher</title> | |
| <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #4a6bff; | |
| --secondary-color: #f5f7ff; | |
| --accent-color: #ff6b6b; | |
| --text-color: #2c3e50; | |
| --light-text: #7f8c8d; | |
| --success-color: #2ecc71; | |
| --warning-color: #f39c12; | |
| --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| --border-radius: 12px; | |
| --transition: all 0.3s ease; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| color: var(--text-color); | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .logo i { | |
| font-size: 2rem; | |
| color: var(--primary-color); | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .subtitle { | |
| color: var(--light-text); | |
| font-size: 1.1rem; | |
| } | |
| .built-with { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| font-size: 0.9rem; | |
| color: var(--light-text); | |
| } | |
| .built-with a { | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| font-weight: 600; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--shadow); | |
| padding: 30px; | |
| margin-bottom: 20px; | |
| transition: var(--transition); | |
| } | |
| .card:hover { | |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); | |
| } | |
| .input-section { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| @media (max-width: 768px) { | |
| .input-section { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| label { | |
| font-weight: 600; | |
| color: var(--text-color); | |
| font-size: 0.95rem; | |
| } | |
| .file-upload { | |
| border: 2px dashed var(--primary-color); | |
| border-radius: var(--border-radius); | |
| padding: 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| background-color: var(--secondary-color); | |
| } | |
| .file-upload:hover { | |
| background-color: rgba(74, 107, 255, 0.1); | |
| border-color: var(--accent-color); | |
| } | |
| .file-upload i { | |
| font-size: 2rem; | |
| color: var(--primary-color); | |
| margin-bottom: 10px; | |
| } | |
| .file-upload p { | |
| color: var(--light-text); | |
| font-size: 0.9rem; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .url-input { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| padding: 12px 15px; | |
| border: 1px solid #ddd; | |
| border-radius: var(--border-radius); | |
| font-size: 1rem; | |
| transition: var(--transition); | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 3px rgba(74, 107, 255, 0.1); | |
| } | |
| .btn { | |
| padding: 12px 25px; | |
| border: none; | |
| border-radius: var(--border-radius); | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: #3a5bef; | |
| transform: translateY(-2px); | |
| } | |
| .btn-secondary { | |
| background-color: var(--secondary-color); | |
| color: var(--primary-color); | |
| border: 1px solid var(--primary-color); | |
| } | |
| .btn-secondary:hover { | |
| background-color: rgba(74, 107, 255, 0.1); | |
| } | |
| .btn-add { | |
| background-color: var(--success-color); | |
| color: white; | |
| } | |
| .btn-add:hover { | |
| background-color: #27ae60; | |
| } | |
| .btn-remove { | |
| background-color: var(--accent-color); | |
| color: white; | |
| } | |
| .btn-remove:hover { | |
| background-color: #e74c3c; | |
| } | |
| .audio-preview { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| margin-top: 15px; | |
| padding: 10px; | |
| background-color: var(--secondary-color); | |
| border-radius: var(--border-radius); | |
| } | |
| .audio-info { | |
| flex: 1; | |
| } | |
| .audio-name { | |
| font-weight: 600; | |
| margin-bottom: 5px; | |
| color: var(--text-color); | |
| } | |
| .audio-duration { | |
| font-size: 0.85rem; | |
| color: var(--light-text); | |
| } | |
| audio { | |
| width: 100%; | |
| margin-top: 10px; | |
| } | |
| .progress-container { | |
| margin: 20px 0; | |
| } | |
| .progress-bar { | |
| height: 6px; | |
| background-color: #ecf0f1; | |
| border-radius: 3px; | |
| overflow: hidden; | |
| } | |
| .progress { | |
| height: 100%; | |
| background-color: var(--primary-color); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .status { | |
| text-align: center; | |
| margin-top: 10px; | |
| font-size: 0.9rem; | |
| color: var(--light-text); | |
| } | |
| .results { | |
| display: none; | |
| margin-top: 30px; | |
| } | |
| .results-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .results-title { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--text-color); | |
| } | |
| .match-percentage { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| color: var(--success-color); | |
| } | |
| .timestamps { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| gap: 15px; | |
| } | |
| .timestamp-card { | |
| background-color: var(--secondary-color); | |
| border-radius: var(--border-radius); | |
| padding: 15px; | |
| transition: var(--transition); | |
| } | |
| .timestamp-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: var(--shadow); | |
| } | |
| .timestamp-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .timestamp-time { | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| font-size: 1.1rem; | |
| } | |
| .similarity-score { | |
| background-color: var(--success-color); | |
| color: white; | |
| padding: 3px 8px; | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| } | |
| .timestamp-details { | |
| font-size: 0.9rem; | |
| color: var(--light-text); | |
| margin-bottom: 10px; | |
| } | |
| .waveform-container { | |
| height: 80px; | |
| background-color: white; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .waveform { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .no-results { | |
| text-align: center; | |
| padding: 40px; | |
| color: var(--light-text); | |
| } | |
| .no-results i { | |
| font-size: 3rem; | |
| margin-bottom: 15px; | |
| color: var(--primary-color); | |
| opacity: 0.5; | |
| } | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(255, 255, 255, 0.9); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .loading-content { | |
| text-align: center; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 4px solid rgba(74, 107, 255, 0.2); | |
| border-radius: 50%; | |
| border-top-color: var(--primary-color); | |
| animation: spin 1s ease-in-out infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| color: var(--text-color); | |
| } | |
| .error-message { | |
| background-color: #ffebee; | |
| color: #c62828; | |
| padding: 15px; | |
| border-radius: var(--border-radius); | |
| margin: 15px 0; | |
| display: none; | |
| } | |
| .error-message.show { | |
| display: block; | |
| } | |
| .tooltip { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .tooltip .tooltiptext { | |
| visibility: hidden; | |
| width: 200px; | |
| background-color: #555; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 8px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| margin-left: -100px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| font-size: 0.8rem; | |
| } | |
| .tooltip:hover .tooltiptext { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="built-with"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| <div class="container"> | |
| <header> | |
| <div class="logo"> | |
| <i class="fas fa-wave-square"></i> | |
| <h1>Audio Similarity Matcher</h1> | |
| </div> | |
| <p class="subtitle">Find exact timestamps where audio files match</p> | |
| </header> | |
| <div class="card"> | |
| <div class="input-section"> | |
| <div class="input-group"> | |
| <label for="audioUpload">Upload Audio File</label> | |
| <div class="file-upload" id="fileUploadArea"> | |
| <i class="fas fa-cloud-upload-alt"></i> | |
| <p>Click to upload or drag and drop</p> | |
| <p class="file-name" id="fileName">No file selected</p> | |
| </div> | |
| <input type="file" id="audioUpload" accept="audio/*"> | |
| </div> | |
| <div class="input-group"> | |
| <label>Or Enter Audio URLs</label> | |
| <div class="url-input"> | |
| <input type="text" id="audioUrl" placeholder="https://example.com/audio.mp3"> | |
| <button class="btn btn-add" id="addUrlBtn"> | |
| <i class="fas fa-plus"></i> Add | |
| </button> | |
| </div> | |
| <div id="urlList" class="url-list"></div> | |
| </div> | |
| </div> | |
| <div class="progress-container"> | |
| <div class="progress-bar"> | |
| <div class="progress" id="progressBar"></div> | |
| </div> | |
| <div class="status" id="statusText">Ready to analyze</div> | |
| </div> | |
| <div class="error-message" id="errorMessage"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| <span id="errorText"></span> | |
| </div> | |
| <button class="btn btn-primary" id="analyzeBtn"> | |
| <i class="fas fa-search"></i> Analyze Similarity | |
| </button> | |
| </div> | |
| <div class="card results" id="resultsSection"> | |
| <div class="results-header"> | |
| <h2 class="results-title">Similarity Results</h2> | |
| <div class="match-percentage" id="matchPercentage">0%</div> | |
| </div> | |
| <div class="timestamps" id="timestampsContainer"> | |
| <div class="no-results"> | |
| <i class="fas fa-music"></i> | |
| <p>No similarity results yet. Upload audio files and click analyze to find matches.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-content"> | |
| <div class="spinner"></div> | |
| <div class="loading-text">Analyzing audio similarity...</div> | |
| <div class="loading-subtext" id="loadingSubtext">Processing audio files</div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const audioUpload = document.getElementById('audioUpload'); | |
| const fileUploadArea = document.getElementById('fileUploadArea'); | |
| const fileName = document.getElementById('fileName'); | |
| const audioUrl = document.getElementById('audioUrl'); | |
| const addUrlBtn = document.getElementById('addUrlBtn'); | |
| const urlList = document.getElementById('urlList'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const statusText = document.getElementById('statusText'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const errorText = document.getElementById('errorText'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const timestampsContainer = document.getElementById('timestampsContainer'); | |
| const matchPercentage = document.getElementById('matchPercentage'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const loadingSubtext = document.getElementById('loadingSubtext'); | |
| // State | |
| let uploadedFiles = []; | |
| let audioUrls = []; | |
| let audioContext = null; | |
| let isProcessing = false; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Set up event listeners | |
| audioUpload.addEventListener('change', handleFileUpload); | |
| fileUploadArea.addEventListener('click', () => audioUpload.click()); | |
| fileUploadArea.addEventListener('dragover', handleDragOver); | |
| fileUploadArea.addEventListener('drop', handleDrop); | |
| addUrlBtn.addEventListener('click', addAudioUrl); | |
| analyzeBtn.addEventListener('click', analyzeSimilarity); | |
| // Initialize audio context | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| // Load any saved state | |
| loadState(); | |
| }); | |
| // File upload handlers | |
| function handleFileUpload(e) { | |
| const files = e.target.files; | |
| if (files.length > 0) { | |
| const file = files[0]; | |
| uploadedFiles = [file]; | |
| fileName.textContent = file.name; | |
| updateStatus('File ready for analysis'); | |
| } | |
| } | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| fileUploadArea.style.borderColor = '#2ecc71'; | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| fileUploadArea.style.borderColor = 'var(--primary-color)'; | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| const file = files[0]; | |
| if (file.type.startsWith('audio/')) { | |
| uploadedFiles = [file]; | |
| fileName.textContent = file.name; | |
| updateStatus('File ready for analysis'); | |
| } else { | |
| showError('Please drop an audio file'); | |
| } | |
| } | |
| } | |
| // URL handlers | |
| function addAudioUrl() { | |
| const url = audioUrl.value.trim(); | |
| if (url) { | |
| if (!isValidUrl(url)) { | |
| showError('Please enter a valid URL'); | |
| return; | |
| } | |
| audioUrls.push(url); | |
| updateUrlList(); | |
| audioUrl.value = ''; | |
| updateStatus('URL added for analysis'); | |
| } | |
| } | |
| function isValidUrl(url) { | |
| try { | |
| new URL(url); | |
| return true; | |
| } catch (e) { | |
| return false; | |
| } | |
| } | |
| function updateUrlList() { | |
| urlList.innerHTML = ''; | |
| if (audioUrls.length === 0) return; | |
| const list = document.createElement('div'); | |
| list.className = 'url-items'; | |
| audioUrls.forEach((url, index) => { | |
| const urlItem = document.createElement('div'); | |
| urlItem.className = 'audio-preview'; | |
| const urlInfo = document.createElement('div'); | |
| urlInfo.className = 'audio-info'; | |
| const urlName = document.createElement('div'); | |
| urlName.className = 'audio-name'; | |
| urlName.textContent = truncateUrl(url); | |
| const urlIndex = document.createElement('div'); | |
| urlIndex.className = 'audio-duration'; | |
| urlIndex.textContent = `URL ${index + 1}`; | |
| urlInfo.appendChild(urlName); | |
| urlInfo.appendChild(urlIndex); | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.className = 'btn btn-remove btn-small'; | |
| removeBtn.innerHTML = '<i class="fas fa-trash"></i>'; | |
| removeBtn.onclick = () => removeUrl(index); | |
| urlItem.appendChild(urlInfo); | |
| urlItem.appendChild(removeBtn); | |
| list.appendChild(urlItem); | |
| }); | |
| urlList.appendChild(list); | |
| } | |
| function truncateUrl(url) { | |
| if (url.length <= 40) return url; | |
| return url.substring(0, 20) + '...' + url.substring(url.length - 15); | |
| } | |
| function removeUrl(index) { | |
| audioUrls.splice(index, 1); | |
| updateUrlList(); | |
| updateStatus('URL removed'); | |
| } | |
| // Analysis functions | |
| async function analyzeSimilarity() { | |
| if (isProcessing) return; | |
| // Validate inputs | |
| if (uploadedFiles.length === 0 && audioUrls.length === 0) { | |
| showError('Please upload an audio file or add a URL'); | |
| return; | |
| } | |
| // Show loading overlay | |
| isProcessing = true; | |
| loadingOverlay.style.display = 'flex'; | |
| resultsSection.style.display = 'none'; | |
| try { | |
| // Simulate analysis process | |
| updateProgress(0, 'Loading audio files...'); | |
| await simulateDelay(500); | |
| updateProgress(20, 'Extracting audio features...'); | |
| await simulateDelay(800); | |
| updateProgress(40, 'Analyzing frequency patterns...'); | |
| await simulateDelay(1000); | |
| updateProgress(60, 'Comparing audio segments...'); | |
| await simulateDelay(1200); | |
| updateProgress(80, 'Calculating similarity scores...'); | |
| await simulateDelay(800); | |
| // Generate mock results | |
| const results = generateMockResults(); | |
| updateProgress(100, 'Analysis complete!'); | |
| await simulateDelay(500); | |
| // Display results | |
| displayResults(results); | |
| } catch (error) { | |
| showError('Analysis failed: ' + error.message); | |
| } finally { | |
| isProcessing = false; | |
| loadingOverlay.style.display = 'none'; | |
| } | |
| } | |
| function generateMockResults() { | |
| // Generate random similarity percentage between 30% and 95% | |
| const similarity = Math.floor(Math.random() * 65) + 30; | |
| // Generate 3-7 timestamp matches | |
| const numMatches = Math.floor(Math.random() * 5) + 3; | |
| const timestamps = []; | |
| for (let i = 0; i < numMatches; i++) { | |
| // Generate random timestamps (in seconds) | |
| const start = Math.floor(Math.random() * 180); // 0-3 minutes | |
| const duration = Math.floor(Math.random() * 30) + 5; // 5-35 seconds | |
| // Generate similarity score for this segment (higher than overall) | |
| const segmentSimilarity = Math.min(100, similarity + Math.floor(Math.random() * 30)); | |
| timestamps.push({ | |
| start: formatTime(start), | |
| end: formatTime(start + duration), | |
| duration: duration, | |
| similarity: segmentSimilarity, | |
| waveform: generateMockWaveform() | |
| }); | |
| } | |
| return { | |
| overallSimilarity: similarity, | |
| timestamps: timestamps.sort((a, b) => b.similarity - a.similarity) | |
| }; | |
| } | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| function generateMockWaveform() { | |
| const points = []; | |
| const numPoints = 100; | |
| // Generate random waveform data | |
| for (let i = 0; i < numPoints; i++) { | |
| points.push(Math.random() * 0.8 + 0.1); | |
| } | |
| return points; | |
| } | |
| function displayResults(results) { | |
| // Update match percentage | |
| matchPercentage.textContent = `${results.overallSimilarity}%`; | |
| // Clear previous results | |
| timestampsContainer.innerHTML = ''; | |
| // Create timestamp cards | |
| results.timestamps.forEach((timestamp, index) => { | |
| const card = document.createElement('div'); | |
| card.className = 'timestamp-card'; | |
| const header = document.createElement('div'); | |
| header.className = 'timestamp-header'; | |
| const time = document.createElement('div'); | |
| time.className = 'timestamp-time'; | |
| time.textContent = `${timestamp.start} - ${timestamp.end}`; | |
| const score = document.createElement('div'); | |
| score.className = 'similarity-score'; | |
| score.textContent = `${timestamp.similarity}% match`; | |
| header.appendChild(time); | |
| header.appendChild(score); | |
| const details = document.createElement('div'); | |
| details.className = 'timestamp-details'; | |
| details.textContent = `Duration: ${timestamp.duration} seconds`; | |
| const waveformContainer = document.createElement('div'); | |
| waveformContainer.className = 'waveform-container'; | |
| const waveform = document.createElement('div'); | |
| waveform.className = 'waveform'; | |
| waveform.id = `waveform-${index}`; | |
| waveformContainer.appendChild(waveform); | |
| card.appendChild(header); | |
| card.appendChild(details); | |
| card.appendChild(waveformContainer); | |
| timestampsContainer.appendChild(card); | |
| // Render waveform chart | |
| renderWaveformChart(index, timestamp.waveform); | |
| }); | |
| // Show results section | |
| resultsSection.style.display = 'block'; | |
| updateStatus('Analysis complete. Found ' + results.timestamps.length + ' matching segments.'); | |
| } | |
| function renderWaveformChart(index, data) { | |
| const options = { | |
| series: [{ | |
| name: 'Amplitude', | |
| data: data | |
| }], | |
| chart: { | |
| type: 'area', | |
| height: 80, | |
| sparkline: { | |
| enabled: true | |
| }, | |
| animations: { | |
| enabled: false | |
| } | |
| }, | |
| stroke: { | |
| curve: 'smooth', | |
| width: 1.5, | |
| colors: [var(--primary-color)] | |
| }, | |
| fill: { | |
| type: 'gradient', | |
| gradient: { | |
| shadeIntensity: 1, | |
| opacityFrom: 0.7, | |
| opacityTo: 0.1, | |
| stops: [0, 100] | |
| } | |
| }, | |
| xaxis: { | |
| labels: { | |
| show: false | |
| }, | |
| axisBorder: { | |
| show: false | |
| }, | |
| axisTicks: { | |
| show: false | |
| } | |
| }, | |
| yaxis: { | |
| show: false | |
| }, | |
| grid: { | |
| show: false, | |
| padding: { | |
| top: 0, | |
| right: 0, | |
| bottom: 0, | |
| left: 0 | |
| } | |
| }, | |
| tooltip: { | |
| enabled: false | |
| } | |
| }; | |
| const chart = new ApexCharts(document.querySelector(`#waveform-${index}`), options); | |
| chart.render(); | |
| } | |
| // Utility functions | |
| function updateStatus(text) { | |
| statusText.textContent = text; | |
| } | |
| function updateProgress(percent, text) { | |
| progressBar.style.width = `${percent}%`; | |
| loadingSubtext.textContent = text; | |
| } | |
| function showError(message) { | |
| errorText.textContent = message; | |
| errorMessage.classList.add('show'); | |
| // Hide error after 5 seconds | |
| setTimeout(() => { | |
| errorMessage.classList.remove('show'); | |
| }, 5000); | |
| } | |
| function simulateDelay(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| function loadState() { | |
| // In a real app, you might load from localStorage | |
| // For this demo, we'll just initialize empty arrays | |
| uploadedFiles = []; | |
| audioUrls = []; | |
| } | |
| // Save state (not used in this demo but would be in a real app) | |
| function saveState() { | |
| // localStorage.setItem('audioSimilarityState', JSON.stringify({ | |
| // uploadedFiles: uploadedFiles.map(f => ({ name: f.name, size: f.size })), | |
| // audioUrls: audioUrls | |
| // })); | |
| } | |
| </script> | |
| </body> | |
| </html> |