TonyLikeDev commited on
Commit
ebe2347
·
1 Parent(s): 9c781a8

redesign UI two-column layout and add MP4 support

Browse files
Dockerfile CHANGED
@@ -15,6 +15,7 @@ COPY frontend/ ./frontend/
15
 
16
  RUN mkdir -p backend/uploads separated
17
 
 
18
  RUN python -c "from df.enhance import init_df; init_df()"
19
 
20
  EXPOSE 7860
 
15
 
16
  RUN mkdir -p backend/uploads separated
17
 
18
+ RUN python -c "from demucs.pretrained import get_model; get_model('mdx_extra')"
19
  RUN python -c "from df.enhance import init_df; init_df()"
20
 
21
  EXPOSE 7860
backend/demucs_runner.py CHANGED
@@ -14,21 +14,66 @@ def _get_model():
14
  return _model, _df_state
15
 
16
 
17
- def run_demucs(input_path: str) -> str:
18
- # Convert mp3 to wav — DeepFilterNet can't read mp3 directly
19
- if input_path.lower().endswith('.mp3'):
20
- wav_path = input_path.rsplit('.', 1)[0] + '.wav'
21
- subprocess.run(['ffmpeg', '-i', input_path, wav_path, '-y'], check=True)
22
- input_path = wav_path
23
-
24
  model, df_state = _get_model()
25
- audio, _ = load_audio(input_path, sr=df_state.sr())
26
- enhanced = enhance(model, df_state, audio)
27
- enhanced = enhance(model, df_state, enhanced) # second pass
 
 
 
28
 
 
29
  filename = os.path.splitext(os.path.basename(input_path))[0]
 
30
  os.makedirs("separated", exist_ok=True)
31
- output_path = f"separated/{filename}_enhanced.wav"
32
- save_audio(output_path, enhanced, df_state.sr())
33
 
34
- return output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return _model, _df_state
15
 
16
 
17
+ def _process_audio(wav_path: str, atten_lim_db) -> str:
 
 
 
 
 
 
18
  model, df_state = _get_model()
19
+ audio, _ = load_audio(wav_path, sr=df_state.sr())
20
+ enhanced = enhance(model, df_state, audio, atten_lim_db=atten_lim_db)
21
+ enhanced_wav = wav_path.replace('.wav', '_enhanced.wav')
22
+ save_audio(enhanced_wav, enhanced, df_state.sr())
23
+ return enhanced_wav
24
+
25
 
26
+ def run_demucs(input_path: str, atten_lim_db: int = None) -> tuple[str, str]:
27
  filename = os.path.splitext(os.path.basename(input_path))[0]
28
+ ext = os.path.splitext(input_path)[1].lower()
29
  os.makedirs("separated", exist_ok=True)
 
 
30
 
31
+ if ext == '.mp4':
32
+ # Extract audio from video
33
+ audio_path = f"separated/{filename}_audio.wav"
34
+ subprocess.run([
35
+ 'ffmpeg', '-i', input_path, '-vn', '-ar', '44100', '-ac', '2',
36
+ audio_path, '-y'
37
+ ], check=True)
38
+
39
+ # Step 1: Demucs vocal separation
40
+ subprocess.run(
41
+ ['demucs', '--two-stems=vocals', '-n', 'mdx_extra', audio_path],
42
+ check=True
43
+ )
44
+ vocals_path = f"separated/mdx_extra/{filename}_audio/vocals.wav"
45
+
46
+ # Step 2: DeepFilterNet cleanup
47
+ enhanced_wav = _process_audio(vocals_path, atten_lim_db)
48
+
49
+ # Mux enhanced audio back into original video
50
+ output_path = f"separated/{filename}_enhanced.mp4"
51
+ subprocess.run([
52
+ 'ffmpeg', '-i', input_path, '-i', enhanced_wav,
53
+ '-c:v', 'copy', '-map', '0:v:0', '-map', '1:a:0',
54
+ output_path, '-y'
55
+ ], check=True)
56
+
57
+ return output_path, 'video'
58
+
59
+ else:
60
+ # Audio file — convert to wav if needed
61
+ if ext == '.mp3':
62
+ wav_path = input_path.rsplit('.', 1)[0] + '.wav'
63
+ subprocess.run(['ffmpeg', '-i', input_path, wav_path, '-y'], check=True)
64
+ input_path = wav_path
65
+
66
+ # Step 1: Demucs vocal separation
67
+ subprocess.run(
68
+ ['demucs', '--two-stems=vocals', '-n', 'mdx_extra', input_path],
69
+ check=True
70
+ )
71
+ vocals_path = f"separated/mdx_extra/{filename}/vocals.wav"
72
+
73
+ # Step 2: DeepFilterNet cleanup
74
+ enhanced_wav = _process_audio(vocals_path, atten_lim_db)
75
+
76
+ output_path = f"separated/{filename}_enhanced.wav"
77
+ os.rename(enhanced_wav, output_path)
78
+
79
+ return output_path, 'audio'
backend/main.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  import shutil
3
  import uuid
