mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-02 16:45:15 -07:00
fix sample upload blocking the event loop and causing server timeouts
Move audio validation and saving to thread pool so librosa/ffmpeg decoding doesn't block the async event loop. Combine validate + load into a single pass to avoid decoding the file twice. Add 50 MB upload limit and chunked reads to prevent unbounded memory allocation. Closes #278
This commit is contained in:
@@ -102,6 +102,10 @@ async def delete_profile(
|
|||||||
return {"message": "Profile deleted successfully"}
|
return {"message": "Profile deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_MAX_FILE_SIZE = 50 * 1024 * 1024 # 50 MB
|
||||||
|
SAMPLE_UPLOAD_CHUNK_SIZE = 1024 * 1024 # 1 MB
|
||||||
|
|
||||||
|
|
||||||
@router.post("/profiles/{profile_id}/samples", response_model=models.ProfileSampleResponse)
|
@router.post("/profiles/{profile_id}/samples", response_model=models.ProfileSampleResponse)
|
||||||
async def add_profile_sample(
|
async def add_profile_sample(
|
||||||
profile_id: str,
|
profile_id: str,
|
||||||
@@ -115,8 +119,16 @@ async def add_profile_sample(
|
|||||||
file_suffix = _uploaded_ext if _uploaded_ext in _allowed_audio_exts else ".wav"
|
file_suffix = _uploaded_ext if _uploaded_ext in _allowed_audio_exts else ".wav"
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=file_suffix, delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(suffix=file_suffix, delete=False) as tmp:
|
||||||
content = await file.read()
|
total_size = 0
|
||||||
tmp.write(content)
|
while chunk := await file.read(SAMPLE_UPLOAD_CHUNK_SIZE):
|
||||||
|
total_size += len(chunk)
|
||||||
|
if total_size > SAMPLE_MAX_FILE_SIZE:
|
||||||
|
Path(tmp.name).unlink(missing_ok=True)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=413,
|
||||||
|
detail=f"File too large (max {SAMPLE_MAX_FILE_SIZE // (1024 * 1024)} MB)",
|
||||||
|
)
|
||||||
|
tmp.write(chunk)
|
||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from ..database import (
|
|||||||
Generation as DBGeneration,
|
Generation as DBGeneration,
|
||||||
)
|
)
|
||||||
from ..models import EffectConfig
|
from ..models import EffectConfig
|
||||||
from ..utils.audio import validate_reference_audio, load_audio, save_audio
|
from ..utils.audio import validate_reference_audio, validate_and_load_reference_audio, load_audio, save_audio
|
||||||
from ..utils.images import validate_image, process_avatar
|
from ..utils.images import validate_image, process_avatar
|
||||||
from ..utils.cache import _get_cache_dir, clear_profile_cache
|
from ..utils.cache import _get_cache_dir, clear_profile_cache
|
||||||
from .tts import get_tts_model
|
from .tts import get_tts_model
|
||||||
@@ -117,11 +117,16 @@ async def add_profile_sample(
|
|||||||
Returns:
|
Returns:
|
||||||
Created sample
|
Created sample
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()
|
profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()
|
||||||
if not profile:
|
if not profile:
|
||||||
raise ValueError(f"Profile {profile_id} not found")
|
raise ValueError(f"Profile {profile_id} not found")
|
||||||
|
|
||||||
is_valid, error_msg = validate_reference_audio(audio_path)
|
# Validate and load audio in a single pass, off the event loop
|
||||||
|
is_valid, error_msg, audio, sr = await asyncio.to_thread(
|
||||||
|
validate_and_load_reference_audio, audio_path
|
||||||
|
)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
raise ValueError(f"Invalid reference audio: {error_msg}")
|
raise ValueError(f"Invalid reference audio: {error_msg}")
|
||||||
|
|
||||||
@@ -130,8 +135,7 @@ async def add_profile_sample(
|
|||||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
dest_path = profile_dir / f"{sample_id}.wav"
|
dest_path = profile_dir / f"{sample_id}.wav"
|
||||||
audio, sr = load_audio(audio_path)
|
await asyncio.to_thread(save_audio, audio, str(dest_path), sr)
|
||||||
save_audio(audio, str(dest_path), sr)
|
|
||||||
|
|
||||||
db_sample = DBProfileSample(
|
db_sample = DBProfileSample(
|
||||||
id=sample_id,
|
id=sample_id,
|
||||||
|
|||||||
+24
-6
@@ -217,22 +217,40 @@ def validate_reference_audio(
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (is_valid, error_message)
|
Tuple of (is_valid, error_message)
|
||||||
"""
|
"""
|
||||||
|
result = validate_and_load_reference_audio(
|
||||||
|
audio_path, min_duration, max_duration, min_rms
|
||||||
|
)
|
||||||
|
return (result[0], result[1])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_and_load_reference_audio(
|
||||||
|
audio_path: str,
|
||||||
|
min_duration: float = 2.0,
|
||||||
|
max_duration: float = 30.0,
|
||||||
|
min_rms: float = 0.01,
|
||||||
|
) -> Tuple[bool, Optional[str], Optional[np.ndarray], Optional[int]]:
|
||||||
|
"""
|
||||||
|
Validate and load reference audio in a single pass.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, error_message, audio_array, sample_rate)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
audio, sr = load_audio(audio_path)
|
audio, sr = load_audio(audio_path)
|
||||||
duration = len(audio) / sr
|
duration = len(audio) / sr
|
||||||
|
|
||||||
if duration < min_duration:
|
if duration < min_duration:
|
||||||
return False, f"Audio too short (minimum {min_duration} seconds)"
|
return False, f"Audio too short (minimum {min_duration} seconds)", None, None
|
||||||
if duration > max_duration:
|
if duration > max_duration:
|
||||||
return False, f"Audio too long (maximum {max_duration} seconds)"
|
return False, f"Audio too long (maximum {max_duration} seconds)", None, None
|
||||||
|
|
||||||
rms = np.sqrt(np.mean(audio**2))
|
rms = np.sqrt(np.mean(audio**2))
|
||||||
if rms < min_rms:
|
if rms < min_rms:
|
||||||
return False, "Audio is too quiet or silent"
|
return False, "Audio is too quiet or silent", None, None
|
||||||
|
|
||||||
if np.abs(audio).max() > 0.99:
|
if np.abs(audio).max() > 0.99:
|
||||||
return False, "Audio is clipping (reduce input gain)"
|
return False, "Audio is clipping (reduce input gain)", None, None
|
||||||
|
|
||||||
return True, None
|
return True, None, audio, sr
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"Error validating audio: {str(e)}"
|
return False, f"Error validating audio: {str(e)}", None, None
|
||||||
|
|||||||
Reference in New Issue
Block a user