Fix audio upload validation cleanup

This commit is contained in:
adavyas 2026-04-08 22:58:36 -04:00
parent 2776d1d415
commit 14c26d037a
4 changed files with 56 additions and 11 deletions

View File

@ -103,7 +103,7 @@ Our approach involves...
**JSON Files**: Structured data is converted to string format.
**Audio Files**: `.mp3` and `.wav` uploads are transcribed by OpenAI during upload using `whisper-1` by default. Large audio files may be split into multiple audio segments before transcription and then merged back into ordered transcript text before message chunking.
**Audio Files**: `.mp3` and `.wav` uploads are transcribed directly by OpenAI during upload using `whisper-1` by default. The resulting transcript is then split into normal message-sized text chunks before messages are created.
### Chunking Strategy

View File

@ -246,16 +246,14 @@ async def is_validated_audio_upload(file: UploadFile) -> bool:
content_type = processor.normalize_content_type(filename, file.content_type or "")
suffix = processor.get_output_suffix(filename, content_type)
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
temp_path = Path(temp_file.name)
try:
while chunk := await file.read(UPLOAD_VALIDATION_CHUNK_BYTES):
temp_file.write(chunk)
finally:
await file.seek(0)
temp_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
temp_path = Path(temp_file.name)
while chunk := await file.read(UPLOAD_VALIDATION_CHUNK_BYTES):
temp_file.write(chunk)
await asyncio.to_thread(
processor.probe_audio_duration_seconds_from_path,
temp_path,
@ -266,7 +264,9 @@ async def is_validated_audio_upload(file: UploadFile) -> bool:
return False
raise
finally:
temp_path.unlink(missing_ok=True)
await file.seek(0)
if temp_path is not None:
temp_path.unlink(missing_ok=True)
class FileProcessingService:

View File

@ -16,6 +16,7 @@ from starlette.datastructures import Headers
from src import models, schemas
from src.config import settings
from src.exceptions import ValidationException
from src.models import Peer, Workspace
from src.routers.messages import create_messages_with_file
from src.utils.files import ExtractedFileText
@ -946,7 +947,13 @@ async def test_malformed_audio_upload_returns_validation_error(
form_data = {"peer_id": test_peer.name}
url = _get_upload_url(test_workspace.name, session_name)
with patch.dict("src.utils.files.CLIENTS", {"openai": object()}, clear=False):
with (
patch.dict("src.utils.files.CLIENTS", {"openai": object()}, clear=False),
patch(
"src.utils.files.AudioProcessor._probe_audio_duration_seconds",
side_effect=ValidationException("Uploaded audio is invalid or unreadable"),
),
):
response = client.post(url, files=files, data=form_data)
assert response.status_code == 422

View File

@ -1,5 +1,6 @@
import asyncio
import io
from types import TracebackType
from unittest.mock import AsyncMock, patch
import pytest
@ -7,6 +8,7 @@ from fastapi import UploadFile
from openai import AsyncOpenAI
from starlette.datastructures import Headers
import src.utils.files as file_utils
from src.config import settings
from src.exceptions import ValidationException
from src.utils.clients import CLIENTS, transcribe_audio
@ -299,3 +301,39 @@ async def test_audio_processor_normalizes_small_mime_only_audio_filename():
assert extracted.text == "normalized"
assert extracted.metadata["audio_segment_count"] == 1
@pytest.mark.asyncio
async def test_validated_audio_upload_cleans_up_temp_file_on_write_failure():
file = UploadFile(
file=io.BytesIO(b"audio-bytes"),
filename="voice.mp3",
headers=Headers({"content-type": "audio/mpeg"}),
)
class FailingTempFile:
name: str = "/tmp/test-audio-validation.mp3"
def __enter__(self) -> "FailingTempFile":
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
return False
def write(self, _chunk: bytes) -> int:
raise OSError("disk full")
with (
patch("src.utils.files.tempfile.NamedTemporaryFile", return_value=FailingTempFile()),
patch("src.utils.files.Path.unlink") as mock_unlink,
pytest.raises(OSError, match="disk full"),
):
await file_utils.is_validated_audio_upload(file)
mock_unlink.assert_called_once_with(missing_ok=True)
assert file.file.tell() == 0