Fix audio upload validation cleanup
This commit is contained in:
parent
2776d1d415
commit
14c26d037a
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue