dFN3 / dfnv3-base
trysem's picture
Create dfnv3-base
8bdeb34 verified
<!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 */
.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 Section -->
<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 Content Area -->
<main class="bg-slate-800 rounded-2xl border border-slate-700 shadow-xl overflow-hidden">
<!-- Settings & Controls -->
<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>
<!-- Upload Zone -->
<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>
<!-- Status & Progress UI -->
<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>
<!-- Results UI -->
<div id="resultsContainer" class="hidden mt-8 space-y-6">
<div class="grid md:grid-cols-2 gap-6">
<!-- Original Audio -->
<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>
<!-- Cleaned Audio -->
<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>
<!-- Action Buttons -->
<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>
<!-- Application Logic -->
<script type="module">
// --- 1. SHARED-ARRAY-BUFFER POLYFILL ---
if (typeof window.SharedArrayBuffer === 'undefined') {
window.SharedArrayBuffer = window.ArrayBuffer;
}
// --- 2. HUGGING FACE DIRECT FETCH INTERCEPTOR ---
// Since you uploaded the files directly to the root of your Hugging Face repo,
// we intercept the library's requests (which expect nested /v2/pkg/ folders)
// and strictly redirect them to your raw Hugging Face file URLs.
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 we have a match, swap the URL while preserving all original fetch configurations
if (newUrl) {
if (request instanceof Request) {
// Rebuild the request to keep internal library headers/signals intact
args[0] = new Request(newUrl, request);
} else {
args[0] = newUrl;
}
}
}
// Let normal requests pass completely unfiltered
return originalFetch.apply(this, args);
};
// Initialize Icons
lucide.createIcons();
// UI Elements
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;
// Settings Listener
noiseLevel.addEventListener('input', (e) => {
noiseLevelValue.textContent = e.target.value;
});
// Drag and Drop Listeners
['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 {
// Prepare original audio player
const originalUrl = URL.createObjectURL(file);
originalAudio.src = originalUrl;
// 1. Decode original file into AudioBuffer
setStatus('loading', 'Decoding Audio...', 'Processing raw audio data...');
const audioData = await file.arrayBuffer();
// DeepFilterNet targets 48kHz for optimal quality
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)...');
// Import v1.2.1 which features SIMD optimizations and accurate APIs
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.");
// Initialize DeepFilterNet configuration
const level = parseInt(noiseLevel.value, 10);
const proc = new ProcessorClass({
sampleRate: targetSampleRate,
noiseReductionLevel: level,
// The fetch interceptor above handles the exact routing,
// but we provide your base URL here to satisfy the config requirements.
assetConfig: { cdnUrl: 'https://huggingface.co/trysem/DeepFilterNet3/resolve/main' }
});
// Execute Initialization
await proc.initialize();
// 2. Set up Offline Rendering Context for faster-than-realtime processing
setStatus('loading', 'Cleaning Audio...', 'Running the neural network over the audio frames...');
// We use mono processing as DeepFilterNet is single-channel optimized for voice
const offlineCtx = new OfflineAudioContext(1, audioBuffer.length, targetSampleRate);
const sourceNode = offlineCtx.createBufferSource();
// Mixdown to mono if stereo
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;
}
// Connect to DeepFilter AI node
const dfNode = await proc.createAudioWorkletNode(offlineCtx);
sourceNode.connect(dfNode);
dfNode.connect(offlineCtx.destination);
sourceNode.start(0);
// Render out the cleaned audio
const renderedBuffer = await offlineCtx.startRendering();
// 3. Convert resulting buffer to a downloadable WAV
setStatus('loading', 'Finalizing...', 'Encoding cleaned audio to WAV format...');
const wavBlob = audioBufferToWav(renderedBuffer);
if (cleanedBlobUrl) URL.revokeObjectURL(cleanedBlobUrl);
cleanedBlobUrl = URL.createObjectURL(wavBlob);
// Show UI
setStatus('done', 'Processing Complete!', 'Your audio has been successfully cleaned.');
cleanedAudio.src = cleanedBlobUrl;
resultsContainer.classList.remove('hidden');
// Set up Download button
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.');
}
}
/**
* Converts an AudioBuffer to a valid WAV Blob.
* DeepFilterNet outputs standard Float32, we convert to 16-bit PCM WAV.
*/
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;
// Write WAV Header
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); // length = 16
setUint16(1); // PCM (uncompressed)
setUint16(numOfChan);
setUint32(buffer.sampleRate);
setUint32(buffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
setUint16(numOfChan * 2); // block-align
setUint16(16); // 16-bit
setUint32(0x61746164); // "data" - chunk
setUint32(length - pos - 4); // chunk length
// Read channel data
for (i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
// Write Interleaved Audio Data
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>