diff --git a/assemblyai/api.py b/assemblyai/api.py index 7c20645..3719879 100644 --- a/assemblyai/api.py +++ b/assemblyai/api.py @@ -84,14 +84,14 @@ def delete_transcript( def upload_file( client: httpx.Client, - audio_file: BinaryIO, + audio_file: Union[bytes, BinaryIO], ) -> str: """ Uploads the given file. Args: `client`: the HTTP client - `audio_file`: an opened file (in binary mode) + `audio_file`: the raw audio bytes, or an opened file (in binary mode) Returns: The URL of the uploaded audio file. """ diff --git a/assemblyai/transcriber.py b/assemblyai/transcriber.py index 7bf40ca..43d766d 100644 --- a/assemblyai/transcriber.py +++ b/assemblyai/transcriber.py @@ -863,7 +863,7 @@ def __init__( self._client = client self.config = config - def upload_file(self, data: Union[str, BinaryIO]) -> str: + def upload_file(self, data: Union[str, bytes, BinaryIO]) -> str: if isinstance(data, str): with open(data, "rb") as audio_file: return api.upload_file( @@ -904,7 +904,7 @@ def transcribe_url( def transcribe_file( self, *, - data: Union[str, BinaryIO], + data: Union[str, bytes, BinaryIO], config: types.TranscriptionConfig, poll: bool, ) -> Transcript: @@ -919,7 +919,7 @@ def transcribe_file( def transcribe( self, - data: Union[str, BinaryIO], + data: Union[str, bytes, BinaryIO], config: Optional[types.TranscriptionConfig], poll: bool, ) -> Transcript: @@ -942,7 +942,7 @@ def transcribe( def transcribe_group( self, *, - data: List[Union[str, BinaryIO]], + data: List[Union[str, bytes, BinaryIO]], config: Optional[types.TranscriptionConfig], poll: bool, return_failures: Optional[bool] = False, @@ -1075,7 +1075,7 @@ def config(self, config: types.TranscriptionConfig) -> None: """ self._impl.config = config - def upload_file(self, data: Union[str, BinaryIO]) -> str: + def upload_file(self, data: Union[str, bytes, BinaryIO]) -> str: """ Uploads an audio file which can be specified as local path or binary object. @@ -1087,7 +1087,7 @@ def upload_file(self, data: Union[str, BinaryIO]) -> str: return self._impl.upload_file(data=data) def upload_file_async( - self, data: Union[str, BinaryIO] + self, data: Union[str, bytes, BinaryIO] ) -> concurrent.futures.Future[str]: """ Uploads an audio file which can be specified as local path or binary object. @@ -1104,14 +1104,14 @@ def upload_file_async( def submit( self, - data: Union[str, BinaryIO], + data: Union[str, bytes, BinaryIO], config: Optional[types.TranscriptionConfig] = None, ) -> Transcript: """ Submits a transcription job without waiting for its completion. Args: - data: An URL, a local file (as path), or a binary object. + data: An URL, a local file (as path), raw `bytes`, or a binary object. config: Transcription options and features. If `None` is given, the Transcriber's default configuration will be used. """ @@ -1123,7 +1123,7 @@ def submit( def submit_group( self, - data: List[Union[str, BinaryIO]], + data: List[Union[str, bytes, BinaryIO]], config: Optional[types.TranscriptionConfig] = None, return_failures: Optional[bool] = False, ) -> Union[TranscriptGroup, Tuple[TranscriptGroup, List[types.AssemblyAIError]]]: @@ -1131,7 +1131,7 @@ def submit_group( Submits multiple transcription jobs without waiting for their completion. Args: - data: A list of local paths, URLs, or binary objects (can be mixed). + data: A list of local paths, URLs, raw `bytes`, or binary objects (can be mixed). config: Transcription options and features. If `None` is given, the Transcriber's default configuration will be used. return_failures: Whether to include a list of errors for transcriptions that failed due to HTTP errors @@ -1145,14 +1145,14 @@ def submit_group( def transcribe( self, - data: Union[str, BinaryIO], + data: Union[str, bytes, BinaryIO], config: Optional[types.TranscriptionConfig] = None, ) -> Transcript: """ - Transcribes an audio file which can be specified as local path, URL, or binary object. + Transcribes an audio file which can be specified as local path, URL, raw `bytes`, or binary object. Args: - data: An URL, a local file (as path), or a binary object. + data: An URL, a local file (as path), raw `bytes`, or a binary object. config: Transcription options and features. If `None` is given, the Transcriber's default configuration will be used. """ @@ -1165,14 +1165,14 @@ def transcribe( def transcribe_async( self, - data: Union[str, BinaryIO], + data: Union[str, bytes, BinaryIO], config: Optional[types.TranscriptionConfig] = None, ) -> concurrent.futures.Future[Transcript]: """ - Transcribes an audio file which can be specified as local path, URL, or binary object. + Transcribes an audio file which can be specified as local path, URL, raw `bytes`, or binary object. Args: - data: An URL, a local file (as path), or a binary object. + data: An URL, a local file (as path), raw `bytes`, or a binary object. config: Transcription options and features. If `None` is given, the Transcriber's default configuration will be used. """ @@ -1186,7 +1186,7 @@ def transcribe_async( def transcribe_group( self, - data: List[Union[str, BinaryIO]], + data: List[Union[str, bytes, BinaryIO]], config: Optional[types.TranscriptionConfig] = None, return_failures: Optional[bool] = False, ) -> Union[TranscriptGroup, Tuple[TranscriptGroup, List[types.AssemblyAIError]]]: @@ -1194,7 +1194,7 @@ def transcribe_group( Transcribes a list of files (as local paths, URLs, or binary objects). Args: - data: A list of local paths, URLs, or binary objects (can be mixed). + data: A list of local paths, URLs, raw `bytes`, or binary objects (can be mixed). config: Transcription options and features. If `None` is given, the Transcriber's default configuration will be used. return_failures: Whether to include a list of errors for transcriptions that failed due to HTTP errors @@ -1209,7 +1209,7 @@ def transcribe_group( def transcribe_group_async( self, - data: List[Union[str, BinaryIO]], + data: List[Union[str, bytes, BinaryIO]], config: Optional[types.TranscriptionConfig] = None, return_failures: Optional[bool] = False, ) -> concurrent.futures.Future[ @@ -1219,7 +1219,7 @@ def transcribe_group_async( Transcribes a list of files (as local paths, URLs, or binary objects) asynchronously. Args: - data: A list of local paths, URLs, or binary objects (can be mixed). + data: A list of local paths, URLs, raw `bytes`, or binary objects (can be mixed). config: Transcription options and features. If `None` is given, the Transcriber's default configuration will be used. return_failures: Whether to include a list of errors for transcriptions that failed due to HTTP errors diff --git a/tests/unit/test_transcriber.py b/tests/unit/test_transcriber.py index 6500199..fe07564 100644 --- a/tests/unit/test_transcriber.py +++ b/tests/unit/test_transcriber.py @@ -313,6 +313,115 @@ def test_transcribe_file_binary_succeeds(httpx_mock: HTTPXMock): assert len(httpx_mock.get_requests()) == 3 +@pytest.mark.parametrize( + "method_name", + [ + "transcribe", + "transcribe_async", + "transcribe_group", + "transcribe_group_async", + "submit", + "submit_group", + "upload_file", + "upload_file_async", + ], +) +def test_transcribe_data_param_accepts_bytes(method_name): + """ + Regression test for #102: the public ``data`` parameter of the transcribe/ + upload methods must advertise ``bytes`` in its type annotation (passing raw + ``bytes`` already works at runtime; this locks in the type hint so static + type checkers accept it). + """ + import typing + + method = getattr(aai.Transcriber, method_name) + hints = typing.get_type_hints(method) + annotation = hints["data"] + + # The annotation is either ``Union[str, bytes, BinaryIO]`` (single-file + # methods) or ``List[Union[str, bytes, BinaryIO]]`` (group methods). Unwrap + # a ``List[...]`` wrapper before inspecting the Union members. + args = typing.get_args(annotation) + if args and args[0] not in (str, bytes) and typing.get_origin(annotation) in ( + list, + typing.List, + ): + annotation = args[0] + args = typing.get_args(annotation) + + assert bytes in args, ( + f"{method_name} 'data' annotation {annotation!r} must include bytes" + ) + + +def test_transcribe_group_bytes_succeeds(httpx_mock: HTTPXMock): + """ + Tests whether transcribing a group of raw ``bytes`` inputs works (#102). + """ + + mock_transcript_response = factories.generate_dict_factory( + factories.TranscriptCompletedResponseFactory + )() + + files_data = [os.urandom(10), os.urandom(10)] + + response_1 = copy.deepcopy(mock_transcript_response) + response_2 = copy.deepcopy(mock_transcript_response) + + expected_audio_urls = ["https://example.org/1.wav", "https://example.org/2.wav"] + response_1["audio_url"] = expected_audio_urls[0] + response_2["audio_url"] = expected_audio_urls[1] + + # two uploads (one per bytes input) + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_UPLOAD}", + status_code=httpx.codes.OK, + method="POST", + json={"upload_url": expected_audio_urls[0]}, + match_content=files_data[0], + ) + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_UPLOAD}", + status_code=httpx.codes.OK, + method="POST", + json={"upload_url": expected_audio_urls[1]}, + match_content=files_data[1], + ) + + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_TRANSCRIPT}", + status_code=httpx.codes.OK, + method="POST", + json=response_1, + ) + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_TRANSCRIPT}", + status_code=httpx.codes.OK, + method="POST", + json=response_2, + ) + + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_TRANSCRIPT}/{response_1['id']}", + status_code=httpx.codes.OK, + method="GET", + json=response_1, + ) + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_TRANSCRIPT}/{response_2['id']}", + status_code=httpx.codes.OK, + method="GET", + json=response_2, + ) + + transcriber = aai.Transcriber() + transcript_group = transcriber.transcribe_group(files_data) + + assert len(transcript_group.transcripts) == 2 + assert {t.audio_url for t in transcript_group} == set(expected_audio_urls) + + def test_transcribe_group_urls_succeeds(httpx_mock: HTTPXMock): """ Tests whether the transcription of multiple URLs work.