Repository: awslabs/amazon-transcribe-streaming-sdk

Issue:
"IdentifyLanguage" option is not supported
Please add the "IdentifyLanguage" option.
It is supported in the API as well as in batch Transcribe SDK.

Discussion:
@pippuri: I guess this SDK is no longer being maintained? Perhaps AWS should remove the link https://docs.aws.amazon.com/transcribe/latest/dg/getting-started-sdk.html@mirobat: Added in #99, pending release@mirobat: Released in v0.6.4

Solution patch:
diff --git a/.codecov.yml b/.codecov.yml
index 5cd767e..458bb7e 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -1,4 +1,6 @@
 coverage:
+    ignore:
+        - "tests/*"
     status:
         patch:
             default:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 261156c..f4f81c1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
 
     steps:
       - name: Checkout Repository
@@ -29,12 +29,11 @@ jobs:
         run: |
           tox -e py
       - name: Upload coverage
-        if: matrix.python-version == 3.7
-        uses: codecov/codecov-action@v1
+        if: matrix.python-version == 3.11
+        uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
-          file: ./coverage.xml
+          files: ./coverage.xml
           flags: unittests
           name: codecov-umbrella
-          yml: ./codecov.yml
           fail_ci_if_error: true
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 92d71a7..2e9e74c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,13 +8,13 @@ on:
 
 jobs:
   lint:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-24.04
 
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
+    - name: Set up Python 3.11
       uses: actions/setup-python@v2
       with:
-        python-version: 3.9
+        python-version: 3.11
     - name: Run pre-commit
       uses: pre-commit/action@v2.0.0
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
index 51707ee..314fd89 100644
--- a/.github/workflows/typecheck.yml
+++ b/.github/workflows/typecheck.yml
@@ -13,13 +13,13 @@ jobs:
     steps:
       - name: Checkout Repository
         uses: actions/checkout@v1
-      - name: Set Up Python 3.7
+      - name: Set Up Python 3.11
         uses: actions/setup-python@v1
         with:
-          python-version: 3.7
+          python-version: 3.11
       - name: Install tox
         run: |
-          python3.7 -m pip install tox
+          python3.11 -m pip install tox
       - name: Run type checks
         run: |
             tox -e type
diff --git a/amazon_transcribe/client.py b/amazon_transcribe/client.py
index 5229e74..b401f0e 100644
--- a/amazon_transcribe/client.py
+++ b/amazon_transcribe/client.py
@@ -14,7 +14,7 @@
 
 import re
 from binascii import unhexlify
-from typing import Optional
+from typing import Optional, List
 
 from amazon_transcribe import AWSCRTEventLoop
 from amazon_transcribe.auth import AwsCrtCredentialResolver, CredentialResolver
@@ -75,15 +75,21 @@ class TranscribeStreamingClient:
         media_sample_rate_hz: int,
         media_encoding: str,
         vocabulary_name: Optional[str] = None,
+        vocabulary_names: Optional[List[str]] = None,
         session_id: Optional[str] = None,
         vocab_filter_method: Optional[str] = None,
         vocab_filter_name: Optional[str] = None,
+        vocab_filter_names: Optional[List[str]] = None,
         show_speaker_label: Optional[bool] = None,
         enable_channel_identification: Optional[bool] = None,
         number_of_channels: Optional[int] = None,
         enable_partial_results_stabilization: Optional[bool] = None,
         partial_results_stability: Optional[str] = None,
         language_model_name: Optional[str] = None,
