From 7c632c314d12e98721a81fc85fe8de419b63604c Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Wed, 28 Feb 2024 10:19:06 +0000 Subject: [PATCH 01/12] Keys override --- src/icespeak/voices/__init__.py | 4 ++-- src/icespeak/voices/aws_polly.py | 33 ++++++++++++++++++++------------ src/icespeak/voices/azure.py | 26 +++++++++++++++---------- src/icespeak/voices/tiro.py | 4 ++-- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/icespeak/voices/__init__.py b/src/icespeak/voices/__init__.py index 2565fec..70b0249 100644 --- a/src/icespeak/voices/__init__.py +++ b/src/icespeak/voices/__init__.py @@ -28,7 +28,7 @@ from pydantic import BaseModel, Field -from icespeak.settings import MAX_SPEED, MIN_SPEED, SETTINGS, TextFormats +from icespeak.settings import Keys, MAX_SPEED, MIN_SPEED, SETTINGS, TextFormats from icespeak.transcribe import DefaultTranscriber if TYPE_CHECKING: @@ -125,5 +125,5 @@ def load_api_keys(self) -> None: raise NotImplementedError @abstractmethod - def text_to_speech(self, text: str, options: TTSOptions) -> Path: + def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None) -> Path: raise NotImplementedError diff --git a/src/icespeak/voices/aws_polly.py b/src/icespeak/voices/aws_polly.py index 4de112c..16dccf5 100644 --- a/src/icespeak/voices/aws_polly.py +++ b/src/icespeak/voices/aws_polly.py @@ -30,7 +30,7 @@ import boto3 -from icespeak.settings import API_KEYS, SETTINGS +from icespeak.settings import API_KEYS, Keys, SETTINGS from . import BaseVoice, ModuleAudioFormatsT, ModuleVoicesT, TTSOptions @@ -47,6 +47,14 @@ class AWSPollyVoice(BaseVoice): _lock = Lock() + def _create_client(self, aws_key: AWSPollyKey) -> boto3.client: + return boto3.client( + "polly", + region_name=aws_key.region_name.get_secret_value(), + aws_access_key_id=aws_key.aws_access_key_id.get_secret_value(), + aws_secret_access_key=aws_key.aws_secret_access_key.get_secret_value(), + ) + @property @override def name(self): @@ -63,22 +71,23 @@ def audio_formats(self): return AWSPollyVoice._AUDIO_FORMATS @override - def load_api_keys(self): - assert API_KEYS.aws, "AWS Polly API key missing." - + def load_api_keys(self, key_override: Keys | None = None): + aws_key = key_override.aws if key_override and key_override.aws else API_KEYS.aws + assert aws_key, "AWS Polly API key missing." self._aws_client: Any = None with AWSPollyVoice._lock: if self._aws_client is None: # See boto3.Session.client for arguments - self._aws_client = boto3.client( - "polly", - region_name=API_KEYS.aws.region_name.get_secret_value(), - aws_access_key_id=API_KEYS.aws.aws_access_key_id.get_secret_value(), - aws_secret_access_key=API_KEYS.aws.aws_secret_access_key.get_secret_value(), - ) + self._aws_client = self._create_client(aws_key) @override - def text_to_speech(self, text: str, options: TTSOptions): + def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): + if keys_override and keys_override.aws: + _LOG.info(f"Using overridden AWS keys") + client = self._create_client(keys_override.aws) + else: + _LOG.info(f"Using default AWS keys") + client = self._aws_client # Special preprocessing for SSML markup if options.text_format == "ssml": # Adjust voice speed as appropriate @@ -99,7 +108,7 @@ def text_to_speech(self, text: str, options: TTSOptions): "OutputFormat": options.audio_format, } _LOG.debug("Synthesizing with AWS Polly: %s", aws_args) - response: dict[str, Any] = self._aws_client.synthesize_speech(**aws_args) + response: dict[str, Any] = client.synthesize_speech(**aws_args) except Exception: _LOG.exception("Error synthesizing speech.") raise diff --git a/src/icespeak/voices/azure.py b/src/icespeak/voices/azure.py index 63b173b..6fa2d48 100644 --- a/src/icespeak/voices/azure.py +++ b/src/icespeak/voices/azure.py @@ -29,7 +29,7 @@ import azure.cognitiveservices.speech as speechsdk -from icespeak.settings import API_KEYS, SETTINGS +from icespeak.settings import API_KEYS, Keys, SETTINGS from icespeak.transcribe import DefaultTranscriber, strip_markup from . import BaseVoice, ModuleAudioFormatsT, ModuleVoicesT, TTSOptions @@ -162,7 +162,7 @@ def audio_formats(self): return AzureVoice._AUDIO_FORMATS @override - def load_api_keys(self): + def load_api_keys(self, key_override: Keys | None = None): if OPENSSL_VERSION_INFO[0] != 1: # Azure only works with legacy OpenSSL # See issues regarding compatibility with OpenSSL version >=3: @@ -172,17 +172,23 @@ def load_api_keys(self): "OpenSSL version not compatible with Azure Cognitive Services, TTS might not work." ) - if API_KEYS.azure is None: - raise RuntimeError("Azure API keys missing.") + azure_key = key_override.azure if key_override and key_override.azure else API_KEYS.azure + assert azure_key, "Azure API key missing." - AzureVoice.AZURE_KEY = API_KEYS.azure.key.get_secret_value() - AzureVoice.AZURE_REGION = API_KEYS.azure.region.get_secret_value() + AzureVoice.AZURE_KEY = azure_key.key.get_secret_value() + AzureVoice.AZURE_REGION = azure_key.region.get_secret_value() @override - def text_to_speech(self, text: str, options: TTSOptions): - speech_conf = speechsdk.SpeechConfig( - subscription=AzureVoice.AZURE_KEY, region=AzureVoice.AZURE_REGION - ) + def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): + if keys_override and keys_override.azure: + _LOG.info(f"Using overridden Azure keys") + subscription = keys_override.azure.key + region = keys_override.azure.region + else: + _LOG.info(f"Using default Azure keys") + subscription = API_KEYS.azure.key + region = API_KEYS.azure.region + speech_conf = speechsdk.SpeechConfig(subscription=subscription, region=region) azure_voice_id = AzureVoice._VOICES[options.voice]["id"] speech_conf.speech_synthesis_voice_name = azure_voice_id diff --git a/src/icespeak/voices/tiro.py b/src/icespeak/voices/tiro.py index 5bc2366..baf433a 100644 --- a/src/icespeak/voices/tiro.py +++ b/src/icespeak/voices/tiro.py @@ -29,7 +29,7 @@ import requests -from icespeak.settings import SETTINGS +from icespeak.settings import Keys, SETTINGS from icespeak.transcribe import strip_markup from . import BaseVoice, ModuleAudioFormatsT, ModuleVoicesT, TTSOptions @@ -70,7 +70,7 @@ def load_api_keys(self): pass @override - def text_to_speech(self, text: str, options: TTSOptions): + def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): # TODO: Tiro's API supports a subset of SSML tags # See https://tts.tiro.is/#tag/speech/paths/~1v0~1speech/post From b9b6e63b6710d1e490b050df884b95c3d95536fa Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Wed, 28 Feb 2024 10:53:21 +0000 Subject: [PATCH 02/12] Revert changes to load_api_keys --- .python-version | 1 + src/icespeak/voices/aws_polly.py | 12 ++++++++---- src/icespeak/voices/azure.py | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..4a2f614 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +icespeak-venv diff --git a/src/icespeak/voices/aws_polly.py b/src/icespeak/voices/aws_polly.py index 16dccf5..ebc2d0e 100644 --- a/src/icespeak/voices/aws_polly.py +++ b/src/icespeak/voices/aws_polly.py @@ -71,14 +71,18 @@ def audio_formats(self): return AWSPollyVoice._AUDIO_FORMATS @override - def load_api_keys(self, key_override: Keys | None = None): - aws_key = key_override.aws if key_override and key_override.aws else API_KEYS.aws - assert aws_key, "AWS Polly API key missing." + def load_api_keys(self): + assert API_KEYS.aws, "AWS Polly API key missing." + self._aws_client: Any = None with AWSPollyVoice._lock: if self._aws_client is None: # See boto3.Session.client for arguments - self._aws_client = self._create_client(aws_key) + self._aws_client = boto3.client( + "polly", + region_name=API_KEYS.aws.region_name.get_secret_value(), + aws_access_key_id=API_KEYS.aws.aws_access_key_id.get_secret_value(), + aws_secret_access_key=API_KEYS.aws.aws_secret_access_key.get_secret_value(), @override def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): diff --git a/src/icespeak/voices/azure.py b/src/icespeak/voices/azure.py index 6fa2d48..6791117 100644 --- a/src/icespeak/voices/azure.py +++ b/src/icespeak/voices/azure.py @@ -162,7 +162,7 @@ def audio_formats(self): return AzureVoice._AUDIO_FORMATS @override - def load_api_keys(self, key_override: Keys | None = None): + def load_api_keys(self): if OPENSSL_VERSION_INFO[0] != 1: # Azure only works with legacy OpenSSL # See issues regarding compatibility with OpenSSL version >=3: @@ -172,11 +172,11 @@ def load_api_keys(self, key_override: Keys | None = None): "OpenSSL version not compatible with Azure Cognitive Services, TTS might not work." ) - azure_key = key_override.azure if key_override and key_override.azure else API_KEYS.azure - assert azure_key, "Azure API key missing." + if API_KEYS.azure is None: + raise RuntimeError("Azure API keys missing.") - AzureVoice.AZURE_KEY = azure_key.key.get_secret_value() - AzureVoice.AZURE_REGION = azure_key.region.get_secret_value() + AzureVoice.AZURE_KEY = API_KEYS.azure.key.get_secret_value() + AzureVoice.AZURE_REGION = API_KEYS.azure.region.get_secret_value() @override def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): From fdf9ac62c4ea23aca73078c17ef5a9cf1da3c17d Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Wed, 28 Feb 2024 10:56:18 +0000 Subject: [PATCH 03/12] Syntax fix --- src/icespeak/voices/aws_polly.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/icespeak/voices/aws_polly.py b/src/icespeak/voices/aws_polly.py index ebc2d0e..e78e490 100644 --- a/src/icespeak/voices/aws_polly.py +++ b/src/icespeak/voices/aws_polly.py @@ -83,6 +83,7 @@ def load_api_keys(self): region_name=API_KEYS.aws.region_name.get_secret_value(), aws_access_key_id=API_KEYS.aws.aws_access_key_id.get_secret_value(), aws_secret_access_key=API_KEYS.aws.aws_secret_access_key.get_secret_value(), + ) @override def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): From ca0d3fef7344c306caafd928ee8d2863bbdb4f14 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Thu, 7 Mar 2024 15:23:00 +0000 Subject: [PATCH 04/12] CR amends --- .gitignore | 1 + .python-version | 1 - src/icespeak/tts.py | 2 +- src/icespeak/voices/__init__.py | 6 ++++-- src/icespeak/voices/aws_polly.py | 10 ++++++---- src/icespeak/voices/azure.py | 10 ++++++---- src/icespeak/voices/tiro.py | 6 ++++-- tests/test_parser.py | 6 +++--- 8 files changed, 25 insertions(+), 17 deletions(-) delete mode 100644 .python-version diff --git a/.gitignore b/.gitignore index 64067ad..ee4effd 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,7 @@ pypy/ pypy* cpython*/ venv +.python-version # Installer logs pip-log.txt diff --git a/.python-version b/.python-version deleted file mode 100644 index 4a2f614..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -icespeak-venv diff --git a/src/icespeak/tts.py b/src/icespeak/tts.py index d17cd0b..b0e3188 100644 --- a/src/icespeak/tts.py +++ b/src/icespeak/tts.py @@ -64,7 +64,7 @@ def _setup_voices() -> tuple[VoicesT, ServicesT]: for service in services: _LOG.debug("Loading voices from service: %s", service) if not service.available: - _LOG.info("Voices from service %s not availble.", service) + _LOG.info("Voices from service %s not available.", service) continue for voice, info in service.voices.items(): # Info about each voice diff --git a/src/icespeak/voices/__init__.py b/src/icespeak/voices/__init__.py index 70b0249..bd5c73d 100644 --- a/src/icespeak/voices/__init__.py +++ b/src/icespeak/voices/__init__.py @@ -28,7 +28,7 @@ from pydantic import BaseModel, Field -from icespeak.settings import Keys, MAX_SPEED, MIN_SPEED, SETTINGS, TextFormats +from icespeak.settings import MAX_SPEED, MIN_SPEED, SETTINGS, Keys, TextFormats from icespeak.transcribe import DefaultTranscriber if TYPE_CHECKING: @@ -125,5 +125,7 @@ def load_api_keys(self) -> None: raise NotImplementedError @abstractmethod - def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None) -> Path: + def text_to_speech( + self, text: str, options: TTSOptions, keys_override: Keys | None = None + ) -> Path: raise NotImplementedError diff --git a/src/icespeak/voices/aws_polly.py b/src/icespeak/voices/aws_polly.py index e78e490..6513500 100644 --- a/src/icespeak/voices/aws_polly.py +++ b/src/icespeak/voices/aws_polly.py @@ -30,7 +30,7 @@ import boto3 -from icespeak.settings import API_KEYS, Keys, SETTINGS +from icespeak.settings import API_KEYS, SETTINGS, AWSPollyKey, Keys from . import BaseVoice, ModuleAudioFormatsT, ModuleVoicesT, TTSOptions @@ -86,12 +86,14 @@ def load_api_keys(self): ) @override - def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): + def text_to_speech( + self, text: str, options: TTSOptions, keys_override: Keys | None = None + ): if keys_override and keys_override.aws: - _LOG.info(f"Using overridden AWS keys") + _LOG.debug("Using overridden AWS keys") client = self._create_client(keys_override.aws) else: - _LOG.info(f"Using default AWS keys") + _LOG.debug("Using default AWS keys") client = self._aws_client # Special preprocessing for SSML markup if options.text_format == "ssml": diff --git a/src/icespeak/voices/azure.py b/src/icespeak/voices/azure.py index 6791117..930c172 100644 --- a/src/icespeak/voices/azure.py +++ b/src/icespeak/voices/azure.py @@ -29,7 +29,7 @@ import azure.cognitiveservices.speech as speechsdk -from icespeak.settings import API_KEYS, Keys, SETTINGS +from icespeak.settings import API_KEYS, SETTINGS, Keys from icespeak.transcribe import DefaultTranscriber, strip_markup from . import BaseVoice, ModuleAudioFormatsT, ModuleVoicesT, TTSOptions @@ -179,13 +179,15 @@ def load_api_keys(self): AzureVoice.AZURE_REGION = API_KEYS.azure.region.get_secret_value() @override - def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): + def text_to_speech( + self, text: str, options: TTSOptions, keys_override: Keys | None = None + ): if keys_override and keys_override.azure: - _LOG.info(f"Using overridden Azure keys") + _LOG.debug("Using overridden Azure keys") subscription = keys_override.azure.key region = keys_override.azure.region else: - _LOG.info(f"Using default Azure keys") + _LOG.debug("Using default Azure keys") subscription = API_KEYS.azure.key region = API_KEYS.azure.region speech_conf = speechsdk.SpeechConfig(subscription=subscription, region=region) diff --git a/src/icespeak/voices/tiro.py b/src/icespeak/voices/tiro.py index baf433a..7811c13 100644 --- a/src/icespeak/voices/tiro.py +++ b/src/icespeak/voices/tiro.py @@ -29,7 +29,7 @@ import requests -from icespeak.settings import Keys, SETTINGS +from icespeak.settings import SETTINGS, Keys from icespeak.transcribe import strip_markup from . import BaseVoice, ModuleAudioFormatsT, ModuleVoicesT, TTSOptions @@ -70,7 +70,9 @@ def load_api_keys(self): pass @override - def text_to_speech(self, text: str, options: TTSOptions, keys_override: Keys | None = None): + def text_to_speech( + self, text: str, options: TTSOptions, keys_override: Keys | None = None + ): # TODO: Tiro's API supports a subset of SSML tags # See https://tts.tiro.is/#tag/speech/paths/~1v0~1speech/post diff --git a/tests/test_parser.py b/tests/test_parser.py index 8224521..2ab958b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -136,9 +136,9 @@ def test_greynirssmlparser(): # ------------------------- # Test voice engine specific transcription - assert "Dora" in VOICES - # Gudrun, the default voice, and Dora don't spell things the same - gp2 = GreynirSSMLParser("Dora") + assert "Karl" in VOICES + # Gudrun, the default voice, and Karl don't spell things the same + gp2 = GreynirSSMLParser("Karl") alphabet = "aábcdðeéfghiíjklmnoópqrstuúvwxyýþæöz" n1 = gp.transcribe(gssml(alphabet, type="spell")) n2 = gp2.transcribe(gssml(alphabet, type="spell")) From 189a424cffaa0b447fd5a26f9e37f3fa3a282477 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Thu, 7 Mar 2024 15:25:13 +0000 Subject: [PATCH 05/12] Revert change to test_parser --- tests/test_parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 2ab958b..8224521 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -136,9 +136,9 @@ def test_greynirssmlparser(): # ------------------------- # Test voice engine specific transcription - assert "Karl" in VOICES - # Gudrun, the default voice, and Karl don't spell things the same - gp2 = GreynirSSMLParser("Karl") + assert "Dora" in VOICES + # Gudrun, the default voice, and Dora don't spell things the same + gp2 = GreynirSSMLParser("Dora") alphabet = "aábcdðeéfghiíjklmnoópqrstuúvwxyýþæöz" n1 = gp.transcribe(gssml(alphabet, type="spell")) n2 = gp2.transcribe(gssml(alphabet, type="spell")) From 35bc9f0c738796de359f7efcb3aaf50f517c766f Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Thu, 7 Mar 2024 15:28:36 +0000 Subject: [PATCH 06/12] Added keys_override to tts_to_file --- src/icespeak/tts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/icespeak/tts.py b/src/icespeak/tts.py index b0e3188..01d09c4 100644 --- a/src/icespeak/tts.py +++ b/src/icespeak/tts.py @@ -34,7 +34,7 @@ from cachetools import LFUCache, cached -from .settings import SETTINGS, TRACE +from .settings import SETTINGS, TRACE, Keys from .transcribe import TranscriptionOptions # TODO: Re implement Tiro @@ -153,6 +153,7 @@ def tts_to_file( transcription_options: TranscriptionOptions | None = None, *, transcribe: bool = True, + keys_override: Keys | None = None ) -> TTSOutput: """ # Text-to-speech @@ -195,7 +196,7 @@ def tts_to_file( text = service.Transcriber.token_transcribe(text, options=transcription_options) output = TTSOutput( - file=service.text_to_speech(text, tts_options), + file=service.text_to_speech(text, tts_options, keys_override), text=text, ) _LOG.debug("tts_to_file, out: %s", output) From 49309221af42aa5c4651ea608e44b55efd55fc60 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Thu, 7 Mar 2024 16:24:35 +0000 Subject: [PATCH 07/12] Added test in test_tts --- src/icespeak/settings.py | 8 ++++++++ tests/test_tts.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/icespeak/settings.py b/src/icespeak/settings.py index 9bef80b..994bed1 100644 --- a/src/icespeak/settings.py +++ b/src/icespeak/settings.py @@ -182,6 +182,14 @@ class Keys(BaseModel): # TODO: Re-implement TTS with Tiro tiro: Literal[None] = Field(default=None) + def __hash__(self): + return hash((self.azure, self.aws, self.google, self.tiro)) + + def __eq__(self, other): + if isinstance(other, Keys): + return (self.azure, self.aws, self.google, self.tiro) == (other.azure, other.aws, other.google, other.tiro) + return False + API_KEYS = Keys() diff --git a/tests/test_tts.py b/tests/test_tts.py index 0e56f8c..a2442e4 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -21,10 +21,12 @@ from __future__ import annotations import pytest +from unittest.mock import patch, MagicMock from icespeak import TTSOptions, tts_to_file -from icespeak.settings import API_KEYS, TextFormats, suffix_for_audiofmt +from icespeak.settings import API_KEYS, AWSPollyKey, Keys, TextFormats, suffix_for_audiofmt from icespeak.transcribe import strip_markup +from icespeak.tts import SERVICES, VOICES def test_voices_utils(): @@ -71,6 +73,20 @@ def test_Azure_speech_synthesis(): path.unlink() +@pytest.mark.skipif(API_KEYS.azure is None, reason="Missing Azure API Key.") +@pytest.mark.network() +def test_Azure_speech_synthesis_with_keys_override(): + # Test Azure Cognitive Services + tts_out = tts_to_file( + _TEXT, + TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Gudrun"), + ) + path = tts_out.file + assert path.is_file(), "Expected audio file to exist" + assert path.stat().st_size > _MIN_AUDIO_SIZE, "Expected longer audio data" + path.unlink() + + @pytest.mark.skipif(API_KEYS.google is None, reason="Missing Google API Key.") @pytest.mark.network() def test_Google_speech_synthesis(): @@ -97,3 +113,22 @@ def test_Tiro_speech_synthesis(): assert path.is_file(), "Expected audio file to exist" assert path.stat().st_size > _MIN_AUDIO_SIZE, "Expected longer audio data" path.unlink() + +@patch.dict(SERVICES, {"mock_service": MagicMock()}) +@patch.dict(VOICES, {"Dora": {"service": "mock_service"}}) +def test_keys_override_in_tts_to_file(): + """Test if keys_override is correctly passed into service.text_to_speech.""" + _TEXT = "Test" + SERVICES["mock_service"].audio_formats = ["mp3"] + keys_override = Keys(aws=AWSPollyKey(aws_access_key_id="test", aws_secret_access_key="test", region_name="test")) + tts_to_file( + _TEXT, + TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), + transcribe=False, + keys_override=keys_override + ) + SERVICES["mock_service"].text_to_speech.assert_called_once_with( + _TEXT, + TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), + keys_override + ) \ No newline at end of file From 6fa42ab5dd901118f32d378af4a8e0c3d66d86db Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Thu, 7 Mar 2024 16:28:43 +0000 Subject: [PATCH 08/12] AWS and Azure test with keys override --- tests/test_tts.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_tts.py b/tests/test_tts.py index a2442e4..f6a563f 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -59,6 +59,20 @@ def test_AWSPolly_speech_synthesis(): path.unlink() +@pytest.mark.skipif(API_KEYS.aws is None, reason="Missing AWS Polly API Key.") +@pytest.mark.network() +def test_AWSPolly_speech_synthesis_with_keys_override(): + tts_out = tts_to_file( + _TEXT, + TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), + keys_override=API_KEYS, + ) + path = tts_out.file + assert path.is_file(), "Expected audio file to exist" + assert path.stat().st_size > _MIN_AUDIO_SIZE, "Expected longer audio data" + path.unlink() + + @pytest.mark.skipif(API_KEYS.azure is None, reason="Missing Azure API Key.") @pytest.mark.network() def test_Azure_speech_synthesis(): @@ -80,6 +94,7 @@ def test_Azure_speech_synthesis_with_keys_override(): tts_out = tts_to_file( _TEXT, TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Gudrun"), + keys_override=API_KEYS, ) path = tts_out.file assert path.is_file(), "Expected audio file to exist" From f58301d8f0ff5a2bf93eea71315cb5e3e545bf25 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Fri, 8 Mar 2024 09:52:04 +0000 Subject: [PATCH 09/12] Ruff fix --- src/icespeak/settings.py | 7 ++++++- src/icespeak/tts.py | 2 +- tests/test_tts.py | 27 +++++++++++++++++++++------ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/icespeak/settings.py b/src/icespeak/settings.py index 994bed1..67d67e8 100644 --- a/src/icespeak/settings.py +++ b/src/icespeak/settings.py @@ -187,7 +187,12 @@ def __hash__(self): def __eq__(self, other): if isinstance(other, Keys): - return (self.azure, self.aws, self.google, self.tiro) == (other.azure, other.aws, other.google, other.tiro) + return (self.azure, self.aws, self.google, self.tiro) == ( + other.azure, + other.aws, + other.google, + other.tiro, + ) return False diff --git a/src/icespeak/tts.py b/src/icespeak/tts.py index 01d09c4..ef95f1e 100644 --- a/src/icespeak/tts.py +++ b/src/icespeak/tts.py @@ -153,7 +153,7 @@ def tts_to_file( transcription_options: TranscriptionOptions | None = None, *, transcribe: bool = True, - keys_override: Keys | None = None + keys_override: Keys | None = None, ) -> TTSOutput: """ # Text-to-speech diff --git a/tests/test_tts.py b/tests/test_tts.py index f6a563f..2db7931 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -18,13 +18,21 @@ """ +# ruff: noqa: S106 from __future__ import annotations +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import patch, MagicMock from icespeak import TTSOptions, tts_to_file -from icespeak.settings import API_KEYS, AWSPollyKey, Keys, TextFormats, suffix_for_audiofmt +from icespeak.settings import ( + API_KEYS, + AWSPollyKey, + Keys, + TextFormats, + suffix_for_audiofmt, +) from icespeak.transcribe import strip_markup from icespeak.tts import SERVICES, VOICES @@ -129,21 +137,28 @@ def test_Tiro_speech_synthesis(): assert path.stat().st_size > _MIN_AUDIO_SIZE, "Expected longer audio data" path.unlink() + @patch.dict(SERVICES, {"mock_service": MagicMock()}) @patch.dict(VOICES, {"Dora": {"service": "mock_service"}}) def test_keys_override_in_tts_to_file(): """Test if keys_override is correctly passed into service.text_to_speech.""" _TEXT = "Test" SERVICES["mock_service"].audio_formats = ["mp3"] - keys_override = Keys(aws=AWSPollyKey(aws_access_key_id="test", aws_secret_access_key="test", region_name="test")) + keys_override = Keys( + aws=AWSPollyKey( + aws_access_key_id="test", + aws_secret_access_key="test", + region_name="test", + ) + ) tts_to_file( _TEXT, TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), transcribe=False, - keys_override=keys_override + keys_override=keys_override, ) SERVICES["mock_service"].text_to_speech.assert_called_once_with( _TEXT, TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), - keys_override - ) \ No newline at end of file + keys_override, + ) From a97c22415080a552091aadd7eb3f3b95dc782c67 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Fri, 8 Mar 2024 11:09:15 +0000 Subject: [PATCH 10/12] Azure fix --- src/icespeak/voices/azure.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/icespeak/voices/azure.py b/src/icespeak/voices/azure.py index 930c172..4e1c882 100644 --- a/src/icespeak/voices/azure.py +++ b/src/icespeak/voices/azure.py @@ -184,12 +184,12 @@ def text_to_speech( ): if keys_override and keys_override.azure: _LOG.debug("Using overridden Azure keys") - subscription = keys_override.azure.key - region = keys_override.azure.region + subscription = keys_override.azure.key.get_secret_value() + region = keys_override.azure.region.get_secret_value() else: _LOG.debug("Using default Azure keys") - subscription = API_KEYS.azure.key - region = API_KEYS.azure.region + subscription = AzureVoice.AZURE_KEY + region = AzureVoice.AZURE_REGION speech_conf = speechsdk.SpeechConfig(subscription=subscription, region=region) azure_voice_id = AzureVoice._VOICES[options.voice]["id"] From f8706a6d532c92e3ec2fbee9779691be037c6719 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Fri, 8 Mar 2024 11:25:41 +0000 Subject: [PATCH 11/12] Minor improvement in aws_polly --- src/icespeak/voices/aws_polly.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/icespeak/voices/aws_polly.py b/src/icespeak/voices/aws_polly.py index 6513500..f113929 100644 --- a/src/icespeak/voices/aws_polly.py +++ b/src/icespeak/voices/aws_polly.py @@ -77,13 +77,7 @@ def load_api_keys(self): self._aws_client: Any = None with AWSPollyVoice._lock: if self._aws_client is None: - # See boto3.Session.client for arguments - self._aws_client = boto3.client( - "polly", - region_name=API_KEYS.aws.region_name.get_secret_value(), - aws_access_key_id=API_KEYS.aws.aws_access_key_id.get_secret_value(), - aws_secret_access_key=API_KEYS.aws.aws_secret_access_key.get_secret_value(), - ) + self._aws_client = self._create_client(API_KEYS.aws) @override def text_to_speech( From 0974c83d9079d7e6f502f76f635f8d2d74cd4730 Mon Sep 17 00:00:00 2001 From: Thorvaldur Helgason Date: Tue, 12 Mar 2024 08:49:50 +0000 Subject: [PATCH 12/12] CR amends --- tests/test_tts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_tts.py b/tests/test_tts.py index 2db7931..83286bd 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -151,14 +151,15 @@ def test_keys_override_in_tts_to_file(): region_name="test", ) ) + opts = TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora") tts_to_file( _TEXT, - TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), + opts, transcribe=False, keys_override=keys_override, ) SERVICES["mock_service"].text_to_speech.assert_called_once_with( _TEXT, - TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora"), + opts, keys_override, )