diff --git a/src/groq/resources/audio/transcriptions.py b/src/groq/resources/audio/transcriptions.py index 98b70d67..74e41896 100644 --- a/src/groq/resources/audio/transcriptions.py +++ b/src/groq/resources/audio/transcriptions.py @@ -25,6 +25,14 @@ __all__ = ["Transcriptions", "AsyncTranscriptions"] +def _get_response_format_type( + response_format: Literal["json", "text", "verbose_json"] | Omit, +) -> type[Transcription] | type[str]: + # `text` responses are served as text/plain, so casting to a model makes the parser + # fall back to json.loads() and return whatever type the transcript happens to parse as. + return str if response_format == "text" else Transcription + + class Transcriptions(SyncAPIResource): @cached_property def with_raw_response(self) -> TranscriptionsWithRawResponse: @@ -167,7 +175,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Transcription: + ) -> Transcription | str: """ Transcribes audio into the input language. @@ -239,7 +247,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Transcription, + cast_to=_get_response_format_type(response_format), ) @@ -385,7 +393,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Transcription: + ) -> Transcription | str: """ Transcribes audio into the input language. @@ -457,7 +465,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Transcription, + cast_to=_get_response_format_type(response_format), ) diff --git a/src/groq/resources/audio/translations.py b/src/groq/resources/audio/translations.py index 1f0483c1..91ef9183 100644 --- a/src/groq/resources/audio/translations.py +++ b/src/groq/resources/audio/translations.py @@ -25,6 +25,14 @@ __all__ = ["Translations", "AsyncTranslations"] +def _get_response_format_type( + response_format: Literal["json", "text", "verbose_json"] | Omit, +) -> type[Translation] | type[str]: + # `text` responses are served as text/plain, so casting to a model makes the parser + # fall back to json.loads() and return whatever type the transcript happens to parse as. + return str if response_format == "text" else Translation + + class Translations(SyncAPIResource): @cached_property def with_raw_response(self) -> TranslationsWithRawResponse: @@ -60,7 +68,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Translation: + ) -> Translation | str: """Translates audio into English. Args: @@ -119,7 +127,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Translation, + cast_to=_get_response_format_type(response_format), ) @@ -158,7 +166,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Translation: + ) -> Translation | str: """Translates audio into English. Args: @@ -217,7 +225,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Translation, + cast_to=_get_response_format_type(response_format), ) diff --git a/tests/test_audio_response_format.py b/tests/test_audio_response_format.py new file mode 100644 index 00000000..1cfe3a29 --- /dev/null +++ b/tests/test_audio_response_format.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import os + +import httpx +import pytest +from respx import MockRouter + +from groq import Groq, AsyncGroq +from groq.types.audio import Translation, Transcription + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + +# A transcript is arbitrary user speech, so it may happen to be valid JSON. +TEXT_BODIES = ["Hello there.", "42", "true", "null", "3.5", "[1, 2]", '{"text": "hi"}'] + + +def _text_response(body: str) -> httpx.Response: + return httpx.Response(200, headers={"Content-Type": "text/plain; charset=utf-8"}, content=body) + + +class TestTranscriptions: + @pytest.mark.parametrize("body", TEXT_BODIES) + @pytest.mark.respx(base_url=base_url) + def test_response_format_text_returns_str(self, body: str, respx_mock: MockRouter, client: Groq) -> None: + respx_mock.post("/openai/v1/audio/transcriptions").mock(return_value=_text_response(body)) + + transcription = client.audio.transcriptions.create( + model="whisper-large-v3", file=("audio.wav", b"RIFF"), response_format="text" + ) + assert transcription == body + + @pytest.mark.respx(base_url=base_url) + def test_response_format_json_returns_model(self, respx_mock: MockRouter, client: Groq) -> None: + respx_mock.post("/openai/v1/audio/transcriptions").mock( + return_value=httpx.Response(200, json={"text": "Hello there."}) + ) + + transcription = client.audio.transcriptions.create( + model="whisper-large-v3", file=("audio.wav", b"RIFF"), response_format="json" + ) + assert isinstance(transcription, Transcription) + assert transcription.text == "Hello there." + + @pytest.mark.parametrize("body", TEXT_BODIES) + @pytest.mark.respx(base_url=base_url) + async def test_async_response_format_text_returns_str( + self, body: str, respx_mock: MockRouter, async_client: AsyncGroq + ) -> None: + respx_mock.post("/openai/v1/audio/transcriptions").mock(return_value=_text_response(body)) + + transcription = await async_client.audio.transcriptions.create( + model="whisper-large-v3", file=("audio.wav", b"RIFF"), response_format="text" + ) + assert transcription == body + + +class TestTranslations: + @pytest.mark.parametrize("body", TEXT_BODIES) + @pytest.mark.respx(base_url=base_url) + def test_response_format_text_returns_str(self, body: str, respx_mock: MockRouter, client: Groq) -> None: + respx_mock.post("/openai/v1/audio/translations").mock(return_value=_text_response(body)) + + translation = client.audio.translations.create( + model="whisper-large-v3", file=("audio.wav", b"RIFF"), response_format="text" + ) + assert translation == body + + @pytest.mark.respx(base_url=base_url) + def test_response_format_json_returns_model(self, respx_mock: MockRouter, client: Groq) -> None: + respx_mock.post("/openai/v1/audio/translations").mock( + return_value=httpx.Response(200, json={"text": "Hello there."}) + ) + + translation = client.audio.translations.create( + model="whisper-large-v3", file=("audio.wav", b"RIFF"), response_format="json" + ) + assert isinstance(translation, Translation) + assert translation.text == "Hello there." + + @pytest.mark.parametrize("body", TEXT_BODIES) + @pytest.mark.respx(base_url=base_url) + async def test_async_response_format_text_returns_str( + self, body: str, respx_mock: MockRouter, async_client: AsyncGroq + ) -> None: + respx_mock.post("/openai/v1/audio/translations").mock(return_value=_text_response(body)) + + translation = await async_client.audio.translations.create( + model="whisper-large-v3", file=("audio.wav", b"RIFF"), response_format="text" + ) + assert translation == body