+        identify_language: Optional[bool] = None,
+        preferred_language: Optional[str] = None,
+        identify_multiple_languages: Optional[bool] = None,
+        language_options: Optional[List[str]] = None,
     ) -> StartStreamTranscriptionEventStream:
         """Coordinate transcription settings and start stream.
 
@@ -100,7 +106,8 @@ class TranscribeStreamingClient:
         than 5 minutes.
 
         :param language_code:
-            Indicates the source language used in the input audio stream.
+            Indicates the source language used in the input audio stream. Set to
+            None if identify_languages or identify_multiple_languages is set to True
         :param media_sample_rate_hz:
             The sample rate, in Hertz, of the input audio. We suggest that you
             use 8000 Hz for low quality audio and 16000 Hz for high quality audio.
@@ -108,6 +115,9 @@ class TranscribeStreamingClient:
             The encoding used for the input audio.
         :param vocabulary_name:
             The name of the vocabulary to use when processing the transcription job.
+        :param vocabulary_names:
+            When using language identification, the name of the vocabulary to
+            use for each language option.
         :param session_id:
             A identifier for the transcription session. Use this parameter when you
             want to retry a session. If you don't provide a session ID,
@@ -118,7 +128,11 @@ class TranscribeStreamingClient:
         :param vocab_filter_name:
             The name of the vocabulary filter you've created that is unique to
             your AWS account. Provide the name in this field to successfully
-            use it in a stream.
+            use it in a stream. Use only when identify_languages and
+            identify_multiple_languages are set to None
+        :param vocab_filter_names:
+            The name of the vocabulary filters to use for each language option. To be
+            used in conjunction with identify_languages and identify_multiple_languages
         :param show_speaker_label:
             When true, enables speaker identification in your real-time stream.
         :param enable_channel_identification:
@@ -144,21 +158,43 @@ class TranscribeStreamingClient:
             overall transcription accuracy. Defaults to "high" if not set explicitly.
         :param language_model_name:
             The name of the language model you want to use.
+        :param identify_language:
+            if True, the language of the stream will be automatically detected. Set
+            language_code to None and provide at least two language_options when
+            identify_language is True.
+        :param preferred_language:
+            Adding a preferred language can speed up the language identification
+            process, which is helpful for short audio clips.
+        :param identify_multiple_languages:
+            If true, all languages spoken in the stream are identified. A multilingual
+            transcripts is created your transcript using each identified language.
+            You must also provide at least two language_options and set
+            language_code to None
+        :param language_options:
+            A list of possible language to use when identify_language or
+            identify_multiple_languages is set to True. Note that not all languages
+             supported by Transcribe are supported for multiple language identification
         """
         transcribe_streaming_request = StartStreamTranscriptionRequest(
             language_code,
             media_sample_rate_hz,
             media_encoding,
             vocabulary_name,
+            vocabulary_names,
             session_id,
             vocab_filter_method,
             vocab_filter_name,
+            vocab_filter_names,
             show_speaker_label,
             enable_channel_identification,
             number_of_channels,
             enable_partial_results_stabilization,
             partial_results_stability,
             language_model_name,
+            identify_language,
+            preferred_language,
+            identify_multiple_languages,
+            language_options,
         )
         endpoint = await self._endpoint_resolver.resolve(self.region)
 
diff --git a/amazon_transcribe/deserialize.py b/amazon_transcribe/deserialize.py
index 4d31585..0aa2568 100644
--- a/amazon_transcribe/deserialize.py
+++ b/amazon_transcribe/deserialize.py
@@ -187,6 +187,7 @@ class TranscribeStreamingEventParser:
             is_partial=current_node.get("IsPartial"),
             alternatives=alternatives,
             channel_id=current_node.get("ChannelId"),
+            language_code=current_node.get("LanguageCode"),
         )
 
     def _parse_alternative_list(self, current_node: Any) -> List[Alternative]:
diff --git a/amazon_transcribe/model.py b/amazon_transcribe/model.py
index 38e241e..98a4bd8 100644
--- a/amazon_transcribe/model.py
+++ b/amazon_transcribe/model.py
@@ -158,6 +158,7 @@ class Result:
         is_partial: Optional[bool] = None,
         alternatives: Optional[List[Alternative]] = None,
         channel_id: Optional[str] = None,
+        language_code: Optional[str] = None,
     ):
         self.result_id = result_id
         self.start_time = start_time