4
 
5
- from fastapi import FastAPI, File, HTTPException, UploadFile
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.responses import FileResponse
8
  from fastapi.staticfiles import StaticFiles
@@ -22,7 +22,7 @@ UPLOAD_DIR = "backend/uploads"
22
 
23
 
24
  @app.post("/upload")
25
- async def upload_audio(file: UploadFile = File(...)):
26
  ext = os.path.splitext(file.filename)[1]
27
  unique_name = f"{uuid.uuid4()}{ext}"
28
  save_path = os.path.join(UPLOAD_DIR, unique_name)
@@ -30,18 +30,22 @@ async def upload_audio(file: UploadFile = File(...)):
30
  with open(save_path, "wb") as buffer:
31
  shutil.copyfileobj(file.file, buffer)
32
 
33
- output_path = run_demucs(save_path)
34
  track_name = os.path.splitext(unique_name)[0]
35
 
36
- return {"download_url": f"/download/{track_name}", "status": "done"}
37
 
38
 
39
  @app.get("/download/{track_name}")
40
- def download_audio(track_name: str):
41
- file_path = f"separated/{track_name}_enhanced.wav"
42
- if not os.path.exists(file_path):
43
- raise HTTPException(status_code=404, detail="File not found")
44
- return FileResponse(file_path, media_type="audio/wav", filename="enhanced.wav")
 
 
 
 
45
 
46
 
47
  app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
 
2
  import shutil
3
  import uuid
4
 
5
+ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.responses import FileResponse
8
  from fastapi.staticfiles import StaticFiles
 
22
 
23
 
24
  @app.post("/upload")
25
+ async def upload_audio(file: UploadFile = File(...), atten_lim_db: int = Form(None)):
26
  ext = os.path.splitext(file.filename)[1]
27
  unique_name = f"{uuid.uuid4()}{ext}"
28
  save_path = os.path.join(UPLOAD_DIR, unique_name)
 
30
  with open(save_path, "wb") as buffer:
31
  shutil.copyfileobj(file.file, buffer)
32
 
33
+ output_path, file_type = run_demucs(save_path, atten_lim_db=atten_lim_db)
34
  track_name = os.path.splitext(unique_name)[0]
35
 
36
+ return {"download_url": f"/download/{track_name}", "file_type": file_type, "status": "done"}
37
 
38
 
39
  @app.get("/download/{track_name}")
40
+ def download_file(track_name: str):
41
+ for ext, media_type, label in [
42
+ ("_enhanced.mp4", "video/mp4", "enhanced.mp4"),
43
+ ("_enhanced.wav", "audio/wav", "enhanced.wav"),
44
+ ]:
45
+ file_path = f"separated/{track_name}{ext}"
46
+ if os.path.exists(file_path):
47
+ return FileResponse(file_path, media_type=media_type, filename=label)
48
+ raise HTTPException(status_code=404, detail="File not found")
49
 
50
 
51
  app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
