Skip to content

Commit 3a810b0

Browse files
committed
Hopefully fixed filepath bug in modal deployment
1 parent ce324e6 commit 3a810b0

5 files changed

Lines changed: 105 additions & 10 deletions

File tree

audio_separator/remote/api_client.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88
import requests
99

10+
# Get package version for debugging
11+
try:
12+
from importlib.metadata import version
13+
AUDIO_SEPARATOR_VERSION = version("audio-separator")
14+
except ImportError:
15+
try:
16+
import pkg_resources
17+
AUDIO_SEPARATOR_VERSION = pkg_resources.get_distribution("audio-separator").version
18+
except Exception:
19+
AUDIO_SEPARATOR_VERSION = "unknown"
20+
1021

1122
class AudioSeparatorAPIClient:
1223
"""Client for interacting with a remotely deployed Audio Separator API."""
@@ -198,7 +209,7 @@ def separate_audio_and_wait(
198209

199210
# Submit the separation job with all parameters
200211
models_desc = models or ([model] if model else ["default"])
201-
self.logger.info(f"Submitting separation job for '{file_path}' with models: {models_desc}")
212+
self.logger.info(f"Submitting separation job for '{file_path}' with models: {models_desc} (audio-separator v{AUDIO_SEPARATOR_VERSION})")
202213

203214
result = self.separate_audio(
204215
file_path,
@@ -262,16 +273,27 @@ def separate_audio_and_wait(
262273
# Check if completed
263274
if current_status == "completed":
264275
self.logger.info("✅ Separation completed!")
276+
277+
files_list = status.get("files", [])
278+
self.logger.info(f"🔍 Job status returned {len(files_list)} files")
279+
for i, file in enumerate(files_list):
280+
self.logger.info(f" [{i}] '{file}' (len={len(file)}, repr={repr(file)})")
265281

266-
result = {"task_id": task_id, "status": "completed", "files": status.get("files", [])}
282+
result = {"task_id": task_id, "status": "completed", "files": files_list}
267283

268284
# Download files if requested
269285
if download:
270286
downloaded_files = []
271-
self.logger.info(f"📥 Downloading {len(status.get('files', []))} output files...")
287+
files_list = status.get("files", [])
288+
self.logger.info(f"📥 Downloading {len(files_list)} output files...")
289+
290+
# Log the filenames we're trying to download for debugging
291+
self.logger.info(f"🔍 Files to download: {files_list}")
272292

273-
for filename in status.get("files", []):
293+
for i, filename in enumerate(files_list):
274294
try:
295+
self.logger.info(f"🔍 [{i+1}/{len(files_list)}] Attempting to download: '{filename}' (len={len(filename)})")
296+
275297
if output_dir:
276298
output_path = f"{output_dir.rstrip('/')}/{filename}"
277299
else:
@@ -282,6 +304,13 @@ def separate_audio_and_wait(
282304
self.logger.info(f" ✅ Downloaded: {downloaded_path}")
283305
except Exception as e:
284306
self.logger.error(f" ❌ Failed to download {filename}: {e}")
307+
308+
# Try to get server version on download failure for debugging
309+
try:
310+
server_version = self.get_server_version()
311+
self.logger.error(f"🔍 Server version when download failed: {server_version}")
312+
except Exception as version_error:
313+
self.logger.error(f"🔍 Could not get server version: {version_error}")
285314

286315
result["downloaded_files"] = downloaded_files
287316
self.logger.info(f"🎉 Successfully downloaded {len(downloaded_files)} files!")
@@ -322,7 +351,29 @@ def download_file(self, task_id: str, filename: str, output_path: Optional[str]
322351
try:
323352
# URL encode the filename to handle spaces and special characters
324353
encoded_filename = quote(filename, safe='')
325-
response = self.session.get(f"{self.api_url}/download/{task_id}/{encoded_filename}", timeout=60)
354+
download_url = f"{self.api_url}/download/{task_id}/{encoded_filename}"
355+
356+
# Debug logging to understand what's happening
357+
self.logger.info(f"🔍 Download details:")
358+
self.logger.info(f" Original filename: '{filename}'")
359+
self.logger.info(f" Encoded filename: '{encoded_filename}'")
360+
self.logger.info(f" Download URL: {download_url}")
361+
self.logger.info(f" Task ID: {task_id}")
362+
363+
response = self.session.get(download_url, timeout=60)
364+
365+
# Log response details for debugging
366+
self.logger.info(f"🔍 Response status: {response.status_code}")
367+
if response.status_code != 200:
368+
try:
369+
self.logger.error(f"🔍 Response headers: {dict(response.headers)}")
370+
except Exception:
371+
self.logger.error(f"🔍 Response headers: {response.headers}")
372+
try:
373+
self.logger.error(f"🔍 Response text (first 500 chars): {response.text[:500]}")
374+
except Exception:
375+
self.logger.error(f"🔍 Response text: <unavailable>")
376+
326377
response.raise_for_status()
327378

328379
with open(output_path, "wb") as f:

audio_separator/remote/deploy_modal.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from importlib.metadata import version
3131
import typing
3232
from typing import Optional
33+
from urllib.parse import unquote
3334

3435
# Third-party imports
3536
from fastapi import FastAPI, File, Form, HTTPException, Response, UploadFile
@@ -102,7 +103,7 @@
102103
.pip_install(
103104
[
104105
# Core audio-separator with GPU support (this pulls in most dependencies from pyproject.toml)
105-
"audio-separator[gpu]>=0.35.0",
106+
"audio-separator[gpu]>=0.35.1",
106107
# FastAPI and web server dependencies for Modal API deployment
107108
"fastapi>=0.104.0",
108109
"uvicorn[standard]>=0.24.0",
@@ -616,7 +617,12 @@ async def download_file(task_id: str, filename: str) -> Response:
616617
Download a separated audio file
617618
"""
618619
try:
619-
file_data = get_file_function.remote(task_id, filename)
620+
# Explicitly URL-decode the filename to handle edge cases with special characters
621+
# FastAPI normally handles this, but complex filenames with multiple special chars
622+
# may not be decoded properly in all cases
623+
decoded_filename = unquote(filename)
624+
625+
file_data = get_file_function.remote(task_id, decoded_filename)
620626

621627
# Detect file type from content
622628
detected_type = filetype.guess(file_data)
@@ -625,10 +631,10 @@ async def download_file(task_id: str, filename: str) -> Response:
625631
content_type = detected_type.mime
626632
else:
627633
# Log when we can't detect the file type
628-
print(f"WARNING: Could not detect MIME type for {filename}, using generic type")
634+
print(f"WARNING: Could not detect MIME type for {decoded_filename}, using generic type")
629635
content_type = "application/octet-stream"
630636

631-
return Response(content=file_data, media_type=content_type, headers={"Content-Disposition": f"attachment; filename={filename}"})
637+
return Response(content=file_data, media_type=content_type, headers={"Content-Disposition": f"attachment; filename={decoded_filename}"})
632638

633639
except FileNotFoundError as exc:
634640
raise HTTPException(status_code=404, detail="File not found") from exc

poetry.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ pytest-cov = ">=4.1.0"
7373
matplotlib = ">=3.8.0"
7474
pillow = ">=10.1.0"
7575
scikit-image = ">=0.22.0"
76+
filetype = ">=1"
7677

7778
[tool.black]
7879
line-length = 140

tests/unit/test_remote_api_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def test_download_file_success(self, mock_file, mock_get, api_client):
161161
"""Test successful file download."""
162162
mock_response = Mock()
163163
mock_response.content = b"fake audio file content"
164+
mock_response.status_code = 200
164165
mock_response.raise_for_status.return_value = None
165166
mock_get.return_value = mock_response
166167

@@ -176,6 +177,7 @@ def test_download_file_default_output_path(self, mock_file, mock_get, api_client
176177
"""Test file download with default output path."""
177178
mock_response = Mock()
178179
mock_response.content = b"fake audio file content"
180+
mock_response.status_code = 200
179181
mock_response.raise_for_status.return_value = None
180182
mock_get.return_value = mock_response
181183

@@ -190,6 +192,7 @@ def test_download_file_with_spaces_in_filename(self, mock_file, mock_get, api_cl
190192
"""Test file download with spaces in filename (URL encoding)."""
191193
mock_response = Mock()
192194
mock_response.content = b"fake audio file content"
195+
mock_response.status_code = 200
193196
mock_response.raise_for_status.return_value = None
194197
mock_get.return_value = mock_response
195198

@@ -208,6 +211,7 @@ def test_download_file_with_special_characters(self, mock_file, mock_get, api_cl
208211
"""Test file download with special characters in filename."""
209212
mock_response = Mock()
210213
mock_response.content = b"fake audio file content"
214+
mock_response.status_code = 200
211215
mock_response.raise_for_status.return_value = None
212216
mock_get.return_value = mock_response
213217

@@ -226,6 +230,7 @@ def test_download_file_with_unicode_characters(self, mock_file, mock_get, api_cl
226230
"""Test file download with unicode characters in filename."""
227231
mock_response = Mock()
228232
mock_response.content = b"fake audio file content"
233+
mock_response.status_code = 200
229234
mock_response.raise_for_status.return_value = None
230235
mock_get.return_value = mock_response
231236

@@ -438,3 +443,23 @@ def test_separate_audio_and_wait_with_special_character_filenames(self, mock_sle
438443
]
439444
actual_calls = [call.args for call in mock_download.call_args_list]
440445
assert actual_calls == expected_calls
446+
447+
@patch("requests.Session.get")
448+
@patch("builtins.open", new_callable=mock_open)
449+
def test_download_file_server_side_url_decoding_scenario(self, mock_file, mock_get, api_client):
450+
"""Test that the client properly URL-encodes filenames that require server-side decoding."""
451+
mock_response = Mock()
452+
mock_response.content = b"fake audio file content"
453+
mock_response.status_code = 200
454+
mock_response.raise_for_status.return_value = None
455+
mock_get.return_value = mock_response
456+
457+
# Test the exact problematic filename from the bug report
458+
problematic_filename = "Bloc Party - The Prayer (Vocals model_bs_roformer_ep_317_sdr_12.9755.ckpt)_(Instrumental)_mel_band_roformer_karaoke_aufr33_viperx_sdr_10.flac"
459+
result = api_client.download_file("test-task-bug", problematic_filename)
460+
461+
# Verify URL was properly encoded - this is the exact URL that should be sent
462+
expected_url = "https://test-api.example.com/download/test-task-bug/Bloc%20Party%20-%20The%20Prayer%20%28Vocals%20model_bs_roformer_ep_317_sdr_12.9755.ckpt%29_%28Instrumental%29_mel_band_roformer_karaoke_aufr33_viperx_sdr_10.flac"
463+
mock_get.assert_called_once_with(expected_url, timeout=60)
464+
assert result == problematic_filename
465+
mock_file.assert_called_once_with(problematic_filename, "wb")

0 commit comments

Comments
 (0)