@@ -165,13 +166,15 @@ class Result:
         self.is_partial = is_partial
         self.alternatives = alternatives
         self.channel_id = channel_id
+        self.language_code = language_code
 
 
 class StartStreamTranscriptionRequest:
     """Transcription Request
 
     :param language_code:
-        Indicates the source language used in the input audio stream.
+        Indicates the source language used in the input audio stream. Set to
+        None if identify_multiple_languages is set to True
 
     :param media_sample_rate_hz:
         The sample rate, in Hertz, of the input audio. We suggest that you
@@ -226,6 +229,15 @@ class StartStreamTranscriptionRequest:
         overall transcription accuracy.
     :param language_model_name:
         The name of the language model you want to use.
+    :param identify_multiple_languages:
+        If true, all languages spoken in the stream are identified. A multilingual
+            transcripts is created your transcript using each identified language.
+            You must also provide at least two language_options and set
+            language_code to None
+    :param language_options:
+        A list of possible language to use when identify_multiple_languages is
+        set to True. Note that not all languages supported by Transcribe are
+        supported for multiple language identification
     """
 
     def __init__(
@@ -234,24 +246,32 @@ class StartStreamTranscriptionRequest:
         media_sample_rate_hz=None,
         media_encoding=None,
         vocabulary_name=None,
+        vocabulary_names=None,
         session_id=None,
         vocab_filter_method=None,
         vocab_filter_name=None,
+        vocab_filter_names=None,
         show_speaker_label=None,
         enable_channel_identification=None,
         number_of_channels=None,
         enable_partial_results_stabilization=None,
         partial_results_stability=None,
         language_model_name=None,
+        identify_language=None,
+        preferred_language=None,
+        identify_multiple_languages=False,
+        language_options=None,
     ):
 
         self.language_code: Optional[str] = language_code
         self.media_sample_rate_hz: Optional[int] = media_sample_rate_hz
         self.media_encoding: Optional[str] = media_encoding
         self.vocabulary_name: Optional[str] = vocabulary_name
+        self.vocabulary_names: Optional[List[str]] = vocabulary_names
         self.session_id: Optional[str] = session_id
         self.vocab_filter_method: Optional[str] = vocab_filter_method
         self.vocab_filter_name: Optional[str] = vocab_filter_name
+        self.vocab_filter_names: Optional[List[str]] = vocab_filter_names
         self.show_speaker_label: Optional[bool] = show_speaker_label
         self.enable_channel_identification: Optional[
             bool
@@ -262,6 +282,10 @@ class StartStreamTranscriptionRequest:
         ] = enable_partial_results_stabilization
         self.partial_results_stability: Optional[str] = partial_results_stability
         self.language_model_name: Optional[str] = language_model_name
+        self.identify_language: Optional[bool] = identify_language
+        self.preferred_language: Optional[str] = preferred_language
+        self.identify_multiple_languages: Optional[bool] = identify_multiple_languages
+        self.language_options: Optional[List[str]] = language_options or []
 
 
 class StartStreamTranscriptionResponse:
@@ -324,6 +348,7 @@ class StartStreamTranscriptionResponse:
         media_sample_rate_hz=None,
         media_encoding=None,
         vocabulary_name=None,
+        vocabulary_names=None,
         session_id=None,
         vocab_filter_name=None,
         vocab_filter_method=None,
@@ -339,6 +364,7 @@ class StartStreamTranscriptionResponse:
         self.media_sample_rate_hz: Optional[int] = media_sample_rate_hz
         self.media_encoding: Optional[str] = media_encoding
         self.vocabulary_name: Optional[str] = vocabulary_name
+        self.vocabulary_names: Optional[List[str]] = vocabulary_names
         self.session_id: Optional[str] = session_id
         self.transcript_result_stream: TranscriptResultStream = transcript_result_stream
         self.vocab_filter_name: Optional[str] = vocab_filter_name