docs/step-5-ui-redesign-and-mp4.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Step 5 — UI Redesign + MP4 Support
2
+
3
+ **Status: TODO**
4
+
5
+ ---
6
+
7
+ ## Goals
8
+
9
+ 1. Redesign the frontend to match the Enhance-style UI (upload zone, file card, toggle)
10
+ 2. Add MP4 support — extract audio, clean it, mux back into video
11
+
12
+ ---
13
+
14
+ ## UI Redesign Plan
15
+
16
+ ### Layout
17
+
18
+ ```
19
+ +------------------------------------------+
20
+ | Voice Isolation |
21
+ | |
22
+ | +------------------------------------+ |
23
+ | | [icon] | |
24
+ | | Drop your file here | | <- dashed upload zone
25
+ | | or Choose File | |
26
+ | +------------------------------------+ |
27
+ | |
28
+ | +------------------------------------+ |
29
+ | | filename.mp3 0:33 [del] | | <- file card (shown after pick)
30
+ | +------------------------------------+ |
31
+ | |
32
+ | [ Upload & Clean ] |
33
+ | |
34
+ | [status message] |
35
+ | |
36
+ | Original ●──────── Enhanced | <- toggle
37
+ | |
38
+ | +------------------------------------+ |
39
+ | | ▶ ─────────────────── 0:33 | | <- single audio/video player
40
+ | +------------------------------------+ |
41
+ +------------------------------------------+
42
+ ```
43
+
44
+ ### Key changes from current UI
45
+
46
+ | Current | New |
47
+ |---|---|
48
+ | Plain file input | Dashed drag-and-drop zone |
49
+ | Two separate players always visible | One player, toggled between original and enhanced |
50
+ | No file info shown | File card with name, duration, delete button |
51
+ | Blue button only | Purple accent color throughout |
52
+
53
+ ### Files to change
54
+
55
+ - `frontend/index.html` — new structure
56
+ - `frontend/style.css` — full rewrite
57
+ - `frontend/app.js` — add drag-and-drop, toggle logic, file card, duration display
58
+
59
+ ---
60
+
61
+ ## MP4 Support Plan
62
+
63
+ ### How it works
64
+
65
+ ```
66
+ User uploads MP4
67
+
68
+ ffmpeg extracts audio track → temp.wav
69
+
70
+ DeepFilterNet cleans the audio → enhanced.wav
71
+
72
+ ffmpeg muxes enhanced audio back into original video → output.mp4
73
+
74
+ User downloads clean MP4
75
+ ```
76
+
77
+ ### Backend changes
78
+
79
+ **`backend/demucs_runner.py`**
80
+ - Detect if input is `.mp4`
81
+ - If mp4: extract audio with ffmpeg, process, mux back into video
82
+ - If audio: existing flow (convert to wav if needed, process, return wav)
83
+
84
+ **`backend/main.py`**
85
+ - Update `/download/{track_name}` to serve either `.wav` or `.mp4` depending on what was uploaded
86
+ - Return correct `media_type` in the response
87
+
88
+ **`frontend/index.html`**
89
+ - Change `accept="audio/*"` to `accept="audio/*,video/mp4"`
90
+
91
+ **`frontend/app.js`**
92
+ - Detect if uploaded file is video
93
+ - Show `<video>` player instead of `<audio>` player for mp4 files
94
+ - Toggle works the same way
95
+
96
+ ---
97
+
98
+ ## Implementation Order
99
+
100
+ 1. Update `demucs_runner.py` — MP4 extraction and mux logic
101
+ 2. Update `main.py` — serve mp4 or wav correctly
102
+ 3. Rewrite `index.html` — new structure
103
+ 4. Rewrite `style.css` — new design
104
+ 5. Rewrite `app.js` — drag-drop, file card, toggle, video/audio player
105
+ 6. Test locally with both mp3 and mp4 files
106
+ 7. Push to Hugging Face
107
+
108
+ ---
109
+
110
+ ## ffmpeg commands used
111
+
112
+ ```bash
113
+ # Extract audio from mp4
114
+ ffmpeg -i input.mp4 -vn -ar 44100 -ac 2 audio.wav -y
115
+
116
+ # Mux enhanced audio back into video (replace audio, keep video stream)
117
+ ffmpeg -i input.mp4 -i enhanced.wav -c:v copy -map 0:v:0 -map 1:a:0 output.mp4 -y
118
+ ```
119
+
120
+ ---
121
+
122
+ *Created: 2026-05-01*
frontend/app.js CHANGED
@@ -1,47 +1,126 @@
1
- const uploadBtn = document.getElementById('uploadBtn');
2
- const fileInput = document.getElementById('fileInput');
3
- const status = document.getElementById('status');
4
- const players = document.getElementById('players');
5
- const originalPlayer = document.getElementById('originalPlayer');
6
- const cleanedPlayer = document.getElementById('cleanedPlayer');
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- uploadBtn.addEventListener('click', async () => {
9
- const file = fileInput.files[0];
10
 
11
- if (!file) {
12
- status.textContent = 'Please select an audio file first.';
13
- status.className = 'error';
14
- return;
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  uploadBtn.disabled = true;
18
- players.hidden = true;
 
19
  status.className = '';
20
- status.textContent = 'Uploading and processing... this takes 1-3 minutes on CPU.';
21
 
22
  try {
23
  const formData = new FormData();
24
  formData.append('file', file);
 
 
25
 
26
- const response = await fetch('/upload', {
27
- method: 'POST',
28
- body: formData,
29
- });
30
-
31
- if (!response.ok) {
32
- throw new Error(`Server error: ${response.status}`);
33
- }
34
 
35
  const data = await response.json();
 
 
 
 
36
 
37
- originalPlayer.src = URL.createObjectURL(file);
38
- cleanedPlayer.src = data.download_url;
 
 
 
 
 
 
 
 
39
 
40
- originalPlayer.addEventListener('play', () => cleanedPlayer.pause());
41
- cleanedPlayer.addEventListener('play', () => originalPlayer.pause());
 
42
 
43
- players.hidden = false;
44
- status.textContent = 'Done! Compare the two tracks below.';
 
45
  status.className = 'success';
46
 
47
  } catch (err) {
@@ -51,3 +130,14 @@ uploadBtn.addEventListener('click', async () => {
51
  uploadBtn.disabled = false;
52
  }
53
  });
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const uploadZone = document.getElementById('uploadZone');
2
+ const fileInput = document.getElementById('fileInput');
3
+ const chooseBtn = document.getElementById('chooseBtn');
4
+ const fileCard = document.getElementById('fileCard');
5
+ const fileName = document.getElementById('fileName');
6
+ const fileDuration = document.getElementById('fileDuration');
7
+ const deleteBtn = document.getElementById('deleteBtn');
8
+ const attenSlider = document.getElementById('attenSlider');
9
+ const attenValue = document.getElementById('attenValue');
10
+ const uploadBtn = document.getElementById('uploadBtn');
11
+ const status = document.getElementById('status');
12
+ const emptyState = document.getElementById('emptyState');
13
+ const playerSection = document.getElementById('playerSection');
14
+ const toggleEnhanced = document.getElementById('toggleEnhanced');
15
+ const audioPlayer = document.getElementById('audioPlayer');
16
+ const videoPlayer = document.getElementById('videoPlayer');
17
+ const downloadLink = document.getElementById('downloadLink');
18
 
19
+ const attenSteps = [null, 80, 40, 20, 10, 5];
20
+ const attenLabels = ['Off', 'High', 'Medium', 'Low', 'Minimal', 'Barely'];
21
 
22
+ let originalSrc = null;
23
+ let enhancedSrc = null;
24
+ let isVideo = false;
25
+ let selectedFile = null;
26
+
27
+ // --- File picking ---
28
+ chooseBtn.addEventListener('click', () => fileInput.click());
29
+ uploadZone.addEventListener('click', (e) => { if (e.target !== chooseBtn) fileInput.click(); });
30
+
31
+ uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
32
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
33
+ uploadZone.addEventListener('drop', (e) => {
34
+ e.preventDefault();
35
+ uploadZone.classList.remove('dragover');
36
+ const file = e.dataTransfer.files[0];
37
+ if (file) handleFile(file);
38
+ });
39
+
40
+ fileInput.addEventListener('change', () => {
41
+ if (fileInput.files[0]) handleFile(fileInput.files[0]);
42
+ });
43
+
44
+ function handleFile(file) {
45
+ selectedFile = file;
46
+ fileName.textContent = file.name;
47
+ fileDuration.textContent = '';
48
+ fileCard.hidden = false;
49
+ uploadBtn.hidden = false;
50
+ status.textContent = '';
51
+ status.className = '';
52
+
53
+ // Get duration via a temp media element
54
+ const url = URL.createObjectURL(file);
55
+ const media = file.type.startsWith('video') ? document.createElement('video') : document.createElement('audio');
56
+ media.src = url;
57
+ media.addEventListener('loadedmetadata', () => {
58
+ const mins = Math.floor(media.duration / 60);
59
+ const secs = Math.floor(media.duration % 60).toString().padStart(2, '0');
60
+ fileDuration.textContent = `${mins}:${secs}`;
61
+ URL.revokeObjectURL(url);
62
+ });
63
+ }
64
+
65
+ deleteBtn.addEventListener('click', () => {
66
+ fileInput.value = '';
67
+ selectedFile = null;
68
+ fileCard.hidden = true;
69
+ uploadBtn.hidden = true;
70
+ playerSection.hidden = true;
71
+ emptyState.hidden = false;
72
+ status.textContent = '';
73
+ });
74
+
75
+ // --- Slider ---
76
+ attenSlider.addEventListener('input', () => {
77
+ attenValue.textContent = attenLabels[attenSlider.value];
78
+ });
79
+
80
+ // --- Upload & process ---
81
+ uploadBtn.addEventListener('click', async () => {
82
+ const file = selectedFile || fileInput.files[0];
83
+ if (!file) return;
84
 
85
  uploadBtn.disabled = true;
86
+ playerSection.hidden = true;
87
+ emptyState.hidden = false;
88
  status.className = '';
89
+ status.textContent = 'Processing... this may take a few minutes.';
90
 
91
  try {
92
  const formData = new FormData();
93
  formData.append('file', file);
94
+ const attenDb = attenSteps[attenSlider.value];
95
+ if (attenDb !== null) formData.append('atten_lim_db', attenDb);
96
 
97
+ const response = await fetch('/upload', { method: 'POST', body: formData });
98
+ if (!response.ok) throw new Error(`Server error: ${response.status}`);
 
 
 
 
 
 
99
 
100
  const data = await response.json();
101
+ isVideo = data.file_type === 'video';
102
+
103
+ originalSrc = URL.createObjectURL(file);
104
+ enhancedSrc = data.download_url;
105
 
106
+ // Set up players
107
+ if (isVideo) {
108
+ videoPlayer.hidden = false;
109
+ audioPlayer.hidden = true;
110
+ videoPlayer.src = originalSrc;
111
+ } else {
112
+ audioPlayer.hidden = false;
113
+ videoPlayer.hidden = true;
114
+ audioPlayer.src = originalSrc;
115
+ }
116
 
117
+ toggleEnhanced.checked = false;
118
+ downloadLink.href = enhancedSrc;
119
+ downloadLink.download = isVideo ? 'enhanced.mp4' : 'enhanced.wav';
120
 
121
+ emptyState.hidden = true;
122
+ playerSection.hidden = false;
123
+ status.textContent = 'Done!';
124
  status.className = 'success';
125
 
126
  } catch (err) {
 
130
  uploadBtn.disabled = false;
131
  }
132
  });
133
+
134
+ // --- Toggle between original and enhanced ---
135
+ toggleEnhanced.addEventListener('change', () => {
136
+ const src = toggleEnhanced.checked ? enhancedSrc : originalSrc;
137
+ const player = isVideo ? videoPlayer : audioPlayer;
138
+ const wasPlaying = !player.paused;
139
+ const currentTime = player.currentTime;
140
+ player.src = src;
141
+ player.currentTime = currentTime;
142
+ if (wasPlaying) player.play();
143
+ });
frontend/index.html CHANGED
@@ -7,28 +7,114 @@
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
- <div class="card">
11
- <h1>Voice Isolation</h1>
12
- <p>Upload an audio file to remove background noise.</p>
13
 
14
- <div class="upload-area">
15
- <input type="file" id="fileInput" accept="audio/*">
16
- <button id="uploadBtn">Upload & Clean</button>
 
 
 
 
 
 
17
  </div>
 
18
 
19
- <div id="status"></div>
20
 
21
- <div class="players" id="players" hidden>
22
- <div class="player-box">
23
- <p>Original</p>
24
- <audio id="originalPlayer" controls></audio>
 
 
 
 
 
 
 
 
 
25
  </div>
26
- <div class="player-box">
27
- <p>Cleaned</p>
28
- <audio id="cleanedPlayer" controls></audio>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </div>
30
- </div>
31
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  <script src="app.js"></script>
34
  </body>
 
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
 
 
 
10
 
11
+ <header>
12
+ <div class="brand">
13
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2">
14
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
15
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
16
+ <line x1="12" y1="19" x2="12" y2="23"/>
17
+ <line x1="8" y1="23" x2="16" y2="23"/>
18
+ </svg>
19
+ <span>Voice Isolation</span>
20
  </div>
21
+ </header>
22
 
23
+ <main>
24
 
25
+ <!-- LEFT PANEL -->
26
+ <aside class="left-panel">
27
+
28
+ <!-- Upload zone -->
29
+ <div class="upload-zone" id="uploadZone">
30
+ <input type="file" id="fileInput" accept="audio/*,video/mp4" hidden>
31
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="1.5">
32
+ <path d="M9 19V6l12-3v13"/>
33
+ <circle cx="6" cy="18" r="3"/>
34
+ <circle cx="18" cy="15" r="3"/>
35
+ </svg>
36
+ <p class="upload-title">Enhance</p>
37
+ <button class="choose-btn" id="chooseBtn">Choose files</button>
38
  </div>
39
+
40
+ <!-- File card -->
41
+ <div class="file-card" id="fileCard" hidden>
42
+ <div class="file-info">
43
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
44
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
45
+ </svg>
46
+ <div>
47
+ <span class="file-name" id="fileName"></span>
48
+ <span class="file-duration" id="fileDuration"></span>
49
+ </div>
50
+ </div>
51
+ <button class="delete-btn" id="deleteBtn" title="Remove">
52
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
53
+ <polyline points="3 6 5 6 21 6"/>
54
+ <path d="M19 6l-1 14H6L5 6"/>
55
+ <path d="M10 11v6M14 11v6"/>
56
+ </svg>
57
+ </button>
58
  </div>
59
+
60
+ <!-- Process button -->
61
+ <button class="process-btn" id="uploadBtn" hidden>Enhance Audio</button>
62
+
63
+ <div id="status"></div>
64
+
65
+ </aside>
66
+
67
+ <!-- RIGHT PANEL -->
68
+ <section class="right-panel" id="rightPanel">
69
+
70
+ <div class="empty-state" id="emptyState">
71
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#c7d2fe" stroke-width="1.2">
72
+ <path d="M9 19V6l12-3v13"/>
73
+ <circle cx="6" cy="18" r="3"/>
74
+ <circle cx="18" cy="15" r="3"/>
75
+ </svg>
76
+ <p>Upload a file to get started</p>
77
+ </div>
78
+
79
+ <div class="player-section" id="playerSection" hidden>
80
+
81
+ <!-- Player -->
82
+ <div class="player-wrap">
83
+ <audio id="audioPlayer" controls></audio>
84
+ <video id="videoPlayer" controls hidden></video>
85
+ </div>
86
+
87
+ <!-- Toggle -->
88
+ <div class="toggle-row">
89
+ <span class="toggle-label">Original</span>
90
+ <label class="toggle-switch">
91
+ <input type="checkbox" id="toggleEnhanced">
92
+ <span class="toggle-slider"></span>
93
+ </label>
94
+ <span class="toggle-label enhanced-label">Enhanced</span>
95
+ </div>
96
+
97
+ <!-- Strength slider -->
98
+ <div class="control-card">
99
+ <div class="control-header">
100
+ <span class="control-name">Noise Reduction</span>
101
+ <span class="control-value" id="attenValue">Max</span>
102
+ </div>
103
+ <input type="range" id="attenSlider" min="0" max="5" value="0" step="1">
104
+ <div class="slider-hints">
105
+ <span>Less</span>
106
+ <span>More</span>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Download -->
111
+ <a class="download-btn" id="downloadLink" download>Download Enhanced File</a>
112
+
113
+ </div>
114
+
115
+ </section>
116
+
117
+ </main>
118
 
119
  <script src="app.js"></script>
120
  </body>
frontend/style.css CHANGED
@@ -5,106 +5,318 @@
5
  }
6
 
7
  body {
8
- font-family: sans-serif;
9
- background: #f0f2f5;
10
- display: flex;
11
- justify-content: center;
12
- align-items: flex-start;
13
- padding: 60px 20px;
14
  min-height: 100vh;
 
 
15
  }
16
 
17
- .card {
 
18
  background: white;
19
- padding: 40px;
20
- border-radius: 16px;
21
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
22
- max-width: 580px;
23
- width: 100%;
24
  }
25
 
26
- h1 {
27
- font-size: 28px;
28
- margin-bottom: 8px;
29
- color: #111;
 
 
 
30
  }
31
 
32
- p {
33
- color: #666;
34
- margin-bottom: 24px;
 
 
 
35
  }
36
 
37
- .upload-area {
 
 
 
 
 
 
38
  display: flex;
39
  flex-direction: column;
40
  gap: 12px;
41
- margin-bottom: 24px;
42
  }
43
 
44
- input[type="file"] {
45
- font-size: 14px;
46
- color: #444;
 
 
 
 
 
47
  }
48
 
49
- button {
50
- background: #2563eb;
51
- color: white;
52
- border: none;
53
- padding: 12px 24px;
54
- border-radius: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  cursor: pointer;
56
- font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  font-weight: 500;
58
- transition: background 0.2s;
59
- width: fit-content;
 
 
 
60
  }
61
 
62
- button:hover {
63
- background: #1d4ed8;
 
 
 
64
  }
65
 
66
- button:disabled {
67
- background: #93c5fd;
68
- cursor: not-allowed;
 
 
 
 
 
 
 
69
  }
70
 
71
- #status {
 
 
 
 
 
 
 
 
 
72
  font-size: 14px;
73
- color: #555;
74
- margin-bottom: 24px;
75
- min-height: 20px;
76
  }
77
 
78
- #status.error {
79
- color: #dc2626;
 
 
 
 
 
 
80
  }
