Harden audio transcription error handling
This commit is contained in:
parent
0664b77912
commit
dc0531142c
|
|
@ -10,6 +10,7 @@ from typing import Any, Protocol
|
|||
|
||||
from fastapi import UploadFile
|
||||
from nanoid import generate as generate_nanoid
|
||||
from openai import APIError
|
||||
from sqlalchemy import Integer, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ AUDIO_EXTENSION_CONTENT_TYPES = {
|
|||
".wav": "audio/wav",
|
||||
}
|
||||
UPLOAD_VALIDATION_CHUNK_BYTES = 1024 * 1024
|
||||
AUDIO_PROBE_TIMEOUT_SECONDS = 10
|
||||
GENERIC_CONTENT_TYPES = {
|
||||
"",
|
||||
"application/octet-stream",
|
||||
|
|
@ -156,11 +158,14 @@ class AudioProcessor:
|
|||
content,
|
||||
suffix,
|
||||
)
|
||||
text = await transcribe_audio(
|
||||
content,
|
||||
filename=normalized_filename,
|
||||
content_type=normalized_content_type,
|
||||
)
|
||||
try:
|
||||
text = await transcribe_audio(
|
||||
content,
|
||||
filename=normalized_filename,
|
||||
content_type=normalized_content_type,
|
||||
)
|
||||
except APIError as exc:
|
||||
raise FileProcessingError("Audio transcription failed") from exc
|
||||
return ExtractedFileText(
|
||||
text=text,
|
||||
metadata={
|
||||
|
|
@ -218,12 +223,15 @@ class AudioProcessor:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=AUDIO_PROBE_TIMEOUT_SECONDS,
|
||||
)
|
||||
return max(float(result.stdout.strip()), 0.0)
|
||||
except FileNotFoundError as exc:
|
||||
raise ValidationException(
|
||||
"Audio uploads require ffmpeg and ffprobe to be installed on the server"
|
||||
) from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ValidationException("Audio validation timed out") from exc
|
||||
except (subprocess.CalledProcessError, ValueError) as exc:
|
||||
raise ValidationException("Uploaded audio is invalid or unreadable") from exc
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import asyncio
|
||||
import io
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import UploadFile
|
||||
from openai import AsyncOpenAI
|
||||
from openai import APIError, 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.exceptions import FileProcessingError, ValidationException
|
||||
from src.utils.clients import CLIENTS, transcribe_audio
|
||||
from src.utils.files import AudioProcessor, FileProcessingService
|
||||
|
||||
|
|
@ -60,6 +63,19 @@ def test_probe_audio_duration_cleans_up_temp_file_on_write_failure():
|
|||
mock_unlink.assert_called_once_with(missing_ok=True)
|
||||
|
||||
|
||||
def test_probe_audio_duration_timeout_raises_validation_exception():
|
||||
processor = AudioProcessor()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"src.utils.files.subprocess.run",
|
||||
side_effect=subprocess.TimeoutExpired(cmd="ffprobe", timeout=10),
|
||||
),
|
||||
pytest.raises(ValidationException, match="Audio validation timed out"),
|
||||
):
|
||||
processor.probe_audio_duration_seconds_from_path(Path("/tmp/audio.mp3"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audio_upload_requires_openai_client_before_processing():
|
||||
file = UploadFile(
|
||||
|
|
@ -181,6 +197,26 @@ async def test_audio_processor_extract_text_transcribes_directly():
|
|||
assert "transcription_fallback_used" not in extracted.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audio_processor_extract_text_wraps_provider_errors():
|
||||
processor = AudioProcessor()
|
||||
request = httpx.Request("POST", "https://api.openai.com/v1/audio/transcriptions")
|
||||
|
||||
with (
|
||||
patch.object(processor, "_probe_audio_duration_seconds", return_value=1.0),
|
||||
patch(
|
||||
"src.utils.files.transcribe_audio",
|
||||
side_effect=APIError("provider failed", request=request, body=None),
|
||||
),
|
||||
pytest.raises(FileProcessingError, match="Audio transcription failed"),
|
||||
):
|
||||
await processor.extract_text(
|
||||
b"audio-bytes",
|
||||
filename="seg-0.mp3",
|
||||
content_type="audio/mpeg",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audio_processor_extract_text_probes_in_background_thread():
|
||||
processor = AudioProcessor()
|
||||
|
|
|
|||
Loading…
Reference in New Issue