diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41ea216..730b020 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel setuptools - pip install -e '.[dev]' + python -m pip install -e '.[dev]' - name: Set up API keys run: | # Azure TTS key @@ -35,7 +35,7 @@ jobs: echo '${{ secrets.AWS_POLLY_KEY }}' > keys/AWSPollyServerKey.json - name: Test with pytest run: | - pytest --run-slow -vvvrP --log-level=DEBUG --capture=tee-sys + python -m pytest --run-slow -vvvrP --log-level=DEBUG --capture=tee-sys # - name: Lint with pre-commit hooks # run: | # pre-commit run --all-files diff --git a/.gitignore b/.gitignore index ee4effd..fdae95a 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,5 @@ $RECYCLE.BIN/ audio/* keys/* +keys*/ +env.sh diff --git a/README.md b/README.md index 97178ee..d9e590f 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,15 @@ python3 -m pip install -e '.[dev]' Before using, place API keys for the relevant services in the `/keys` folder (or a folder specified by the `ICESPEAK_KEYS_DIR` environment variable). +Alternately, you can set the following environment variables: + +```sh +export ICESPEAK_AWSPOLLY_API_KEY=your-aws-polly-api-key +export ICESPEAK_AZURE_API_KEY=your-azure-api-key +export ICESPEAK_GOOGLE_API_KEY=your-google-api-key +export ICESPEAK_OPENAI_API_KEY=your-openai-api-key +``` + Output audio files are saved to the directory specified by the `ICESPEAK_AUDIO_DIR` environment variable. By default Icespeak creates the directory `/icespeak` diff --git a/pyproject.toml b/pyproject.toml index 56d650c..f18c38a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [project] name = "icespeak" -# Versioning is automatic (w/setuptools-scm) -dynamic = ["version"] +version = "0.3.7" description = "Icespeak - Icelandic TTS library" authors = [{ name = "MiĆ°eind ehf.", email = "mideind@mideind.is" }] readme = { file = "README.md", content-type = "text/markdown" } @@ -26,24 +25,24 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ # "aiohttp[speedups]>=3.8.4", - "requests>=2.31.0", - "typing-extensions>=4.7.1", + "requests>=2.32.3", + "typing-extensions>=4.12.2", "pydantic==2.3.0", "pydantic-settings>=2.0.3", "cachetools>=5.5.0", # For parsing Icelandic text - "islenska==1.0.3", + "islenska<2.0.0", "reynir<4.0.0", "tokenizer<4.0.0", # Azure TTS - "azure-cognitiveservices-speech>=1.38.0", + "azure-cognitiveservices-speech>=1.41.1", # Google TTS #"google-cloud-texttospeech>=2.14.0", # AWS Polly TTS "botocore>=1.21.40", "boto3>=1.18.40", # OpenAI TTS - "openai>=1.44", + "openai>=1.52.2", ] [project.urls] @@ -63,6 +62,7 @@ dev = [ "ruff>=0.5.7", "pre-commit>=3.3.3", "mypy>=1.4.1", + "boto3-stubs>=1.35.48", ] # *** Configuration of tools *** diff --git a/src/icespeak/settings.py b/src/icespeak/settings.py index 64c4897..88e8bda 100644 --- a/src/icespeak/settings.py +++ b/src/icespeak/settings.py @@ -217,47 +217,61 @@ def __eq__(self, other: object): _kd = SETTINGS.KEYS_DIR if not (_kd.exists() and _kd.is_dir()): _LOG.warning( - "Keys directory missing or incorrect, TTS will not work! Set to: %s", _kd + "Keys directory missing or incorrect: %s", _kd ) -else: - # Load API keys, logging exceptions in level DEBUG so they aren't logged twice, - # as exceptions are logged as warnings when voice modules are initialized - try: + +# Load API keys, logging exceptions in level DEBUG so they aren't logged twice, +# as exceptions are logged as warnings when voice modules are initialized + +# Amazon Polly +try: + if key := os.getenv("ICESPEAK_AWSPOLLY_API_KEY"): + API_KEYS.aws = AWSPollyKey.model_validate_json(key) + else: API_KEYS.aws = AWSPollyKey.model_validate_json( (_kd / SETTINGS.AWSPOLLY_KEY_FILENAME).read_text().strip() ) - except Exception as err: - _LOG.debug( - "Could not load AWS Polly API key, ASR with AWS Polly will not work. Error: %s", - err, - ) - try: +except Exception as err: + _LOG.debug( + "Could not load AWS Polly API key, ASR with AWS Polly will not work. Error: %s", + err, + ) +# Azure +try: + if key := os.getenv("ICESPEAK_AZURE_API_KEY"): + API_KEYS.azure = AzureKey.model_validate_json(key) + else: API_KEYS.azure = AzureKey.model_validate_json( (_kd / SETTINGS.AZURE_KEY_FILENAME).read_text().strip() ) - except Exception as err: - _LOG.debug( - "Could not load Azure API key, ASR with Azure will not work. Error: %s", err - ) - try: +except Exception as err: + _LOG.debug( + "Could not load Azure API key, ASR with Azure will not work. Error: %s", err + ) +# Google +try: + if key := os.getenv("ICESPEAK_GOOGLE_API_KEY"): + API_KEYS.google = json.loads(key) + else: API_KEYS.google = json.loads( (_kd / SETTINGS.GOOGLE_KEY_FILENAME).read_text().strip() ) - except Exception as err: - _LOG.debug( - "Could not load Google API key, ASR with Google will not work. Error: %s", - err, - ) - try: - # First try to load the key from environment variable OPENAI_API_KEY - if key := os.getenv("OPENAI_API_KEY"): - API_KEYS.openai = OpenAIKey(api_key=SecretStr(key)) - else: - API_KEYS.openai = OpenAIKey.model_validate_json( - (_kd / SETTINGS.OPENAI_KEY_FILENAME).read_text().strip() - ) - except Exception as err: - _LOG.debug( - "Could not load OpenAI API key, ASR with OpenAI will not work. Error: %s", - err, +except Exception as err: + _LOG.debug( + "Could not load Google API key, ASR with Google will not work. Error: %s", + err, + ) +# OpenAI +try: + # First try to load the key from environment variable OPENAI_API_KEY + if key := os.getenv("ICESPEAK_OPENAI_API_KEY"): + API_KEYS.openai = OpenAIKey(api_key=SecretStr(key)) + else: + API_KEYS.openai = OpenAIKey.model_validate_json( + (_kd / SETTINGS.OPENAI_KEY_FILENAME).read_text().strip() ) +except Exception as err: + _LOG.debug( + "Could not load OpenAI API key, ASR with OpenAI will not work. Error: %s", + err, + ) diff --git a/tests/test_tts.py b/tests/test_tts.py index ecf71d1..93838ce 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -25,6 +25,7 @@ from unittest.mock import MagicMock, patch import pytest +from pydantic import SecretStr from icespeak import TTSOptions, tts_to_file from icespeak.settings import ( @@ -144,12 +145,12 @@ def test_OpenAI_speech_synthesis(): 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"] + SERVICES["mock_service"].audio_formats = ["mp3"] # type: ignore keys_override = Keys( aws=AWSPollyKey( - aws_access_key_id="test", - aws_secret_access_key="test", - region_name="test", + aws_access_key_id=SecretStr("test"), + aws_secret_access_key=SecretStr("test"), + region_name=SecretStr("test"), ) ) opts = TTSOptions(text_format=TextFormats.TEXT, audio_format="mp3", voice="Dora") @@ -159,7 +160,7 @@ def test_keys_override_in_tts_to_file(): transcribe=False, keys_override=keys_override, ) - SERVICES["mock_service"].text_to_speech.assert_called_once_with( + SERVICES["mock_service"].text_to_speech.assert_called_once_with( # type: ignore _TEXT, opts, keys_override,