diff --git a/amazon_transcribe/serialize.py b/amazon_transcribe/serialize.py
index 88056ed..18d8944 100644
--- a/amazon_transcribe/serialize.py
+++ b/amazon_transcribe/serialize.py
@@ -12,7 +12,7 @@
 # language governing permissions and limitations under the License.
 
 
-from typing import Any, Dict, Tuple, Optional
+from typing import Any, Dict, Tuple, Optional, List
 
 from amazon_transcribe.request import Request
 from amazon_transcribe.structures import BufferableByteStream
@@ -56,6 +56,10 @@ class TranscribeStreamingSerializer:
     ) -> Dict[str, str]:
         return self._serialize_header(header, value)
 
+    def _serialize_list_header(self, header: str, value: List[str]) -> Dict[str, str]:
+        languages = ",".join(value)
+        return self._serialize_str_header(header, languages)
+
     def serialize_start_stream_transcription_request(
         self, endpoint: str, request_shape: StartStreamTranscriptionRequest
     ) -> Request:
@@ -78,6 +82,14 @@ class TranscribeStreamingSerializer:
         headers.update(
             self._serialize_str_header("vocabulary-name", request_shape.vocabulary_name)
         )
+
+        if request_shape.vocabulary_names:
+            headers.update(
+                self._serialize_list_header(
+                    "vocabulary-names",
+                    request_shape.vocabulary_names,
+                )
+            )
         headers.update(
             self._serialize_str_header("session-id", request_shape.session_id)
         )
@@ -93,6 +105,13 @@ class TranscribeStreamingSerializer:
                 request_shape.vocab_filter_name,
             )
         )
+        if request_shape.vocab_filter_names:
+            headers.update(
+                self._serialize_list_header(
+                    "vocabulary-filter-names",
+                    request_shape.vocab_filter_names,
+                )
+            )
         headers.update(
             self._serialize_bool_header(
                 "show-speaker-label",
@@ -130,6 +149,36 @@ class TranscribeStreamingSerializer:
             )
         )
 
+        headers.update(
+            self._serialize_bool_header(
+                "identify-language",
+                request_shape.identify_language,
+            )
+        )
+
+        headers.update(
+            self._serialize_str_header(
+                "preferred-language",
+                request_shape.preferred_language,
+            )
+        )
+
+        if request_shape.identify_multiple_languages:
+            headers.update(
+                self._serialize_bool_header(
+                    "identify-multiple-languages",
+                    request_shape.identify_multiple_languages,
+                )
+            )
+
+        if request_shape.language_options:
+            headers.update(
+                self._serialize_list_header(
+                    "language-options",
+                    request_shape.language_options,
+                )
+            )
+
         _add_required_headers(endpoint, headers)
 
         body = BufferableByteStream()
diff --git a/docs/conf.py b/docs/conf.py
index 493fe69..b002327 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -72,4 +72,4 @@ pygments_style = 'sphinx'
 #html_static_path = ['_static']
 
 intersphinx_mapping = {
-    'python': ('https://docs.python.org/3.7', None),}
+    'python': ('https://docs.python.org/3.11', None),}
diff --git a/setup.py b/setup.py
index 8ed544a..f51ddfe 100644
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ setup(
     include_package_data=True,
     install_requires=requires,
     extras_require={},
-    python_requires=">= 3.7",
+    python_requires=">= 3.8",
     license="Apache License 2.0",
     classifiers=[
         "Development Status :: 2 - Pre-Alpha",
@@ -46,9 +46,9 @@ setup(
         "License :: OSI Approved :: Apache Software License",
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
     ],
 )


Test patch:
diff --git a/tests/functional/test_serialize.py b/tests/functional/test_serialize.py
index 405a11c..d6efd86 100644
--- a/tests/functional/test_serialize.py
+++ b/tests/functional/test_serialize.py
@@ -21,9 +21,24 @@ def request_shape():
     )
 
 
