| <!DOCTYPE html> |
| <html lang="en" class="dark"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>DeepFilterNet Audio Cleaner</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/lucide@latest"></script> |
| <script> |
| tailwind.config = { |
| darkMode: 'class', |
| theme: { |
| extend: { |
| colors: { |
| primary: { |
| 50: '#eff6ff', |
| 100: '#dbeafe', |
| 500: '#3b82f6', |
| 600: '#2563eb', |
| 900: '#1e3a8a', |
| } |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| .drag-active { |
| border-color: #3b82f6 !important; |
| background-color: rgba(59, 130, 246, 0.1) !important; |
| } |
| .hidden { display: none !important; } |
| |
| |
| .spinner { |
| animation: spin 1s linear infinite; |
| } |
| @keyframes spin { |
| from { transform: rotate(0deg); } |
| to { transform: rotate(360deg); } |
| } |
| </style> |
| </head> |
| <body class="bg-slate-900 text-slate-200 min-h-screen font-sans selection:bg-primary-500 selection:text-white pb-12"> |
| |
| <div class="max-w-4xl mx-auto px-4 pt-12"> |
| |
| |
| <header class="text-center mb-10"> |
| <div class="inline-flex items-center justify-center p-3 bg-primary-900/50 rounded-2xl mb-4 border border-primary-500/30 text-primary-500"> |
| <i data-lucide="mic" class="w-8 h-8"></i> |
| <i data-lucide="sparkles" class="w-5 h-5 ml-1 absolute transform translate-x-4 -translate-y-4 text-yellow-400"></i> |
| </div> |
| <h1 class="text-4xl font-bold mb-3 text-white">DeepFilterNet Offline Cleaner</h1> |
| <p class="text-slate-400 max-w-2xl mx-auto text-lg"> |
| Remove background noise and enhance speech clarity entirely in your browser. |
| Powered by the <a href="https://github.com/Rikorose/DeepFilterNet" target="_blank" class="text-primary-500 hover:underline">DeepFilterNet3 AI model</a>. |
| <span class="text-green-400 font-medium text-sm block mt-2"> |
| <i data-lucide="shield-check" class="w-4 h-4 inline align-text-bottom"></i> |
| 100% Client-Side Privacy: Your audio never leaves your device. |
| </span> |
| </p> |
| </header> |
|
|
| |
| <main class="bg-slate-800 rounded-2xl border border-slate-700 shadow-xl overflow-hidden"> |
| |
| |
| <div class="p-6 border-b border-slate-700 bg-slate-800/50 flex flex-col md:flex-row items-center justify-between gap-4"> |
| <div class="flex items-center gap-3"> |
| <i data-lucide="sliders-horizontal" class="w-5 h-5 text-slate-400"></i> |
| <label for="noiseLevel" class="text-sm font-medium">Noise Reduction Level:</label> |
| <span id="noiseLevelValue" class="text-primary-500 font-bold w-8 text-center">100</span> |
| </div> |
| <input type="range" id="noiseLevel" min="0" max="100" value="100" class="w-full md:w-64 accent-primary-500 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer"> |
| </div> |
|
|
| |
| <div class="p-8"> |
| <div id="dropzone" class="border-2 border-dashed border-slate-600 rounded-xl p-10 text-center cursor-pointer transition-all duration-200 hover:border-primary-500 hover:bg-slate-700/30 group"> |
| <input type="file" id="fileInput" accept="audio/*" class="hidden"> |
| <i data-lucide="upload-cloud" class="w-12 h-12 mx-auto text-slate-400 group-hover:text-primary-500 transition-colors mb-4"></i> |
| <h3 class="text-xl font-semibold text-white mb-2">Drag & Drop Audio File</h3> |
| <p class="text-slate-400 text-sm">or click to browse from your device</p> |
| <p class="text-slate-500 text-xs mt-4">Supports WAV, MP3, AAC, OGG</p> |
| </div> |
|
|
| |
| <div id="statusContainer" class="hidden mt-8 p-6 bg-slate-900/50 rounded-xl border border-slate-700"> |
| <div class="flex items-center gap-4 mb-2"> |
| <i data-lucide="loader-2" id="statusIcon" class="w-6 h-6 text-primary-500 spinner"></i> |
| <h4 id="statusTitle" class="text-lg font-medium text-white">Initializing Engine...</h4> |
| </div> |
| <p id="statusMessage" class="text-sm text-slate-400 ml-10">Loading WebAssembly and DeepFilterNet3 neural network models...</p> |
| </div> |
|
|
| |
| <div id="resultsContainer" class="hidden mt-8 space-y-6"> |
| <div class="grid md:grid-cols-2 gap-6"> |
| |
| <div class="bg-slate-900 rounded-xl p-5 border border-slate-700"> |
| <h4 class="text-sm font-semibold text-slate-400 mb-3 uppercase tracking-wider flex items-center gap-2"> |
| <i data-lucide="headphones" class="w-4 h-4"></i> Original Audio |
| </h4> |
| <audio id="originalAudio" controls class="w-full h-10 rounded outline-none"></audio> |
| </div> |
| |
| |
| <div class="bg-primary-900/20 rounded-xl p-5 border border-primary-500/30"> |
| <h4 class="text-sm font-semibold text-primary-400 mb-3 uppercase tracking-wider flex items-center gap-2"> |
| <i data-lucide="check-circle-2" class="w-4 h-4"></i> Cleaned Audio |
| </h4> |
| <audio id="cleanedAudio" controls class="w-full h-10 rounded outline-none"></audio> |
| </div> |
| </div> |
|
|
| |
| <div class="flex justify-center pt-4"> |
| <button id="downloadBtn" class="flex items-center gap-2 bg-primary-600 hover:bg-primary-500 text-white px-6 py-3 rounded-xl font-medium transition-all shadow-lg shadow-primary-500/20 hover:shadow-primary-500/40"> |
| <i data-lucide="download" class="w-5 h-5"></i> |
| Download Cleaned Audio (.wav) |
| </button> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| |
| <script type="module"> |
| |
| if (typeof window.SharedArrayBuffer === 'undefined') { |
| window.SharedArrayBuffer = window.ArrayBuffer; |
| } |
| |
| |
| |
| |
| |
| const originalFetch = window.fetch; |
| window.fetch = async function(...args) { |
| let request = args[0]; |
| let reqUrl = typeof request === 'string' ? request : (request && request.url); |
| |
| if (reqUrl && typeof reqUrl === 'string') { |
| let newUrl = null; |
| |
| if (reqUrl.includes('df_bg.wasm')) { |
| console.log("Redirecting WASM fetch to Hugging Face..."); |
| newUrl = 'https://huggingface.co/trysem/DeepFilterNet3/resolve/main/df_bg.wasm'; |
| } else if (reqUrl.includes('DeepFilterNet3_onnx.tar.gz')) { |
| console.log("Redirecting Model fetch to Hugging Face..."); |
| newUrl = 'https://huggingface.co/trysem/DeepFilterNet3/resolve/main/DeepFilterNet3_onnx.tar.gz'; |
| } |
| |
| |
| if (newUrl) { |
| if (request instanceof Request) { |
| |
| args[0] = new Request(newUrl, request); |
| } else { |
| args[0] = newUrl; |
| } |
| } |
| } |
| |
| |
| return originalFetch.apply(this, args); |
| }; |
| |
| |
| lucide.createIcons(); |
| |
| |
| const dropzone = document.getElementById('dropzone'); |
| const fileInput = document.getElementById('fileInput'); |
| const noiseLevel = document.getElementById('noiseLevel'); |
| const noiseLevelValue = document.getElementById('noiseLevelValue'); |
| |
| const statusContainer = document.getElementById('statusContainer'); |
| const statusIcon = document.getElementById('statusIcon'); |
| const statusTitle = document.getElementById('statusTitle'); |
| const statusMessage = document.getElementById('statusMessage'); |
| |
| const resultsContainer = document.getElementById('resultsContainer'); |
| const originalAudio = document.getElementById('originalAudio'); |
| const cleanedAudio = document.getElementById('cleanedAudio'); |
| const downloadBtn = document.getElementById('downloadBtn'); |
| |
| let cleanedBlobUrl = null; |
| |
| |
| noiseLevel.addEventListener('input', (e) => { |
| noiseLevelValue.textContent = e.target.value; |
| }); |
| |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| dropzone.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| dropzone.addEventListener(eventName, () => dropzone.classList.add('drag-active'), false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| dropzone.addEventListener(eventName, () => dropzone.classList.remove('drag-active'), false); |
| }); |
| |
| dropzone.addEventListener('drop', (e) => { |
| let dt = e.dataTransfer; |
| let files = dt.files; |
| if (files.length > 0) handleFile(files[0]); |
| }); |
| |
| dropzone.addEventListener('click', () => fileInput.click()); |
| fileInput.addEventListener('change', function() { |
| if (this.files.length > 0) handleFile(this.files[0]); |
| }); |
| |
| function setStatus(state, title, message) { |
| statusContainer.classList.remove('hidden'); |
| resultsContainer.classList.add('hidden'); |
| statusTitle.textContent = title; |
| statusMessage.textContent = message; |
| |
| if (state === 'error') { |
| statusIcon.setAttribute('data-lucide', 'alert-circle'); |
| statusIcon.classList.remove('spinner'); |
| statusIcon.classList.add('text-red-500'); |
| } else if (state === 'done') { |
| statusIcon.setAttribute('data-lucide', 'check-circle-2'); |
| statusIcon.classList.remove('spinner'); |
| statusIcon.classList.replace('text-primary-500', 'text-green-500'); |
| } else { |
| statusIcon.setAttribute('data-lucide', 'loader-2'); |
| statusIcon.classList.add('spinner'); |
| statusIcon.classList.replace('text-green-500', 'text-primary-500'); |
| statusIcon.classList.replace('text-red-500', 'text-primary-500'); |
| } |
| lucide.createIcons(); |
| } |
| |
| async function handleFile(file) { |
| if (!file.type.startsWith('audio/')) { |
| setStatus('error', 'Invalid File Type', 'Please upload a valid audio file format.'); |
| return; |
| } |
| |
| try { |
| |
| const originalUrl = URL.createObjectURL(file); |
| originalAudio.src = originalUrl; |
| |
| |
| setStatus('loading', 'Decoding Audio...', 'Processing raw audio data...'); |
| const audioData = await file.arrayBuffer(); |
| |
| |
| const targetSampleRate = 48000; |
| const decodeCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: targetSampleRate }); |
| const audioBuffer = await decodeCtx.decodeAudioData(audioData); |
| |
| setStatus('loading', 'Loading AI Model...', 'Fetching WebAssembly & Weights (~17MB)...'); |
| |
| |
| const dfModule = await import('https://esm.sh/deepfilternet3-noise-filter@1.2.1'); |
| const ProcessorClass = dfModule.DeepFilterNet3Core || dfModule.DeepFilterNoiseFilterProcessor; |
| |
| if (!ProcessorClass) throw new Error("Could not initialize the DeepFilterNet AI Processor."); |
| |
| |
| const level = parseInt(noiseLevel.value, 10); |
| |
| const proc = new ProcessorClass({ |
| sampleRate: targetSampleRate, |
| noiseReductionLevel: level, |
| |
| |
| assetConfig: { cdnUrl: 'https://huggingface.co/trysem/DeepFilterNet3/resolve/main' } |
| }); |
| |
| |
| await proc.initialize(); |
| |
| |
| setStatus('loading', 'Cleaning Audio...', 'Running the neural network over the audio frames...'); |
| |
| |
| const offlineCtx = new OfflineAudioContext(1, audioBuffer.length, targetSampleRate); |
| const sourceNode = offlineCtx.createBufferSource(); |
| |
| |
| if (audioBuffer.numberOfChannels > 1) { |
| const monoBuffer = offlineCtx.createBuffer(1, audioBuffer.length, targetSampleRate); |
| const monoData = monoBuffer.getChannelData(0); |
| const left = audioBuffer.getChannelData(0); |
| const right = audioBuffer.getChannelData(1); |
| for (let i = 0; i < audioBuffer.length; i++) { |
| monoData[i] = (left[i] + right[i]) / 2; |
| } |
| sourceNode.buffer = monoBuffer; |
| } else { |
| sourceNode.buffer = audioBuffer; |
| } |
| |
| |
| const dfNode = await proc.createAudioWorkletNode(offlineCtx); |
| sourceNode.connect(dfNode); |
| dfNode.connect(offlineCtx.destination); |
| sourceNode.start(0); |
| |
| |
| const renderedBuffer = await offlineCtx.startRendering(); |
| |
| |
| setStatus('loading', 'Finalizing...', 'Encoding cleaned audio to WAV format...'); |
| const wavBlob = audioBufferToWav(renderedBuffer); |
| |
| if (cleanedBlobUrl) URL.revokeObjectURL(cleanedBlobUrl); |
| cleanedBlobUrl = URL.createObjectURL(wavBlob); |
| |
| |
| setStatus('done', 'Processing Complete!', 'Your audio has been successfully cleaned.'); |
| cleanedAudio.src = cleanedBlobUrl; |
| resultsContainer.classList.remove('hidden'); |
| |
| |
| downloadBtn.onclick = () => { |
| const a = document.createElement('a'); |
| a.style.display = 'none'; |
| a.href = cleanedBlobUrl; |
| a.download = `Cleaned_${file.name.split('.')[0] || 'audio'}.wav`; |
| document.body.appendChild(a); |
| a.click(); |
| setTimeout(() => document.body.removeChild(a), 100); |
| }; |
| |
| } catch (err) { |
| console.error(err); |
| setStatus('error', 'Processing Failed', err.message || 'An error occurred during audio processing. Check console for details.'); |
| } |
| } |
| |
| |
| |
| |
| |
| function audioBufferToWav(buffer) { |
| const numOfChan = buffer.numberOfChannels; |
| const length = buffer.length * numOfChan * 2 + 44; |
| const bufferData = new ArrayBuffer(length); |
| const view = new DataView(bufferData); |
| const channels = []; |
| let i, sample, offset = 0, pos = 0; |
| |
| |
| setUint32(0x46464952); |
| setUint32(length - 8); |
| setUint32(0x45564157); |
| setUint32(0x20746d66); |
| setUint32(16); |
| setUint16(1); |
| setUint16(numOfChan); |
| setUint32(buffer.sampleRate); |
| setUint32(buffer.sampleRate * 2 * numOfChan); |
| setUint16(numOfChan * 2); |
| setUint16(16); |
| setUint32(0x61746164); |
| setUint32(length - pos - 4); |
| |
| |
| for (i = 0; i < buffer.numberOfChannels; i++) { |
| channels.push(buffer.getChannelData(i)); |
| } |
| |
| |
| while (pos < buffer.length) { |
| for (i = 0; i < numOfChan; i++) { |
| sample = Math.max(-1, Math.min(1, channels[i][pos])); |
| sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; |
| view.setInt16(offset, sample, true); |
| offset += 2; |
| } |
| pos++; |
| } |
| return new Blob([bufferData], { type: "audio/wav" }); |
| |
| function setUint16(data) { |
| view.setUint16(offset, data, true); |
| offset += 2; |
| } |
| function setUint32(data) { |
| view.setUint32(offset, data, true); |
| offset += 4; |
| } |
| } |
| </script> |
| </body> |
| </html> |