81
 
82
- #status.success {
83
- color: #16a34a;
 
 
 
 
 
 
 
 
84
  }
85
 
86
- .players {
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  display: flex;
88
  flex-direction: column;
89
  gap: 20px;
90
  }
91
 
92
- .player-box {
93
- background: #f8fafc;
94
- border: 1px solid #e2e8f0;
95
- border-radius: 10px;
96
- padding: 16px;
97
  }
98
 
99
- .player-box p {
100
- font-weight: 600;
101
- color: #333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  margin-bottom: 10px;
 
 
 
103
  font-size: 14px;
104
- text-transform: uppercase;
105
- letter-spacing: 0.05em;
106
  }
107
 
108
- audio {
 
 
 
 
 
 
109
  width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
 
5
  }
6
 
7
  body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
9
+ background: #f5f5ff;
 
 
 
 
10
  min-height: 100vh;
11
+ display: flex;
12
+ flex-direction: column;
13
  }
14
 
15
+ /* Header */
16
+ header {
17
  background: white;
18
+ border-bottom: 1px solid #e8e8f0;
19
+ padding: 0 32px;
20
+ height: 52px;
21
+ display: flex;
22
+ align-items: center;
23
  }
24
 
25
+ .brand {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 8px;
29
+ font-weight: 700;
30
+ font-size: 16px;
31
+ color: #1e1b4b;
32
  }