+@pytest.fixture
+def multi_lid_request():
+    return StartStreamTranscriptionRequest(
+        language_code=None,
+        media_sample_rate_hz=9000,
+        media_encoding="pcm",
+        identify_multiple_languages=True,
+        vocabulary_names=["EnglishVoc", "GermanVoc"],
+        vocab_filter_names=["EVF", "GVF"],
+        language_options=["en-US", "de-DE"],
+    )
+
+
+request_serializer = TranscribeStreamingSerializer()
+
+
 class TestStartStreamTransactionRequest:
     def test_serialization(self, request_shape):
-        request_serializer = TranscribeStreamingSerializer()
         request = request_serializer.serialize_start_stream_transcription_request(
             endpoint="https://transcribe.aws.com",
             request_shape=request_shape,
@@ -37,13 +52,29 @@ class TestStartStreamTransactionRequest:
         assert isinstance(request.body, BufferableByteStream)
 
     def test_serialization_with_missing_endpoint(self, request_shape):
-        request_serializer = TranscribeStreamingSerializer()
         with pytest.raises(ValidationException):
             request_serializer.serialize_start_stream_transcription_request(
                 endpoint=None,
                 request_shape=request_shape,
             )
 
+    def test_serialization_with_multi_lid(self, multi_lid_request):
+        request = request_serializer.serialize_start_stream_transcription_request(
+            endpoint="https://transcribe.aws.com",
+            request_shape=multi_lid_request,
+        ).prepare()
+
+        assert "x-amzn-transcribe-language-code" not in request.headers
+        assert request.headers["x-amzn-transcribe-sample-rate"] == "9000"
+        assert request.headers["x-amzn-transcribe-media-encoding"] == "pcm"
+        assert (
+            request.headers["x-amzn-transcribe-identify-multiple-languages"] == "True"
+        )
+        assert request.headers["x-amzn-transcribe-language-options"] == "en-US,de-DE"
+        cv_header = "x-amzn-transcribe-vocabulary-names"
+        assert request.headers[cv_header] == "EnglishVoc,GermanVoc"
+        assert request.headers["x-amzn-transcribe-vocabulary-filter-names"] == "EVF,GVF"
+
 
 class TestAudioEventSerializer:
     def test_serialization(self):
diff --git a/tests/integration/test_client.py b/tests/integration/test_client.py
index 9d0a48d..670e117 100644
--- a/tests/integration/test_client.py
+++ b/tests/integration/test_client.py
@@ -9,6 +9,25 @@ from amazon_transcribe.exceptions import (
 )
 from tests.integration import TEST_WAV_PATH
 
+request_options = [
+    # plain request with a known language
+    {"language_code": "en-US"},
+    # language identification
+    {
+        "language_code": None,
+        "identify_language": True,
+        "language_options": ["en-US", "de-DE"],
+        "preferred_language": "en-US",
+    },
+    # multiple language identification
+    {
+        "language_code": None,
+        "identify_multiple_languages": True,
+        "language_options": ["en-US", "de-DE"],
+        "vocab_filter_names": ["english", "german"],
+    },
+]
+
 
 class TestClientStreaming:
     @pytest.fixture
@@ -31,11 +50,12 @@ class TestClientStreaming:
         return byte_generator
 
     @pytest.mark.asyncio
-    async def test_client_start_transcribe_stream(self, client, wav_bytes):
+    @pytest.mark.parametrize("request_args", request_options)
+    async def test_client_start_transcribe_stream(
+        self, client, wav_bytes, request_args
+    ):
         stream = await client.start_stream_transcription(
-            language_code="en-US",
-            media_sample_rate_hz=16000,
-            media_encoding="pcm",
+            media_sample_rate_hz=16000, media_encoding="pcm", **request_args
         )
 
         async for chunk in wav_bytes():
@@ -50,6 +70,8 @@ class TestClientStreaming:
             for result in results:
                 for alt in result.alternatives:
                     last_transcript = alt.transcript
+                if "identify_multiple_languages" in request_args:
+                    assert result.language_code in request_args["language_options"]
         # Assert that we got some words back as the service may change its response
         assert len(last_transcript.split(" ")) != 0