33
 
34
+ /* Main layout */
35
+ main {
36
+ display: flex;
37
+ flex: 1;
38
+ height: calc(100vh - 52px);
39
+ overflow: hidden;
40
  }
41
 
42
+ /* Left panel */
43
+ .left-panel {
44
+ width: 260px;
45
+ min-width: 260px;
46
+ background: white;
47
+ border-right: 1px solid #e8e8f0;
48
+ padding: 20px 16px;
49
  display: flex;
50
  flex-direction: column;
51
  gap: 12px;
52
+ overflow-y: auto;
53
  }
54
 
55
+ /* Upload zone */
56
+ .upload-zone {
57
+ border: 2px dashed #c7d2fe;
58
+ border-radius: 14px;
59
+ padding: 28px 16px;
60
+ text-align: center;
61
+ cursor: pointer;
62
+ transition: border-color 0.2s, background 0.2s;
63
  }
64
 
65
+ .upload-zone:hover,
66
+ .upload-zone.dragover {
67
+ border-color: #6366f1;
68
+ background: #f5f3ff;
69
+ }
70
+
71
+ .upload-title {
72
+ font-size: 15px;
73
+ font-weight: 600;
74
+ color: #1e1b4b;
75
+ margin: 10px 0 12px;
76
+ }
77
+
78
+ .choose-btn {
79
+ background: white;
80
+ border: 1.5px solid #d1d5db;
81
+ color: #374151;
82
+ padding: 6px 16px;
83
+ border-radius: 20px;
84
+ font-size: 13px;
85
  cursor: pointer;
86
+ transition: border-color 0.2s;
87
+ }
88
+
89
+ .choose-btn:hover { border-color: #6366f1; color: #6366f1; }
90
+
91
+ /* File card */
92
+ .file-card {
93
+ background: #6366f1;
94
+ border-radius: 10px;
95
+ padding: 10px 12px;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ }
100
+
101
+ .file-info {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ min-width: 0;
106
+ }
107
+
108
+ .file-name {
109
+ display: block;
110
+ font-size: 13px;
111
  font-weight: 500;
112
+ color: white;
113
+ white-space: nowrap;
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ max-width: 140px;
117
  }
118
 
119
+ .file-duration {
120
+ display: block;
121
+ font-size: 11px;
122
+ color: #c7d2fe;
123
+ margin-top: 2px;
124
  }
125
 
126
+ .delete-btn {
127
+ background: none;
128
+ border: none;
129
+ color: #c7d2fe;
130
+ cursor: pointer;
131
+ padding: 4px;
132
+ flex-shrink: 0;
133
+ display: flex;
134
+ align-items: center;
135
+ transition: color 0.2s;
136
  }
137
 
138
+ .delete-btn:hover { color: white; }
139
+
140
+ /* Process button */
141
+ .process-btn {
142
+ width: 100%;
143
+ background: #6366f1;
144
+ color: white;
145
+ border: none;
146
+ padding: 12px;
147
+ border-radius: 10px;
148
  font-size: 14px;
149
+ font-weight: 600;
150
+ cursor: pointer;
151
+ transition: background 0.2s;
152
  }
153
 
154
+ .process-btn:hover { background: #4f46e5; }
155
+ .process-btn:disabled { background: #a5b4fc; cursor: not-allowed; }
156
+
157
+ /* Status */
158
+ #status {
159
+ font-size: 13px;
160
+ color: #6b7280;
161
+ text-align: center;
162
  }
163
 
164
+ #status.error { color: #dc2626; }
165
+ #status.success { color: #16a34a; }
166
+
167
+ /* Right panel */
168
+ .right-panel {
169
+ flex: 1;
170
+ padding: 28px 32px;
171
+ overflow-y: auto;
172
+ display: flex;
173
+ flex-direction: column;
174
  }
175
 
176
+ /* Empty state */
177
+ .empty-state {
178
+ flex: 1;
179
+ display: flex;
180
+ flex-direction: column;
181
+ align-items: center;
182
+ justify-content: center;
183
+ gap: 12px;
184
+ color: #c7d2fe;
185
+ font-size: 14px;
186
+ }
187
+
188
+ /* Player section */
189
+ .player-section {
190
  display: flex;
191
  flex-direction: column;
192
  gap: 20px;
193
  }
194
 
195
+ .player-wrap {
196
+ background: white;
197
+ border-radius: 16px;
198
+ padding: 20px;
199
+ box-shadow: 0 2px 12px rgba(99,102,241,0.08);
200
  }
201
 
202
+ audio, video {
203
+ width: 100%;
204
+ border-radius: 8px;
205
+ }
206
+
207
+ /* Toggle */
208
+ .toggle-row {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ gap: 12px;
213
+ }
214
+
215
+ .toggle-label {
216
+ font-size: 14px;
217
+ font-weight: 500;
218
+ color: #9ca3af;
219
+ }
220
+
221
+ .enhanced-label { color: #6366f1; font-weight: 600; }
222
+
223
+ .toggle-switch {
224
+ position: relative;
225
+ display: inline-block;
226
+ width: 48px;
227
+ height: 26px;
228
+ }
229
+
230
+ .toggle-switch input { display: none; }
231
+
232
+ .toggle-slider {
233
+ position: absolute;
234
+ inset: 0;
235
+ background: #e5e7eb;
236
+ border-radius: 999px;
237
+ cursor: pointer;
238
+ transition: background 0.2s;
239
+ }
240
+
241
+ .toggle-slider:before {
242
+ content: '';
243
+ position: absolute;
244
+ width: 20px;
245
+ height: 20px;
246
+ left: 3px;
247
+ top: 3px;
248
+ background: white;
249
+ border-radius: 50%;
250
+ transition: transform 0.2s;
251
+ box-shadow: 0 1px 4px rgba(0,0,0,0.15);
252
+ }
253
+
254
+ .toggle-switch input:checked + .toggle-slider { background: #6366f1; }
255
+ .toggle-switch input:checked + .toggle-slider:before { transform: translateX(22px); }
256
+
257
+ /* Control card */
258
+ .control-card {
259
+ background: white;
260
+ border-radius: 14px;
261
+ padding: 16px 20px;
262
+ box-shadow: 0 2px 12px rgba(99,102,241,0.06);
263
+ }
264
+
265
+ .control-header {
266
+ display: flex;
267
+ justify-content: space-between;
268
+ align-items: center;
269
  margin-bottom: 10px;
270
+ }
271
+
272
+ .control-name {
273
  font-size: 14px;
274
+ font-weight: 500;
275
+ color: #374151;
276
  }
277
 
278
+ .control-value {
279
+ font-size: 13px;
280
+ color: #6366f1;
281
+ font-weight: 600;
282
+ }
283
+
284
+ input[type="range"] {
285
  width: 100%;
286
+ accent-color: #6366f1;
287
+ cursor: pointer;
288
+ }
289
+
290
+ .slider-hints {
291
+ display: flex;
292
+ justify-content: space-between;
293
+ font-size: 11px;
294
+ color: #9ca3af;
295
+ margin-top: 6px;
296
+ }
297
+
298
+ /* Download button */
299
+ .download-btn {
300
+ display: block;
301
+ text-align: center;
302
+ background: white;
303
+ color: #6366f1;
304
+ border: 1.5px solid #6366f1;
305
+ padding: 12px;
306
+ border-radius: 10px;
307
+ font-size: 14px;
308
+ font-weight: 600;
309
+ text-decoration: none;
310
+ transition: background 0.2s, color 0.2s;
311
+ }
312
+
313
+ .download-btn:hover {
314
+ background: #6366f1;
315
+ color: white;
316
+ }
317
+
318
+ /* Responsive */
319
+ @media (max-width: 640px) {
320
+ main { flex-direction: column; height: auto; }
321
+ .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #e8e8f0; }
322
  }
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  fastapi
2
  uvicorn
3
  python-multipart
 
4
  deepfilternet
5
  torch
6
  aiofiles
 
1
  fastapi
2
  uvicorn
3
  python-multipart
4
+ demucs
5
  deepfilternet
6
  torch
7
  aiofiles